C++指针(超详解)

在C++中,指针是一个非常重要的概念,它代表了一个内存地址,而不是一个具体的值。

指针的引入主要是为了解决什么问题?主要的作用是什么?

指针的引入主要是为了高效地管理内存和提供灵活的数据结构操作。具体来说,指针解决了以下几个关键问题:

(1)动态内存管理:指针允许程序在运行时动态地分配和释放内存,这对于处理大小未知的数据(如数组、链表等)非常重要。通过newdelete,程序可以根据需求申请和释放内存,有效利用系统资源。

(2)实现一些高效的数据结构:指针是实现复杂数据结构(如链表、树、图等)的基础。它们允许数据元素之间通过地址直接连接,从而实现灵活的插入和删除操作,这些操作在数组中通常较为低效。

(3)访问和修改数据:指针使得可以直接访问内存中的数据,通过指针可以方便地读取和修改数据,特别是在处理结构体和类的成员时。

(4)资源管理:指针提供了对系统资源(如文件、网络连接等)的灵活管理。通过指针,可以实现对资源的共享和引用计数,帮助避免资源泄漏。

指针实现上述功能背后的内在机制是什么?

指针背后的内在机制主要涉及内存管理和地址引用的概念。

每个变量在内存中都有一个唯一的地址。指针存储的是这个地址,而不是变量的值。这使得指针能够直接访问和操作内存中的数据。指针的类型决定了指针所指向的数据的类型。在C++中,指针类型与所指向的变量类型匹配,以便正确地进行内存访问和操作。例如,int*指针表示它指向一个整型变量,编译器能够根据类型信息进行适当的解引用。

使用new关键字,程序可以在堆内存中动态分配内存。分配的内存地址通过指针返回,程序可以利用这个指针来访问和修改这块内存。通过delete关键字,可以释放这块内存,避免内存泄漏。

指针支持算术运算,例如自增和自减。这是因为指针存储的是地址,增加或减少指针的值会移动到下一个或前一个相同类型的数据位置。这在处理数组时特别有用。

在现代C++中,智能指针(如std::shared_ptrstd::unique_ptr)通过引用计数和自动内存管理来增强指针的功能。这些机制帮助自动管理内存的生命周期,减少内存泄漏的风险。

以下是指针的主要作用及其使用方法的详细讲解:

一.指针的定义和基本操作

指针的定义使用*符号,表示指针类型。你可以通过&运算符获取变量的地址。

int a = 10; //定义一个整型变量
int* p = &a; //定义一个整型指针,指向变量a的地址

其中int*的作用是声明一个指向整型(int)的指针,p是指针变量,&是取地址符,

int* p = &a;

所以,上述代码的意思是将整型变量a的地址用&取出赋值给指针变量p

代码示例:

#include <iostream>

int main(){

    int a = 10;

    int* p = &a;

    std::cout << "p : " << p << std::endl;//打印p指向的地址空间

    return 0;
}

运行结果:

p : 0x1c0c1ff864

二.访问和修改值

通过指针,可以访问和修改指针指向的值。使用*运算符可以解引用指针,从而访问它所指向的变量。

std::cout << "Value of a: " << *p << std::endl; // 输出10
*p = 20;  // 修改指针p所指向的值
std::cout << "New value of a: " << a << std::endl; // 输出20

代码示例:

#include <iostream>

int main(){

    int a = 10;

    int* p = &a;


    std::cout << "修改前指针p指向的地址空间存储的值*p: " << *p << std::endl;//打印p指向的地址空间存储的内容

    *p = 20;

    std::cout << "修改前指针p指向的地址空间存储的值*p: " << *p<< std::endl;

    std::cout << "修改后a的值为:" << a << std::endl;

    return 0;
}

运行结果:

修改前指针p指向的地址空间存储的值*p: 10
修改前指针p指向的地址空间存储的值*p: 20
修改后a的值为:20

上述代码中有两个*号,它们的作用不一样。int* p中的*号用于声明一个指针类型,*p = 20中的*号是解引用操作,用来解引用指针,获取指针所指向地址的内容。

三. 动态内存分配

指针常用于动态内存分配,通过new关键字申请内存,使用delete释放内存。

int* arr = new int[5]; // 动态分配一个整型数组
for (int i = 0; i < 5; ++i) {
    arr[i] = i + 1; // 初始化数组
}
for (int i = 0; i < 5; ++i) {
    std::cout << arr[i] << " "; // 输出1 2 3 4 5
}
delete[] arr; // 释放动态分配的内存

以下是一个具体的代码示例,说明如何使用指针进行动态内存分配,通过new关键字申请内存,并使用delete释放内存:

#include <iostream>

int main() {
    // 动态分配一个整型变量
    int* ptr = new int; // 使用new关键字申请内存
    *ptr = 42;          // 给动态分配的内存赋值

    std::cout << "Value: " << *ptr << std::endl; // 输出42

    // 释放动态分配的内存
    delete ptr; // 使用delete释放内存
    ptr = nullptr; // 将指针置为nullptr以避免悬空指针

    // 动态分配一个整型数组
    int size = 5;
    int* arr = new int[size]; // 申请一个整型数组的内存

    // 初始化数组
    for (int i = 0; i < size; ++i) {
        arr[i] = i + 1; // 给数组赋值
    }

    // 输出数组内容
    std::cout << "Array: ";
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " "; // 输出1 2 3 4 5
    }
    std::cout << std::endl;

    // 释放数组的内存
    delete[] arr; // 使用delete[]释放数组内存
    arr = nullptr; // 将指针置为nullptr

    return 0;
}

代码解释:

(1)动态分配单个整型:

  • int* ptr = new int;:通过new申请一块整型内存。
  • *ptr = 42;:给这块内存赋值。
  • delete ptr;:使用delete释放这块内存,避免内存泄漏。

(2)动态分配整型数组

  • int* arr = new int[size];:申请一个整型数组的内存。
  • 通过循环初始化数组并输出内容。
  • delete[] arr;:使用delete[]释放数组的内存。

这种动态内存管理方法使得程序在运行时能够灵活处理不同大小的数据。

四. 指针与函数

指针可以作为函数参数,允许函数修改外部变量的值。这被称为“按引用传递”。

#include <iostream>

// 函数声明,接受整型指针作为参数
void modifyValue(int* ptr) {
    *ptr = 20; // 修改指针所指向的值
}

int main() {
    int value = 10; // 定义一个整型变量
    std::cout << "Before: " << value << std::endl; // 输出10

    // 将变量的地址传递给函数
    modifyValue(&value); // 使用&获取value的地址

    std::cout << "After: " << value << std::endl; // 输出20

    return 0;
}

代码解释:

1.函数定义

  • void modifyValue(int* ptr):函数接受一个整型指针作为参数。
  • 在函数内部,通过*ptr = 20;修改指针指向的值。

2.主函数

  • 定义一个整型变量value,初始值为10。
  • 调用modifyValue(&value);,使用&运算符传递value的地址。

3.输出结果

  • 在函数调用前输出value的值,结果为10。
  • 函数调用后,value的值被修改为20,再次输出结果为20。

这个示例清楚地展示了通过指针实现按引用传递,使得函数可以直接修改外部变量的值。

五. 指针数组和数组指针

指针数组是一个数组,其每个元素都是指针;而数组指针是指向数组的指针。

1.指针数组

指针数组是一个数组,其中每个元素都是指向某个类型数据的指针。

指针数组的代码示例:

#include <iostream>

int main() {
    // 定义三个整型变量
    int a = 10, b = 20, c = 30;

    // 定义一个指针数组,其中每个元素都是指向整型的指针
    int* ptrArr[3]; // ptrArr 是一个有3个元素的数组,每个元素都是 int*

    // 将指针数组中的每个元素指向不同的整型变量
    ptrArr[0] = &a;
    ptrArr[1] = &b;
    ptrArr[2] = &c;

    // 通过指针数组访问并输出每个变量的值
    for (int i = 0; i < 3; ++i) {
        std::cout << "Value of ptrArr[" << i << "] points to: " << *ptrArr[i] << std::endl;
    }

    return 0;
}

输出结果:

Value of ptrArr[0] points to: 10
Value of ptrArr[1] points to: 20
Value of ptrArr[2] points to: 30

代码解释:

  • int* ptrArr[3];:声明了一个指针数组,该数组包含三个元素,每个元素是一个int*(指向整型的指针)。
  • ptrArr[0] = &a; 等操作将指针数组的每个元素指向变量abc的地址。
  • *ptrArr[i]解引用每个指针,输出指针指向的值。

2.数组指针

数组指针是一个指向整个数组的指针。数组指针与指针数组的不同在于它是指向数组本身,而不是数组中的每个元素。

数组指针的代码示例:

#include <iostream>

int main() {
    // 定义一个具有3个元素的整型数组
    int arr[3] = {10, 20, 30};

    // 定义一个指向该数组的指针
    int (*ptrToArray)[3] = &arr; // ptrToArray 是一个指向包含3个整型元素的数组的指针

    // 通过数组指针访问数组的内容
    for (int i = 0; i < 3; ++i) {
        std::cout << "Value at arr[" << i << "] = " << (*ptrToArray)[i] << std::endl;
    }

    return 0;
}

输出结果:

Value at arr[0] = 10
Value at arr[1] = 20
Value at arr[2] = 30

代码解释:

  • int (*ptrToArray)[3];:定义了一个指向整型数组的指针,该指针指向一个包含3个整型元素的数组。
  • ptrToArray = &arr;:将数组的地址赋值给数组指针。
  • (*ptrToArray)[i]:通过数组指针解引用,然后使用下标运算符访问数组元素。

区别总结:

  • 指针数组:是一个数组,每个元素都是一个指针。示例中ptrArr是一个包含3个指针的数组。
  • 数组指针:是一个指针,它指向一个数组。示例中ptrToArray是一个指向包含3个整型元素的数组的指针。

这两个概念通过不同的语法和使用场景实现了对数组和指针的操作,理解它们的区别对于处理复杂数据结构时非常重要。

六. 指向指针的指针(二级指针)

可以定义指向指针的指针,这在多级指针操作中很有用。

1.指针的定义

  • 一级指针:是一个指针变量,指向一个普通变量,并保存该普通变量的地址;
  • 二级指针:是一个指针变量,指向一个一级指针,并保存该一级指针的地址;

2.引入二级指针

代码示例:

#include <iostream>

int main(){
    int a = 10;
    int b = 20;
    int* p = &a;
    int** s = &p;

    //一次解引用*s 此时类型为 int*
    *s = &b;
    std::cout << "*s: " << *s << std::endl;
    //二次解引用**s 此时类型为 int
    **s = 200;
    std::cout << "**s: " << **s << std::endl;
    return 0;
}

运行结果:

*s: 0x5b10dff850
**s: 200

逻辑关系如下:
a是一个int类型的变量,一级指针p指向a,并保存a的地址;
二级指针变量s指向一级指针变量p,并保存p的地址

二级指针s解引用操作:

一次解引用
*s的类型变成了(int*)(代表着一级指针p)间接改变了p的指向,从a的地址变成了b的地址;
二次解引用
**s的类型变成了int (代表着变量b),此时**s = 200;(等价于b = 200;)

3.深入理解二级指针

代码示例:

#include <iostream>

int main(){
    
    //普通变量
    int a1 = 1;
    int a2 = 1;
    int a3 = 1;
    
    //一级指针
    int* p1 = &a1;
    int* p2 = &a2;
    int* p3 = &a3;
    
    //二级指针
    int **s = &p1;
    
    
    
    return 0;
}

(假设a1,a2,a3空间连续,p1,p2,p3空间连续)逻辑图如下:

接下来我们结合上面的逻辑图看看下面这张表

分析:

s + 1:

  • s 是一个指向指针的指针,表示它是一个二级指针,类型为 int**。它指向一个指向 int 类型的指针(即 int*)。
  • s + 1 的意思是移动到下一个指针的地址,即s+1 表示二级指针s指向了p2。由于 sint** 类型,移动的字节数为 sizeof(int*),也就是 4 个字节(假设在32位系统上,指针的大小为4字节)。

*s + 1:

  • *s 是对 s 进行一次解引用操作。由于 s 是一个 int**,解引用 *s 会得到一个 int*,即 s 所指向的 p1(一级指针)。
  • *s + 1 时,表示对 p1 进行操作,p1 是一个 int*,它指向一个 int 类型的变量,然后*s + 1 ,相当于p1指向了a2的地址。
  • *s + 1 表示将 p1 向下一个 int 类型的位置移动,因为 sizeof(int) 通常是 4 个字节(在大多数系统中)。所以,这个表达式移动了 4 字节,类型变为 int*

**s+1:

  • **s 是对 s 进行两次解引用。*s 是一个 int*,进一步解引用 **s 就得到了 int 类型的数据,也就是 p1 所指向的具体 int 值。
  • 在表达式 **s + 1 中,我们对 int 类型的数据进行操作,相当于取出 a1 的值并加 1。
  • 由于 a1 是一个 int,其值加 1 就得到新的 int 值。**s + 1 结果是一个 int 类型,a1 = 1,**s+1 = 2。

七.智能指针

智能指针(Smart Pointer)是C++中的一种类模板,用于管理动态内存分配的生命周期,以防止内存泄漏和悬空指针问题。在C++11标准引入后,智能指针成为了现代C++编程中重要的资源管理工具。

1.为什么要使用智能指针?

智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

如下例子就是内存泄露的例子:

#include <iostream>
#include <string>
#include <memory>

using namespace std;


// 动态分配内存,没有释放就return
void memoryLeak1() {
	string *str = new string("动态分配内存!");
	return;
}

// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
	string *str = new string("内存泄露!");

	// ...此处省略一万行代码

	// 发生某些异常,需要结束函数
	if (1) {
		return -1;
	}
	/
	// 另外,使用try、catch结束函数,也会造成内存泄漏!
	/

	delete str;	// 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
	return 1;
}


int main(void) {

	memoryLeak1();

	memoryLeak2();

	return 0;
} 

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!

(a)C++98 提供了 auto_ptr 模板的解决方案

(b)C++11 增加unique_ptr、shared_ptr 和weak_ptr

2.auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

然而,由于它的实现方式存在一些问题,auto_ptr 在 C++11 标准中被弃用,并最终在 C++17 中被移除,替代它的是更安全和强大的 unique_ptr、shared_ptr 和weak_ptr

auto_ptr 的主要缺陷是它的所有权转移语义,这意味着当一个 auto_ptr 对象被赋值给另一个 auto_ptr 时,原对象将失去对资源的所有权,而新对象接管资源。这种非直观的行为会导致程序中意外的内存错误,尤其在多线程环境下更加危险。

3.unique_ptr、shared_ptr 和weak_ptr

  • unique_ptr:独占式指针,同一时刻只能有一个指针指向同一个对象
  • shared_ptr:共享式指针,同一时刻可以有多个指针指向同一个对象
  • weak_ptr:用来解决shared_ptr相互引用导致的死锁问题

(1)unique_ptr(独占指针)

①.语法结构:

包含头文件:首先,需要包含 <memory> 头文件。

#include <memory>

创建 unique_ptr: 使用 std::make_unique 创建 unique_ptr

std::unique_ptr<int> ptr = std::make_unique<int>(42);

同时也可以使用下述代码创建:

std::unique_ptr<int> ptr(new int(42));

上述代码将创建一个 unique_ptr,并将其初始化为指向值为 42 的整数。

访问和修改对象: 可以使用 * 操作符来解引用和访问所指向的对象。

std::cout << *ptr << std::endl; // 输出 42
*ptr = 100; // 修改值

无法进行复制构造和赋值操作,unique_ptr 没有copy构造函数,不支持普通的拷贝和赋值操作。

// 示例:
int main() 
{
    // 创建一个unique_ptr实例
    unique_ptr<int> pInt(new int(42));
    unique_ptr<int> pInt2(pInt);    // 报错
    unique_ptr<int> pInt3 = pInt;   // 报错
}

unique_ptr 虽然没有支持普通的拷贝和赋值操作,但却提供了一种移动机制来将指针的所有权从一个 unique_ptr 转移给另一个 unique_ptr 。如果需要转移所有权,可以使用std::move()函数。

移动 unique_ptr: 可以使用 std::move 将所有权转移给另一个 unique_ptr

std::unique_ptr<int> ptr2 = std::move(ptr1);

此时,ptr 1将变为 nullptrptr2 拥有原始资源。

可以返回unique_ptr

  • unique_ptr不支持拷贝操作,但却有一个例外:可以从函数中返回一个unique_ptr。
// 示例:
unique_ptr<int> clone(int p)
{
    unique_ptr<int> pInt(new int(p));
    return pInt;    // 返回unique_ptr
}

int main() {
    int p = 5;
    unique_ptr<int> ret = clone(p);
    cout << *ret << endl;
}

以下是一个完整示例:     

#include <iostream>

#include <memory>

//using namespace std;

void demo_unique_ptr(){
    //创建 unique_ptr
//    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::unique_ptr<int> ptr1(new int(42));
    std::cout << "独占式智能指针变量ptr1指向的地址空间存储的值为: " << *ptr1 <<std::endl;

    std::cout << "独占式智能指针变量ptr1指向的地址: " << ptr1.get() << std::endl;
    //unique_ptr 不是原始指针类型,无法直接进行流输出。要正确输出 unique_ptr 指向,可以使用 ptr2.get()

    //移动所有权
    std::unique_ptr<int> ptr2 = std::move(ptr1);

    if (!ptr1){
        std::cout << "独占式智能指针变量ptr1是空在move之后" << std::endl;

    }

    std::cout << "独占式智能指针变量ptr2指向的地址空间存储的值为: " << *ptr2 << std::endl;
    std::cout << "独占式智能指针变量ptr2指向的地址: " << ptr2.get() << std::endl;

}

int main(){

    demo_unique_ptr();

    return 0;
}

运行结果:

独占式智能指针变量ptr1指向的地址空间存储的值为: 42
独占式智能指针变量ptr1指向的地址: 0x24db92c63f0
独占式智能指针变量ptr1是空在move之后
独占式智能指针变量ptr2指向的地址空间存储的值为: 42
独占式智能指针变量ptr2指向的地址: 0x24db92c63f0
②unique_ptr使用场景

(a)为动态申请的资源提供异常安全保证:
我们先来看看下面这一段代码:

void Func()
{
    int *p = new int(5);

    // ...(可能会抛出异常)

    delete p;
}

这是我们传统的写法:当我们动态申请内存后,有可能我们接下来的代码由于抛出异常或者提前退出(if语句)而没有执行delete操作。

解决的方法是使用 unique_ptr 来管理动态内存,只要 unique_ptr 指针创建成功,其析构函数都会被调用,确保动态资源被释放。

void Func()
{
    unique_ptr<int> p(new int(5));
}

(b)返回函数内动态申请资源的所有权:

unique_ptr<int> Func(int p)
{
    unique_ptr<int> pInt(new int(p));
    return pInt;    // 返回unique_ptr
}

int main() {
    int p = 5;
    unique_ptr<int> ret = Func(p);
    cout << *ret << endl;
    // 函数结束后,自动释放资源
}

(c)在容器中保存指针

int main() 
{
    vector<unique_ptr<int>> vec;
    unique_ptr<int> p(new int(5));
    vec.push_back(std::move(p));    // 使用移动语义
}

(2)shared_ptr(共享指针)

std::shared_ptr 是 C++11 引入的一种智能指针,用于实现共享所有权的内存管理。多个 shared_ptr 可以指向同一个对象,当最后一个指向该对象的 shared_ptr 被销毁时,内存会被自动释放。它适用于多个实体共享相同资源,并且需要自动管理该资源的场景。

①语法结构

初始化:

使用 std::make_shared 创建对象,避免了单独使用 newshared_ptr 组合的可能性提高效率

std::shared_ptr<int> ptr = std::make_shared<int>(10);  // 创建一个 shared_ptr 指向 10

直接使用 new 创建对象(不推荐,效率略低,因为可能涉及两次内存分配)。

std::shared_ptr<int> ptr(new int(10));  // 直接使用 new 初始化

代码示例:

#include <iostream>

#include <memory>

int main(){
    //第一种创建方法(推荐)
    // 创建和初始化 shared_ptr
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10); // 使用 make_shared
    std::shared_ptr<int> ptr2 = ptr1; // 共享 ptr1 的对象
    //第二种创建方法
    std::shared_ptr<int> ptr3(new int(10));
    std::shared_ptr<int> ptr4 = ptr3;
    
    return 0;
}

引用计数

可以使用 use_count() 函数查看当前指向该对象的 shared_ptr 个数:

std::cout << ptr.use_count();  // 输出当前有多少个 shared_ptr 指向同一个对象

移动和拷贝

shared_ptr 支持拷贝和移动操作,拷贝时引用计数增加,移动时不会增加引用计数。

std::shared_ptr<int> ptr2 = ptr1;  // 引用计数增加
std::shared_ptr<int> ptr3 = std::move(ptr2);  // 移动语义,ptr2 变为 nullptr

检查对象是否有效

可以通过 operator boolget() 来检查指针是否为空。

if (ptr) {  // 或者 if (ptr.get() != nullptr)
    std::cout << "Pointer is valid." << std::endl;
} else {
    std::cout << "Pointer is null." << std::endl;
}

重置 shared_ptr

ptr.reset();  // 将 shared_ptr 置空并释放资源
ptr.reset(new int(20));  // 指向新的对象

自定义删除器

shared_ptr 允许使用自定义的删除器来控制资源的释放方式。

void customDeleter(int* p) {
    std::cout << "Custom deleter is called!" << std::endl;
    delete p;
}

std::shared_ptr<int> ptr(new int(10), customDeleter);  // 使用自定义删除器

②shared_ptr 的使用场景

资源共享:当多个对象需要共享同一块资源时,例如缓存机制、图节点、树节点等数据结构。

复杂对象的生命周期管理:在类之间共享复杂对象,并希望对象在不再被使用时自动销毁。

避免手动内存管理:当对象需要动态分配内存并希望它自动管理内存生命周期时,避免手动 delete 带来的内存泄漏问题。

多线程环境下的安全使用:由于引用计数是线程安全的,可以在多线程环境中共享资源。

shared_ptr 代码示例

以下是一个使用 shared_ptr 管理多个指针共享同一个对象的完整代码示例:

#include <iostream>
#include <memory>  // 使用 shared_ptr 需要包含此头文件

int main() {
    // 使用 make_shared 创建一个 shared_ptr,指向整数 10
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    std::cout << "Initial value: " << *ptr1 << std::endl;  // 输出 10

    // 创建另一个 shared_ptr,ptr2 共享 ptr1 的对象
    std::shared_ptr<int> ptr2 = ptr1;
    std::cout << "Reference count after ptr2: " << ptr1.use_count() << std::endl;  // 输出 2

    // 修改 ptr2 指向的值
    *ptr2 = 20;
    std::cout << "Modified value through ptr2: " << *ptr1 << std::endl;  // 输出 20

    // 释放 ptr2 对象
    ptr2.reset();
    std::cout << "Reference count after ptr2 reset: " << ptr1.use_count() << std::endl;  // 输出 1

    // ptr1 仍然有效
    std::cout << "Final value: " << *ptr1 << std::endl;  // 输出 20

    return 0;  // ptr1 被销毁时,内存自动释放
}

代码解析:

使用 std::make_shared 创建了一个 shared_ptr,指向整数 10。

通过拷贝,ptr2 共享了 ptr1 的对象,引用计数变为 2。

通过 ptr2 修改了共享对象的值,ptr1 也可以看到变化。

调用 ptr2.reset() 释放了 ptr2 的所有权,引用计数减为 1。

程序退出时,ptr1 被销毁,动态内存自动释放。

(3)weak_ptr

weak_ptr 是 C++11 引入的智能指针,用于用于解决shared_ptr相互引用时产生死锁问题的智能指针。它是一种非拥有权的智能指针,不能直接操作资源,而是依赖于 std::shared_ptrweak_ptr 不会影响对象的引用计数,它只是提供了一种可以访问资源但不影响资源生命周期的机制。

①语法结构

定义和初始化

weak_ptr 必须依赖于 shared_ptr,不能直接指向动态分配的对象。

#include <memory>

std::shared_ptr<int> sp = std::make_shared<int>(10);  // 创建 shared_ptr
std::weak_ptr<int> wp = sp;  // 创建 weak_ptr,指向同一个对象

检查 weak_ptr 是否有效

weak_ptr 可以通过 expired() 方法来检查其所指向的对象是否已经被销毁:

if (wp.expired()) {
    std::cout << "Object is already destroyed." << std::endl;
} else {
    std::cout << "Object is still valid." << std::endl;
}

访问资源

通过 lock() 方法将 weak_ptr 转换为一个 shared_ptr,如果资源还存在,则返回有效的 shared_ptr,否则返回空指针。

if (auto sp = wp.lock()) {
    std::cout << "Object value: " << *sp << std::endl;
} else {
    std::cout << "Object has been destroyed." << std::endl;
}

重置 weak_ptr

可以通过 reset() 方法将 weak_ptr 置为空,不再引用任何对象。

wp.reset();  // 将 weak_ptr 重置为空

std::weak_ptr 的使用场景

解决循环引用问题:在复杂数据结构中,可能会存在 shared_ptr 之间相互引用的情况,例如双向链表或图结构。如果两个对象互相持有对方的 shared_ptr,即使没有任何外部对象再引用它们,它们也无法被销毁。此时可以使用 weak_ptr 来打破这种循环引用。

缓存机制:在某些情况下,可能需要缓存一些对象,但又不希望这些对象永久存在。例如,缓存中可以存放一些 shared_ptr,同时通过 weak_ptr 来跟踪对象是否还存在,如果对象被销毁,则无需从缓存中删除它。

观察者模式:在观察者模式中,观察者通常不应该持有被观察对象的强引用(shared_ptr),而应该通过 weak_ptr 观察对象是否仍然有效,以避免观察者和被观察者之间的循环引用。

std::weak_ptr 代码实例

以下代码展示了如何使用 std::weak_ptr 解决 shared_ptr 的循环引用问题:

#include <iostream>
#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;  // 指向下一个节点的 shared_ptr
    std::weak_ptr<Node> prev;    // 指向前一个节点的 weak_ptr,防止循环引用
    int value;

    Node(int val) : value(val) {
        std::cout << "Node created with value: " << value << std::endl;
    }
    ~Node() {
        std::cout << "Node destroyed with value: " << value << std::endl;
    }
};

int main() {
    // 创建两个节点
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    // 建立双向链接
    node1->next = node2;  // node1 指向 node2
    node2->prev = node1;  // node2 的 weak_ptr 指向 node1(不会增加引用计数)

    // 访问 node1 的下一个节点的值
    if (auto sp = node1->next->prev.lock()) {  // 使用 lock() 安全访问 node1
        std::cout << "Accessed node1 value from node2: " << sp->value << std::endl;
    } else {
        std::cout << "Node1 has been destroyed." << std::endl;
    }

    // 销毁节点并自动释放内存
    node1.reset();  // 释放 node1
    node2.reset();  // 释放 node2

    return 0;
}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

代码解析:

Node 类Node 类包含两个指针,一个是 nextstd::shared_ptr<Node>),指向下一个节点,另一个是 prevstd::weak_ptr<Node>),指向上一个节点。

双向链接node1node2 通过 shared_ptrweak_ptr 实现了双向链接,防止了循环引用的问题。

安全访问:通过 weak_ptr.lock() 安全访问 node1,只有在 node1 仍然有效时才会进行操作。

内存释放:由于 weak_ptr 不增加引用计数,当 shared_ptr 被销毁时,内存会被正确释放,防止了循环引用导致的内存泄漏。

指针是C语言中非常重要的概念之一。指针可以指向内存中的某个特定位置,通过指针可以直接访问这个位置的数据。在C语言中,指针变量的声明需要使用特殊的语法,即在变量名前加上"*"符号。 使用指针可以实现一些复杂的操作,比如动态内存分配和结构体等。通过指针,可以在函数之间传递数据,并且可以节省内存空间。但是指针的使用也需要小心,因为指针操作涉及到直接访问内存,所以在操作指针时需要确保内存的正确性,否则可能会导致程序崩溃或数据损坏。 对于初学者来说,学习指针可能有一定的难度。为了帮助初学者更好地理解和掌握指针的概念和用法,有一些PDF资料可以提供参考。这些PDF可以详细介绍指针的基本知识,包括指针的声明和初始化、指针的运算、指针与数组的关系等。此外,这些PDF还会提供一些指针的常见应用案例,帮助初学者进行实践。 在学习指针时,建议先理解指针的基本概念,然后通过实践来巩固和深入理解。可以编写一些简单的程序,使用指针来实现一些基本功能,比如交换两个变量的值或者动态分配数组等。通过实践,可以更好地理解指针的运作机制和应用场景。 总而言之,指针是C语言中一项重要而复杂的特性。初学者可以通过阅读相关的PDF资料,结合实践来学习和掌握指针的概念和用法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值