C++ 期末救命讲义 按样题考点整理
🧩 第2、5、6题样张

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;
}
✅ 样题输出
ClassA1
ClassB
ClassA copy
ClassA
invoke ClassA2
Delete ClassA
Done!
Delete ClassA
Delete ClassB
Delete ClassA

字幕版时间线:一行输出配一行原因

代码正在做什么 此刻输出 人话解释
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 变化
00 % 10 == 00
1010 的倍数10
2010 的倍数30
3010 的倍数60
4010 的倍数100
5010 的倍数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. 给 breathevirtual:它就从“看工作牌”变成“看身份证”。
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
代码正在做什么此刻值为什么
1int abc::w = 1;w = 1static 成员先在类外初始化。它像班费,全班对象共用一份。
2abc a;a.i = 2构造里先 ++w,w 从 1 变 2,再给 a.i。
3abc b;b.i = 3w 继续加,变成 3。
4abc c;c.i = 4w 再加,变成 4。注意 a.w、b.w、c.w 看到的都是同一个 w。
5cout << a.w << " " << b.i4 3a.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.
代码正在做什么此刻输出为什么
1arithmetic(24, 6)46 不是 0,正常返回 24 / 6。
2arithmetic(1, 3)0.333333强转成 double,所以不是整数除法。具体小数位看环境,写 0.333333 通常够用。
3arithmetic(9, 0)先不输出除法d 是 0,直接 throw DivideZero,后面的 return 不执行。
4catch (DivideZero& e)EXCEPTION...catch 接住异常,输出 e.display()。