c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

本章重点:

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 几种数组、指针之间的比较
  6. 函数指针
  7. 函数指针数组
  8. 指向函数指针数组的指针
  9. 回调函数
  10. 指针和数组的笔试题

前情回顾:

  1. 指针是一个存储地址的变量,地址是一块内存空间的唯一标识;
  2. 指针的大小是固定的(4字节或8字节);
  3. 指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;
  4. 指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;

接下来,就让我们继续探讨指针的进阶主题。


一、字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*
一般使用方式:

int main()
{
	char ch = 'a';
	char *pch = &ch;
	*pch = 'A';

	return 0;
}

还有另外一种使用方法:

#include <stdio.h>

int main()
{
	const char *pstr = "hello world"; // 这里是把一个字符串放入了pstr指针变量中吗?
	printf("%s\n", pstr);

	return 0;
}

在上面的代码中很容易让人误认为是将字符串放入了指针中,但实际上只是将字符串的首字符地址放入了pstr中。

所以就出现了下面的这个问题:(它会输出什么呢)

#include <stdio.h>

int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";
	char *str3 = "hello world";
	char *str4 = "hello world";

	if (str1 == str2)
	{
		printf("str1 == str2\n");
	}
	else
	{
		printf("str1 != str2\n");

	}

	if (str3 == str4)
	{
		printf("str3 == str4\n");
	}
	else
	{
		printf("str3 != str4\n");

	}

	return 0;
}

运行结果:
在这里插入图片描述
其中,str1和str2是两个数组名,它们代表了它们首元素的地址。这是两个不同的数组,在内存中分配的空间也不相同,所以它们的首元素地址肯定不同;
str3和str4是两个指针变量,它们指向字符串hello world,这是一个字面常量字符串,在内存中只需保存一份,所以str3和str4指向同一片空间,即str3 == str4。

二、指针数组

数组在后,指针是用来修饰数组的,所以指针数组是一个数组,数组内部元素类型为指针。
这个在初阶的指针中介绍过了,这里就简单回忆一下。

int main()
{
	int *arr1[3]; // 数组大小为3,数组元素为int *
	char *arr2[10]; // 数组大小为10,数组元素为char *
	char **arr3[20]; // 数组大小为20,数组元素为char **
}

它的使用和普通数组的相同的。

三、数组指针

由上面的指针数组的解释可以看出,数组指针,重点在指针,前面的数组是用来修饰描述指针的,
所以数组指针是一个指向数组的指针 - - 保存数组的地址。
注意与数组首元素地址作出区分,它们虽然值属性相同,但类型属性不同。

3.1数组指针的定义

对于普通的指针,我们都已经很熟悉了。
整型指针:int * - - 指向整型数据的指针;
浮点型指针:float * - - 指向浮点型数据的指针;
下面代码中哪个是指向数组的指针呢?

int main()
{
	int *arr1[5];
	int (*arr2)[5];

	return 0;
}

分析:
根据操作符的优先级,下标引用操作符[]的优先级是比间接访问操作符*更高的,所以arr1是先与[]结合形成数组,再根据数组的定义为type ArrayName[const_Arraysize]可知,数组名前面的是数组的类型,所以数组arr1的元素类型为int *,即arr1是一个元素类型是int *的指针数组

3.2&数组名VS数组名

对于下面的数组

int main()
{
	int arr[10];

	return 0;
}

其中的arr&arr分别是指什么?
我们都知道数组名arr表示首元素地址。但是存在两种特殊情况:

  1. sizeof(<数组名>),此时的数组名表示整个数组;
  2. &<数组名>,此时的数组名也表示整个数组;

所以虽然arr&arr的值是相同的,但它们的类型不同,两个地址所能访问的空间大小也不同。&arr的类型为int (*)[10],指针运算时的步长为10个int类型的大小。

3.3数组指针的使用

那么学习了数组指针之后,我们要怎么使用它呢?

看代码

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int (*parr)[10] = &arr;

	return 0;
}

上述代码虽然没有错误,但将一个一维数组的地址存入一个数组指针中并没有什么实际意义,毕竟数组创建的意义就是存储、使用这一组数据,一个数组指针在运算时,会直接跳过整个数组。

真正的使用场景如下:

#include <stdio.h>

void initArr(int arr[3][5])
{
	int i = 0;
	int j = 0;

	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			arr[i][j] = i * 5 + (j + 1);
		}
	}
}

void printArr(int (*arr)[5])
{
	int i = 0;
	int j = 0;

	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { 0 };
	initArr(arr);
	printArr(arr);

	return 0;
}

initArr函数使用了int arr[][]作为参数,但实际上这就是一个数组指针,形参中写成这个形式,只是出于可读性的考虑,实际上在将二维数组作为函数参数传递时,二维数组会退化成一个指向数组的指针。这就和printArr函数的形参类型一致。

也就是说数组指针一般是用来和二维数组配对使用的。

下面代码都是什么意思呢?

int arr[5];
int *parr1[5];
int (*parr2)[5];
int (*parr3[10])[5];

arr是一个一维数组,数组元素类型为int
parr1是一个指针数组,数组元素为int *
parr2是一个数组指针,指针类型为int [5];
parr3是一个数组,数组元素为数组指针;

parr3的分析:
parr3先与[]结合,所以它是一个数组,数组中有10个元素;
剩余的部分就是数组中元素的类型,int (*)[5]就是该数组的类型,这是一个数组指针。
该数组指针指向的数组类型为int [5],这个数组有5个元素,元素类型为int;

四、数组传参、指针传参

在写代码时,难免会遇到需要将【数组】或【指针】作为参数传递给函数,那么此时的函数参数应该如何设计呢?

1.一维数组传参

分析下面代码

// 代码1
void test(int arr[10]){} // ok?
// 代码2
void test(int arr[]){} // ok?
// 代码3
void test(int *arr){} // ok?
// 代码4
void test1(int *arr1[10]){} // ok?
// 代码5
void test1(int *arr1[]){} // ok?
// 代码6
void test1(int **arr1){} // ok?

int main()
{
	int arr[10];
	int *arr1[10];
	test(arr);
	test1(arr1);

	return 0;
}

上述代码中,全部函数的参数都是正确的。
对于test函数:
代码1~3很简单,在初阶指针部分介绍过了,这里就不再赘述;
test1函数:
代码4直接用一个指针数组来接收一个指针数组肯定是没问题的。
代码6使用二级指针来接收,回忆一下数组传参,使用数组名作为参数时,它会退化为一个指向首元素的指针。数组的首元素类型为int *,指向int *类型的指针就是二级指针,所以代码6也是正确的。
可能有人对其中的代码5有疑惑,请对比代码2和代码5,这两种有什么相同之处,有什么不同之处?
相同:两者都是一维数组,且数组的大小并未指明;
不同:两者的元素类型不同,代码2中的元素类型为int,代码5中的元素类型为int *;
在一维数组中数组的大小是可以缺省不写的,所以相同的代码中的数组大小也可以缺省不写。

但是出于可读性的考虑,建议使用代码4或代码6的写法

验证对代码5的分析是否正确的代码

#include <stdio.h>

void test1(int* arr1[], int *arr) {
	int i = 0;
	for (i = 0; i < 10; i++) // 这里的 10,也可以使用函数参数获取,这是数组的大小
	{
		arr1[i] = arr + i;
	}
} 

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("&arr[%d] == %p\n", i, &arr[i]);
	}
	printf("\n");
	int *arr1[10];
	//test(arr);
	test1(arr1, arr);
	for (i = 0; i < 10; i++)
	{
		printf("arr1[%d] == %p\n", i, arr1[i]);
	}

	return 0;
}

运行结果:
在这里插入图片描述
这里可以看到,这个缺省的指针数组确实保存了arr这个数组中元素的地址。

2.二维数组传参

分析下面代码

// 代码1
void test(int arr[3][5]){}
// 代码2
void test(int arr[][5]){}
// 代码3
void test(int arr[3][]){}
// 代码4
void test(int arr[][]){}

// 代码 1~ 4中只有 1和 2是正确的,原因是二维数组只能省略行数,但一列有多少个元素不能省略
// 至于为什么是这样,请看心法中指针部分

// 代码5
void test(int *arr){}
// 代码6
void test(int* arr[5]){}
// 代码7
void test(int (*arr)[5]){}
// 代码8
void test(int **arr){}

// 代码 5~ 8中能正常使用的是代码 5和代码 7
// 代码 6和代码 8本质上是一样的
// 代码 6中形参为一个指针数组,但是我们都知道,数组在函数传参时会退化成指针,
//    它在这里退化成了一个指向int *类型的指针,也就是一个二级指针 -- int **
// 至于代码 7一个二维数组在传参时也会退化成指向它首元素的指针,二维数组的首元素为一个一维数组,
//    指向一维数组的指针就是数组指针,所以这里用一个数组指针来接收是没有任何问题的
// 而代码 5为什么正确呢,这里就需要大家明确知道一点:一个二维数组它在内存中的空间是连续分配的
//    除此之外,指针就是一个地址,不同类型的指针只是能类型属性不同,值属性是没有区别的,
//    当我们使用一个指向int类型的指针来接收二维数组的参数时,我们的目的就是要访问这个数组中保存的元素,
//    无论是要修改它们或仅仅是读取它们都无所谓,只需明确一点,数组中的数据是什么类型 -- 这里是int
//   所以当我们要使用指针来访问一个int类型的值时,要用什么类型的指针呢?
//    没错,int *类型的指针,使用这个指针和对应元素的下标,我们就能通过指针运算来访问整个二维数组

int main()
{
	int arr[3][5] = { 0 };
	test(arr);

	return 0;
}

代码5的验证

#include <stdio.h>

void initArr(int* arr, int rowSize, int colSize)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rowSize; i++)
	{
		for (j = 0; j < colSize; j++)
		{
			*(arr + i * colSize + j) = i * colSize + j + 1;
		}
	}
}

void printArr(int* arr, int rowSize, int colSize)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rowSize; i++)
	{
		for (j = 0; j < colSize; j++)
		{
			printf("%d ", *(arr + i * colSize + j));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5];
	int rowSize = sizeof(arr) / sizeof(arr[0]);
	int colSize = sizeof(arr[0]) / sizeof(arr[0][0]);
	initArr(arr, rowSize, colSize);
	printArr(arr, rowSize, colSize);

	return 0;
}

二维数组的内存分布:
在这里插入图片描述

运行结果:
在这里插入图片描述

对于代码6和8的分析:
因为这两种代码都是二级指针作为形参,在传参时,实参是二维数组名,这是该数组的起始地址,传递进函数后,函数是通过它形参的类型来访问这个地址的。
对于二级指针arr,解引用一次后的*arr的类型是int *,但是需要注意*arr是直接将arr这个地址指向的空间中的值当成了这个int *类型的值,换句话说,就是二维数组中的首元素中的首元素(第一个int类型的值被当成了一个地址来使用),这时*arr这个一级指针就是一个野指针。
在这里插入图片描述

通过代码来验证上述的分析,看看是否真的是这样

void test(int** arr)
{
	arr[0][0];
}

int main()
{
	int arr[3][5] = { 6 };
	test(arr);

	return 0;
}

调试过程:
在这里插入图片描述
可以看到的确和分析的一样,*arr中的值确实是和有符号数6存储在内存中的值相同。*arr变成了孤魂野鬼(野指针)。

笔者在学习这个地方时曾经产生过一个问题,同样的地址,对于int (*arr)[5]int **arr,使用arr[i][j]来尝试访问元素时,为什么会产生两种不同的效果呢?arr[i][j]的本质是*(*(arr + i) + j),这两种类型的arr中的值不是一样的吗?为什么使用二级指针时,*arr直接将这个地址的值当成了一个地址,导致*arr变成了野指针;而使用数组指针时,*arr又没有这样呢?

看一个简单的例子

int main()
{
	int arr[5] = { 0 };
	*&arr[0];
	*&arr;
	
	return 0;
}

在这里插入图片描述
可以看到,
对一个数组指针解引用时会得到一个数组,并得到数组的首元素地址;
对一个int类型的指针解引用时,会得到int类型的值;
类比二维数组,
将二维数组名传给类型为int **的形参时,*arr就是对一个int *类型的指针解引用,这就得到了一个int *类型的值,所以arr这个地址指向的值就被当成了一个地址来使用;
将二维数组名传给类型为int (*)[]的形参时,对数组指针解引用就得到了一个数组;

解引用得到的值,是根据指针指向数据的类型来决定它是以什么样的形式处理的,
指向的类型是一个数组,指针指向内存中的值就是这个数组的首元素,*parr就是数组的地址;
指向的类型是一个int类型的数,内存中的值就是这个int的补码,*parr就是int类型的值;
指向的类型是一个指针,内存中的值就是这个指针指向的地址,*parr就是地址。

我们再从汇编的角度来看看,这两种类型在执行arr[i][j]时有什么不同。

void test1(int** arr)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			arr[i][j] = i * 5 + j;
		}
	}
}

void test2(int (*arr)[5])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			arr[i][j] = i * 5 + j;
		}
	}
}

int main()
{
	int arr[3][5] = { 0 };
	test1(arr);
	test2(arr);

	return 0;
}

test1中的汇编:
在这里插入图片描述
在test1中的执行过程是:

  1. 计算i * 5的值,并将其放入eax中,等价于eax = i * 5
  2. 将j的值加到eax中,等价于eax = eax + j
  3. 将i的值保存到ecx中,等价于ecx = i
  4. 将arr的值保存到edx中,等价于edx = arr,注意这里的arr是一个地址
  5. 将edx + ecx * 4这个地址中的值,保存到ecx中,等价于ecx = *(edx + ecx * 4)
  6. 将j的值保存到edx中,等价于edx = j
  7. 将eax中的值保存到ecx + edx * 4这个地址中,等价于*(ecx + edx * 4) = eax,即*(*(arr + i * 4) + j * 4) = i * 5 + j

test2中的汇编:
在这里插入图片描述
在test2中的执行过程是:

  1. 计算i * 5的值,并将其放入eax中,等价于eax = i * 5
  2. 将j的值加到eax中,等价于eax = eax + j
  3. 将i * 14h的值保存到ecx中,这里的14h是一个十六进制数,h后缀表示这是一个十六进制数,等价于ecx = i * 20
  4. 将arr的值加到ecx中,等价于ecx = arr + ecx,注意这里的arr是一个地址,相当于指针运算。
  5. 将j的值保存到edx中,等价于edx = j
  6. 将eax中的值保存到ecx + edx * 4这个地址中,等价于*(ecx + edx * 4) = eax,即*(*(arr + i * 20) + j * 4)

可以看到,在这两种代码中,对于*(arr + i)的操作,前者只移动了4个字节,后者移动了20个字节(5个int类型值所占空间的大小),所以这也说明了数据类型的重要性,同样的值因为类型不同,同样的操作却产生了不同的结果。
指针指向内容的类型是什么,解引用就会得到一个什么类型的值,
二级指针指向一级指针,所以对二级指针解引用就得到了一个一级指针;
数组指针指向一个数组,所以对数组指针解引用就得到了一个数组;
即:解引用操作实际上是得到了一片空间,这片空间的值是什么类型,就会把里面的值以什么类型来处理。
得到一个数组,就是得到它的首元素地址,所以这就是为什么,相同的操作,二级指针会直接将第一个元素当作地址来使用,而数组指针得到的还是一样(只是值属性相同)的地址。

所以在数组传参时,一定要注意类型,是什么类型就用什么类型接收

3.一级指针传参

#include <stdio.h>

void print(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int *parr = arr;
	// 一级指针传给函数
	print(parr, sz);

	return 0;
}

思考一下,当函数参数为一个一级指针时,它能接收哪些参数?
如:

void test(int *arr){}
它能接收什么参数呢?

// 一个int类型变量的地址,传址调用函数,用该函数修改这个变量的值;
void test(int *a)
{
	*a = 20;
}
int main()
{
	int a = 10;
	test(&a);
	
	return 0;
}
// 一个数组,或是类似的结构,通过地址访问一组数据;
void print(int *arr, int sz)
{
	int i = 0;
	for(i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);

	return 0;
}

4.二级指针传参

#include <stdio.h>

void test(int** pp)
{
	printf("%d", **pp);
}

int main()
{
	int n = 10;
	int *p = &n;
	int **pp = &p;
	test(pp);
	test(&p);

	return 0;
}

同样的,二级指针作为函数参数时,它能接收什么样的参数呢?

int main()
{
	int a = 10;
	int *p = &a;
	int **pp = &p;
	// 首先它肯定能接收一个二级指针
	test(pp);
	test(&p);
	// 它还能接收一个指针数组
	// 数组名表示首元素的地址,该数组的元素类型为指针,所以指针的地址应该使用二级指针来接收
	int *arr[10] = { 0 };
	test(arr);

	return 0;
}

5.几种数组、指针之间的比较

在这里我们来对比一下,二级指针、指针数组、数组指针;

// 二级指针
int **arr1;
// 指针数组
int *arr2[5];
// 数组指针
int (*arr3)[5];

它们在内存的位置:
在这里插入图片描述
其中蓝色和绿色部分的值为地址,粉色部分为int类型值。二级指针之间在内存上是离散的。
在这里插入图片描述
在这里插入图片描述

在将指针或数组作为函数的参数时,一定要保证形参和实参的类型相同

五、函数指针

首先来看一段代码

#include <stdio.h>

void test()
{

}

int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);

	return 0;
}

运行结果:
在这里插入图片描述
可以看到输出的这两个都是函数test的地址,这里与数组作区分;
我们都知道数组名代表的是首元素地址,只有两种例外,sizeof&
而这里就不需要考虑那么多了,fun_name和&fun_name都表示函数的地址,没有例外。即,*fun_name()fun_name()是完全等价的。

这里出现了函数地址,那么我们应该用什么变量来保存这个地址呢?
没错,指针,函数指针。

看看下面两种代码哪一个表示函数指针,能够保存test的地址

void test()
{}

// 代码1
void *pfun1()
// 代码2
void (*pfun2)()

正确的是代码2,这里也使用操作符的优先级,()的优先级是高于*的,
所以代码1中,pfun1先与()结合,它是一个函数,它的返回值为void *
代码2中pfun2先*结合,它先是一个指针,剩下的就是指针的类型,void (),这是一个返回值类型为void的没有参数的函数。

看两段有趣的代码:

// 代码1
(*(void (*)())0)();
// 代码2
void (*signal(int, void (*)(int)))(int);

它们是表示什么的呢?

分析:
代码1:

  1. 首先找到0,在0前面的是(void (*)()),这是一个强制类型转换,将0转换为一个指针,这个指针指向一个函数,函数没有参数,返回值类型为void;即,将0转换为了一个函数指针,变成了一个地址;
  2. 然后是对上一步得到的地址进行解引用,*(void (*)())0,得到了这个函数;
  3. 最后和剩下的()结合,(*(void (*)())0)(),调用这个函数;

所以这段代码的作用是调用地址在0x00000000处的函数,函数为void fun()

代码2:

  1. signal先与()结合,变成一个函数,这个函数有两个参数,一个为int,另一个是一个函数指针,这个函数指针指向的函数有一个int类型参数,返回值类型为void,即,signal(int, void (*)(int))是一个函数,因为函数的参数中只有类型,没有形参名,所以这是一个函数声明。
  2. 剩下的部分是这个函数的返回值类型,也就是说void (*)(int)是这个函数的返回值类型,这也是一个函数指针。

所以这个代码是声明了一个函数,有两个参数,一个为int类型,一个为函数指针void(*)(int),返回值类型是函数指针void(*)(int)

// 代码2如果写成下面这样会更容易理解
void(*)(int) signal(int, void(*)(int)); // err
// 但是很遗憾语法不允许这样写
// 要说明一个函数的返回类型为指针,函数名必须和 *在一起
// 那我们还有其它方法吗?
// 有的,用typedef
typedef void(*)(int) fun_ret; // err
// 这样写可以吗?
// 很遗憾,还是不行
// 这里的 fun_ret也必须和 *在一起,才能说明这是一个指针
// 所以下面就是代码2的另一种易于理解的写法
typedef void(*fun_ret)(int) ;
fun_ret signal(int, fun_ret);

这里要注意,区分解引用操作符*和定义指针变量时的*,解引用操作符*只能对一个指针变量使用,这里只有一个signal看起来像是一个名字,所以这里的*就只能是定义指针时的*了;signal先是一个函数,又分是在调用,还是在声明;这里的函数参数中只有类型没能形参名,所以只能是在声明函数,所以这就确定了signal是一个函数名,这个函数的有两个参数,一个int类型,一个函数指针;除此之外,剩余部分就只有函数的返回值类型了,所以void (*)(int)是它的返回值类型。

六、函数指针数组

通过前面对数组指针和指针数组的学习,这个函数指针数组,大家应该都能一眼就看出它不是人(指针),它是一个数组,数组中元素的类型为函数指针。

那么函数指针数组应该如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

正确的应该是parr1;

分析:
parr1先与[]结合形成数组,剩余的是数组中元素的类型,也就是int (*)(),这是一个函数指针。
parr2先与[]结合形成数组,剩余的是数组中元素的类型,也就是int* ()这是一个函数,但c语言中并没有函数数组这种东西。
在这里插入图片描述
parr3也是先与parr3结合形成数组,剩余部分虽然也是int (*)()一个函数指针,但这不符合语法。

函数指针数组有什么用呢?它是出于什么目的设计出来的呢?
我们先来看一段代码。

// 计算器
#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int Sub(int a, int b)
{
	return a - b;
}

int Mul(int a, int b)
{
	return a * b;
}

int Div(int a, int b)
{
	return a / b;
}

void menu()
{
	printf("*****************\n");
	printf("** 1.Add 2.Sub **\n");
	printf("** 3.Mul 4.Div **\n");
	printf("**    0.Exit   **\n");
	printf("*****************\n");
}

int main()
{
	int input = 0;
	do
	{
		int a = 0;
		int b = 0;
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出计算器\n");
			break;
		case 1:
			printf("请输入操作数:>");
			scanf("%d %d", &a, &b);
			printf("ret = %d\n", Add(a, b));
			break;
		case 2:
			printf("请输入操作数:>");
			scanf("%d %d", &a, &b);
			printf("ret = %d\n", Sub(a, b));
			break;
		case 3:
			printf("请输入操作数:>");
			scanf("%d %d", &a, &b);
			printf("ret = %d\n", Mul(a, b));
			break;
		case 4:
			printf("请输入操作数:>");
			scanf("%d %d", &a, &b);
			printf("ret = %d\n", Div(a, b));
			break;
		default:
			printf("没有这个选项\n");
			break;
		}
	}while(input);

	return 0;
}

这段代码中通过switch分支语句来实现不同的计算操作。但我们可以发现,在这个分支语句中有较多的重复语句,有什么办法能够减少这些语句呢?仔细观察可以发现,AddSubMulDiv这四个函数的参数类型和返回值类型都相同,这就能够利用函数指针数组来减少这些重复代码了,这些重复代码中只有调用的函数不同,所以我们就可以将这几个函数的地址放入一个数组中,通过不同的下标我们就能调用不同的函数,这也实现了switch的分支功能。

接下来就是使用函数指针数组实现的代码:

// 计算器 -- 函数指针数组
#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int Sub(int a, int b)
{
	return a - b;
}

int Mul(int a, int b)
{
	return a * b;
}

int Div(int a, int b)
{
	return a / b;
}

void menu()
{
	printf("*****************\n");
	printf("** 1.Add 2.Sub **\n");
	printf("** 3.Mul 4.Div **\n");
	printf("**    0.Exit   **\n");
	printf("*****************\n");
}

int main()
{
	int input = 0;
	int (*op[4])(int, int) = { Add, Sub, Mul, Div }; // 函数指针数组
	do
	{
		int a = 0;
		int b = 0;
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入操作数:>");
			scanf("%d %d", &a, &b);
			printf("ret = %d\n", op[input - 1](a, b)); // 通过函数指针数组来调用不同的函数
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else
			printf("没有这个选项\n");
	} while (1);

	return 0;
}

可以看到使用了函数指针数组之后,代码简洁了许多。
这就是函数指针数组的使用场景:转移表

七、指向函数指针数组的指针

在使用普通的数组时,我们了解到c语言中是使用数组的地址来使用这个数组的,那么函数指针数组的指针是什么呢?
指向函数指针数组的指针,它是一个指针,指针指向一个数组,数组中的元素都是函数指针,也就是函数指针数组指针
那么这个指针应该如何定义呢?

// 一个普通的一维数组
int a[10];
// 指针这个一维数组的指针,数组指针
int (*pa)[10];
// 可以看到定义一个数组的指针,直接将数组名分出来,剩余的是数组的类型,将指针变量名放在[]前就成了数组指针
// 同理,下面就是定义一个函数指针数组指针的分析过程:
// 一个函数指针数组
void (*parr[10])();
// 这个数组的类型为 void(*[10])()
// 定义一个这个类型的指针
// *p
// 将这个指针放在[10]的前就成了一个函数指针数组指针了
void (*(*p)[10])();
// 再逆向分析回去
// 从 p开始,它与 *结合,它是一个指针
// 接着,它与 []结合说明指针指向的是一个数组,剩余部分是数组中元素的类型
// void(*)()这是一个函数指针,所以这是一个指向函数指针数组的指针

// 示例
#include <stdio.h>

void test(const char* str)
{
	printf("%s\n", str);
}

int main()
{
	const char *str = "hello world";
	// 函数指针
	void(*pfun)(const char*) = test;
	printf("pfun:");
	pfun(str);
	// 函数指针数组
	void(*pfun_buffer[5])(const char*);
	pfun_buffer[0] = test;
	printf("pfun_buffer[0]:");
	pfun_buffer[0](str);
	// 函数指针数组指针
	void(*(*ppfun_buffer)[5])(const char*) = &pfun_buffer;
}

八、回调函数

什么是回调函数?
来看定义:

回调函数是通过函数指针调用的函数。如果你把函数的指针(地址),当成参数传给另一个函数,当这个指针被用来调用它指针的函数时,我们就称其为回调函数。回调函数不应该由函数的实现方调用,而是在特定事件或条件发生时由另一方调用,用于对该事件或条件进行响应。

有了回调函数那么对于上面的计算器的实现代码又有了除函数指针数组之外的另一种实现方法 - - 使用回调函数

// 计算器 -- 回调函数
#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int Sub(int a, int b)
{
	return a - b;
}

int Mul(int a, int b)
{
	return a * b;
}

int Div(int a, int b)
{
	return a / b;
}

void calc(int (*pfun)(int, int))
{
	int a = 0;
	int b = 0;
	printf("请输入操作数:>");
	scanf("%d %d", &a, &b);
	printf("ret = %d\n", pfun(a, b));
}

void menu()
{
	printf("*****************\n");
	printf("** 1.Add 2.Sub **\n");
	printf("** 3.Mul 4.Div **\n");
	printf("**    0.Exit   **\n");
	printf("*****************\n");
}

int main()
{
	int input = 0;
	int (*op[4])(int, int) = { Add, Sub, Mul, Div };
	do
	{
		int a = 0;
		int b = 0;
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出计算器\n");
			break;
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		default:
			printf("没有这个选项\n");
			break;
		}
	} while (input);

	return 0;
}

在这里的switch语句使用了calc()这个函数,通过这个函数调用不同计算函数。

下面我们来看看回调函数的另一个使用场景:
qsort函数的使用:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这个函数实现了对任意一个数组中的元素的快速排序。思考一下,对一个数组进行排序,在不考虑排序算法的情况下,逻辑都是相同的,只有比较两个元素的大小关系的部分的不同的。
比如我们要将一个int类型的数组进行排序,我们是需要比较两个int类型的值的大小关系的,这个可以直接使用<这些关系操作符来实现比较,但如果是两个字符串呢?显然就不能使用这些关系操作符来进行比较的。

那么,这个库函数是如何实现对任意的数组都能进行排序的呢?
这个函数的实现者肯定是无法知悉使用者会对什么类型的数组进行排序,所以是无法实现元素之间比较的函数的,那么谁知道这些元素是如何比较的呢?没错,函数使用者肯定是知道这些元素是怎么比较的,那么元素比较函数就由使用者来实现,让这个qsort函数来调用这个比较函数,这样就能完成排序功能了。
这里就使用函数调用函数的技巧,也就是回调函数。

这里我们来试用一下这个qsort函数

// 由上面的库函数说明中得知 qsort有四个参数
// 第一个形参 base表示待排序数组的起始地址,这里形参的类型为 void *,这是一个无类型的指针,可以接收任意类型的指针,
//     这里并不能使用某些具体类型的指针,因为这个函数无法预知它将要排序什么类型的数据
// 第二个形参 width表示这个数组中一个元素占多少个字节,这个参数是用来帮助函数交换元素的,具体如何使用在下面的冒泡排序版模拟实现中有详细说明
// 第三个形参 num表示这个数组有几个元素
// 第四个形参传入比较函数的指针,这个函数返回值是 int,有两个参数两个参数都是 void*类型
// 参数 1小于参数 2时返回值 <0;参数 1等于参数 2时返回值 =0;参数 1大于参数 2时返回值 >0
// 两个参数的类型为 void*是因为它能接收任意类型的元素

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// 比较两个int类型数据的大小
int int_cmp(const void *a, const void* b)
{
	return (*((int *)a) - *((int *)b));
}

// 比较两个字符串类型数据的大小
int str_cmp(const void* elem1, const void* elem2)
{
	return strcmp((char *)elem1, (char *)elem2);
}

int main()
{
	int ints[5] = { 1, 3, 2, 4, 5 };
	int sz = sizeof(ints) / sizeof(ints[0]);
	printf("排序前:\n");
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("strs[%d] = %d\n", i, ints[i]);
	}
	qsort(ints, sz, sizeof(ints[0]), int_cmp);
	printf("排序后:\n");
	for (i = 0; i < sz; i++)
	{
		printf("strs[%d] = %d\n", i, ints[i]);
	}

	printf("\n");
	char* strs[5] = { "abc", "bcd", "acd", "uqr", "zdf" };
	sz = sizeof(strs) / sizeof(strs[0]);
	printf("排序前:\n");
	for (i = 0; i < sz; i++)
	{
		printf("strs[%d] = %s\n", i, strs[i]);
	}
	qsort(strs, sz, sizeof(strs[0]), str_cmp);
	printf("排序后:\n");
	for (i = 0; i < sz; i++)
	{
		printf("strs[%d] = %s\n", i, strs[i]);
	}

	return 0;
}

运行结果:
在这里插入图片描述

下面我们也尝试使用回调函数来写写代码,实现任意类型的冒泡排序:

这是对int类型数组的冒泡排序函数,我们可以在此基础上修改出我们想要的能排任意类型的函数

void bubble_sort(int* arr, int sz)
{
	int i = 0;
	int j = 0;
	int flag = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

冒泡排序的整体逻辑不变,只需要在元素比较部分和元素交换部分进行修改即可。

#include <stdio.h>

typedef struct
{
	char name[20];
	int age;
	float salary;
} Clerk;

// 排序时交换两个元素
// size表示一个元素所占字节数,虽然排序函数的实现者不知道待排序的元素是什么类型,
// 但只要知道了这个元素占多少字节,直接将两个元素的内存空间中的值一一对换就能实现两个元素的交换
void swap(void* elem1, void* elem2, int size)
{
	int i = 0;
	// 直接将内存中存储的值交换,无需考虑类型
	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)elem1 + i);
		*((char*)elem1 + i) = *((char*)elem2 + i);
		*((char*)elem2 + i) = tmp;
	}
}

// 通过职员的薪水来比较
int clerk_cmp_by_salary(const void* elem1, const void* elem2)
{
	return (((Clerk*)elem1)->salary - ((Clerk*)elem2)->salary);
}

void bubble_sort(void* arr, int size, int sz, int (*cmp)(const void* elem1, const void* elem2))
{
	int i = 0;
	int j = 0;
	int flag = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			// 函数并不知道这里的 arr指针是什么类型,所以通过 arr + j是无法获取到第 j个元素的
			// 但函数是知道一个元素占多少字节,只需要将这个指针转换为 char*类型,
			// 然后向后移动 j * size个字节就能找到第 j个元素
			// 至于从这个指针访问到这个元素的数据,就交给比较函数即可,
			// 此时指针值属性相同,在比较函数中只需通过强制类型转换就能实现访问整个元素的目的
			if (cmp((char*)arr + j * size, (char*)arr + (j + 1) * size) > 0)
			{
				// swap交换函数是由排序代码作者完成的。
				// swap()接收两个 void *类型的参数和一个int类型的参数,
				// 两个指针指向待排序元素的首字节地址,size表示这个元素占几个字节;
				// 虽然排序函数代码的作者并不知道这个元素是什么类型,
				// 但是交换两个元素只需要将这两个元素的内存中的值全部按顺序交换即可,
				// 所以这个交换函数可以由排序函数代码的作者实现。
				swap((char*)arr + j * size, (char*)arr + (j + 1) * size, size);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

void print_clerks(Clerk* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("第%d个职员:\n", i + 1);
		printf("name = %s\n", arr[i].name);
		printf("age = %d\n", arr[i].age);
		printf("salary = %f\n", arr[i].salary);
	}
}

int main()
{
	Clerk clerks[3] = { { "zhangsan", 22, 30000.0 },
						{ "lisi", 24, 25000.0 },
						{ "wangwu", 21, 40000.0 } };
	int sz = sizeof(clerks) / sizeof(clerks[0]);
	printf("排序前:\n");
	print_clerks(clerks, sz);
	printf("\n");
	bubble_sort(clerks, sizeof(clerks[0]), sz, clerk_cmp_by_salary);
	printf("排序后:\n");

	print_clerks(clerks, sz);


	return 0;
}

运行结果:
在这里插入图片描述
这里是通过职员的薪资来排序的,你也可以提供你自己的比较函数,来以你想要的方式来进行排序。如果你想要降序排序,只需将你的比较函数的返回值反过来即可,即:elem1大于elem2返回<0 …。

九、指针和数组的笔试题

代码1

int main()
{
	// 一维数组
	int a[] = { 1,2,3,4 }; // 数组 a使用完全初始化,数组大小为 4
	printf("%d\n", sizeof(a)); // sizeof(<数组名>)这是数组名使用的一种特殊情况,此时数组名表示整个数组,这是在计算整个数组所占内存大小,16 
	printf("%d\n", sizeof(a + 0)); // a + 0,此时的数组名在参与运算,不属于特殊情况,所以表示首元素地址,这是在计算地址所占内存大小,4/8
	printf("%d\n", sizeof(*a)); // *a这是首元素,是在计算元素所占内存大小,元素类型为int,4
	printf("%d\n", sizeof(a + 1)); // 和 a + 0相同,在计算地址占内存的大小,4/8
	printf("%d\n", sizeof(a[1])); // 第二个元素,计算元素大小,4
	printf("%d\n", sizeof(&a)); // 数组的地址,计算地址占内存大小,4/8
	printf("%d\n", sizeof(*&a)); // &a表示数组的地址,对这个地址解引用,拿到整个数组,计算数组的大小,16
	printf("%d\n", sizeof(&a + 1)); // 数组地址 + 1,跳过整个数组,但仍是地址,4/8
	printf("%d\n", sizeof(&a[0])); // 首元素地址,4/8
	printf("%d\n", sizeof(&a[0] + 1)); // 首元素地址 + 1,第二个元素的地址,4/8

	return 0;
}

运行结果:
在这里插入图片描述

代码2

#include <stdio.h>

int main()
{
	// 字符数组
	char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
	printf("%d\n", sizeof(arr)); // 计算整个数组所占内存空间大小,6
	printf("%d\n", sizeof(arr + 0)); // 地址,4/8
	printf("%d\n", sizeof(*arr)); // 首元素,char类型占内存空间的大小,1
	printf("%d\n", sizeof(arr[1])); // 第二个元素,占内存空间的大小,1
	printf("%d\n", sizeof(&arr)); // 数组的地址,4/8
	printf("%d\n", sizeof(&arr + 1)); // 数组的地址往后移动一个数组的大小处的地址,4/8
	printf("%d\n", sizeof(&arr[0] + 1)); // 首元素地址往后移动一个元素大小,第二个元素的地址,4/8

	return 0;
}

运行结果:
在这里插入图片描述

代码3

#include <stdio.h>
#include <string.h>

int main()
{
	// 字符数组
	char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
	printf("%d\n", strlen(arr)); // 从 arr数组首元素开始计算字符串长度,没有 \0,随机值
	printf("%d\n", strlen(arr + 0)); // 与上面一样
	printf("%d\n", strlen(*arr)); // *arr是首元素,是一个char类型的数据,不是指针,将char类型的值当作指针,err
	printf("%d\n", strlen(arr[1])); // 同上,err
	printf("%d\n", strlen(&arr)); // 将数组的地址传给char *类型的形参,和第一个一样,随机值
	printf("%d\n", strlen(&arr + 1)); // 比上面的随机值小6,这是跳过了一个数组的长度
	printf("%d\n", strlen(&arr[0] + 1)); // 比上面的随机值小1,跳过了一个元素

	return 0;
}

在这里插入图片描述

代码4

int main()
{
	// 字符数组
	char arr[] = "abcdef"; // 此时数组 arr中有七个元素,不要忘记最后的 \0
	printf("%d\n", sizeof(arr)); // 数组大小,7
	printf("%d\n", sizeof(arr + 0)); // 地址大小,4/8
	printf("%d\n", sizeof(*arr)); // 首元素大小,1
	printf("%d\n", sizeof(arr[1])); // 元素大小,1
	printf("%d\n", sizeof(&arr)); // 地址大小,4/8
	printf("%d\n", sizeof(&arr + 1)); // 地址大小,4/8
	printf("%d\n", sizeof(&arr[0] + 1)); // 地址大小,4/8

	return 0;
}

运行结果:
在这里插入图片描述

代码5

#include <stdio.h>
#include <string.h>

int main()
{
	// 字符数组
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr)); // 字符串长度,6
	printf("%d\n", strlen(arr + 0)); // 同上,6
	printf("%d\n", strlen(*arr)); // *str是一个char类型数据,err
	printf("%d\n", strlen(arr[1])); // 同上,err
	printf("%d\n", strlen(&arr)); // 地址是数组的起始位置,数组中字符串的长度,6
	printf("%d\n", strlen(&arr + 1)); // 随机值
	printf("%d\n", strlen(&arr[0] + 1)); // 5

	return 0;
}

在这里插入图片描述

代码6

#include <stdio.h>

int main()
{
	char *p = "abcdef";
	printf("%d\n", sizeof(p)); // 地址的大小,4/8
	printf("%d\n", sizeof(p + 1)); // 地址的大小,4/8
	printf("%d\n", sizeof(*p)); // p指向元素的大小,这是将字符串首元素地址赋给了p,所以p指向元素为char,1
	printf("%d\n", sizeof(p[0])); // 等价于*(p + 0),是计算指针指向元素的大小,1
	printf("%d\n", sizeof(&p)); // 指针p的地址,计算地址的大小,4/8
	printf("%d\n", sizeof(&p + 1)); // 计算地址的大小,4/8
	printf("%d\n", sizeof(&p[0] + 1)); // 地址大小,4/8

	return 0;
}

运行结果:
在这里插入图片描述

代码7

#include <stdio.h>
#include <string.h>

int main()
{
	char* p = "abcdef";
	printf("%d\n", strlen(p)); // 计算字符串长度,6
	printf("%d\n", strlen(p + 1)); // 从'b'字符开始计算字符串长度,5
	printf("%d\n", strlen(*p)); // err
	printf("%d\n", strlen(p[0])); // err
	printf("%d\n", strlen(&p)); // p指针的地址,计算从这个地址开始的字符串的长度,完全未知,随机值
	printf("%d\n", strlen(&p + 1)); // 同上,随机值,与上面的随机值没有关系,&p这个地址中可能有 \0,也可能没有
	printf("%d\n", strlen(&p[0] + 1)); // 这是从'b'字符开始计算字符串长度,5

	return 0;
}

在这里插入图片描述
注意这里的&p&数组名的区别,&p是一个完全未知的位置,&数组名还是数组的起始位置。

代码8

#include <stdio.h>

int main()
{
	// 二维数组
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a)); // 数组名在 sizeof中表示整个数组,这是在计算整个数组的大小,3 * 4 * 4 = 48
	printf("%d\n", sizeof(a[0][0])); // a[0][0]表示元素,int类型,4
	printf("%d\n", sizeof(a[0])); // 这是这个二维数组中的第一行数组,是一个数组名,4 * 4 = 16
	printf("%d\n", sizeof(a[0] + 1)); // 第二个数组的地址,4/8
	printf("%d\n", sizeof(*(a[0] + 1))); // 对一维数组的地址解引用,得到一个int类型的元素,4
	printf("%d\n", sizeof(a + 1)); // 地址,4/8
	printf("%d\n", sizeof(*(a + 1))); // a是二维数组首元素的地址,+1之后类型不变,所以这是在计算这个地址指向类型所占内存的空间,一行,4 * 4 = 16
	printf("%d\n", sizeof(&a[0] + 1)); // 地址,4/8
	printf("%d\n", sizeof(*(&a[0] + 1))); // &a[0] + 1这个地址指向一行,4 * 4 = 16
	printf("%d\n", sizeof(*a)); // a表示首元素地址,*a拿到首元素,首元素是一行数组,4 * 4 = 16
	printf("%d\n", sizeof(a[3])); // 同上,虽然从代码上看会越界,但它的类型属性和一行数组相同,结果同样是16
	 							// 并且,sizeof在计算操作数的大小时,并不会计算其中表达式,所以这里并没有发生越界访问。

	return 0;
}

运行结果:
在这里插入图片描述
注意:其中的易错点

  1. sizeof(a[0]),这里的a[0]是二维数组的首元素,所以a[0]其实是一个一维数组,那么a[0]就是这个数组的名字,也符合数组名的使用规则。
    在这里插入图片描述
  2. sizeof(*(a[0] + 1)),同样的要记住a[0]是一个一维数组名,直接运算,表示的是它首元素的地址,这是就是一个int类型值的地址,所以最后解引用得到的也是一个int类型值,所以结果为4。这里较容易将a[0]当成一个数组,+ 1跳过一个一维数组,然后,哎呀,解引用得到的还是一个一维数组,这就错了。
    上图同样可以证明这一点。

在判断sizeof计算的结果时,一定要抓住计算的元素类型是什么。

再次重复数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组
  2. &数组名,这里的数组名表示整个数组
  3. 其它情况下,数组名表示数组首元素地址

笔试题

1.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	int a[5] = { 1,2,3,4,5 };
	int *ptr = (int *)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));

	return 0;
}

分析:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述

2.下面代码会输出什么呢?

#include <stdio.h>

typedef struct {
	int num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
} Test;

int main()
{
	// 假设 p的值为0x100000。如下的表达式的值分别是多少?
	// 结构体在内存中占 20/32字节
	Test *p = (Test*)0x100000;
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);

	return 0;
}

分析:32位机器下
p是一个结构体指针,占内存空间20字节,指针 + 1,地址移动步长为1个结构体的大小,也就是20字节,所以p + 0x1输出00100014
将p强制转换为unsigned long类型,+1直接进行算术运算即可,所以(unsigned long)p + 0x1,在内存的值为00100001
将p强制转换为usigned int *类型,此时p指针 + 1会跳过一个int类型占内存空间长度,所以(unsigned int*)p + 0x1输出00100004

运行结果:
在这里插入图片描述

3.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	int a[4] = { 1,2,3,4 };
	int *ptr1 = (int *)(&a + 1);
	int *ptr2 = (int *)((int)a + 1);
	printf("%x,%x\n",ptr1[-1], *ptr2);

	return 0;
}

分析:
在这里插入图片描述
ptr1[-1]等价于*(ptr1 - 1),所以它输出的值为4。
ptr2指向的位置如图所示,这里就需要考虑int数值在内存中是如何存储的了,当前使用机器是小端字节序,所以这个数组在内存中的值如下
在这里插入图片描述
还原成int类型的数值为02000000,所以最终的输出结果以十六进制的格式输出,应该为4,2000000

运行结果:
在这里插入图片描述

4.下面代码会输出什么呢?

#include <stdio.h>
int main()
{
	int a[3][2] = { (0, 1),(2, 3),(4, 5) };
	int *p;
	p = a[0];
	printf("%d\n", p[0]);

	return 0;
}

注意,这里是使用了逗号表达式来初始化数组,这里只初始化了3个元素,是不完全初始化。初始化结果为:
在这里插入图片描述
这里的a[0]是图中第一个一维数组的数组名,在这里表示这个一维数组的首元素地址,所以指针p指向1,p[0]看作*(p + 0),所以这里输出1。

运行结果:
在这里插入图片描述

5.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	int a[5][5]; 
	int (*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

	return 0;
}

分析:
在这里插入图片描述
在这里插入图片描述
指针相减得到这两个指针之间的元素个数,所以它的结果为-4,以地址格式输出为fffffffc,以int类型输出为-4

运行结果:
在这里插入图片描述

6.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
	int *ptr1 = (int *)(&aa + 1);
	int *ptr2 = (int *)(*(aa + 1));
	printf("%d,%d\n", *(ptr1 - 1), *(ptr2 - 1));

	return 0;
}

分析:
在这里插入图片描述
所以程序的输出应该为10,5

运行结果:
在这里插入图片描述

7.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	char *a[] = { "work", "at", "alibaba" };
	char **pa = a;
	pa++;
	printf("%s\n", *pa);

	return 0;
}

分析:
在这里插入图片描述
pa++,将跳过一个char *类型的空间,也就是pa此时指向a[1]处,所以这个程序就是输出了a[1]这个字符串。

运行结果:
在这里插入图片描述

8.下面代码会输出什么呢?

#include <stdio.h>

int main()
{
	char *c[] = { "ENTER", "NEW", "POINT", "FIRST" };
	char **pc[] = { c + 3, c + 2, c + 1, c };
	char ***ppc = pc;
	printf("%s\n", **++ppc);
	printf("%s\n", *--*++ppc+3);
	printf("%s\n", *ppc[-2]+3);
	printf("%s\n", ppc[-1][-1]+1);

	return 0;
}

分析:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
printf("%s\n", **++ppc):此时先将ppc往后移动一个char **的长度,pcc指向pc[1],*++ppc拿到pc[1]的值,*++ppc此时指向c[2],再解引用*++ppc,得到c[3]这个字符指针,所以这条语句将输出POINT

printf("%s\n", *--*++ppc+3),此时的执行顺序为((*(--(*(++ppc))))+3),ppc在计算前指向pc[1]:

  1. ++ppc后,ppc指向pc[2];
  2. *(++ppc),拿到pc[2]的值,*(++ppc)此时指向c[1];
  3. --(*(++ppc)),此时指向c[0];
  4. *(--(*(++ppc))),此时获得c[0]的值,也就是ENTER的地址;
  5. (*(--(*(++ppc)))+3),上一步得到了ENTER的首元素的’E’的地址,再+3,此时该指针指向’T’后的’E’,所以输出ER

printf("%s\n", *ppc[-2]+3),此时ppc指向pc[2],*ppc[-2]+3,转换为*(*(ppc - 2)) + 3

  1. ppc - 2指向pc[0];
  2. *(ppc - 2),指向c[3];
  3. *(*(ppc - 2)),指向FIRST
  4. *(*(ppc - 2)) + 3,指向ST;所以这条语句输出ST;

printf("%s\n", ppc[-1][-1]+1),此时ppc仍指向pc[2],将这条语句转换为*(*(ppc - 1) - 1) + 1

  1. ppc - 1指向pc[1];
  2. *(ppc - 1)指向c[2];
  3. *(ppc - 1) - 1指向c[1];
  4. *(*(ppc - 1) - 1)指向NEW
  5. *(*(ppc - 1) - 1) + 1指向EW;所以这条语句输出EW

运行结果:
在这里插入图片描述


总结

本章节对c语言中会遇到的所有指针和数组内容作出了详细的介绍,并对一些易错点作出了总结,在最后列出了一些笔试题,并对它们进行了详细的说明。希望这篇文章对你的c语言指针学习有帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值