📄 一句话概括
第十章讲的是"把数据和方法装进同一个盒子,并让这个盒子自己管好一生"。六块内容:造类型(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;可以跨文件、可以嵌套;未命名命名空间=本文件私有 |
二、必须记住的八个结论
class和struct几乎是一回事,只差默认访问权限(class是private、struct是public)。选哪个是表达意图:struct表"一堆数据"、class表"一个有规矩的对象"(10.1)。- 封装的本质是"约定入口",不是"把数据锁起来"。判据:外界能不能在不直接碰数据的情况下把事情办完(10.1.1、9.2.3)。
- 构造函数让对象"一出生就是合法的"。它自动执行、想忘也忘不了——这就是它比"手动调
init()"强的地方(10.2)。 - 成员初始化按"声明顺序",不按"初始化列表的顺序"。别让一个成员的初值依赖另一个成员(10.2.1)。
- 复制构造函数=浅拷贝。成员是值类型时够用;成员有"指向自己申请内存的指针"时,浅拷贝会造成"改一个动另一个"和"重复释放"——必须自己写(10.2.2)。
- "三法则":需要写析构函数(说明拥有资源)时,通常也要管好复制构造函数。三个函数要一起想(10.3)。
this是"当前对象的地址",是成员函数里那个隐形的第一个参数。p.show()等价于show(&p);静态成员函数没有this,所以碰不到普通成员(10.4.4)。- 友元是"发卡"不是"拆墙"——它只对点名的对象开放,单向且不传递。能精确(只放行一个函数)就别图省事(放行整个类)(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
改一处、编一次——六个错误各自会报出什么样的错、为什么报,一次全部体验完。 这比读十遍文字有用得多。
四、常见误区(本章汇总)
- "把数据全设成
private、给每个字段配get/set就叫封装"——那是"门开得跟没墙一样"。好的封装是暴露有意义的行为(deposit、withdraw),而不是暴露字段(10.1.1、9.2.3)。 - "忘记写构造函数也没关系,编译器会给一个"——它给的那个是"空的",什么都不做(成员还是垃圾值)。要初始化就得自己写(10.2)。
- "复制就是复制,不会有坑"——成员里有指针时,默认的浅拷贝就是雷(10.2.2)。写完类的最后一个习惯动作是问自己:"我有没有需要自己管理的资源?"
- "析构函数会释放对象本身的内存"——对象本身的字节由系统回收(局部对象出作用域、堆对象
delete);析构函数负责的是"对象自己申请的那些额外资源"(10.3)。 - "友元破坏了封装,不该用"——它常常比"开一堆
get/set"更保护封装。关键不是"用不用",而是"给多大权限":能只放行一个函数,就别放行整个类(10.5 组)。 - "
using namespace std;写在文件开头是标准做法"——那是教程为了排版简洁。工程规范基本禁止它,尤其禁止写在头文件里(10.6 组)。从今天起试着写成std::cout。
五、下一章预告
本章学会了"造一个类"。下一章开始学"让一个类站在另一个类的肩膀上"——继承。
你会看到 9.2.3 说的三大特点里,第二个(继承)和第三个(多态)正式登场;本章埋下的几个伏笔也会一一兑现:
- 10.1.1 的
protected——那时说"留给继承用",下一章它就派上用场了。 - 10.2.1 的"子类怎么调用父类的构造函数"——构造函数在继承里的新规则。
- 10.3 的虚析构函数——析构函数在继承里的新坑。
- 10.4.4 的
this——它在多态里会变得更有意思。
自测八问(点开对答案)
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::` 全名。- ☑ 第十一章 继承与派生 —— 已完结:小结(让类站在类的肩膀上:继承语法与权限、构造顺序、隐藏同名函数、运算符重载、多重继承、
virtual与动态绑定、抽象类)