1. 从C到C++:为什么需要类和对象?
我第一次接触C++的类和对象概念时,内心充满疑惑——C语言的结构体(struct)不是已经能很好地组织数据了吗?直到在实际项目中遇到一个图形处理的需求,才真正理解面向对象编程的价值。
假设我们要处理屏幕上1000个矩形,用C语言的结构体方式:
c复制struct Rect {
int x, y;
int width, height;
};
void draw_rect(struct Rect* r) {
// 绘制逻辑
}
void move_rect(struct Rect* r, int dx, int dy) {
r->x += dx;
r->y += dy;
}
这种方式下,数据和操作是分离的。当项目规模扩大时,这种分离会导致代码难以维护。而C++的类将数据和操作封装在一起:
cpp复制class Rect {
public:
int x, y;
int width, height;
void draw() {
// 绘制逻辑
}
void move(int dx, int dy) {
x += dx;
y += dy;
}
};
这种封装带来的最直接好处是:当我们调用rect.move()时,不需要再传递rect指针,因为move()函数天然就知道它要操作哪个对象的数据。
2. 类的基本结构:从蓝图到实体
2.1 类定义的核心要素
一个完整的类定义通常包含以下部分:
cpp复制class Student { // 类名
private: // 访问修饰符
// 私有成员变量
std::string name;
int age;
public:
// 构造函数
Student(const std::string& n, int a) : name(n), age(a) {}
// 成员函数
void introduce() {
std::cout << "我叫" << name << ", 今年" << age << "岁" << std::endl;
}
// 访问器(getter)
std::string getName() const { return name; }
// 修改器(setter)
void setAge(int a) { age = a; }
};
这里有几个关键点需要注意:
- 类名一般采用大驼峰命名法
- 成员变量通常加前缀m_或后缀_以示区分(可选)
- getter函数应该声明为const,表示不会修改对象状态
2.2 对象实例化的三种方式
cpp复制// 栈上分配
Student s1("张三", 20); // 直接初始化
Student s2 = {"李四", 21}; // 列表初始化(C++11)
// 堆上分配
Student* s3 = new Student("王五", 22);
// 注意区分以下两种写法
Student s4; // 调用默认构造函数
Student s5(); // 这是一个函数声明!不是对象创建
实际开发中,我推荐优先使用栈上分配,除非对象生命周期需要延长。堆上分配记得配合delete使用,否则会导致内存泄漏。
3. 深入理解访问控制
3.1 三种访问修饰符的区别
| 修饰符 | 类内访问 | 派生类访问 | 外部访问 |
|---|---|---|---|
| private | ✔ | ✖ | ✖ |
| protected | ✔ | ✔ | ✖ |
| public | ✔ | ✔ | ✔ |
一个常见的误区是过度使用public。根据封装原则,成员变量应该尽量private,通过getter/setter提供访问接口。
3.2 类与结构体的微妙差异
虽然class和struct在C++中几乎可以互换,但社区形成了以下约定俗成的用法:
cpp复制// 用struct表示纯数据结构
struct Point {
int x;
int y;
};
// 用class表示有行为的对象
class Circle {
private:
Point center;
double radius;
public:
void draw() { /*...*/ }
double area() const { return 3.14 * radius * radius; }
};
技术层面上,唯一区别是默认访问权限:
- struct默认public
- class默认private
4. 构造函数与初始化
4.1 构造函数的特殊性质
构造函数有几个独特之处:
- 没有返回类型(连void都没有)
- 名字必须与类名完全相同
- 可以被重载
cpp复制class Date {
private:
int year, month, day;
public:
// 默认构造函数
Date() : year(1970), month(1), day(1) {}
// 带参数的构造函数
Date(int y, int m, int d) : year(y), month(m), day(d) {}
// 委托构造函数(C++11)
Date(int y) : Date(y, 1, 1) {}
};
4.2 初始化列表的重要性
在早期项目中,我曾这样写构造函数:
cpp复制// 不推荐的写法
Date(int y, int m, int d) {
year = y;
month = m;
day = d;
}
后来发现初始化列表才是更优选择:
cpp复制// 推荐的写法
Date(int y, int m, int d) : year(y), month(m), day(d) {}
原因有三:
- 效率更高(避免先默认初始化再赋值)
- 对const成员和引用成员必须使用初始化列表
- 初始化顺序更明确
5. 类的高级特性
5.1 static成员:属于类而非对象
static成员用于表示类级别的属性和行为:
cpp复制class Employee {
private:
static int count; // 记录创建的员工总数
public:
Employee() { ++count; }
~Employee() { --count; }
static int getCount() { return count; }
};
// 必须在类外初始化static成员
int Employee::count = 0;
使用场景:
- 统计对象数量
- 共享配置信息
- 工具类方法
5.2 const成员函数:承诺不修改对象
const成员函数是C++的重要特性,它向编译器承诺不会修改对象状态:
cpp复制class BankAccount {
private:
double balance;
public:
// const成员函数
double getBalance() const {
// balance = 0; // 错误!不能在const函数中修改成员
return balance;
}
// 非const成员函数
void withdraw(double amount) {
balance -= amount;
}
};
const正确性带来的好处:
- 使接口更清晰
- 允许const对象调用const成员函数
- 帮助编译器优化
6. 实战经验与常见陷阱
6.1 对象生命周期管理
新手常犯的错误是混淆栈对象和堆对象的生命周期:
cpp复制void problematic() {
Student s("临时", 18); // 栈对象,函数结束自动销毁
Student* p = new Student("堆上", 19);
// 忘记delete p; 导致内存泄漏
}
解决方案:
- 优先使用栈对象
- 使用智能指针管理堆对象
- 遵循RAII原则
6.2 this指针的妙用
this指针在以下场景特别有用:
cpp复制class Example {
private:
int value;
public:
void setValue(int value) {
// 参数名与成员变量名相同时
this->value = value;
}
Example& chain() {
// 支持链式调用
return *this;
}
};
我曾在一个日志类中大量使用this返回引用来实现流畅接口:
cpp复制logger.info("消息").tag("重要").color(red);
7. 现代C++中的类特性
7.1 default和delete修饰符
C++11引入了显式控制默认函数的机制:
cpp复制class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
7.2 移动语义与右值引用
现代C++最重要的特性之一:
cpp复制class Buffer {
private:
char* data;
size_t size;
public:
// 移动构造函数
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
理解这些概念需要时间,但一旦掌握,可以大幅提升程序效率。
8. 设计高质量的类
根据我的项目经验,设计良好的类应该:
- 职责单一(一个类只做一件事)
- 封装良好(隐藏实现细节)
- 接口简洁(方法数量适中)
- 易于扩展(考虑未来需求变化)
- 异常安全(保证操作原子性)
以文件操作为例,比较差的实现:
cpp复制// 不好的设计:功能太多
class FileManager {
public:
void open();
void close();
void read();
void write();
void compress();
void encrypt();
void sendByEmail();
};
更好的设计:
cpp复制// 好的设计:职责分离
class File {
public:
void open();
void close();
void read();
void write();
};
class FileCompressor {
public:
void compress(File& file);
};
class FileEncryptor {
public:
void encrypt(File& file);
};
这种设计更符合SOLID原则,每个类都有明确的职责,更容易维护和扩展。
