在C++面向对象编程中,类和对象是最基础也是最重要的概念。类可以看作是一个自定义的数据类型,它封装了数据(成员变量)和操作这些数据的方法(成员函数)。而对象则是类的具体实例,就像用int定义变量一样,我们用类来创建对象。
每个C++类都有一组特殊的默认成员函数,它们由编译器自动生成,但也可以由程序员显式定义。这些函数包括:
这些函数共同构成了类的"生命周期管理"机制,理解它们的行为对于编写健壮的C++代码至关重要。
实际开发经验:在大型项目中,正确实现这些特殊成员函数可以避免90%以上的资源管理问题。特别是在涉及动态内存分配、文件句柄或网络连接等资源时,必须仔细考虑这些函数的实现。
构造函数的核心任务不是分配内存(对象的内存通常在创建时就已经分配),而是初始化对象的状态。这包括:
cpp复制class Date {
public:
// 无参构造函数
Date() : year_(1), month_(1), day_(1) {}
// 带参构造函数
Date(int year, int month, int day)
: year_(year), month_(month), day_(day) {
if (!IsValidDate()) {
throw std::invalid_argument("Invalid date");
}
}
private:
int year_;
int month_;
int day_;
bool IsValidDate() const {
// 验证日期合法性的逻辑
}
};
C++中有三种形式的默认构造函数:
关键点在于:这三种形式不能同时存在,因为它们都会导致调用时的歧义。
cpp复制class Example {
public:
// 形式1:编译器生成
// Example() = default;
// 形式2:无参构造
Example() { /*...*/ }
// 形式3:全缺省构造
// Example(int x = 0) { /*...*/ } // 与形式2冲突
};
初始化列表是构造函数的重要组成部分,它允许我们在对象构造时就完成成员变量的初始化,而不是先默认构造再赋值。
cpp复制class Student {
public:
Student(const std::string& name, int age)
: name_(name), // 直接调用string的拷贝构造
age_(age), // 直接初始化
scores_(new int[5]{}) { // 动态数组初始化
}
private:
std::string name_;
int age_;
int* scores_;
};
性能提示:对于非基本类型的成员变量,使用初始化列表通常比在构造函数体内赋值更高效,因为它避免了先默认构造再赋值的开销。
析构函数在对象生命周期结束时自动调用,具体时机包括:
cpp复制class FileHandler {
public:
FileHandler(const std::string& filename)
: file_(fopen(filename.c_str(), "r")) {
if (!file_) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandler() {
if (file_) {
fclose(file_);
file_ = nullptr;
}
}
private:
FILE* file_;
};
cpp复制class DatabaseConnection {
public:
virtual ~DatabaseConnection() {
if (connected_) {
try {
Disconnect();
} catch (...) {
// 记录错误但不抛出
}
}
}
protected:
virtual void Disconnect() {
// 断开数据库连接的具体实现
}
private:
bool connected_ = false;
};
拷贝构造函数用于创建一个对象的副本。对于包含动态资源的类,必须实现深拷贝。
cpp复制class String {
public:
String(const char* str = "") {
size_ = strlen(str);
data_ = new char[size_ + 1];
strcpy(data_, str);
}
// 拷贝构造函数
String(const String& other)
: size_(other.size_) {
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
}
~String() {
delete[] data_;
}
private:
char* data_;
size_t size_;
};
赋值运算符需要处理自赋值情况,并遵循"拷贝并交换"惯用法。
cpp复制class String {
public:
// 赋值运算符
String& operator=(String other) { // 注意:传值而非引用
swap(other);
return *this;
}
void swap(String& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
// ... 其他成员 ...
};
最佳实践:使用"拷贝并交换"惯用法可以简化赋值运算符的实现,并自动处理自赋值和异常安全问题。
cpp复制class Complex {
public:
Complex(double real = 0.0, double imag = 0.0)
: real_(real), imag_(imag) {}
// 算术运算符
Complex operator+(const Complex& rhs) const {
return Complex(real_ + rhs.real_, imag_ + rhs.imag_);
}
// 比较运算符
bool operator==(const Complex& rhs) const {
return real_ == rhs.real_ && imag_ == rhs.imag_;
}
// 流输出运算符(通常声明为友元)
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
return os << "(" << c.real_ << ", " << c.imag_ << ")";
}
private:
double real_;
double imag_;
};
cpp复制class Counter {
public:
// 前置++
Counter& operator++() {
++count_;
return *this;
}
// 后置++
Counter operator++(int) {
Counter temp = *this;
++(*this);
return temp;
}
private:
int count_ = 0;
};
遵循三/五法则:如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,那么它很可能需要全部这三个函数(C++11后加上移动构造和移动赋值,称为五法则)。
使用RAII管理资源:资源获取即初始化(RAII)是C++的核心惯用法,确保资源在对象生命周期内始终有效。
优先使用=default和=delete:明确表明使用编译器生成的版本或禁用某些操作。
cpp复制class NonCopyable {
public:
NonCopyable() = default;
~NonCopyable() = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 允许移动(C++11)
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
考虑异常安全性:特别是在资源管理类中,确保操作要么完全成功,要么保持对象不变。
合理使用const成员函数:将不修改对象状态的成员函数声明为const,提高代码的可读性和安全性。
当派生类对象被赋值给基类对象时,会发生对象切片,丢失派生类特有的部分。
解决方案:
当两个类互相包含对方的实例或指针时,可能导致循环引用。
解决方案:
单参数构造函数可能引起意外的隐式类型转换。
解决方案:
cpp复制class FileName {
public:
explicit FileName(const std::string& name) : name_(name) {}
private:
std::string name_;
};
void ProcessFile(const FileName& file);
// 正确用法
ProcessFile(FileName("data.txt"));
// 错误用法(如果构造函数不是explicit)
// ProcessFile("data.txt"); // 隐式转换
返回值优化(RVO/NRVO):现代编译器可以优化函数返回对象的拷贝操作。
移动语义的应用:对于临时对象或即将销毁的对象,使用移动语义避免不必要的拷贝。
小对象优化:对于小型对象,可以考虑直接在栈上分配,避免动态内存分配的开销。
内联关键函数:对于简单的成员函数,使用inline可以减少函数调用开销。
cpp复制class Point {
public:
// 简单的getter/setter适合内联
int x() const { return x_; }
void set_x(int x) { x_ = x; }
// 复杂的操作不适合内联
double DistanceTo(const Point& other) const;
private:
int x_, y_;
};
cpp复制class ResourceHolder {
public:
ResourceHolder()
: resource_(std::make_unique<Resource>()) {}
// 不需要显式定义析构函数、拷贝构造等
// 编译器生成的默认版本就能正确工作
private:
std::unique_ptr<Resource> resource_;
};
cpp复制class Buffer {
public:
Buffer(size_t size)
: size_(size), data_(new char[size]) {}
// 移动构造函数
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
private:
size_t size_;
char* data_;
};
下面是一个完整的日期类实现,展示了前面讨论的各种概念的实际应用:
cpp复制#include <iostream>
#include <stdexcept>
#include <string>
class Date {
public:
// 构造函数
Date(int year = 1970, int month = 1, int day = 1)
: year_(year), month_(month), day_(day) {
if (!IsValid()) {
throw std::invalid_argument("Invalid date");
}
}
// 拷贝构造函数
Date(const Date&) = default;
// 拷贝赋值运算符
Date& operator=(const Date&) = default;
// 移动构造函数
Date(Date&&) = default;
// 移动赋值运算符
Date& operator=(Date&&) = default;
// 析构函数
~Date() = default;
// 比较运算符
bool operator==(const Date& other) const {
return year_ == other.year_ &&
month_ == other.month_ &&
day_ == other.day_;
}
bool operator<(const Date& other) const {
if (year_ != other.year_) return year_ < other.year_;
if (month_ != other.month_) return month_ < other.month_;
return day_ < other.day_;
}
// 算术运算符
Date& operator+=(int days);
Date operator+(int days) const {
Date result = *this;
result += days;
return result;
}
// 流输出运算符
friend std::ostream& operator<<(std::ostream& os, const Date& date) {
return os << date.year_ << "-"
<< date.month_ << "-"
<< date.day_;
}
// 验证日期有效性
bool IsValid() const;
// 获取月份天数
static int GetDaysInMonth(int year, int month);
private:
int year_;
int month_;
int day_;
// 辅助函数
void Normalize();
};
// 实现略...
这个日期类展示了:
在实际项目中,这样的类设计可以满足大多数日期处理需求,同时保持了良好的性能和安全性。