1. 从结构到类的跨越
记得刚学C++那会儿,我把类简单地理解为"带函数的struct"。直到在项目中踩了几个坑才明白,这种认知实在太肤浅。类和结构体虽然语法相似,但设计理念天差地别。结构体只是数据的容器,而类则是数据和行为的有机整体。
在嵌入式项目中,我曾用结构体管理传感器数据:
cpp复制struct SensorData {
float temperature;
float humidity;
uint32_t timestamp;
};
后来需求变更,需要增加数据校验功能。如果继续用结构体,校验逻辑就得散落在各处代码中。改用类封装后:
cpp复制class SensorData {
public:
bool validate() const {
return (temperature > -40.0f) &&
(humidity >= 0.0f && humidity <= 100.0f);
}
// 其他成员函数...
private:
float temperature;
float humidity;
uint32_t timestamp;
};
这个转变让代码维护性大幅提升。类的真正威力在于它通过访问控制、成员函数等机制,实现了数据与行为的绑定。
2. 深入理解构造函数
2.1 构造函数的本质
构造函数最容易被误解的地方就是它的调用时机。我曾在一个多线程项目里踩过坑:以为对象定义后构造函数会立即执行,结果因为线程调度延迟导致对象未初始化就被使用。实际上,构造函数的调用是对象生命周期开始的标志。
一个完整的构造函数应该保证:
- 建立类不变量(确保对象始终有效的条件)
- 获取必要资源
- 达到"就绪"状态
比如智能指针类的构造函数:
cpp复制template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T* ptr = nullptr)
: ptr_(ptr), ref_count_(new size_t(1)) {
if (ptr_ == nullptr) {
*ref_count_ = 0; // 空指针的特殊处理
}
}
// ...
};
2.2 委托构造的妙用
C++11引入的委托构造特性可以大幅减少重复代码。在开发网络库时,我遇到过多个构造函数参数组合的情况:
cpp复制class Socket {
public:
Socket() : Socket(DEFAULT_PORT, AF_INET) {}
explicit Socket(int port) : Socket(port, AF_INET) {}
Socket(int port, int domain) {
// 实际的初始化代码
fd_ = socket(domain, SOCK_STREAM, 0);
if (fd_ == -1) throw SocketException();
// ...
}
};
这种链式调用确保所有构造路径最终都汇聚到同一个主构造函数,避免初始化逻辑分散。
3. 拷贝控制的陷阱
3.1 三/五法则的实战意义
在内存管理密集型项目中,违反三/五法则会导致灾难性后果。我曾调试过一个内存泄漏问题,最终发现是因为类实现了析构函数但没禁用拷贝:
cpp复制class Buffer {
public:
Buffer(size_t size) : data_(new char[size]), size_(size) {}
~Buffer() { delete[] data_; }
// 缺失拷贝控制
private:
char* data_;
size_t size_;
};
当这种对象被拷贝时,两个实例会指向同一块内存,析构时导致双重释放。正确的做法是显式定义或删除拷贝操作:
cpp复制class Buffer {
public:
// ...
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
// 或者实现深拷贝
};
3.2 移动语义的合理使用
移动语义不是万能的。在实现线程池时,我曾过度使用移动语义导致问题:
cpp复制class Task {
public:
Task(Task&& other) noexcept
: func_(std::move(other.func_)),
promise_(std::move(other.promise_)) {}
// ...
};
后来发现某些lambda捕获的局部变量在移动后失效。经验法则是:
- 对资源句柄(如指针、文件描述符)使用移动
- 对可能持有局部引用的对象谨慎移动
- 对小型平凡类型直接拷贝可能更高效
4. 运算符重载的艺术
4.1 数学运算符的最佳实践
为矩阵类重载运算符时,我总结出几个要点:
- 非成员函数实现对称运算符(如+、-)
- 成员函数实现复合赋值(如+=)
- 返回值优化要考虑NRVO
cpp复制class Matrix {
public:
Matrix& operator+=(const Matrix& rhs) {
// 实现复合赋值
return *this;
}
};
// 非成员函数实现普通加法
Matrix operator+(Matrix lhs, const Matrix& rhs) {
lhs += rhs; // 利用拷贝+复合赋值
return lhs; // 可能触发NRVO
}
4.2 流运算符的注意事项
重载<<运算符时常见错误是忘记返回流引用:
cpp复制std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
os << obj.data(); // 正确:返回os引用
return os;
}
我曾见过这样的错误实现:
cpp复制void operator<<(std::ostream& os, const MyClass& obj) {
os << obj.data(); // 错误:无法链式调用
}
这会导致无法进行链式输出如std::cout << a << b。
5. 类设计的进阶技巧
5.1 类型安全的枚举
传统枚举容易造成命名污染和隐式转换。在游戏开发中,我们改用枚举类:
cpp复制enum class GameState : uint8_t {
Loading,
Menu,
Playing,
Paused
};
void setState(GameState state);
这样使用时必须显式指定作用域GameState::Loading,且不会隐式转换为整数。
5.2 策略模式的类实现
通过将算法封装到类中,可以实现运行时策略切换。比如排序策略:
cpp复制class SortStrategy {
public:
virtual void sort(Container&) const = 0;
virtual ~SortStrategy() = default;
};
class QuickSort : public SortStrategy { /*...*/ };
class MergeSort : public SortStrategy { /*...*/ };
class Sorter {
public:
void setStrategy(std::unique_ptr<SortStrategy> strategy) {
strategy_ = std::move(strategy);
}
void execute(Container& c) {
strategy_->sort(c);
}
private:
std::unique_ptr<SortStrategy> strategy_;
};
这种模式在需要动态切换算法的场景非常有用。
6. 类模板实战经验
6.1 模板特化的时机
在为不同数据类型优化算法时,模板特化能派上大用场。比如内存拷贝的优化:
cpp复制template<typename T>
void fastCopy(T* dest, const T* src, size_t count) {
// 通用实现
std::copy(src, src + count, dest);
}
template<>
void fastCopy<float>(float* dest, const float* src, size_t count) {
// 使用SIMD指令优化float拷贝
_mm256_store_ps(dest, _mm256_load_ps(src));
}
但要注意过度特化会导致代码膨胀,一般只在性能关键路径使用。
6.2 CRTP模式的应用
奇异递归模板模式(CRTP)可以实现静态多态。在实现对象池时:
cpp复制template<typename Derived>
class ObjectPool {
public:
Derived* acquire() {
if (pool_.empty()) {
return new Derived;
}
auto obj = pool_.back();
pool_.pop_back();
return obj;
}
// ...
};
class MyClass : public ObjectPool<MyClass> {
// ...
};
这种方式避免了虚函数开销,适合性能敏感场景。
7. 异常安全的类设计
7.1 RAII原则的贯彻
资源获取即初始化(RAII)是C++的核心哲学。在文件操作类中:
cpp复制class File {
public:
explicit File(const std::string& path)
: handle_(fopen(path.c_str(), "rb")) {
if (!handle_) throw FileOpenError();
}
~File() {
if (handle_) fclose(handle_);
}
// 禁用拷贝
File(const File&) = delete;
File& operator=(const File&) = delete;
// 允许移动
File(File&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
private:
FILE* handle_;
};
这种设计确保文件句柄在任何情况下都能正确关闭。
7.2 强异常保证的实现
提供强异常保证的类需要精心设计。比如数组类的插入操作:
cpp复制template<typename T>
class Vector {
public:
void insert(size_t pos, const T& value) {
if (pos > size_) throw OutOfRange();
if (size_ == capacity_) {
// 先分配新内存,成功后再修改状态
auto new_capacity = capacity_ * 2;
T* new_data = static_cast<T*>(::operator new(new_capacity * sizeof(T)));
// 转移现有元素
for (size_t i = 0; i < pos; ++i) {
new (new_data + i) T(std::move(data_[i]));
}
new (new_data + pos) T(value);
for (size_t i = pos; i < size_; ++i) {
new (new_data + i + 1) T(std::move(data_[i]));
}
// 所有操作成功后才替换指针
::operator delete(data_);
data_ = new_data;
capacity_ = new_capacity;
} else {
// 就地构造
new (data_ + size_) T(value);
for (size_t i = size_; i > pos; --i) {
data_[i] = std::move(data_[i-1]);
}
data_[pos] = value;
}
++size_;
}
};
这种实现确保在异常发生时对象状态不变。
8. 性能优化的类技巧
8.1 小对象优化
对于小型对象,直接存储在容器中比通过指针间接访问更高效。字符串类的实现常用此技巧:
cpp复制class String {
static const size_t SSO_MAX = 15;
union {
struct {
char* ptr;
size_t size;
size_t capacity;
} large;
char small[SSO_MAX + 1];
};
bool isSmall() const {
return small[SSO_MAX] == '\0';
}
public:
// 接口实现...
};
当字符串长度小于SSO_MAX时,直接使用栈空间存储,避免堆分配。
8.2 热路径优化
在游戏引擎开发中,对高频调用的类方法需要特别优化。比如向量类的点积运算:
cpp复制class Vector3 {
public:
float dot(const Vector3& other) const noexcept {
#if defined(__SSE__)
__m128 a = _mm_loadu_ps(&x);
__m128 b = _mm_loadu_ps(&other.x);
__m128 dp = _mm_dp_ps(a, b, 0x71);
return _mm_cvtss_f32(dp);
#else
return x*other.x + y*other.y + z*other.z;
#endif
}
private:
alignas(16) float x, y, z, w;
};
通过平台特定的指令集优化,可以显著提升性能。
9. 多线程环境下的类设计
9.1 线程安全的单例模式
双重检查锁定模式需要谨慎实现:
cpp复制class Singleton {
public:
static Singleton& instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
instance_.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
private:
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
// 其他私有成员...
};
C++11后的更简单实现:
cpp复制Singleton& Singleton::instance() {
static Singleton instance;
return instance;
}
编译器会保证静态局部变量的线程安全初始化。
9.2 无锁数据结构设计
实现线程安全的队列时,无锁设计可以避免锁竞争:
cpp复制template<typename T>
class LockFreeQueue {
public:
void enqueue(const T& value) {
Node* newNode = new Node(value);
Node* oldTail = tail_.load();
while (!tail_.compare_exchange_weak(oldTail, newNode)) {}
oldTail->next.store(newNode);
}
bool dequeue(T& result) {
Node* oldHead = head_.load();
Node* next;
do {
next = oldHead->next.load();
if (next == nullptr) return false;
} while (!head_.compare_exchange_weak(oldHead, next));
result = next->value;
delete oldHead;
return true;
}
private:
struct Node {
T value;
std::atomic<Node*> next;
Node(const T& val) : value(val), next(nullptr) {}
};
std::atomic<Node*> head_;
std::atomic<Node*> tail_;
};
这种实现适合高并发场景,但需要仔细处理内存顺序和ABA问题。