什么是二级指针
我们常说的指针,通常说的就是一级指针,这一块就不在本文的讨论范围内了,默认大家都清楚其原理了。
二级指针,即指向指针的指针,主要用于间接操作一级指针(直接指向数据的指针),其典型应用场景包括但不限于以下几点:
- 函数参数传递:当需要在函数内部修改传入指针的值(即改变指针所指的内存位置)时,可以传递一级指针的地址(即二级指针)。这样函数内就可以通过二级指针修改一级指针的指向,而不仅仅是其指向的内存内容。
- 动态内存分配与释放:在某些内存管理函数中,为了能够更改调用者提供的指针以返回新分配的内存地址,会接收一个一级指针的二级指针作为参数。例如,重新分配内存时,
realloc()
函数可能会接收一个二级指针,并在必要时更新它以指向新的内存块。 - 多级数据结构:在处理如链表、树等复杂数据结构时,节点中包含指向下一个节点或子节点的指针,二级指针可用于遍历或修改这些结构中的指针关系。
- 数组的指针数组:有时需要创建一个数组,其中每个元素都是一个指针。在这种情况下,二级指针可以用来指向这样的指针数组,便于整体操作或传递。
这里我主要说一下第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)是指一个指针变量仍然保存着已经释放或失效的(通过调用 delete
或 free
)内存区域的地址,其状态就像是一个悬挂在空中,没有线的风筝。
这种情况下,指针不再是有效的,因为其所指向的内存可能已被系统回收,或者其内容已被其他数据覆盖。当程序继续尝试通过悬挂指针访问或修改内存时,会导致未定义行为、程序崩溃、数据损坏或安全漏洞等问题。
#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
之后将指针的指向改为nullptr
,detele
只是把指针所指的内存给释放了,但它并没有把指针本身给干掉,所以实际上还是可以访问的。
#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)