专栏放在【C++知识总结】,会持续更新,期待支持🌹
类是什么?
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它(实例化后才会开辟物理空间,后面会讲到);比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。也可以理解为这是一个自定义类型,与C语言中的结构体(struct)颇有类似。
struct与class
在C语言中,我们知道struct是用来定义结构体的关键字,其实在C++中的struct也依然保留了在C语言中的功能,不仅如此,C++中的struct还可以用来定义函数,并且C语言中,我们用struct来定义一个结构体,那么定义一个与它同类型的变量要写成struct xxx的形式,而C++中用struct来定义一个结构体,那么同类型的变量则可以省略struct。
类的定义
在C++中,对于这种自定义类型的定义,其实更喜欢用class来代替struct,class为定义类的关键字,class后面为类的名字,{}内为类的主体,主体中的内容就是类的内容,分为成员变量(类的属性)以及成员函数(类的方法)。
类的两种定义的方式:
一种是将成员函数定义在类里面(编译器可能会当成内联函数处理)
另一种是将成员函数声明与定义分离(工作中推荐第二种)
这里需要注意的是,在类中定义成员函数以及成员变量时,不需要考虑定义的先后顺序,也就是说,即使成员变量放在成员函数的下面,成员函数中依然可以使用成员变量。
类访问限定符
class中有三种访问限定符:public、protected、private。
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
另外注意:class的默认访问权限为private,struct为public(因为struct要兼容C),访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类的实例化以及调用
所谓类的实例化其实就是用类创建一个具体的实体变量,我们上面的class Person中的Person仅仅只是声明,不占用物理空间,只有实例化后,才会占有空间。当然,一个类可以进行多个实例化。
Person p;//p占有实际的物理空间,这里就是一个实例化的过程
//一个类可以进行多个实例化
Person p1;
Person p2;
我们可以通过.操作符来实现类中方法的调用(public影响的区域可以在类外直接调用)
class Person { public://公有,在类外可以直接访问 void Print() { //... } private://私有,在类外不能直接访问 char* _name; int _age; short _height; }; int main() { Person p;//实例化 p.Print();//通过.来调用函数Print return 0; }
类对象大小的计算
既然实例化后才会占有具体的空间,那么实例化后,它的空间大小应该是多少呢?成员函数与成员变量又是何存储的呢?假如是个空类,实例化后还会不会占用空间呢?
类对象的存储方式
实际上,成员函数虽然是定义在类中,但是它并不存储在类里,假如它是存储在类中,而每个实例化后的对象都各自拥有各自的成员函数,则会造成严重的资源浪费,因为成员函数就好比小区中的健身器材、公共厕所等公共共有的设施,只需要存在一份就足够了。成员函数实际上是存在于公共代码段中。
所以实例化后的对象的大小,只需要计算成员变量大小即可,当然,类对象大小的计算与struct一样遵循结构体内存对齐规则。这里需要注意的是,假如一个类中只有成员函数而没有成员变量,或者什么都没有,则实例化后对象的大小为1byte。
class Person { public://成员函数不参与大小的计算 void Print() { //... } private://遵循结构体内存对齐规则 char* _name; int _age; short _height; }; //空类 class test1 {}; //只有成员函数 class test2 { public: void test() {} }; int main() { Person p;//实例化 test1 p1; test2 p2; p.Print();//通过.来调用函数Print cout << sizeof(p) << endl;//12 cout << sizeof(p1) << endl;//1 cout << sizeof(p2) << endl;//1 return 0; }
结构体内存对齐规则(与C语言中的struct相同)
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
this指针
在上面我们知道,成员函数是属于公有的,是类实例化后的各个对象所共同使用,那么不禁有一个问题,就是既然是公有的,那么它是怎么区分是谁调用的呢?就比如说下面的一个例子:
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; //函数Init是如何区分是d1还是d2调用的呢? d1.Init(2023, 2, 10); d2.Init(2022, 2, 10); }
对于这个问题,C++是通过this指针来解决的,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
也就是说,虽然我们这里只是传了三个参数,但实际上还有一个隐藏的参数this指针,也就是说,编译器实际上是这么来处理的:
this指针的特性
this指针的类型为 Date* const,也就是说,this是不可被修改的,this实际上就是实例化对象的地址
this只能在成员函数内部使用
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以 对象中不存储this指针。
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
关于this指针为空
只要不涉及到解引用操作,程序都会正常运行,空指针也是可以调用类中的函数的,仅仅只凭借.或者->是不能判断是否涉及空指针的解引用,而是应该根据调用的函数的内容,来判断是否对空指针进行解引用。
class Test { public: void Print1() { cout <<"Print()" << endl; } void Print2() { cout << _a << endl; } void Init(int a=20)//缺省参数 { _a = a; } private: int _a; }; int main() { Test* d1 = nullptr; //不能仅凭借*以及->来断定就是空指针解引用 //d1->Print1();//程序正常运行 //d1->Print2();//程序崩溃,因为函数内容中涉及到了空指针解引用:this->_a(nullptr->_a) //d1->Init();//崩溃,原因同上,nullptr->_a=a; //(*d1).Print1();//正常运行,函数中仅仅只是打印一个字符串 //(*d1).Print2();//崩溃,原因同上2 //(*d1).Init();//崩溃,同上 }
封装的概念与作用
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
就好比我们的使用计算机的时候,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此 计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++中便可以通过类来实现数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。这样用户使用起来也会更加方便。