C语言进阶:指针的进阶

目录

1. 字符指针

2. 指针数组

3. 数组指针

3.1 数组指针的定义

3.2 &数组名 VS 数组名

3.3 数组指针的使用

4. 数组参数、指针参数

4.1 一维数组传参

4.2 二维数组传参

4.3 一级指针传参

4.4 二级指针传参

5. 函数指针

6. 函数指针数组

7. 指向函数指针数组的指针

8. 回调函数

9. 指针和数组笔试题解析

10. 指针笔试题


指针的主题,我们在 C语言笔记:指针 章节已经接触过了,我们知道了指针的概念:

1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。

3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。

4. 指针的运算。

这个章节,我们继续探讨指针的高级主题。

1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* 

int main()
{
    char ch = 'w';
    char* pc = &ch;
    *pc = 'a';
    printf("%c\n", *pc); // 'a'

    char* p = "abcdef";
    //本质是把首字符'a'的地址(起始地址)存到了p里,而非把整个字符串存进去
    //"abcdef" - 常量字符串(不能改)
    //*p = 'w'; //会报错
    printf("%s\n", p);//"abcdef"
    //%s,则会从首字符开始打印,直到遇到'\0'结束

    //如果想对"abcdef"进行修改,怎么办?
    //放到一个数组里,通过修改数组达到修改字符串的目的
    char arr[10] = "abcdef"; 
    char* p2 = arr;
    *p2 = 'w';
    printf("%s\n", p2);//"wbcdef"

    return 0;
}

注:特别容易认为是把字符串 hello bit 放到字符指针 p 里了,但是本质是把字符串 hello bit 首字符的地址放到了p中。

一道面试题

#include <stdio.h>
int main()
{
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    const char* p1 = "abcdef";//常量字符串,不能被修改,该语句不加const是不严谨的
    const char* p2 = "abcdef";
    if (arr1 == arr2)
        printf("arr1 == arr2\n");
    else
        printf("arr1 != arr2\n");

    if (p1 == p2)
        printf("p1 == p2\n");//p1和p2变量的内容是相同的
    else
        printf("p1 != p2\n");

    return 0;
}

运行结果如下:

解析

str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针,指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

2. 指针数组

指针数组是一个存放指针的数组

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	
	int* arr[3] = { &a, &b, &c };//指针数组 : 储存指针
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(arr[i]));//10 20 30
	}
	
	return 0;
}

指针数组的使用 - 整型示例:

int main()
{
	int arr1[] = { 1,2,3 };
	int arr2[] = { 2,3,4 };
	int arr3[] = { 3,4,5 };
    
    //指针数组的使用 - 整型示例
	int* arr[3] = { arr1, arr2, arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 3; j++)
		{
			printf("%d ", arr[i][j]);//arr[i]获得的是数组名,arr[i][j]表示选定数组的元素
		}
		printf("\n");
	}

	return 0;
}

指针数组的使用 - 字符指针数组示例:

int main()
{
	char* p1 = "zhangsan";
	char* p2 = "lisi";
	char* p3 = "wangwu";

	char* arr[] = { p1, p2, p3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s\n", arr[i]);
	}
	return 0;
}

3. 数组指针

3.1 数组指针的定义

数组指针是指针?还是数组?

答案是:指针

我们已经熟悉:

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。 

下面代码哪个是数组指针(其实就是看 * 与什么结合)

int *p1[10];    //指针数组(整型)
int (*p2)[10];  //数组指针,该指针指向一个数组,数组有5个元素,每个元素的类型是int
//p1, p2分别是什么?

解析:

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后接着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。

//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
int main()
{
	//整型指针 - 指向整型的指针
	int a = 10;
	int* p = &a;
	//字符指针 - 指向字符的指针
	char ch = 'w';
	char* pc = &ch;
	
	//字符数组
	char arr2[5]; 
	char(*pb)[5] = &arr2;
	
	//指针数组
	char* ch[5]; 
	char* (*pd)[5] = &ch; //注意

	//数组指针 = 数组的地址
	int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(* pa)[10] = &arr1;
	//&数组名拿到的是数组的地址
	//pa就是数组指针,该指针指向了一个数组,数组10个元素,每个元素的类型都是int

	return 0;
}

3.2 &数组名 VS 数组名

我们需要理解:下面代码的输出结果是一样的,为什么又说数组名是首元素地址,&数组名是整个数组的地址?

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//arr;      //首元素的地址
	//&arr;     //取出整个数组的地址
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr);
	//三个输出结果是一样的
	return 0;
}

从值的角度看,数组的地址和数组首元素的地址是一样的,但是他们各自的意义是不同的

arr + 1 =>     跳过一个数组元素

&arr  + 1=>   跳过一整个数组

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
	//把arr(首元素地址)赋给一个指针变量:
	
	int* p1 = arr;
	//把整个数组的地址赋给一个变量:
	//p1 + 1;//跳过一个数组元素(4字节)
	
	printf("p1 = %p\n", p1);          //00B3FA30
	printf("p1 + 1 = %p\n", p1 + 1);  //00B3FA34 两者相差4个字节(一个数组元素)

	int(*p2)[10] = &arr;//此时&arr的类型是 int(*)[10]
	//如果用 int *p3 = &arr;会出现警告:int *和 int (*)[10]的间接级别不同
	//p2 + 1;//跳过一个数组
	
	printf("p2 = %p\n", p2);         //00B3FA30
	printf("p2 + 1 = %p\n", p2 + 1); //00B3FA58 两者相差40个字节 (一个数组)

	return 0;
}

3.3 数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址

不常见的用法:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*p)[10] = &arr; //把数组arr的地址赋值给数组指针变量p
    // 等价于 int a[10] = &arr;
	//但是我们一般很少这样写代码,表示数组内元素还不如直接把首元素地址赋值给p
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
        // 等价于 printf("%d ", a[i]);
	}
    return 0;
}

数组指针的使用,一般常见于二维数组

对于二维数组而言,数组名表示第一行的地址,可以用数组指针接受

注意:

对于二维数组arr[3][5],我们要理解成3个int [5]的数组。第一行的地址就是一个一维数组(int arr[5])的地址,用数组指针(*p)[5] 接收。下列代码中:  *(p+i)  相当于拿到了二维数组的一行(实际上是拿到了这一行的数组名),( *(p + i) )[ j ]相当于找到该数组的第j个元素。实际上(*(p + i))[ j ] 完全等价于 *(*(p + i) + j)

//打印一位数组
void print(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		//printf("%d ", arr[i]);   //1 - 编译器自己也会把arr[i]翻译成 2
		printf("%d ", *(arr + i)); //2 - 和1完全等价
	}
}

//打印二维数组
void print1(int arr[3][5], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

void print2(int(*p)[5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf("%d ", (*(p + i))[j]);
            //等价于 printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}

void print3(int(*p)[3][5], int row, int col)
{
	//代码与print1一致
}

//以一维数组的使用作对比,便于理解
int main()
{
	//一维数组
	int arr2[] = { 1,2,3,4,5,6 };
	print(arr2, 6);
	
    //二维数组
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print2(arr, 3, 5);//arr首元素的地址
	//写一个函数 打印arr数组的内容
	//向函数传入数组(首元素地址),行,列
	//注:此时的首元素的地址是指什么?是1的地址,还是第一行的地址?
	//在评论二维数组的时候,先想象二维数组是一维数组
	//对于arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} },看作一维数组时,相当于只有3个元素
	//这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	
	print3(&arr, 3, 5);//&arr取出的整个二维数组的地址

	return 0;
}

练习与总结: 

int main()
{
	int arr[5];           //arr是数组,数组有5个元素,每个元素的类型是int
	int* parr1[10];       //parr1是一个指针数组,数组有10个元素,每个元素的类型是int*
	int(*parr2)[10];      //parr2是一个数组指针,该指针指向了一个数组,数组有10个元素,每个元素的类型是int
	
    int(*parr3[10])[5];
	//parr3先和[]结合,是一个数组,数组名parr3和[10]结合,说明parr3中有10个元素
	//对于int a[10];而言,拿掉数组名和[],剩下的则是数组中每个元素的类型
	//说明parr3[10]的类型是int(*)[5],即数组指针类型
	//结论:parr3是一个有10个元素的数组,其中每个元素是指向了一个大小为5的数组指针,类型为int(*)[5]

	return 0;
}

上述代码中 :

1.parr2 是否等于 &parr1 ?

答案是:No,parr2元素类型是int,而parr1元素类型是int *,如果要将parr1存到parr2中,应该这样定义:int * (* parr2)[10] = &parr1;

2.int(*parr3[10])[5];的图示如下:

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维数组传参

//下面哪些传参是正确的?
void test(int arr[])     //ok
{}
void test(int arr[10])   //ok
{}
void test(int* arr)      //ok
{}
void test2(int* arr[20]) //ok
{}
void test2(int** arr)    //ok 重点理解 : int* 的地址 用二级指针int **接收
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 }; //指针数组,每个元素的类型都是int * => 首元素也是int *类型
	test(arr);
	test2(arr2);//传入的首元素地址,即int *的地址

	return 0;
}

4.2 二维数组传参

void test(int arr[3][5]) //ok
{}
void test(int arr[][])  //err
{}
void test(int arr[][5]) //ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)     //err,不能用int* 接收二维数组一行的地址
{}
void test(int* arr[5])  //err, int* arr[5]是一个指针数组,每个元素类型是int *
{}
void test(int(*arr)[5]) //ok 把第一行的地址存进去了
{}

void test(int** arr)    //完全错误,二级指针用于存放一级指针变量的地址
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);//第一行的地址 - 一维数组的地址
}

总结:二维数组传参,函数形参的设计只能省略第一个[ ]的数字(多维数组也是如此),
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

4.3 一级指针传参

void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;   //p中存储的arr首元素地址,用int *p接收
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数

比如:

void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
void test1(int* p)  //整形的指针 : 需要传入一个整型的地址
{}
void test2(char* p) //同理,需要传入一个字符的地址
{}

int main()
{
	int a = 10;
	int* ptr = &a;
	int arr[10];

	test1(&a);   //ok
	test1(ptr);  //ok
	test1(arr);  //ok

	char ch = 'w';
	char* pc = &ch;
	char arr2[5];
	
	test2(&ch);  //ok
	test2(pc);   //ok
	test2(arr2); //ok

	return 0;
}

4.4 二级指针传参

void test(int** ptr)
{
    printf("num = %d\n", **ptr);
}
int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数

void test(char** p)
{}
int main()
{
	char c = 'b';
	char* pc = &c;
	char** ppc = &pc;
	char* arr[10];
	test(&pc);
	test(ppc);
	
	test(arr);
	
	return 0;
}

5. 函数指针

是指针,指向函数的指针,那么如何得到函数的地址呢?

//与数组指针作对比,理解函数指针
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*p)[10] = &arr; //p为数组指针,取出数组的地址:(*p)表示p是一个指针,(*p)[10]表示p指向一个数组,该数组有10个元素,每个元素类型位int

	//函数指针:
	//是一个指向函数的指针 - 存放函数的地址
	//那么如何拿到函数的地址?
	printf("%p\n", &Add); //00E013CA
	printf("%p\n", Add);  //00E013CA
	//可见: 函数名是函数的地址, &函数名 还是函数的地址

	int (*pf)(int, int) = &Add;
	//pf为函数指针
	//(*pf)表示pf是一个指针,三个int分别对应 : 返回类型,参数类型,参数类型

	return 0;
}

以模拟strlen函数为例:

#include <stdio.h>
#include <assert.h>

int my_strlen(const char* str)
{
	assert(str != NULL);
	int count = 0;
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

int main()
{
	const char str[7] = "abcdef";
	int ret1 = my_strlen(str);
	printf("%d\n", ret1); //6

	//该如何定义ps?
	int (* ps)(const char*); 
	ps = my_strlen; //把函数名,即函数的地址放到ps中,ps = my_strlen
	 
	//如何使用函数指针?
	int   ret2 = (*ps)(str);
	//对比ret1 = my_strlen(str);
	int   ret3 = ps(str);

	printf("%d\n", ret2); //6
	printf("%d\n", ret3); //6
	return 0;
}

以模拟strcpy函数为例:

//模拟实现strcoy
char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	char* ret = dest; // 为了记录dest的起始地址
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[20] = { 0 };
	char arr2[] = "hello bit";
	
	//函数指针
	char (*pf)(char*, const char*) = &my_strcpy; //&可省略
	(*pf)(arr1, arr2);//(*pf)可改为pf
	printf("%s\n", arr1);

	return 0;
}

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
示例一的解析:
//(*(void (*)())0)()
//其中 void (*)()    是函数指针类型,括起来放于0的前方表示把0强制类型转换为函数指针类型,即地址类型
//*(void (*)())0,    在前用*则是对该地址进行解引用操作,即调用该函数
//(*(void (*)())0)() 最后的()说明该函数无传参

示例:

//示例
void test()
{
	printf("hehe\n");
}

int main()
{
	printf("%p \n", test); //00E61253该地址即为函数的地址,函数的代码保存在以该地址为起始地址的空间里去了
	(*test)(); //调用test函数,加不加*都行
	(*( void (*)() )(*test)) (); //把test从00E61253的int类型强制类型转换成void (*)()类型,再用*解引用调用该函数,最后的括号说明无传参
	return 0;
}
示例二的解析:
void (*signal(int , void(*)(int)))(int);
//没有函数体,首先判断是函数声明。(另外,函数声明不需要写参数名)
//由于()优先级高于*,signal会先与()结合,该括号内部是参数类型: 整型:int、函数指针类型:void(*)(int)
//剩下的函数指针类型void (*)(int) 为该函数的返回类型
//总结:函数名为signal,函数参数为:整型:int、函数指针类型:void(*)(int),该指针指向的函数参数类型是int,返回类型为void,
//该函数的返回类型也是一个函数指针,该指针指向的函数参数类型是int,返回类型为void
//简单来说 : 该函数调用时传入一个整形和一个函数地址,返回一个函数地址

示例二的简化版 :

//简化版:
typedef void(* pf_t )(int); //把void(*)(int)类型重命名为 pf_t
pf_t signal(int, pf_t); //简化版本

//对typedef的补充:
typedef int* pi_t;   //把int *重命名为pi_t
#define PINT_T int* 

int main()
{
	int*   p1, p2;//p1是指针,p2是整型
	pi_t   p3, p4;//p3,p4都是指针
	PINT_T p5, p6;//p5是指针,p6是整型
	//相当于 int* p5,p6;
}

6. 函数指针数组

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	int* arr[4];//整型指针数组
	
	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = Sub;
	//函数指针数组
	//存放的是函数指针

	//能否把Add和Sub的地址放到同一个数组中? 可以,这样的数组就是函数指针数组
	int (*pf[4])(int, int) = {Add, Sub};
	//pf先与[4]结合,说明是个数组,去掉p[4]不看,可见该数组类型是int (*)(int, int)的函数指针类型
	//此时的pf就是函数指针数组

	return 0;
}

函数指针数组又叫转移表

看下面一段代码:实现计算器的加减乘除

//实现一个能实现加减乘除的计算器,且能多次计算
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)//没有考虑除数为0的情况
{
	return x / y;
}

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 x = 0;
	int y = 0;
	int ret = 0;

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
			case 1:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Add(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 2:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Sub(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 3:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Mul(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 4:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Div(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 0:
				printf("退出计算器\n");
				break;
			default:
				printf("选择错误,请重新选择\n");
				break;
		}
	} while (input);

	return 0;
}

使用函数指针数组的实现:

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)//没有考虑除数为0的情况
{
	return x / y;
}

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 x = 0;
	int y = 0;
	int ret = 0;
	int(*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};//加0是为了让元素下标和元素对应起来

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);

			ret = pfArr[input](x, y);
			printf("结果是 : %d\n", ret);
		}
		else
		{
			printf("选则错误\n");
		}
	} while(input);
	return 0;
}

7. 指向函数指针数组的指针

指向函数指针数组的指针是一个 指针

指针指向一个数组 ,数组的元素都是函数指针

int main()
{
	//引子 : 对于指针数组
	int* arr[10];
	int* pa = &arr; //取出指针数组的地址

	int(*pf)(int, int);        //函数指针
	int(*pfArr[5])(int, int);  //函数指针数组 - 存放函数指针的数组
	
	
	int(*(*ppfArr)[5])(int, int) = &pfArr;
	//&pfArr取出的函数指针数组的地址,ppFArr为指向函数指针数组的指针
	//ppfArr先与*结合,说明是一个指针,再与[5]结合说明该指针指向一个有5个元素的数组
	//此时去掉(*ppfArr)[5],还剩下int(*)(int, int),该类型即为该数组的元素类型

	return 0;
}

8. 回调函数

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

回调函数的示例一:

对于计算器的加减乘除的代码中,我们可以发现case 1 - 4代码基本一致,有些冗余:

switch (input)
		{
			case 1:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Add(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 2:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Sub(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 3:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Mul(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 4:
				printf("请输入两个操作数:>");
				scanf("%d%d", &x, &y);
				ret = Div(x, y);
				printf("结果是 : %d\n", ret);
				break;
			case 0:
				printf("退出计算器\n");
				break;
			default:
				printf("选择错误,请重新选择\n");
				break;
		}

能否把这四个相似 (其他都一样,只有调用加减乘除的函数不同,这几个函数的参数,返回类型都相同) 的代码用一个函数实现?这就需要用到回调函数:

void calc(int (*pf)(int, int)) //参数为函数指针类型,指向调用函数的地址
{
	int x = 0;
	int y = 0;
	int ret = 0;

	printf("请输入两个操作数:>");
	scanf("%d%d", &x, &y);
	ret = pf(x, y);
	printf("结果是 : %d\n", ret);
}
switch (input)
	{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
	}

回调函数的示例二:排序

首先介绍下qsort的使用方法

void qsort(void* base, //接受任意类型的地址
		   size_t num, //有几个元素需要排序
		   size_t width, //如果不传入宽度,则不知道到底传入的元素所占字节大小
		   int (*compare)(const void* elem1, const void* elem2)); //elem1 和 elem2是两个待比较元素的地址
//qsort需要头文件<stdlib.h>和<search.h>
//四个参数分别为 : 目标数组起始位置, 该数组元素个数, 数组中一个元素占几个字节, 比较的方法

需注意qsort的第四个参数:比较方法,通过给出比较方法,即可让qsort实现多种类型的比较

,且比较后,若elem1 <  elem2返回一个负数,相等则返回0,若elem1 > elem2 返回一个正数

例如:整数的比较方法:

int cmp_int(const void* e1, const void* e2) //void* 无类型指针,不能直接解引用操作的,需要强制类型转换
{
	return *(int*)e1 - *(int*)e2; //若e1 > e2,返回一个大于0的数字, 若e1 < e2, 返回一个小于0的数字, 若相等返回0
}

使用qsort对整数排序:

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

int cmp_int(const void* e1, const void* e2) //void* 无类型指针,不能直接解引用操作的,需要强制类型转换
{
	return *(int*)e1 - *(int*)e2; //若e1 > e2,返回一个大于0的数字, 若e1 < e2, 返回一个小于0的数字, 若相等返回0
}

print_arr(int *arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	
	printf("\n");
}

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

	print_arr(arr, sz); // 0 1 2 3 4 5 6 7 8 9
	
	return 0;
}

qsort对结构体(按年龄)排序:

struct S
{
	char name[20];
	int age;
};

int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct S*)e1)->age - ((struct S*)e2)->age;
}

//qsort 排序结构体数组
void test2()
{
	struct S arr[] = { {"zhangsan", 20},{"lisi", 80},{"wangwu", 5} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}

int main()
{
	test2();
	return 0;
}

qsort对结构体(按名字)排序:

int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct S*)e1)->name, ((struct S*)e2)->name);
	//大于返回正数,相等返回0,小于返回负数
}   

void test3()
{
	struct S arr[] = { {"zhangsan", 20},{"lisi", 80},{"wangwu", 5} };
	int sz = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}

int main()
{
	test3();
	return 0;
}

可见:参数使用void*的好处在于,可以接受任意数据类型的地址

//void* 的指针是无具体类型的指针
//其优点在于可以接受任意数据类型的地址
//但void*的指针不能直接+ -整数的操作
//且void*的指针不能直接解引用的操作
int main()
{
	int a = 10;
	int* pa = &a;
	//char* pc = &a; //char* 和 int*不属于同一类型,会报警告

	void* pv = &a;

	return 0; 
}

有了上述知识,即可写出可以对任意元素类型进行排序的冒泡排序代码:

void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1,const void* e2))
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j*width, (char*)(base) + (j+1) * width) > 0) 
			//用char*强制类型转换的原因是,char类型指针,加1跳过的字节数也是1,加4则跳过4个字节
			//如果比较的是int类型,用char*强制类型转换后,每加一次宽度width,则相当于跳过了一个int型元素
			{
				//交换,由于不知道待排序元素类型,故无法创建临时变量辅助交换,此时如何实现交换?
				//交换待排序元素的每一个字节,每个元素共有width个字节
				int k = 0;
				for (k = 0; k < width; k++)
				{
					char tmp = *((char*)base + j * width + k);
					*((char*)base + j * width + k ) = *((char*)(base)+(j + 1) * width + k);
					*((char*)(base)+(j + 1) * width + k) = tmp;
				}
			}
		}
	}
}

交换步骤简化后的冒泡排序总代码:

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

int cmp_int(const void* e1, const void* e2) //void* 无类型指针,不能直接解引用操作的,需要强制类型转换
{
	return *(int*)e1 - *(int*)e2; //若e1 > e2,返回一个大于0的数字, 若e1 < e2, 返回一个小于0的数字, 若相等返回0
}

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

	printf("\n");
}

void _Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *(buf1 + i);
		*(buf1 + i) = *(buf2 + i);
		*(buf2 + i) = tmp;
	}
}

void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)(base)+(j + 1) * width) > 0)
				//用char*强制类型转换的原因是,char类型指针,加1跳过的字节数也是1,加4则跳过4个字节
				//如果比较的是int类型,用char*强制类型转换后,每加一次宽度width,则相当于跳过了一个int型元素
			{
				//交换
				_Swap((char*)base + j * width, (char*)(base)+(j + 1) * width, width);
			}
		}
	}
}

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

	//qsort(arr, sz, 4, cmp_int);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

	print_arr(arr, sz); // 0 1 2 3 4 5 6 7 8 9
}

struct S
{
	char name[20];
	int age;
};

int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct S*)e1)->age - ((struct S*)e2)->age;
}

void test2()
{
	struct S arr[] = { {"zhangsan", 20},{"lisi", 80},{"wangwu", 5} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	
	//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}

int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct S*)e1)->name, ((struct S*)e2)->name);
	//大于返回正数,相等返回0,小于返回负数
}   

void test3()
{
	struct S arr[] = { {"zhangsan", 20},{"lisi", 80},{"wangwu", 5} };
	int sz = sizeof(arr) / sizeof(arr[0]);

	//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}


int main()
{
	
	test1();
	test2();
	test3();

	return 0;
}

9. 指针和数组笔试题解析

数组名表示数组首元素的地址,但是有2个例外:
1. sizeof(数组名), 数组名表示整个数组,计算的是数组的总大小,单位是字节。
2. &数组名, 数组名表示整个数组, &数组名 取出的是整个数组的地址。
除此之外, 其他所有遇到的数组名都表示数组首元素的地址。

一维数组笔试题:

//数组名表示数组首元素的地址
//但是有2个例外
//1. sizeof(数组名), 数组名表示整个数组,计算的是数组的总大小,单位是字节
//2. &数组名, 数组名表示整个数组, &数组名 取出的是整个数组的地址
//除此之外, 其他所有遇到的数组名都表示数组首元素的地址
int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));          //16 整个数组大小
	printf("%d\n", sizeof(a + 0));      //4  首元素地址
	printf("%d\n", sizeof(*a));         //4  首元素
	printf("%d\n", sizeof(a + 1));      //4  2的地址
	printf("%d\n", sizeof(a[1]));       //4  2
	printf("%d\n", sizeof(&a));         //4  数组的地址,但还是4个字节
	printf("%d\n", sizeof(*&a));        //16 取地址和解引用抵消了,相当于sizeof(a)
	printf("%d\n", sizeof(&a + 1));     //4  跳过一个数组后的地址
	printf("%d\n", sizeof(&a[0]));      //4  首元素地址
	printf("%d\n", sizeof(&a[0] + 1));  //4  2的地址
	
	return 0;
}

字符数组笔试题:

int main()
{
	//字符数组
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));          //6  
	printf("%d\n", sizeof(arr + 0));      //4  a的地址
	printf("%d\n", sizeof(*arr));         //1  a
	printf("%d\n", sizeof(arr[1]));       //1  b
	printf("%d\n", sizeof(&arr));         //4  数组的地址
	printf("%d\n", sizeof(&arr + 1));     //4  跳过一个数组后的元素的地址
	printf("%d\n", sizeof(&arr[0] + 1));  //4  b的地址
	
	printf("%d\n", strlen(arr));          //随机值  定义时没写'\0'
	printf("%d\n", strlen(arr + 0));      //随机值
	printf("%d\n", strlen(*arr));         //err     会从a的ASCII码值(97)当做地址去访问,而97并不是一个合法的地址,访问97的地址是一个随便的值 - 野指针
	printf("%d\n", strlen(arr[1]));       //err     同理
	printf("%d\n", strlen(&arr));         //随机值  
	printf("%d\n", strlen(&arr + 1));     //随机值  从f后往后找'\0'
	printf("%d\n", strlen(&arr[0] + 1));  //随机值  从b开始往后走
	
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));          //7       '\0'也要算
	printf("%d\n", sizeof(arr + 0));      //4       首元素a的地址
	printf("%d\n", sizeof(*arr));         //1       a
	printf("%d\n", sizeof(arr[1]));       //1       a
	printf("%d\n", sizeof(&arr));         //4       数组的地址也是地址
	printf("%d\n", sizeof(&arr + 1));     //4       跳过整个数组(包括'\0')后的地址
	printf("%d\n", sizeof(&arr[0] + 1));  //4       b的地址
	
	printf("%d\n", strlen(arr));          //6    
	printf("%d\n", strlen(arr + 0));      //6       首元素地址
	printf("%d\n", strlen(*arr));         //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       b的地址
	
	char* p = "abcdef";
	
	printf("%d\n", sizeof(p));            //4       指针变量p的大小
	printf("%d\n", sizeof(p + 1));        //4       b的地址
	printf("%d\n", sizeof(*p));           //1       a
	printf("%d\n", sizeof(p[0]));         //1       a  p[0] = *(p + 0) = *p
	printf("%d\n", sizeof(&p));           //4       p的地址
	printf("%d\n", sizeof(&p + 1));       //4       p的地址跳过一个char*后的地址
	printf("%d\n", sizeof(&p[0] + 1));    //4       b的地址
	
	printf("%d\n", strlen(p));            //6       
	printf("%d\n", strlen(p + 1));        //5
	printf("%d\n", strlen(*p));           //err
	printf("%d\n", strlen(p[0]));         //err
	printf("%d\n", strlen(&p));           //随机值  p的地址往后走找'\0'
	printf("%d\n", strlen(&p + 1));       //随机值  p的地址跳过一个char*后的地址往后找'\0'
	printf("%d\n", strlen(&p[0] + 1));    //5       b的地址
	return 0;
}

二维数组笔试题:

int main()
{
	int a[3][4] = { 0 };
	printf("%p\n", &a[0][0]);//009FF918
	printf("%p\n", a[0] + 1);//009FF91C
	//两者相差4,说明a[0] + 1表示的是第一行第二个元素

	printf("%d\n", sizeof(a));            //48       整个数组元素的大小12 * 4
	printf("%d\n", sizeof(a[0][0]));      //4
	printf("%d\n", sizeof(a[0]));         //16       对于二维数组而言,a[0]单独放在sizeof内部表示第一行的数组名
	printf("%d\n", sizeof(a[0] + 1));     //4        重点理解 : a[0]并没有单独放在sizeof内部,此时表示的是第一行的第一个元素的地址 + 1
	printf("%d\n", sizeof(*(a[0] + 1)));  //4        第一行第二个元素
	printf("%d\n", sizeof(a + 1));        //4        第二行的地址,即数组的地址。(需要放到一个数组指针中去)
	printf("%d\n", sizeof(*(a + 1)));     //16       对指向第二行的数组指针的解引用
	printf("%d\n", sizeof(&a[0] + 1));    //4        第一行的数组名取地址 : 第一行的地址, 再 + 1 => 第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1))); //16       第二行的大小
	printf("%d\n", sizeof(*a));           //16       对第一行的地址解引用 : 算的是第一行的大小
	printf("%d\n", sizeof(a[3]));         //16       看起来越界了,但sizeof并不会访问那块空间,仅仅计算大小,即如果该二维数组有第四行,那第四行会占多大空间

	return 0;
}

10. 指针笔试题

笔试题1:

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d %d", *(a + 1), *(ptr - 1)); //2 5
    return 0;
}
//程序的结果是什么?

笔试题2:

//程序的结果是什么?
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}* p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);               //00000014
	//0x100000 + 20 
	//0x100014
	
	printf("%p\n", (unsigned long)p + 0x1);//00000001
	//0x100001 整型+1就是+1,没什么

	printf("%p\n", (unsigned int*)p + 0x1);//00000004
	//整型地址 + 1
	//0x100000 + 4
	//0x100004
	return 0;
}

 笔试题3:

//程序的结果是什么?
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int* ptr1 = (int*)(&a + 1);  
    int* ptr2 = (int*)((int)a + 1);
    //注意: 当a从地址强制类型转换成int时再加1,只向前走了一个字节(是整数+1而非整型指针+1)
    //例如 : 假设a地址为0x00000010强制转换成整数 => 16,16 + 1 =>17, 17强制转换成int * => 0x00000011
    //可见, 两者只相差了一个字节
    
    //由于ptr2是int*类型, 此时对其用解引用操作, 会访问4个字节
    //对于数组a[4]:在内存中 : (小端字节序)
    //01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
    //此时访问的是00 00 00 02 即访问的是0x02000000   =>2000000
    
    printf("%x,%x", ptr1[-1], *ptr2);//4 , 2000000
    //ptr1[-1] = *(ptr1 + (-1)) = *(ptr1 - 1) => 0x4 => 4

    return 0;
}

  笔试题4(很坑的题目):

//程序的结果是什么?
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    //如果要把(0,1) ,(2,3), (4 , 5)分别放入二维数组的第一、二、三行,应该这样初始化:
    // int a[3][2] = {
   
   {0 , 1}, {2 , 3}, {4 , 5}};
    //此时{}内是三个逗号表达式, 等价于{1 , 3 , 5};

    //如果三个()改为{}那么输出结果为0

    int* p;
    p = a[0];
    printf("%d", p[0]); //1
   
    return 0;
}

笔试题5:

//程序的结果是什么?

//指针与指针相减,得到的是他们之间的元素个数
//要学会把二维数组看作一位数组处理(因为都是连续存放的)
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]);//fffffffc , -4

    // &p[4][2] - &a[4][2] = -4
    //10000000000000000000000000000100 - 原码
    //11111111111111111111111111111011 - 反码
    //11111111111111111111111111111100 - 补码
    //1111 1111 1111 1111 1111 1111 1111 1100
    //f    f    f    f    f    f    f    c
    return 0;
}

图解:

注 : 1.指针与指针相减,得到的是他们之间的元素个数(可以为负数)
2.要学会把二维数组看作一位数组处理(因为都是连续存放的) 

笔试题6:

//程序的结果是什么?
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", *(ptr1 - 1), *(ptr2 - 1)); //10 , 5
    return 0;
}

笔试题7:

//程序的结果是什么?
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa); //at 画图很好分析
	return 0;
}

笔试题8:

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

	printf("%s\n", *-- * ++cpp + 3); //ER
	//先算++cpp, 解引用得到c+1, 再--  => c ,
	//再解引用=>拿到了ENTER的首字母E的地址 再加3 
	//=>指向E,向后打印字符串 : ER
	
	printf("%s\n", *cpp[-2] + 3);    //ST
	//*cpp[-2] = *(cpp - 2),(之前cpp存的是cp+2,减2后得cp)
	//即 : *cpp[-2] + 3 = **(cpp - 2) + 3 => *cp + 3 =>FIRST的首元素地址 + 3 =>打印结果为ST

	printf("%s\n", cpp[-1][-1] + 1); //EW
	//cpp[-1][-1] + 1 = *(*(cpp - 1) - 1) + 1 (cpp之前存的是cp + 2)
	//*(cpp - 1)相当于对(cp + 2 - 1)进行解引用,拿到的是c+2
	//原式化简为 : *(c + 2 - 1) => *(c + 1) =>NEW的首元素地址 + 1开始打印 =>EW

	return 0;
}

图示: 


如果对本文涉及的代码有需要,可进入我的gitee主页进行下载学习

猜你喜欢

转载自blog.csdn.net/m0_62934529/article/details/123781402