【C++类和对象】从结构体到类、构造函数与析构函数详解 1. 类的定义 —— class vs structC 中class和struct都可以用来定义类。class ClassName{// 成员变量属性// 成员函数方法}; // 分号不能省略区别代码示例与用法归类class中成员默认是private私有struct中成员默认是public公有C 升级了 struct里面可以放函数struct ListNodeCPP {void Init(int x) {next nullptr;val x;}ListNodeCPP* next;int val;}; // 不再需要 typedefListNodeCPP 本身就是类型名✅建议一般情况下用class定义类用struct定义纯数据聚合如链表节点。成员变量的命名习惯为了区分成员变量和普通变量常见约定在成员变量的后面加_year_前面加__year前面加mm_yearclass Date {private:int _year; // 前面加 _int _month;int _day;};2. 访问限定符 —— 封装的第一步限定符类外访问说明public✅ 可以对外接口protected❌ 不可以继承相关暂时和 private 一样private❌ 不可以内部实现细节class Date {public: // 从这里开始之后的成员对外可见void Init(int year, int month, int day);private: // 从这里开始之后的成员对外隐藏int _year;int _month;int _day;};作用域从该限定符出现的位置开始到下一个限定符或类结束为止。class默认privatestruct默认public。3. 类域 —— 影响编译查找规则类定义了一个新的作用域。在类体外定义成员函数时需要用::指明属于哪个类class Date {public:void Init(int year, int month, int day);private:int _year;int _month;int _day;};// 类外定义 → 必须指定类域 Date::void Date::Init(int year, int month, int day) {_year year; // 在当前函数作用域找不到 _year会去 Date 类域中找_month month;_day day;}如果不指定Date::编译器会把Init当成全局函数找不到_year等成员就会报错【补充::的用法与原理详解】在刚才的代码中我们使用了void Date::Init(...)这里的::称为作用域解析运算符Scope Resolution Operator。它是 C 中非常重要的符号直接决定了编译器去哪里查找名字。1.::的用法左边是什么右边是什么::的左右两侧分工明确 域具体成员左侧左操作数指定要查找的“域”Scope。它可以是类名如Date、命名空间名如std或者留空空着表示全局作用域。右侧右操作数该域内的具体成员。包括成员函数名如Init、成员变量名、类型名如size_t或静态成员。2.::的原理编译器如何处理核心原理::的核心作用就是强制指定查找路径它会命令编译器“跳过”默认的层层查找规则直接去指定的域里找。默认查找规则不加::时编译器遵循就近原则即名字查找规则先在当前局部域函数体内找找不到再去全局域找。如果局部有同名变量编译器绝对不会去全局找。使用::后的查找规则编译器解析到A::B时会先将A当作一个限定符识别A到底是一个类还是一个命名空间如果是::B则直接定位到全局域。确定A的作用域范围后编译器只在该作用域内部的符号表中搜索B。如果B在该作用域中不存在编译器会直接报错“未定义标识符”而不会再去外部的全局域碰运气。通俗的讲AB可以粗略理解为“去A中找B”为什么要这样设计意义解决名字冲突隐藏问题当局部变量与全局变量重名时用::变量名可以精准指名道姓让编译器不再受“就近原则”干扰。实现声明与定义分离类外定义在类体外定义成员函数时如void Date::Init()::告诉编译器“Init是Date家族的成员”。如果不加::编译器会认为你在定义一个全局函数Init那么函数内部访问_year时编译器会去全局找自然就找不到私有成员从而报错。访问命名空间成员通过std::cout的方式可以将庞大的标准库隔离在std域中避免与用户自定义的cout发生冲突。4. 实例化与对象大小4.1 实例化 —— 从图纸到房子类就像一张设计图规定了有哪些房间成员变量但本身不占用物理空间。对象是根据设计图建造出来的房子真正占用内存空间。class Date {private:int _year; // 声明未开空间int _month;int _day;};int main() {Date d1; // 实例化此时才分配空间Date d2; // 可以实例化多个对象return 0;}4.2 对象大小 —— 内存对齐规则对象中只存储成员变量不存储成员函数函数在代码段。C 规定对象大小遵循内存对齐规则VS 默认对齐数为 8第一个成员在偏移量为 0 的地址。其他成员对齐到对齐数的整数倍地址。对齐数 min(成员大小, 默认对齐数) 取成员大小与默认对齐数更小的那个结构体总大小为最大对齐数的整数倍。不同数据类型在常见平台下占用的字节数:数据类型32 位环境字节64 位环境字节说明char11字符类型固定 1 字节bool11布尔类型固定 1 字节short22短整型固定 2 字节int44整型固定 4 字节long48Linux/4Windows⚠️ 视平台而定Windows 64 位下 long 仍然是 4 字节long long88长长整型固定 8 字节float44单精度浮点固定 4 字节double88双精度浮点固定 8 字节指针T*48指针大小 地址总线宽度32 位系统 4 字节64 位系统 8 字节class A {private:char _ch; // 1 字节偏移 0int _i; // 4 字节对齐数 4从偏移 4 开始 → 中间填充 3 字节}; // 总大小 8 字节最大对齐数为 48 是 4 的倍数总大小为整数倍class B {}; // 空类大小为1占位标识对象存在5. 构造函数 —— 自动初始化5.1 为什么需要构造函数之前我们写Date或Stack时需要手动调用Init()函数来初始化对象。构造函数让对象在实例化时自动调用不需要用户手动初始化。5.2 构造函数的特点特点说明函数名与类名相同Date()、Stack()无返回值不需要写void自动调用对象实例化时自动执行可以重载支持多个构造函数默认生成用户不写编译器自动生成一个什么是“默认构造函数”有人可能会误以为“默认构造”就是编译器自动生成的那个。实际上默认构造函数的定义是不传实参就可以调用的构造函数。它包含以下三种类型示例说明① 无参构造函数Date()用户显式定义无参数② 全缺省构造函数Date(int y 1, int m 1, int d 1)所有参数都有默认值③ 编译器自动生成的无参构造用户完全不写任何构造函数时对内置类型不做初始化这三种默认构造函数无法共存先把一个注释掉全缺省构造和普通带参构造在类外定义的函数体上没有任何区别。唯一的区别在于类内声明时全缺省写了默认值普通带参没写。既然函数体一模一样而全缺省构造既能不传参、又能传部分、又能传全部它已经完全覆盖了普通带参构造的功能所以你根本不需要再写一个普通带参构造只写全缺省一个就足够了!6. 析构函数 —— 自动清理资源6.1 为什么需要析构函数构造函数负责初始化析构函数负责资源清理释放动态分配的内存、关闭文件等。对象生命周期结束时析构函数自动调用不需要手动调用。class Stack {public:Stack(int n 4) {_a (int*)malloc(sizeof(int) * n);_capacity n;_top 0;}~Stack() { // 析构函数~类名free(_a);_a nullptr;_top _capacity 0;}private:int* _a;size_t _capacity;size_t _top;};6.2 析构函数的特点特点说明函数名~类名如~Date()、~Stack()无参数不能重载一个类只有一个析构函数无返回值不需要写void自动调用对象生命周期结束时自动执行默认生成用户不写编译器自动生成6.3 编译器自动生成的析构函数对内置类型不做处理对自定义类型成员调用该成员的析构函数6.4 什么时候需要自己写析构类类型是否需要显示写析构原因Date❌ 不需要没有资源申请编译器默认即可Stack✅必须写内部有malloc申请的资源必须free6.5 多个对象的析构顺序C 规定后定义的对象先析构类似于栈后进先出。7. C 与 C 的 Stack 对比示意维度C 语言实现C 类实现数据和函数分离封装在一起初始化//initialization需要手动调用Init()构造函数自动调用资源释放需要手动调用Destroy()析构函数自动调用访问控制无法控制任意访问private隐藏内部细节类型名需要typedef简化类名本身就是类型