C/C++中,指针是一个常用的编程概念,它本身也是一个变量,只不过其存储的值是一个地址。
指针存在的意义,可以使程序直接访问内存、间接操作数据、高效传递数据等。
指针用的好,可以使编程更加高效灵活,因此,理解指针的本质,对于C/C++编程是十分必要的。如果理解的不够透彻,一来在编写代码时可能迷惑不清,二来如果使用不恰当,轻则代码的运行与预期不符,重则程序崩溃。
指针可以有多级,最常用的就是一级指针,简称指针,再者就是在某些场景下会用到二级指针,再复杂的三级及以上的指针,使用的极少。
本篇,就先来探究一下一级指针。
1 指针使用场景举例------两数相加
比如一个计算两个int相加的函数,注意这里只是举例,不需要纠结简单的加法为什么要写一个函数。
实现方式呢,可能有多种。
1.1 方法一:求和结果通过返回值(不需要使用指针)
将两个加数作为函数参数,结果通过函数的返回值返回,很好理解,如下示例代码。
test1.c
c
// gcc test1.c -o test1
#include <stdio.h>
int sum(int a, int b)
{
int res = 0;
res = a + b:
return res;
}
int main()
{
int m = 1;
int n = 2;
int calc_result = sum(m, n);
printf("calc_result:%d\n", calc_result);
return 0;
}
不涉及指针,代码一目了然,所见即所得,这里就不作过多解释了。
下面开始探究指针。
1.2 方法二:求和结果通过函数传参(需要使用指针)
如果计算结果也改为从参数传入,就需要用到指针了。为什么要使用指针,不使用指针会怎样?
1.2.1 如果不使用指针传参
test2.c
c
// gcc test2.c -o test2
#include <stdio.h>
void sum(int a, int b, int result)
{
int res = 0;
res = a + b;
result = res;
printf("[%s] a:%d(%p), b:%d(%p), result:%d(%p), res:%d(%p)\n",
__func__, a, &a, b, &b, result, &result, res, &res);
}
int main()
{
int m = 1;
int n = 2;
int calc_result = 0;
printf("[%s] m:%d(%p), m:%d(%p), calc_result:%d(%p)\n",
__func__, m, &m, n, &n, calc_result, &calc_result);
sum(m, n, calc_result);
printf("[%s] calc_result:%d\n", __func__, calc_result);
return 0;
}
在学习函数参数的时候,有一个知识点:形参与实参 ,函数定义时,函数后面括号里是形式参数 ,只是一个变量名,用来占位、接收数据,而在函数调用时,在函数的调用位置,这个时候是实际参数 ,而函数的调用中参数,是把实参的值复制给形参,形参在函数里面怎么改,都不会影响外面的实参。
因为,上述对求和结果,通过参数的形式,不采用指针的方式,是无法将计算的结果(形参 )传递给外面的实参的。
运行结果,可以看到求和结果无法传递出来:

根据打印的变量地址,画图来分析各变量的地址与数据传递关系:

可以看到,求和结果修改的只是函数内的形参。
1.2.2 使用指针传参
test3.c
c
// gcc test3.c -o test3
#include <stdio.h>
void sum(int a, int b, int *result)
{
int res = 0;
res = a + b;
*result = res;
printf("[%s] a:%d(%p), b:%d(%p), result:%p(%p)[*result:%d], res:%d(%p)\n",
__func__, a, &a, b, &b, result, &result, *result, res, &res);
}
int main()
{
int m = 1;
int n = 2;
int calc_result = 0;
printf("[%s] m:%d(%p), m:%d(%p), calc_result:%d(%p)\n",
__func__, m, &m, n, &n, calc_result, &calc_result);
sum(m, n, &calc_result);
printf("calc_result:%d\n", calc_result);
return 0;
}
运行结果,可以看到求和结果传递出来了:

分析各变量的地址与数据传递关系:

因为函数参数中result是指针,即告诉sum函数外部的求和变量的地址,因为sum函数可以通过直接修改外部变量的值,实现将内部的求和结果通过参数的方式传递值外部(其实是对数据进行一种间接访问 的方式,通过解引用找到变量,再改值)。
1.2.1 再进一步理解指针传参
通过上面的例子,应该对指针以及函数传参有更深的理解了。
那再来讨论一个问题,上面的代码中函数参数只是对求和结果使用指针的形式,那两个相加的数,使用可以使用指针,或是否有必要使用指针呢?
test4.c
c
// gcc test4.c -o test4
#include <stdio.h>
void sum(const int *a, const int *b, int *result)
{
*result = *a + *b;
printf("[%s] a:%p(%p)[*a:%d], b:%p(%p)[*b:%d], result:%p(%p)[*result:%d]\n",
__func__, a, &a, *a, b, &b, *b, result, &result, *result);
}
int main()
{
int m = 1;
int n = 2;
int calc_result = 0;
printf("[%s] m:%d(%p), m:%d(%p), calc_result:%d(%p)\n",
__func__, m, &m, n, &n, calc_result, &calc_result);
sum(&m, &n, &calc_result);
printf("calc_result:%d\n", calc_result);
return 0;
}
运行结果,依然的对的:

分析各变量的地址与数据传递关系:

两个相加的数,改为使用指针,对于内部求和逻辑来说,只是对求和的两个数据,从直接访问 变为了间接访问(通过解引用获取到数据的值)而已,因此结果仍是正确的。
int值的加数改为指针,实际是没有必要的,函数参数都是要拷贝一份副本的,直接传int值,副本只需要4字节,改为传指针后,副本是指针类型,就需要8字节了。
1.3 一个可能出错的用法
继续来看,下面的代码是否有问题:
c
// gcc test5.c -o test5
#include <stdio.h>
void sum(int a, int b, int *result)
{
int res = 0;
res = a + b;
printf("[%s] a:%d(%p), b:%d(%p), result:%p(%p), res:%d(%p)\n",
__func__, a, &a, b, &b, result, &result, res, &res);
*result = res;
printf("[%s] a:%d(%p), b:%d(%p), result:%p(%p)[*result:%d], res:%d(%p)\n",
__func__, a, &a, b, &b, result, &result, *result, res, &res);
}
int main()
{
int m = 1;
int n = 2;
int *calc_result = NULL;
printf("[%s] m:%d(%p), m:%d(%p), calc_result:%p(%p)\n",
__func__, m, &m, n, &n, calc_result, &calc_result);
sum(m, n, calc_result);
printf("calc_result:%d\n", *calc_result);
return 0;
}
虽然语法上没有什么问题,但这里就涉及到C指针容易遇到了一个坑------访问空地址!
运行结果,程序崩掉了:

仍然是画图,分析各变量的地址与数据传递关系:

可以看到,外部指针变量指向的空地址,并将这个空地址传递到了函数内部,函数内部通过解引用,操作空地址时,就会导致程序崩溃,因为不知道这个空地址指向哪里,在程序运行是就会表现为程序崩溃(Segmentation fault)。
2 关于取地址(&)与解引用(*)
上述示例代码,涉及到了取地址 (&)与解引用(*)这两个概念。
字面意思上很好理解:
- 取地址(&),就是取变量的地址,得到指针,也就是变量的存储位置
- 解引用(*),就是根据指针,得到对应的值
可能容易迷惑的是,这两个操作,通过函数指针传参,在函数内部操作时,可能会迷惑,因为这实际涉及到了二级指针。

对于一个指针int *result:
- 指针本身 :是一个地址(指针变量是不带星号的,指针的类型是
int *,即指向int类型数据的指针) - 解引用:该地址指向的值
- 取地址 :该地址在内存的位置,即指针变量本身的地址,或地址的地址,也就是二级指针
简单来说,对于指针作为函数参数,在函数内部操作时,通常情况只需要用到解引用(*),以及不带任何符号的指针本身。
而对指针在进行取地址(&),就是二级指针了,也就是指针本身这个8个字节(64位系统)数据的存储位置,通常情况是不需要关心这个地址的。
3 总结
本篇介绍了C指针的一些知识,通过实际的例子,来理解C指针的用法。