C++学习之动态内存和拷贝控制
一、动态内存
- 动态内存管理是指在程序运行时动态地分配和释放内存。C++提供了几种方式来管理动态内存,包括使用原始指针和智能指针。
- 使用原始指针
- 分配内存,使用new操作符来分配内存
- 使用delete操作符来释放内存
- 使用delete[]操作符来释放数组
int* p = new int; // 分配一个整数的内存
*p = 10; // 设置值
p = nullptr; // 将指针置为nullptr,防止悬空指针
//分配数组
int* arr = new int[10]; // 分配一个包含10个整数的数组
arr[0] = 1;
delete[] arr; // 释放之前分配的数组内存
arr = nullptr; // 将指针置为nullptr,防止悬空指针
- 使用智能指针(#include <memory>)
-
它们自动管理内存,避免了忘记调用delete或使用悬空指针的错误。
-
智能指针是C++11引入的,它们在内存管理方面提供了更安全、更自动化的机制。C++标准库中提供了三种主要的智能指针
-
std::unique_ptr
表示唯一拥有某块内存的智能指针,不能被复制,但可以被移动
#include <memory>
std::unique_ptr<int> p1(new int(10)); // 创建一个唯一的智能指针
//std::unique_ptr<int> 表示一个智能指针,它拥有一个指向int类型的指针。
//具有“唯一拥有”内存的性质。
//这句代码等同于,但通常建议直接使用原来的单行代码
(int* rawPtr = new int(10); // 步骤1:在堆上分配一个整数
std::unique_ptr<int> p1(rawPtr); // 步骤2:用std::unique_ptr管理该指针
)
//当p1超出作用域时(如函数结束时),它会自动释放分配的内存,不需要手动调用delete。
*p1 = 20;
std::unique_ptr<int> p2 = std::move(p1); // p1的所有权转移到p2
// p1现在是空的,不能再访问
std::shared_ptr
允许多个智能指针共享同一块内存,引用计数机制会自动管理内存的释放
#include <memory>
std::shared_ptr<int> p1(new int(42)); // 使用new动态分配内存
std::shared_ptr<int> p2 = p1; // p1和p2共享这块内存
// 引用计数自动管理内存,当最后一个shared_ptr被销毁时内存自动释放
p1.reset();// p1 被销毁,但内存未释放,因为 p2 仍在使用
std::weak_ptr
用于打破std::shared_ptr之间的循环引用。它不改变引用计数,不直接管理内存
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // wp 观察 sp 管理的内存
//std::weak_ptr 不能直接解引用(即不能像 shared_ptr 或原始指针那样使用 * 或 ->),因为它不拥有该资源。
//为了使用 std::weak_ptr 指向的对象,需要先调用 lock() 函数,这会返回一个指向对象的 std::shared_ptr。
//lock():尝试将 weak_ptr 升级为 shared_ptr
//expired():检查资源是否已被销毁,如果被销毁返回 true,否则返回 false。
//如果原对象已经被销毁,lock() 会返回一个空的 std::shared_ptr。
循环引用问题范例
- 当两个对象互相持有 std::shared_ptr 时,会发生循环引用。即使这两个对象的生命周期结束,内存也不会被释放。std::weak_ptr 可以打破这种循环。
struct B;
struct A {
std::shared_ptr<B> b_ptr; // A 拥有 B
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::shared_ptr<A> a_ptr; // B 拥有 A
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B
b->a_ptr = a; // B 持有 A,形成循环引用
// 此时 a 和 b 超出作用域时,内存不会释放,因为它们的引用计数永远不会归零
return 0;
}
- 使用std::weak_ptr打破循环
struct B;
struct A {
std::shared_ptr<B> b_ptr; // A 拥有 B
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::weak_ptr<A> a_ptr; // B 弱引用 A,不拥有 A
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B
b->a_ptr = a; // B 通过 weak_ptr 弱引用 A,打破循环
// 当 a 和 b 超出作用域时,内存会正确释放
return 0;
}
模版
- 模板(Templates)是C++中用于实现泛型编程的一种功能。它允许编写与具体类型无关的代码,从而能够在同一段代码上操作不同的数据类型。
1. 函数模版
- 函数模板允许你定义一个泛型函数,能够对不同类型的数据进行相同的操作。在定义时并不指定具体类型,只有在调用时才根据传入的参数推导出实际类型。
template <typename T>
T add(T a, T b) {
return a + b;
}
template <typename U>
float sum(U a,U b){
return a+b+0.01;
}
参数说明:T是一个占位符类型,意味着这个函数可以接受任何类型 T。
T add(T a, T b):定义了一个函数模板 add,它接受两个类型为 T 的参数并返回一个 T 类型的值。
2. 类模版
- 类模板允许你定义一个泛型类,能够操作不同类型的数据。通过模板参数,类可以对各种类型进行操作,而不需要为每种类型单独定义类。
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}//构造函数初始化列表,将v的值赋值给value
T getValue() {
return value;
}
};
- 具体的使用方法
#include <iostream>
template <typename T>
//在 C++ 中,template <typename T> 和 template <class Type> 是等效的。两者都用于定义模板,区别仅仅在于命名习惯,没有功能上的不同。
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T getValue() {
return value;
}
};
int main() {
Box<int> intBox(123); // 使用 **int 类型**实例化 Box
Box<double> doubleBox(45.67); // 使用 double 类型实例化 Box
std::cout << "intBox: " << intBox.getValue() << std::endl;
std::cout << "doubleBox: " << doubleBox.getValue() << std::endl;
return 0;
}
拷贝控制
- 拷贝控制(Copy Control)是指与对象的复制、赋值和销毁相关的机制。
1.拷贝构造函数
- 拷贝构造函数的主要作用是在对象创建时通过已有对象的副本来初始化新的对象。它确保在按值传递、函数返回值或初始化列表等操作中正确复制对象。
- 语法
ClassName(const ClassName& other);//other是拷贝的对象,也是ClassName类型。
- 如果你不提供自定义的拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数,它会逐个成员地进行浅拷贝(按位复制每个成员)。可能会发生以下问题
- 浅拷贝:多个对象共享同一块资源,导致资源被多个对象同时操作。
- 内存泄漏:对象销毁时释放了资源,另一个对象再使用该资源时会出现未定义行为 - 代码举例
class MyClass {
private:
int* data; // 动态分配的内存
public:
MyClass(int value) {
data = new int(value); // 动态分配内存并存储值
}
// 自定义拷贝构造函数
MyClass(const MyClass& other) {
//*other.data中.比*先执行
data = new int(*other.data); // 深拷贝,分配新内存并复制值
}
~MyClass() {
delete data; // 释放动态内存
}
int getData() const {
return *data; // 返回指向的值
}
};
int main() {
MyClass obj1(42); // obj1.data 指向 42
MyClass obj2 = obj1; // 使用拷贝构造函数,obj2.data 也指向 42
//等同于MyClass obj2(obj1);
std::cout << obj1.getData() << std::endl; // 输出 42
std::cout << obj2.getData() << std::endl; // 输出 42
return 0;
}
2. 拷贝赋值运算符(copy-assignment operator)
- 拷贝赋值运算符允许我们通过自定义对象的赋值行为,尤其是在类对象包含动态资源(如动态内存、文件句柄等)时,拷贝赋值运算符可确保对象能够正确地管理这些资源,避免资源泄露或重复释放等问题。
- 语法
ClassName& operator=(const ClassName& other);
operator= 是拷贝赋值运算符的符号。
const ClassName& other 表示赋值源对象是 const 引用,以避免不必要的复制,并确保源对象不被修改。
返回类型 ClassName& 是对当前对象的引用,这允许链式赋值操作(如 obj1 = obj2 = obj3;)。
- 如果没有显式定义拷贝赋值运算符,编译器会为类生成一个默认的拷贝赋值运算符。默认的行为是按成员变量逐个赋值,类似于浅拷贝。对于不需要特殊资源管理的类,这种浅拷贝是足够的
- 实现步骤
1. 检查自赋值
2. 释放已有资源
3. 复制资源
4. 返回当前对象的引用 - 范例
class MyClass {
private:
int* data; // 指向动态分配的内存
public:
MyClass(int value) {
data = new int(value); // 动态分配内存并初始化
}
// 自定义拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
// 检查自赋值
if (this == &other) { //此时的&可是在右边的喔,为取地址符
return *this; // 如果是自赋值,直接返回当前对象
}
// 释放当前对象的资源
delete data; // 释放当前对象中旧的动态内存
// 分配新内存并复制数据
data = new int(*other.data);
// 返回当前对象的引用
return *this;
}
~MyClass() {
delete data; // 析构函数中释放动态内存
}
int getValue() const {
return *data;
}
};
3. 移动构造函数(move constructor)
- 移动构造函数是 C++11 引入的一种特殊构造函数,用于实现移动语义。它允许对象的资源(如动态内存、文件句柄等)从一个对象转移到另一个对象,而不是复制它们,从而提高程序的效率,尤其是在处理大量数据或资源时。
- noexcept 关键字,移动构造函数通常使用 noexcept 关键字,表示该函数不会抛出异常。
- 语法
ClassName(ClassName&& other);
- 范例
#include <iostream>
#include <utility> // 为 std::move 提供支持
class MyClass {
private:
int* data; // 动态分配的内存
public:
// 构造函数
MyClass(int value) {
data = new int(value); // 动态分配内存并初始化
std::cout << "Constructor: Allocated memory" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept // 参数是右值引用
: data(other.data) { // 直接接管资源
other.data = nullptr; // 清空源对象的指针
std::cout << "Move Constructor: Transferred ownership" << std::endl;
}
// 析构函数
~MyClass() {
if (data) {
delete data; // 释放动态内存
std::cout << "Destructor: Released memory" << std::endl;
}
}
// 打印值
void printValue() const {
if (data) {
std::cout << "Value: " << *data << std::endl;
} else {
std::cout << "No value" << std::endl;
}
}
};
// 示例函数,返回一个 MyClass 临时对象
MyClass createObject(int value) {
MyClass temp(value);
return temp; // 这里将会触发移动构造函数
}
int main() {
std::cout << "Creating obj1" << std::endl;
MyClass obj1(10); // 正常构造函数
std::cout << "Moving obj1 to obj2" << std::endl;
MyClass obj2 = std::move(obj1); // 触发移动构造函数
std::cout << "obj2 value: ";
obj2.printValue(); // 打印 obj2 的值
std::cout << "obj1 value: ";
obj1.printValue(); // 打印 obj1 的值,资源已经被转移
std::cout << "\nCreating obj3 using createObject()" << std::endl;
MyClass obj3 = createObject(20); // 返回值优化,触发移动构造函数
std::cout << "obj3 value: ";
obj3.printValue(); // 打印 obj3 的值
return 0;
}
4. 移动赋值函数(move-assignement operator)
- 它用于将一个对象的资源转移给另一个已经存在的对象,避免不必要的深拷贝操作。
- 语法
ClassName& operator=(ClassName&& other) noexcept;
ClassName&& 表示右值引用,表明这个赋值运算符只接受右值对象。
noexcept 通常被标记为该函数不会抛出异常,以保证程序的安全和效率,特别是在标准容器(如 std::vector)中使用时。
- 范例
class MyClass {
private:
int* data; // 动态分配的内存
public:
// 构造函数
MyClass(int value) : data(new int(value)) {
std::cout << "Constructor: Allocated memory" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept
: data(other.data) {
other.data = nullptr;
std::cout << "Move Constructor: Transferred ownership" << std::endl;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) { // 检查自赋值
delete data; // 释放当前对象的资源
data = other.data; // 接管其他对象的资源
other.data = nullptr; // 清空源对象的指针
std::cout << "Move Assignment: Transferred ownership" << std::endl;
}
return *this; // 返回当前对象
}
// 析构函数
~MyClass() {
if (data) {
delete data;
std::cout << "Destructor: Released memory" << std::endl;
}
}
// 打印值
void printValue() const {
if (data) {
std::cout << "Value: " << *data << std::endl;
} else {
std::cout << "No value" << std::endl;
}
}
};
5. 析构函数(destructor)
- 析构函数(Destructor)是类的一个特殊成员函数,当对象的生命周期结束时自动调用,用于清理对象分配的资源,如动态内存、文件句柄等。析构函数不需要也不能被显式调用,系统会在对象销毁时自动执行它。
- 作用
- 释放资源:当对象超出作用域或被显式销毁时,析构函数会被调用,通常用于释放构造函数或其他函数中分配的资源。
- 清理动作:除了释放动态内存,析构函数还可以关闭文件、断开网络连接、释放互斥锁等 - 特点
- 析构函数的名字与类名相同,前面加上波浪符 ~。
- 一个类只能有一个析构函数,且没有返回值,也不接受参数。
- 析构函数不能被重载(因为没有参数)。
- 如果不显式定义,编译器会为类生成一个默认析构函数,但默认析构函数只会对类的非动态资源进行处理。 - 注意事项
- 动态内存管理:如果类内部使用了动态内存(如通过 new 分配的内存),必须在析构函数中使用 delete 释放。
- 避免重复释放资源:在移动构造或移动赋值时,原对象的资源可能已经转移。此时需要确保原对象的指针已被置为 nullptr,以避免析构函数重复释放资源。
- 继承与虚析构函数:如果类有继承关系,基类的析构函数应当声明为虚函数(virtual),以确保删除指向派生类的基类指针时能够正确调用派生类的析构函数。