类和对象(上)—— 从入门到深入,一文搞懂C++类的核心概念
前言
C++ 是一门面向对象的编程语言,而类是面向对象编程的基石。理解类和对象,是迈入 C++ 大门的关键一步。本文将详细讲解 C++ 中类和对象的基础知识,包括类的定义、访问限定符、类域、实例化、对象大小计算、this 指针等核心内容,并对比 C 语言和 C++ 实现 Stack 的差异,帮助你深刻体会封装的优势。
本文基于 C++11/17 标准,所有代码示例均已测试通过。
一、类的定义
1.1 基本语法
类是一种用户自定义的数据类型,它可以包含成员变量(属性)和成员函数(方法)。定义类使用 class 关键字,后面跟类名,类体放在花括号 {} 中,末尾必须加分号。
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;
}; // 分号不能省略!
坑点提醒:
-
类定义结束时,分号
;绝对不能省略,否则会导致编译错误。 -
成员变量通常加特殊标识以区分局部变量,例如
_var、var_或m_var,但这只是编码习惯,编译器并不强制。
1.2 struct 也可以定义类
C++ 兼容 C 语言的 struct,并对其进行了扩展:struct 中不仅可以有成员变量,还可以定义成员函数。此外,struct 的类名可以直接作为类型名,无需 typedef。
// C 风格的结构体
typedef struct ListNodeC
{
struct ListNodeC* next;
int val;
} LTNode;
// C++ 风格的类(使用 struct)
struct ListNodeCPP
{
void Init(int x)
{
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
};
int main()
{
ListNodeCPP node; // 直接使用类名
node.Init(10);
return 0;
}
区别: class 和 struct 的唯一区别在于默认访问权限不同(见下一节)。
1.3 inline 成员函数
定义在类内部的成员函数默认是 inline 的。如果函数体较长或包含循环、递归等复杂逻辑,编译器可能会忽略 inline 请求。
class Stack
{
public:
// 直接定义在类内部,默认为 inline
void Push(int x)
{
array[top++] = x;
}
// 声明,不在类内定义,不是 inline
void Init(int n = 4);
};
二、访问限定符 —— 封装的基石
C++ 通过访问限定符实现封装:将数据和操作数据的方法绑定在一起,并控制外部对数据的访问权限。
| 限定符 | 类内访问 | 类外访问 | 继承后 |
|---|---|---|---|
| public | ✔️ | ✔️ | 子类可访问 |
| protected | ✔️ | ❌ | 子类可访问(与private的区别在继承中体现) |
| private | ✔️ | ❌ | 子类不可访问 |
语法规则:
-
访问限定符从出现的位置开始,直到下一个限定符出现为止。
-
若没有后续限定符,则作用域到类结束。
-
class默认访问权限为private,struct默认为public。
class Date
{
public:
void Init(int year, int month, int day); // 对外提供接口
private:
int _year; // 隐藏数据,外部不能直接访问
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2025, 4, 19); // ✔️ public 函数可以调用
// d._year = 2025; // ❌ 私有成员不可访问
return 0;
}
为什么要把成员变量设为 private?
这样可以通过公有成员函数来规范数据的修改,例如增加参数检查、日志记录等,避免外部随意破坏对象状态。
三、类域 —— 名称查找的屏障
类定义了一个新的作用域(类域)。类内成员的名字被“包裹”在这个作用域中,在类外访问时必须使用 :: 作用域解析符指明所属的类。
class Stack
{
public:
void Init(int n = 4); // 声明
private:
int* array;
size_t capacity;
size_t top;
};
// 类外定义成员函数,必须加 Stack::
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc failed");
return;
}
capacity = n;
top = 0;
}
坑点: 如果忘记写 Stack::,编译器会将 Init 当作全局函数处理,那么函数体内使用的 array、capacity 等变量就找不到声明,导致编译错误。
类域对查找的影响:
编译器先在当前作用域查找名字,找不到再到类域中查找(成员函数内部还会在类的成员中查找)。
四、实例化 —— 从图纸到房子
4.1 概念
类 只是一个模板(图纸),不占用内存空间;对象 是类的实例(房子),占用物理内存。用类创建对象的过程称为实例化。
class Date
{
int _year; // 声明,不是定义,不分配空间
int _month;
int _day;
};
int main()
{
Date d1; // 实例化,此时为 d1 分配 12 字节(假设 int 4 字节)
Date d2; // 再实例化一个独立对象
return 0;
}
类比:
-
类 = 建筑设计图(规划了房间的数量、尺寸,但还没盖楼)
-
对象 = 根据图纸盖出来的实际房子(可以住人,存储数据)
4.2 对象的大小
核心结论: 对象中只存储成员变量,不存储成员函数。成员函数存放在代码段,所有对象共用同一份函数代码。
4.2.1 为什么成员函数不占对象空间?
试想:如果每个对象都存储一份成员函数的指针,那么实例化 100 个 Date 对象就会重复存储 100 次完全相同的指针,造成巨大浪费。因此编译器将函数代码单独存放,通过 this 指针(见后文)区分调用者。
4.2.2 内存对齐规则
C++ 对象的大小计算遵循结构体内存对齐规则(与 C 语言相同):
-
第一个成员在偏移量为 0 的地址。
-
其他成员要对齐到对齐数的整数倍地址。
对齐数 =min(成员自身大小, 编译器默认对齐数)
VS 默认对齐数为 8,Linux 默认对齐数为 4(可修改)。 -
结构体总大小必须是所有成员最大对齐数的整数倍。
-
如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍,整体大小也需满足最大对齐数的整数倍。
示例:
class A
{
char _ch; // 1 字节,偏移 0
int _i; // 4 字节,对齐数为 min(4,8)=4,需从偏移 4 开始
}; // 总大小 = 8(偏移 0-7,是最大对齐数 4 的倍数)
class B
{
void Print() {} // 空函数,不影响大小
}; // 大小 = 1(占位)
class C {}; // 大小 = 1(占位)
空类占 1 字节的原因:
如果一个对象完全不占内存,那么如何区分两个不同的空对象?给 1 字节只是为了“占位”,表示对象存在。
B b1, b2;
cout << &b1 << endl; // 输出某个地址
cout << &b2 << endl; // 输出另一个地址,相差至少 1 字节
4.2.3 对齐示意图
A 类对象布局(假设 32 位环境,默认对齐数 8):
+----+---+----+----+
| ch | | i | |
+----+---+----+----+
0 1 4 8
偏移0: char (1)
偏移1-3: 填充 (3)
偏移4-7: int (4)
总大小 = 8
五、this 指针 —— 让成员函数知道“为谁工作”
5.1 问题引入
Date d1, d2;
d1.Init(2025, 4, 19);
d2.Init(2025, 7, 5);
Init 函数内部对 _year、_month、_day 赋值,但编译器如何知道应该修改 d1 的数据还是 d2 的数据?
5.2 this 指针的原理
编译器处理方式:
每个非静态成员函数在编译时,会被隐式增加一个形参 —— 当前类类型的指针,命名为 this。
-
真实的函数原型:
void Init(Date* const this, int year, int month, int day); -
调用时:
d1.Init(2025, 4, 19);→Init(&d1, 2025, 4, 19); -
函数内访问成员变量:
_year = year;→this->_year = year;
5.3 this 指针的特性
-
不能显式修改 this 的值:
this = nullptr;编译报错(this本质是Date* const类型)。 -
可以在成员函数内显式使用 this:例如
this->_year = year;完全合法。 -
不能在实参和形参位置显式写 this(由编译器处理),但可以在函数体内使用。
-
this 指针存放在哪里? 一般作为隐含参数通过寄存器或栈传递(取决于调用约定),不在对象内部。常见答案:栈(因为它是函数参数)。
5.4 经典陷阱 —— 空指针调用成员函数
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
void PrintA()
{
cout << _a << endl; // 访问成员变量
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // ✅ 正常运行(未解引用 this,只打印常量)
p->PrintA(); // ❌ 运行时崩溃(访问 this->_a,this 为空)
return 0;
}
解释:
-
p->Print()编译后为call Print地址,Print 函数内没有访问this的任何成员,所以不会解引用空指针,程序正常。 -
p->PrintA()需要读取this->_a,相当于解引用空指针,导致崩溃。
结论: 空指针可以调用不访问成员变量的成员函数,但调用任何访问成员变量的函数都会崩溃。
六、C 与 C++ 实现 Stack 对比 —— 感受封装的力量
6.1 C 语言版本(过程化)
typedef struct Stack
{
int* a;
int top;
int capacity;
} ST;
void STInit(ST* ps);
void STPush(ST* ps, int x);
int STTop(ST* ps);
// ... 等等
缺点:
-
数据和操作分离,使用时必须手动传递结构体指针。
-
缺乏访问控制,任何代码都可以直接修改
ps->top,破坏栈结构。
6.2 C++ 版本(面向对象)
class Stack
{
public:
void Init(int n = 4); // 缺省参数
void Push(int x);
int Top();
void Destroy();
private:
int* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack s;
s.Init(); // 自动传递 this,不用传地址
s.Push(1);
s.Push(2);
cout << s.Top() << endl;
s.Destroy();
}
优势:
-
数据和操作封装在一起,逻辑更清晰。
-
通过
private隐藏成员变量,外部只能通过公有接口访问,保证了栈的完整性。 -
成员函数自动获得 this 指针,调用更自然。
-
支持缺省参数,简化初始化。
七、常见坑点总结
| 问题 | 正确做法 |
|---|---|
| 类定义末尾忘记分号 | 加上 ; |
| 类外定义成员函数不写类域 | 加上 ClassName:: |
| 空指针调用访问成员变量的函数 | 确保指针有效或改用对象调用 |
| 计算对象大小时误将函数也算入 | 记住:只有成员变量(+ 对齐填充) |
| 在成员函数参数中显式写 this | 不能写,编译器自动处理 |
| 混淆 class 和 struct 的默认访问权限 | class → private,struct → public |
八、练习题(自我检测)
1. 下面程序编译运行结果是?
class A {
public:
void Print() { cout << "A::Print()" << endl; }
};
int main() {
A* p = nullptr;
p->Print();
return 0;
}
A. 编译报错 B. 运行崩溃 C. 正常运行
答案:C(不访问成员变量,不会解引用 this)
2. 下面程序编译运行结果是?
class A {
public:
void Print() { cout << _a << endl; }
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print();
return 0;
}
A. 编译报错 B. 运行崩溃 C. 正常运行
答案:B(访问 this->_a,this 为空)
3. this 指针存在内存哪个区域?
A. 栈 B. 堆 C. 静态区 D. 常量区 E. 对象里面
答案:A(作为函数参数,位于栈上)
结语
本文详细介绍了 C++ 类和对象的 6 大核心概念:定义、访问限定符、类域、实例化与大小计算、this 指针以及封装对比。这些都是面向对象编程的基石,后续的构造函数、析构函数、继承和多态都将建立在这些基础之上。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!如有疑问,请在评论区留言,我会尽快回复。
更多推荐
所有评论(0)