目录
前言
本篇博客文章将介绍C/C++中指针的初阶知识。首先,我们知道内置指针类型有 int*, char*, double*, short*等,那为什么要存在不同的指针类型,不同类型的指针可否混用。其次,当我们假定数组arr[]并对其进行访问时,有多少种可行的方式可以对其进行按需访问。最后二阶指针是什么,怎么用,为什么要有二级指针?以上都是本篇指针初阶会突出讲述的部分,希望通过本篇文章,初学者可以对指针有并非浮于表面的恰当理解。
注:以下内容使用 x86 32位配置器系统运行
一、指针的概念与类型区分的必要性
1. 指针概念
指针相对于一个内存单元来说,指的是单元的地址,该单元的内容里面存放的是数据。在 C 语言中,允许用指针变量来存放指针,因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。
对于上面这段晦涩难懂的话,翻译过来就是:
1. 在C/C++中指针就是地址
2. 我们常说的指针其实是指针变量
新概念又来了,什么是指针变量,什么是地址?
指针同理:
那么这样即不难看出p1实际上也是一种变量,只不过其类型为指针类型,所以称之为指针变量。
2. 指针类型
int a = 10;
printf("address of a = %p\n", &a); // 打印a的地址
首先我们给出一个整形变量a并初始化为10,接着变量a在内存中必定有内存占用,我们使用%p读取变量a的地址:
其次,我们声明两个整形指针p1,p2指向变量a的地址,注意指针指向a有两种声明方式,p1采用初始化给指针变量赋值,p2采用声明后再定义p2的具体指向。同时我们对其指向的地址进行访问:
发现三者地址完全一致,说明p1和p2确实找到了变量a所在的地址。那我们接下来声明一个char类型指针指向整形变量a试试看地址还如我们所料吗?
char* p_c = (char*)&a;
printf("address of p_c = %p\n", p_c);
运行结果:
这里我们可以看到四者地址值全部一致,那为什么要区分指针类型,用一个全能的指针类型不就好了吗?接着往下看:
打开调试窗口:
我们可以看到整形变量a在内存中占4个字节,由于内存窗口中地址下的元素值需要倒着看,所以该地址下元素值16进制表示为64,10进制下即为100。
随即我们给a设定一个大一些的值,以便内存窗口显示的变量值验证是否需要倒着看:
可以看出我们通过利用char*类型的指针变量p_c改变变量a的值,但是只将a的第一个字节值设定为0,回过头一想,这是不是和指针类型所对应的数据类型字节数有关呢?
先看其中几个对应的字节数:
接着设想把上面代码中的char*改为short*会不会只改变变量a的两个字节的值呢?
可以看到结果很理想,确实只将后两个字节置0。
所以我们认识到指针类型的设定是必不可少的,否则会很出现意料之外的结果,同时指针强转的安全性也需要我们认真把握。
二、指针与数组读写
1. 下标访问
首先,我们定义一个普通数组arr同时给出常见的利用取下标方式对数组元素进行读写案例:
int arr[N_NUM] = { 1,2,3,4,5,6,7,8,9,10 };
// 利用下标访问
for(int i = 0;i< N_NUM;++i)
{
printf("%d\t", arr[i]);
}
打开调试窗口:
运行结果:
通过上面的调试窗口,我们加深了数组元素下标大小的地址是由低到高增长的,这里的整形数组可以看到每个元素之间的地址差为4,同时也印证了整形元素大小为4字节。
2. 指针访问
接着我们使用指针遍历:(同样利用上面设定好的数组)
如我们所知,数组名代表指向数组首元素的指针,所以我们可以直接在该指针上进行移位操作访问数组各元素:
for(int i = 0;i<10;++i)
{
printf("%d\t", *(arr+i));
}
通过利用指针偏移量i访问数组各元素的值并打印。
接着给出新定义指针指向数组首地址时,遍历访问还可以这样写:
// 利用指针访问
// 正向 对应地址由低到高
for(int* p = arr;p<&arr[N_NUM];)
{
printf("%d\t", *p++);
}
printf("\n");
// 反向 对应地址由高到低
for(int* p = &arr[N_NUM];p>arr;)
{
printf("%d\t", *--p);
}
printf("\n");
以上四种方法运行结果:
可以看到均成功实现了对数组的遍历,达到了预期目标。
但是细心的有没有发现似乎在后两种遍历方法中可能存在着越界访问,例如正向的for循环中每次需要和数组尾元素arr[N_NUM]的地址作比较,然而数组声明大小即为N_NUM, 下标可取闭区间为[0, N_NUM-1]。同样地,反向遍历中也存在使得指针p指向arr[-1]的情况。
事实上,C语言标准在这里是允许存在访问arr[N_NUM]的地址的,但是对访问arr[N_NUM]却没有明确标准,不能保证访问地址一定安全。所以我们大多数情况下避免访问数组头指针以前的地址,可以相对越界向后访问查询地址。
三、二级指针的概念和存在意义
1. 动态内存分配
当我们需要利用函数返回堆区创建的数组时,我们需要在函数外对函数内部分配的动态内存指针进行接收,无论使用malloc(C语言)还是new(C++)实现堆区申请内存,返回的类型都是指定类型的指针,外界需要接收这份新创建的指针(也称地址)时,就需要通过函数的传入参数或返回值来将这份地址传递到函数外部,在此思路下我们进行一下尝试:
1)利用返回值实现
int* allocateMemory_returnValue(int size)
{
int* arr = (int*)malloc(sizeof(int) * size);
return arr;
}
void test4()
{
// 利用二级指针返回堆上创建的指针
int* arr = nullptr;
int size = 10;
arr = allocateMemory_returnValue(size);
for (int i = 0; i < size; ++i) // 对堆区动态数组赋值
{
arr[i] = i*i;
}
for(int i = 0;i<size;++i) // 对堆区动态数组访问读取
{
printf("%d\t", arr[i]);
}
printf("\n");
delete[] arr;
}
调试窗口:
运行窗口:
由此我们可以看到利用返回值将创建的堆区内存传出函数外并被成功访问是可行的!
当然,希望更加细致理解和选择指针返回值类型可以参考:函数返回值和传入参数选择--指针与引用http://t.csdn.cn/8s2HK
2)利用传入参数(指针类型)实现
首先我们想到传入指针的话,是否可以直接将指针指向函数内创建的堆区空间,这样出于指针的内外共享特性应该就可以实现动态数组返回,实践如下:
void allocateMemory_inputParameter_1(int* arr, int size)
{
int* p = (int*)malloc(sizeof(int) * size);
arr = p;
}
上面我们使用*p指针接收了malloc返回的动态内存地址,再将指针变量p赋值给传入参数*arr指针,如果不好理解,我再举个例子:
像例子中的指针*p_a指向a的地址,那么指针变量p_a里存放的就是a的地址,同时*p_b也指向p_a,所以两者指针取地址是相同的,或者说指针变量里存放的值就都是变量a的地址。
现在言归正传,再来试试调用上面 allocateMemory_inputParameter_1() 函数会不会成功实现呢?
运行结果:
很遗憾,程序中断,原因是动态分配内存函数外部的*arr指针并未脱离空指针(NULL/nullptr),说明 allocateMemory_inputParameter_1() 在这里并未将堆空间传回函数外部并赋值给arr指针变量。很是疑惑,为什么已经在函数内有将内存赋值给传入参数的操作,但外部却没有成功接收到呢?
这里就有一个很重要的概念,很多人听过但是并不理解真正的含义,那便是:
指针传参的本质上也是值传递
当我们使用指针作为函数参数时,外部指针传入参数内实际上会新建一个临时变量,也就是看似外部指针直接传入函数被使用,实际上函数内部对传入的指针操作究其本质是对实参的拷贝进行操作,所以说形参不同于实参。指针作为传入参数使得函数内的操作可以保留影响到函数外(例如对指针直接赋值),是因为实参指针和形参指针指向的地址一致!
反观我们函数中的操作 int* p = (int*)malloc(sizeof(int) * size); arr = p; 实际上是对形参即临时拷贝对象的重新赋值,改变其指向,所以并不会引起实参的改变。
那这就没办法解决了吗,倒也不是,接着看:
方法一:
利用C++中引用特性,使得实参和形参是同一个对象,此时我们对指针*arr赋值时,就可以很好地通过指针变量arr传回堆区创建的内存空间,方法如下:
void allocateMemory_inputParameter_1(int* &arr, int size)
{
int* p = (int*)malloc(sizeof(int) * size);
arr = p;
}
调试窗口:
运行窗口:
注意 allocateMemory_inputParameter_1() 函数调用处,其函数参数列表中arr的类型推导为引用,所以会把实参以引用形式传入函数,使得内外对指针操作保持一致。
但是,如果我们是.c文件仅仅用于写C语言代码时,这种方式就不适配了,没有类似于C++中的引用类型 int*& 的概念,在C语言中,我们可以通过传递指向指针的指针(二级指针)来实现类似于引用的效果。
方法二:
二级指针即为指向指针的指针,实际上并没有多级指针,几级指针在编程角度描述的只是在当前变量前面需要写多少个*才可以真正取到目标变量的值而已,比如给个基础例子:
int n = 88;
int* p_n = &n;
int** pp_n = &p_n;
printf("address:\tn:%p\tp_n:%p\tpp_n:%p\n", &n, p_n, *pp_n);
printf("**pp_n = %d\t*p_n = %d\tn = %d", **pp_n, *p_n, n);
运行结果:
我们知道指针中的 * 和 & 是相反的操作,显然上面测试代码中:指针变量p_n里存放的是n的地址,指针变量*pp_n里存放的是指针变量p_n的地址,*(*pp_n)相当于对*pp_n进行解引用操作,可以取到*pp_n里存放的值。同时通过运行结果验证p_n和*pp_n的地址是相同的,都等同于变量n的地址。
所以相对变量n来说,<注:常说的指针其实是指针变量,为了严谨后面表述括号里给出正确的称呼> p_n是一级指针(变量),pp_n是二级指针(变量);对于p_n指针变量来说,pp_n是一级指针(变量)。
现在再来利用二级指针解决动态内存分配的问题:
void allocateMemory_inputParameter_2(int** arr, int size)
{
*arr = (int*)malloc(sizeof(int) * size);
}
我们这里使用传入二级指针的方法,利用*arr接收malloc返回的一级指针,相当于二级指针**arr指向了malloc传回的一级指针类型,传回的一级指针指向堆区申请的堆内存地址。通过传递指针的指针,我们可以在函数内部修改指针的值,从而影响到函数外部的arr指针。
当然,既然指针参数列表发生变化,调用处也要发生相应的变化:
void test4()
{
// 利用二级指针返回堆上创建的指针
int* arr = nullptr;
int size = 10;
allocateMemory_inputParameter_2(&arr, size);
for (int i = 0; i < size; ++i) // 对堆区动态数组赋值
{
arr[i] = i*i;
}
for(int i = 0;i<size;++i) // 对堆区动态数组访问读取
{
printf("%d\t", arr[i]);
}
printf("\n");
free(arr);
}
调试窗口和运行结果:
可以看到,利用二级指针同样可以在不依靠返回值返回和C++中引用特性的前提下进行动态内存分配,实现利用自定义函数在堆区申请空间,构建动态数组。
2. 构建类二维数组
首先我们需要知道什么是指针数组,下面举例:
我们发现整形数组arr_i内部元素都是整形,那指针数组内部元素不就都是指针吗?所以对指针数组初始化时需要在 {} 初始化列表中传入指针(变量)元素。
给出构建的类二维数组代码:
void test5()
{
int arr1[] = { 1,2,3,4,5,6 };
int arr2[] = { 2,3,4,5,6,7 };
int arr3[] = { 3,4,5,6,7,8 };
int* array[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 6; j++)
{
printf("%d\t", array[i][j]);
}
printf("\n");
}
}
运行结果:
二维数组类似一维数组有个重要特性:元素间的地址连续。又会有疑问,这里每行元素间的地址都连续吗?不妨调试窗口看看:
我们可以看到arr1,arr2,arr3的地址并不连续,同时由于局部变量存放在栈区的特性,各个整形数组按声明顺序对应地址由高到低存放。并且可以看到这里array指针数组的首位指针即 array[0] = *arr1,同时array二阶指针的地址并不等于组合后数组首元素地址(&array[0]),由以上标色两点得出结论这个看似实现了二维数组,严格来讲只能称其为类二维数组。
总结
本文介绍了C/C++中指针的基础知识。在前言中,我们提到了指针在程序中的重要性和作用。接着,我们讨论了指针的概念以及为什么需要区分指针的类型。通过了解指针的概念和类型,我们可以更好地理解指针的使用。
在第二部分中,我们讨论了指针与数组的读写操作。我们介绍了通过下标访问数组元素和通过指针访问数组元素的两种方式。通过这两种方式,我们可以使用指针来处理数组,实现对数组元素的操作。
接下来,我们介绍了二级指针的概念和存在意义。我们讨论了二级指针在动态内存分配中的应用,包括利用返回值和传入参数实现动态内存分配的两种方法。我们还介绍了如何使用二级指针来构建类二维数组。
通过本文的学习,我们对指针在C/C++中的基础知识有了更深入的理解。指针是一项重要而强大的工具,对于理解和操作内存具有重要作用。熟练掌握指针的概念和使用方法,将有助于我们编写更高效和灵活的程序。