一次学透C指针C进阶

Posted 林慢慢i

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一次学透C指针C进阶相关的知识,希望对你有一定的参考价值。

前言:本章主要内容是C指针进阶部分,旨在一次性讲透指针,主要以程序代码块形式讲解。有些难度,坚持住兄弟们!


不知道为啥直接引入目录显示不全,只能插个目录图片了。
在这里插入图片描述


数组与指针

实例1
	char* ps = "hello";
	char arr[] = "hello";

完成以上赋值后,进行如下输出,思考下输出结果?

printf("%c\\n", *ps);
printf("%s\\n", ps);
printf("%s\\n", arr);

答案:分别输出h、hello、hello,第一个h是因为*ps指针指向的就是hello的首字符h,故输出h;后俩个输出本质上一样的,数组名arr和ps一样都是hello的首字符地址,故均输出hello。



实例2

思考下列程序输出结果是两个都相同呢?还是存在不同?(题选自剑指offer)


    char str1[] = "hello";
    char str2[] = "hello";
    const char* str3 = "hello";
    const char* str4 = "hello";
    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");

在这里插入图片描述

答案:str1和str2不同,str3和str4相同。因为str1[]、str2[]两个数组分别创建了一块空间,分别放了hello,而数组名又代表首元素地址,str1和str2分别指向两个hello的首地址,这俩地址不同,故str1和str2不同;而str3和str4指向的hello是常量字符串的首元素地址,该字符串不能被修改,内存中只存一份,故str3和str4相同。



指针数组

定义:本质上是数组,但数组中存放的是指针(地址)

int* arr[3];//存放整形指针的数组
实例1
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]));
}

解析:arr[i]内存放的分别是a、b、c的地址,想要打印出具体数字就需要对arr[i]使用解引用操作符*



实例2
int a[5] = { 1,2,3,4,5 };
int b[] = { 2,3,4,5,6 };
int c[] = { 3,4,5,6,7 };

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

在这里插入图片描述

解析:int* arr[3] = {a,b,c};相当于int* arr[3] 里边三个指针类型的元素分别指向数组a、数组b、数组c的首地址。输出结果将分别输出数组a、b、c所有元素。



数组指针

定义:是一种指针,是指向数组的指针

实例1

怎么取一个数组的地址,直接用数组名行不行?

int arr[10]={1,2,3};
arr;
&arr;
int (*parr)[10] = &arr;

注释:“&arr”取出的是数组的地址,“arr”是数组首元素的地址,二者本质上不同,但是打印出来显示的值是一样的。parr就是一个数组指针,parr中存放的就是数组的地址。

思考下对于存放以下指针数组应该用什么样子的数组指针

double* d[5];

应该用:

double* (*pd)[5] = &d;//ok pd就是一个数组指针

在这里插入图片描述



实例2

对于数组arr[]而言,arr与&arr意义完全是不一样的,以下代码感受以下:

	int arr[10] = {0};

	int* p1 = arr;
	int (*p2)[10] = &arr;

	printf("%p\\n", p1);
	printf("%p\\n", p1+1);

	printf("%p\\n", p2);
	printf("%p\\n", p2+1);

在这里插入图片描述

注释:p1是整形指针,+1会跳过四个字节,p2是一个数组指针,+1会跳过一个数组。

再提一次,数组名是数组首元素的地址,但是有2个例外:

  1. sizeof(数组名) - 数组名表示整个数组,计算的是整个数组大小,单位是字节
  2. &数组名 - 数组名表示整个数组,取出的是整个数组的地址


实例3

数组指针的数组遍历:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int (*pa)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
	printf("%d ", *((*pa) + i));
}

在这里插入图片描述

但是多多少少有点麻烦,没必要用到数组指针,单纯使用指针int *pa=&arr就足够了。



实例4

二维数组通过数组指针传参

void print2(int(*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\\n");
	}
}

int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7 } };
    
	print2(arr, 3, 5);//arr数组名,表示数组首元素的地址
	return 0;
}

在这里插入图片描述

解析:二维数组的数组名表示首元素的地址,而二维数组的首元素是:第一行。二维数组把每一行看成一个元素。int(*p)[5]是一个指向一维数组的指针,每个数组有5个元素,每个元素都是int类型。

区分下以下数据类型:

int arr[5] 整型数组

int * parr1[10] 整型指针的数组

int ( * parr2)[10] 数组指针,该指针能够指向一个数组,数组有10个元素,每个元素的类型是int

int ( * parr3[10])[5] 一个存放数组指针的数组,能存放10个数组指针,每个数组指针指向一个数组,数组有5个元素,每个元素是int类型,图片拆解如下:

在这里插入图片描述



一维数组参数

判断哪些传参类型可以?哪些不行?

在这里插入图片描述

答案:都正确,这里分析下最后一个二级指针来传递指针函数

在这里插入图片描述



二维数组传参

判断哪些传参类型可以?哪些不行?

在这里插入图片描述

答案见下图(直接手写分析了):

在这里插入图片描述



插播内容(非指针内容),让大家大脑休息下,如下代码思考输出是>还是<呢?
i--;
if (i > sizeof(i))//
{
    printf(">\\n");
}
else
{
    printf("<\\n");
}

第一眼看是不是觉得一点难度都没有,i没有初始化默认是0,i–执行后i的数值是-1,sizeof(i)得到4,-1<4所以认为输出是<,但很遗憾,你成功被坑了。

解析:因为sizeof()得到的数据类型是无符号整型unsigned int,所以-1直接转换成无符号整形的形式,变成一个巨大的数字,所以结果是>。



一级指针传参

void test(char* p)
{
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//p是一级指针
	print(p, sz);
	
	char ch = 'w';
	char* p1 = &ch;
	test(&ch);//char*
	test(p1);

	return 0;
}

解析:遇见函数形式参数类型为一级指针(比如char*)时要知道实际参数传递过来的是地址。



二级指针传参

二级指针的3种传参方式,如下程序所示。

void test(int** p2)
{
	**p2 = 20;
}

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	//怎么把二级指针进行传参呢?
    
	test(ppa);
    printf("%d\\n", a);//
	test(&pa);
    printf("%d\\n", a);//

	int* arr[10] = {0};
	test(arr);
	printf("%d\\n", a);//

	return 0;
}

解析:pa是一级指针,ppa是二级指针,test(ppa)把二级指针ppa进行传参;test(&pa)传一级指针变量的地址;test(arr)传存放一级指针的数组,传递了arr数组名,数组名代表首元素的地址,而arr数组每个元素都是int *类型,int *的地址相当于int * *,这样一来,符合咱们 void test(int ** p2)中形参的要求,也可以传递。



函数指针

定义:存放函数地址的指针

程序1
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//&函数名 - 取到的就是函数的地址
	//pf就是一个函数指针变量
	int (*pf)(int, int) = &Add;

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

在这里插入图片描述

解释:函数名Add和&Add的效果是一样的。&函数名 - 取到的就是函数的地址;函数名-取到的也是函数的地址。



程序2
int main()
{
	//pf就是一个函数指针变量
	//int (*pf)(int, int) = &Add;

	int (*pf)(int, int) = Add;//Add === pf
	int ret1 = (*pf)(3, 5);//方式1
	int ret2 = pf(3, 5);//方式2
	int ret3 = Add(3, 5);//方式3 
	//int ret4 = * pf(3, 5);//err

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

在这里插入图片描述

解释:int ret1 = (*pf)(3, 5); int ret2 = pf(3, 5);int ret3 = Add(3, 5);三种调用方式效果一样,int ( *pf )(int, int) = Add;Add能把地址放进pf,说明两者一样的,Add==pf。方式1和方式2一模一样,那个解引用操作符 * 没有任何作用,就是一个摆设之所以放着 * 就是为了便于理解。



OK,fine,到这里咱们进行一个小总结!

在这里插入图片描述
在这里插入图片描述



阅读两段有趣的代码(程序选自<c陷阱与缺陷>):

程序1
(*(void(*)())0)();

在这里插入图片描述

调用0地址处的函数
该函数无参,返回类型是void

  1. void(*)( ) - 函数指针类型
  2. (void(*)( ))0 - 对0进行强制类型转换,被解释为一个函数地址
  3. *(void( * )( ))0 - 对0地址进行了解引用操作
  4. ( *(void( * )( ) )0)( ) - 调用0地址处的函数


程序2
void (* signal(int, void(*)(int) ) )(int);
  1. signal 和()先结合,说明signal是函数名
  2. signal函数的第一个参数的类型是int,第二个参数的类型是函数指针
    该函数指针,指向一个参数为int,返回类型是void的函数
  3. signal函数的返回类型也是一个函数指针
    该函数指针,指向一个参数为int,返回类型是void的函数
    signal是一个函数的声明

在这里插入图片描述



函数指针数组

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = Sub;
	int (*pfArr[2])(int, int) = {Add, Sub};//pfArr就是函数指针数组

	return 0;
}

解析:int (*pfArr[2])(int, int) = {Add, Sub};函数指针数组,用于存放同类型的函数指针。



设计一个计算器,计算整型变量的加减乘除。

计算器解法1

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 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;
	//计算器-计算整型变量的加、减、乘、除
	//a&b a^b a|b a>>b a<<b a>b

	do {
		menu();

		//pfArr就是函数指针数组
		//转移表 - 《C和指针》

		int (*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);//2

		if (input >= 1 && input <= 4)
		{
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = (pfArr[input])(x, y);
			printf("ret = %d\\n", ret);
		}
		else if(input == 0)
		{
			printf("退出程序\\n");
			break;
		}
		else
		{
			printf("选择错误\\n");
		}
	} while (input);
	return 0;
}
回调函数

定义:回调函数就是一个通过函数指针调用的函数。 如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时, 我们就说这是回调函数

以上是关于一次学透C指针C进阶的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶笔记深入了解进阶指针

C语言进阶笔记深入了解进阶指针

C语言进阶笔记深入了解进阶指针

C语言进阶笔记深入了解进阶指针

C语言进阶学习笔记二指针的进阶(重点必看+代码演示+练习)

C语言进阶学习笔记二指针的进阶(重点必看+代码图解+练习)