作为 C++ 面向对象编程(OOP)的基石,类和对象是从面向过程思维转向面向对象思维的关键。这篇文章是「C++ 类和对象系列」的第一篇,将带你从类的定义、访问控制、实例化到 this 指针,循序渐进理解核心概念,每个知识点都搭配代码示例和通俗解释,帮你夯实基础~
一、类的定义:把数据和行为 "打包"
在面向过程编程中,我们习惯把数据和函数分开定义(比如 C 语言中用结构体存数据,单独写函数操作数据)。而类的核心思想是封装------ 把数据(属性)和操作数据的函数(方法)放在一起,形成一个独立的 "模块"。
1. 类的定义格式
cpp
class 类名 {
// 访问限定符(public/protected/private)
// 成员变量(属性)
// 成员函数(方法)
}; // 注意:分号不能省略!
class:定义类的关键字,相当于 "模板" 的声明;- 类名:自定义标识符(如 Stack、Date),后续可作为类型使用;
- 类体:包含成员变量和成员函数,由访问限定符控制访问权限;
- 分号:类定义结束的标志,不可遗漏(哪怕类体为空)。
2. 成员的命名规范
为了区分成员变量和普通局部变量,通常会给成员变量加特殊标识
- 前缀加
_:如_year、_a(最常用);- 前缀加
m_:如m_top、m_capacity(表示 member);- 后缀加
_:如age_、name_。
3. class vs struct:都能定义类
C++ 兼容 C 语言的struct,同时将其升级为类:
struct中可以定义函数(C 语言中不行);- 核心区别:访问权限默认值不同 :
class默认私有(private);struct默认公有(public)(为了兼容 C 语言结构体的使用习惯);- 建议:优先用
class定义类,struct仅在需要兼容 C 或表示简单数据集合时使用。
4. 成员函数的两种定义方式
(1)类内定义(默认 inline)
成员函数直接写在类体中,编译器会默认将其视为inline函数(内联函数,适合短小函数):
cpp
class Stack {
public:
// 类内定义,默认inline
void Push(int x) {
// 逻辑实现
}
private:
int* _a;
int _top;
int _capacity;
};
(2)类内声明 + 类外定义(非 inline)
成员函数仅在类内声明,定义写在类外,需要用::(作用域解析符)指明所属类域:
cpp
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;
_month = month;
_day = day;
}
⚠️ 注意:类外定义时,::不可省略!否则编译器会把函数当成全局函数,导致 "未声明" 错误。
5. 类名就是类型
和 int、char 一样,类名可以直接作为类型使用(无需 typedef,这是 C++ 的优势):
cpp
struct ListNodeCPP { // struct定义的类
void Init(int x) {
_next = nullptr;
_val = x;
}
ListNodeCPP* _next; // 类名作为指针类型
int _val;
};
// 直接用类名定义对象,无需typedef
ListNodeCPP node;
node.Init(10);
二、访问限定符:控制 "访问权限"
封装的核心不仅是 "打包",还要 "控制访问"------ 哪些成员能在类外直接使用,哪些只能在类内使用。C++ 提供 3 种访问限定符:
1. 访问限定符说明
|-----------|------|------|--------|
| 限定符 | 类内访问 | 类外访问 | 继承中使用 |
| public | 可以 | 可以 | 子类可访问 |
| protected | 可以 | 不可以 | 子类可访问 |
| private | 可以 | 不可以 | 子类不可访问 |
2. 关键规则
- 访问权限的作用域:从该限定符出现的位置开始,到下一个限定符出现或类结束为止;
cpp
#include <iostream>
using namespace std;
class Stack {
// 无访问限定符,class默认private
void Push(int x) {} // private:类外不可访问
public:
void Pop() {} // public:类外可访问
int Top() { return 0; } // public:类外可访问
private:
int* _a; // private:类外不可访问
int _top;
int _capacity;
};
int main() {
Stack st;
st.Pop(); // 正确:public成员
st.Top(); // 正确:public成员
// st.Push(10); 错误:private成员,类外无法访问
// st._a = nullptr; 错误:private成员,类外无法访问
return 0;
}
3. 为什么需要访问控制?
- 保护数据安全:避免类外代码随意修改成员变量(比如栈的
_top指针,随意修改会导致栈结构混乱);- 隐藏实现细节:类外只需关注 "如何使用"(public 方法),无需关心 "如何实现"(private 成员),降低耦合度。
三、类域:解决 "命名冲突"
类定义了一个独立的作用域(类域),类的所有成员都在这个作用域内。这意味着:
- 不同类可以定义同名成员(函数 / 变量),不会冲突;
- 类外访问成员时,必须通过对象 / 指针 / 引用,或用
::指明类域(仅静态成员);- 类外定义成员函数时,必须用
::指明类域(否则视为全局函数)。
cpp
// 类A的类域
class A {
public:
void Print() { cout << "A::Print()" << endl; }
};
// 类B的类域,可定义同名函数Print
class B {
public:
void Print() { cout << "B::Print()" << endl; }
};
int main() {
A a;
B b;
a.Print(); // 调用A::Print()
b.Print(); // 调用B::Print(),无冲突
return 0;
}
四、实例化
类本身只是一个 "模板"(相当于设计图纸),不占用物理内存;只有通过实例化(创建对象),才会在内存中分配空间,存储成员变量。
cpp
class Date { // 类:模板,不占内存
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 成员变量:仅声明,不占内存
int _month;
int _day;
};
int main() {
Date d1; // 实例化:创建对象d1,分配内存(存储_year/_month/_day)
Date d2; // 一个类可以实例化多个对象,每个对象独立存储成员变量
d1.Init(2025, 11, 28);
d2.Init(2025, 11, 29);
return 0;
}
2. 对象的大小:只存成员变量,不存成员函数
仍遵循内存对齐规则。
对象中仅存储成员变量,成员函数存储在代码段(所有对象共享)。
为什么?
- 成员函数是一组固定的指令,所有对象执行时逻辑完全相同,没必要每个对象都存一份(浪费空间);
- 成员函数的地址在编译链接时就已确定(静态绑定,除了动态多态),调用时直接通过地址执行,无需对象存储。
cpp
#include <iostream>
using namespace std;
class A {
public:
void Print() {} // 成员函数,不占对象空间
private:
char _ch; // 1字节
int _i; // 4字节
};
class B {}; // 空类:无成员变量
class C {};
int main() {
A a;
B b;
C c;
cout << sizeof(a) << endl; // 8字节(内存对齐后)
cout << sizeof(b) << endl; // 1字节
cout << sizeof(c) << endl; // 1字节
return 0;
}
3. 内存对齐:对象大小的 "隐形规则"
为什么class A的对象大小是 8 字节,而不是 1+4=5 字节?------ 因为内存对齐。
(1)内存对齐的目的
- 硬件限制:CPU 读取内存时,不是逐字节读取,而是按 "块大小"(如 4 字节、8 字节)读取;
- 性能优化:如果数据跨越两个 "块",CPU 需要读取两次;对齐后只需读取一次,用空间换时间。
(2)空类的大小为什么是 1 字节?
空类(无成员变量)的对象大小为 1 字节,目的是占位------ 表示这个对象在内存中存在,避免多个空类对象地址重叠。
五、this 指针
当我们调用对象的成员函数时,函数如何知道操作的是哪个对象的成员变量?答案是**this指针。**
1. this 指针的本质
this指针是编译器自动添加到成员函数形参列表的隐含指针,指向当前调用该函数的对象;- 它的类型是
类名* const(如Date* const this),表示指针本身不可修改(不能指向其他对象),但可以修改指向对象的成员;- 我们不能在形参或实参中显式写
this,但可以在成员函数内部显式使用。
2. 编译器的 "幕后操作"
我们写的代码:
cpp
class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year, _month, _day;
};
int main() {
Date d1;
d1.Init(2025, 11, 28);
return 0;
}
编译器实际处理后的代码(简化):
cpp
// 编译器给成员函数添加this形参
void Date::Init(Date* const this, int year, int month, int day) {
this->_year = year; // 隐含通过this访问成员变量
this->_month = month;
this->_day = day;
}
int main() {
Date d1;
// 编译器自动传入对象地址作为this实参
Date::Init(&d1, 2025, 11, 28);
return 0;
}
3. this 指针的使用场景
(1)区分成员变量和局部变量
当成员变量和局部变量同名时,用this指针明确指向成员变量:
cpp
class Person {
public:
void SetAge(int age) {
this->age = age; // this->age 指成员变量,age指局部变量
}
private:
int age;
};
(2)返回当前对象本身
在链式调用中常用(如 string 类的 append、operator+=):
cpp
class Date {
public:
Date& AddDay(int day) {
_day += day;
return *this; // 返回当前对象的引用
}
private:
int _year, _month, _day;
};
// 链式调用
Date d;
d.Init(2025, 11, 28);
d.AddDay(1).AddDay(2); // 先加1天,再加2天
4. this 指针的存储位置
- 通常存储在栈上(作为函数形参);
- 部分编译器(如 VS)会优化,将其存储在寄存器(如 ecx)中,提高访问效率;
- 注意:调用成员函数时,对象必须存在。