1. C++面向对象编程基础
C++作为一门支持面向对象编程的语言,类和对象是其核心概念。让我们从一个简单的类定义开始:
cpp复制class Student {
private:
string name;
int age;
public:
// 构造函数
Student(string n, int a) : name(n), age(a) {}
// 成员函数
void display() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
这个Student类包含了两个私有成员变量name和age,一个构造函数,以及一个公有的成员函数display()。构造函数使用初始化列表来初始化成员变量,这是一种更高效的初始化方式。
注意:在C++中,类定义通常以分号结束,这与Java等语言不同,初学者容易忘记这一点。
1.1 类与对象的关系
类就像是一个蓝图,而对象则是根据这个蓝图创建的具体实例。我们可以这样创建和使用对象:
cpp复制Student stu1("Alice", 20); // 创建对象
stu1.display(); // 调用成员函数
在内存中,每个对象都有自己独立的成员变量存储空间,但所有对象共享同一份成员函数代码。这是C++实现对象的高效方式。
1.2 访问控制与封装
C++提供了三种访问控制修饰符:
public:类外可以直接访问private:只能在类内部访问(默认)protected:类内部和派生类中可以访问
良好的封装实践建议将数据成员设为private,通过public成员函数来访问和修改它们。这可以保护数据不被意外修改,也便于后期维护。
2. 运算符重载深入解析
运算符重载是C++的一个强大特性,它允许我们为自定义类型定义运算符的行为。让我们以复数类为例:
cpp复制class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载+运算符
Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
// 重载<<运算符(通常声明为友元)
friend ostream& operator<<(ostream& os, const Complex& c);
};
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << "+" << c.imag << "i";
return os;
}
2.1 可重载的运算符
C++中大部分运算符都可以重载,但有几个例外:
.(成员访问).*(成员指针访问)::(作用域解析)?:(条件运算符)sizeof
提示:重载运算符时,最好保持其原有的语义。例如,
+运算符应该执行某种"加法"操作,而不是完全无关的功能。
2.2 成员函数与全局函数重载
运算符可以重载为成员函数或全局函数,但有几点区别:
- 必须作为成员函数重载的运算符:
=,(),[],-> - 流运算符
<<和>>通常重载为全局函数 - 对称运算符(如
+)通常重载为全局函数以实现类型转换对称性
2.3 特殊运算符重载案例
让我们看一个更复杂的例子 - 重载下标运算符[]:
cpp复制class IntArray {
private:
int* data;
int size;
public:
IntArray(int s) : size(s) {
data = new int[size];
}
~IntArray() {
delete[] data;
}
// 重载[]运算符
int& operator[](int index) {
if(index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
// const版本的重载
const int& operator[](int index) const {
if(index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
};
这个例子展示了如何通过运算符重载实现类似内置数组的访问方式,同时增加了边界检查的安全性。
3. 动态内存管理详解
C++中的动态内存管理是一个需要特别小心的领域。让我们先看一个基本的例子:
cpp复制class DynamicArray {
private:
int* ptr;
int size;
public:
DynamicArray(int s) : size(s) {
ptr = new int[size]; // 动态分配内存
}
~DynamicArray() {
delete[] ptr; // 释放内存
}
// 拷贝构造函数
DynamicArray(const DynamicArray& other) : size(other.size) {
ptr = new int[size];
for(int i = 0; i < size; i++) {
ptr[i] = other.ptr[i];
}
}
// 拷贝赋值运算符
DynamicArray& operator=(const DynamicArray& other) {
if(this != &other) { // 防止自赋值
delete[] ptr; // 释放原有资源
size = other.size;
ptr = new int[size];
for(int i = 0; i < size; i++) {
ptr[i] = other.ptr[i];
}
}
return *this;
}
};
3.1 内存分配与释放
C++中使用new和delete来管理动态内存:
new:分配内存并调用构造函数delete:调用析构函数并释放内存new[]和delete[]:用于数组
警告:必须配对使用
new/delete和new[]/delete[],混用会导致未定义行为。
3.2 三/五法则
当一个类需要自定义析构函数时,通常也需要自定义拷贝构造函数和拷贝赋值运算符(三法则)。在C++11及以后,还可能需要移动构造函数和移动赋值运算符(五法则)。
3.3 智能指针
现代C++推荐使用智能指针来管理动态内存,主要有三种:
unique_ptr:独占所有权,不可拷贝shared_ptr:共享所有权,引用计数weak_ptr:不增加引用计数的shared_ptr观察者
使用智能指针的例子:
cpp复制#include <memory>
class ResourceUser {
private:
shared_ptr<Resource> res;
public:
ResourceUser(shared_ptr<Resource> r) : res(r) {}
// 不需要显式释放资源
};
4. 综合应用实例
让我们结合前面所学,实现一个更完整的String类:
cpp复制class MyString {
private:
char* data;
size_t length;
void freeMemory() {
if(data) {
delete[] data;
data = nullptr;
length = 0;
}
}
public:
// 构造函数
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 析构函数
~MyString() {
freeMemory();
}
// 拷贝构造函数
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// 移动构造函数 (C++11)
MyString(MyString&& other) noexcept {
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if(this != &other) {
freeMemory();
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// 移动赋值运算符 (C++11)
MyString& operator=(MyString&& other) noexcept {
if(this != &other) {
freeMemory();
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// 重载+运算符
MyString operator+(const MyString& other) const {
MyString result;
result.length = length + other.length;
result.data = new char[result.length + 1];
strcpy(result.data, data);
strcat(result.data, other.data);
return result;
}
// 重载==运算符
bool operator==(const MyString& other) const {
if(length != other.length) return false;
return strcmp(data, other.data) == 0;
}
// 重载[]运算符
char& operator[](size_t index) {
if(index >= length) throw out_of_range("Index out of range");
return data[index];
}
// const版本的[]运算符
const char& operator[](size_t index) const {
if(index >= length) throw out_of_range("Index out of range");
return data[index];
}
// 重载<<运算符
friend ostream& operator<<(ostream& os, const MyString& str) {
os << str.data;
return os;
}
};
4.1 实现细节分析
这个MyString类展示了多个重要概念的综合应用:
- 动态内存管理(构造函数、析构函数)
- 深拷贝与浅拷贝(拷贝构造函数、拷贝赋值运算符)
- 移动语义(移动构造函数、移动赋值运算符)
- 运算符重载(
+,==,[],<<)
4.2 性能优化考虑
在实际开发中,我们还可以考虑以下优化:
- 小字符串优化(SSO):对小字符串不使用堆分配
- 写时复制(COW):多个对象共享同一份数据直到需要修改
- 预分配空间:减少频繁的内存分配
4.3 异常安全
我们的实现保证了基本异常安全:
- 构造函数要么完全成功,要么抛出异常而不留下资源泄漏
- 赋值运算符在分配新内存成功后才释放旧内存
- 移动操作标记为
noexcept,这对标准库容器很重要
5. 常见问题与调试技巧
5.1 内存泄漏检测
内存泄漏是C++程序常见问题。可以使用以下方法检测:
- 重载
new和delete来跟踪分配和释放 - 使用工具如Valgrind(Linux)或Visual Studio的内存诊断工具
- 使用智能指针减少手动内存管理
5.2 调试技巧
调试类和动态内存问题时:
- 在构造函数和析构函数中添加打印语句,跟踪对象生命周期
- 使用
assert验证不变量 - 对于运算符重载,先编写测试用例验证行为
5.3 常见陷阱
- 浅拷贝问题:默认的拷贝构造函数和赋值运算符执行浅拷贝,对于包含指针的类这通常是个问题
- 自赋值问题:在赋值运算符中忘记检查自赋值
- 异常不安全:在修改对象状态前分配资源,可能导致资源泄漏
- 移动后使用:使用被移动后的对象(其状态通常为空或未指定)
6. 现代C++的最佳实践
6.1 使用RAII原则
RAII(Resource Acquisition Is Initialization)是C++的核心思想:
- 在构造函数中获取资源
- 在析构函数中释放资源
- 使用栈对象管理资源生命周期
6.2 优先使用智能指针
除非有特殊需求,否则应优先使用智能指针而非原始指针:
unique_ptr用于独占所有权shared_ptr用于共享所有权weak_ptr用于打破循环引用
6.3 移动语义的应用
C++11引入的移动语义可以显著提升性能:
- 对大型资源持有类实现移动操作
- 使用
std::move显式转移所有权 - 将移动构造函数和移动赋值运算符标记为
noexcept
6.4 运算符重载的现代风格
现代C++中运算符重载的一些好实践:
- 对于对称运算符,重载为友元函数
- 提供
const和非const版本的下标运算符 - 对于比较运算符,通常只需要实现
==和<,其他可以通过它们派生 - 考虑使用
operator bool()实现安全的布尔转换
在实际项目中,合理使用这些特性可以编写出既高效又易于维护的C++代码。掌握类和对象、运算符重载和动态内存管理是成为熟练C++开发者的关键步骤。
