1. 类的定义 —— class vs struct
C++ 中,class和struct都可以用来定义类。
class ClassName
{
// 成员变量(属性)
// 成员函数(方法)
}; // 分号不能省略
区别:
代码示例与用法归类:
class中成员默认是private(私有)struct中成员默认是public(公有)C++ 升级了 struct,里面可以放函数
struct ListNodeCPP {
void Init(int x) {
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
}; // 不再需要 typedef,ListNodeCPP 本身就是类型名✅建议:一般情况下,用
class定义类,用struct定义纯数据聚合(如链表节点)。成员变量的命名习惯
为了区分成员变量和普通变量,常见约定:在成员变量的
后面加
_:year_前面加
_:_year前面加
m:m_yearclass Date {
private:
int _year; // 前面加 _
int _month;
int _day;
};2. 访问限定符 —— 封装的第一步
class Date {限定符 类外访问 说明 public✅ 可以 对外接口 protected❌ 不可以 继承相关,暂时和 private 一样 private❌ 不可以 内部实现细节
public: // 从这里开始,之后的成员对外可见
void Init(int year, int month, int day);
private: // 从这里开始,之后的成员对外隐藏
int _year;
int _month;
int _day;
};- 作用域:从该限定符出现的位置开始,到下一个限定符或类结束为止。
class默认private,struct默认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在该作用域中不存在,编译器会直接报错(“未定义标识符”),而不会再去外部的全局域碰运气。通俗的讲,A::B,可以粗略理解为“去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 位环境(字节) 说明 char1 1 字符类型,固定 1 字节 bool1 1 布尔类型,固定 1 字节 short2 2 短整型,固定 2 字节 int4 4 整型,固定 4 字节 long4 8(Linux)/4(Windows) ⚠️ 视平台而定,Windows 64 位下 long 仍然是 4 字节 long long8 8 长长整型,固定 8 字节 float4 4 单精度浮点,固定 4 字节 double8 8 双精度浮点,固定 8 字节 指针 T*4 8 指针大小 = 地址总线宽度:32 位系统 4 字节,64 位系统 8 字节 class A {
private:
char _ch; // 1 字节,偏移 0
int _i; // 4 字节,对齐数 4,从偏移 4 开始 → 中间填充 3 字节
}; // 总大小 = 8 字节(最大对齐数为 4,8 是 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简化类名本身就是类型