第一次接触C++编程练习题时,我还在大学计算机系的实验室里调试着满是bug的代码。那些看似简单的题目背后,往往隐藏着语言特性的精妙运用和算法思维的严谨考验。十多年过去,当我以工程师身份面试应届生时,依然会从基础练习题中观察候选人的代码素养。
C++练习题不同于普通代码编写,它是对语言核心特性的针对性训练。好的练习题应该像瑞士军刀一样,每个题目都聚焦一个特定的语言特性或编程范式。比如指针操作、内存管理、多态实现这些C++的招牌特性,都需要通过刻意练习才能真正掌握。
字符串反转是经典入门题,但不同解法能体现思维层次:
cpp复制// 新手常见写法
string reverseString(string s) {
string result;
for(int i=s.length()-1; i>=0; i--) {
result += s[i];
}
return result;
}
// 优化后的版本
string reverseString(string s) {
int left = 0, right = s.length()-1;
while(left < right) {
swap(s[left++], s[right--]);
}
return s;
}
第一个版本虽然功能正确,但存在三个典型问题:频繁的字符串拼接导致性能损耗、没有利用原字符串空间、代码可读性一般。第二个版本则展示了C++程序员应有的空间意识和算法思维。
设计银行账户系统是检验OOP能力的试金石。我曾见过一个学员这样设计:
cpp复制class BankAccount {
private:
string owner;
double balance;
static int totalAccounts;
public:
BankAccount(string name) : owner(name), balance(0) {
totalAccounts++;
}
void deposit(double amount) {
if(amount <= 0) throw invalid_argument("Amount must be positive");
balance += amount;
}
// 其他方法...
};
这个设计有几个亮点:使用构造器初始化列表、静态成员跟踪类实例、参数有效性检查。但缺少了拷贝控制(copy control)的相关设计,这是很多初学者容易忽略的要点。
二叉树遍历看似基础,但能考察递归和迭代两种思维模式。非递归实现尤其考验对栈的理解:
cpp复制vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> s;
TreeNode* curr = root;
while(curr || !s.empty()) {
while(curr) {
s.push(curr);
curr = curr->left;
}
curr = s.top();
s.pop();
result.push_back(curr->val);
curr = curr->right;
}
return result;
}
这个实现中,内层while循环负责左子树压栈,外层处理右子树。我建议在纸上画出栈的变化过程,这对理解非递归遍历至关重要。
使用gdb调试段错误(segmentation fault)是每个C++程序员的必修课。当遇到指针问题时:
bash复制g++ -g program.cpp -o program
gdb ./program
run
backtrace
在gdb中,backtrace命令能显示函数调用栈,frame命令可以查看特定栈帧,print命令检查变量值。我曾用这个方法在一个复杂项目中定位到野指针问题,节省了数小时盲目排查的时间。
从C++11开始引入的智能指针能大幅降低内存管理难度:
cpp复制class TreeNode {
public:
int val;
shared_ptr<TreeNode> left;
shared_ptr<TreeNode> right;
TreeNode(int x) : val(x) {}
};
void buildTree() {
auto root = make_shared<TreeNode>(1);
root->left = make_shared<TreeNode>(2);
root->right = make_shared<TreeNode>(3);
// 无需手动释放内存
}
注意shared_ptr的循环引用问题。在树结构中,父节点到子节点用shared_ptr,子节点到父节点建议用weak_ptr。
使用Google Test框架为练习题添加测试用例:
cpp复制TEST(BankAccountTest, DepositNegativeAmount) {
BankAccount account("Test");
EXPECT_THROW(account.deposit(-100), invalid_argument);
}
TEST(StringTest, ReverseEmptyString) {
string empty;
EXPECT_EQ(reverseString(empty), empty);
}
良好的测试应该包含正常情况、边界情况和异常情况。测试驱动开发(TDD)能显著提高代码质量。
cpp复制// 错误示例
int* createArray(int size) {
int arr[size];
return arr; // 返回局部变量地址
}
// 正确做法
int* createArray(int size) {
int* arr = new int[size];
return arr; // 调用者需负责delete[]
}
更现代的写法是直接返回vector,避免手动内存管理。这是我在代码评审中最常指出的问题之一。
当类声明和实现分离时:
cpp复制// bankaccount.h
#pragma once
class BankAccount {
void deposit(double amount);
};
// bankaccount.cpp
#include "bankaccount.h"
void BankAccount::deposit(double amount) { ... }
编译时需同时指定源文件:
bash复制g++ main.cpp bankaccount.cpp -o program
忘记链接实现文件会导致undefined reference错误,这是新手常踩的坑。
cpp复制// 传统写法
for(int i=0; i<vec.size(); i++) {
cout << vec[i] << " ";
}
// 现代写法
for(auto& item : vec) {
cout << item << " ";
}
// 算法库应用
sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // 降序排序
});
合理使用STL算法和lambda表达式能让代码更简洁高效。我建议熟记
当基础题目已经熟练后,可以尝试这些方向:
每个方向都对应着C++的不同深度领域。例如实现简易vector时,你会深刻理解动态扩容、迭代器失效等概念。