# 指针
# 指针与指针变量
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
解释
总而言之,C中的变量对应的内存空间中一旦存放了另一个变量的一份地址(也叫指针),为了跟其他普通变量区分开来,那它就有了个头衔,指针变量
# 声明指针变量
// 声明指针变量
int *pi;
// 声明后初始化
float *pp; // 首先声明变量 pp 为指针变量
pp = &value1; // 然后将变量 value1 的地址赋值给指针变量 pp
// 声明并初始化
int *pt = &value2; // 声明指针变量 pt 并将变量 value2 的地址赋值给它
2
3
4
5
6
7
8
9
notes:
- 声明一个指针跟声明普通变量的方式一致,不过为了跟普通变量区分开来,特意在变量名前面加了个
*
; - 第二种方式,在初始化时不可以写成
*pp = &value1
; - 第三种方式就相当于第二种方式的简写。
- 注意前面的数据类型,这就表明指针变量的数据类型也不可以乱赋值,如下例子:
// 声明普通变量并初始化
float a = 99.5, b = 10.6;
char c = '@', d = '#';
// 声明指针变量并初始化
int *p1 = &a;
float *p2 = &c;
// 修改指针变量的数据
p1 = &b;
p2 = &d;
2
3
4
5
6
7
8
9
说明:
- 指针变量的值是可以修改的;
- float 型的指针变量只能存 float 型变量的地址,char、int 或者其他的也一样;
- 定义指针变量时必须带
*
,给指针变量赋值时不能带*
。
一张图来直观的描述上例

# 通过指针获取变量
指针变量里的内存空间存储着地址值,通过这个地址就可以找到对应的数据,简单来说,通过指针变量就可以找到对应的数据。
下面看一个例子:
#include <stdio.h>
int main()
{
int value1 = 10;
int *p1 = &value1;
printf("The value of p1 is: %d\n", *p1);
}
2
3
4
5
6
7
8
输出结果
The value of value1 is: 10
The value of p1 is: 10
2
从结果上来看,两者打印的结果一致,说明此时的 value1
与 *p1
,通过这两个变量名都可以找到存储着数据 10
的那一块内存。
补充
虽然两者都可以找到,但是显然直接用 value1
效率更高些,通过下图说明

# 理解
*
与&
有个 int
型变量 a,pa 是指向它的指针,说一说 *&a
和&*pa
的区别:
*&a 等价于 *(&a), 首先 &a 等价于 pa, 外面加了个 *, 等价于 *pa, 也就是通过指针获取变量,所以其值对应 a 的值
;&*pa 等价于 &(*pa), 首先 *pa 值对应 a 的值,可写为 &a, 也就是取 a 的地址,而 a 的地址也存在其指针变量 pa 对应的内存中,所以等价于 pa
- 对于
*
的总结:
- 乘性运算符;
- 定义指针变量时,用于区分普通变量与指针变量;
- 通过指针获取具体值时使用。
# 指针算数运算
指针可以完成 ++, --, +, -
算数运算。
大家知道,指针就是地址,指针的运算,也就是地址的运算,也就是内存的运算,字节的运算。
这就与普通的加减运算不同,指针的加减运算的结果跟数据类型的长度有直接关系,比如下例:
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a;
printf("a的地址:%d\n", &a);
printf("pa的初始值:%d\n", pa);
pa++;
printf("pa加1后的值:%d\n", pa);
}
2
3
4
5
6
7
8
9
10
11
12
变量 a 为 int 型变量,内存占 4 个字节,我们把 1 字节比喻成下图的一个正方形,那么变量 a 所占的内存总长度如下范围。
而指针变量 pa 指向的位置正是 a 的头部,如图所示。

由于使用 int 声明的指针变量,而每个 int 变量都占 4 字节的长度,所以当执行 pa++
时,其实就是在 pa 的初始位置往后移动了 4 字节的长度,此时的指向图如下所示:

打印输出结果如下:

正好加了四字节的长度。
补充
也不能说长度吧,可以理解成:
- 这么长的数字都是一个个的字节块,无穷个字节块就是全部内存的大小,而不同的类型的变量、数组所占的字节块的数量、占了哪些字节快都不相同,
- 比如 a 变量为 int 型,它就占了从 16579776 开始到 16579780结束的四个字节块,由于 pa 也是这部分内存的指针变量,所以初始值也一样。
- 当 pa 加一后,因为其数据类型也为 int 型,所以往后移四个长度,移动到了 16579780。
不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
# 指向数组的指针
如上一节,指针可以参与加减算数运算,对于数组:
- 数组是一系列具有相同类型的数据的集合,数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存;
- C语言中数组第一个元素的地址称为数组的首地址;
- 数组名可以被看作是一个指针,指向数组的第 0 个元素,也就是指向数组第 0 个元素的指针。
注意
数组名和数组首地址绝不等价,但是现阶段的学习把数组名当做指向第 0 个元素的指针使用即可
# 直接用数组名充当指针遍历数组
#include <stdio.h>
int main()
{
int arr[3] = { 1, 3, 5 };
int length = sizeof(arr) / sizeof(int);
for (int i = 0; i < length; i++) {
printf("arr[%d] = %d\n", i, *(arr + i));
}
}
2
3
4
5
6
7
8
9
10
由于可以把 arr 数组名看作指针,也就是 arr 存了数组第一个元素地址,对于 *(arr + i)
的理解:
- 首先
arr + i
,当i = 0
时,指向数组第一个元素的地址,所以是第一个元素,当i = 1
时,由于为 int,数组每个元素占四个字节长度,而且数组中元素连续排列,所以arr + 1
执行的是数组第一个元素往后移四个字节的长度,也就是对应第二个元素的地址,以此类推,可以对应一个一个的元素。 *(arr + i)
,指针变量在非定义时加*
,表示通过指针执行取值操作,表示通过地址(arr + i),拿到对应的值。
# 使用数组指针来遍历
补充
- 如果一个指针指向了数组,我们就称它为数组指针
- 数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,如:数组内元素为 int, 那数组指针也必须为 int。
#include <stdio.h>
int main()
{
int arr[3] = { 9, 3, 7 }, *p = arr;
int length = sizeof(arr) / sizeof(int);
for (int i = 0; i < length; i++) {
printf("arr[%d] = %d\n", i, *(p + i));
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 拓展
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++
分别是什么意思呢?
*p++
等价于*(p++)
,表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。*++p
等价于*(++p)
,会先进行++p
运算,使得 p 的值增加,指向下一个元素,整体上相当于*(p+1)
,所以会获得第 n+1 个数组元素的值。(*p)++
就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
# 指向字符串的指针
# 在字符串中使用指针
字符数组归根结底还是一个数组,关于指针和数组的规则同样也适用于字符数组
#include <stdio.h>
#include <string.h>
/*使用指针遍历字符串并将其中的字符一个个的打印出来*/
int main()
{
char str[] = "This is a test message.";
long len = strlen(str);
/*使用字符串名充当指针*/
for (int i = 0; i < len; i++) {
printf("%d: %c\n", i, *(str + i));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 第二种声明字符串的形式
之前在声明字符串时使用的是字符数组的形式,还有一种声明字符串的方式为直接使用一个指针指向字符串,使用这种声明方式来声明得到的字符串称为字符串常量。
char *str = "this is a string";
/*或者*/
char *aStr;
aStr = "this is another string";
2
3
4
其中,字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是 char
,所以 str 的类型也必须是 char *
。
输出使用指针声明的字符串:
#include <stdio.h>
#include <string.h>
int main()
{
char *str = "This is a test message.";
long len = strlen(str);
/*直接输出整体字符串*/
printf("str is: %s\n", str);
/*两种遍历该字符串方法*/
for (int i = 0; i < len; i++) {
printf("%c\n", *(str + i));
}
for (int i = 0; i < len; i++) {
printf("%c\n", str[i]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 两种声明字符串方式的差别
它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
也就是说,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
因此我们将第二种形式的字符串称为字符串常量。
# 解析
这里的不可修改为修改内容,但是改变指针指向是允许的,如下:
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "I love C!"; //正确,这里改变了 str 的指向
str[3] = 'P'; //错误,字符串常量不可修改
return 0;
}
2
3
4
5
6
7
# 使用场景
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
# 小结
C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
# 指针与函数
# 指针作为函数参数
之前学的函数参数都是一些简单类型,直接给个形参往函数里传就行,但是像字符串、数组、动态分配的内存如若想作为参数,可以使用指针。
注意
使用了指针就要注意了,在函数内部机会直接修改内存中的数据,一旦改了,全局便都改了,所以,像那种需要全局修改的简单类型变量数据,也可以通过指针传到函数中。
#include <stdio.h>
/*交换两个变量数据*/
void swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 4, b = 9;
printf("a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
输出结果

用数组作为函数参数
#include <stdio.h>
/*遍历整型数组并返回最大的数*/
int findArrMax(int *arr, int len)
{
int maxValue = *arr; //假设第一个元素为最大值
for (int i = 0; i < len; i++) {
maxValue = maxValue > *(arr + i) ? maxValue : *(arr + i);
}
return maxValue;
}
int main()
{
int arr[3];
int len = sizeof(arr) / sizeof(int);
printf("Please press three number: (The two numbers are separated by spaces)\n");
for (int i = 0; i < len; i++) {
scanf("%d", arr + i);
}
printf("The max value is: %d\n", findArrMax(arr, len));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
notes
- 参数 arr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。
- 数组 arr 的每个元素都是整数,
scanf()
在读取用户输入的整数时,要求给出存储它的内存的地址,arr+i
就是第 i 个数组元素的地址。
数组作为形参的多种写法
如上,用指针作为形参是一种写法,还有如下写法:
int findArrMax(int arr[3], int len) {}
// 或者也可以省略数组长度,把形参简写为下面的形式
int findArrMax(int arr[], int len) {}
2
3
notes
int arr[3]
这种形式只能说明函数期望用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素;实际上这两种形式的数组定义都是假象,不管是
int arr[3]
还是int arr[]
都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *arr
这样的指针;不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 arr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。
拓展
C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。
# 指针作为函数返回值
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。这里的函数别忘了在声明时前面加 *
。
#include <stdio.h>
#include <string.h>
/*传入两个字符串指针,返回长度较大的指针*/
char *getLongerStr(char *str1, char *str2)
{
if (strlen(str1) > strlen(str2)) {
return str1;
}
else {
return str2;
}
}
int main()
{
char str1[30], str2[30];
gets(str1);
gets(str2);
printf("The longer one is: %s", getLongerStr(str1, str2));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 二级指针
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
#include <stdio.h>
/*多级指针的定义*/
int main()
{
int a = 100;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
printf("a = %d, &a = %#X\n", a, &a);
printf("p1 = %#X, &p1 = %#X\n", p1, &p1);
printf("p2 = %#X, &p2 = %#X\n", p2, &p2);
printf("p3 = %#X, &p3 = %#X\n", p3, &p3);
/*通过多级指针取值*/
printf("a = %d\n", a);
printf("*p1 = %d\n", *p1);
printf("**p2 = %d\n", **p2);
printf("***p3 = %d\n", ***p3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
打印结果

用图描述各级指针间的关系

# NULL 指针
赋为 NULL
值的指针被称为空指针。
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯
#include <stdio.h>
int main ()
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
/*输出*/
/*ptr 的地址是 0x0*/
2
3
4
5
6
7
8
9
10
11
12
# 指针数组
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。
#include <stdio.h>
int main()
{
int a = 3, b = 6, c = 9;
/*声明一个指针数组*/
int *arr[] = { &a, &b, &c };
int len = sizeof(arr) / sizeof(int);
for (int i = 0; i < len; i++) {
/*指针数组中存的都是指针,也就是都是地址*/
printf("arr[%d] = %#X\n", i, *(arr + i));
/*由于存的每一项都是地址,那通过这个地址再取出确切的值*/
printf("It points value: %d\n", **(arr + i));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 指针数组结合字符串数组
#include <stdio.h>
#include <string.h>
int main()
{
/*字符串数组*/
char *strArr[] = {
"string1",
"string2",
"string3"
};
for (int i = 0; i < strlen(strArr); i++) {
/*每一项所存的指针地址*/
printf("strArr[%d] = %#X\n", i, *(strArr + i));
/*指针所对应的具体值*/
printf("strArr[%d] = %s\n", i, *(strArr + i));
}
return 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
notes
- 字符数组 strArr 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。
- 只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。上面为简写形式,等价于
char *str0 = "string1";
char *str1 = "string2";
char *str2 = "string3";
char *strArr[3] = {str0, str1, str2};
2
3
4
# 二维数组指针
# 指针数组和二维数组指针的区别
- 指针数组:前面的一节为指针数组,指针数组是一个数组,指的是一个数组里面的项存着一个个的指针;
- 二维数组指针:为指针,它指向一个二维数组。
下面为声明方法:
// 优先级方面 [] > *
int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5]; //二维数组指针,不能去掉括号
/*这里表明指针 p2指向了一个 int 型的数组 [5]*/
2
3
4
# 二维数组的内存排布

- 上面例子中,每个 int 数据占 4 个字节,那么一行就是 4×4 占 16 个字节的内存;
- 上述二维数组可以拆成三个一维数组,每一个一维数组又包含四个元素;
- 各个元素的地址也是紧密排布的,如下:
# 声明二维数组指针
接着上面例子,我们声明一个指针来指向这个二维数组。
int (*p)[4] = a;
/*这里表明指针 p 指向了一个 int 型的数组 [4]*/
/*其中,[4] 为一个长度为4的数组*/
2
3
notes:
- 指针 p 初始是指向 a 的头部,假设地址为 1000,当执行
p + 1
操作时,按照原来的操作移动向下一个元素,地址移动一个 int 数据的长度,也就是地址+4
; - 但是我们需要移动到下一行,地址移动四个 int 数据的长度,所以地址要
+16
,正好是这个二维数组拆成三个一维数组后,一个一维数组所占的字节长度; - 这也正是我们在声明 p 指针时,让它指向一个 int 型数组 [4] 的原因,长度也正好是4;
- 这样在执行
p+1
操作时,刚好往后移动 16 字节的长度。
#include <stdio.h>
/*通过指针遍历二维数组*/
int main()
{
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10 ,11} };
int(*p)[4] = a;
// p指向一个 int 型数组 <=> *p 为该数组具体数据,里面是 a 的首元素地址
// (*p + 1)为下一位元素具体值,里面存了 a 的第二个元素地址
// (*(*p + 1)) 就为取 a 的第二个元素具体数据
for (int i = 0; i < 4; i++) {
printf("%d\n", *(*p + i));
}
/*遍历二维数组*/
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("a[%d][%d] = %d ", i, j, *(*(p+i) + j));
}
printf("\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
大致图例:
首先遍历二维数组第一行,也就是 i = 0
时

然后遍历第二行,也就是 i = 1
时,此时 p + i 为 p + 1
,往下移动一行,16字节的长度

# 函数指针
# 概述
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
# 声明函数指针
returnType (*pointerName)(paramsList)
- 从左向右依次是:函数返回值类型, 指针名, 参数列表;
- 参数列表为:
参数类型 + 参数名
或只写参数类型
两种形式; - 中间的指针名括号不可省略。
# 实例
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
return a > b ? a : b;
}
int main(){
int x, y, maxVal;
//定义函数指针
int (*pMax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
printf("Please input two numbers:\n");
scanf("%d %d", &x, &y);
maxVal = (*pMax)(x, y);
printf("The max value is: %d\n", maxVal);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15