类、对象、构造函数与析构函数:C++ 如何表达不变量
C 的结构体把数据字段聚合起来。 C++ 的类进一步把数据和操作放在一起,并用构造函数建立不变量,用析构函数释放资源,用访问控制隐藏内部状态。 学习类不是为了把函数塞进结构体,而是为了让对象从创建到销毁都保持正确。
类定义
class Counter {
public:
void increment() {
++value_;
}
int value() const {
return value_;
}
private:
int value_ = 0;
};
public 部分是外部接口。
private 部分是内部实现。
外部代码不能直接修改 value_。
这让类可以维护自己的不变量。
对象是类的实例
Counter counter;
counter.increment();
std::cout << counter.value() << '\n';
counter 是一个对象。
对象有自己的成员变量。
成员函数通过隐式的 this 指针访问当前对象。
void increment() {
++this->value_;
}
通常不需要显式写 this,但知道它存在有助于理解成员函数。
构造函数建立初始状态
构造函数在对象创建时运行。 它没有返回类型,名字和类名相同。
class Buffer {
public:
explicit Buffer(std::size_t size)
: size_(size), data_(new char[size]) {}
private:
std::size_t size_;
char* data_;
};
成员初始化列表在函数体之前初始化成员。 资源成员应尽量在初始化列表里建立。
explicit 防止意外隐式转换
class Port {
public:
explicit Port(int value) : value_(value) {}
private:
int value_;
};
没有 explicit 时,某些单参数构造函数可能被用作隐式转换。
这会让函数调用变得不透明。
工程代码中,单参数构造函数默认考虑 explicit。
析构函数释放资源
析构函数在对象销毁时运行。
名字是 ~ClassName。
class Buffer {
public:
explicit Buffer(std::size_t size)
: size_(size), data_(new char[size]) {}
~Buffer() {
delete[] data_;
}
private:
std::size_t size_;
char* data_;
};
这段代码展示了析构机制。
现代 C++ 更推荐用 std::vector<char> 或 std::unique_ptr<char[]> 管理内存,减少手写 delete。
RAII 的基本形状
class File {
public:
explicit File(const char* path) {
file_ = std::fopen(path, "rb");
if (file_ == nullptr) {
throw std::runtime_error("open failed");
}
}
~File() {
if (file_ != nullptr) {
std::fclose(file_);
}
}
private:
std::FILE* file_ = nullptr;
};
对象构造成功,资源有效。 对象析构,资源释放。 这让错误路径更可靠。
成员函数的 const
int value() const {
return value_;
}
成员函数后的 const 表示它不会修改当前对象的可观察状态。
这让 const Counter& 也能调用它。
void print(const Counter& counter) {
std::cout << counter.value() << '\n';
}
只读操作应该标记 const。
这能让接口边界更清晰。
封装不是把字段藏起来而已
封装的目标是保护不变量。
class Percent {
public:
explicit Percent(int value) {
if (value < 0 || value > 100) {
throw std::out_of_range("percent");
}
value_ = value;
}
int value() const {
return value_;
}
private:
int value_ = 0;
};
如果字段公开,外部随时能写成 -100。
私有字段和构造校验一起保证对象始终合法。
struct 和 class
C++ 中 struct 和 class 很像。
主要默认访问权限不同:
struct默认 public。class默认 private。
工程习惯:
- 简单数据聚合用
struct。 - 有不变量和行为的对象用
class。
struct Point {
int x;
int y;
};
Point 只是数据。
Percent 有合法范围,适合 class。
拷贝构造和赋值
Counter a;
Counter b = a;
如果类没有特殊资源,默认拷贝通常可用。 如果类拥有裸资源,默认浅拷贝可能出错。
class Buffer {
char* data_;
};
复制这个对象只会复制指针值。 两个对象可能释放同一块内存。 后续 RAII 文章会深入 Rule of Three/Five/Zero。
对象析构顺序
局部对象离开作用域时按创建相反顺序析构。
{
File a("a.txt");
File b("b.txt");
}
先析构 b,再析构 a。
成员变量按声明顺序构造,按相反顺序析构。
声明顺序可以表达资源依赖。
静态成员
静态成员属于类,不属于某个对象。
class IdGenerator {
public:
static int next();
};
静态成员适合无状态工具或共享状态。 如果包含可变共享状态,要做并发审计。 不要把静态成员当作全局变量的另一种写法。
友元要少用
friend 可以让外部函数或类访问 private 成员。
它会扩大封装边界。
class Value {
friend bool equal(const Value&, const Value&);
};
友元适合操作符、测试或紧密协作类型。 滥用会让 private 失去意义。
工程风险
类和对象常见风险:
- 构造函数没有建立完整不变量。
- 析构函数抛异常。
- 拥有资源的类被默认浅拷贝。
- 成员初始化顺序和声明顺序不一致。
- getter/setter 把内部状态完全暴露。
- 静态可变成员造成并发问题。
const成员函数缺失导致接口不可用。- 单参数构造函数缺少
explicit。
这些问题会在对象模型深入后更加明显。
小结
类的价值是表达对象不变量。 构造函数负责让对象从一开始就合法。 析构函数负责让资源在离开作用域时释放。 访问控制负责保护内部状态。 C++ 的工程能力从这里开始:把人为纪律转化为类型和生命周期规则。