1. 类的定义
类是C++中的一种自定义类型,是某个具体事物或概念的抽象化代码表示,通过类的成员(变量+函数/方法),可以表征出事物或概念的特征。
1.1 类定义的格式
class Stack { public: // 成员函数 void Init(int n = 4) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } void Push(int x) { // ...扩容 array[top++] = x; } int Top() { assert(top > 0); return array[top - 1]; } void Destroy() { free(array); array = nullptr; top = capacity = 0; } private: // 成员变量 int* array; size_t capacity; size_t top; }; // 分号不能省略
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省 略。类和C语言中的结构体类似,类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数/类函数。一般来说,这些类函数是专门设计来服务于这个类类型的变量(对象)。
为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_ 或者 m 开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
// _成员变量名 int* _array; size_t _capacity; size_t _top; // 成员变量名_ int* array_; size_t capacity_; size_t top_; // m成员变量名 int* mArray; size_t mCapacity; size_t mTop;
C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,一般情况下我们还是推荐用class定义类。
定义在类中的成员函数默认为inline。
1.2 访问限定符
上面Stack类中的“public”和“private”就是访问限定符,除此之外,还有“protected”。
他们的作用是限定类成员被访问的权限。
• C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是一样的,以后继承章节才能体现出他们的区别。
• 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 “}”即类结束。
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
习惯上,被public修饰的成员统一放到前面,被private修饰的成员统一放到后面。
被public修饰的一般是类函数,被private修饰的一般是成员变量。
1.3 类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作 用域操作符指明成员属于哪个类域。
前面提到,在类中定义的函数默认为inline函数,如果不希望某个函数被当作inline函数,则可将类函数的声明与定义分开,只在类中声明,而在类外进行定义。
class Stack { public: // 成员函数 void Init(int n = 4); void Push(int x); int Top(); void Destroy(); private: // 成员变量 int* array; size_t capacity; size_t top; }; // 分号不能省略 void Stack::Init(int n = 4) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } void Stack::Push(int x) { // ...扩容 array[top++] = x; } int Stack::Top() { assert(top > 0); return array[top - 1]; } void Stack::Destroy() { free(array); array = nullptr; top = capacity = 0; }
类域影响的是编译的查找规则,上面程序中的函数如果不指定类域Stack,那么编译器就把这些函数当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道这些函数是成员函数,当前域找不到的array等成员,就会到类域中去查找。
2. 对象的定义(实例化)
简单来说,类只是一种类似于结构体的类型,用类类型定义出的变量,就是该类的对象。
2.1 实例化的概念
用类类型在物理内存中创建对象的过程,称为类实例化出对象。
类是对象进行的一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多 少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房 子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。
以“Date类”为例:
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; };
这里给出了定义“Date类”类型变量的蓝图,利用该类进行实例化,就可以得到“Date类”类型的变量。
int main() { // Date类实例化出对象day1和day2 Date day1; Date day2; return 0; }
2.2 this指针
相信读者已经注意到,类函数看上去似乎有些奇怪:这些函数直接使用了类中的成员变量,而没有对应形参的传入,那么这些函数怎么知道成员变量是谁的呢?。
我们从类函数的调用方式入手:
int main() { Date day1; day1.print(2024, 7, 29); Date day2; day2.print(2004, 12, 18); return 0; }
因为类函数也是类的成员,所以使用成员访问操作符“.”访问对象的成员即可调用类函数。
可以看到,类函数的调用是与某个对象绑定在一起的,所以类函数能知道成员变量是哪个对象的。
但是,在底层上是如何做到的呢?
编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做“this指针”。比如Date类的Init的真实原型为:
void Init(Date* const this, int year, int month, int day);
所以,在调用类函数时,当前对象的地址就会通过“this指针”传入函数中,函数就能对该对象的成员变量进行操作。
类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值的语句相当于是:
this->_year = year;
2.3 对象的大小
我们已经了解过C语言中结构体的大小C语言结构体的大小,结构体内存对齐_c语言 struct大小-CSDN博客
相比于结构体,类增加了成员函数,那么这些成员函数会使实例化出的对象的空间如何变化呢?
对于同一个类,不同的对象之间成员变量一般不同,但是成员函数完全相同。所以,没有单独为每一个对象存储成员函数的必要。其次,函数被编译后是一段指令,对象中没办法存储,这些指令 存储在一个单独的区域(代码段),那么对象中非要存储的话,也只能是成员函数的指针。同一个函数只有一个地址,也没有必要分别存储。最后,其实函数指针本身就是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。
总结来说,每个类的对象的大小只与成员变量有关,成员函数不存储在对象中。在此基础上,符合内存对齐的原则。