1. 从结构体到类的跨越
在C语言中,结构体(struct)是我们组织数据的利器,但到了C++中,类(class)的出现彻底改变了我们编程的方式。记得我第一次从C转向C++时,最震撼的就是发现结构体突然"活"了起来——它们不仅能存储数据,还能自己干活了!
cpp复制// C风格的结构体
struct Point {
int x;
int y;
};
// C++中的类
class Point {
public:
void move(int dx, int dy) {
x += dx;
y += dy;
}
private:
int x;
int y;
};
这个简单的Point类已经展示了面向对象编程的核心思想:封装。我们把数据(x,y)和操作这些数据的方法(move)打包在一起,形成一个自包含的"黑盒子"。在实际项目中,这种封装带来的好处是巨大的:
- 数据保护:通过private关键字,我们可以隐藏内部实现细节,防止外部代码随意修改关键数据
- 接口稳定:即使内部实现改变(比如改用极坐标存储点),只要公有接口不变,使用这个类的代码就不需要修改
- 职责明确:每个类都有清晰的职责边界,代码组织更合理
提示:刚开始使用类时,最容易犯的错误是把所有成员都设为public。记住一个原则:除非有充分理由,否则成员变量都应该设为private。
2. 构造函数与对象初始化
构造函数是类中最重要的特殊成员函数之一。它决定了对象如何诞生,也直接影响代码的健壮性。我曾经在一个项目中追踪了整整两天的bug,最后发现只是因为一个类没有正确初始化成员变量。
2.1 构造函数的多种形式
cpp复制class Student {
public:
// 默认构造函数
Student() : id(0), name(""), score(0.0) {}
// 带参数的构造函数
Student(int i, string n, double s) : id(i), name(n), score(s) {}
// 委托构造函数(C++11)
Student(int i) : Student(i, "", 0.0) {}
// 拷贝构造函数
Student(const Student& other)
: id(other.id), name(other.name), score(other.score) {}
private:
int id;
string name;
double score;
};
每种构造函数都有其特定用途:
- 默认构造函数:创建对象时不提供任何参数
- 带参构造函数:创建时直接初始化
- 委托构造函数:避免重复代码
- 拷贝构造函数:用一个已有对象初始化新对象
2.2 初始化列表的重要性
构造函数后的冒号和初始化列表不是可选项,而是应该成为你的习惯。比较以下两种写法:
cpp复制// 不好的写法:先默认初始化,再赋值
Student::Student(int i, string n, double s) {
id = i;
name = n;
score = s;
}
// 好的写法:使用初始化列表
Student::Student(int i, string n, double s)
: id(i), name(n), score(s) {}
后者效率更高,特别是对于类类型成员(如string)。前者实际上会先调用默认构造函数,再进行赋值操作,而后者直接调用拷贝构造函数一步到位。
3. 析构函数与资源管理
如果说构造函数负责对象的诞生,那么析构函数就负责对象的"善后"。在C++中,理解析构函数的调用时机对防止资源泄漏至关重要。
3.1 基本析构函数
cpp复制class FileHandler {
public:
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) {
throw runtime_error("文件打开失败");
}
}
~FileHandler() {
if (file) {
fclose(file);
cout << "文件已关闭" << endl;
}
}
private:
FILE* file;
};
这个简单的FileHandler类展示了RAII(Resource Acquisition Is Initialization)原则:资源获取即初始化。文件在构造函数中打开,在析构函数中自动关闭,即使发生异常也能保证资源被释放。
3.2 析构函数的调用时机
理解析构函数的调用时机可以避免很多内存问题:
- 局部对象:当对象离开作用域时
- 动态分配对象:使用delete时
- 容器中的对象:容器销毁时
- 临时对象:表达式结束时
- 异常抛出时:栈展开过程中
我曾经遇到一个案例:一个开发者创建了一个管理网络连接的类,但在析构函数中没有正确关闭连接,导致服务器端的连接数不断累积,最终服务崩溃。正确的析构函数实现应该是:
cpp复制class NetworkConnection {
public:
~NetworkConnection() {
if (connected) {
gracefulDisconnect(); // 优雅断开连接
connected = false;
}
}
// ... 其他成员函数 ...
private:
bool connected = false;
};
4. this指针的妙用
this指针是每个非静态成员函数中可用的特殊指针,它指向调用该成员函数的对象。理解this指针能帮你写出更灵活的代码。
4.1 解决命名冲突
cpp复制class Person {
public:
Person(string name) {
this->name = name; // 使用this区分参数和成员变量
}
private:
string name;
};
虽然现代C++更推荐在构造函数初始化列表中进行初始化,但在其他成员函数中,this指针仍然是解决命名冲突的好办法。
4.2 链式调用
通过返回*this,可以实现方法的链式调用,这在设计流畅接口时特别有用:
cpp复制class Calculator {
public:
Calculator& add(double x) { value += x; return *this; }
Calculator& sub(double x) { value -= x; return *this; }
double get() const { return value; }
private:
double value = 0.0;
};
// 使用示例
double result = Calculator().add(10).sub(5).add(3.5).get();
这种模式在构建器(Builder)模式中很常见,可以让代码更简洁易读。
5. 静态成员:类的共享属性
静态成员属于类本身而非特定对象,它们在所有对象间共享。合理使用静态成员可以优化内存使用并实现类级别的功能。
5.1 静态成员变量
cpp复制class Employee {
public:
Employee(string n) : name(n) { ++totalEmployees; }
~Employee() { --totalEmployees; }
static int getTotal() { return totalEmployees; }
private:
string name;
static int totalEmployees; // 声明
};
// 静态成员必须在类外定义
int Employee::totalEmployees = 0;
这个Employee类使用静态成员totalEmployees来跟踪创建的员工总数。注意静态成员变量必须在类外定义(分配存储空间),否则会导致链接错误。
5.2 静态成员函数
静态成员函数只能访问静态成员,不能访问非静态成员(因为没有this指针)。它们通常用于:
- 工厂方法
- 工具函数
- 单例模式实现
cpp复制class MathUtils {
public:
static double pi() { return 3.141592653589793; }
static double degToRad(double deg) { return deg * pi() / 180.0; }
};
// 使用时不需创建对象
double rad = MathUtils::degToRad(45.0);
在实际项目中,我经常用静态成员函数来组织相关的工具函数,比全局函数更易于管理和避免命名冲突。
6. 友元:打破封装的特例
友元(friend)是C++中一种打破封装的特例机制,它允许特定函数或类访问当前类的私有成员。虽然友元破坏了封装性,但在某些场景下非常有用。
6.1 友元函数
cpp复制class Matrix {
public:
friend Matrix multiply(const Matrix& a, const Matrix& b);
private:
double data[4][4];
};
Matrix multiply(const Matrix& a, const Matrix& b) {
Matrix result;
// 可以直接访问a和b的私有成员data
// ... 矩阵乘法实现 ...
return result;
}
在这个例子中,multiply函数不是Matrix的成员函数,但被声明为友元后可以访问Matrix的私有数据。这在实现运算符重载时特别常见。
6.2 友元类
整个类可以被声明为友元,这样它的所有成员函数都能访问私有成员:
cpp复制class LinkedList {
private:
struct Node { /*...*/ };
Node* head;
friend class LinkedListIterator; // 迭代器需要访问内部实现
};
class LinkedListIterator {
public:
LinkedListIterator(const LinkedList& list)
: current(list.head) {} // 直接访问私有成员head
// ... 迭代器实现 ...
};
友元关系不可传递(A是B的友元,B是C的友元,不意味着A是C的友元),也不继承。在实际项目中,我建议谨慎使用友元,只在确实需要访问内部实现时使用,比如实现迭代器或某些特定的运算符重载。
7. 类的作用域与名字查找
理解类的作用域和名字查找规则对于编写正确的C++代码至关重要,特别是在处理继承和模板时。
7.1 类作用域的基本规则
在类定义中,名字查找遵循以下顺序:
- 当前块作用域
- 类作用域(包括基类)
- 外围命名空间作用域
cpp复制int x = 10; // 全局x
class ScopeTest {
public:
void print() {
cout << x << endl; // 使用成员x
cout << ::x << endl; // 使用全局x
}
private:
int x = 20; // 成员x
};
7.2 名字隐藏与using声明
派生类中的名字会隐藏基类中的同名成员,即使参数列表不同:
cpp复制class Base {
public:
void func(int) { cout << "Base::func(int)" << endl; }
};
class Derived : public Base {
public:
void func(double) { cout << "Derived::func(double)" << endl; }
// 隐藏了Base::func(int)
};
Derived d;
d.func(1); // 调用Derived::func(double),1被转换为1.0
如果需要暴露基类的重载版本,可以使用using声明:
cpp复制class Derived : public Base {
public:
using Base::func; // 引入Base中的所有func重载
void func(double) { cout << "Derived::func(double)" << endl; }
};
Derived d;
d.func(1); // 现在调用Base::func(int)
在实际编码中,名字隐藏常常导致意外的行为。我建议在重写方法时使用override关键字(C++11引入),这样编译器会在你意外隐藏而非重写时给出警告。
8. 类的前向声明与不完全类型
前向声明(forward declaration)是一种只声明类名而不定义其内容的技术,它在处理类之间的相互依赖时非常有用。
8.1 基本用法
cpp复制class B; // 前向声明
class A {
public:
void setB(B* b) { this->b = b; }
private:
B* b; // 只能使用指针或引用
};
class B {
public:
void setA(A* a) { this->a = a; }
private:
A* a;
};
前向声明允许我们在知道类B的完整定义前就使用它,这在以下场景特别有用:
- 打破循环依赖
- 减少头文件包含,加快编译速度
- 隐藏实现细节
8.2 不完全类型的限制
使用前向声明的类是不完全类型(incomplete type),只能进行有限操作:
- 声明指针或引用
- 作为函数参数/返回类型(但不能访问成员)
- 声明友元关系
不能进行的操作包括:
- 定义该类型的对象
- 访问成员
- 使用sizeof
我曾经在一个大型项目中通过合理使用前向声明,将编译时间减少了近30%。关键是将头文件中的包含关系降到最低,只在源文件中包含必要的完整定义。
