1. 为什么你需要一份“参考答案”,而不是“标准答案”?
在C++学习的漫长旅途中,尤其是啃《C++ Primer Plus》这类经典大部头时,几乎每个学习者都会在课后编程练习前卡壳。你可能会在搜索引擎里输入“C++ Primer Plus 编程练习答案”,希望能找到一份“标准答案”来对照,快速验证自己的思路。但我想告诉你的是,对于编程学习而言,尤其是像C++这样强调底层理解和灵活运用的语言,“参考答案”的价值远大于“标准答案”。
首先,编程问题往往没有唯一的解。同一个问题,可以用不同的算法、不同的数据结构、甚至不同的编码风格来实现。一份所谓的“标准答案”可能会固化你的思维,让你误以为只有这一种“正确”写法。而一份好的“参考答案”,则会展示多种可能的实现路径,并解释每种选择的优劣,这更能锻炼你的编程思维和问题解决能力。
其次,《C++ Primer Plus》的练习题设计精妙,很多题目旨在引导你思考语言特性的边界、理解编译器的行为、以及培养良好的编程习惯。直接看“答案”会跳过这个最重要的思考过程。参考答案的作用,应该是在你经过充分思考、尝试编写并调试了自己的代码之后,用来对比、反思和提升的。它帮你检查逻辑漏洞,学习更优雅或更高效的写法,理解题目背后更深层的知识点。
最后,网络上流传的许多“答案”质量参差不齐,可能存在错误、过时的写法(比如使用了被弃用的C风格字符串处理而非std::string),或者忽略了现代C++的最佳实践。因此,拥有一份经过筛选、附带详细解说的“参考答案”,对于自学者来说至关重要。
接下来的内容,我将以《C++ Primer Plus》的典型练习题为例,为你展示如何构建和使用一份高质量的“参考答案”库。我会重点解析几个核心章节的经典题目,不仅给出代码,更重要的是拆解题目意图、分析常见陷阱、并对比不同实现方案的优劣。这不仅仅是给你答案,更是给你一套自学和验证的方法论。
2. 从基础到复合:变量、循环与分支的练习精解
《C++ Primer Plus》的前几章是奠定基础的黄金时期。这里的练习看似简单,却极易埋下隐患。我们以第四章“复合类型”和第五章“循环和关系表达式”的交叉练习为例。
2.1 示例:统计输入字符(第5章,练习5)
题目回顾:编写一个程序,要求用户输入一系列字符,直到输入@为止。程序需要统计输入的字符数,并分别统计数字、字母(区分大小写)和其他字符的数量,同时将字母转换为大写输出。
常见新手陷阱:
- 输入缓冲与字符读取:直接使用
cin >> ch会跳过空白符(空格、制表符、换行符)。而题目通常要求统计所有字符,包括空白符。这里必须使用cin.get(ch)或ch = getchar()来读取每一个字符。 - 循环条件与边界:循环应持续读取,直到读取到
@。注意,这个@本身不应被计入统计。 - 字符分类函数:手动用ASCII码范围判断(如
ch >= '0' && ch <= '9')虽然可行,但使用C++标准库的<cctype>头文件中的函数(如isdigit(ch),isalpha(ch))更安全、可读性更好,因为它考虑了本地化设置。 - 大小写转换:同样,使用
toupper(ch)比手动计算(ch - 'a' + 'A')更推荐。
参考答案与深度解析:
#include <iostream> #include <cctype> // 用于 isdigit, isalpha, toupper int main() { using namespace std; char ch; int digitCount = 0, alphaCount = 0, otherCount = 0; cout << "Enter characters (enter @ to stop):\n"; // 使用 cin.get(ch) 读取每一个字符,包括空格和换行 while (cin.get(ch) && ch != '@') { if (isdigit(ch)) { digitCount++; } else if (isalpha(ch)) { alphaCount++; // 转换为大写并输出 cout << char(toupper(ch)); // toupper返回int,需强制转换回char } else { otherCount++; } } cout << "\n\nStatistics:\n"; cout << "Digits: " << digitCount << endl; cout << "Alphabets: " << alphaCount << endl; cout << "Other characters: " << otherCount << endl; // 清空输入缓冲区中可能残留的字符(包括换行符),为后续输入做准备 // 这是一个良好的习惯,但在此简单示例中非必须 // cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); return 0; }为什么这样写?
while (cin.get(ch) && ch != ‘@’):这是核心循环。cin.get(ch)在成功读取一个字符到ch后返回cin对象,其布尔值为true;如果遇到文件结束或错误,则为false。&& ch != ‘@’确保在读取到@时立即终止循环,且@不会被处理。这种写法将读取操作和条件判断合并,非常简洁。- 使用
<cctype>函数:这是现代C++鼓励的做法。它使代码意图更清晰(isdigit一看就知道是判断数字),且避免了硬编码ASCII值,提高了代码的可移植性。 cout << char(toupper(ch));:toupper函数返回int类型,直接输出可能被当作整数。将其强制转换回char是安全的输出方式。- 关于缓冲区清理的注释:在实际更复杂的程序中,混合使用
cin >>和cin.get()时,缓冲区里残留的换行符会导致问题。我以注释形式提到了cin.ignore,这是处理这类问题的关键技巧,提醒读者注意这个潜在坑点。
2.2 示例:动态结构数组与new/delete(第4章,练习9)
题目回顾:编写一个程序,动态分配一个结构数组(结构体包含商品名和价格),让用户输入信息,然后按输入顺序和按价格排序后分别输出。
核心知识点:动态内存管理(new []/delete [])、结构体使用、排序算法(或std::sort)、用户输入处理。
参考答案框架与关键点:
#include <iostream> #include <string> #include <algorithm> // 用于 std::sort #include <limits> // 用于 std::numeric_limits struct Item { std::string name; double price; }; int main() { using namespace std; int numItems; cout << "How many items do you wish to enter? "; cin >> numItems; cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 清除数字后的换行符! // 1. 动态分配数组 Item* itemArray = new Item[numItems]; // 2. 读取数据 for (int i = 0; i < numItems; ++i) { cout << "Enter item #" << i + 1 << " name: "; getline(cin, itemArray[i].name); // 使用getline读取可能包含空格的商品名 cout << "Enter item #" << i + 1 << " price: "; cin >> itemArray[i].price; cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 再次清除换行符! } // 3. 按输入顺序输出 cout << "\nHere is your item list (original order):\n"; for (int i = 0; i < numItems; ++i) { cout << itemArray[i].name << ": $" << itemArray[i].price << endl; } // 4. 按价格排序 // 使用lambda表达式定义比较规则,这是现代C++的优雅写法 sort(itemArray, itemArray + numItems, [](const Item& a, const Item& b) { return a.price < b.price; }); cout << "\nHere is your item list (sorted by price):\n"; for (int i = 0; i < numItems; ++i) { cout << itemArray[i].name << ": $" << itemArray[i].price << endl; } // 5. 释放动态分配的内存 delete[] itemArray; return 0; }深度解析与避坑指南:
cin.ignore的至关重要性:这是本题最大的坑。当使用cin >> numItems读取整数后,用户按下的回车键(‘\n’)会留在输入缓冲区。紧接着的getline(cin, itemArray[0].name)会立刻读到这个空行,导致第一次循环跳过名称输入。cin.ignore(...)的作用就是清空缓冲区直到遇到换行符,为getline准备好干净的输入环境。在每次cin >>之后跟一个ignore是处理混合输入时的黄金法则。- 动态内存管理:必须成对使用
new Item[numItems]和delete[] itemArray。使用delete而非delete[]是未定义行为,可能导致内存泄漏或程序崩溃。在现代C++中,更推荐使用std::vector<Item>来完全避免手动内存管理,但此题旨在练习new/delete。 - 使用
std::sort和Lambda:手动实现冒泡或选择排序可以,但std::sort是标准库提供的更高效、更不易出错的算法。Lambda表达式[](const Item& a, const Item& b) { return a.price < b.price; }清晰地定义了排序依据(按价格升序)。这是理解C++函数对象和泛型编程的好起点。 - 结构体与
std::string:在结构体中使用std::string管理字符串,比C风格的char数组安全、方便得多。它自动处理内存分配和释放,是《C++ Primer Plus》后期强调的“面向对象”和“现代C++”思想的体现。
3. 函数、内存模型与代码组织的进阶挑战
当学习进入函数、内存模型(自动存储、静态存储、动态存储)和多个源代码文件组织时,练习的综合性大大增强。这里的关键是理解数据如何在不同函数和生命周期中传递和保存。
3.1 示例:递归与静态局部变量(第7章,练习8)
题目回顾:编写一个函数,它接受一个char*参数和一个char参数。函数返回该字符在字符串中出现的次数。编写一个程序来测试它。进阶:修改函数,使其能统计该函数被调用了几次(提示:使用静态局部变量)。
知识点:指针与字符串、函数定义、静态存储持续性。
参考答案与解析:
#include <iostream> #include <cstring> // 为了 strlen,但我们的函数不直接使用它 // 基础版本:统计字符出现次数 int countChar(const char* str, char ch) { int count = 0; if (!str) return 0; // 防御性编程:检查空指针 while (*str) { // 遍历字符串直到空字符 if (*str == ch) { count++; } str++; // 移动指针到下一个字符 } return count; } // 进阶版本:增加调用次数统计 int countCharWithCallCount(const char* str, char ch) { static int callCount = 0; // 静态局部变量,生命周期贯穿整个程序运行期 callCount++; std::cout << "[Debug] Function has been called " << callCount << " time(s).\n"; int count = 0; if (!str) return 0; while (*str) { if (*str == ch) count++; str++; } return count; } int main() { using namespace std; char testStr[] = "Hello, this is a test string!"; char target = 't'; // 测试基础版本 int result1 = countChar(testStr, target); cout << "The character '" << target << "' appears " << result1 << " times.\n"; // 测试进阶版本 int result2 = countCharWithCallCount(testStr, target); cout << "Result (with call count): " << result2 << endl; result2 = countCharWithCallCount("Another test", 'e'); cout << "Result (with call count): " << result2 << endl; // 再次调用,观察静态变量callCount的变化 result2 = countCharWithCallCount(testStr, 's'); cout << "Result (with call count): " << result2 << endl; return 0; }为什么静态局部变量是关键?
- 作用域与生命周期:函数内的普通局部变量(如
int count)具有自动存储持续性,在函数每次被调用时创建,函数结束时销毁。而用static修饰的局部变量(static int callCount)具有静态存储持续性。它在程序首次执行到其声明语句时初始化(通常为0),之后即使函数调用结束,该变量占用的内存也不会释放,其值会保持到下一次函数调用。 - 本题中的应用:
callCount用于记录函数被调用的总次数,而不是某一次调用中的次数。这正是静态局部变量的典型应用场景:在函数调用间保留状态信息。输出会显示调用次数依次递增。 - 初始化:静态局部变量只在第一次调用时初始化。如果写成
static int callCount = 10;,那么它第一次被初始化为10,后续调用不会再执行=10这个操作。
3.2 示例:多文件编程与头文件保护(第9章相关)
题目回顾(综合练习):将上述countChar函数和countCharWithCallCount函数分别放在独立的源代码文件中,并创建一个头文件来声明它们,最后在main.cpp中调用。
项目结构:
project/ ├── charCounter.h // 头文件,包含函数声明 ├── charCounter.cpp // 包含 countChar 函数定义 ├── advancedCounter.cpp // 包含 countCharWithCallCount 函数定义 └── main.cpp // 主程序,包含main函数charCounter.h(头文件):
// charCounter.h #ifndef CHARCOUNTER_H // 头文件保护,防止重复包含 #define CHARCOUNTER_H // 基础版本函数声明 int countChar(const char* str, char ch); // 进阶版本函数声明 int countCharWithCallCount(const char* str, char ch); #endif // CHARCOUNTER_HcharCounter.cpp:
// charCounter.cpp #include "charCounter.h" int countChar(const char* str, char ch) { int count = 0; if (!str) return 0; while (*str) { if (*str == ch) count++; str++; } return count; }advancedCounter.cpp:
// advancedCounter.cpp #include <iostream> #include "charCounter.h" int countCharWithCallCount(const char* str, char ch) { static int callCount = 0; callCount++; std::cout << "[Debug] Function has been called " << callCount << " time(s).\n"; int count = 0; if (!str) return 0; while (*str) { if (*str == ch) count++; str++; } return count; }main.cpp:
// main.cpp #include <iostream> #include "charCounter.h" int main() { // ... 测试代码与之前相同 ... char testStr[] = "Hello, this is a test string!"; std::cout << countChar(testStr, 't') << std::endl; std::cout << countCharWithCallCount(testStr, 's') << std::endl; return 0; }编译与链接(以g++为例):
g++ -c charCounter.cpp -o charCounter.o g++ -c advancedCounter.cpp -o advancedCounter.o g++ -c main.cpp -o main.o g++ main.o charCounter.o advancedCounter.o -o myProgram核心要点:
- 头文件的作用:声明函数(和类、全局变量等),告诉编译器“这个函数存在,它的接口长这样”。
#include “charCounter.h”本质上是将头文件内容复制到源文件中。 - 头文件保护
#ifndef/#define/#endif:这是防止同一个头文件被同一个源文件多次包含的经典方法。多次包含会导致重复声明,引发编译错误。 - 分离编译:每个
.cpp文件独立编译成目标文件(.o或.obj),最后链接器将所有目标文件以及标准库链接成可执行文件。这样做的好处是,修改一个源文件只需重新编译该文件,再重新链接即可,大大提升大型项目的编译效率。 #include的区别:#include <iostream>用于包含标准库头文件,编译器在系统路径中查找。#include “charCounter.h”用于包含自定义头文件,编译器首先在当前目录或指定的项目目录中查找。
4. 面向对象编程:类设计、继承与多态的实战演练
《C++ Primer Plus》的后半部分重点转向面向对象编程(OOP)。这里的练习考察你对类、对象、构造函数、析构函数、继承、多态等核心概念的理解和应用能力。
4.1 示例:一个简单的银行账户类(第10章,练习7)
题目回顾:设计一个BankAccount类,包含以下私有数据成员:储户姓名、账号、存款余额。公有成员函数包括:创建账户并初始化的构造函数、显示姓名账号和余额的函数、存款函数、取款函数。取款函数需确保余额充足。
类设计思路:
- 数据隐藏:姓名、账号、余额设为
private,这是封装的基本原则。 - 接口设计:提供公有的构造函数、
show()、deposit(double)、withdraw(double)函数。 - 构造函数:用于初始化对象状态。可以考虑提供默认构造函数和带参数的构造函数。
- 取款逻辑:取款前检查余额,不足则拒绝操作并提示。
参考答案:
// bankaccount.h #ifndef BANKACCOUNT_H #define BANKACCOUNT_H #include <string> class BankAccount { private: std::string depositorName; std::string accountNumber; double balance; public: // 构造函数 BankAccount(); // 默认构造函数 BankAccount(const std::string& name, const std::string& accNum, double bal = 0.0); // 功能函数 void show() const; // const成员函数,承诺不修改对象状态 bool deposit(double amount); // 存款,返回是否成功(总是成功) bool withdraw(double amount); // 取款,返回是否成功(可能失败) }; #endif// bankaccount.cpp #include <iostream> #include “bankaccount.h” // 默认构造函数 BankAccount::BankAccount() : depositorName(“”), accountNumber(“”), balance(0.0) {} // 带参构造函数,使用成员初始化列表(更高效) BankAccount::BankAccount(const std::string& name, const std::string& accNum, double bal) : depositorName(name), accountNumber(accNum), balance(bal) { if (bal < 0) { std::cout << “Warning: Initial balance cannot be negative. Setting to 0.\n”; balance = 0.0; } } void BankAccount::show() const { std::cout << “Depositor: “ << depositorName << std::endl; std::cout << “Account Number: “ << accountNumber << std::endl; std::cout << “Balance: $” << balance << std::endl; } bool BankAccount::deposit(double amount) { if (amount <= 0) { std::cout << “Deposit amount must be positive.\n”; return false; } balance += amount; std::cout << “Successfully deposited $” << amount << std::endl; return true; } bool BankAccount::withdraw(double amount) { if (amount <= 0) { std::cout << “Withdrawal amount must be positive.\n”; return false; } if (amount > balance) { std::cout << “Insufficient funds! Withdrawal denied.\n”; return false; } balance -= amount; std::cout << “Successfully withdrew $” << amount << std::endl; return true; }测试程序:
// main.cpp #include “bankaccount.h” #include <iostream> int main() { using std::cout; using std::endl; // 使用带参构造函数 BankAccount myAccount(“John Doe”, “123456789”, 1000.0); cout << “Initial account info:\n”; myAccount.show(); cout << endl; // 测试存款 myAccount.deposit(500.0); myAccount.show(); cout << endl; // 测试取款(成功) if (myAccount.withdraw(200.0)) { cout << “Withdrawal successful.\n”; } myAccount.show(); cout << endl; // 测试取款(失败) if (!myAccount.withdraw(2000.0)) { cout << “Withdrawal failed as expected.\n”; } myAccount.show(); return 0; }设计亮点与思考:
const成员函数:show()被声明为const,因为它不修改对象的数据成员。这既是良好的设计习惯,也允许在const BankAccount对象上调用此函数。- 构造函数初始化列表:在
BankAccount::BankAccount(...)中,使用初始化列表: depositorName(name), ...来初始化成员,这比在构造函数体内赋值更高效(对于非内置类型,如std::string,避免了先默认构造再赋值的过程)。 - 输入验证:在构造函数和
withdraw、deposit函数中加入了基本的输入验证(如检查金额正负、余额是否充足),这是健壮性编程的基本要求。 - 返回值设计:
deposit和withdraw返回bool类型,指示操作成功与否。调用方可以根据返回值决定后续逻辑。
4.2 示例:继承与多态——图形类层次(第13章,练习4)
题目回顾:设计一个基类Shape,并派生出Rectangle、Square、Circle等类。基类包含纯虚函数area()和perimeter()。每个派生类实现自己的面积和周长计算。使用基类指针数组来管理不同图形对象,并计算总面积和总周长。
这是OOP的核心综合练习,考察抽象、继承、多态和动态绑定的理解。
参考答案框架:
// shapes.h #ifndef SHAPES_H #define SHAPES_H #include <cmath> // 用于M_PI,但注意M_PI不是标准C++的一部分,可用 std::numbers::pi (C++20) const double PI = 3.14159265358979323846; class Shape { public: virtual double area() const = 0; // 纯虚函数,使Shape成为抽象类 virtual double perimeter() const = 0; // 纯虚函数 virtual ~Shape() {} // 虚析构函数,确保正确释放派生类对象 }; class Rectangle : public Shape { private: double width, height; public: Rectangle(double w, double h) : width(w), height(h) {} virtual double area() const override { return width * height; } virtual double perimeter() const override { return 2 * (width + height); } }; class Square : public Rectangle { // Square “是一种” Rectangle public: Square(double side) : Rectangle(side, side) {} // 调用基类构造函数 // 面积和周长函数继承自Rectangle,无需重写 }; class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} virtual double area() const override { return PI * radius * radius; } virtual double perimeter() const override { return 2 * PI * radius; } }; #endif测试与多态应用:
// main.cpp #include <iostream> #include <vector> #include “shapes.h” int main() { using namespace std; // 使用基类指针的容器来管理不同类型的图形对象 vector<Shape*> shapes; shapes.push_back(new Rectangle(5.0, 3.0)); shapes.push_back(new Square(4.0)); shapes.push_back(new Circle(2.5)); shapes.push_back(new Rectangle(2.0, 6.0)); double totalArea = 0.0; double totalPerimeter = 0.0; for (Shape* shape : shapes) { totalArea += shape->area(); // 动态绑定,调用正确的area() totalPerimeter += shape->perimeter(); // 动态绑定 cout << “Area: “ << shape->area() << “, Perimeter: “ << shape->perimeter() << endl; } cout << “\nTotal Area: “ << totalArea << endl; cout << “Total Perimeter: “ << totalPerimeter << endl; // 释放动态分配的内存 for (Shape* shape : shapes) { delete shape; } shapes.clear(); return 0; }核心概念解析:
- 抽象类与纯虚函数:
Shape类中的area()和perimeter()被声明为= 0,这使得Shape成为抽象类。你不能创建Shape的对象,但可以创建Shape*指针。这强制所有派生类必须实现这些函数,保证了接口的一致性。 - 继承关系:
Square公有继承自Rectangle,这符合“正方形是一种矩形”的“is-a”关系。Square的构造函数简单地用相同的边长调用Rectangle的构造函数。 - 多态与动态绑定:
vector<Shape*>中存放的是指向基类Shape的指针,但实际指向的是Rectangle、Square或Circle对象。当通过基类指针调用area()或perimeter()时,程序会在运行时根据指针实际指向的对象类型来决定调用哪个版本的函数。这就是多态,它通过虚函数表(vtable)机制实现。 - 虚析构函数:基类
Shape的析构函数被声明为virtual。这是至关重要的。当通过delete一个Shape*指针来删除一个派生类对象时,如果析构函数不是虚函数,那么只会调用Shape的析构函数,而不会调用派生类的析构函数,可能导致派生类独有的资源(如动态内存)泄漏。将其设为虚函数确保了正确调用完整的析构链。 override关键字(C++11):在派生类中重写虚函数时使用override是一个好习惯。它让编译器检查你是否正确地重写了基类的虚函数(函数签名必须一致),如果拼写错误或参数不同,编译器会报错,避免难以察觉的错误。
5. 模板、STL与异常处理:现代C++的必备技能
最后一部分的练习往往涉及泛型编程和标准模板库(STL),这是写出高效、通用、现代C++代码的关键。
5.1 示例:模板函数与STL算法(第16章,练习7)
题目回顾:编写一个模板函数,它接受一个数组和数组长度,返回数组中最大元素的索引。在程序中分别用int数组和double数组测试。然后,尝试用STL的std::max_element算法实现同样的功能。
传统模板函数实现:
#include <iostream> #include <algorithm> // for std::max_element // 模板函数:返回数组中最大元素的索引 template <typename T> int findMaxIndex(const T arr[], int size) { if (size <= 0) return -1; // 处理边界情况 int maxIndex = 0; for (int i = 1; i < size; ++i) { if (arr[i] > arr[maxIndex]) { maxIndex = i; } } return maxIndex; } int main() { // 测试int数组 int intArr[] = {12, 45, 2, 67, 23, 9}; int intSize = sizeof(intArr) / sizeof(intArr[0]); int intMaxIdx = findMaxIndex(intArr, intSize); std::cout << “Max integer is at index [“ << intMaxIdx << “]: “ << intArr[intMaxIdx] << std::endl; // 测试double数组 double dblArr[] = {3.14, 2.718, 1.414, 9.8}; int dblSize = sizeof(dblArr) / sizeof(dblArr[0]); int dblMaxIdx = findMaxIndex(dblArr, dblSize); std::cout << “Max double is at index [“ << dblMaxIdx << “]: “ << dblArr[dblMaxIdx] << std::endl; return 0; }使用STLstd::max_element:
#include <iostream> #include <algorithm> // for std::max_element #include <iterator> // for std::distance int main() { int intArr[] = {12, 45, 2, 67, 23, 9}; int intSize = sizeof(intArr) / sizeof(intArr[0]); // std::max_element 返回指向最大元素的迭代器(这里是指针) int* maxElementPtr = std::max_element(intArr, intArr + intSize); if (maxElementPtr != intArr + intSize) { // 确保找到了 // 计算索引:指针差值,或使用 std::distance int maxIndex = std::distance(intArr, maxElementPtr); // 或者 int maxIndex = maxElementPtr - intArr; std::cout << “Max integer (STL) is at index [“ << maxIndex << “]: “ << *maxElementPtr << std::endl; } // 对于其他容器,如 std::vector,用法类似 std::vector<double> vec = {3.14, 2.718, 1.414, 9.8}; auto vecMaxIt = std::max_element(vec.begin(), vec.end()); if (vecMaxIt != vec.end()) { int vecMaxIndex = std::distance(vec.begin(), vecMaxIt); std::cout << “Max in vector is at index [“ << vecMaxIndex << “]: “ << *vecMaxIt << std::endl; } return 0; }对比与启示:
- 模板的威力:
findMaxIndex函数模板可以处理任何定义了>运算符的类型,实现了代码复用。 - STL的优雅与强大:
std::max_element是泛型算法,它接受一对迭代器(表示范围),返回指向最大元素的迭代器。它比自己写的循环更简洁、更不易出错,并且经过高度优化。配合std::distance可以轻松获得索引。 - 迭代器抽象:STL算法基于迭代器工作,这使得它们可以无缝应用于数组、
vector、list、deque等各种容器,实现了算法与数据结构的分离,这是泛型编程思想的精髓。
5.2 示例:异常处理(第15章,练习6)
题目回顾:修改之前的BankAccount::withdraw函数,当取款金额超过余额时,抛出一个自定义的异常(如InsufficientFundsException),并在main函数中捕获和处理这个异常。
异常处理版本:
// 自定义异常类 class InsufficientFundsException : public std::exception { private: std::string message; public: InsufficientFundsException(const std::string& accNum, double balance, double amount) : message(“Account “ + accNum + “ has insufficient funds. Balance: $” + std::to_string(balance) + “, Attempted withdrawal: $” + std::to_string(amount)) {} virtual const char* what() const noexcept override { return message.c_str(); } }; // 修改后的 BankAccount::withdraw 成员函数 bool BankAccount::withdraw(double amount) { if (amount <= 0) { throw std::invalid_argument(“Withdrawal amount must be positive.”); } if (amount > balance) { // 抛出自定义异常,携带详细信息 throw InsufficientFundsException(accountNumber, balance, amount); } balance -= amount; std::cout << “Successfully withdrew $” << amount << std::endl; return true; } // main函数中的使用 int main() { BankAccount acc(“Alice”, “ACC001”, 100.0); try { acc.deposit(50.0); acc.withdraw(200.0); // 这将抛出异常 acc.withdraw(30.0); // 这行不会被执行 } catch (const InsufficientFundsException& e) { std::cerr << “Withdrawal failed: “ << e.what() << std::endl; // 可以进行一些恢复操作,比如记录日志、提示用户等 } catch (const std::exception& e) { // 捕获其他标准异常 std::cerr << “Standard exception caught: “ << e.what() << std::endl; } catch (...) { // 捕获所有其他未知异常 std::cerr << “Unknown exception caught!” << std::endl; } // 程序可以继续执行 acc.show(); return 0; }异常处理的核心思想:
- 分离正常逻辑与错误处理:使用异常可以将错误处理代码从主业务逻辑中分离出来,使代码更清晰。函数在遇到无法处理的错误时“抛出”(
throw)异常,调用者通过try-catch块来“捕获”(catch)并处理异常。 - 异常类继承体系:自定义异常通常继承自
std::exception,并重写what()方法以提供错误描述。这允许你用基类引用来捕获所有派生类异常(如catch (const std::exception& e))。 - 资源管理:异常可能会改变程序的正常执行流,因此要特别注意资源泄漏问题(如动态内存、文件句柄)。这就是为什么RAII(资源获取即初始化)和智能指针如此重要——它们在对象析构时自动释放资源,即使异常发生也能保证。
- 谨慎使用:异常处理有一定性能开销,不应被用于普通的控制流。它适用于那些罕见的、严重的、函数本身无法处理的错误情况。
通过以上从基础语法到高级特性的层层递进的练习解析,我希望展示的不仅仅是一份“答案”,更是一种学习C++的方法:理解题目意图、思考多种解决方案、注意边界条件和陷阱、并最终用清晰、健壮、现代的C++代码来实现。记住,编程是实践的艺术,反复敲打这些练习,理解每一行代码背后的“为什么”,远比单纯地复制粘贴答案要重要得多。当你能够独立完成并深入理解《C++ Primer Plus》中的大部分练习时,你的C++功底就已经相当扎实了。