目录
一.什么是指针?
1. 为什么要写成int* p?
2. & 这个是什么?
二.指针的细节:
1.一级指针(p,*p,&p的区别):
2.二级指针(pp,*pp,**pp,&p的区别):
三.使用指针操作数组:
1.操作一维数组:
2.操作二维数组:
(1)使用指针定义一个二维数组:
(2)使用指针访问二维数组:
怎么理解array[m]的含义是m+1行的起始地址?
(3)使用指针遍历二维数组:
四.在函数中使用指针传参:
五.指针与引用的区别:
指针:
引用:
五.使用指针进行动态内存分配:
1.使用new分配空间:
单个变量分配:
数组分配:
2.使用delete释放内存:
释放单个变量:
释放数组:
3.动态内存分配的使用:
4.注意事项:
六.智能指针的使用:
1. 什么是智能指针
2.智能指针的类型:
3.unique_ptr 的使用:
构造 :
a. reset():
b. release():
c. get():
d. operator* 和 operator->:
(1)不允许左值复制赋值操作:
(2)允许临时右值赋值:
(3)在 STL 容器中使用 std::unique_ptr:
(4)支持对象数组的内存管理:
如何判断引用计数的增加与减少?
为什么引用计数会无法归零?
5.weak_ptr 的使用:
总结:
这篇文章帮助你了解指针的细节以及什么是智能指针,一步一步带你正确理解指针。本文码了近两万字,也希望你慢慢阅读,如有不解以及对一些内容表示不认同,欢迎评论区留言,话不多说进入正题......
一.什么是指针?
指针是一个变量,其值为另一个变量的地址,即内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type* name;
就比如说咱们拿下面指针举例子:
#include <iostream>
using namespace std;
int main(){int arr[] = {1,2,3,4,5,6};int* p = arr;//指向数组首元素地址int* p = &arr[0];//指向数组首元素地址
}
这里的指针p指向首元素地址,肯定会有疑问,怎么写成这样的?不要急,看完下面的简述你就明白了。
1. 为什么要写成int* p?
其实 int* p 与 int *p 没有区别,两种写法均可,只是我一般习惯这样写是因为我把 int* 当成一个整型的指针类型,这样方便下面对于指针的理解。指针本身是一个变量,它存储的是另一个变量的内存地址。通过指针,我们可以间接访问和操作指针所指向的变量。所以,我们一般给指针赋值赋的是地址值。
2. & 这个是什么?
在 C++ 中,& 的意思是取地址操作符,用于获取一个变量在内存中的地址。我们在上面强调指针的赋值需要赋一个地址值,而取地址操作符修饰的会获取其地址,所以一般我们写成int* p = &arr[0]。
二.指针的细节:
1.一级指针(p,*p,&p的区别):
先看下面的例子:
#include <iostream>
using namespace std;
int main(){int arr[] = {1,2,3,4,5,6};int* p = arr;cout << p << endl; //p指向的地址cout << &arr[0] << endl; //p指向的地址cout << *p << endl; //p指向的地址的值cout << &p << endl; //&p是p本身的地址,即指针变量在内存中的位置
}
*p 表示对指针p
所指向的地址内容的解引用,也就是获取p所指向的地址值的值,因为当前p指向的是数组首元素地址,所以 *p = 1 。
&p 表示的是对这个指针p进行取地址的操作,也就是获取 指针p 本身的地址,即指针变量在内存中的地址值。
2.二级指针(pp,*pp,**pp,&p的区别):
所谓的二级指针,其含义就是指向指针的指针,它指向另一个指针的地址。使用二级指针可以在某些情况下处理多级指针或动态数据结构,例如二维数组、链表等。
int a = 10; // 定义一个整型变量
int* p = &a; // 定义一个一级指针,指向变量a的地址
int** pp = &p; // 定义一个二级指针,指向一级指针p的地址
下面我们看例子分析:
#include <iostream>
using namespace std;
int main(){int value = 100;int* p = &value;int** pp = &p;cout << "&p: " << &p << endl;cout << "&value:" << &value << endl;// value的地址值cout << "p: " << p << endl;// p指向value,所以p的值就是value的地址值cout << "&p:" << &p << endl;//&p输出p本身的地址cout << "pp:" << pp << endl;//pp指向p,所以pp就是p的地址cout << "*pp:" << *pp << endl; // *pp 表示 pp 所指向的内容,即 pp 指向的 p 的值也就是value的地址值。cout << "**pp: " << **pp << endl;// **pp 表示 *pp 指向的内容,也就是p所指向的value的值cout << "*(*pp): " << *(*pp) << endl;// 跟上面结果一样cout << "&pp:" << &pp << endl;// pp本身的地址return 0;
}
pp:因为这个pp指针指向的是指针p的地址,所以pp的值就是指针p的地址值。
*pp:因为pp指向的是指针p的地址,所以*pp就是将pp指针解引用,也就是获取指针p的值,而指针p的值是指针p所指向的value变量的地址值,所以*pp的值就是value的地址值。
**pp:**pp也可以理解成*(*pp),因为*pp本身的值就是value的地址值,所以将value这个地址值解引用得到的就是value变量的值。
&pp:&pp的含义跟上面一样,将指针pp取地址,也就是pp指针本身的地址值。
三.使用指针操作数组:
1.操作一维数组:
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{int var[MAX] = {10, 100, 200};int *ptr = var;// 指针中的数组地址for (int i = 0; i < MAX; i++){cout << "var[" << i << "]的内存地址为 ";cout << ptr << endl;cout << "var[" << i << "] 的值为 ";cout << *ptr << endl;// 移动到下一个位置ptr++;}return 0;
}
输出值:
var[0]的内存地址为 0x7fff59707adc
var[0] 的值为 10
var[1]的内存地址为 0x7fff59707ae0
var[1] 的值为 100
var[2]的内存地址为 0x7fff59707ae4
var[2] 的值为 200
2.操作二维数组:
在C++中,二维数组可以看作是数组的数组。使用指针来操作二维数组能够提供更灵活的内存管理和访问方式。
(1)使用指针定义一个二维数组:
int rows = 3, cols = 4;
int** array = new int*[rows]; // 分配指向整型指针的指针数组
for (int i = 0; i < rows; i++) {array[i] = new int[cols]; // 为每一行分配内存
}
(2)使用指针访问二维数组:
怎么理解array[m]的含义是m+1行的起始地址?
假设array[1][1]是一个二维数组,那么array[1]代表含义是第二行的起始位置,在C++中,数组名可以隐式地转换为指向其第一个元素的指针。对于一个二维数组
array[m][n]
,array[i]
是指向第i
行的指针,它实际上等价于&array[i][0]
,即第二行的第一个元素的地址。当你写
array[i]
时,编译器将其解释为指向array
中第i
行的指针。array[1]
实际上是&array[1][0]
,这是因为array
是一个包含多个数组的数组,每一行都是一个数组。因此,array[i]
返回的是指向该行第一个元素的指针,方便进行进一步的指针运算。
*(*(array + 1) + 2) = 10; // 等价于 array[1][2] = 10
array
是一个指向指针的指针,即指向一个指针数组的指针。在动态分配的二维数组中,array
代表的是第一行的指针。array + 1
将array
指向的地址向后移动一个指针大小的字节,指向第二行的起始地址。这实际上是获得了指向第二行的指针。*(array + 1)
通过解引用操作符*
获取第二行的指针。换句话说,*(array + 1)
返回的是第二行数组的地址。- 在此基础上加
2
,实际上是将指向第二行的指针向后移动两个int
大小的字节,即指向第二行第三个元素的地址。这是因为在内存中,二维数组是按行存储的,+2
表示从第二行的起始地址向后移动两个元素的位置。 - 最外层的
*
表示解引用,即获取指向的内存地址中的值。因此,*(array + 1) + 2
最终指向的是第二行第三个元素的地址,加上外面的*
之后,解引用后就获取了该元素的值。
(3)使用指针遍历二维数组:
for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {*(*(array + i) + j) = i * cols + j; // 通过指针访问}
}
四.在函数中使用指针传参:
看下面的例子:
#include <iostream>
using namespace std;
void printArr(int* p,int size){for(int i = 0;i < size;i++){cout << *p << " ";p++; // p 自增1,指向下一元素的地址}
}
int main(){int arr[] = {1,2,3,4,5,6};int size = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小printArr(arr,size);return 0;
}
在上面例子中,我们将arr数组首元素地址传到函数形参指针p中,也就是在函数中指针p指向的地址就是数组首元素地址,那么如果我们将指针p在main函数内定义,并且让指针p赋值数组首元素地址,那么当指针p在函数中移动,直到数组尾元素停止,那么此时 main 函数中的指针p指向的是首元素还是尾元素?我们看下面的例子:
#include <iostream>
using namespace std;
//在调用函数 printArr 时,p 的值(即指针的地址)被复制到 printArr 函数的参数 p 中。
//printArr 函数内部的 p 是原指针的一个拷贝,改变这个拷贝不会影响调用它的外部指针的值。
//printArr 函数内部的 p 仅在 printArr 函数的作用域内有效,退出该函数后不再存在。
void printArr(int* p,int size){cout << "p初始地址" << p << endl; for(int i = 0;i < size;i++){cout << *p << " ";p++; // p 自增1,指向下一元素的地址}cout << endl;cout << "p移动后的地址" << p << endl;
}
int main(){int arr[] = {1,2,3,4,5,6};int size = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小int* p = arr;printArr(p,size);cout << p << endl;//仍然指向arr[0]cout << &arr[0] << endl;return 0;
}
在调用函数 printArr 时,p 的值(即指针的地址)被复制到 printArr 函数的参数 p 中。printArr 函数内部的 p 是原指针的一个拷贝,改变这个拷贝不会影响调用它的外部指针的值。printArr 函数内部的 p 仅在 printArr 函数的作用域内有效,退出该函数后不再存在。
那么如果将函数返回值设置成指针类型,将在函数内指针p移动后的地址返回,我们就可以让main函数内的指针p指向尾元素地址。如下面的例子:
#include <iostream>
using namespace std;
int* printArr(int* p,int size){cout << "p初始地址" << p << endl; for(int i = 0;i < size;i++){cout << *p << " ";p++; // p 自增1,指向下一元素的地址}cout << endl;cout << "p移动后的地址" << p << endl;return p; // 注意返回的是 p 而不是 &p,返回的是 p 所指向的下一个元素的地址
}
int main(){int arr[] = {1,2,3,4,5,6};int size = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小int* p = arr;p = printArr(p,size);cout << p << endl;//指向的位置是printArr函数返回的地址cout << &arr[0] << endl;return 0;
}
五.指针与引用的区别:
我们以最常见的交换两个数的函数为例,分别使用指针以及引用:
指针:
#include <iostream>
using namespace std;
//使用指针交换两个数
void swapAB(int* p,int* q){int temp = *p;*p = *q;*q = temp;
}
int main(){int a = 100;int b = 200;swapAB(&a, &b);cout << "使用指针交换两个数->" << endl;cout << "a = " << a << ", b = " << b << endl;return 0;
}
引用:
#include <iostream>
using namespace std;
void swapAB(int& p, int& q){int temp = p;p = q;q = temp;
}
int main(){int a = 100;int b = 200;swapAB(a,b);cout << "使用引用来交换两个数->" << endl;cout << "a = " << a << ", b = " << b << endl;return 0;
}
引用很容易与指针混淆,它们之间有三个主要的不同:
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
五.使用指针进行动态内存分配:
在C++中,动态内存分配允许程序在运行时根据需要分配和释放内存,这对于处理不确定大小的数据结构(如数组、链表、树等)非常有用。动态内存分配主要通过 new
和 delete
操作符来实现。以下是动态内存分配的详细讲解。
1.使用new分配空间:
单个变量分配:
int* ptr = new int; // 分配一个 int 类型的变量
*ptr = 10; // 给分配的内存赋值
数组分配:
int* arr = new int[5]; // 动态分配一个包含5个整数的数组
for (int i = 0; i < 5; ++i) {arr[i] = i * 10; // 给数组元素赋值
}
2.使用delete释放内存:
释放单个变量:
delete ptr; // 释放之前通过 new 分配的内存
ptr = nullptr; // 将指针设为 nullptr,避免野指针
释放数组:
delete[] arr; // 释放之前通过 new[] 分配的内存
arr = nullptr; // 将指针设为 nullptr
3.动态内存分配的使用:
下面是一个完整的示例,展示如何使用动态内存分配创建一个动态数组并进行操作。
#include <iostream>
using namespace std;
int main() {int size = 10;// 动态分配内存int* arr = new int[size];// 赋值for (int i = 0; i < size; ++i) {arr[i] = i * 2; // 将数组元素赋值为其索引的两倍}// 输出for (int i = 0; i < size; ++i) {cout << arr[i] << " " << endl;}// 释放内存delete[] arr;arr = nullptr; // 避免野指针return 0;
}
4.注意事项:
- 内存泄漏:动态分配的内存如果未被释放,将导致内存泄漏。确保在不再使用时调用
delete
或delete[]
释放内存。 - 重复释放:避免对同一内存区域调用
delete
两次,以防止程序崩溃。 - 未初始化指针:在使用指针之前,总是应将其初始化为
nullptr
。 - 指针安全:使用智能指针(如
std::unique_ptr
和std::shared_ptr
)可以自动管理内存,减少手动内存管理的复杂性和风险。(下面要讲)
六.智能指针的使用:
1. 什么是智能指针
智能指针是C++11引入的特性,用于管理动态分配的内存。智能指针自动控制内存的生命周期,能够帮助程序员防止内存泄漏和其他与内存管理相关的问题。
2.智能指针的类型:
unique_ptr
:独占式智能指针,不能被复制,只能转移所有权。shared_ptr
:共享式智能指针,允许多个指针共享同一块内存,通过引用计数来管理内存。weak_ptr
:弱引用智能指针,防止共享指针的循环引用,通常与shared_ptr
一起使用。
3.unique_ptr 的使用:
unique_ptr
是一种独占式智能指针,意味着同一时间只能有一个 unique_ptr
指向某个对象。它会自动管理对象的生命周期,一旦 unique_ptr
超出作用域,所指向的对象将被释放。
reset()
:重置智能指针,释放旧对象并可指向新对象。release()
:释放所有权,返回裸指针,智能指针不再管理该对象。get()
:返回指向智能指针管理的对象的原始指针,不影响智能指针的管理。operator*
和operator->
:提供对象访问功能。
构造 :
std::unique_ptr<MyClass> ptr1(new MyClass());
a. reset():
reset()
方法可以用来释放当前指针所指向的对象,并可以用新对象替代它。
void reset(T* ptr = nullptr);
// 重置为 nullptr,释放之前的对象
ptr.reset();
// 再次分配新对象
ptr.reset(new MyClass());
b. release():
release()
方法会将控制的对象的所有权转移给调用者,并返回指向该对象的指针。调用 release()
后,unique_ptr
不再管理该对象,因此不会在析构时释放它。
int main() {std::unique_ptr<MyClass> ptr(new MyClass()); // 释放控制权,ptr不再管理MyClass对象MyClass* rawPtr = ptr.release(); // 此时ptr为空if (!ptr) {std::cout << "ptr is null after release\n";}// 需要手动释放rawPtrdelete rawPtr;return 0;
}
c. get():
get()
方法返回指向当前管理对象的裸指针,但不转移所有权。使用该方法可以获取智能指针所管理的对象的原始指针。
int main() {std::unique_ptr<MyClass> ptr(new MyClass());MyClass* rawPtr = ptr.get(); // 获取原始指针// 使用rawPtr ... return 0; // ptr将自动释放对象
}
d. operator*
和 operator->:
unique_ptr
重载了 *
和 ->
操作符,使得可以像普通指针一样访问对象的成员。
int main() {std::unique_ptr<MyClass> ptr(new MyClass());// 使用 operator*(*ptr).~MyClass(); // 显示调用析构函数(一般不推荐)// 使用 operator->// ptr->someMethod();return 0; // ptr将自动释放对象
}
下面看一个例子:
#include <iostream>
#include <memory>class MyClass {
public:MyClass() { std::cout << "MyClass Constructor\n"; }~MyClass() { std::cout << "MyClass Destructor\n"; }void display() { std::cout << "Hello from MyClass!\n"; }
};int main() {std::unique_ptr<MyClass> ptr(new MyClass()); // 创建一个 unique_ptrptr->display(); // 调用成员函数// 当 ptr 超出作用域时,内存将自动释放return 0;
}
std::unique_ptr<MyClass> ptr(new MyClass());
:使用new
创建MyClass
对象,并用unique_ptr
管理它。ptr->display();
:使用->
运算符调用对象的成员函数。- 当
ptr
超出作用域时,MyClass
的析构函数会被自动调用,内存会被释放。
(1)不允许左值复制赋值操作:
std::unique_ptr
的设计目的在于确保同一时间只有一个指针拥有某个对象的所有权。为了实现这一点,unique_ptr
禁止进行左值的复制操作。这意味着你不能将一个 unique_ptr
赋值给另一个 unique_ptr
,这样会导致两个指针指向同一内存,破坏所有权的唯一性。
#include <iostream>
#include <memory>class MyClass {
public:MyClass() { std::cout << "MyClass Constructor\n"; }~MyClass() { std::cout << "MyClass Destructor\n"; }
};int main() {std::unique_ptr<MyClass> ptr1(new MyClass());// std::unique_ptr<MyClass> ptr2 = ptr1; // 编译错误:不能复制 unique_ptrstd::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 使用 std::move 转移所有权if (!ptr1) {std::cout << "ptr1 is now null.\n"; // ptr1 已被置为 nullptr}return 0;
}
(2)允许临时右值赋值:
std::unique_ptr
允许将临时右值赋值给另一个 unique_ptr
。右值是可以被移动的对象,移动操作不会涉及到复制,而是将资源的所有权从一个指针转移到另一个指针。
#include <iostream>
#include <memory>class MyClass {
public:MyClass() { std::cout << "MyClass Constructor\n"; }~MyClass() { std::cout << "MyClass Destructor\n"; }
};std::unique_ptr<MyClass> createObject() {return std::make_unique<MyClass>(); // 返回一个临时的 unique_ptr
}int main() {std::unique_ptr<MyClass> ptr = createObject(); // 允许右值赋值return 0;
}
createObject()
函数返回一个std::unique_ptr<MyClass>
的临时对象。- 在
main()
函数中,ptr
直接接受这个右值,合法并有效。
(3)在 STL 容器中使用 std::unique_ptr:
尽管 std::unique_ptr
可以存储在 STL 容器中,如 std::vector
,但我们不能直接赋值或复制 unique_ptr
。这符合 unique_ptr
的设计原则。
#include <iostream>
#include <memory>
#include <vector>class MyClass {
public:MyClass(int id) : id(id) { std::cout << "MyClass Constructor: " << id << "\n"; }~MyClass() { std::cout << "MyClass Destructor: " << id << "\n"; }
private:int id;
};int main() {std::vector<std::unique_ptr<MyClass>> vec;vec.push_back(std::make_unique<MyClass>(1)); // 合法,使用右值vec.push_back(std::make_unique<MyClass>(2)); // 合法,使用右值// vec.push_back(vec[0]); // 编译错误:不能复制 unique_ptrreturn 0;
}
(4)支持对象数组的内存管理:
std::unique_ptr
可以使用数组形式来管理动态分配的数组内存。需要注意的是,使用 std::unique_ptr
管理数组时,必须使用 std::unique_ptr<Type[]>
。
#include <iostream>
#include <memory>int main() {std::unique_ptr<int[]> arr(new int[5]); // 创建一个动态数组// 初始化数组for (int i = 0; i < 5; ++i) {arr[i] = i * 10;}// 输出数组内容for (int i = 0; i < 5; ++i) {std::cout << "arr[" << i << "] = " << arr[i] << "\n";}// 不需要手动 delete,arr 超出作用域时会自动释放内存return 0;
}
4.shared_ptr 的使用:
shared_ptr
允许多个指针共享同一块内存。它使用引用计数来管理内存,只有当所有引用都释放后,才会释放内存。
什么是share_ptr的引用计数?
引用计数是
std::shared_ptr
管理内存的核心机制。它用于跟踪有多少个shared_ptr
实例共享同一块内存(即同一个对象)。每次创建一个shared_ptr
实例指向某个对象时,该对象的引用计数会增加;每当一个shared_ptr
实例被销毁、重置或重新赋值时,引用计数会减少。
下面是关于它的使用方法:
#include <iostream>
#include <memory>struct MyClass {MyClass(int value) : value(value) {std::cout << "Constructor called: " << value << std::endl;}~MyClass() {std::cout << "Destructor called: " << value << std::endl;}int value;
};int main() {// 1. 创建 shared_ptrstd::shared_ptr<MyClass> ptr1(new MyClass(10));// 2. 访问值std::cout << "Value of ptr1: " << ptr1->value << std::endl;// 3. 使用 make_shared 创建 shared_ptr,分配内存效率更高(推荐使用)auto ptr2 = std::make_shared<MyClass>(20);std::cout << "Value of ptr2: " << ptr2->value << std::endl;// 4. 复制 shared_ptrstd::shared_ptr<MyClass> ptr3 = ptr1; // 引用计数增加std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 2// 5. 使用 reset() 方法后,指针的引用计数会-1// 当释放旧对象,指向新对象,这个时候ptr指针的引用计数为1ptr1.reset(new MyClass(30));std::cout << "Value of ptr1 after reset: " << ptr1->value << std::endl;std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 1std::cout << "ptr3 use count: " << ptr3.use_count() << std::endl; // 1// 6. 交换 ptr2 和 ptr1ptr1.swap(ptr2);std::cout << "After swap:" << std::endl;std::cout << "Value of ptr1: " << ptr1->value << std::endl; // 20std::cout << "Value of ptr2: " << ptr2->value << std::endl; // 30// 7. 移动共享指针std::shared_ptr<MyClass> ptr4 = std::move(ptr2);std::cout << "After move:" << std::endl;std::cout << "ptr2 is now: " << (ptr2 ? "not null" : "null") << std::endl; // nullstd::cout << "Value of ptr4: " << ptr4->value << std::endl; // 30// 8. use_count获取引用计数 和 unique判断是否唯一性(唯一返回1不唯一返回0)std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 1std::cout << "ptr4 unique: " << ptr4.unique() << std::endl; // 1return 0;
}
如何判断引用计数的增加与减少?
#include <iostream>
#include <memory>struct Node {int value;std::shared_ptr<Node> next; // 使用 shared_ptr 管理下一个节点Node(int val) : value(val) {std::cout << "Node constructed: " << value << std::endl;}~Node() {std::cout << "Node destructed: " << value << std::endl;}
};int main() {std::shared_ptr<Node> head = std::make_shared<Node>(1);std::cout << "Reference count after creating head: " << head.use_count() << std::endl; // 1head->next = std::make_shared<Node>(2);std::cout << "Reference count for head: " << head.use_count() << std::endl; // 1std::cout << "Reference count for head->next: " << head->next.use_count() << std::endl; // 1std::shared_ptr<Node> anotherPtr = head; // 引用计数增加std::cout << "Reference count after anotherPtr = head: " << head.use_count() << std::endl; // 2std::cout << "anotherPtr.reset(): " << anotherPtr.use_count() << std::endl; // 2anotherPtr.reset(); // 引用计数减少std::cout << "Reference count after anotherPtr.reset(): " << head.use_count() << std::endl; // 1head.reset(); // 引用计数减少,节点被销毁std::cout << "Reference count after head.reset(): " << (head ? head.use_count() : 0) << std::endl; // 0return 0;
}
创建
head
:
- 创建
head
后,引用计数为 1。创建
head->next
:
head->next
的引用计数为 1。复制
shared_ptr
:
- 当
anotherPtr
指向head
时,head
的引用计数增加到 2。重置
anotherPtr
:
- 调用
anotherPtr.reset()
后,引用计数减少到 1。重置
head
:
- 最后调用
head.reset()
,引用计数降到 0,触发节点的析构函数,释放内存。
下面看代码示例:
#include <iostream>
#include <memory>class MyClass {
public:MyClass() { std::cout << "MyClass Constructor\n"; }~MyClass() { std::cout << "MyClass Destructor\n"; }void display() { std::cout << "Hello from MyClass!\n"; }
};
void sharePointerExample() {std::shared_ptr<MyClass> ptr1(new MyClass()); // 创建第一个 shared_ptr{std::shared_ptr<MyClass> ptr2 = ptr1; // ptr2 共享 ptr1 的所有权ptr2->display(); // 使用 ptr2std::cout << "Reference Count: " << ptr2.use_count() << std::endl; // 输出引用计数} // ptr2 超出作用域,引用计数减少std::cout << "Reference Count: " << ptr1.use_count() << std::endl; // 输出引用计数
} // ptr1 超出作用域,内存释放
int main() {sharePointerExample();return 0;
}
std::shared_ptr<MyClass> ptr1(new MyClass());
:创建一个shared_ptr
,并管理MyClass
对象的内存。std::shared_ptr<MyClass> ptr2 = ptr1;
:ptr2
共享ptr1
的所有权,引用计数增加。ptr2.use_count()
:返回指向同一对象的shared_ptr
数量。- 当
ptr2
超出作用域时,引用计数减少,内存不会立即释放,因为ptr1
仍然持有该内存。
但是,使用share_ptr需要注意,会出现循环引用的问题,例如下面代码:
#include <iostream>
#include <memory>struct Node {std::shared_ptr<Node> next; // 指向下一个节点Node() {std::cout << "Node created." << std::endl;}~Node() {std::cout << "Node destroyed." << std::endl;}
};int main() {std::shared_ptr<Node> node1 = std::make_shared<Node>();std::shared_ptr<Node> node2 = std::make_shared<Node>();// 循环引用node1->next = node2; // node1指向node2node2->next = node1; // node2指向node1return 0;
}
在上述代码中,
node1
和node2
互相引用,这导致它们的引用计数永远不会降到零,因此程序结束时它们的析构函数不会被调用,造成内存泄漏。
为什么引用计数会无法归零?
node1->next
赋值为node2
,这使得node2
的引用计数增加到 2(因为现在node1
也引用了node2
)。node2->next
赋值为node1
,这使得node1
的引用计数增加到 2(因为现在node2
也引用了node1
)。此时node1与node2
的引用计数都为 2。当
main
函数结束时,node1
和node2
的作用域结束,会发生下面情况:
node1
和node2
的析构:它们的析构函数被调用。当一个shared_ptr
被销毁时,它会减少所管理对象的引用计数。如果node1
的引用计数从 2 降到 1,则只减少了指向node1
的引用,node2
的引用计数仍然为 2。同理node2
的引用计数从 2 降到 1,仅减少了指向node2
的引用而node1的引用计数仍然不变还是1。所以总体来说由于node1
和node2
互相引用,这就导致它们的引用计数不会归零,但在使用weak_ptr
后,可以在下一步中自动释放内存。
为了解决这个问题,可以将 next
指针改为 std::weak_ptr
。这样,Node
结构体的成员 next
不会增加引用计数,从而避免循环引用。(见下面)
5.weak_ptr 的使用:
weak_ptr
是一种弱引用智能指针,它不会影响 shared_ptr
的引用计数。weak_ptr
通常与 shared_ptr
一起使用,用于解决循环引用的问题。因为weak_ptr
不会增加引用计数,因此它不会阻止被管理对象的析构。
我们先改造上面因循环引用而出现无法析构的代码:
#include <iostream>
#include <memory>struct Node {std::weak_ptr<Node> next; // 使用 weak_ptr 避免循环引用Node() {std::cout << "Node created." << std::endl;}~Node() {std::cout << "Node destroyed." << std::endl;}
};int main() {std::shared_ptr<Node> node1 = std::make_shared<Node>();std::shared_ptr<Node> node2 = std::make_shared<Node>();// 使用 weak_ptr 避免循环引用node1->next = node2; // node1指向node2node2->next = node1; // node2指向node1return 0;
}
使用
std::weak_ptr
:将next
的类型从std::shared_ptr<Node>
改为std::weak_ptr<Node>
,这样next
不会增加引用计数。防止内存泄漏:当
main()
函数结束时,node1
和node2
的引用计数将都降到零,析构函数将被调用,内存正常释放。这样就可以巧妙地避免了share_ptr的弊端 。
下面是 weak_ptr 的使用方法:
#include <iostream>
#include <memory>struct Node {int value;std::shared_ptr<Node> next; // 使用 shared_ptr 管理下一个节点Node(int val) : value(val) {std::cout << "Node constructed: " << value << std::endl;}~Node() {std::cout << "Node destructed: " << value << std::endl;}
};int main() {std::shared_ptr<Node> head = std::make_shared<Node>(1);head->next = std::make_shared<Node>(2);// 1.创建 weak_ptr 指向 headstd::weak_ptr<Node> weakHead = head;// 2.检查 weak_ptr 是否有效,lock() 方法尝试将 weak_ptr 转换为 shared_ptr。// 只有在对象存在的情况下才访问或操作对象,避免因访问空指针而导致的错误// if判断这一行不仅尝试获取一个 shared_ptr,还会在其成功时进行判断。if (auto sharedHead = weakHead.lock()) {std::cout << "Weak pointer is valid, value: " << sharedHead->value << std::endl;} else {std::cout << "Weak pointer is expired." << std::endl;}head.reset(); // 重置 head,引用计数为0// 3.再次检查 weak_ptr,lock() 方法尝试将 weak_ptr 转换为 shared_ptr。if (auto sharedHead = weakHead.lock()) {std::cout << "Weak pointer is valid, value: " << sharedHead->value << std::endl;} else {std::cout << "Weak pointer is expired." << std::endl;// 这时输出节点不再存在}return 0;
}
使用
weak_ptr
指向的对象,我们必须通过lock()
方法将其转换为shared_ptr
。这是因为weak_ptr
本身并不拥有对象的所有权,也不直接引用对象,因此不能直接访问对象的方法或成员。
下面看代码示例:
#include <iostream>
#include <memory>class MyClass {
public:MyClass() { std::cout << "MyClass Constructor\n"; }~MyClass() { std::cout << "MyClass Destructor\n"; }void display() { std::cout << "Hello from MyClass!\n"; }
};
int main() {std::shared_ptr<MyClass> sharedPtr(new MyClass()); // 创建 shared_ptrstd::weak_ptr<MyClass> weakPtr = sharedPtr; // 创建 weak_ptrstd::cout << "Reference Count: " << sharedPtr.use_count() << std::endl; // 输出引用计数if (auto tempPtr = weakPtr.lock()) { // 尝试获取 shared_ptrtempPtr->display(); // 使用 tempPtr} else {std::cout << "Weak pointer is expired.\n";}sharedPtr.reset(); // 释放 shared_ptrif (auto tempPtr = weakPtr.lock()) { // 再次尝试获取tempPtr->display();} else {std::cout << "Weak pointer is expired.\n";}return 0;
}
std::weak_ptr<MyClass> weakPtr = sharedPtr;
:创建一个weak_ptr
,指向shared_ptr
的对象,但不增加引用计数。weakPtr.lock()
:尝试获取一个shared_ptr
,如果原对象已被释放,返回nullptr
。- 使用
weak_ptr
可以避免循环引用的问题,允许指向的对象在不再被使用时被正确释放。
下面通过链表代码来解析这两个指针的相互使用:
#include <iostream>
#include <memory>struct Node {int value;std::weak_ptr<Node> next; // 使用 weak_ptr 避免循环引用Node(int val) : value(val) {std::cout << "Node constructed: " << value << std::endl;}~Node() {std::cout << "Node destructed: " << value << std::endl;}void printValue() {std::cout << "Node value: " << value << std::endl;}
};int main() {std::shared_ptr<Node> head = std::make_shared<Node>(1);std::shared_ptr<Node> second = std::make_shared<Node>(2);// 形成链表结构head->next = second; // head 指向 secondsecond->next = head; // second 指向 head (注意:此时为 weak_ptr)// 使用 weak_ptr 调用成员函数if (auto sharedNext = head->next.lock()) { // 尝试获取 shared_ptrsharedNext->printValue(); // 调用 printValue 函数} else {std::cout << "Next node is expired." << std::endl;}// 重置 head,触发析构head.reset(); // 此时引用计数降为 1,second 仍然存在// 再次尝试访问 secondif (auto sharedNext = second->next.lock()) { // 尝试获取 shared_ptrsharedNext->printValue(); // 调用 printValue 函数} else {std::cout << "Next node is expired." << std::endl;}second.reset(); // 触发析构return 0;
}
总结:
std::unique_ptr
:
- 独占内存,自动释放。
- 不可复制,但可以移动。
- 适合用于表示独占资源。
std::shared_ptr
:
- 允许多个指针共享同一资源,使用引用计数。
- 适合用于需要多个所有者的场景。
std::weak_ptr
:
- 防止循环引用,允许观察对象但不拥有。
- 常与
shared_ptr
一起使用。