一、面向过程 vs 面向对象

写 C++ 类和对象之前,首要任务是完成编程思维转换。

  • 面向过程(C 语言思想):核心关注点「怎么做」,也就是解决问题的步骤。数据与操作数据的函数相互分离。
    我们定义结构体struct Student存储数据,再单独写void PrintStudent(Student* s)处理数据,数据与操作数据的函数是分离的,没有绑定。
  • 面向对象(C++ 思想):核心关注点「谁来做」。把数据(属性)、操作数据的方法(行为)封装整合为一个整体,也就是对象。

🌰通俗例子:把大象装进冰箱

  • 面向过程:打开冰箱门 → 抬大象进去 → 关上冰箱门
  • 面向对象:冰箱.开门()大象.走进()冰箱.关门()

二、类的引入与定义

1. 什么是类?

类(Class)是描述对象的蓝图 / 模板,封装对象的属性(成员变量)与行为(成员函数)。先有类,再通过类创建具体对象。

2. 两种定义方式

C++ 中structclass都可以定义类,唯一区别:

  • struct:默认访问权限 public
  • class:默认访问权限 private
class Student {
public:
    // 成员函数:行为
    void SetName(const char* name) {
        // 逻辑省略
    }
    void Print() {
        cout << _name << endl;
    }

private:
    // 成员变量:属性,一般私有化
    char _name[20];
    int _age;
}; // 类定义末尾分号不能省略!

三、访问限定符与封装

1. 三大访问限定符

用来管控类外部能否访问类内成员,是实现封装的基础:

  1. public 公有:类内、类外均可访问,一般对外提供接口函数;
  2. protected 保护:类内、派生类(子类)可以访问,类外无法访问(继承章节详解);
  3. private 私有:仅类内部可以访问,类外禁止访问,通常存放成员变量。

2. 封装的本质

封装不等于单纯把数据隐藏。
核心思想:隐藏内部复杂实现细节,只对外提供稳定、统一的公开接口

类比开车:你只需要操作方向盘、油门(public 接口),不需要了解发动机火花塞如何点火(private 内部细节)。即便厂商升级发动机内部结构,只要油门、方向盘接口不变,驾驶方式完全不用改动。

四、类的作用域与实例化

1. 类作用域

类属于独立作用域。

  • 成员函数写在类体内,可直接访问所有成员;
  • 如果类内声明、类外定义成员函数,必须通过::作用域解析符指明归属类。
class Student {
public:
    void Print(); // 类内声明
private:
    char _name[20];
};

// 类外实现,指定属于Student类
void Student::Print() {
    cout << _name << endl;
}

2. 类的实例化

类仅仅是图纸,本身不分配物理内存(静态成员除外)
通过类创建对象的过程叫做实例化,实例化之后,才会在栈 / 堆上分配内存。

int main() {
    Student s1;                     // 栈上实例化对象
    Student* s2 = new Student();    // 堆上实例化对象
    return 0;
}

五、类的大小与对象内存模型

很多新手疑惑:类里面同时存在成员变量、成员函数,创建出来的对象占用多少内存?

误区纠正

不存在「对象大小 = 成员变量大小 + 成员函数大小」。
如果每个对象都存储一份函数代码,大量实例化对象时,内存会严重冗余。

对象存储模型

  1. 成员变量:存储在每一个对象内部;
  2. 成员函数:统一存放于代码段(只读区),所有同类对象共享同一份函数代码。

类大小计算规则:遵循结构体内存对齐

内存对齐目的:提升 CPU 内存读取效率。
对齐核心规则:

  1. 第一个成员变量偏移地址为 0;
  2. 其余成员对齐到「对齐数」整数倍地址;

对齐数 = min (编译器默认对齐数,当前成员大小)
VS 默认对齐数 8;Linux gcc 默认对齐数 4

  1. 类总大小必须是最大对齐数的整数倍;
  2. 类中嵌套其他类时,嵌套类对齐至自身最大对齐数倍数。

示例演示:

class A {
public:
    char c; // 1字节,偏移0
    int i;  // 4字节,对齐到4倍数,1~3填充
};
// 总大小:8字节

class B {
public:
    int i;  // 4字节,偏移0
    char c; // 1字节,偏移4
};
// 原始占用5字节,补齐至最大对齐数4的倍数 → 总大小8字节

特殊场景:空类

class Empty {};

空类大小为 1 字节
C++ 标准规定:每一个对象必须拥有唯一独立地址。编译器自动插入 1 字节占位,用来区分多个不同空类对象。

六、this 指针 —— 打通对象与成员函数的关键

1. this 指针诞生的原因

成员函数只有一份,保存在代码段。当多个对象调用同一个成员函数时,函数如何区分当前操作哪一个对象的数据?

答案:this 指针
调用成员函数时,编译器自动将调用对象的地址,作为隐藏形参传递给成员函数,函数内部this指针就指向当前对象。

// 用户编写代码
s1.Print();

// 编译器底层处理伪代码
Student::Print(&s1);

2. this 指针核心特性

  1. 类型:Student* const this —— 指针本身不可修改,不能指向其他对象;不加 const 时,可以修改指向对象内部成员;
  2. 存储位置:属于函数隐式形参,存放于栈空间;
  3. 空指针风险:若对象指针为nullptr,this 指针即为空。函数内不访问任何成员变量程序可能不崩溃,但属于未定义行为,工程中严禁这么写。

3. 典型应用:链式调用

函数返回*this(对象自身引用),实现连续调用:

class Counter {
public:
    Counter& Add() {
        _count++;
        return *this;
    }
private:
    int _count = 0;
};

int main() {
    Counter c;
    c.Add().Add().Add(); // 链式连续调用
    return 0;
}

全文总结

本篇完整梳理 C++ 类与对象入门核心知识点:

  1. 思维转变:从面向过程「步骤驱动」切换到面向对象「实体驱动」;
  2. 类基础:class 定义、访问限定符,理解封装是隐藏实现、暴露接口;
  3. 实例化与内存模型:类是模板,对象占用内存仅存放成员变量,遵循内存对齐;空类占用 1 字节;
  4. this 指针:编译器隐式传递对象地址,连接对象与共享的成员函数,支持链式调用。