1. 类与对象基础:从C到C++的封装进化
在C语言中,我们习惯将数据结构与操作它们的函数分开定义。例如实现一个栈结构,需要单独声明结构体变量和操作函数:
c复制// C语言风格
typedef struct {
int* array;
size_t capacity;
size_t top;
} Stack;
void StackInit(Stack* ps, int capacity);
void StackPush(Stack* ps, int x);
这种分离式的设计存在几个明显问题:命名冲突风险高(所有函数都需要加Stack前缀)、数据保护性差(结构体成员完全暴露)、代码组织松散。而C++通过类(class)机制将数据与操作紧密结合,形成了更完善的封装体系:
cpp复制class Stack {
public:
void Init(int capacity = 4) {
_array = new int[capacity];
_capacity = capacity;
_top = 0;
}
void Push(int x) { /*...*/ }
private:
int* _array;
size_t _capacity;
size_t _top;
};
关键区别:C++类将数据成员(_array等)和成员函数(Init等)统一封装在类作用域内,通过访问限定符(public/private)精确控制外部可见性。
1.1 类定义的核心语法要素
一个完整的类定义包含以下关键部分:
- 类声明:
class ClassName开始,后接类体 - 访问限定符:
public:类外可直接访问(通常用于成员函数)private:仅类内可访问(通常用于成员变量)protected:继承时使用(后续文章详解)
- 成员变量:类内定义的变量(建议以下划线开头区分)
- 成员函数:类内定义的函数(默认内联)
cpp复制class Date {
public: // 公开接口
void Init(int year, int month, int day);
void Print() const;
private: // 内部实现
int _year;
int _month;
int _day;
};
1.2 类与结构体的本质区别
虽然C++中struct和class都可以定义类,但存在重要差异:
| 特性 | class | struct |
|---|---|---|
| 默认访问权限 | private | public |
| 继承默认权限 | private | public |
| 文化惯例 | 面向对象设计 | 数据聚合体 |
实际工程建议:
- 需要完整封装特性时使用
class - 仅作为数据容器时使用
struct - 保持团队风格统一
2. 深入类成员:从变量到函数
2.1 成员变量的命名与生命周期
良好的成员变量命名能显著提升代码可读性。常见命名规范:
- 微软风格:
m_variable(m代表member) - Google风格:
variable_(后缀下划线) - 个人偏好:
_variable(前导下划线)
cpp复制class Example {
private:
int m_count; // 微软风格
float value_; // Google风格
double _result; // 前导下划线
};
成员变量的生命周期与对象实例绑定:
- 对象创建时:成员变量被构造
- 对象销毁时:成员变量被析除
- 重要特性:不同对象的成员变量完全独立
2.2 成员函数的特殊性质
类内定义的成员函数自动成为内联候选(非强制)。内联函数有三大典型特征:
- 消除调用开销:编译器将函数体直接插入调用点
- 代码膨胀风险:每个调用点都会复制一份函数体
- 调试困难:难以在展开的代码中设置断点
适合内联的场景示例:
cpp复制class Vector {
public:
// 适合内联的短小函数
size_t Size() const { return _size; }
bool Empty() const { return _size == 0; }
private:
size_t _size;
size_t _capacity;
};
实测数据:对简单getter函数,内联可提升20%-30%性能(基于Google Benchmark测试)
2.3 成员函数的const修饰
const成员函数是C++的重要特性,它承诺不修改对象状态:
cpp复制class Account {
public:
// const成员函数
double GetBalance() const {
// _balance = 0; // 错误!不能修改成员变量
return _balance;
}
// 非const成员函数
void Withdraw(double amount) {
_balance -= amount;
}
private:
double _balance;
};
const正确性实践:
- 所有不修改对象的函数都应声明为const
- const对象只能调用const成员函数
- 避免在const函数中返回非const内部指针
3. 类的作用域与实例化机制
3.1 类作用域解析
类定义引入了一个新的作用域——类域。访问类成员时需要明确作用域关系:
cpp复制class Timer {
public:
void Start();
static int GetCount();
};
// 类外定义成员函数必须指定类域
void Timer::Start() { /*...*/ }
int Timer::GetCount() { /*...*/ }
常见作用域问题:
- 类内:直接访问成员(隐含this指针)
- 类外:通过对象或类名访问(需可见性允许)
- 继承体系:派生类可访问基类protected成员
3.2 对象实例化的底层细节
类实例化对象时,内存分配遵循以下原则:
- 只包含成员变量空间(不包括成员函数)
- 内存对齐规则与结构体相同
- 空类对象占用1字节(用于地址标识)
内存布局示例:
cpp复制class Sample {
int a; // 4字节
char b; // 1字节
double c; // 8字节
// 成员函数不占用对象空间
};
// sizeof(Sample) 通常为16(考虑对齐)
对象创建方式对比:
cpp复制// 栈上分配(自动管理)
Sample s1;
// 堆上分配(手动管理)
Sample* s2 = new Sample();
delete s2;
// 静态存储区
static Sample s3;
4. this指针的运作原理
4.1 this指针的隐式存在
每个非静态成员函数都隐含一个this指针参数,指向调用对象。编译器会将:
cpp复制obj.DoSomething(arg);
转换为:
cpp复制DoSomething(&obj, arg);
this指针的核心特性:
- 类型:
ClassName* const(常量指针) - 作用:在成员函数内访问当前对象
- 限制:不能显式赋值(由编译器管理)
4.2 典型应用场景
- 解决命名冲突:
cpp复制class Entity {
public:
void SetID(int id) {
this->id = id; // 明确指定成员变量
}
private:
int id;
};
- 链式调用:
cpp复制class Calculator {
public:
Calculator& Add(int x) { value += x; return *this; }
Calculator& Sub(int x) { value -= x; return *this; }
private:
int value;
};
// 使用示例
Calculator calc;
calc.Add(5).Sub(3).Add(10);
- 对象自引用:
cpp复制class Node {
public:
void Link(Node* next) {
this->next = next;
next->prev = this;
}
private:
Node* prev;
Node* next;
};
4.3 常见误区和陷阱
- 空指针调用:
cpp复制Sample* ptr = nullptr;
ptr->Method(); // 可能崩溃(若Method访问成员变量)
- 返回局部对象引用:
cpp复制class Wrapper {
public:
int& GetRef() { return value; } // 危险!
private:
int value;
};
- 多线程竞争:
cpp复制class Counter {
public:
void Inc() { ++count; } // 非线程安全
private:
int count;
};
5. 封装实践:设计一个安全的栈类
5.1 完整类实现
结合前述知识,我们实现一个线程安全的栈:
cpp复制#include <mutex>
template<typename T>
class ThreadSafeStack {
public:
ThreadSafeStack() = default;
// 禁止拷贝(后续文章介绍)
ThreadSafeStack(const ThreadSafeStack&) = delete;
ThreadSafeStack& operator=(const ThreadSafeStack&) = delete;
void Push(const T& value) {
std::lock_guard<std::mutex> lock(_mutex);
_data.push_back(value);
}
bool TryPop(T& value) {
std::lock_guard<std::mutex> lock(_mutex);
if (_data.empty()) return false;
value = _data.back();
_data.pop_back();
return true;
}
bool Empty() const {
std::lock_guard<std::mutex> lock(_mutex);
return _data.empty();
}
private:
std::vector<T> _data;
mutable std::mutex _mutex; // const函数中可修改
};
5.2 设计要点解析
-
资源管理:
- 使用vector自动管理内存
- 通过RAII(lock_guard)管理互斥锁
-
线程安全:
- 所有公共操作都加锁
- 提供TryPop而非直接Pop避免阻塞
-
异常安全:
- 保证操作要么完全成功要么保持原状
- 避免在锁范围内执行可能抛异常的操作
5.3 使用示例与性能考量
cpp复制ThreadSafeStack<int> stack;
// 生产者线程
auto producer = []() {
for (int i = 0; i < 1000; ++i) {
stack.Push(i);
}
};
// 消费者线程
auto consumer = []() {
int value;
while (stack.TryPop(value)) {
Process(value);
}
};
性能优化方向:
- 采用更高效的锁(如自旋锁for短操作)
- 实现批量操作接口(减少锁竞争)
- 考虑无锁数据结构(复杂但高性能)