狗都能看懂的C++二级指针/悬挂指针的原理和应用

什么是二级指针

我们常说的指针,通常说的就是一级指针,这一块就不在本文的讨论范围内了,默认大家都清楚其原理了。

二级指针,即指向指针的指针,主要用于间接操作一级指针(直接指向数据的指针),其典型应用场景包括但不限于以下几点:

  1. 函数参数传递:当需要在函数内部修改传入指针的值(即改变指针所指的内存位置)时,可以传递一级指针的地址(即二级指针)。这样函数内就可以通过二级指针修改一级指针的指向,而不仅仅是其指向的内存内容。
  2. 动态内存分配与释放:在某些内存管理函数中,为了能够更改调用者提供的指针以返回新分配的内存地址,会接收一个一级指针的二级指针作为参数。例如,重新分配内存时,realloc()函数可能会接收一个二级指针,并在必要时更新它以指向新的内存块。
  3. 多级数据结构:在处理如链表、树等复杂数据结构时,节点中包含指向下一个节点或子节点的指针,二级指针可用于遍历或修改这些结构中的指针关系。
  4. 数组的指针数组:有时需要创建一个数组,其中每个元素都是一个指针。在这种情况下,二级指针可以用来指向这样的指针数组,便于整体操作或传递。

这里我主要说一下第1点和第2点,是比较常用的。也是一级指针无法替代二级指针的主要原因。

由于C++的函数是进行值传递的,一级指针作为函数的值传递时,不能直接改变该指针本身的指向。在C++中,当将一级指针作为函数参数传递时,实际上是将指针的值(即它所存储的内存地址)复制一份传递给函数。在函数内部,我们操作的是这个指针副本,而非原始指针本身。因此,即使在函数内部改变了这个副本指针的指向,使得它指向了一个新的内存位置,这种改变仅作用于函数内部,不会影响到函数外部的原始指针。原始指针的指向在函数调用结束后仍保持不变。

例如:

void change_pointer(int* ptr) 
{
	ptr = new int(10); // 改变函数内部ptr的指向
    std::cout << "*myPtr func: " << *ptr << std::endl;
}

int main() 
{
	int* myPtr = new int(5);
	std::cout << "*myPtr before: " << *myPtr << std::endl;

	change_pointer(myPtr); // 函数内部ptr指向了新分配的内存

	std::cout << "*myPtr after: " << *myPtr << std::endl; // 输出仍然是原来的值,因为myPtr的指向未变

	delete myPtr; // 清理原分配的内存
	return 0;
}

运行结果为:

*myPtr before: 5
*myPtr func: 10
*myPtr after: 5

在这个例子中,虽然change_pointer函数内部将ptr指向了一个新分配的整数(值为10),但这并不会影响到main函数中myPtr的指向。因此,myPtr在调用前后依然指向同一块内存,其值未发生变化。

要改变一级指针作为函数参数时其在函数外部的指向,通常需要使用二级指针。通过传递一级指针的地址(即二级指针)作为参数,函数内就可以通过二级指针间接修改原始一级指针的值(即其指向)。如下所示:

void change_pointer(int** ptr) 
{
	*ptr = new int(10); // 通过解引用二级指针,改变原始一级指针的指向
	std::cout << "**myPtr func: " << **ptr << std::endl;
}

int main() 
{
	int* myPtr = new int(5);
	std::cout << "*myPtr before: " << *myPtr << std::endl;

	change_pointer(&myPtr); // 传递myPtr的地址

	std::cout << "*myPtr after: " << *myPtr << std::endl; // 输出已变为10,因为myPtr的指向已改变

	delete myPtr; // 清理新分配的内存
	return 0;
}

运行结果为:

*myPtr before: 5
**myPtr func: 10
*myPtr after: 10

在这个版本中,change_pointer函数接收一个int**类型的参数,即一个指向int*的指针。在函数内部,通过解引用二级指针*ptr来改变它所指向的一级指针myPtr的值,从而成功地在函数外部改变了myPtr的指向。

指针悬挂

指针悬挂(Dangling Pointer)是指一个指针变量仍然保存着已经释放或失效的(通过调用 deletefree)内存区域的地址,其状态就像是一个悬挂在空中,没有线的风筝。

这种情况下,指针不再是有效的,因为其所指向的内存可能已被系统回收,或者其内容已被其他数据覆盖。当程序继续尝试通过悬挂指针访问或修改内存时,会导致未定义行为、程序崩溃、数据损坏或安全漏洞等问题。

#include <iostream>

int main() 
{
    int* ptr = new int(10);

    std::cout << "*ptr value: " << *ptr << std::endl; 
    delete ptr;
    
    std::cout << "*ptr value: " << *ptr << std::endl; // 还是能访问,但原来指向的内存地址已经被释放了,成为悬挂指针

    return 0;
}

运行结果为:

*ptr value: 10
*ptr value: -1614026882

正确的做法是要在delete之后将指针的指向改为nullptrdetele只是把指针所指的内存给释放了,但它并没有把指针本身给干掉,所以实际上还是可以访问的。

#include <iostream>

int main() 
{
    int* ptr = new int(10);

    std::cout << "*ptr value: " << *ptr << std::endl; 
    delete ptr;
    ptr = nullptr;

    std::cout << "*ptr value: " << *ptr << std::endl; // 已经不能访问了,强行访问会报错

    return 0;
}

运行结果为:

*ptr value: 10
Segmentation fault (core dumped)

所以实际开发中,顶层开发者在访问指针时需要判空,避免程序崩溃。

int main() 
{
    int* ptr = new int(10);

    std::cout << "*ptr value: " << *ptr << std::endl; 
    delete ptr;
    ptr = NULL;

    if (ptr != NULL)
    {
        std::cout << "*ptr value: " << *ptr << std::endl; // 已经不能访问了,强行访问会报错
    }

    return 0;
}

野指针

野指针是一个未初始化的指针,也就是说,它的值是未知的,可能指向任意内存地址。如果我们试图通过这样的指针访问或操作内存,同样可能导致未定义行为:

int* ptr;  // ptr is a wild pointer.
*ptr = 10;  // Undefined behavior.

总的来说,悬挂指针是在其指向的内存已经被释放或失效后仍被使用,而野指针则是一开始就未经初始化就被使用的指针。两者都可能导致程序崩溃或数据损坏,因此在编程时需要特别小心。

二级指针避免指针悬挂

这里说一下我在实际开发中遇到的一个问题,原来我们在设计底层框架时,给到顶层开发者使用的是一级指针,这就会导致顶层使用者调用release函数时,没有正确释放指针。

void release(int* ptr) 
{
    delete ptr;
    ptr = nullptr; 
}


int main(int argc, char *argv[])
{
    int *p = new int(10);

    std::cout << *p << std::endl;
    release(p);
    std::cout << *p << std::endl;	// 访问到未知内存位置,导致结果错乱
}

运行结果为:

10
1737309655

release函数中,我们确实正确地释放了ptr指向的内存,并将其设为nullptr。但是,这里出现了一个关键点,即函数内部对形参的修改不会影响到函数外部对应的实参。正如一直强调的,当将指针作为值参数传递给函数时,函数内部接收到的是该指针的一个副本。在release函数内部对ptr赋值为nullptr,只改变了副本的值,不会影响到main函数中的原始指针p

因此,尽管release函数内部将ptr设为了nullptr,但在main函数中,p的值并未改变,它仍然保持着释放前的内存地址。这就是为什么在释放后您还能打印出一个看似随机的数值(实际上是已释放内存的新内容,访问已释放内存的行为未定义)。

所以这里我们就可以用二级指针对函数进行改写。当然还可以用引用进行改写。

// 二级指针的写法
void release(int** ptr) 			
{
    delete *ptr;
    *ptr = nullptr; 
}


int main(int argc, char *argv[])
{
    int *p = new int(10);

    std::cout << *p << std::endl;
    release(&p);
    std::cout << *p << std::endl;	// 已经不可访问,代表正常释放了
}

// 引用的写法
void release(int*& ptr) 
{
    delete ptr;
    ptr = nullptr; 
}

int main(int argc, char *argv[])
{
    int *p = new int(10);

    std::cout << *p << std::endl;
    release(p);
    std::cout << *p << std::endl;
}

运行结果为:

10
Segmentation fault (core dumped)
  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值