在C++编程中,类(class)是面向对象编程的核心概念。上一篇文章我们介绍了类的基本概念和简单用法,今天要深入探讨类的更多细节特性。作为一个从C转C++的老程序员,我清楚地记得当初被类的各种特性搞得晕头转向的经历。类不仅仅是一个数据结构的升级版,它真正体现了面向对象编程的三大特性:封装、继承和多态。
在实际项目开发中,合理设计类结构可以大幅提升代码的可维护性和复用性。比如在开发游戏引擎时,我们通常会设计一个基类GameObject,然后派生出Player、Enemy等子类。这种层次结构让代码逻辑清晰,扩展方便。但要做到这一点,必须深入理解类的各种特性和使用技巧。
构造函数是类中最重要的成员函数之一。与普通函数类似,构造函数也支持重载和默认参数。在实际开发中,这能提供更灵活的初始化方式。
cpp复制class Person {
public:
// 带默认参数的构造函数
Person(string name = "Unknown", int age = 0)
: m_name(name), m_age(age) {}
// 重载构造函数
Person(int age) : m_name("Unknown"), m_age(age) {}
private:
string m_name;
int m_age;
};
注意:当同时使用重载和默认参数时,要小心避免歧义。比如如果同时有Person()和Person(string name="Unknown"),那么Person p;会导致编译错误,因为编译器无法确定调用哪个构造函数。
构造函数后的初始化列表是C++特有的语法,它比在构造函数体内赋值更高效,特别是对于类类型成员和const成员。
cpp复制class Student {
public:
Student(string name, int score)
: m_name(name), // 直接调用string的拷贝构造函数
m_score(score) // 基本类型差别不大
{
// 如果在构造函数体内赋值:
// m_name = name; // 这会先调用默认构造函数,再调用赋值运算符
}
private:
string m_name;
int m_score;
};
对于const成员和引用成员,必须在初始化列表中初始化,因为它们一旦创建就不能再赋值。
cpp复制class ConstDemo {
public:
ConstDemo(int num) : m_constNum(num), m_refNum(num) {}
private:
const int m_constNum;
int& m_refNum;
};
C++11引入了委托构造函数的概念,允许一个构造函数调用同类中的另一个构造函数,避免代码重复。
cpp复制class Rectangle {
public:
Rectangle() : Rectangle(0, 0) {} // 委托给下面的构造函数
Rectangle(int w, int h)
: width(w), height(h) {}
private:
int width;
int height;
};
析构函数在对象生命周期结束时自动调用,负责资源清理。对于管理资源的类(如动态内存、文件句柄等),必须自定义析构函数。
cpp复制class FileHandler {
public:
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) {
throw runtime_error("Failed to open file");
}
}
~FileHandler() {
if (file) {
fclose(file);
file = nullptr;
}
}
private:
FILE* file;
};
经验:遵循RAII(资源获取即初始化)原则,在构造函数中获取资源,在析构函数中释放资源。这样即使发生异常,资源也能被正确释放。
当类包含指针成员或管理资源时,通常需要自定义拷贝构造函数和拷贝赋值运算符,避免浅拷贝问题。
cpp复制class String {
public:
String(const char* str = "") {
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
// 拷贝构造函数
String(const String& other) {
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
// 拷贝赋值运算符
String& operator=(const String& other) {
if (this != &other) { // 防止自赋值
delete[] m_data;
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
return *this;
}
~String() {
delete[] m_data;
}
private:
char* m_data;
};
C++11引入了移动语义,通过移动构造函数和移动赋值运算符,可以避免不必要的拷贝,提高性能。
cpp复制class String {
public:
// 移动构造函数
String(String&& other) noexcept
: m_data(other.m_data) {
other.m_data = nullptr;
}
// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
return *this;
}
};
使用std::move可以将左值转换为右值引用,触发移动语义:
cpp复制String s1("hello");
String s2 = std::move(s1); // 调用移动构造函数
静态数据成员属于类本身,而不是类的某个对象。所有对象共享同一个静态成员。
cpp复制class Counter {
public:
Counter() { ++count; }
~Counter() { --count; }
static int getCount() { return count; }
private:
static int count; // 声明
};
int Counter::count = 0; // 定义并初始化
静态成员函数没有this指针,只能访问静态成员。常用于工具函数或工厂方法。
cpp复制class MathUtils {
public:
static double pi() { return 3.1415926; }
static int max(int a, int b) { return a > b ? a : b; }
};
// 使用
double area = MathUtils::pi() * radius * radius;
友元打破了封装性,应谨慎使用,但在某些情况下很有必要,比如运算符重载。
cpp复制class Box {
public:
Box(double l = 0, double w = 0, double h = 0)
: length(l), width(w), height(h) {}
friend void printBox(const Box& box); // 友元函数声明
private:
double length, width, height;
};
// 友元函数定义
void printBox(const Box& box) {
// 可以访问私有成员
cout << "Box: " << box.length << " "
<< box.width << " " << box.height << endl;
}
cpp复制class Window {
friend class WindowManager; // WindowManager可以访问Window的私有成员
private:
int x, y;
int width, height;
};
class WindowManager {
public:
void moveWindow(Window& w, int newX, int newY) {
w.x = newX; // 可以访问Window的私有成员
w.y = newY;
}
};
mutable修饰的成员可以在const成员函数中被修改,常用于缓存、锁等场景。
cpp复制class Cache {
public:
int getValue() const {
if (!valid) {
cachedValue = expensiveCalculation(); // 可以修改mutable成员
valid = true;
}
return cachedValue;
}
private:
int expensiveCalculation() const { /*...*/ }
mutable int cachedValue;
mutable bool valid = false;
};
当前向声明类时,只能使用指针或引用,不能创建对象或访问成员。
cpp复制class B; // 前向声明
class A {
public:
void setB(B* b) { m_b = b; }
private:
B* m_b;
};
class B {
// B的实现
};
单一职责原则:一个类应该只有一个引起它变化的原因。不要把太多功能塞进一个类。
优先使用组合而非继承:除非确实需要多态,否则优先使用组合来复用代码。
接口最小化:只暴露必要的接口,保持类的简洁性。
const正确性:尽可能将成员函数声明为const,明确哪些函数不会修改对象状态。
避免返回内部成员的引用或指针:这会破坏封装性,可能导致外部代码修改内部状态。
考虑异常安全:确保即使在异常发生时,对象仍处于有效状态。
遵循三/五法则:如果需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的一个,通常需要自定义全部三个(C++11后加上移动构造函数和移动赋值运算符,成为五法则)。
在实际项目中,我曾见过一个违反三法则的类导致内存泄漏。该类管理了一个动态数组,自定义了析构函数释放内存,但没有自定义拷贝构造函数和拷贝赋值运算符。当对象被拷贝时,两个对象指向同一块内存,析构时会被释放两次,导致程序崩溃。这个教训让我深刻理解了这些规则的重要性。