C++ Core Guidelines 阅读笔记(2):函数与类设计
发布于:
前言
本文是 C++ Core Guidelines 阅读笔记的第二篇,重点讨论函数设计和类设计的规范。良好的函数和类设计是写出高质量 C++ 代码的基础。
1. 函数设计规范
函数是程序的基本构建块,良好的函数设计是高质量代码的基础。
1.1 函数应该简短
函数应该足够短,能够一眼理解其功能:
// 好的:简短函数
bool is_valid_email(const std::string& email) {
return email.find('@') != std::string::npos &&
email.find('.') != std::string::npos;
}
// 不好:过长函数(超过 50 行)
void process_order(Order& order) {
// 100+ 行代码,难以理解和维护
// 包含多个职责,难以测试
}
1.2 函数应该做一件事
单一职责原则适用于函数设计:
// 好的:单一职责
void validate_user(const User& user);
void save_user(const User& user);
void notify_user(const User& user);
// 不好:做多件事
void validate_and_save_user(User& user) {
validate_user(user); // 职责 1
save_user(user); // 职责 2
notify_user(user); // 职责 3
// 违反单一职责,难以测试和维护
}
1.3 函数参数数量
函数参数应该尽可能少,通常不超过 3 个:
// 好的:参数少(1-3 个)
void draw_circle(const Point& center, double radius);
void process(int x, int y, int z); // 3 个参数,可接受
// 不好:参数太多
void draw_shape(double x, double y, double w, double h,
Color c, Style s, int z); // 7 个参数,太多
1.4 使用结构体传递多个参数
当参数较多时,使用结构体封装:
// 好的:使用结构体
struct DrawParams {
Point center;
double radius;
Color color;
Style style;
int z_order;
};
void draw_circle(const DrawParams& params);
// 使用:DrawParams params{center, 10.0, Color::Red, Style::Solid, 0};
// draw_circle(params);
// 不好:多个参数
void draw_circle(double x, double y, double r, Color c, Style s, int z);
// 使用:draw_circle(10, 20, 5.0, Color::Red, Style::Solid, 0);
// 容易传错参数顺序
2. 函数参数传递
2.1 参数传递规则
// 小对象(< 3 words):值传递
void process(int x, double y);
// 大对象:const 引用传递
void process(const std::vector<int>& vec);
// 需要修改:非 const 引用传递
void modify(std::vector<int>& vec);
// 移动语义:右值引用传递
void take_ownership(std::vector<int>&& vec);
2.2 避免输出参数
// 好的:返回值
std::vector<int> compute_result();
// 不好:输出参数
void compute_result(std::vector<int>& out);
2.3 使用默认参数
// 好的:默认参数
void draw_circle(const Point& center, double radius = 1.0,
Color color = Color::Black);
// 不好:函数重载(如果只是参数不同)
void draw_circle(const Point& center);
void draw_circle(const Point& center, double radius);
void draw_circle(const Point& center, double radius, Color color);
3. 类设计原则
类是面向对象编程的核心,良好的类设计遵循特定的原则。
3.1 类应该表示一个概念
类应该表示一个清晰的概念或实体:
// 好的:表示一个概念
class Circle {
private:
Point center_;
double radius_;
public:
void draw() const;
double area() const;
double circumference() const;
// 所有操作都与 Circle 概念相关
};
// 不好:不表示一个概念
class Utility { // 工具类,没有明确概念
public:
static void func1(); // 功能 1
static void func2(); // 功能 2
static void func3(); // 功能 3
// 没有统一的概念,只是函数的集合
};
3.2 保持类小而专注
类应该小而专注,遵循单一职责原则:
// 好的:小而专注
class FileReader {
public:
std::string read(const std::string& path);
// 只负责读取文件
};
class FileWriter {
public:
void write(const std::string& path, const std::string& content);
// 只负责写入文件
};
// 不好:类太大
class FileManager { // 包含太多功能
public:
void read(); // 职责 1:读取
void write(); // 职责 2:写入
void compress(); // 职责 3:压缩
void encrypt(); // 职责 4:加密
void delete_file(); // 职责 5:删除
// 违反单一职责原则
};
3.3 使用组合而非继承
优先使用组合而非继承,除非是真正的 is-a 关系:
// 好的:组合
class Car {
private:
Engine engine_; // 组合:Car has an Engine
Wheel wheels_[4]; // 组合:Car has Wheels
Transmission transmission_; // 组合:Car has a Transmission
};
// 不好:不必要的继承
class Car : public Vehicle { // 如果不是真正的 is-a 关系
// 继承会带来不必要的耦合
};
3.4 类的职责划分
4. 数据封装
4.1 数据应该是私有的
// 好的:数据私有
class Point {
private:
double x_, y_;
public:
double x() const { return x_; }
double y() const { return y_; }
void set_x(double x) { x_ = x; }
void set_y(double y) { y_ = y; }
};
// 不好:数据公有
class Point {
public:
double x, y; // 公有数据
};
4.2 提供访问器
// 好的:提供访问器
class User {
private:
std::string name_;
public:
const std::string& name() const { return name_; }
void set_name(const std::string& name) { name_ = name; }
};
5. 构造函数
5.1 使用构造函数初始化列表
// 好的:初始化列表
class MyClass {
private:
int value_;
std::string name_;
public:
MyClass(int v, const std::string& n)
: value_(v), name_(n) {}
};
// 不好:在构造函数体内赋值
MyClass(int v, const std::string& n) {
value_ = v; // 先默认构造,再赋值
name_ = n;
}
5.2 使用 = default 和 = delete
class MyClass {
public:
MyClass() = default; // 使用默认构造函数
MyClass(const MyClass&) = delete; // 禁止拷贝
MyClass& operator=(const MyClass&) = delete;
MyClass(MyClass&&) = default; // 允许移动
MyClass& operator=(MyClass&&) = default;
};
6. 析构函数
6.1 基类应该有虚析构函数
// 好的:虚析构函数
class Base {
public:
virtual ~Base() = default;
};
// 不好:非虚析构函数
class Base {
public:
~Base() {} // 如果 Base 会被继承,应该是虚的
};
6.2 析构函数应该简单
// 好的:简单析构函数
class MyClass {
std::unique_ptr<Resource> resource_;
public:
~MyClass() = default; // 自动清理
};
// 不好:复杂析构函数
~MyClass() {
// 大量清理逻辑
// 可能抛出异常
}
7. 运算符重载
7.1 保持语义一致性
// 好的:语义一致
class Complex {
public:
Complex operator+(const Complex& other) const;
Complex& operator+=(const Complex& other);
};
// 通常实现为:
Complex operator+(const Complex& a, const Complex& b) {
Complex result = a;
result += b; // 复用 +=
return result;
}
7.2 避免过度重载
// 好的:只重载有意义的运算符
class Vector {
public:
Vector operator+(const Vector& other) const;
Vector& operator+=(const Vector& other);
};
// 不好:重载不常用的运算符
Vector operator%(const Vector& other) const; // % 对向量没有意义
8. 继承设计
8.1 使用 public 继承表示 is-a 关系
// 好的:is-a 关系
class Circle : public Shape {
// Circle is a Shape
};
// 不好:不是 is-a 关系
class Stack : public std::vector<int> { // Stack is not a vector
};
8.2 避免深继承层次
// 好的:浅继承层次
class Base {};
class Derived : public Base {};
// 不好:深继承层次
class Base {};
class Derived1 : public Base {};
class Derived2 : public Derived1 {};
class Derived3 : public Derived2 {}; // 太深
9. 接口设计
9.1 接口应该稳定
// 好的:稳定接口
class Database {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual ~Database() = default;
};
// 不好:频繁变化的接口
class Database {
public:
virtual void connect_v2() = 0; // 版本号在接口中
};
9.2 使用抽象接口
// 好的:抽象接口
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
// 不好:具体实现
class Circle {
public:
void draw() const {
// 具体实现
}
};
11. 实际工程案例
11.1 案例:函数重构
问题:函数过长,难以维护
// 原始代码:100+ 行
void process_order(Order& order) {
// 验证订单
if (order.items.empty()) {
// ...
}
// 计算价格
// 应用折扣
// 更新库存
// 发送通知
// ...
}
改进:拆分为多个小函数
// 改进代码:每个函数职责单一
void validate_order(const Order& order);
Price calculate_price(const Order& order);
void apply_discount(Order& order);
void update_inventory(const Order& order);
void send_notification(const Order& order);
void process_order(Order& order) {
validate_order(order);
calculate_price(order);
apply_discount(order);
update_inventory(order);
send_notification(order);
}
11.2 案例:类设计改进
问题:类太大,职责不清
// 原始设计:God Object
class UserManager {
// 用户管理
// 权限管理
// 通知管理
// 日志管理
// ...
};
改进:拆分为多个小类
// 改进设计:职责分离
class User {
// 用户数据和行为
};
class PermissionManager {
// 权限管理
};
class NotificationService {
// 通知服务
};
class Logger {
// 日志管理
};
12. 设计模式应用
12.1 策略模式
12.2 模板方法模式
// 模板方法模式
class Algorithm {
public:
void run() {
step1();
step2(); // 虚函数,子类可以重写
step3();
}
protected:
virtual void step2() = 0;
private:
void step1() {}
void step3() {}
};
13. 性能考虑
13.1 函数调用开销
13.2 类设计对性能的影响
// 好的:数据局部性好
class Point {
double x_, y_; // 数据紧凑
};
// 不好:数据分散
class Point {
std::unique_ptr<double> x_; // 间接访问
std::unique_ptr<double> y_;
};
14. 最佳实践检查清单
14.1 函数设计检查清单
- 函数简短:< 50 行
- 单一职责:只做一件事
- 参数少:< 4 个参数
- 命名清晰:表达意图
- 无副作用:纯函数更好
- 异常安全:正确处理异常
14.2 类设计检查清单
- 表示一个概念:有明确的语义
- 小而专注:< 500 行
- 单一职责:只负责一件事
- 数据私有:封装数据
- 接口稳定:避免频繁变化
- 使用组合:优先组合而非继承
15. 小结
函数和类的设计应该遵循单一职责、小而专注的原则。
核心要点:
- 函数设计:简短、单一职责、参数少、使用结构体传递多个参数
- 参数传递:根据对象大小和用途选择传递方式
- 类设计:表示一个概念、小而专注、使用组合
- 数据封装:数据私有、提供访问器
- 构造函数:使用初始化列表、= default/= delete
- 析构函数:基类虚析构、简单实现
- 运算符重载:保持语义一致性、避免过度重载
- 继承设计:public 继承表示 is-a、避免深层次
- 接口设计:稳定、抽象接口
遵循这些规范,可以设计出更清晰、更易维护的 C++ 代码。