一、这一章在讲什么

第九章的主题是:类和对象

如果说前面几章主要是在练 C++ 的基本语法,那么这一章开始真正进入面向对象编程的核心区域。它回答的是下面这些问题:

  • 类到底是什么,和对象是什么关系
  • 为什么成员变量和成员函数要被放进同一个“壳”里
  • publicprivate 到底在保护什么
  • 构造函数、析构函数为什么重要
  • 为什么带原始指针的类会遇到“浅复制”问题
  • 复制构造函数和移动构造函数分别解决什么问题
  • thisfriendstructunion、聚合初始化分别扮演什么角色

这一章真正训练的,不只是“会写 class”,而是开始建立一种更接近工程代码的思维:对象如何诞生、如何被访问、如何被复制、如何被销毁


二、类和对象的关系

2.1 类是蓝图,对象是实例

类可以理解成一种“模板”或“蓝图”,它描述一种事物应该具备哪些数据和哪些行为。

例如书里用 Human 来模拟一个人:

  • 数据:姓名、年龄、出生地
  • 行为:说话、自我介绍

类本身只是定义,不会自动运行。真正进入程序运行阶段的是对象

Human firstMan;

这里 firstMan 才是对象,也叫这个类的一个实例。

2.2 类把“数据”和“操作数据的函数”放在一起

这是类最重要的意义之一。

过去用过程式写法时,数据和函数很容易分散;而类强调把:

  • 成员数据
  • 成员函数

放进同一个逻辑单元里。

这就是封装的起点。

2.3 对象既可以在栈上创建,也可以在堆上创建

Human firstMan;
Human* firstWoman = new Human();
delete firstWoman;

第一种是普通局部对象,通常放在栈上;第二种是动态分配的对象,通过指针持有。

这一点会直接影响后面成员访问方式和生命周期管理方式。


三、成员访问:.->

3.1 句点运算符访问普通对象成员

Human firstMan;
firstMan.age = 30;
firstMan.IntroduceSelf();

如果你手里拿的是对象本身,就使用 .

3.2 指针运算符访问对象指针成员

Human* firstWoman = new Human();
firstWoman->age = 28;
firstWoman->IntroduceSelf();

如果你手里拿的是“指向对象的指针”,就使用 ->

它和下面写法等价:

(*firstWoman).IntroduceSelf();

3.3 这里的核心不是语法,而是“你手里到底是什么”

很多人把 .-> 当成单纯语法记忆,但本质区别在于:

  • .:你拿到的是对象
  • ->:你拿到的是对象地址

这一点和第八章的指针知识是直接连起来的。


四、publicprivate 真正解决什么问题

4.1 public 是对外接口,private 是内部实现

类设计时,不是所有成员都应该暴露给外部。

例如年龄 age 如果直接公开:

eve.age = -100;

那外部代码就可以随意写入错误状态。

如果把它改成私有:

class Human
{
private:
    int age;
public:
    void SetAge(int humansAge)
    {
        if (humansAge > 0)
            age = humansAge;
    }
};

那么类就能控制这个值如何被修改。

4.2 这就是封装和抽象

封装强调的是:把相关数据和行为装进一起。

抽象强调的是:对外只暴露必要接口,隐藏内部细节

这意味着:

  • 外部不必知道对象内部怎么存
  • 外部只需要知道该怎么用

4.3 getter / setter 的重点不是“形式感”

很多初学者会觉得 GetAge()SetAge() 只是绕一圈。

但真正价值在于:

  • 可以校验输入
  • 可以改变对外暴露方式
  • 可以在不改调用方的前提下重构内部实现

也就是说,类不是把数据“锁死”,而是给数据访问加规则。


五、构造函数:对象出生时的规则

5.1 构造函数的基本特点

构造函数:

  • 与类同名
  • 没有返回类型
  • 在对象创建时自动调用
class Human
{
public:
    Human();
};

5.2 构造函数最重要的用途:初始化

成员变量尤其是 int、指针这样的成员,如果不初始化,很容易变成垃圾值。

所以构造函数最适合做的事,就是让对象一出生就处于合理状态。

class Human
{
private:
    int age;
public:
    Human()
    {
        age = 1;
    }
};

5.3 默认构造函数

能够在不传参数情况下调用的构造函数,通常就叫默认构造函数。

例如:

Human person;

如果类只提供了一个必须传参数的构造函数,那么这种写法就不成立了。

5.4 构造函数可以重载

同一个类可以有多个构造函数,只要参数列表不同:

Human();
Human(int age);
Human(string name, int age);

这让对象创建方式更灵活。

5.5 没有默认构造函数的类

如果你写了带参数构造函数,但没有提供默认构造函数:

class Human
{
public:
    Human(int age);
};

那么 Human h; 就会编译失败。

这是第九章里很容易踩的点。

5.6 带默认值的构造函数参数

如果构造函数的参数都有默认值,那么这个构造函数也可以承担默认构造函数的角色:

Human(int age = 1);

于是:

Human h;
Human h2(20);

都成立。

5.7 初始化列表比“函数体里赋值”更接近对象真实初始化

Human(string name, int age) : name(name), age(age) {}

初始化列表尤其适合这些场景:

  • 常量成员
  • 引用成员
  • 成员对象本身也需要构造

虽然书里只是引入,但这一点在后面章节会越来越重要。


六、析构函数:对象离场时的规则

6.1 析构函数的特点

析构函数:

  • 名字是 ~类名
  • 没有参数
  • 没有返回类型
  • 一个类只能有一个析构函数,不能重载
~Human();

6.2 析构函数最重要的用途:释放资源

如果构造函数里申请了资源,析构函数就应该负责清理资源。

最典型的例子就是动态内存:

class MyString
{
private:
    char* buffer;
public:
    ~MyString()
    {
        delete[] buffer;
    }
};

6.3 没写析构函数不一定立刻出错,但很容易留下泄漏

如果类里只有普通值类型成员,编译器生成的默认析构通常没问题。

但如果类自己拥有:

  • 动态内存
  • 文件句柄
  • socket
  • 其他外部资源

那“默认析构”往往就不够了。


七、复制构造函数:为什么对象复制会出问题

7.1 对象也会被复制

对象不是只有你写 = 才复制。

这些场景也会触发复制:

  • 按值传参
  • 按值返回
  • 用一个同类对象初始化另一个对象
MyString b = a;
UseMyString(a);
return temp;

7.2 浅复制的问题

如果类里有原始指针成员,编译器默认复制往往只是“把指针值复制过去”。

也就是说:

  • 两个对象拿到的是同一个地址
  • 但它们以为自己各自拥有资源

一旦析构,就可能出现:

  • 重复释放
  • 悬空指针
  • 程序崩溃

7.3 深复制才是拥有资源类真正需要的复制方式

深复制不是复制地址,而是:

  • 重新申请一份资源
  • 把源对象内容复制过来
MyString(const MyString& copySource)
{
    buffer = new char[strlen(copySource.buffer) + 1];
    strcpy(buffer, copySource.buffer);
}

这样两个对象才真正互不影响。

7.4 复制构造函数参数为什么是 const ClassName&

这是经典面试点。

原因有两个:

  • 按引用传递,避免复制构造函数调用自己造成无限递归
  • const 保证复制过程中不修改源对象

7.5 复制构造函数和复制赋值不是一回事

MyString b = a; // 复制构造
b = a;          // 复制赋值

这两个动作发生的时机不同,语义也不同。

书里重点讲的是复制构造,但它已经明显提示了后面会进入复制赋值运算符。


八、移动构造函数:为了性能,不必总是深复制

8.1 为什么需要移动构造

有些对象只是临时值。

如果对这种临时对象也做深复制,成本可能很高,尤其对象内部资源很大时。

8.2 移动构造的核心不是“复制”,而是“接管”

移动构造函数的核心思想是:

  • 直接接管源对象手里的资源
  • 把源对象置为空或无害状态
MyString(MyString&& moveSource)
{
    buffer = moveSource.buffer;
    moveSource.buffer = NULL;
}

8.3 它解决的是“短命临时对象还被昂贵复制”的问题

这就是 C++11 引入移动语义的根本动机之一。

第九章只是开个头,但已经把一个很重要的性能方向点出来了。


九、构造函数和析构函数还能控制对象怎么存在

这一章有个很有意思的地方:构造函数和析构函数不仅能做初始化和清理,还能参与限制对象存在方式

9.1 禁止复制的类

书里的传统写法是:

  • 把复制构造函数声明为私有
  • 把复制赋值运算符声明为私有

这样外部就不能复制对象。

这适用于“不应该被复制”的资源类。

9.2 单例类

单例模式的核心组合是:

  • 私有构造函数
  • 私有复制控制
  • 静态实例访问函数

它的目标是:全局只允许存在一个实例

9.3 禁止在栈上实例化

如果把析构函数设成私有,那么局部对象离开作用域时编译器无法正常调用析构函数,因此对象不能直接创建在栈上。

这种技巧的本质是:通过“销毁规则”反向限制“创建方式”。

9.4 构造函数也可能触发类型转换

单参数构造函数允许这种写法:

Human kid = 10;

因为编译器会把 10 当作构造参数,隐式创建一个 Human

如果不想允许这种隐式转换,就使用:

explicit Human(int age);

这在工程里通常是更稳妥的写法。


十、其他必须掌握的几个关键点

10.1 this 指针

在非静态成员函数里,this 表示当前对象地址。

this->age = humansAge;

它常见于:

  • 区分成员和同名参数
  • 返回当前对象
  • 强调“当前对象正在操作自己”

静态成员函数没有 this,因为它不绑定某个具体对象。

10.2 sizeof(class) 只统计数据成员占用

sizeof 用在类上时,关注的是对象数据布局,而不是成员函数代码大小。

它通常会受到:

  • 数据成员本身大小
  • 对齐
  • padding

的影响。

所以它不等于“把每个成员简单相加”。

10.3 structclass 的核心差异很小

在 C++ 里,structclass 非常像。

常见差别只有两个默认值:

  • struct 默认成员访问权限是 public
  • class 默认成员访问权限是 private

也就是说,语义上不是两个世界,更多是默认约定不同。

10.4 friend 是受控开口

friend 函数或 friend 类可以访问类的私有成员。

它不是“取消封装”,而是“由类作者明确授权的例外通道”。

该用时可以用,但应该克制。

10.5 union 适合“同一块内存按不同方式解释”

共用体一次只应有一个有效成员处于活动状态。

它的关键特点是:

  • 多个成员共享同一块内存
  • sizeof(union) 通常等于最大成员大小

这是一种节省空间、但也更容易误用的机制。

10.6 聚合初始化

当类或结构满足聚合类型条件时,可以整体初始化:

Aggregate1 a1{2017, 3.14};

但如果类里有私有成员、用户定义构造函数等,就可能不再是聚合类型。

10.7 constexpr 类和对象

如果构造函数和成员函数满足要求,可以让编译器在编译期完成计算:

constexpr Human somePerson(15);

这有助于提升性能,也让“对象”不再只是运行时概念。


十一、本章最容易混淆的点

  • 类不是对象,类是定义,对象才是运行时实例
  • .-> 的区别不在符号本身,而在你手里拿的是对象还是指针
  • private 不是“完全不能访问”,而是“不能从类外直接访问”
  • 有带参构造函数不代表还有默认构造函数
  • 复制构造和复制赋值不是同一件事
  • 原始指针成员一旦参与默认复制,极容易触发浅复制问题
  • 析构函数负责资源释放,但不能代替正确的复制控制
  • explicit 解决的是“你没想转换,编译器却帮你转换了”
  • sizeof(class) 不是成员简单相加,还受对齐影响
  • friend 是例外授权,不是常规接口设计

十二、这一章学完后应该真正掌握什么

如果这一章掌握到位,至少应该能清楚说出下面这些事:

  • 为什么类能把数据和操作数据的方法组织在一起
  • 什么时候应该把成员声明为 private
  • 构造函数和析构函数分别最适合做什么
  • 为什么带原始指针的类需要警惕浅复制
  • 复制构造和移动构造分别解决什么问题
  • explicit 为什么是单参数构造函数的高频关键字
  • structfriendunion、聚合初始化各自解决什么场景

这一章表面上是在讲“类怎么写”,但更深一层其实是在讲:

你如何设计一个对象,让它从创建到销毁都保持语义清晰、状态合理、资源安全。

这才是第九章真正的核心。