深度解刨 —— 进阶指针(C语言进阶)(万字长文)

前言

指针(指针变量)是C语言最强大的功能之一,同时也是最棘手的功能之一,尽管指针很容易被我们误用,但是它的低位是不可动摇的。学好指针能让我们更加灵活的使用C语言,学好指针是学好编程的关键。

我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的 + - 整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

如果还没有看的伙伴可以去前面的初阶指针看看,连接如下:
初阶指针:https://blog.csdn.net/IT_Infector/article/details/120248670?spm=1001.2014.3001.5501

这一篇文章,我们继续探讨指针的高级主题。

一、 字符指针

字符指针有两种用法:

#include<stdio.h>

int main()
{
	//第一种用法 —— 第一种用法是很常见的一个用法
	char ch1 = 'q';
	char* pc = &ch1;

	//第二种用法 —— 不太常见的用法
	char* ch2 = "abcdef";  //"abcdef"为常量字符串 —— 放在常量区
	//上面的代码意思为 —— 把常量字符串的首元素地址放在指针变量ch2中
	printf("%c\n", *ch2);  //打印值为:a
	printf("%s\n", ch2);   //打印值为:abcdef

	return 0;
}


补充:
1.常量字符串是放在静态区的 和 全局变量放在同一位置。
2.上面 打印字符串 和 打印字符 一个没有解引用,一个解引用了,这是为什么呢?
  打印一个字符时需要解引用 —— 因为要打印地址中的内容
  打印字符串时不需要解引用直接传地址 —— 因为C语言就是这样规定的:打印字符串,要传首元素的地址

例题:

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    char *str3 = "hello bit.";
    char *str4 = "hello bit.";
    if(str1 ==str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");
       
    if(str3 ==str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");
       
    return 0;
}


答案为:
str1 and str2 are not same
str3 and str4 are same

解析:
—————————————————————————————————————————————  str3和str4相同。
这里str3和str4指向的是一个同一个常量字符串。
C/C++会把常量字符串存储到单独的一个内存区域 —— 静态区
常量字符串是不能被改变的,所以就统一在静态区开辟一块空间 —— 
当用到相同常量字符串时,直接会在静态区中找到,不会重新开辟一块空间的。
所以当几个指针,指向同一个字符串的时候,他们实际会指向同一块内存。
————————————————————————————————————————————  str1和str2不同
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。

二、 指针数组

首先我们要讨论:指针数组是指针还是数组

int arr1[5] —— 存放整型的数组 —— 整型数组
char arr2[5] —— 存放字符的数组 —— 字符数组

指针数组 —— 存放指针的数组
int* arr3[5] —— 存放整型指针的数组 —— 整型指针数组
char* arr4[5] —— 存放字符指针的数组 —— 字符指针数组

由上面可以得到 —— 指针数组是:数组

第一种(存放整数地址的数组)

#include<stdio.h>

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	//存放整数地址的数组
	int* arr[4] = { &a, &b, &c, &d };
	//利用指针数组打印a, b, c, d的值
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", *arr[i]);  //通过arr[i]找到a, b, c, d的地址 —— 然后解引用
	}
	return 0;
}

在这里插入图片描述

第二种(存放整型数组首元素地址的数组)

#include<stdio.h>

int main()
{
	int arr1[5] = { 1, 2, 3, 4, 5 };
	int arr2[5] = { 2, 3, 4, 5, 6 };
	int arr3[5] = { 3, 4, 5, 6, 7 };
	//存放整型数组首元素地址的数组
	int* arr[3] = { arr1, arr2, arr3 };
	//利用存放整型数组首元素地址的数组打印数组中的数
	int i = 1;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]); //arr[i] = *(arr + i)
			//*(arr[i] + j) =  arr[i][j] —— arr[i]表示找到了数组首元素的地址 
			//—— (arr[i] + j) 表示数组的第j个元素地址 —— 再解引用就可以了
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

第三种(存放常量字符串首元素地址的数组)

#include<stdio.h>
int main()
{
	char* ch1 = "abc";
	char* ch2 = "def";
	char* ch3 = "ghi";
	//存放常量字符串首元素地址的数组
	char* ch[3] = { ch1, ch2, ch3 };
	//利用存放常量字符串首元素地址的数组,打印常量字符串
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s\n", ch[i]); //利用ch[i]找到常量字符串的首元素地址
	}
	return 0;
}

在这里插入图片描述

三、 数组指针

1. 数组指针的定义

首先我们要讨论:数组指针是数组还是指针

int* a —— 整型指针 —— 存放整型的指针
char* b —— 字符指针 —— 存放字符的指针
所以:数组指针 —— 存放数组的指针 —— 为指针

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
p1, p2分别是什么?

答案:
p1为指针数组
p2为数组指针

解析:
————————————————————————————————————————   int* p1[10]
由于 [] 的结合性比 * 的结合性高 —— 所以 p1 先和 [] 结合
 —— 成为数组,又因为数组中存放是指针 —— 所以为指针数组
 
————————————————————————————————————————   int(*p2)[10]
p先和*结合,说明p是一个指针变量 —— 然后指着指向的是一个大小为10个整型的数组。
所以p是一个指针,指向一个数组 —— 叫数组指针。
#include<stdio.h>

int main()
{
	int a = 0;
	int* pa = &a; //pa为整型指针 —— 存放整型的指针

	char b = 0;
	char* pb = &b; //pb为字符指针 —— 存放字符的指针

	int arr[10] = { 0 };
	int(*parr)[10] = &arr;  //parr为数组指针 —— 存放数组的指针

	return 0;
}

2. &数组名VS数组名

数组名:

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

  2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。

#include <stdio.h>
int main()
{
    int arr[10] = { 1,2,3,4,5 };
    printf("%p\n", arr);
    printf("%p\n", &arr);
    printf("----------\n");
    printf("%p\n", arr + 1);   //数组名 + 1 —— 跳过一个元素
    printf("%p\n", &arr + 1);  //&数组名 + 1 —— 跳过整个数组
    return 0;
}


解析:
arr的地址加一 —— 数组的地址跳过 4
&arr的地址加一 —— 数组的地址跳过 40。
有不同的结果是因为:&arr表示的是整个数组的地址,arr表示的是数组第一个元素的地址

在这里插入图片描述

3. 数组指针的使用

3.1 一种比较别扭的使用
#include<stdio.h>

void Print1(int parr[], int sz)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", parr[i]);
	}
}

void Print2(int(*parr)[5], int sz)
{
	int j = 0;
	for (j = 0; j < 5; j++)
	{
		printf("%d ", parr[0][j]);
		//parr[0][i] = (*(parr + 0))[i] = (*parr)[i]
		//可以把parr看做是一个二维数组 —— 这个二维数组只有一行
		//先访问到二维数组中的第一行 —— 然后再访问每行中的元素
		//就可以写成parr[0][i]
	}
}

int main()
{
	int arr[5] = { 1, 2, 3, 4, 5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print1(arr, sz);  //打印数组中的值 —— 一维数组常用的方式
	printf("\n");
	Print2(&arr, sz);  //打印数组中的值 —— 比较别扭的使用
	return 0;
}
3.2 常见的使用
#include<stdio.h>

void Print1(int arr[3][3], int x, int y)    //一般使用
{
	int i = 0;
	for (i = 0; i < x; i++)
	{
		int j = 0;
		for (j = 0; j < y; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

void Print2(int(*parr)[3], int x, int y)  //用数组指针
                                          //二维数组传首元素地址用int(*)[3]类型接收
{
	int i = 0;
	for (i = 0; i < x; i++)
	{
		int j = 0;
		for (j = 0; j < y; j++)
		{
			printf("%d ", parr[i][j]);
			//parr[i][j] = *(*(parr + i)+j) = *(parr + i)[j]
			//(parr + i) —— 表示第(i+1)行一维数组的地址
			//*(parr + i) —— 表示第(i+1)行一维数组首元素地址
			//*(*(parr + i) + j) —— 表示第(i+1)行一维数组第(j+1)个元素
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	Print1(arr, 3, 3);  //打印二维数组
	Print2(arr, 3, 3);  //打印二维数组
	//arr为数组名 —— 是首元素的地址 —— 二维数组首元素地址为一维数组(二维数组是由多个一维数组,组成的)
	//所以二维数组的数组名的类型为int(*)[3] —— 数组指针
	return 0;
}
3.3 解释下面代码的意思
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];


解析:
———————————————  int arr[5]
整型数组 —— 变量arr先和[]结合 —— 表示数组 —— 数组有5个元素,每个元素是int类型

———————————————  int *parr1[10]
指针数组 —— 变量parr先和[]结合 —— 表示数组 —— 数组有10个元素,每个元素是int*类型

———————————————  int (*parr2)[10]
数组指针 —— 变量parr先和 * 结合 —— 表示指针 —— 指针指向一个数组
 —— 这个数组有10个元素 —— 每一个元素是int类型

———————————————  int (*parr3[10])[5]
变量parr先和[]结合 —— 表示数组 —— 这个数组有10个元素 —— 每个元素为一个数组指针
 —— 这个指针指向一个一个数组 —— 这个数组有5元素,每个元素为int类型

画图解析:int (*parr3[10])[5]
在这里插入图片描述

四、 数组参数、指针参数

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

1. 一维数组传参

#include <stdio.h>

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
//传过来的arr2为首元素的地址 —— 而arr2中的元素为整型指针
// —— 接收指针的地址用二级指针
{}

int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

2. 二维数组传参

void test(int arr[3][5])   //ok
{}
void test(int arr[][])     //no
{}
void test(int arr[][5])    //ok
{}

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

void test(int *arr)       //no
{}
void test(int* arr[5])    //no
{}
void test(int (*arr)[5])  //ok —— arr为数组名 —— 是首元素的地址
                          //二维数组首元素地址为一维数组(二维数组是由多个一维数组,组成的)
{}
void test(int **arr)      //no
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);
}

3. 一级指针传参

#include <stdio.h>
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;
    int sz = sizeof(arr)/sizeof(arr[0]);
    //一级指针p,传给函数
    print(p, sz);
    return 0;
}

4. 二级指针传参

#include <stdio.h>
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;
}

五、 函数指针

1. 认识函数指针

我们知道数组名和&数组名是不一样的

那函数名和&函数名是不是一样的呢?

#include<stdio.h>

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

int main()
{
	printf("%p\n", Add);
	printf("%p\n", &Add);
	return 0;
}

在这里插入图片描述
这里我们直接给出:

函数名和&函数名是一个意思 —— 这一点不同于数组和数组名

上面的代码输出的是两个地址,这两个地址是 Add 函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:

int Add(int x, int y)
{
	return x + y;
}
//下面Add1和Add2哪个有能力存放Add函数的地址?
int (*Add1)(int, int);
int *Add2 (int, int);


答案:Add1有能力存放Add函数的地址
解析:
int (*Add1)(int, int) —— 其中的 Add1 和 * 先结合为指针 —— 指针指向的是函数 —— 
函数有两个参数,一个为int类型,另一个为int类型 —— 返回值为int(整数)

2. 简单的使用函数指针

#include<stdio.h>

int Add(int x, int y)  //Add函数
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int (*pf)(int, int) = &Add;  //pf为函数指针 —— 指针指向Add函数
	//int (*pf)(int, int) = Add; —— 上面的一行代码,也可以这样写 —— 因为:&Add和Add表达的意思一样
	int ret = Add(a, b);
	printf("%d\n", ret);
	ret = (*pf)(20, 30);  //利用函数指针调用函数
	//ret = pf(20, 30); —— 其中上面的代码,也可以这样写 —— pf表示函数地址,&Add和Add表达的意思一样
	printf("%d\n", ret);
	return 0;
}


打印值分别为:30 50

3. 练习

解释:下面两行代码的意思。

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


解析:
———————————————————  (*(void (*)())0)();
void (*)() —— 函数指针类型
(void (*)())0 —— 把0强制转换为函数地址
(*(void (*)())0) —— 解引用,调用0地址处的函数
(*(void (*)())0)() —— 调用的函数无参数
这是一次函数的调用 —— 把地址0强制转换成函数地址,解引用地址,调用0出的函数,调用的函数无参数

————————————————————  void (*signal(int , void(*)(int)))(int);
signal(int , void(*)(int)) —— 函数signal有两个参数,一个是int类型,一个是void(*)(int)(函数指针)
剩下的为:void (*)(int) —— 这个为返回类型 —— 函数指针
这是一次函数声明:
函数signal有两个参数,一个是int(整型)类型,一个是void(*)(int)(函数指针)类型
返回值为void (*)(int)(函数指针)类型

代码2太复杂,如何简化:

typedef void(*pfun_t)(int);
//利用typedef关键字 —— 把 void(*)(int)(函数指针)类型 —— 换名为:pfun_t
pfun_t signal(int, pfun_t);

六、 函数指针数组

整型指针 —— 有整型指针数组
字符指针 —— 有字符指针数组
数组指针 —— 有数组指针数组
那函数指针有没有函数指针数组呢?
答案当然是肯定的:有函数指针数组。

函数指针数组的写法:

int (*pf)(int, int)  
//函数指针的写法
//pf指向一个指针,这个指针有两个参数,都为int类型,返回值也为int类型

int (*pfarr[4])(int, int)  
//函数指针数组的写法
//pfarr指向一个数组,这个数组有4个元素,每个元素为一个函数指针类型
//这个指针有两个参数,都为int类型,返回值也为int类型。

函数指针数组的使用

不用函数指针写计算器:

#include<stdio.h>

void menu()
{
	printf("*******************************\n");
	printf("*****   1.add     2.sub   *****\n");
	printf("*****   3.mul     4.div   *****\n");
	printf("*****   0.exit            *****\n");
	printf("*******************************\n");
}

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)
{
	return x / y;
}

int main()
{
	int input = 0;
	do
	{
		int x, y;
		int ret = 0;
		menu();
		printf("请输入选项:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			Sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			Mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			Div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
		    printf("输入错误\n");
		}
	} while (input);
	return 0;
}

使用函数指针写计算器:

#include<stdio.h>

void menu()
{
	printf("*******************************\n");
	printf("*****   1.add     2.sub   *****\n");
	printf("*****   3.mul     4.div   *****\n");
	printf("*****   0.exit            *****\n");
	printf("*******************************\n");
}

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)
{
	return x / y;
}

int main()
{
	int input = 0;
	do
	{
		int x, y;
		int ret = 0;
		menu();
		printf("请输入选项:");
		scanf("%d", &input);
		int (*parr[5])(int x, int y) = { 0, Add, Sub, Mul, Div };
		if (0 == input)
		{
			printf("退出游戏\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*parr[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else
		{
			printf("输入错误\n");
		}
	} while (input);
	return 0;
}

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

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

#include<stdio.h>
 
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*pf)(int, int) = &Add;  //函数指针
	int(*pfarr[4])(int, int) = {0, Add};  //函数指针数组
	int(*(*ppfarr)[4])(int, int) = &ppfarr;  //指向函数指针数组的指针
	return 0;
}

八、 回调函数

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

下面利用计算器进行举例:

#include<stdio.h>

void menu()
{
	printf("*******************************\n");
	printf("*****   1.add     2.sub   *****\n");
	printf("*****   3.mul     4.div   *****\n");
	printf("*****   0.exit            *****\n");
	printf("*******************************\n");
}

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)
{
	return x / y;
}

void Calc(int (*pf)(int, int))
{
	int x, y;
	int ret = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	ret = (*pf)(x, y);
	printf("ret = %d\n", ret);
}

int main()
{
	int input = 0;
	do
	{
		int x, y;
		int ret = 0;
		menu();
		printf("请输入选项:");
		scanf("%d", &input);
		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");
		}
	} while (input);
	return 0;
}
  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT技术博主-方兴未艾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值