1. 类的定义
1.1 类定义格式
在 C++ 中,类是一种用户自定义的数据类型,它将数据成员和成员函数封装在一起。类定义的基本格式如下:
class ClassName { // 成员变量 // 成员函数 }; // 注意末尾的分号定义在类里面的函数默认都为内联(inline)函数。
例如,定义一个简单的日期类:
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; };1.2 访问限定符
C++ 提供了三种访问限定符,用于控制类成员的访问权限:
- public:公有成员,在类外可以直接访问。
- protected:保护成员,在类外不能直接访问,但可以在派生类中访问。
- private:私有成员,在类外不能直接访问,只能在类内部访问。
访问限定符的作用域从该限定符出现的位置开始,到下一个访问限定符出现之前结束。如果类(class)定义开头没有写访问限定符,则默认是 private。
1.3 类域
类定义了一个新的作用域,称为类域。类中的所有成员都位于类域中。在类外定义成员函数时,需要使用作用域操作符::来指明成员所属的类。
// 在类外定义成员函数 void Date::Print() { cout << _year << "-" << _month << "-" << _day << endl; }2. struct 来表示类
在 C++ 中,struct也可以当作类定义的关键字来使用。用struct定义的类与用class定义的类在功能上几乎完全一致,都可以包含数据成员和成员函数,也都可以使用访问限定符。
两者的唯一区别在于默认的访问限定符不同:
struct的默认访问限定符是public,即成员在类外可以直接访问。class的默认访问限定符是private,即成员在类外不能直接访问。
例如,下面用struct定义一个日期类:
struct Date { // 默认是 public,可以直接访问 void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; };因此,在 C++ 中struct和class都可以用来定义类,选择哪一个主要取决于默认访问权限的偏好:希望成员默认公开时用struct,希望成员默认私有时用class。
注意:struct 在 C++ 中可以直接作为类型来定义变量,当然C中的做法同样兼容。
typedef struct ListNode { int val; ListNode* next;//struct ListNode* next; };2. 实例化
2.1 实例化概念
用类类型创建对象的过程,称为类的实例化。类本身只是一个模板,不占用实际的存储空间;只有实例化出对象后,系统才会为对象分配内存。
在类中的成员变量只是声明,并未开空间。
Date d1; // 实例化一个 Date 对象 Date d2; // 实例化另一个 Date 对象同一个类可以实例化出多个对象,每个对象都拥有自己独立的数据成员存储空间,但成员函数是共享的。
2.2 对象大小
对象的大小等于其所有非静态数据成员的大小之和,同时需要考虑内存对齐规则。成员函数不占用对象空间,因为它们存放在代码段中,由所有对象共享。
补充:内存对齐的优势?
- 提高 CPU 访问内存的效率,避免一次变量跨两次内存块读取;
- 适配部分硬件,非对齐访问会直接报错崩溃;
- 有利于 CPU 缓存,提升缓存命中率;
- 便于 DMA、硬件寄存器等底层硬件操作。
内存对齐不是没有代价:会产生内存空洞(padding 填充字节),浪费少量内存空间,属于空间换时间。
class A { public: void Func() {} // 成员函数不占对象空间 private: int _a; // 4 字节 char _b; // 1 字节,考虑对齐后可能占 4 字节 }; // sizeof(A) 在 32 位平台下通常为 8 字节空类的大小为 1 字节,这是为了区分不同的空对象地址。
2.3 内存对齐规则
内存对齐是指编译器在分配结构体或类对象的内存时,按照一定的规则在成员之间插入填充字节(padding),使每个成员的起始地址满足对齐要求。C++ 中主要遵循以下几条规则:
- 第一个成员变量,放在结构体 / 类偏移为
0的地址处。- 其余成员变量:存放起始地址必须是对齐数的整数倍。
- ✅对齐数计算公式:
对齐数 = min(编译器默认对齐数,该成员自身大小)- VS 默认对齐数:8;
- 结构体 / 类整体总大小:必须是最大对齐数的整数倍。
- 最大对齐数:所有成员各自对齐数里面的最大值。
- 嵌套结构体场景
- 被嵌套的结构体:对齐到它自己的最大对齐数的整数倍地址。
- 外层结构体总大小:必须包含所有成员(嵌套结构体也算),整体是全部最大对齐数的整数倍。
下面通过一个例子来理解对齐过程:
struct S { char _c; // 1 字节,偏移 0 int _i; // 4 字节,需对齐到 4 的倍数,偏移 4 char _d; // 1 字节,偏移 8 }; // 总大小 = 12 字节(最大对齐数 4,12 是 4 的倍数) struct A { char a; // 1字节 int b; // 4字节 }; struct B { char x; // 1字节 struct A sa; //按照A最大对齐数4,大小为8 double y; // 8字节 }; //总大小是:243. this 指针
C++ 编译器给每个非静态成员函数增加了一个隐藏的指针参数,称为 this 指针。this 指针指向调用该成员函数的对象,通过它可以在成员函数内部访问对象的成员。
void Date::Print() { // this 指向调用 Print 的对象 cout << this->_year << "-" << this->_month << "-" << this->_day << endl; }this 指针的特点:
- this 指针的类型是
Class Name* const,即指针本身不能被修改。 - this 指针是成员函数的隐含参数,不需要也不能在调用时显式传递。
- this 指针存储在栈上,是成员函数的一个局部变量。
- C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
- this 指针存在于内存中的栈。
4. C++ 和 C 语言实现 Stack 对比
下面分别用 C 语言和 C++ 实现一个栈,对比两种语言在数据封装和代码组织上的差异。
C 语言实现
typedef struct Stack { int* _a; int _top; int _capacity; } Stack; void StackInit(Stack* ps) { ps->_a = (int*)malloc(sizeof(int) * 4); ps->_top = 0; ps->_capacity = 4; } void StackPush(Stack* ps, int x) { if (ps->_top == ps->_capacity) { ps->_capacity *= 2; ps->_a = (int*)realloc(ps->_a, sizeof(int) * ps->_capacity); } ps->_a[ps->_top++] = x; }C 语言中,数据和操作数据的函数是分离的,函数必须显式接收结构体指针作为参数,并且结构体成员默认是公开的,缺乏访问控制。
C++ 实现
class Stack { public: void Init() { _a = new int[4]; //new 是C++开辟空间的一个关键字,后续介绍 _top = 0; _capacity = 4; } void Push(int x) { if (_top == _capacity) { _capacity *= 2; int* tmp = new int[_capacity]; for (int i = 0; i < _top; ++i) tmp[i] = _a[i]; delete[] _a; _a = tmp; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; };C++ 将数据和操作封装在类中,成员函数通过 this 指针隐式访问对象数据,代码更简洁;同时通过 private 访问限定符保护数据,增强了安全性。