Auc的个人博客

vuePress-theme-reco Auc    2020 - 2021
Auc的个人博客 Auc的个人博客

选择一个主题吧~

  • 暗黑
  • 自动
  • 明亮
主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
author-avatar

Auc

62

文章

11

标签

主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
  • C

    • 写在前面
    • C语言概述
    • 数据类型、运算符与表达式
    • 语句、函数、作用域与数组与字符串
    • 指针
    • 结构体
    • typedef关键字

指针

vuePress-theme-reco Auc    2020 - 2021

指针

Auc 2021-03-10 C语言笔记

# 指针

# 指针与指针变量

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。

解释

总而言之,C中的变量对应的内存空间中一旦存放了另一个变量的一份地址(也叫指针),为了跟其他普通变量区分开来,那它就有了个头衔,指针变量

# 声明指针变量

// 声明指针变量
int *pi;

// 声明后初始化
float *pp; // 首先声明变量 pp 为指针变量
pp = &value1; // 然后将变量 value1 的地址赋值给指针变量 pp

// 声明并初始化
int *pt = &value2; // 声明指针变量 pt 并将变量 value2 的地址赋值给它
1
2
3
4
5
6
7
8
9

notes:

  1. 声明一个指针跟声明普通变量的方式一致,不过为了跟普通变量区分开来,特意在变量名前面加了个 *;
  2. 第二种方式,在初始化时不可以写成 *pp = &value1;
  3. 第三种方式就相当于第二种方式的简写。
  4. 注意前面的数据类型,这就表明指针变量的数据类型也不可以乱赋值,如下例子:
// 声明普通变量并初始化
float a = 99.5, b = 10.6;
char c = '@', d = '#';
// 声明指针变量并初始化
int *p1 = &a;
float *p2 = &c;
// 修改指针变量的数据
p1 = &b;
p2 = &d;
1
2
3
4
5
6
7
8
9

说明:

  1. 指针变量的值是可以修改的;
  2. float 型的指针变量只能存 float 型变量的地址,char、int 或者其他的也一样;
  3. 定义指针变量时必须带 *,给指针变量赋值时不能带 *。

一张图来直观的描述上例

# 通过指针获取变量

指针变量里的内存空间存储着地址值,通过这个地址就可以找到对应的数据,简单来说,通过指针变量就可以找到对应的数据。

下面看一个例子:

#include <stdio.h>

int main()
{
	int value1 = 10;
	int *p1 = &value1;
	printf("The value of p1 is: %d\n", *p1);
}
1
2
3
4
5
6
7
8

输出结果

The value of value1 is: 10
The value of p1 is: 10
1
2

从结果上来看,两者打印的结果一致,说明此时的 value1 与 *p1,通过这两个变量名都可以找到存储着数据 10 的那一块内存。

补充

虽然两者都可以找到,但是显然直接用 value1 效率更高些,通过下图说明

# 理解

  1. *与&

有个 int 型变量 a,pa 是指向它的指针,说一说 *&a和&*pa的区别:

  • *&a 等价于 *(&a), 首先 &a 等价于 pa, 外面加了个 *, 等价于 *pa, 也就是通过指针获取变量,所以其值对应 a 的值;
  • &*pa 等价于 &(*pa), 首先 *pa 值对应 a 的值,可写为 &a, 也就是取 a 的地址,而 a 的地址也存在其指针变量 pa 对应的内存中,所以等价于 pa
  1. 对于 * 的总结:
  • 乘性运算符;
  • 定义指针变量时,用于区分普通变量与指针变量;
  • 通过指针获取具体值时使用。

# 指针算数运算

指针可以完成 ++, --, +, - 算数运算。

大家知道,指针就是地址,指针的运算,也就是地址的运算,也就是内存的运算,字节的运算。

这就与普通的加减运算不同,指针的加减运算的结果跟数据类型的长度有直接关系,比如下例:

#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);
}
1
2
3
4
5
6
7
8
9
10
11
12

变量 a 为 int 型变量,内存占 4 个字节,我们把 1 字节比喻成下图的一个正方形,那么变量 a 所占的内存总长度如下范围。

而指针变量 pa 指向的位置正是 a 的头部,如图所示。

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

打印输出结果如下:

正好加了四字节的长度。

补充

也不能说长度吧,可以理解成:

  1. 这么长的数字都是一个个的字节块,无穷个字节块就是全部内存的大小,而不同的类型的变量、数组所占的字节块的数量、占了哪些字节快都不相同,
  2. 比如 a 变量为 int 型,它就占了从 16579776 开始到 16579780结束的四个字节块,由于 pa 也是这部分内存的指针变量,所以初始值也一样。
  3. 当 pa 加一后,因为其数据类型也为 int 型,所以往后移四个长度,移动到了 16579780。

不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。

# 指向数组的指针

如上一节,指针可以参与加减算数运算,对于数组:

  1. 数组是一系列具有相同类型的数据的集合,数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存;
  2. C语言中数组第一个元素的地址称为数组的首地址;
  3. 数组名可以被看作是一个指针,指向数组的第 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));
	}
}
1
2
3
4
5
6
7
8
9
10

由于可以把 arr 数组名看作指针,也就是 arr 存了数组第一个元素地址,对于 *(arr + i) 的理解:

  1. 首先 arr + i,当 i = 0 时,指向数组第一个元素的地址,所以是第一个元素,当 i = 1 时,由于为 int,数组每个元素占四个字节长度,而且数组中元素连续排列,所以 arr + 1 执行的是数组第一个元素往后移四个字节的长度,也就是对应第二个元素的地址,以此类推,可以对应一个一个的元素。
  2. *(arr + i) ,指针变量在非定义时加*,表示通过指针执行取值操作,表示通过地址(arr + i),拿到对应的值。

# 使用数组指针来遍历

补充

  1. 如果一个指针指向了数组,我们就称它为数组指针
  2. 数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,如:数组内元素为 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;
}
1
2
3
4
5
6
7
8
9
10
11

# 拓展

假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?

  1. *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。
  2. *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
  3. (*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));
	}
}
1
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";
1
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]);
	}
}
1
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;
}
1
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);
}
1
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;
}
1
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

  1. 参数 arr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。
  2. 数组 arr 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,arr+i 就是第 i 个数组元素的地址。

数组作为形参的多种写法

如上,用指针作为形参是一种写法,还有如下写法:

int findArrMax(int arr[3], int len) {}
// 或者也可以省略数组长度,把形参简写为下面的形式
int findArrMax(int arr[], int len) {}
1
2
3

notes

  1. int arr[3] 这种形式只能说明函数期望用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素;

  2. 实际上这两种形式的数组定义都是假象,不管是 int arr[3] 还是 int arr[] 都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为 int *arr这样的指针;

  3. 不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 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;
}
1
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);
}
1
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*/
1
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));
	}
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

notes

  1. 字符数组 strArr 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。
  2. 只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。上面为简写形式,等价于
char *str0 = "string1";
char *str1 = "string2";
char *str2 = "string3";
char *strArr[3] = {str0, str1, str2};
1
2
3
4

# 二维数组指针

# 指针数组和二维数组指针的区别

  1. 指针数组:前面的一节为指针数组,指针数组是一个数组,指的是一个数组里面的项存着一个个的指针;
  2. 二维数组指针:为指针,它指向一个二维数组。

下面为声明方法:

// 优先级方面 [] > *
int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5];  //二维数组指针,不能去掉括号
/*这里表明指针 p2指向了一个 int 型的数组 [5]*/
1
2
3
4

# 二维数组的内存排布

  1. 上面例子中,每个 int 数据占 4 个字节,那么一行就是 4×4 占 16 个字节的内存;
  2. 上述二维数组可以拆成三个一维数组,每一个一维数组又包含四个元素;
  3. 各个元素的地址也是紧密排布的,如下:

# 声明二维数组指针

接着上面例子,我们声明一个指针来指向这个二维数组。

int (*p)[4] = a;
/*这里表明指针 p 指向了一个 int 型的数组 [4]*/
/*其中,[4] 为一个长度为4的数组*/
1
2
3

notes:

  1. 指针 p 初始是指向 a 的头部,假设地址为 1000,当执行 p + 1 操作时,按照原来的操作移动向下一个元素,地址移动一个 int 数据的长度,也就是地址 +4;
  2. 但是我们需要移动到下一行,地址移动四个 int 数据的长度,所以地址要 +16,正好是这个二维数组拆成三个一维数组后,一个一维数组所占的字节长度;
  3. 这也正是我们在声明 p 指针时,让它指向一个 int 型数组 [4] 的原因,长度也正好是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");
	}
}
1
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)

  1. 从左向右依次是:函数返回值类型, 指针名, 参数列表;
  2. 参数列表为:参数类型 + 参数名 或 只写参数类型 两种形式;
  3. 中间的指针名括号不可省略。

# 实例

#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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15