ClassA1
ClassB
ClassA copy
ClassA
invoke ClassA2
Delete ClassA
Done!
Delete ClassA
Delete ClassB
Delete ClassA
02 运行结果题:把代码当成一条时间线
运行结果题最怕“盯着一大坨代码发呆”。新版做法:先看样题输出,再拆时间线,最后背做题方法。
第2题
构造 + 拷贝 + 析构
ClassA / ClassB 输出题
先抓关键词
`ClassB deri;` 是出生;`ClassA base = deri;` 是切片拷贝;`find(deri);` 是引用传参,不拷贝。
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA() { cout << "ClassA" << endl; }
ClassA(int i) { cout << "ClassA" << i << endl; }
ClassA(const ClassA& src) { cout << "ClassA copy" << endl; }
virtual ~ClassA() { cout << "Delete ClassA" << endl; }
void invoke(int i = 7) const {
cout << "invoke ClassA" << i << endl;
}
};
class ClassB: public ClassA {
public:
ClassB() : ClassA(1) { cout << "ClassB" << endl; }
virtual ~ClassB() { cout << "Delete ClassB" << endl; }
void invoke(int i = 9) const {
cout << "invoke ClassB" << i << endl;
}
};
void find(const ClassA& inKlep) {
ClassA theClassA;
inKlep.invoke(2);
}
int main() {
ClassB deri;
ClassA base = deri;
find(deri);
cout << "Done!" << endl;
return 0;
}
✅ 样题输出
字幕版时间线:一行输出配一行原因
| 步 | 代码正在做什么 | 此刻输出 | 人话解释 |
|---|---|---|---|
| 1 | ClassB deri; |
ClassA1 | 要造 ClassB,得先造它身体里的 ClassA 父类部分。`ClassB()` 写了 : ClassA(1),所以父类构造输出 `ClassA1`。 |
| 2 | ClassB deri; |
ClassB | 父类部分造完了,才轮到 ClassB 自己出生。继承构造顺序就是:先父后子。 |
| 3 | ClassA base = deri; |
ClassA copy | 用 ClassB 初始化 ClassA,新对象 `base` 只接住 ClassA 那一层,ClassB 那层被切掉,这叫对象切片。 |
| 4 | find(deri); |
无输出 | 参数是 const ClassA&,引用像“外号”,不会复制对象,所以这里没有 `copy`。 |
| 5 | ClassA theClassA; |
ClassA | 进入 `find` 后,新建一个普通 ClassA 局部变量。它出生,就调用无参构造。 |
| 6 | inKlep.invoke(2); |
invoke ClassA2 | `invoke` 前面没有 `virtual`,所以不看真实对象是不是 ClassB,只看声明类型 ClassA&。 |
| 7 | find 结束 |
Delete ClassA | `theClassA` 是函数里的临时局部变量,函数结束它先退场。 |
| 8 | cout << "Done!" |
Done! | 这行是明牌输出,没有陷阱。 |
| 9 | main 结束,先销毁 base |
Delete ClassA | 局部变量退场顺序和出生顺序相反。`base` 比 `deri` 后出生,所以先退场。 |
| 10 | main 结束,再销毁 deri |
Delete ClassB Delete ClassA |
`deri` 是 ClassB 对象。析构顺序和构造反过来:先子类 ClassB,再父类 ClassA。 |
考场记法
这题就是“出生、切片、临时对象、倒着退场”。看到 `const ClassA&` 不要喊拷贝,它只是引用。
第5题
循环累加
Add 类:while 循环输出题
class Add {
private:
int n;
public:
Add(int nn) {
n = nn;
}
int add() {
int sum = 0;
int i = 0;
while (true) {
if (i % 10 == 0) {
sum = sum + i;
}
++i;
if (i > n) break;
}
return sum;
}
};
int main() {
Add a(50);
cout << a.add() << endl;
return 0;
}
✅ 输出
150
| 会被加进去的 i | 为什么 | sum 变化 |
|---|---|---|
| 0 | 0 % 10 == 0 | 0 |
| 10 | 10 的倍数 | 10 |
| 20 | 10 的倍数 | 30 |
| 30 | 10 的倍数 | 60 |
| 40 | 10 的倍数 | 100 |
| 50 | 10 的倍数 | 150 |
考场记法
这题不用被 class 吓住。真正干活的是循环:`0 + 10 + 20 + 30 + 40 + 50 = 150`。
第6题
多态综合
Animal / Dog:虚函数、非虚函数、切片
这题是样题里的大 boss
它把构造、拷贝、虚函数、非虚函数、析构都揉在一起。别硬背原题,按时间线拆,换名字也能做。
class Animal {
public:
Animal() { cout << "A ctor" << endl; }
Animal(int n) { cout << "A ctor " << n << endl; }
Animal(const Animal& src) { cout << "A copy ctor" << endl; }
virtual ~Animal() { cout << "A dtor" << endl; }
virtual void speak(int n = 5) const {
cout << "A::speak " << n << endl;
}
void breathe() const {
cout << "A::breathe" << endl;
}
};
class Dog : public Animal {
public:
Dog() : Animal(3) { cout << "D ctor" << endl; }
virtual ~Dog() { cout << "D dtor" << endl; }
void speak(int n = 9) const override {
cout << "D::speak " << n << endl;
}
void breathe() const {
cout << "D::breathe" << endl;
}
};
void bar(const Animal& a) {
Animal tmp;
a.speak(1);
a.breathe();
}
int main() {
Dog d; Animal a = d; bar(d);
cout << "---" << endl;
Animal* p = new Dog();
p->speak(); delete p; return 0;
}
✅ 样题输出
A ctor 3
D ctor
A copy ctor
A ctor
D::speak 1
A::breathe
A dtor
---
A ctor 3
D ctor
D::speak 9
D dtor
A dtor
A dtor
D dtor
A dtor
小心:这里不能只背结果
样题里写的是 `D::speak 9`。但期末很可能改类名、改数字、改调用方式,所以真正要掌握的是:虚函数看真实对象,非虚函数看声明类型,构造析构按时间线。另:如果严格抠标准 C++ 的“虚函数默认参数静态绑定”,这个点可能有争议;考试时优先跟老师课件口径。
字幕版时间线:每一行输出从哪里来
| 步 | 代码正在做什么 | 此刻输出 | 为什么是它 |
|---|---|---|---|
| 1 | Dog d; |
A ctor 3 | Dog 出生前,先造父类 Animal 部分。`Dog()` 写了 : Animal(3),所以不是 `A ctor`,而是 `A ctor 3`。 |
| 2 | Dog d; |
D ctor | 父类造完,Dog 自己再出生。口诀:构造先父后子。 |
| 3 | Animal a = d; |
A copy ctor | 用 Dog 去初始化 Animal,只拷贝父类 Animal 那一片。Dog 的部分像被裁掉了,这就是对象切片。 |
| 4 | bar(d); |
无输出 | 参数是 const Animal&,引用传参不拷贝。要是这里不是引用,才可能多出拷贝构造。 |
| 5 | Animal tmp; |
A ctor | 进入 `bar` 之后,函数里新建一个普通 Animal 临时对象,所以调用无参构造。 |
| 6 | a.speak(1); |
D::speak 1 | `speak` 是虚函数。虚函数像“看身份证”:真实对象是 Dog,所以用 Dog 的函数体;括号里明确传了 1,所以输出 1。 |
| 7 | a.breathe(); |
A::breathe | `breathe` 不是虚函数。非虚函数像“看工作牌”:参数声明是 Animal&,所以用 Animal 版本。 |
| 8 | bar 结束 |
A dtor | `tmp` 是 `bar` 里面的局部变量,函数结束就退场。 |
| 9 | cout << "---" |
--- | 分隔线,明牌输出,别想复杂。 |
| 10 | Animal* p = new Dog(); |
A ctor 3 | 虽然左边是 Animal 指针,右边真实创建的是 Dog。Dog 出生依然先造 Animal(3)。 |
| 11 | Animal* p = new Dog(); |
D ctor | 父类部分造完,再造 Dog 自己。 |
| 12 | p->speak(); |
D::speak 9 | 样题输出写 9。考试先按老师/样题思路:`speak` 是虚函数,看真实对象 Dog,走 Dog 版本,没有传参就用 Dog 里的默认值 9。 |
| 13 | delete p; |
D dtor | Animal 的析构函数是 virtual,所以用父类指针删 Dog 时,能先调用 Dog 的析构。 |
| 14 | delete p; |
A dtor | Dog 拆完,再拆它身体里的 Animal 父类部分。口诀:析构先子后父。 |
| 15 | main 结束,销毁 a |
A dtor | `a` 是前面切片得到的 Animal 对象,不是 Dog,所以只输出 Animal 析构。 |
| 16 | main 结束,销毁 d |
D dtor A dtor |
`d` 是完整 Dog 对象,退场时先 Dog,再 Animal。局部变量是倒序销毁:`a` 后出生,所以比 `d` 先走。 |
如果老师改题,盯这 4 个开关
1. 把
Animal a = d 改成 Animal& a = d:切片没了。2. 给
breathe 加 virtual:它就从“看工作牌”变成“看身份证”。3. 把
bar(const Animal&) 改成 bar(Animal):值传递会多一次拷贝。4. 去掉
virtual ~Animal():父类指针 delete 子类对象会变危险,析构顺序可能出问题。| 代码现象 | 你要说的人话 |
|---|---|
virtual void speak | 说话看真实身份:真 Dog 就用 Dog 的 speak。 |
void breathe | 呼吸不是 virtual,看牌面身份:Animal& 就用 Animal 的 breathe。 |
Animal a = d | 大盒子装小盒子,只留下父类那一片,叫对象切片。 |
virtual ~Animal() | 用父类指针 delete 子类对象时,能正确从子类开始拆。 |
第31题
static 成员
complex / abc:静态成员输出题
先抓关键词
static int w; 是全班共享一份,不是每个对象一份。样题里类名有点混乱,按意图理解为同一个类。
class abc {
public:
static int w;
int i;
abc() { i = ++w; }
};
int abc::w = 1;
int main() {
abc a, b, c;
cout << a.w << " " << b.i << endl;
}
✅ 输出
4 3| 步 | 代码正在做什么 | 此刻值 | 为什么 |
|---|---|---|---|
| 1 | int abc::w = 1; | w = 1 | static 成员先在类外初始化。它像班费,全班对象共用一份。 |
| 2 | abc a; | a.i = 2 | 构造里先 ++w,w 从 1 变 2,再给 a.i。 |
| 3 | abc b; | b.i = 3 | w 继续加,变成 3。 |
| 4 | abc c; | c.i = 4 | w 再加,变成 4。注意 a.w、b.w、c.w 看到的都是同一个 w。 |
| 5 | cout << a.w << " " << b.i | 4 3 | a.w 是共享的 w,最终为 4;b.i 是 b 出生时拿到的 3。 |
第32题
引用返回
函数调用放在等号左边
int z = 15;
int& f() { return z; }
int main() {
f() = 20;
cout << z;
}
✅ 输出
20
人话翻译
f() 返回的不是 z 的复印件,而是 z 本人。f() = 20; 就像直接写 z = 20;。
第34题
纯虚函数 + 多态
Oyster / VirginiaOyster 输出题
先抓关键词
virtual void print() const = 0; 说明 Oyster 是抽象类;但指针可以指向子类对象。baseClassPtr->print() 调用子类版本。
class Oyster {
public:
Oyster(string genusString) {
genus = genusString;
}
string getPhylum() const {
return "Mollusca";
}
virtual string getName() const {
return "Oyster class";
}
string getGenus() const {
return genus;
}
virtual void print() const = 0; // 纯虚函数:Oyster 是抽象类
private:
string genus;
};
class VirginiaOyster : public Oyster {
public:
VirginiaOyster() : Oyster("Crassostrea") {}
virtual string getName() const {
return "VirginiaOyster class";
}
virtual void print() const {
cout << "Phylum:" << getPhylum().c_str()
<< "\tGenus: " << getGenus().c_str()
<< "\tName: " << getName().c_str();
}
};
int main() {
VirginiaOyster oyster;
Oyster *baseClassPtr;
baseClassPtr = &oyster;
baseClassPtr->print();
cout << endl;
return 0;
}
✅ 输出
Phylum:Mollusca Genus: Crassostrea Name: VirginiaOyster class
先把这题翻译成人话
Oyster 是“牡蛎总类”,VirginiaOyster 是“弗吉尼亚牡蛎这个具体子类”。父类指针 Oyster *baseClassPtr 像一张通用标签:标签上写“牡蛎”,但它可以贴在真正的弗吉尼亚牡蛎对象上。调用 print() 时,因为 print 是虚函数,C++ 会看标签背后真正贴的是谁。
| 步 | 代码正在做什么 | 内部怎么变化 | 此刻输出 | 为什么 |
|---|---|---|---|---|
| 1 | VirginiaOyster oyster; |
先创建一个子类对象。因为子类继承父类,所以对象身体里有两层:里面有一层 Oyster,外面再套一层 VirginiaOyster。 |
无输出 | 这行只是“出生”,还没执行 cout。 |
| 2 | VirginiaOyster() : Oyster("Crassostrea") {} |
子类构造函数先喊父类构造函数:把字符串 "Crassostrea" 交给 Oyster。 |
无输出 | 构造函数里只是传值,没有打印语句。 |
| 3 | Oyster(string genusString) { genus = genusString; } |
父类私有成员 genus 被存成 Crassostrea。这就是后面 getGenus() 能拿到的值。 |
无输出 | genus 变了,但屏幕不会自动显示变量变化。 |
| 4 | Oyster *baseClassPtr; |
声明一个父类指针。现在它只是一个“能指向 Oyster 或子类对象的遥控器”,还没有指向任何对象。 | 无输出 | 声明变量不会输出。 |
| 5 | baseClassPtr = &oyster; |
&oyster 是取地址。父类指针现在指向这个 VirginiaOyster 对象的父类部分,但真实对象仍然是完整的子类对象。 |
无输出 | 赋值只是让指针“指过去”,没有 cout。 |
| 6 | baseClassPtr->print(); |
-> 表示“通过指针调用成员函数”。因为 print 前面有 virtual,C++ 不只看指针类型 Oyster*,还会看真实对象是 VirginiaOyster。 |
先不急,进入子类 print | 虚函数的核心:父类指针调用,实际跑子类版本。 |
| 7 | cout << "Phylum:" << getPhylum().c_str() |
getPhylum() 是父类普通函数,固定返回 "Mollusca"。.c_str() 是把 C++ 的 string 转成 C 风格字符串,方便输出。 |
Phylum:Mollusca | 这里第一次真正遇到 cout,所以屏幕开始出现内容。 |
| 8 | << "\tGenus: " << getGenus().c_str() |
\t 是制表符,作用像按一下 Tab。getGenus() 取出第 3 步存进去的 Crassostrea。 |
Genus: Crassostrea | 这不是新算出来的,是前面构造对象时已经存好的。 |
| 9 | << "\tName: " << getName().c_str() |
getName() 也是虚函数。真实对象是 VirginiaOyster,所以调用子类版本,返回 "VirginiaOyster class"。 |
Name: VirginiaOyster class | 如果没有子类重写,才会用父类的 "Oyster class"。 |
| 10 | cout << endl; |
前面输出都在同一行,最后这里换行。 | 换行 | endl 可以理解为“按回车”。 |
第35题
异常处理
try / throw / catch 输出题
class DivideZero {
public:
DivideZero() : out("EXCEPTION: Division by zero attempted.") {}
string display() const {
return out;
}
private:
string out;
};
double arithmetic(int n, int d) {
if (d == 0)
throw DivideZero(); // 遇到除数为 0,扔异常
return static_cast<double>(n) / d;
}
int main() {
try {
cout << arithmetic(24, 6) << endl;
cout << arithmetic(1, 3) << endl;
cout << arithmetic(9, 0.0) << endl;
}
catch (DivideZero &e) {
cout << e.display() << endl;
}
return 0;
}
✅ 输出
4
0.333333
EXCEPTION: Division by zero attempted.| 步 | 代码正在做什么 | 此刻输出 | 为什么 |
|---|---|---|---|
| 1 | arithmetic(24, 6) | 4 | 6 不是 0,正常返回 24 / 6。 |
| 2 | arithmetic(1, 3) | 0.333333 | 强转成 double,所以不是整数除法。具体小数位看环境,写 0.333333 通常够用。 |
| 3 | arithmetic(9, 0) | 先不输出除法 | d 是 0,直接 throw DivideZero,后面的 return 不执行。 |
| 4 | catch (DivideZero& e) | EXCEPTION... | catch 接住异常,输出 e.display()。 |