作为一名有十年C++开发经验的工程师,我深知面向对象编程(OOP)是C++的核心精髓。今天我将通过五个实际案例,手把手带你完成从类设计到测试的全过程,这些案例覆盖了构造函数、封装、数据校验等关键OOP概念。不同于教科书上的理论讲解,我会分享在实际工程中的设计考量和常见陷阱。
教学管理系统需要一个能够记录课程基本信息的类。原始的GradeBook类只包含课程名称,现在需要扩展以下功能:
cpp复制// GradeBook.h
#include <string>
class GradeBook {
public:
GradeBook(std::string courseName, std::string teacherName);
void setCourseName(std::string);
std::string getCourseName() const;
void setTeacherName(std::string);
std::string getTeacherName() const;
void displayMessage() const;
private:
std::string courseName;
std::string teacherName;
};
关键设计点:将teacherName声明为std::string而非char数组,既保证安全性又简化内存管理。所有get方法使用const修饰,承诺不修改对象状态。
构造函数采用委托set方法的方式,避免重复校验逻辑:
cpp复制// GradeBook.cpp
GradeBook::GradeBook(string courseName, string teacherName) {
setCourseName(courseName); // 复用set方法的校验逻辑
setTeacherName(teacherName);
}
displayMessage方法的实现展示了信息组织的技巧:
cpp复制void GradeBook::displayMessage() const {
cout << "Welcome to the grade book for\n"
<< getCourseName() << "!" << endl;
cout << "This course is presented by: "
<< getTeacherName() << endl;
}
经验之谈:即使可以直接访问成员变量,也优先通过get方法获取,这保持了封装的一致性,未来修改get逻辑时不会影响其他代码。
有效的测试应该覆盖各种边界情况:
cpp复制// main.cpp
int main() {
// 正常情况测试
GradeBook gradeBook1("CS101", "Professor A");
// 长字符串测试
GradeBook gradeBook2("CS102 Data Structures in C++", "Professor B with Very Long Name");
// 空字符串测试
GradeBook gradeBook3("", "");
gradeBook3.setCourseName("New Course");
gradeBook3.setTeacherName("New Teacher");
gradeBook1.displayMessage();
gradeBook2.displayMessage();
gradeBook3.displayMessage();
return 0;
}
银行账户类需要严格的数据校验:
cpp复制// Account.h
class Account {
public:
Account(int initialBalance);
void credit(int amount);
void debit(int amount);
int getBalance() const;
private:
int balance;
};
构造函数中的余额校验:
cpp复制Account::Account(int initialBalance) {
if (initialBalance >= 0) {
balance = initialBalance;
} else {
cerr << "错误:初始余额不能为负" << endl;
balance = 0; // 自动校正为0
}
}
取款操作的双重校验:
cpp复制void Account::debit(int amount) {
if (amount <= 0) {
cerr << "错误:取款金额必须为正数" << endl;
return;
}
if (amount <= balance) {
balance -= amount;
} else {
cerr << "错误:取款金额超过账户余额" << endl;
}
}
金融系统经验:所有资金操作必须先校验后执行,且错误处理要明确。cerr比cout更适合错误输出,可以被重定向到日志系统。
虽然这个基础版本不考虑多线程,但在实际银行系统中必须考虑:
cpp复制// 伪代码展示线程安全版本
class ThreadSafeAccount {
mutex mtx;
// ...
void debit(int amount) {
lock_guard<mutex> lock(mtx);
// 原有逻辑
}
};
发票类需要处理商品信息和计算金额,关键校验点:
cpp复制// Invoice.cpp
void Invoice::setQuantity(int quantity) {
this->quantity = (quantity < 0) ? 0 : quantity;
}
void Invoice::setPrice(int price) {
this->price = (price < 0) ? 0 : price;
}
int Invoice::getInvoiceAmount() {
return quantity * price; // 自动使用校正后的值
}
在构造函数中复用set方法:
cpp复制Invoice::Invoice(string number, string desc, int qty, int price) {
setPartNumber(number); // 即使没有校验也保持统一接口
setPartDescription(desc);
setQuantity(qty); // 会执行负数校验
setPrice(price); // 会执行负数校验
}
工程经验:即使某些set方法目前没有复杂逻辑,也保持通过set方法初始化成员变量的习惯,这样未来添加校验逻辑时无需修改构造函数。
员工类需要处理月薪信息,特别注意:
cpp复制// Employee.cpp
void Employee::setSalary(int salary) {
if(salary >= 0) {
monthSalary = salary;
} else {
cerr << "警告:薪资设置为负值,自动校正为0" << endl;
monthSalary = 0;
}
}
10%涨薪操作需要考虑整数运算的精度问题:
cpp复制// main.cpp
int emp1Current = emp1.getSalary();
emp1.setSalary(static_cast<int>(emp1Current * 1.1)); // 明确类型转换
// 更精确的版本(四舍五入):
emp1.setSalary(static_cast<int>(emp1Current * 1.1 + 0.5));
薪资计算陷阱:直接使用int计算百分比会导致精度损失。在实际系统中,应该使用定点数或货币专用类型。
精确计算年龄需要考虑当前是否已过生日:
cpp复制int HeartRates::getAge() {
int currentYear, currentMonth, currentDay;
cout << "请输入当前日期(年 月 日,空格分隔): ";
cin >> currentYear >> currentMonth >> currentDay;
int age = currentYear - birthYear;
// 生日未过,年龄减1
if(currentMonth < birthMonth ||
(currentMonth == birthMonth && currentDay < birthDay)) {
age--;
}
return age >= 0 ? age : 0; // 处理未来日期情况
}
根据AHA标准实现心率计算:
cpp复制void HeartRates::getTargetHeartRate() const {
int age = getAge(); // 注意:这里实际需要先调用getAge()
int maxHR = 220 - age;
cout << "目标心率范围: "
<< static_cast<int>(maxHR * 0.5 + 0.5) << "-" // 四舍五入
<< static_cast<int>(maxHR * 0.85 + 0.5)
<< " bpm" << endl;
}
医疗系统注意:实际项目中,心率公式应该设计为可配置的,因为不同机构可能使用不同计算公式(如208-0.7×年龄)。
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据意外变化 | 忘记const修饰get方法 | 所有不修改对象的get方法加const |
| 无效参数导致崩溃 | 缺少参数校验 | 在set方法和构造函数中添加校验 |
| 计算精度丢失 | 使用int进行除法 | 先乘后除或使用浮点数 |
| 对象状态不一致 | 直接访问成员变量 | 统一通过方法访问成员 |
cpp复制std::string getFirstName() const { return firstName; }
频繁调用的小函数使用inline修饰
对于大型对象,传递const引用而非值:
cpp复制void setFirstName(const std::string& first);
cpp复制Employee(std::string&& first, std::string&& last, int sal)
: firstName(std::move(first)), lastName(std::move(last)) {...}
在实际项目中,这些类还需要考虑:
例如,工业级的Account类可能这样处理错误:
cpp复制void Account::debit(int amount) {
if(amount <= 0) {
throw InvalidAmountException("取款金额必须为正数");
}
if(amount > balance) {
throw InsufficientBalanceException("余额不足");
}
balance -= amount;
TransactionLogger::log(this, -amount); // 记录交易
}
通过这五个类的完整实现,我们系统性地实践了面向对象设计的核心原则。建议读者可以尝试扩展这些类的功能,比如为Invoice类添加折扣计算,或者为Employee类添加职称属性,进一步巩固OOP技能。