C++ --- 指针的使用(如何理解指针?指针的细节你又了解多少?)

目录

一.什么是指针?

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)支持对象数组的内存管理:

 4.shared_ptr 的使用:

什么是share_ptr的引用计数?

如何判断引用计数的增加与减少?

为什么引用计数会无法归零?

 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
  1. array 是一个指向指针的指针,即指向一个指针数组的指针。在动态分配的二维数组中,array 代表的是第一行的指针。
  2. array + 1array 指向的地址向后移动一个指针大小的字节,指向第二行的起始地址。这实际上是获得了指向第二行的指针。
  3. *(array + 1) 通过解引用操作符 * 获取第二行的指针。换句话说,*(array + 1) 返回的是第二行数组的地址。
  4. 在此基础上加 2,实际上是将指向第二行的指针向后移动两个 int 大小的字节,即指向第二行第三个元素的地址。这是因为在内存中,二维数组是按行存储的,+2 表示从第二行的起始地址向后移动两个元素的位置。
  5. 最外层的 * 表示解引用,即获取指向的内存地址中的值。因此,*(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++中,动态内存分配允许程序在运行时根据需要分配和释放内存,这对于处理不确定大小的数据结构(如数组、链表、树等)非常有用。动态内存分配主要通过 newdelete 操作符来实现。以下是动态内存分配的详细讲解。

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.注意事项:

  • 内存泄漏:动态分配的内存如果未被释放,将导致内存泄漏。确保在不再使用时调用 deletedelete[] 释放内存。
  • 重复释放:避免对同一内存区域调用 delete 两次,以防止程序崩溃。
  • 未初始化指针:在使用指针之前,总是应将其初始化为 nullptr
  • 指针安全:使用智能指针(如 std::unique_ptrstd::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;
}

 在上述代码中,node1node2 互相引用,这导致它们的引用计数永远不会降到零,因此程序结束时它们的析构函数不会被调用,造成内存泄漏。

为什么引用计数会无法归零?

node1->next 赋值为 node2,这使得 node2 的引用计数增加到 2(因为现在 node1 也引用了 node2)。node2->next 赋值为 node1,这使得 node1 的引用计数增加到 2(因为现在 node2 也引用了 node1)。此时node1与node2 的引用计数都为 2。

main 函数结束时,node1node2 的作用域结束,会发生下面情况:

node1node2 的析构:它们的析构函数被调用。当一个 shared_ptr 被销毁时,它会减少所管理对象的引用计数。如果 node1 的引用计数从 2 降到 1,则只减少了指向 node1 的引用,node2 的引用计数仍然为 2。同理 node2 的引用计数从 2 降到 1,仅减少了指向 node2 的引用而node1的引用计数仍然不变还是1。所以总体来说由于 node1node2 互相引用,这就导致它们的引用计数不会归零,但在使用 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() 函数结束时,node1node2 的引用计数将都降到零,析构函数将被调用,内存正常释放。

这样就可以巧妙地避免了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 一起使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/884189.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vi —— 终端中的编辑器

目标 vi 简介打开和新建文件三种工作模式常用命令分屏命令常用命令速查图 01. vi 简介 1.1 学习 vi 的目的 在工作中&#xff0c;要对 服务器 上的文件进行 简单 的修改&#xff0c;可以使用 ssh 远程登录到服务器上&#xff0c;并且使用 vi 进行快速的编辑即可常见需要修改…

stm32cubeIde 使用笔记

划分flash空间 需要更改STM32xxx_FLASH.ld文件 输出其他格式文件

图片批量处理神器将每个文件夹中的多张图片拼接,一键实现横向和纵向的长图拼接效果,让你的图片处理更高效

是不是经常面对一堆图片文件夹&#xff0c;想要把它们里面的宝贝图片一一拼接起来&#xff0c;却又被繁琐的操作吓得直摇头&#xff1f;别担心&#xff0c;今天我要给大家介绍一位图片处理界的超级英雄——首助编辑高手软件&#xff01;它就像是一位拥有魔法的图片大师&#xf…

【JVM详解JVM优化】聊聊JVM优化

简介&#xff1a; 前面两期文章讲了JVM内存模型&#xff1a;【JVM详解&JVM优化】JVM内存模型-CSDN博客 以及JVM垃圾回收机制&#xff1a;【JVM详解&JVM优化】JVM垃圾回收机制-CSDN博客 在本篇文章中&#xff0c;我们将深入探讨Java虚拟机&#xff08;JVM&#xff09;…

通俗易懂的餐厅例子来讲解JVM

餐厅版本 JVM&#xff08;Java虚拟机&#xff09;可以想象成一个虚拟的计算机&#xff0c;它能够运行Java程序。为了让你更容易理解&#xff0c;我们可以用一个餐厅的比喻来解释JVM&#xff1a; 菜单&#xff08;Java源代码&#xff09;&#xff1a; 想象一下&#xff0c;Java…

一文搞懂各种Attention机制

1.各种Attention 最近在重读Transformer论文的过程中&#xff0c;结合其他看过的资料&#xff0c;对各种Attention概念有进一步的了解。回顾最初刚接触时候的迷糊&#xff0c;觉得有必要写一篇文章记录一下对各种attention新的理解。 2.论文中的Transformer架构图 先上经典的…

Python+Appium+Pytest+Allure自动化测试框架-代码篇

文章目录 自动化测试框架工程目录示例测试代码示例结果查看allurepytest编写pytest测试样例的规则pytest conftest.py向测试函数传参 appium启动appium服务代码端通过端口与appium服务通信对设备进行操作在pytest测试用例中调用appium 更多功能 PythonAppiumPytestAllure自动化…

【C++】红黑树的Iterator改造以及mapset的模拟实现与封装

目录 01.红黑树的迭代器 operator: operator*、-> operator、! 02.红黑树的改造 begin和end方法 keyOfValue insert方法 find方法 size方法 clear方法 03.map&set的模拟实现 01.红黑树的迭代器 前面的博客我们介绍了红黑树的底层原理并手撕了一个自己的红…

微信小程序服务通知

项目中用到了小程序的服务消息通知&#xff0c;通知订单状态信息&#xff0c;下边就是整理的一下代码&#xff0c;放到项目中&#xff0c;把项目的小程序appid和小程序的secret写进去&#xff0c;直接运行即可 提前申请好小程序服务信息通知短信模板&#xff0c;代码需要用到模…

linux命令行的艺术

文章目录 前言基础日常使用文件及数据处理系统调试单行脚本冷门但有用仅限 OS X 系统仅限 Windows 系统在 Windows 下获取 Unix 工具实用 Windows 命令行工具Cygwin 技巧 更多资源免责声明 熟练使用命令行是一种常常被忽视&#xff0c;或被认为难以掌握的技能&#xff0c;但实际…

2024年最新版SSL证书

SSL证书行业变动很大&#xff0c;随着操作系统&#xff0c;浏览器新版本不断增加&#xff0c;对SSL证书兼容性要求越来也高&#xff0c;对于安全性也有所提升&#xff0c;主流CA机构根证书及交叉链迎来了换新&#xff0c;这是为了延续下一个20个年的安全计划的提前不如&#xf…

Spark入门到实践

Spark入门到实践 一、Spark 快速入门1.1 Spark 概述1.2 Spark 最简安装1.3 Spark实现WordCount1.3.1 下载安装Scala1.3.2 添加Spark依赖1.3.3 Scala实现WordCount1.3.4 通过IDEA运行WordCount1.3.5 IDEA配置WordCount输入与输出路径1.3.6 通过IDEA运行WordCount1.3.7 查看运行结…

vue、小程序腾讯地图开放平台使用

一、登录账号 腾讯地图API 官方文档&#xff1a; 腾讯位置服务 - 立足生态&#xff0c;连接未来 二、申请秘钥 key 从首页【开发文档】-【微信小程序 SDK】进到微信小程序的开发文档&#xff1a;微信小程序JavaScript SDK | 腾讯位置服务 然后我们根据【Hello World】的提示…

电赛入门之软件stm32keil+cubemx

hal库可以帮我们一键生成许多基本配置&#xff0c;就不需要自己写了&#xff0c;用多了hal库就会发现原来用基本库的时候都过的什么苦日子&#xff08;笑 下面我们以f103c8t6&#xff0c;也就是经典的最小核心板来演示 一、配置工程 首先来新建一个工程 这里我们配置rcc和sys&…

Elasticsearch开源仓库404 7万多star一夜清零

就在昨晚&#xff0c;有开发者惊奇地发现自己的开源项目 star 数竟然超过了最流行的开源全文搜索引擎 Elasticsearch。发生了什么事&#xff1f;Elasticsearch 竟然跌得比股票还凶 —— 超 7 万 star 的 GitHub 仓库竟然只剩下 200 多。 从社交媒体的动态来看&#xff0c;Elast…

汽车免拆诊断案例 | 2010款起亚赛拉图车发动机转速表指针不动

故障现象  一辆2010款起亚赛拉图车&#xff0c;搭载G4ED 发动机&#xff0c;累计行驶里程约为17.2万km。车主反映&#xff0c;车辆行驶正常&#xff0c;但组合仪表上的发动机转速表指针始终不动。 故障诊断  接车后进行路试&#xff0c;车速表、燃油存量表及发动机冷却温度…

【电商搜索】现代工业级电商搜索技术-亚马逊-经典的Item-to-Item协同推荐算法

【电商搜索】现代工业级电商搜索技术-亚马逊-经典的Item-to-Item协同推荐算法 文章目录 【电商搜索】现代工业级电商搜索技术-亚马逊-经典的Item-to-Item协同推荐算法1. 论文信息2. 算法介绍3. 创新点小结4. 实验效果5. 算法结论6. 代码实现7. 问题及优化方向1. 冷启动问题2. 稀…

Java - 数组实现大顶堆

题目描述 实现思路 要实现一个堆&#xff0c;我们首先要了解堆的概念。 堆是一种完全二叉树&#xff0c;分为大顶堆和小顶堆。 大顶堆&#xff1a;每个节点的值都大于或等于其子节点的值。 小顶堆&#xff1a;每个节点的值都小于或等于其子节点的值。 完全二叉树&#xff…

人工智能与数据安全:Facebook如何应对隐私挑战

在数字时代&#xff0c;数据隐私和安全成为了用户和企业关注的核心问题。作为全球最大的社交媒体平台之一&#xff0c;Facebook面临着日益严峻的隐私挑战。近年来&#xff0c;频繁发生的数据泄露事件和对用户隐私的质疑&#xff0c;使得Facebook在保护用户数据方面倍感压力。为…

2024年ABS分区更新,聚焦管理科学领域新动态

2024学术期刊指南简介 2024年10月30日&#xff0c;英国商学院协会&#xff08;Chartered Association of Business Schools&#xff09;发布了最新的《学术期刊指南&#xff08;Academic Journal Guide&#xff09;》&#xff08;以下简称“《指南》”&#xff09;&#xff0c…