🎯 前言:快速复习策略

基于老师最后一课讲义整理 · 覆盖所有考题类型

📋 总策略:先理解核心机制,再背诵固定答案

大题(代码追踪/编程)靠 理解,选择填空靠 背诵

🧠 需要理解(会变数字/变形式,靠理解机制拿分)

题号考点必须理解的机制关键词
2, 6 代码运行结果 构造/析构顺序 · 虚函数动态绑定 · 非虚静态绑定 · 对象切片 · 默认参数是静态绑定 virtual → 动态绑定 · Animal a = d → 切片 · 指针/引用不切片 · 析构逆序
34 纯虚函数追踪 纯虚函数 = 0 · 抽象类不能实例化 · 子类必须实现纯虚函数 · 基类指针调虚函数 virtual ... = 0 · 抽象类 · 动态绑定
35 异常处理追踪 throw 后直接跳到 catch · try 块内 throw 之后的代码不执行 · catch 匹配类型 try监控 · throw抛出 · catch捕获 · 跳转
31 静态成员追踪 static 成员属于类(全局只有一份)· 所有对象共享 · 必须类外定义 static · 共享一份 · 类名::变量
32 引用返回值 返回引用 = 返回变量本身(可出现在 = 左边)· 返回非引用 = 返回副本(右值) int& f() → 左值 · f() = x 合法
37 浅拷贝危害 编译器默认拷贝是浅拷贝 → 指针成员指向同一内存 → 析构两次崩溃 · 必须深拷贝 浅拷贝 · 深拷贝 · 内存泄漏 · 双重释放
3, 38, 39
40, 42, 43
编程题 模板写法 · 文件读写 ofstream/ifstream · 继承+虚函数编程 · String三大函数 · Stack实现 template <typename T> · ifstream · virtual · new[]/delete[] · 扩容翻倍

📝 需要背诵(死答案,不会变)

题号题型要背的答案记忆口诀
7构造调用时机A · 声明对象时调用声明即构造
8const 成员函数C · int f() constconst 放最后
9this 指针访问A · this->xthis 是指针用 ->
10不可重载运算符C · ?:四大天王:. .* :: ?:
11new 返回值A · 返回指向对象的指针new 返回指针
12拷贝构造时机D · 以上都是初始化/传参/返回 → 三种都触发
13非成员函数C · 友元函数友元不是成员
14inline 特性D · 提速+增体积空间换时间
15抽象类D · 不能实例化含纯虚 = 不能 new
16纯虚函数声明C · virtual void f() = 0;virtual + 参数 + = 0
17构造函数特性B · 不能有返回类型void 都不能写
18拷贝构造传参C · 只能引用传递值传递 → 无限递归
19运行时决定函数C · Dynamic binding动态绑定 = 虚函数机制
20模板关键字C · template模板以 template 开头
21指针赋值错误B · 基类→派生指针父不能转子
22不可重载函数C · 析构函数析构无参 → 无法重载
23析构定义A · A::~A()无返回无参数
24异常捕获B · catch 块catch 捕获处理
25new 失败异常B · bad_alloc分配失败 → bad_alloc
26静态成员特性A、C(多选)加 static + 类外引用加类名::
27引用参数含义C · 对象引用作参数A &a 是引用
28默认构造函数D · A和B都对两种默认构造
29引用 vs 指针D · 以上全是引用:非空/不可改绑/自动解引
30显式调基类C · Base::fun();类名::函数名()
33模板推导B · 5 和 5.2各自匹配 int / double
36野指针后果篡改动态内存,后果难料free 后 str != NULL 仍是原址
41私有成员访问p.height = 1.85 编译错误private 只能类内访问
🧠 构造先父后子,析构先子后父 · 对象传值切片,指针引用多态 · 虚函数动态绑,非虚静态绑

🧬 第 1 章 — 继承与多态

对应考题:第2题、第6题、第34题、第40题 | ⭐⭐⭐⭐⭐ 必考

1.1 先搞懂三个核心概念

什么是"多态"(Polymorphism)?

多态 = 同一个接口,不同的行为。

比如老师说"回答问题",每个学生回答的内容不同——接口一样("回答问题"),行为因人而异(具体回答)。

在 C++ 中,多态靠 virtual 虚函数 实现:通过父类指针或引用调用虚函数时,实际跑的是子类的版本。这就是动态绑定——运行时才决定调哪个函数。

Animal* p = new Dog();    // p 声明为 Animal*,但实际指向 Dog 对象
p->speak();               // speak 是 virtual → 运行时发现实际是 Dog → 调 Dog::speak()
// ↑ 这就是多态:同一个 p->speak() 调用,实际行为取决于 p 指向什么对象
概念含义通俗理解
继承
(Inheritance)
子类自动拥有父类的成员变量和函数儿子继承老爸的房子和财产(可以在此基础上加自己的东西)
虚函数
(Virtual Function)
virtual 关键字标记的函数。通过指针/引用调用时,运行哪个版本取决于实际对象类型,而不是指针/引用的类型遥控器上有"发声"按钮——按下去,电视出声,收音机也出声,但发出来的声音不同
动态绑定
(Dynamic Binding)
运行时根据对象实际类型决定调用哪个函数版本(也叫"后期绑定"、"运行时多态")考试时老师叫"交卷",你交你的,同桌交同桌的——具体交什么,看你是谁

1.2 构造/析构的黄金法则

规则:创建派生类对象时,父类部分必须先构造好,子类才能在其基础上构造。销毁时反过来,子类先清理自己的东西,父类再清理。

📦 创建 Dog 对象的过程
Step 1 Animal() 父类构造 — 先打好"地基"
Step 2 Dog() 子类构造 — 在上面建"房子"
先父 → 后子
💣 销毁 Dog 对象的过程
Step 1 ~Dog() 子类析构 — 先拆"房子"
Step 2 ~Animal() 父类析构 — 再拆"地基"
先子 → 后父(与构造完全相反)
⚠️ 重要前提:只有父类的析构函数声明为 virtual,通过父类指针 delete 时才会正确先调子类析构再调父类析构。否则只调父类析构 → 子类部分的内存泄漏。
Animal* p = new Dog();
delete p;  // 如果 ~Animal() 不是 virtual → 只调 ~Animal(),Dog 部分泄漏!
           // 如果 ~Animal() 是 virtual → 先调 ~Dog(),再调 ~Animal() ✅

1.3 对象切片 —— 考试最爱考的坑

对象切片(Object Slicing):把派生类对象按值赋给父类对象时,派生类多出来的成员被切掉,只保留父类部分。就像把一个圆(子类)塞进方的模具(父类)。

Dog d;              // d 是完整的 Dog 对象,包含 Animal 部分 + Dog 独有部分
Animal a = d;       // ⚠️ 对象切片!a 是从 d 拷贝构造而来
                    // a 只拷贝了 Animal 那部分,Dog 独有部分被"切掉"了
                    // a.speak() 调的是 Animal::speak,不是 Dog::speak
场景是否切片?多态还能生效吗?
Animal a = dogObj; 值拷贝✅ 切片❌ 多态失效
Animal& ref = dogObj; 引用绑定❌ 不切片✅ 多态生效
Animal* ptr = &dogObj; 指针指向❌ 不切片✅ 多态生效
void f(Animal a) 非引用传参✅ 切片❌ 多态失效

记忆:想多态 → 用指针或引用;值拷贝 → 切片,多态消失。

1.4 虚函数 vs 非虚函数 —— 一张表记清楚

调用方式virtual 虚函数非虚函数
通过对象调用 dog.speak()静态绑定 → 调自己版本静态绑定 → 调自己版本
通过指针调用 ptr->speak()动态绑定 → 调实际对象版本 🎯静态绑定 → 调指针类型版本
通过引用调用 ref.speak()动态绑定 → 调实际对象版本 🎯静态绑定 → 调引用类型版本

🔥 虚函数默认参数陷阱

虚函数的默认参数值是静态绑定的——看声明类型,不看实际类型。

class Animal {
public:
    virtual void speak(int n = 5) const { ... }   // 父类默认值 n=5
};
class Dog : public Animal {
public:
    void speak(int n = 9) const override { ... }  // 子类默认值 n=9(但有坑!)
};

Animal* p = new Dog();     // p 声明为 Animal*
p->speak();                // 不传参数 → 用谁的默认值?
// 函数体用 Dog 的(动态绑定)✅ → 输出 "D::speak"
// 默认值用 Animal 的(静态绑定)⚠️ → n=5 而不是 n=9
// 最终输出:D::speak 5(不是 9!)

规则:指针/引用是什么类型,默认值就用那个类型的。这里 p 是 Animal*,所以用 Animal 的 n=5。

📝 第2题 — 执行流程表

从上到下按执行顺序阅读,"正在执行"列对应下方源码中的步骤标记

class ClassA {
public:
    // 构造函数(Constructor):创建对象时自动调用
    ClassA() { cout << "ClassA" << endl; }        // 默认构造:无参数
    ClassA(int i) { cout << "ClassA" << i << endl; } // 有参构造:输出 ClassA + 数字
    ClassA(const ClassA& src) { cout << "ClassA copy" << endl; } // 拷贝构造
    ClassA& operator=(const ClassA& rhs) {          // 赋值运算符(本题没用到)
        cout << "call ClassA" << endl;
    }
    virtual ~ClassA() { cout << "Delete ClassA" << endl; } // 虚析构
    void invoke(int i = 7) const {                 // invoke 是非虚函数!注意:没有 virtual!
        cout << "invoke ClassA" << i << endl;     // 输出 invoke ClassA + 参数值
    }
};

class ClassB: public ClassA {      // ClassB 继承自 ClassA
public:
    ClassB() : ClassA(1) {         // 构造时先调 ClassA(1),再执行自己的构造体
        cout << "ClassB" << endl;
    }
    virtual ~ClassB() { cout << "Delete ClassB" << endl; } // 虚析构
    void invoke(int i = 9) const { // 同名函数,隐藏了 ClassA::invoke(非虚,所以是隐藏不是覆盖)
        cout << "invoke ClassB" << i << endl;
    }
};

void find(const ClassA& inKlep) {  // 形参 inKlep 是 ClassA 的引用
    ClassA theClassA;              // ← 第4步在此执行:创建局部对象
    inKlep.invoke(2);              // ← 第5步在此执行:invoke 不是虚函数!
}                                  // ← 第6步:theClassA 离开作用域,自动销毁

int main() {
    ClassB deri;                   // ← 第1步:创建 ClassB 对象
    ClassA base = deri;            // ← 第2步:拷贝构造(等号左边是新对象 → 不是赋值!)
    find(deri);                    // ← 第3步:调用 find,传引用(不切片)
    cout << "Done!" << endl;      // ← 第7步:打印 Done!
    return 0;                      // ← 第8步:main 结束,局部变量按逆序析构
}
#正在执行输出为什么(关键理解)
1 ClassB deri; ClassA1
ClassB
创建 ClassB 对象。先调用 ClassA(1)(初始化列表中指定了参数1)→ 输出 ClassA1
父类构造完毕后,执行 ClassB() 自身的构造体 → 输出 ClassB
2 ClassA base = deri; ClassA copy 这里 base 是新创建的对象 → 触发拷贝构造函数,不是赋值运算符!
从 deri(ClassB)拷贝到 base(ClassA)→ 对象切片,只拷贝 ClassA 部分。
3 find(deri); 调用 find 函数。实参 deri 绑定到形参 const ClassA& inKlep
因为是引用绑定,不触发拷贝构造,不切片。inKlep 虽然是 ClassA& 类型,但实际引用的仍然是原来的 ClassB 对象。
4 ClassA theClassA; (find 内) ClassA 在 find 函数内部创建一个局部 ClassA 对象 → 调用默认构造函数(无参)→ 输出 ClassA
5 inKlep.invoke(2); (find 内) invoke ClassA2 🔥 本题最关键一步!
invoke 函数没有 virtual 关键字 → 是非虚函数静态绑定
编译器看 inKlep 声明的类型是 const ClassA& → 调用 ClassA::invoke(2) → 输出 invoke ClassA2
📌 如果 invoke 是虚函数,这里会动态绑定到 ClassB::invoke(2),输出 invoke ClassB2。
6 find 结束 → theClassA 离开作用域 Delete ClassA 局部对象 theClassA 被销毁,调用 ~ClassA()
7 cout << "Done!"; Done! 直接输出字符串。
8 return 0; → main 结束 Delete ClassA
Delete ClassB
Delete ClassA
局部变量按构造的逆序析构(后构造的先销毁):
① base 析构 → ~ClassA() 输出 Delete ClassA
② deri 析构 → 虚析构 → 先 ~ClassB() 输出 Delete ClassB,再 ~ClassA() 输出 Delete ClassA

✅ 完整输出(按顺序)

ClassA1
ClassB
ClassA copy
ClassA
invoke ClassA2
Delete ClassA
Done!
Delete ClassA
Delete ClassB
Delete ClassA

核心教训invoke 没有 virtual 关键字 → 非虚函数 → 静态绑定 → 不管 inKlep 实际引用的是什么,只调 ClassA 的版本。

📝 第6题 — 最经典的多态综合题

这道题融合了虚函数、非虚函数、对象切片、虚析构、默认参数——全部考点。

class Animal {
public:
    Animal()    { cout << "A ctor" << endl; }       // 默认构造:输出 "A ctor"
    Animal(int n)   { cout << "A ctor " << n << endl; } // 有参构造:输出 "A ctor N"
    Animal(const Animal& src) { cout << "A copy ctor" << endl; } // 拷贝构造
    virtual ~Animal() { cout << "A dtor" << endl; }  // 虚析构(保证安全 delete)

    virtual void speak(int n = 5) const {            // 虚函数!默认值 n=5
        cout << "A::speak " << n << endl;
    }
    void breathe() const {                            // 非虚函数!
        cout << "A::breathe" << endl;
    }
};

class Dog : public Animal {        // Dog 继承 Animal
public:
    Dog() : Animal(3) {              // 初始化列表:先调 Animal(3)
        cout << "D ctor" << endl;  // 再执行自己的构造体
    }
    virtual ~Dog()  { cout << "D dtor" << endl; }   // 虚析构

    void speak(int n = 9) const override {           // 重写(override)父类虚函数
        cout << "D::speak " << n << endl;
    }
    void breathe() const {                            // 同名非虚函数,隐藏父类版本
        cout << "D::breathe" << endl;
    }
};

void bar(const Animal& a) {        // a 是 Animal 的引用(引用不切片!)
    Animal tmp;                    // ← 步骤4:创建局部 Animal 对象
    a.speak(1);                    // ← 步骤5:通过引用调虚函数
    a.breathe();                   // ← 步骤6:通过引用调非虚函数
}                                  // ← 步骤7:tmp 离开作用域

int main() {
    Dog d;                         // ← 步骤1~2:创建 Dog 对象(构造链)
    Animal a = d;                  // ← 步骤3:拷贝构造 → 对象切片!
    bar(d);                        // 调用 bar,d 以引用方式传入
    cout << "---" << endl;       // ← 步骤8:分隔线

    Animal* p = new Dog();         // ← 步骤9~10:在堆上创建 Dog 对象
    p->speak();                    // ← 步骤11:虚函数 + 默认参数
    delete p;                      // ← 步骤12~13:虚析构链

    return 0;                      // ← 步骤14~16:局部变量栈上析构
}
#正在执行输出为什么(关键理解)
1Dog d; → 先 Animal(3)A ctor 3Dog 构造,先执行初始化列表中的 Animal(3)(父类有参构造)。
2Dog d;Dog()D ctor父类构造完后,执行 Dog() 的构造函数体。
3Animal a = d;A copy ctor🔥 对象切片!左边是新对象 → 拷贝构造。d 是 Dog,a 是 Animal → 只拷贝 Animal 部分。
4bar(d)Animal tmp;A ctor进入 bar 函数,d 以引用方式传入(const Animal& a,不切片)。tmp 是局部 Animal 对象,默认构造。
5a.speak(1)(bar 内)D::speak 1a 是 Animal&,但 speak 是 virtual → 动态绑定 → 实际对象是 Dog → 调用 Dog::speak(1)。
6a.breathe()(bar 内)A::breathebreathe 是非虚函数静态绑定 → a 声明为 Animal& → 调 Animal::breathe。
7bar 结束 → tmp 析构A dtortmp 离开作用域 → ~Animal()。
8cout << "---";---分隔线,回到 main。
9new Dog()Animal(3)A ctor 3堆上分配 Dog → 先父类构造。
10new Dog()Dog()D ctor子类构造函数体。
11p->speak();D::speak 5🔥 默认参数陷阱! p 是 Animal* → 默认值用 Animal 的 n=5。speak 是虚函数 → 函数体动态绑定 Dog 的。所以输出 D::speak 5(不是 9!)。
12delete p;~Dog()D dtor虚析构!先调子类析构。
13delete p;~Animal()A dtor子类析构完后自动调父类析构。
14main 结束 → a 析构A dtor局部变量逆序析构。a 是 Animal(切片后的对象),普通析构。
15main 结束 → d 析构 → ~Dog()D dtord 是 Dog → 虚析构 → 先 ~Dog()。
16d 析构 → ~Animal()A dtor子类析构后自动调父类析构。

✅ 完整输出

A ctor 3
D ctor
A copy ctor
A ctor
D::speak 1
A::breathe
A dtor
---
A ctor 3
D ctor
D::speak 5
D dtor
A dtor
A dtor
D dtor
A dtor

五大考点总结

  • 虚函数动态绑定:步骤5 a.speak(1) 输出 Dog::speak(a 是引用)
  • 非虚函数静态绑定:步骤6 a.breathe() 输出 Animal::breathe
  • 对象切片:步骤3 Animal a = d; 拷贝构造,Dog 部分丢失
  • 默认参数静态绑定:步骤11 p->speak() 默认值用 Animal 的 n=5
  • 虚析构保证安全:步骤12~13 delete p 正确调用 ~Dog() → ~Animal()

📝 第34题 — 纯虚函数与抽象类

纯虚函数:用 = 0 声明的虚函数,没有函数体。
抽象类:包含至少一个纯虚函数的类。不能直接创建对象,必须被继承并实现所有纯虚函数后才能用。

class Oyster {
public:
    // 构造函数:初始化 genus 成员
    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;   // ← 纯虚函数!= 0 表示没有实现,子类必须实现
                                      // Oyster 因此成为抽象类,不能 Oyster o; ❌
private:
    string genus;
};

class VirginiaOyster : public Oyster {
public:
    // 构造时通过初始化列表调用 Oyster("Crassostrea")
    VirginiaOyster() : Oyster("Crassostrea") {}

    // 覆盖父类的 getName 虚函数
    virtual string getName() const { return "VirginiaOyster class"; }

    // 实现父类的纯虚函数 print —— 必须实现,否则 VirginiaOyster 也是抽象类
    virtual void print() const {
        cout << "Phylum:" << getPhylum().c_str()   // 调用继承的 getPhylum()
             << "\tGenus: " << getGenus().c_str()    // 调用继承的 getGenus()
             << "\tName: " << getName().c_str();     // 调用自己的 getName()(虚函数,动态绑定)
    }
};

int main() {
    VirginiaOyster oyster;                 // 创建 VirginiaOyster 对象(合法,它实现了所有纯虚函数)
    Oyster *baseClassPtr;                  // 声明基类指针
    baseClassPtr = &oyster;                // 基类指针指向派生类对象(向上转型,安全)
    baseClassPtr->print();                 // print 是虚函数 → 动态绑定 → 调 VirginiaOyster::print()
    cout << endl;
    return 0;
}
🔍 点击查看输出与解析

输出Phylum:Mollusca    Genus: Crassostrea    Name: VirginiaOyster class

  • print() 是纯虚函数,被 VirginiaOyster 实现
  • getName() 是虚函数 → 动态绑定到 VirginiaOyster::getName()
  • getPhylum()getGenus() 是非虚函数 → 直接继承 Oyster 的版本
  • baseClassPtr->print() 通过基类指针调用 → 多态生效

✍️ 第40题 — 继承 + 虚函数编程

定义 computer 和 Macintosh 类。关键要求:通过 computer* 指针调用 print() 时,必须输出 Macintosh 的完整信息(包括 color)。

这要求 print 必须是 virtual —— 否则永远调 computer 的版本。

class computer {
protected:                     // protected:派生类可以访问,外部不能
    string name;               // 名字成员

public:
    computer(string n) : name(n) {}    // 构造函数:初始化 name

    virtual void print() const {       // ⚠️ 必须是 virtual!否则 p->print() 永远调这个
        cout << "name: " << name << endl;
    }

    virtual ~computer() {}             // ⚠️ 基类析构必须 virtual(好习惯)
};

class Macintosh : public computer {
    string color;                      // 派生类独有的成员

public:
    // 构造函数:先调 computer(n) 初始化 name,再初始化 color
    Macintosh(string n, string c) : computer(n), color(c) {}

    void print() const override {      // override 关键字:确认这是重写父类的虚函数
        cout << "name: " << name << endl;     // name 从父类继承(protected 可访问)
        cout << "color:" << color << endl;      // color 是自己的成员
    }
};

int main() {
    computer *p;                                   // 声明基类指针
    Macintosh imac("Joe's IMAC", "Blue");          // 创建 Macintosh 对象
    p = &imac;                                     // 基类指针指向派生类对象
    p->print();                                    // print 是 virtual → 动态绑定 → 调 Macintosh::print()
    return 0;
}

💡 为什么 print 必须是 virtual?

如果不加 virtualp->print() 中 p 是 computer* 类型 → 静态绑定 → 永远调 computer::print() → 只输出 name,color 永远不会出现。

加上 virtual:运行时发现 p 实际指向 Macintosh → 动态绑定 → 调 Macintosh::print() → name 和 color 都输出。

这就是多态的核心价值:通过基类指针/引用,调用同一个接口,得到不同的行为。

📋 第1章自测

以下代码输出什么?(提示:f() 是虚函数,g() 非虚)
class Base {
public:
    Base() { cout << "B"; }           // 构造:输出 B
    virtual ~Base() { cout << "~B"; } // 虚析构:输出 ~B
    virtual void f() { cout << "Bf"; } // 虚函数
    void g() { cout << "Bg"; }         // 非虚函数
};
class Derived : public Base {
public:
    Derived() { cout << "D"; }         // 构造:先调 Base() 输出 B,再输出 D
    ~Derived() { cout << "~D"; }       // 析构:先输出 ~D,再自动调 ~Base() 输出 ~B
    void f() override { cout << "Df"; } // 重写虚函数
};
int main() {
    Base* p = new Derived();  // 创建 Derived → B D
    p->f();                   // f() 是 virtual → 动态绑定 → Df
    p->g();                   // g() 非虚 → 静态绑定 → Bg
    delete p;                 // 虚析构 → ~D ~B
}

🏗️ 第 2 章 — 构造、析构、拷贝(三大函数)

对应考题:第12、17、18、23、28、37、42题 | ⭐⭐⭐⭐⭐

2.1 什么是"三大函数"?

每一个 C++ 类都至少需要关注三个特殊函数:构造函数(怎么创建)、析构函数(怎么销毁)、拷贝控制(怎么复制)。如果你不写,编译器会帮你自动生成——但自动生成的版本只做浅拷贝,有指针成员时会造成严重问题。

2.2 编译器悄悄帮你生成的代码

函数编译器自动生成版本做了什么类里有指针成员时安全吗?
默认构造函数什么都不做(或调成员的默认构造)✅ 还行
拷贝构造函数浅拷贝——把每个成员逐字节拷贝❌ 两个对象的指针指向同一块内存
赋值运算符浅拷贝——把每个成员逐字节赋值❌ 同上 + 旧内存没释放(泄漏)
析构函数什么都不做(或调成员的析构)❌ 指针指向的堆内存没人释放

2.3 Rule of Three(三法则)

规则:如果你的类需要自定义析构函数、拷贝构造函数、赋值运算符中的任意一个,那么你几乎一定需要全部三个

为什么? 需要自定义析构 → 说明你管理了资源(如堆内存)→ 拷贝时不能简单浅拷贝 → 必须深拷贝 → 所以拷贝构造和赋值运算符也要自定义。

这三个函数是绑定的——写一个就要写三个。

2.4 浅拷贝 vs 深拷贝 —— 可视化对比

❌ 浅拷贝(编译器默认行为)
对象 obj1
ptr地址 0x1000
堆内存 0x1000
"hello"
对象 obj2
ptr地址 0x1000

两个对象的 ptr 指向同一块内存!
析构时 obj1 先释放 → obj2 再释放(double free)→ 💥 崩溃

✅ 深拷贝(你手动实现)
对象 obj1
ptr地址 0x1000
堆内存 0x1000
"hello"
对象 obj2
ptr地址 0x2000
堆内存 0x2000
"hello"

各自分配了独立的内存副本!
析构时各自释放各自的内存 → ✅ 安全

浅拷贝 = 只拷贝指针的值(地址)→ 两个指针指向同一个地方。
深拷贝 = 分配新内存 + 拷贝内容 → 两个指针指向各自的副本。

2.5 String 类完整实现 —— 必背!

⚠️ 这是每年必考的手写代码!四个函数必须能手写出来。

class String {
private:
    char *m_data;          // 指向堆上动态分配的字符数组(这才是真正的字符串)

public:
    // ========== ① 普通构造函数 ==========
    // 作用:根据 C 风格字符串创建 String 对象
    String(const char *str = NULL) {   // 默认参数 NULL:也充当了默认构造函数
        if (str == NULL) {             // 如果传入 NULL 或没传参
            m_data = new char[1];      // 分配 1 字节
            m_data[0] = '\0';          // 空字符串(只有结束符)
        } else {
            m_data = new char[strlen(str) + 1];   // +1 是为了 '\0' 结束符
            strcpy(m_data, str);                  // 把 str 内容拷贝到 m_data
        }
    }

    // ========== ② 拷贝构造函数 ==========
    // 作用:用一个已存在的 String 对象创建新的 String(深拷贝!)
    // 参数必须是 const 引用(不能按值传递,否则会无限递归)
    String(const String &other) {
        m_data = new char[strlen(other.m_data) + 1]; // 分配新内存(不是共享!)
        strcpy(m_data, other.m_data);                 // 拷贝内容
    }

    // ========== ③ 赋值运算符 ==========
    // 作用:把一个 String 对象的内容赋给另一个已经存在的 String
    // 四步走:一看(自赋值)→ 二清(释放旧)→ 三拷(分配+拷贝)→ 四返(return *this)
    String& operator=(const String &other) {
        // 第1步:防止自赋值(s = s 这种操作)
        if (this == &other) return *this;

        // 第2步:释放自己原来持有的旧内存(否则会泄漏)
        delete[] m_data;   // 注意是 delete[] 不是 delete(因为是 char 数组)

        // 第3步:分配新内存并拷贝
        m_data = new char[strlen(other.m_data) + 1];   // 新内存
        strcpy(m_data, other.m_data);                   // 拷贝内容

        // 第4步:返回 *this(支持链式赋值 a = b = c)
        return *this;
    }

    // ========== ④ 析构函数 ==========
    // 作用:对象销毁时释放它持有的堆内存
    ~String() {
        delete[] m_data;   // new[] 必须配 delete[]!否则 undefined behavior
    }
};
📝 赋值运算符四步口诀:一看(检查自赋值)→ 二清(释放旧内存)→ 三拷(分配新内存+拷贝内容)→ 四返(return *this)

为什么拷贝构造函数必须用引用传参?

// ❌ 错误写法:按值传递(pass-by-value)
String(const String other) { ... }
// 问题:为了把实参传给形参 other,编译器需要拷贝实参
// → 拷贝需要调用拷贝构造函数 → 又要传形参 → 又要拷贝 → 无限递归!

// ✅ 正确写法:按引用传递(pass-by-reference)
String(const String &other) { ... }
// 引用不触发拷贝——只是给原对象起了个别名

new[] 必须配 delete[],new 配 delete

int* a = new int;        // 单个 int
delete a;                // ✅ 配 delete

int* b = new int[10];    // int 数组
delete[] b;              // ✅ 配 delete[](不能漏掉 [])

char* c = new char[100]; // char 数组
delete c;                // ❌ 错误!应该用 delete[] c
// 后果:只释放了第一个元素,剩下的 99 个字节泄漏

2.6 第37题 — 浅拷贝导致的内存泄漏 + 双重释放

问题代码:Person 类有指针成员 pFirstName,但没有写拷贝构造和赋值运算符。编译器生成默认版本 → 浅拷贝。

Person Mike("Mike", 25);   // Mike.pFirstName 指向 "Mike"
Person John("John", 30);   // John.pFirstName 指向 "John"

Mike = John;               // ⚠️ 默认赋值运算符 = 浅拷贝!
// 发生了什么:
// 1. Mike.pFirstName 现在指向 "John"(和 John.pFirstName 一样)
// 2. 原来 Mike.pFirstName 指向的 "Mike" 没有被释放 → 内存泄漏!
// 3. 两个对象的 pFirstName 指向同一块内存 "John"
// 4. 析构时两次 delete[] 同一块内存 → 双重释放 → 程序崩溃!
赋值前 ✓
Mike
pName"Mike\0"
John
pName"John\0"
→ Mike = John →
赋值后 ❌
Mike
pName"John\0"
⚠️ 旧 "Mike" 没人释放 → 内存泄漏
John
pName"John\0"

两个 pName 指向同一块内存
析构时 double free → 💥 崩溃

✅ 解决方案(重载赋值运算符)

Person& Person::operator=(const Person& rhs) {
    if (this == &rhs) return *this;          // 防止自赋值
    delete[] pFirstName;                      // 释放旧内存(解决泄漏)
    pFirstName = new char[strlen(rhs.pFirstName) + 1]; // 分配新内存
    strcpy(pFirstName, rhs.pFirstName);       // 拷贝内容(深拷贝)
    return *this;
}

📋 第2章自测

拷贝构造函数如何接收参数?为什么?

📝 第 3 章 — 选择填空题详解

对应考题:第7~33题 | 每道题含原题、选项、答案、详尽解析

👆 速查卡片 · 下拉看每道题的完整解析 👇

题7 · 下列哪个叙述是正确的(A

原题:下列哪个叙述是正确的( )

  1. 构造函数在声明对象的时候被调用
  2. 构造函数在使用对象的时候被调用
  3. 构造函数在声明类的时候被调用
  4. 构造函数在使用类的时候被调用

✅ 答案:A

为什么选 A?
构造函数在创建对象(声明/定义对象)时自动调用。例如 Dog d; 这行代码执行时,构造函数立刻运行。

其他选项为什么错?

  • B 错:"使用对象"是指调用它的成员函数或访问成员变量——此时对象早已构造好了。
  • C 错:类是编译期的概念,只是一个模板/蓝图。你在写 class Foo { }; 时不会有构造函数运行。
  • D 错:同 C,"使用类"没有意义——你用的是对象,不是类。
题8 · 下列正确声明const函数的是(C

原题:下列正确声明const函数的是( )

  1. const int ShowData(void) { /* statements */ }
  2. int const ShowData(void) { /* statements */ }
  3. int ShowData(void) const { /* statements */ }
  4. Both A and B

✅ 答案:C

为什么选 C?
const 成员函数的关键是 const 放在函数签名的最末尾(在参数列表右括号后面)。它承诺这个函数不会修改任何成员变量。

写法含义
int f() const✅ const 成员函数,承诺不修改成员
const int f()❌ 不是 const 成员函数,是返回值类型为 const int
int const f()❌ 同 A,int const = const int
题9 · 使用this指针访问数据成员正确的是(A

原题:下列使用this指针访问类的数据成员中,正确的是( )

  1. this->x
  2. this.x
  3. *this.x
  4. *this-x

✅ 答案:A

为什么选 A?
this 是指针(不是引用、不是对象),访问成员必须用箭头 ->

  • B 错. 用于对象/引用访问成员,this 是指针不能用 .
  • C 错. 优先级高于 *,所以 *this.x 等价于 *(this.x),而 this.x 本身就是错的。
  • D 错:语法错误,毫无意义。
题10 · 哪个运算符不能被重载(C

原题:下列哪个运算符不能被重载( )

  1. []
  2. ->
  3. ?:
  4. *

✅ 答案:C

不可重载的运算符只有 4 个(四大天王):

运算符名称为什么不能重载
.成员访问语言核心机制,重载会破坏语义
.*成员指针访问同上
::作用域解析不是运算符,是语法符号
?:三元条件它是唯一的三元运算符,重载太复杂

[]->* 都可以重载。

题11 · 关于new运算符说法正确的是(A

原题:关于new运算符,下面说法正确的是( )

  1. 返回指向已创建的对象的指针
  2. 创建名为new的对象
  3. 获取一个新类的内存
  4. 告知为对象分配了多少内存

✅ 答案:A

new 在堆上分配内存 + 调用构造函数,然后返回指向这块内存的指针

int* p = new int(42);    // p 是指针,指向堆上值为42的int
Dog* d = new Dog();      // d 是指针,指向堆上新构造的Dog对象
  • B 错new 是运算符,不是对象名。
  • C 错:是在堆上分配内存给对象,不是"获取一个新类"。
  • D 错new 不返回分配了多少内存,它只返回指针。
题12 · 拷贝构造函数调用时机(D

原题:以下哪种情况将执行拷贝构造函数( )

  1. 当用类的一个对象去初始化该类的另一个对象时
  2. 将一个对象作为实参传递给一个非引用类型的形参
  3. 从一个返回类为非引用的函数返回一个对象时
  4. 以上都是

✅ 答案:D — 三种都会触发

// 场景A:用对象初始化新对象
ClassA b = a;          // 拷贝构造
ClassA b(a);           // 也是拷贝构造

// 场景B:非引用传参
void func(ClassA obj) { }  // 传参时拷贝实参到形参
func(a);                    // ← 触发拷贝构造

// 场景C:返回非引用对象
ClassA getObj() {
    ClassA tmp;
    return tmp;             // ← 触发拷贝构造(可能被编译器优化RVO)
}

判断技巧:只要新生成一个对象,并且源是同类对象 → 就是拷贝构造。注意区分:ClassA b = a;(b是新对象)是拷贝构造,而 b = a;(b已存在)是赋值。

题13 · 哪种函数不是类的成员函数(C

原题:下列哪种函数不是类的成员函数( )

  1. 构造函数
  2. 析构函数
  3. 友元函数
  4. 拷贝构造函数

✅ 答案:C

友元函数不是成员函数。友元函数是在类外部定义的普通函数,只是被"授权"可以访问类的私有成员。它没有 this 指针,不能被对象调用。

class MyClass {
    friend void show(MyClass& obj);  // 声明为友元,但定义在类外
};
// show 是普通全局函数,不是 MyClass 的成员!

构造函数、析构函数、拷贝构造函数都是类的特殊成员函数。

题14 · inline函数叙述正确的是(D

原题:下列关于inline函数叙述正确的是( )

  1. 提高运行速度
  2. 降低运行速度
  3. 增加代码大小
  4. Both A and C

✅ 答案:D — A和C都对

inline 的本质:编译器把函数体代码直接"展开"到调用处,省去函数调用指令(压栈、跳转、返回)的开销。

✅ 优点(A)❌ 代价(C)
省去函数调用开销 → 提速函数体被复制多份 → 代码膨胀

所以是"以空间换时间"。适合短小、频繁调用的函数。

题15 · 抽象类(D

原题:抽象类( )

  1. 最多包含一个纯虚函数
  2. 可以实例化为对象
  3. 不能有抽象派生类
  4. 不能被实例化为对象

✅ 答案:D

抽象类 = 包含至少一个纯虚函数的类。因为有未实现的函数,所以不能直接创建对象。

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};
Shape s;  // ❌ 编译错误!抽象类不能实例化
  • A 错:抽象类可以有多个纯虚函数,没有上限。
  • B 错:不能实例化是抽象类的定义性特征。
  • C 错:抽象类派生出来的子类如果不实现所有纯虚函数,它自己也是抽象类——可以有抽象派生类。
题16 · 纯虚函数声明方式(C

原题:下列声明纯虚函数的正确方式是( )

  1. virtual void Display(void){0};
  2. virtual void Display = 0;
  3. virtual void Display(void) = 0;
  4. void Display(void) = 0;

✅ 答案:C

纯虚函数 = virtual + 函数声明 + = 0

virtual void Display(void) = 0;  // ✅ 正确

// ❌ 错误写法分析:
virtual void Display(void){0};   // {0} 是函数体,这不是纯虚函数!
virtual void Display = 0;         // 缺少参数列表 ()
void Display(void) = 0;           // 缺少 virtual 关键字

记忆virtual 返回类型 函数名(参数) = 0; — 必须有 virtual,必须有 = 0

题17 · 构造函数说法正确的是(B

原题:下面关于类的构造函数的说法正确的是( )

  1. 一个类只能有一个构造函数
  2. 不能有返回类型
  3. 可以为void类型
  4. 不能使用缺省参数

✅ 答案:B

构造函数没有返回类型——连 void 都不能写!

class Foo {
public:
    Foo();           // ✅ 正确
    void Foo();      // ❌ 错误!构造函数不能有返回类型
};
// 如果写了 void Foo(),编译器会把它当成普通成员函数!
  • A 错:构造函数可以重载,可以有多个(不同参数)。
  • C 错:构造函数没有任何返回类型。
  • D 错:构造函数可以使用默认参数,如 Foo(int x = 0)
题18 · 拷贝构造函数如何接收参数(C

原题:拷贝构造函数如何接收参数( )

  1. either pass-by-value or pass-by-reference
  2. only pass-by-value
  3. only pass-by-reference
  4. only pass by address

✅ 答案:C — 只能引用传递

为什么不能值传递? 如果拷贝构造函数按值传递:

// ❌ 如果这样写:
String(const String other) { ... }  // 按值传递!
// 为了把实参拷贝到形参 other,又需要调用拷贝构造函数
// → 又进入拷贝构造 → 又要拷贝 → 无限递归!

所以 C++ 标准规定:拷贝构造函数的参数必须是 const ClassName&

为什么不能地址传递(D)? 地址传递就是传指针,String(String* other) 不符合拷贝构造的语法要求,且调用方式会变成 String b(&a) 而非自然的 String b(a)

题19 · 运行时决定所调用的函数(C

原题:下列哪个概念意味着在运行时决定所调用的函数( )

  1. Data hiding
  2. Dynamic Typing
  3. Dynamic binding
  4. Dynamic loading

✅ 答案:C — Dynamic binding(动态绑定)

术语中文含义
Data hiding数据隐藏用 private 封装数据
Dynamic Typing动态类型变量类型在运行时确定(如Python)
Dynamic binding动态绑定运行时决定调用哪个版本的函数(虚函数机制)
Dynamic loading动态加载运行时加载库/DLL
题20 · 函数模板定义关键词(C

原题:所有的函数模板定义都以( )关键词开头

  1. class
  2. virtual
  3. template
  4. operator

✅ 答案:C — template

template <typename T>   // ← 以 template 开头
T max(T a, T b) {
    return a > b ? a : b;
}

A 是干扰项:类模板也以 template 开头,class 可以在 <> 里替代 typename —— 但模板定义本身永远以 template 开头。

题21 · 哪种赋值产生编译错误(B

原题:下列哪种赋值会产生编译错误( )

  1. 将基类对象的地址赋值给基类指针
  2. 将基类对象的地址赋值给派生类指针
  3. 将派生类对象的地址赋值给基类指针
  4. 将派生类对象的地址赋值给派生类指针

✅ 答案:B

class Base { };
class Derived : public Base { };

Base b;  Derived d;

Base* pb = &b;     // A✅ 同类指针
Derived* pd = &b;   // B❌ 编译错误!基类对象不是派生类
Base* pb2 = &d;     // C✅ 向上转型,合法(派生类是基类)
Derived* pd2 = &d;   // D✅ 同类指针

原因:基类对象"不完整"——它缺少派生类新增的成员。Derived 指针期望访问的成员在 Base 对象中不存在。

口诀:子类指针不能指向父类对象("你爸不是你"),父类指针可以指向子类对象("你是你爸的孩子")。

题22 · 不能重载的函数(C

原题:下列函数中,( )不能重载

  1. 成员函数
  2. 非成员函数
  3. 析构函数
  4. 构造函数

✅ 答案:C — 析构函数不能重载

重载 = 同名不同参。析构函数没有参数,也没有返回类型,所以天然无法重载。每个类只有一个析构函数:~ClassName()

构造函数可以有多个(不同参数),普通成员函数和非成员函数都可以重载。

题23 · 析构函数正确定义(A

原题:下面( )选项是对类的析构函数的正确定义

  1. A::~A()
  2. void A::~A(参数)
  3. A::~A(参数)
  4. void A::~A()

✅ 答案:A

析构函数:无返回类型(包括 void)、无参数。

class A {
public:
    ~A();   // 声明
};
A::~A() {   // ✅ 定义:无返回类型,无参数
    // 清理代码
}
  • B/D 错:析构函数不能有返回类型(void 也不行)。
  • C 错:虽然没有返回类型,但析构函数不能有参数。
题24 · 异常处理中捕获异常的代码块(B

原题:( )封装捕获到异常时所执行的代码

  1. try块
  2. catch块
  3. throw
  4. exception

✅ 答案:B

关键字作用
try包围可能抛出异常的代码
catch捕获并处理异常
throw抛出异常
try {
    // 可能抛异常的代码  ← try 块
} catch (ExceptionType& e) {
    // 捕获后执行的代码  ← catch 块(本题答案)
}
题25 · new失败抛出的异常(B

原题:new操作失败时抛出的特殊异常是( )

  1. exception
  2. bad_alloc
  3. DivideByZero
  4. no_exception

✅ 答案:B — bad_alloc

try {
    int* p = new int[999999999999];  // 内存不够
} catch (bad_alloc& e) {               // ← 捕获 bad_alloc
    cout << "内存分配失败!";
}
  • Aexception 是所有异常的基类,太泛。
  • CDivideByZero 不是 C++ 标准异常(整数除零是未定义行为)。
  • D:C++ 标准库没有 no_exception 这个异常。
题26 · 静态数据成员正确的是(A、C

原题:关于静态数据成员下列正确的是( )(多选)

  1. 说明静态数据成员时前边要加修饰符static
  2. 静态数据成员可以在类体内进行初始化
  3. 在类外引用公有静态的数据成员时,必须在静态数据成员前加类名和作用域运算符
  4. 静态数据成员不是所有对象所共享的

✅ 答案:A、C

class MyClass {
public:
    static int count;  // A✅ 声明时加 static
};
int MyClass::count = 0;  // C✅ 类外引用:类名::成员

// B❌ 为什么不能在类体内初始化?
// 静态成员属于类,不属于对象,必须在类外定义。
// 例外:const static 整型可以在类内初始化(C++11+)
class Foo {
    const static int x = 10;  // 特例,可以
};

// D❌ 静态成员是所有对象共享的!这是它的核心特性。
题27 · A &a 的含义(C

原题:已知类A中一个成员函数说明如下:void Set(A &a); 其中,A &a 的含义是( )

  1. 定义指向类A的指针为a
  2. 将a的地址值赋给变量Set
  3. a是类A的对象引用,用来做函数Set()的参数
  4. 变量A与a按位相与作为函数Set()的参数

✅ 答案:C

A &a 声明了一个 A 类型的引用,作为函数参数。

void Set(A &a) {
    // a 是引用,操作 a 就是操作实参本身
    // 不需要 *,不像指针
}
  • A 错& 在类型后面是引用,不是指针。指针是 A* a
  • B 错:没有"赋给Set"这回事,Set 是函数名。
  • D 错:按位与运算符不会出现在函数参数声明中。
题28 · 默认构造函数是(D

原题:默认构造函数是( )

  1. 构造函数的参数均为默认参数
  2. 当没有提供构造函数时,编译器提供的构造函数
  3. 不执行任何初始化操作
  4. A和B

✅ 答案:D — A和B都对

"默认构造函数"有两种形式

形式示例说明
编译器自动生成class Foo { }; // 没写构造编译器提供一个无参的
你写的全默认参数Foo(int x = 0) { }所有参数有默认值,可无参调用

⚠️ 陷阱:只要你自己写了任意一个构造函数(哪怕是有参的),编译器就不再自动提供默认构造函数。此时必须显式写 Foo() { }

  • C 错:默认构造也可以执行初始化,比如默认参数构造可以赋初值。
题29 · 引用与指针的区别(D

原题:引用与指针的区别( )

  1. 引用不能为null
  2. 引用一旦建立不能更改
  3. 引用无需显式解引用
  4. 以上全是

✅ 答案:D — 全部正确

特性引用指针
可为 null❌ 不允许int* p = nullptr;
可改指向❌ 绑定后不能改绑定p = &b;
解引用❌ 自动,直接用 ref✅ 需要 *ptr
int x = 10;
int& r = x;      // 引用:r 就是 x 的别名
r = 20;           // 等价于 x = 20,无需 *

int* p = &x;     // 指针
*p = 30;          // 需要 * 解引用
p = &y;          // 可以指向别的变量
// r = y;        // ❌ 不能改引用绑定!这会把 y 的值赋给 r(即x)
题30 · 显式调用基类函数(C

原题:应在下列程序划线处填入的正确语句是( )

#include <iostream>
class Base{
public:
    void fun(){ cout << "Base::fun" << endl; }
};
class Derived : public Base{
    void fun(){
        ____________ //显式调用基类的函数fun()
        cout << "Derived::fun" << endl;
    }
};
  1. fun();
  2. Base.fun();
  3. Base::fun();
  4. Base->fun();

✅ 答案:C — Base::fun()

在派生类中显式调用被隐藏的基类函数,用 类名::函数名()

  • A 错fun(); 会调用 Derived::fun() 自身 → 无限递归。
  • B 错Base.fun(). 只能用于对象/引用,Base 是类名不是对象。
  • D 错Base->fun() 毫无意义的语法。
题31 · static成员代码追踪(B · 4 3

原题:以下程序输出为( )(代码有笔误,abc 应为 complex

class complex {
public:
    static int w;         // 静态成员:所有对象共享一份
    int i;                // 普通成员:每个对象独立
    complex() { i = ++w; }
};
int complex::w = 1;       // 类外初始化,w = 1

int main() {
    complex a, b, c;      // 连续构造 3 个对象
    cout << a.w << " " << b.i << endl;
}
  1. 3 1
  2. 4 3
  3. 4 1
  4. 3 3

✅ 答案:B — 4 3

逐步追踪

执行w 值(共享)i 值
complex::w = 1;1
complex a; → w=++w=22a.i = 2
complex b; → w=++w=33b.i = 3
complex c; → w=++w=44c.i = 4

a.w 输出最终值 4(所有对象看到同一个 w),b.i 输出 3
本质w 属于类(全局区只有一份),i 属于每个对象(栈上各有各的)。

题32 · 引用返回值(D · 20

原题:以下程序输出为( )

int z = 15;
int& f() { return z; }   // 返回 z 的引用(别名)
int main() {
    f() = 20;             // 等价于 z = 20
    cout << z;
    return 0;
}
  1. 15
  2. z的地址
  3. 编译错误
  4. 20

✅ 答案:D — 20

f() 返回 int&(引用),意味着 f() 就是 z 本身。所以 f() = 20; 等价于 z = 20;

核心:返回引用的函数调用可以出现在赋值号左边(作为左值)。
如果返回类型是 int(非引用),f() = 20 会编译错误,因为返回值是临时右值。

题33 · 函数模板调用返回值(B · 5 和 5.2

原题:给定下述函数模板,调用 maximum(2, 5)maximum(2.3, 5.2) 的返回值是( )

template <class T>
T maximum(T value1, T value2) {
    if (value1 > value2) return value1;
    else return value2;
}
  1. 5 和 类型不匹配错误
  2. 5 和 5.2
  3. 2 和 2.3
  4. 两条错误信息

✅ 答案:B — 5 和 5.2

模板自动推导

  • maximum(2, 5) → 两个实参都是 intT = int → 返回较大的 5(int)
  • maximum(2.3, 5.2) → 两个实参都是 doubleT = double → 返回较大的 5.2(double)

注意:两个调用分别实例化,类型各自匹配,互不干扰。不存在类型不匹配!
但如果写 maximum(2, 5.2) 就会编译错误——两个参数类型不同,T 无法推导。

📇 8 张速记卡片
📇 构造
声明时调用 · 无返回类型 · 可重载 · 可用默认参数
📇 析构
无返回无参数 · 不可重载 · virtual 析构保证安全
📇 拷贝构造
只能 const & · 初始化新对象时调用 · 传值/返回也触发
📇 const 成员
const 在函数签名最后 · 承诺不修改成员
📇 静态成员
类内声明加 static · 类外定义 · 所有对象共享
📇 不可重载
. .* :: ?: · 析构函数
📇 抽象类
含纯虚 = 0 · 不能实例化
📇 异常
try-catch · new 失败抛 bad_alloc

💻 第 4 章 — 编程题实战

对应考题:第3、38、39、40、42、43题 | ⭐⭐⭐⭐

4.1 第3题:输入验证 + 求平方

题目:从键盘输入一个整数,求它的平方。要求做容错处理(检查输入的是否真的是数字)。

解题思路:读入字符串 → 逐字符检查是否全为数字 → 转换 → 计算。不能直接用 cin >> int,因为那不会检查输入是否正确。

#include <iostream>   // cin, cout:输入输出
#include <string>     // string 类型:存储用户输入
#include <cctype>     // isdigit():判断字符是否为数字 '0'~'9'
using namespace std;

int main() {
    string input;                              // 用字符串接收输入(而不是 int)
    cout << "请输入一个整数:";
    cin >> input;                              // 用户输入存到 input

    // === 检查1:是否为空 ===
    if (input.empty()) {                       // input.empty() 检查字符串是否有内容
        cout << "错误:输入为空!" << endl;
        return -1;                             // 非0返回值表示程序异常退出
    }

    // === 检查2:处理负号 ===
    // 如果第一个字符是 '-',说明是负数,从第二个字符开始检查
    int start = (input[0] == '-') ? 1 : 0;     // 三元运算符:有负号则 start=1
    if (start == 1 && input.length() == 1) {   // 只有一个 '-' 没有数字
        cout << "错误:请输入有效整数!" << endl;
        return -1;
    }

    // === 检查3:逐字符判断是否全为数字 ===
    for (int i = start; i < input.length(); i++) {
        if (!isdigit(input[i])) {              // isdigit() 检查字符是否为 '0'~'9'
            cout << "错误:包含非数字字符!" << endl;
            return -1;                         // 发现非数字立即退出
        }
    }

    // === 所有检查通过,安全转换 ===
    int num = stoi(input);                     // stoi: string → int 转换
    cout << num << " 的平方是:" << num * num << endl;
    return 0;                                  // 0 表示程序正常结束
}

关键知识点

  • isdigit(ch):标准函数,判断字符 ch 是不是 '0''9' 之间的数字字符。需要 #include <cctype>
  • stoi(str):C++11 函数,把 string 转成 int。如果字符串不是合法整数会抛异常。
  • 为什么先读字符串再检查? 因为 cin >> int 遇到非数字会静默失败,不告诉你出了问题。
  • empty():string 的成员函数,返回 true 表示字符串长度为 0。

4.2 第38题:数组最大值模板函数

题目:编写求数组最大值的模板函数。
提示:模板函数 = 把类型当作参数,写一次代码,int、char、double 都能用。

template <typename T>       // template 关键字 + 类型参数 T(就像一个占位符)
T arrMax(T arr[], int n) { // 返回类型 T、参数类型 T 都由调用者决定
    T maxVal = arr[0];     // 假设第一个元素是最大值
    for (int i = 1; i < n; i++) {   // 从第二个元素开始遍历
        if (arr[i] > maxVal) {      // 如果当前元素更大
            maxVal = arr[i];        // 更新最大值
        }
    }
    return maxVal;          // 遍历结束,返回找到的最大值
}

// ===== 调用示例 =====
int main() {
    int arr1[] = {10, 20, 15, 12};
    int n1 = sizeof(arr1) / sizeof(arr1[0]); // 计算数组长度:总字节/单元素字节

    char arr2[] = {1, 2, 3};                 // char 也能比较,因为内部转成整数比较
    int n2 = sizeof(arr2) / sizeof(arr2[0]);

    cout << arrMax<int>(arr1, n1) << endl;  // T=int,找出 int 数组中最大的 → 20
    cout << arrMax<char>(arr2, n2);          // T=char,找出 char 数组中最大的 → 3
    return 0;
}

模板关键点

  • template <typename T> 声明这是一个模板,T 是类型占位符。
  • arrMax<int>(arr1, n1)<int> 告诉编译器:把 T 换成 int 来生成一份函数。
  • 模板函数不是真正的函数,编译器根据调用生成具体版本(叫实例化)。

4.3 第39题:文件读写

题目:编写程序读取 example.txt 的内容并显示。

概念fstream 是 C++ 用来读写文件的库。有两种流:ofstream(output,写文件)和 ifstream(input,读文件)。

ofstream(写文件)ifstream(读文件)
用途把数据输出到文件从文件读取数据
打开ofstream f("文件名")ifstream f("文件名")
操作f << 数据(和 cout 一样)getline(f, str)(逐行读)
关闭f.close()f.close()
#include <iostream>   // cout 输出到屏幕
#include <fstream>    // ifstream, ofstream:文件读写
#include <string>     // string 类型
using namespace std;

int main() {
    // === 打开文件 ===
    ifstream infile("example.txt");            // ifstream: 输入文件流,用于读取
    if (!infile.is_open()) {                   // is_open() 检查文件是否成功打开
        cout << "无法打开文件!" << endl;       // 文件不存在或没权限
        return -1;
    }

    // === 逐行读取并输出 ===
    string line;                               // 存储每一行内容
    while (getline(infile, line)) {            // getline: 读一行到 line
        cout << line << endl;                // 输出到屏幕
    }   // 当读到文件末尾,getline 返回 false,循环自动结束

    // === 关闭文件 ===
    infile.close();                            // 释放文件资源(好习惯)
    return 0;
}

关键知识点

  • getline(流, string):从文件中读取一行(遇到换行符停止),自动去掉换行符。
  • is_open():返回 true 表示文件成功打开。
  • 为什么用 while 循环? getline 读取到文件末尾时会返回"假",循环自动停止——不需要手动判断文件有多长。
  • close():虽然对象销毁时会自动关闭,但显式关闭是好习惯。

4.4 第43题:Stack 类模板(栈)

题目:实现一个泛型栈(后进先出,LIFO),支持 push、pop、peek、扩容。
栈 Stack:就像一摞盘子——最后放上去的,最先拿下来。

#include <iostream>
#include <stdexcept>        // underflow_error:栈空时抛出的异常类型
using namespace std;

template <typename T>       // 模板:T 是栈中存储的元素类型
class Stack {
private:
    T* data;                // 指向动态分配的数组,存放栈中元素
    int top;                // 栈顶索引(空栈 = -1,栈非空 = 最上面元素的位置)
    int capacity;           // 数组当前容量(满了就扩容)

    // --- 扩容函数(private,只有 push 内部调用)---
    void resize() {
        capacity *= 2;                         // 容量翻倍(*2 是高效策略)
        T* newData = new T[capacity];          // 分配更大的新数组
        for (int i = 0; i <= top; i++) {       // 把旧数据拷贝到新数组
            newData[i] = data[i];
        }
        delete[] data;                         // 释放旧数组内存(配 new[] 用 delete[])
        data = newData;                        // data 指向新数组
    }

public:
    // --- 构造函数:初始化空栈 ---
    Stack(int cap = 8)                         // 默认容量 8
        : top(-1), capacity(cap) {             // 初始化列表:top 从 -1 开始(空栈)
        data = new T[capacity];                // 在堆上分配数组内存
    }

    // --- 析构函数:释放分配的内存 ---
    ~Stack() {
        delete[] data;                         // new[] 必须配 delete[]
    }

    // --- 入栈 push:把一个元素放到栈顶 ---
    void push(T value) {
        if (top + 1 >= capacity) {             // 栈满了吗?(top+1 = 当前元素个数)
            resize();                          // 满了就扩容
        }
        data[++top] = value;                   // 先 ++top(向上移),再把 value 放进去
    }

    // --- 出栈 pop:移除并返回栈顶元素 ---
    T pop() {
        if (top < 0) {                         // 栈空 = top == -1
            throw underflow_error("Stack is empty"); // 抛异常通知调用者
        }
        return data[top--];                    // 返回 data[top],然后 top--(下移)
    }

    // --- 查看栈顶 peek:只看不删 ---
    T peek() const {                           // const:承诺不修改栈内容
        if (top < 0) {                         // 同样是空栈检查
            throw underflow_error("Stack is empty");
        }
        return data[top];                      // 直接返回,不修改 top
    }

    // --- 获取当前元素个数 ---
    int getSize() const { return top + 1; }    // top 是索引,个数 = top + 1

    // --- 输出运算符重载:从栈顶到栈底打印 ---
    // friend 友元函数:不是成员函数但可以访问私有成员 data、top
    friend ostream& operator<<(ostream& os, const Stack<T>& s) {
        for (int i = s.top; i >= 0; i--) {     // 从栈顶向下遍历
            os << s.data[i];                   // 输出当前元素
            if (i > 0) os << " ";              // 元素之间用空格分隔(最后一个不加空格)
        }
        return os;                             // 返回 ostream 引用,支持链式调用
    }
};
📦 入栈 push(42):top 从 -1 变成 0,data[0] 放入 42
push(42) 前: [ ][ ][ ][ ] top=-1
push(42) 后: [42][ ][ ][ ] top=0
📤 出栈 pop():返回 data[top](即 42),top 减 1 变回 -1
pop() 返回值: 42
pop() 后: [42][ ][ ][ ] top=-1(值还在但逻辑上已删除)
🔍 查看 peek():返回 data[top] 但 top 不变
peek() 返回值:data[top]top 不改变
📏 扩容 resize():新容量 = 旧容量 × 2,拷贝数据,释放旧数组

关键知识点

  • top 为什么从 -1 开始? 空栈时没有元素,索引用 -1 表示。push 时先 ++top 变成 0,再放元素。
  • new[] 配 delete[]:数组形式必须用带 [] 的版本,否则可能内存泄漏。
  • 友元函数 friend:声明为友元后,这个函数虽然定义在类外,但可以访问 private 成员。
  • throw:当操作非法时,抛出异常比返回特殊值更好,因为调用者无法忽略。
  • const 成员函数peek() constgetSize() const 承诺不修改成员变量。
  • 扩容策略:容量 × 2(翻倍)是常用的摊销策略,使得多次 push 的平均时间复杂度保持 O(1)。

⚠️ 第 5 章 — 常见陷阱与易错题

对应考题:第31、32、35、36、37、41题 | ⭐⭐⭐

5.1 野指针(第36题)—— free 后指针不会自动变 NULL

野指针(Dangling Pointer):指针指向的内存已经被释放了,但指针还保存着那个地址。 这时再通过这个指针读写 = 未定义行为,后果不可预测(可能崩溃、可能篡改其他数据)。

#include <cstdlib>   // malloc, free:C 语言风格的内存分配/释放
#include <cstring>   // strcpy:字符串拷贝
#include <cstdio>    // printf:打印输出

int main() {
    char *str = (char *)malloc(100);           // 在堆上申请 100 字节内存
    strcpy(str, "hello");                      // 把 "hello" 拷贝到 str 指向的内存中
    free(str);                                 // 释放 str 指向的内存(还给操作系统)

    // ⚠️ 下面是大坑!free 之后 str 仍然指向原来的地址!
    // free() 只释放内存,不修改指针变量的值
    if (str != NULL) {                         // str 不是 NULL!它还是原来的地址值 0x...
        strcpy(str, "world");                  // 💥 往已释放的内存写数据!非常危险!
        printf(str);                           // 读取已释放的内存,输出什么不可预知
    }
    // 正确做法:free(str); str = NULL; ← 手动置空
    return 0;
}
free 前
str = 0x1000
指向 → "hello"
→ free() →
free 后
str = 0x1000
指向 → ??? (内存已回收)

指针的值仍是 0x1000
但内存已经不属于这个程序了

核心规则

free(ptr)delete ptr 之后,立即把指针设为 NULL

free(str);
str = NULL;       // 必须手动置空!否则 str 就是野指针

delete ptr;
ptr = nullptr;    // C++ 推荐用 nullptr 代替 NULL

5.2 私有成员不能外部访问(第41题)

访问控制:C++ 用 private(私有)、protected(保护)、public(公有)控制谁能访问成员。 这叫做封装——把内部细节藏起来,只暴露必要的接口。

class Person {
private:                       // private:只有类内部的函数可以访问
    double height;             // 这个数据对外部是隐藏的

public:                        // public:任何人都可以访问
    void setHeight(double h) { // 通过这个公有函数间接修改 height
        height = h;
    }
    double getHeight() {       // 通过这个公有函数间接读取 height
        return height;
    }
};

int main() {
    Person p;
    p.height = 1.85;           // ❌ 编译错误!height 是 private,外部不能直接访问
    p.setHeight(1.85);         // ✅ 通过 public 函数设置
    cout << p.getHeight();     // ✅ 通过 public 函数读取
    return 0;
}
修饰符类内部派生类(子类)外部(main 等)
private✅ 可访问❌ 不可❌ 不可
protected✅ 可访问✅ 可访问❌ 不可
public✅ 可访问✅ 可访问✅ 可访问

记忆:private 是秘密(只有自己知道),protected 是家传(儿子也知道),public 是公开的(谁都知道)。

5.3 异常处理(第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 {                                      // try:监控这段代码
        cout << arithmetic(24, 6) << endl;   // d=6 ≠0 → 正常 → 输出 4
        cout << arithmetic(1, 3) << endl;    // d=3 ≠0 → 正常 → 输出 0.333...
        cout << arithmetic(9, 0.0) << endl;  // d=0 → 抛出 DivideZero 异常!
        // ⚠️ 抛异常后,下面这行不会执行!
        cout << "这行不会输出" << endl;
    }
    catch (DivideZero &e) {                    // catch:捕获 DivideZero 类型的异常
        cout << e.display() << endl;         // 打印错误信息
    }
    // catch 执行完后,程序继续从这里往下走
    return 0;
}

// 最终输出:
//   4
//   0.333333
//   EXCEPTION: Division by zero attempted.

三关键字分工

关键字通俗理解代码位置
try"这一片区域可能出事,我看着"包围可能出错的代码
throw"出事了!具体情况是这个对象"检测到错误的地方
catch"我专门处理这类问题"紧跟在 try 后面

关键:throw 之后,当前函数立即退出,程序沿着调用栈往上找最近的 catch。try 块里 throw 之后的代码不会执行。

5.4 static 成员(第31题)—— 所有对象共享一份

static 成员:属于类本身,不属于任何一个对象。 不管创建多少个对象,static 成员在内存中只有一份,所有对象共享。

class complex {
public:
    static int w;          // static 成员:所有对象共享(类内声明)
    int i;                 // 普通成员:每个对象独立一份

    complex() {
        i = ++w;           // 构造时:w 先自增,再把新值赋给 i
    }
};

int complex::w = 1;        // static 成员必须在类外定义+初始化

int main() {
    complex a, b, c;       // 连续创建三个对象
    // a 构造: w 从 1 变成 2 → a.i = 2
    // b 构造: w 从 2 变成 3 → b.i = 3
    // c 构造: w 从 3 变成 4 → c.i = 4

    cout << a.w << " " << b.i << endl;
    // a.w 输出 4(所有对象读到同一个 w,最终值是 4)
    // b.i 输出 3(b 自己的 i 在构造时赋的值)
    return 0;
}
全局区(类级别)
complex::w = 4 ← 只有一份!
对象 a
a.i = 2
对象 b
b.i = 3
对象 c
c.i = 4

三个对象共享同一个 w(在全局区),但各有各的 i(在各自的内存空间)

5.5 引用作为返回值(第32题)—— 函数调用可以出现在 = 左边

引用返回 vs 值返回:值返回是"给你一个副本",引用返回是"给你原件本身"。 因为返回的是原件,所以可以赋值。

int z = 15;               // 全局变量

int& f() {                // 返回类型是 int&(int 的引用,即 int 的别名)
    return z;             // 返回 z 的引用,不是 z 的副本
}

int main() {
    f() = 20;             // 等价于 z = 20!因为 f() 返回的就是 z 本身
    cout << z;           // 输出 20(z 已经被改了)
    return 0;
}

对比

返回 int返回 int&
返回的是什么z 的副本(临时值)z 本身(别名)
f() = 20; 能编译吗❌ 不能(临时值是右值,不能放 = 左边)✅ 能(引用是左值)

记忆:引用返回 = 函数调用可以当变量用(可读可写)。

🏆 终极自测

下面代码犯了 5 个错误,你能全部找出来吗?(对应第37题和第42题的考点)

下面代码的所有问题(多选)
class Resource {
    char* buffer;                              // 指针成员:管理堆内存
public:
    Resource(const char* str) {
        buffer = new char[strlen(str)];        // ❌ 问题1: 没有 +1,'\0' 会溢出
        strcpy(buffer, str);                   // 拷贝字符串到 buffer
    }
    ~Resource() { delete buffer; }             // ❌ 问题2: new[] 应配 delete[]
    // ❌ 问题3/4/5: 没有写拷贝构造和赋值运算符 = 违反 Rule of Three
};
int main() {
    Resource a("hello");                       // 构造 a
    Resource b = a;                            // ❌ 问题3: 默认拷贝构造 → 浅拷贝
    b = a;                                     // ❌ 问题4: 默认赋值运算 → 内存泄漏
                                               // ❌ 问题5: 以上导致析构时双重释放
}