C++类和对象(上)
目录
- 1.从面向过程到面向对象的过渡
- 2.类的引入
- 3.类的定义
- 4.类的使用
- 4.1访问限定符
- 4.2访问成员变量
- 4.3成员函数的访问
- 5.类的实例化
- 6.类对象模型
- 6.1类的大小
- 6.2类对象存储方式猜测
- 7.this指针
- 7.1this指针的引出
- 7.2this指针的特性
1.从面向过程到面向对象的过渡
我们知道C语言是一门面向过程的语言。假如我们要执行洗衣机洗衣服的这个操作,面向过程是这样的:打开洗衣机、放入衣服、加入洗衣粉、加水、关上洗衣机、洗衣机工作、工作完成打开洗衣机、拿出衣服……在我们C语言的编程中这种感受极为明显。那么面向对象便是这样的:我们需要执行用洗衣机洗衣服,那么洗衣机和衣服的关系就是放入和被放入,我们就把衣服放入洗衣机,即可完成工作。面向对象,是关注于对象,而不是关注如何执行与操作的。
假设我们有一个外卖系统,外卖系统需要:客户下单、商家接单、骑手取单、骑手送单。在面向对象中,我们将这四个对象独立开来,重点关注每一个对象的属性和行为。而不是像面向过程那样,关注事情的起因、过程、结果。例如客户是一个对象,我们将客户会执行的操作封装起来,形成一个类。商家是一个对象,封装起来。骑手封装起来。这几个对象之间相互建立联系即可。
当然这只是大体的、笼统的概念。具体的深入需要我们在学习C++的过程中慢慢体会。
2.类的引入
其实在C语言中,我们学习过"类"。没错,就是结构体。我们在使用C语言的时候,可以去描述结构体的属性。
//使用结构体描述一个学生
struct Student
{
char name[20];//姓名
int age;//年龄
char id[12];//学号
};
我们使用这个结构体创建一个变量,即得到了"对象"。但是这个"对象"并没有活力,因为一个完整的对象应该是具有行为的,例如上课、吃饭、睡觉。那么在C++中,就对C的结构体进行了升级,使得在结构体内,可以定义行为。
//使用结构体描述一个学生
struct Student
{
void Learn()
{
//如何学习……
}
void Eat()
{
//如何吃饭……
}
void Sleep()
{
//如何睡觉……
}
char name[20];//姓名
int age;//年龄
char id[12];//学号
};
那么为了凸显C++与C的区别,就引入了一个关键字,类关键字class。
3.类的定义
因为类是有结构体升级而来,所以大体上是相似的。所以在这里,只进行简短的描述。例如上面的例子,类中的所有内容成为类的成员,其中,类中的变量成为类的属性或成员变量;类中定义的函数成为类的方法或成员函数。
我们需要注意的是,如果类中的成员函数的声明与定义写到一起,那么很有可能会被编译器当成内联函数处理。
class Student
{
//成员函数的声明和定义放在一起很可能会被编译器当成内联函数处理
void ShowStu()
{
cout << name << " " << age << " " << tele << endl;
}
char name[20];
int age;
char tele[12];
};
4.类的使用
4.1访问限定符
我们来对比两段代码:
class Student
{
char name[20];
int age;
char tele[12];
};
int main()
{
Student stu1;
stu1.age = 0;
return 0;
}
这段代码会发生报错:
struct Student2
{
char name[20];
int age;
char tele[12];
};
int main()
{
Student2 stu2;//在C++中,定义结构体变量可以不加struct关键字
stu2.age = 0;
return 0;
}
这段代码便能正常运行。
可以看到报错的信息,说我们无法访问私有(private)成员,而结构体没有这个报错。那么我们就知道了,结构体中的成员默认是公有(public)成员,可直接被外部使用。而类中的成员默认是私有(private)成员,不能直接被外部使用。
我们便可以使用另一个访问限定符修饰我们类中的成员:public。
class Student1
{
public:
char name[20];
int age;
char tele[12];
};
这样我们便掌握了两个访问限定符:private 和 public。
说明:
1.private修饰的成员不能被外部使用
2.public修饰的成员能被外部使用
3.protected与private类似(现阶段),不能被外部使用
4.访问限定符的作用域该限定符出现之后,下一个限定符出现之前
5.class定义的类默认为private
由此可以看出类与结构体的区别。
4.2访问成员变量
与结构体类似,先创建一个类变量(对象),由这个变量去访问类中的成员变量。前提是在public的修饰之下。
class Student1
{
public:
char name[20];
int age;
char tele[12];
};
int main()
{
Student1 stu1;
stu1.age = 0;//由对象访问类中的成员变量
return 0;
}
4.3成员函数的访问
与访问成员变量一样,访问成员函数需要通过对象访问:
class Student1
{
public:
void ShowSt()
{
cout << name << " " << age << " " << tele << endl;
}
char name[20];
int age;
char tele[12];
};
int main()
{
Student1 stu1;
cin >> stu1.name;
cin >> stu1.age;
cin >> stu1.tele;
stu1.ShowSt();
return 0;
}
上面是声明与定义不分离的情况。
接下来便是成员函数的声明与定义分离的情况:
class Student1
{
public:
void ShowSt();
char name[20];
int age;
char tele[12];
};
void ShowSt()
{
cout << name << " " << age << " " << tele << endl;
}
int main()
{
Student1 stu1;
cin >> stu1.name;
cin >> stu1.age;
cin >> stu1.tele;
stu1.ShowSt();
return 0;
}
这样的写法是错误的。与命名空间一样,我们需要指明函数是在哪个作用域的,否则无法使用类中的成员变量。所以成员函数的声明与定义分离的时候,在外部定义函数时,必须指明作用域。
void Student1::ShowSt()
{
cout << name << " " << age << " " << tele << endl;
}
由此也可以证实:类的定义带来了一个全新的作用域。
5.类的实例化
类也是一个自定义类型,它本身是不占用空间的。所以类更像是一个模型,而对象便是模型的实现。对象具有类规定的属性和行为。
6.类对象模型
6.1类的大小
既然类是一种自定义类型,那么作为类型,就一定有计算类型大小的方法。并且,类是在C的结构体的基础上升级而来,所以也会保留内存对齐的做法。
//计算A类的大小
class A
{
int x;
char y;
double z;
};
不过,如果类仅仅只有成员变量,那么内存对齐是很好的理解的。但若是类中定义了成员函数呢?
class A
{
public:
void Print()
{
cout << x << " " << y << " " << z << endl;
}
private:
int x;
char y;
double z;
};
我们使用sizeof操作符计算A类的大小,发现类中貌似不包含函数的大小,这是怎么一回事呢?
6.2类对象存储方式猜测
如果我们猜测,类中包含所有成员。
那么这种存储方式与我们观察到的结果是矛盾的。因为我们的结果似乎只计算了成员变量的大小。
我们举这么一个例子:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 3, b = 4;
int A = 6, B = 7;
//调用的是同一个函数吗?
Add(a, b);
Add(A, B);
return 0;
}
答案很明显,调用的是同一个函数。所以在类中定义的函数,不同的对象去调用同一个类的函数,调用的也是同一个函数。
class A
{
public:
void Print()
{
cout << x << " " << y << " " << z << endl;
}
private:
int x;
char y;
double z;
};
int main()
{
A a1;
a1.Print();
A a2;
a2.Print();
//不同的对象调用同一个类的函数,调用的是一个函数
return 0;
}
类中定义的函数,就被放在了公共代码段当中。
既然如此,我们假设类中包含所有的成员变量和成员函数的地址,效果如何?
事实上这是多此一举的。类中的成员函数已经被放在了公共代码段,那么类的对象是可以自动找到它的。如果我们再在类中存储函数的指针,那么就似乎显得不合理。
所以,在C++中,类中只包含成员变量,成员函数被放在了公共代码段。
那么解释刚才我么计算A类的大小,就显得合理了。
那么这样便会衍生出来几个问题:
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
那便是类中如果只有成员函数,或者类中什么都没有,大小如何计算?事实上C++的设计者帮我们考虑了这个问题。类的大小实际就是成员变量的大小之和(需要注意内存对齐),如果是一个空类(不包含有成员变量),那么编译器将会把这个类的大小设定为一个字节。
7.this指针
7.1this指针的引出
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2022, 9, 22);
d2.Init(2022, 9, 23);
d1.Print();
d2.Print();
return 0;
}
既然调用同一种类的成员函数是调用同一个函数。那么函数怎么知道要对哪个对象处理呢?调用d1对象的函数不能处理d2对象的数据吗?这里就有一个隐藏的指针,this指针。这个指针是对象的地址,在函数内访问成员变量的操作都是通过这个指针去访问的。 它对于我们是隐藏的、透明的,编译器自动帮我们完成的。
为什么要有this指针?我们就回到C的代码上研究:
struct A
{
int a;
double b;
};
void Init(A* pa)//在函数内访问结构体变量内容时,需要指针
{
pa->a = 5;
pa->b = 3.0;
}
int main()
{
A a1;
Init(&a1);
return 0;
}
这个Init函数的形参pa指针,就可以理解成this指针。this指着一方面是减少代码量,更加方便;另一方面是可以自动确定访问的对象。
那么在C++中,我们可以认为this指针是这样的:
class Date
{
public:
void Init(Date* this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(&d1,2022, 9, 22);
d2.Init(&d2,2022, 9, 23);
d1.Print(&d1);
d2.Print(&d2);
return 0;
}
7.2this指针的特性
既然C++帮助我们隐藏了this指针,那么我们便不能去显示调用。所以设计者设计this指针的类型是这样的:类名* const this。const在*的右边,就说明this指针的本身不能改变。
1.this指针本质是成员函数的形参,所以在类中不存储有this指针。
2.this指针在成员函数中,一定是第一个参数,只不过被隐藏起来并且this指针不需要我们手动传递,由编译器自动完成。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0; }
这段代码是可以正常运行的。p就是类的一个指针,但是因为成员函数是存储在公共代码段的,所以调用时不发生解引用。进入函数时候,因为this是一个空指针,所以不能发生解引用的操作。而在这里,this并没有去解引用。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0; }
这段代码发生运行错误是当然的。因为this是一个空指针,而函数内部发生了空指针的解引用。