1. 类的定义 —— class vs struct

C++ 中,class 和 struct 都可以用来定义类。

class ClassName

{
    // 成员变量(属性)
    // 成员函数(方法)
};  // 分号不能省略

区别

代码示例与用法归类

  • class 中成员默认是 private(私有)

  • struct 中成员默认是 public(公有)

  •  C++ 升级了 struct,里面可以放函数

  • struct ListNodeCPP {
        void Init(int x) {
            next = nullptr;
            val = x;
        }
        ListNodeCPP* next;
        int val;
    };  // 不再需要 typedef,ListNodeCPP 本身就是类型名

  • ✅ 建议:一般情况下,用 class 定义类,用 struct 定义纯数据聚合(如链表节点)。

  • 成员变量的命名习惯

    为了区分成员变量和普通变量,常见约定:在成员变量

  • 后面加 _year_

  • 前面加 __year

  • 前面加 mm_year

  • class Date {
    private:
        int _year;   // 前面加 _
        int _month;
        int _day;
    };

    2. 访问限定符 —— 封装的第一步

    限定符 类外访问 说明
    public ✅ 可以 对外接口
    protected ❌ 不可以 继承相关,暂时和 private 一样
    private ❌ 不可以 内部实现细节
    class Date {
    public:          // 从这里开始,之后的成员对外可见
        void Init(int year, int month, int day);
    private:         // 从这里开始,之后的成员对外隐藏
        int _year;
        int _month;
        int _day;
    };
  • 作用域:从该限定符出现的位置开始,到下一个限定符或类结束为止。
    class 默认 privatestruct 默认 public
  • 3. 类域 —— 影响编译查找规则

    类定义了一个新的作用域。在类体外定义成员函数时,需要用 :: 指明属于哪个类:

  • 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;   // 在当前函数作用域找不到 _year,会去 Date 类域中找
        _month = month;
        _day = day;
    }

  • 如果不指定 Date::,编译器会把 Init 当成全局函数,找不到 _year 等成员就会报错!

  • 【补充::: 的用法与原理详解】

    在刚才的代码中,我们使用了 void Date::Init(...),这里的 :: 称为作用域解析运算符(Scope Resolution Operator)。它是 C++ 中非常重要的符号,直接决定了编译器去哪里查找名字。

    1. :: 的用法(左边是什么?右边是什么?)

    :: 的左右两侧分工明确: 域::具体成员

  • 左侧(左操作数):指定要查找的“域”(Scope)。它可以是类名(如 Date)、命名空间名(如 std),或者留空(空着表示全局作用域)。

  • 右侧(右操作数):该域内的具体成员。包括成员函数名(如 Init)、成员变量名类型名(如 size_t)或静态成员

2. :: 的原理(编译器如何处理)

核心原理:: 的核心作用就是强制指定查找路径,它会命令编译器“跳过”默认的层层查找规则,直接去指定的域里找。

默认查找规则(不加 :: 时)
编译器遵循就近原则(即名字查找规则):先在当前局部域(函数体内)找,找不到再去全局域找。如果局部有同名变量,编译器绝对不会去全局找。

使用 :: 后的查找规则

  1. 编译器解析到 A::B 时,会先将 A 当作一个限定符,识别 A 到底是一个还是一个命名空间(如果是 ::B,则直接定位到全局域)。

  2. 确定 A 的作用域范围后,编译器只在该作用域内部的符号表中搜索 B。如果 B 在该作用域中不存在,编译器会直接报错(“未定义标识符”),而不会再去外部的全局域碰运气。通俗的讲,A::B,可以粗略理解为“去A中找B”

为什么要这样设计(意义)?

  • 解决名字冲突(隐藏问题):当局部变量与全局变量重名时,用 ::变量名 可以精准指名道姓,让编译器不再受“就近原则”干扰。

  • 实现声明与定义分离(类外定义):在类体外定义成员函数时(如 void Date::Init()),:: 告诉编译器:“Init 是 Date 家族的成员”。如果不加 ::,编译器会认为你在定义一个全局函数 Init,那么函数内部访问 _year 时,编译器会去全局找,自然就找不到私有成员,从而报错。

  • 访问命名空间成员:通过 std::cout 的方式,可以将庞大的标准库隔离在 std 域中,避免与用户自定义的 cout 发生冲突。

  • 4. 实例化与对象大小

    4.1 实例化 —— 从图纸到房子

     就像一张设计图,规定了有哪些房间(成员变量),但本身不占用物理空间。
    对象 是根据设计图建造出来的房子,真正占用内存空间。

  • class Date {
    private:
        int _year;   // 声明,未开空间
        int _month;
        int _day;
    };

    int main() {
        Date d1;     // 实例化,此时才分配空间
        Date d2;     // 可以实例化多个对象
        return 0;
    }

    4.2 对象大小 —— 内存对齐规则

    对象中只存储成员变量,不存储成员函数(函数在代码段)。

    C++ 规定对象大小遵循内存对齐规则(VS 默认对齐数为 8):

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

  • 其他成员对齐到 对齐数 的整数倍地址。
    对齐数 = min(成员大小, 默认对齐数) 取成员大小与默认对齐数更小的那个

  • 结构体总大小为 最大对齐数 的整数倍。

  • 不同数据类型在常见平台下占用的字节数:

  • 数据类型 32 位环境(字节) 64 位环境(字节) 说明
    char 1 1 字符类型,固定 1 字节
    bool 1 1 布尔类型,固定 1 字节
    short 2 2 短整型,固定 2 字节
    int 4 4 整型,固定 4 字节
    long 4 8(Linux) / 4(Windows) ⚠️ 视平台而定,Windows 64 位下 long 仍然是 4 字节
    long long 8 8 长长整型,固定 8 字节
    float 4 4 单精度浮点,固定 4 字节
    double 8 8 双精度浮点,固定 8 字节
    指针 T* 4 8 指针大小 = 地址总线宽度:32 位系统 4 字节,64 位系统 8 字节
  • class A {
    private:
        char _ch;   // 1 字节,偏移 0
        int  _i;    // 4 字节,对齐数 4,从偏移 4 开始 → 中间填充 3 字节
    };  // 总大小 = 8 字节(最大对齐数为 4,8 是 4 的倍数)总大小为整数倍

    class B {};     // 空类,大小为 1占位标识对象存在)

    5. 构造函数 —— 自动初始化

    5.1 为什么需要构造函数?

    之前我们写 Date 或 Stack 时,需要手动调用 Init() 函数来初始化对象。
    构造函数 让对象在实例化时自动调用,不需要用户手动初始化。

    5.2 构造函数的特点

    特点 说明
    函数名与类名相同 Date()Stack()
    无返回值 不需要写 void
    自动调用 对象实例化时自动执行
    可以重载 支持多个构造函数
    默认生成 用户不写,编译器自动生成一个
  • 什么是“默认构造函数”?

    有人可能会误以为“默认构造”就是编译器自动生成的那个。实际上,默认构造函数的定义是:

    不传实参就可以调用的构造函数

    它包含以下 三种

    类型 示例 说明
    ① 无参构造函数 Date() 用户显式定义,无参数
    ② 全缺省构造函数 Date(int y = 1, int m = 1, int d = 1) 所有参数都有默认值
    ③ 编译器自动生成的无参构造 用户完全不写任何构造函数时 对内置类型不做初始化
  • 这三种默认构造函数无法共存!
  • 先把一个注释掉
  • 全缺省构造和普通带参构造,在类外定义的函数体没有任何区别

    唯一的区别在于类内声明时:全缺省写了 =默认值,普通带参没写。

    既然函数体一模一样,而全缺省构造既能不传参、又能传部分、又能传全部,它已经完全覆盖了普通带参构造的功能,所以你根本不需要再写一个普通带参构造,只写全缺省一个就足够了!

    6. 析构函数 —— 自动清理资源

    6.1 为什么需要析构函数?

    构造函数负责初始化,析构函数负责资源清理(释放动态分配的内存、关闭文件等)。
    对象生命周期结束时,析构函数自动调用,不需要手动调用。

  • class Stack {
    public:
        Stack(int n = 4) {
            _a = (int*)malloc(sizeof(int) * n);
            _capacity = n;
            _top = 0;
        }

        ~Stack() {          // 析构函数:~类名
            free(_a);
            _a = nullptr;
            _top = _capacity = 0;
        }

    private:
        int* _a;
        size_t _capacity;
        size_t _top;
    };

    6.2 析构函数的特点

    特点 说明
    函数名 ~类名,如 ~Date()~Stack()
    无参数 不能重载,一个类只有一个析构函数
    无返回值 不需要写 void
    自动调用 对象生命周期结束时自动执行
    默认生成 用户不写,编译器自动生成
  • 6.3 编译器自动生成的析构函数

  • 内置类型:不做处理

  • 自定义类型成员:调用该成员的析构函数

    6.4 什么时候需要自己写析构?

    类类型 是否需要显示写析构 原因
    Date ❌ 不需要 没有资源申请,编译器默认即可
    Stack ✅ 必须写 内部有 malloc 申请的资源,必须 free

    6.5 多个对象的析构顺序

    C++ 规定:后定义的对象先析构(类似于栈,后进先出)。

  • 7. C 与 C++ 的 Stack 对比(示意)

  • 维度 C 语言实现 C++ 类实现
    数据和函数 分离 封装在一起
    初始化//initialization 需要手动调用 Init() 构造函数自动调用
    资源释放 需要手动调用 Destroy() 析构函数自动调用
    访问控制 无法控制,任意访问 private 隐藏内部细节
    类型名 需要 typedef 简化 类名本身就是类型

更多推荐