🔷 第十章 类和对象
📄 一句话概括

第十章讲的是"把数据和方法装进同一个盒子,并让这个盒子自己管好一生"。六块内容:造类型(10.1 组,类怎么写、怎么用)、出生(10.2 组,构造函数与复制构造函数)、消亡(10.3,析构函数)、盒子里的机关(10.4 组,const 成员函数、内联、静态成员、this、嵌套类、局部类)、通风口(10.5 组,友元)、防重名(10.6 组,命名空间)。这一章是全书最重的一章——从此你写的不再是"一堆函数",而是一批各管一摊、各有边界的零件。

📝 本节和前面几节的关系

收束整章。往回看:第九章让我们"想清楚为什么要有对象";第一章到第八章教的每一样东西(类型、表达式、分支循环、函数、数组指针、结构体)在本章都被重新组织了一遍——它们不再是散落的工具,而是"类里的成员"和"对象的行为"。往前看:下一章开始学继承(9.2.3 三大特点里的第二个),那时你会用到本章的构造函数(子类要调用父类的构造函数)和 protected(10.1.1 一直在说"留给继承用")。

一、本章知识地图

块 小节 一句话要点
造类型 10.1 / 10.1.1 / 10.1.2 / 10.1.3 / 10.1.4 类=把数据和方法打包成自定义类型;成员变量放 private、成员函数放 public;class 与 struct 只差默认访问权限;类名::函数名 写类外实现;多文件要分 .h / .cpp
出生 10.2 / 10.2.1 / 10.2.2 构造函数:名字=类名、无返回类型、可重载;初始化列表比体内赋值更直接(const/引用成员只能用它);按声明顺序初始化;复制构造函数=浅拷贝,成员有指针时必须自己写
消亡 10.3 析构函数:~类名()、无参数、不能重载;局部对象在离开作用域时就销毁;析构顺序与构造相反;构造函数里 new 几次,析构里就 delete 几次
盒子里的机关 10.4 组 const 成员函数(承诺不改数据,也是访问 const 对象的唯一途径);类内定义自动内联;静态成员属于类(要类外定义);this 是隐形的"当前对象地址";嵌套类/局部类的区别
通风口 10.5 组 友元=点名放行;三种(函数/类/成员函数);单向、不传递;能精确就别图省事
防重名 10.6 组 命名空间=给名字加姓氏;三种使用方式(全名/using 声明/using namespace);头文件里绝不写 using namespace;可以跨文件、可以嵌套;未命名命名空间=本文件私有

二、必须记住的八个结论

  1. class 和 struct 几乎是一回事,只差默认访问权限(class 是 private、struct 是 public)。选哪个是表达意图:struct 表"一堆数据"、class 表"一个有规矩的对象"(10.1)。
  2. 封装的本质是"约定入口",不是"把数据锁起来"。判据:外界能不能在不直接碰数据的情况下把事情办完(10.1.1、9.2.3)。
  3. 构造函数让对象"一出生就是合法的"。它自动执行、想忘也忘不了——这就是它比"手动调 init()"强的地方(10.2)。
  4. 成员初始化按"声明顺序",不按"初始化列表的顺序"。别让一个成员的初值依赖另一个成员(10.2.1)。
  5. 复制构造函数=浅拷贝。成员是值类型时够用;成员有"指向自己申请内存的指针"时,浅拷贝会造成"改一个动另一个"和"重复释放"——必须自己写(10.2.2)。
  6. "三法则":需要写析构函数(说明拥有资源)时,通常也要管好复制构造函数。三个函数要一起想(10.3)。
  7. this 是"当前对象的地址",是成员函数里那个隐形的第一个参数。p.show() 等价于 show(&p);静态成员函数没有 this,所以碰不到普通成员(10.4.4)。
  8. 友元是"发卡"不是"拆墙"——它只对点名的对象开放,单向且不传递。能精确(只放行一个函数)就别图省事(放行整个类)(10.5 组)。

三、动手 Demo:两关自测

第一关(写代码):一个完整可用的"银行账户"

要求:用本章学的东西,写一个 Account 类。

规格:

项 要求
数据 账号(字符数组)、余额(double)——都私有
构造 开户时给账号和初始余额;余额不能为负(负数就设成 0)
不会自动生成的构造 因为写了带参构造,要自己补一个无参版本(10.2)
方法 deposit(double) 存钱(金额必须 > 0)、withdraw(double) 取钱(不能超额)、show() const 打印
静态成员 统计一共开了几个户(10.4.3)
const 成员函数 show()、getBalance() 都要能对 const 对象调用(10.4.1)

参考实现(自己先写,写不出来再看):

#include <iostream>
#include <cstring>
using namespace std;

class Account {
private:
    char   id[20];
    double balance;
    static int count;                        // 静态成员(10.4.3)

public:
    // ① 无参构造:必须自己补,否则 Account a; 编译不过
    Account() : balance(0) {
        id[0] = '\0';
        count++;
    }

    // ② 带参构造:初始化列表 + 函数体校验
    Account(const char *pid, double b) : balance(b >= 0 ? b : 0) {
        strcpy(id, pid);
        count++;
    }

    // ③ 析构函数:没有申请资源,但可以打印留痕(10.3)
    ~Account() {
        count--;
    }

    // ④ 存钱:校验放这里
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            cout << "存入 " << amount << ",余额 " << balance << endl;
        } else {
            cout << "存入金额必须大于 0" << endl;
        }
    }

    // ⑤ 取钱:不能超额
    bool withdraw(double amount) {
        if (amount <= 0) {
            cout << "取款金额必须大于 0" << endl;
            return false;
        }
        if (amount > balance) {
            cout << "余额不足(当前 " << balance << ")" << endl;
            return false;
        }
        balance -= amount;
        cout << "取出 " << amount << ",余额 " << balance << endl;
        return true;
    }

    // ⑥ const 成员函数:只读(10.4.1)
    double getBalance() const { return balance; }

    void show() const {
        cout << "账号 " << id << ",余额 " << balance << endl;
    }

    static int getCount() { return count; }
};

int Account::count = 0;                      // ★ 静态成员类外定义

// 一个用 const 引用接账款的函数(验证 const 成员函数的作用)
void audit(const Account &a) {
    a.show();                                // 能调,因为 show() 是 const 的
    cout << "审计读到余额:" << a.getBalance() << endl;
}

int main() {
    Account a("A1001", 500);
    Account b("A1002", -50);                 // 非法初始余额,被兜成 0

    a.show();
    b.show();

    a.deposit(200);
    a.deposit(-5);                           // 被拦
    a.withdraw(1000);                        // 余额不足
    a.withdraw(300);

    audit(a);                                // 传 const 引用

    cout << "当前账户数 = " << Account::getCount() << endl;   // 2(用类名调)
    return 0;
}

看到什么算成功:

账号 A1001,余额 500
账号 A1002,余额 0
存入 200,余额 700
存入金额必须大于 0
余额不足(当前 700)
取出 300,余额 400
账号 A1001,余额 400
审计读到余额:400
当前账户数 = 2

如果这一关你能独立写出来:说明本章的核心(封装、构造、静态成员、const 成员函数)你已经会用了。

第二关(找错):下面这个类有六处问题

class Wallet {
public:
    double money;                           // ① ?
    static double rate;                     // ② ?

    Wallet(double m) {                      // ③ ?
        money = m;
    }

    ~Wallet() { delete money; }             // ④ ?

    void setMoney(double money) {           // ⑤ ?
        money = money;
    }

    double getMoney() { return money; }     // ⑥ ?
};

double Wallet::rate = 0.5;

答案:

# 问题 为什么错 怎么改
① money 是 public 数据裸露在外,规则守不住(10.1.1) 挪到 private
② static 成员缺少类外定义 其实这里写了(最后一行),但如果漏了就是 undefined reference——要点是"必须写一次"(10.4.3) 确认类外定义存在
③ 写了带参构造,却没写无参构造 Wallet w; 会报 no matching function(10.2) 补一个 Wallet() : money(0) { }
④ delete money; 是错的 money 是 double,不是指针!delete 只能用在指针上(10.3) 删掉这一行(double 不需要手动释放)
⑤ money = money; 是自赋值 参数名和成员名同名,必须写 this->money = money;(10.4.4) 加 this->,或改参数名
⑥ getMoney() 没加 const 它是只读函数,不加 const 就不能被 const 对象/const 引用调用(10.4.1) 改成 double getMoney() const { return money; }

30 秒实验:把第二关的六处改对,然后在 main 里写:

Wallet w(100);
const Wallet cw(50);            // const 对象
cw.getMoney();                  // 如果 ⑥ 没改,这里会报 discards qualifiers

改一处、编一次——六个错误各自会报出什么样的错、为什么报,一次全部体验完。 这比读十遍文字有用得多。

四、常见误区(本章汇总)

  1. "把数据全设成 private、给每个字段配 get/set 就叫封装"——那是"门开得跟没墙一样"。好的封装是暴露有意义的行为(deposit、withdraw),而不是暴露字段(10.1.1、9.2.3)。
  2. "忘记写构造函数也没关系,编译器会给一个"——它给的那个是"空的",什么都不做(成员还是垃圾值)。要初始化就得自己写(10.2)。
  3. "复制就是复制,不会有坑"——成员里有指针时,默认的浅拷贝就是雷(10.2.2)。写完类的最后一个习惯动作是问自己:"我有没有需要自己管理的资源?"
  4. "析构函数会释放对象本身的内存"——对象本身的字节由系统回收(局部对象出作用域、堆对象 delete);析构函数负责的是"对象自己申请的那些额外资源"(10.3)。
  5. "友元破坏了封装,不该用"——它常常比"开一堆 get/set"更保护封装。关键不是"用不用",而是"给多大权限":能只放行一个函数,就别放行整个类(10.5 组)。
  6. "using namespace std; 写在文件开头是标准做法"——那是教程为了排版简洁。工程规范基本禁止它,尤其禁止写在头文件里(10.6 组)。从今天起试着写成 std::cout。

五、下一章预告

本章学会了"造一个类"。下一章开始学"让一个类站在另一个类的肩膀上"——继承。

你会看到 9.2.3 说的三大特点里,第二个(继承)和第三个(多态)正式登场;本章埋下的几个伏笔也会一一兑现:

自测八问(点开对答案) 1. `class` 和 `struct` 的唯一实质差别是什么?——**默认访问权限**:`class` 默认 `private`,`struct` 默认 `public`。其余(能有方法、能继承、写法)都一样。 2. 构造函数为什么比"手动调 `init()`"可靠?——因为它是**自动执行的**:**对象一被创建就跑,想忘也忘不了**,所以对象"一出生就是合法状态"(10.2)。 3. 哪些成员只能用初始化列表初始化?——**`const` 成员**和**引用成员**(它们出生后就改不了,在函数体里赋值已经太晚)。另外复杂类型(如 `string`)也推荐用初始化列表(省一次赋值)。 4. 复制构造函数什么时候必须自己写?——**类里有"指向自己申请内存的指针"时**(浅拷贝会让两个对象共用一块内存 → 改一个动另一个 + 重复释放)。判据:**"我有没有需要自己管理的资源?"** 5. 局部对象的析构函数什么时候被调用?多个对象的析构顺序?——**离开作用域的那一刻**(不是程序结束);**顺序与构造相反——后造的先销毁**(栈的"后进先出")。 6. `this` 是什么?静态成员函数为什么没有它?——**`this` 是"当前对象的地址"**,是成员函数**隐形的第一个参数**(`p.show()` ≡ `show(&p)`)。**静态成员函数不属于任何对象**,所以没有 `this`,也就**不能访问普通成员**。 7. 友元关系是单向的吗?能传递吗?——**单向**(我让你进 ≠ 你能让我进,要互相就两边都声明);**不传递**(A→B、B→C 不代表 A→C)。 8. 头文件里为什么绝不能写 `using namespace`?——因为头文件会被**很多文件包含**,它会把整个命名空间的名字**塞给所有包含者**,导致别人代码里的同名标识符撞车,**报错还看不出原因**。头文件里应该写 `std::` 全名。

相关笔记