欢迎光临
我们一直在努力

类和对象(上)—— 从入门到深入,一文搞懂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)
    未经允许不得转载:171主机测评 » 类和对象(上)—— 从入门到深入,一文搞懂C++类的核心概念
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址