前言

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;
};  // 分号不能省略!

坑点提醒:

  • 类定义结束时,分号 ; 绝对不能省略,否则会导致编译错误。

  • 成员变量通常加特殊标识以区分局部变量,例如 _varvar_ 或 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 默认访问权限为 privatestruct 默认为 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 当作全局函数处理,那么函数体内使用的 arraycapacity 等变量就找不到声明,导致编译错误。

类域对查找的影响:
编译器先在当前作用域查找名字,找不到再到类域中查找(成员函数内部还会在类的成员中查找)。


四、实例化 —— 从图纸到房子

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 语言相同):

  1. 第一个成员在偏移量为 0 的地址。

  2. 其他成员要对齐到对齐数的整数倍地址。
    对齐数 = min(成员自身大小, 编译器默认对齐数)
    VS 默认对齐数为 8,Linux 默认对齐数为 4(可修改)。

  3. 结构体总大小必须是所有成员最大对齐数的整数倍。

  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 指针以及封装对比。这些都是面向对象编程的基石,后续的构造函数、析构函数、继承和多态都将建立在这些基础之上。

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!如有疑问,请在评论区留言,我会尽快回复。

更多推荐