深入C语言2——指针进阶

23 篇文章 0 订阅
19 篇文章 0 订阅

欢迎回来继续C语言的学习,结束数据存储的板块后,来到了C语言里最重要的内容,指针,指针可以说在初学C语言或者学C++的人眼里都是抬头不见低头见的老朋友了,C语言版的数据结构指针也是主角,所以在之前我们讲过指针后要再开一节来专门对于指针进行一些深究,所以大家可以看到我前面的博客可能一篇里包括好几个板块,但是进入到深入C语言后一篇只会围绕一个主题来讲,因为我们到了真正该用心学习的阶段了,我们的思路只应该属于当前,我们这就开始。

1. 字符指针

我们知道指针和变量一样是有很多类型的,不同类型的指针用来指向不同类型的数据的空间,所以显然不同类型的指针所能指向的空间也是有限的,我们先来看字符指针,如果有一个字符串用指针指向时在内存中会是怎样呢?

#cinlude <stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;//pc指向的是一个常量字符
	const char* p = "hello bit";//"hello bit"是一个常量字符串,存放在内存的常量区
	//上面表达式的作用是:把常量字符串"hello bit"的第一个字符h的地址赋值给p
	//*p = 'w';
	printf("%c", *p);
	printf("%s", p);
	return 0;
}

我们这下知道原来字符指针再存入字符串时是指向了存入这个字符串空间的首,并不是讲这个字符串放在了指针内,当我们解引用时拿到的其实就是这个字符串的首字符,当我们用%c打印出来时就是h,用%s来打印就是从地址有效位置一直向后打印至地址无效处。

那就有这么一道面试题:

#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和str2直接进行相比的话其实对比的是两个数组的首元素地址,那当然是不相等的,因为它们两个压根就存在两个不同的空间,而str3和str4虽然是两个指针,但是它们都共同指向了"hello world."这个字符串,所以在内存中它们指向的是同一块内存空间,如果对str3和str4同时解引用的话拿到的就都是首字母'h',所以str3和str4是相等的,它们在共同维护一块空间。

2. 数组指针

这个概念想必大家都是第一次听说,听过数组听过指针,数组指针是什么鬼,你怕不是连读了。在C语言当中确实有数组指针这个概念,我们只读后缀,后缀是什么它就是什么,所以数组指针就是指针,是用来存放数组的指针,可能大家还是比较懵,说白了就是之前的指针都是用来维护一块空间,数组指针就是用来维护一连串连续的空间,这就是数组指针。

那么该如何定义一块数组指针呢?

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

 大家觉得哪个对呢?看这两段代码时我们需要考虑运算符的优先级,我们知道括号的优先级是非常高的,当有括号时括号内部的表达式优先计算,这已经是大家在数学上就已经了解的了,所以*p要先进性结合,所以这已经是一个指针了,再与外面的int [10]结合,所以第二个式子才是数组指针的正确写法。(这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合)

&数组名vs数组名

大家的老朋友出现了,之前反复提到过的数组名的问题,为什么要再来讲呢,是因为后面我们有很多内容都是和数组有关系的,所以感觉还是给大家整体复习一下这块的知识为好。

那么我们知道&数组名是取出整个数组的地址,而只写数组名只是代表首元素地址,当我们定义一个数组然后分别以%p的形式来打印&数组名和数组名时我们会发现它们的结果是一样的,因为它们都代表同一块空间,但是当我们都进行+1运算时我们会发现结果截然不同,假设我们数组的大小为10,那么&数组名+1跳过的就是整个数组,来到了数组最后,如果只是数组名+1的话只是跳过数组类型大小的一个元素。 

#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

好,有了这些准备只是,那数组指针究竟要怎么用呢?既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址,来看代码:

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //只是为了演示,但是我们一般很少这样写代码
    return 0;
}

&arr放进*p中,代表p指针现在就要维护arr整个数组,这就是数组指针的使用方式。

一个数组指针的使用:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}
void print_arr2(int (*arr)[5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

当我们进行数组传参时,可以再形参位置直接写类型+数组[],也可以通过指针的方式进行传参,如果是一个整型类型的数组,我们就用一个整形指针来指向它的首元素地址,然后将这个指针当作参数参入函数中,计算时也是通过这个指针的前移后移来拿到数组中的待操作元素,那么这段代码中是定义了一个二维数组,我们知道二维数组就是一堆数,它有行也有列,所以我们也可以将二维数组当作一维数组,那么它的每个元素存入的就是一个一维数组,*arr代表的就是3行的元素,所以我们就可以用数组指针的方式来进行传参,也可以达到我们成功访问到数组每个元素的效果。

3. 指针数组

我们都知道了当遇到这种两个概念叠一起时就看后缀,这个后缀是数组,所以我们知道指针数组是用来存指针的数组,也可以理解为存入了一堆数组,与一般以为数组不同的只是把里面的数据元素换成了可以指向空间的指针。

那么指针数组如何定义呢?按照刚才我们分析数组指针的方法来想,如果数组指针是需要优先和指针结合,那么指针数组我就让它先和数组结合呗,如何结合呢?去掉括号是不是优先级会被[]霸占,这样arr是不是就优先和[]结合了,也就变成了数组,再和前面的*结合,所以也就实现了我们对指针数组的定义。

char* arr1[5];//arr是存放字符指针的数组
int* arr2[5];//arr是存放整形指针的数组

那么指针数组如何使用呢?

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	int* arr3[4] = { &a,&b,&c,&d };
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", *(arr3[i]));
	}
	return 0;
} 

 如果指针数组是用来存指针的话,我们定义的4个空间大小的指针数组就要用来存入某些变量的地址,因为arr3体内都是指针,所以当我们解引用arr3取到里面的元素时候拿到的也就是该元素的地址,该元素是整形的,所以%d的形式打印时也就可以成功打印。

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	int* parr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);//p[i]==*(p+i)
			//parr[i][j]==*(parr[i]+j)
		}
		printf("\n");
	}
	return 0;;
}

上述代码这就代表了二维指针数组的定义方法,如果说刚刚的代码是存入的变量的地址,那么这段代码中的parr存入的就是数组的地址,也就实现了二维数组的有行也有列,懂了这些后我还有一点需要强调,我们知道arr是代表首元素地址,既然是地址我们也就可以通过地址找到其中的元素,所以当我们打印数组中的元素的时候可以写arr[i],也可以通过地址的解引用*(arr+i),都可以访问到我们想访问的空间,所以我们是否就可以认为arr[i]==*(arr+i)呢?是的,这样确实符合语法规定,那既然这样我们再回到上述代码,打印parr中的元素时是否就可以不写parr[i][j]呢?太low了,我可是学过指针的人,好,我们就按刚刚分析的方法把parr[i][j]转换一下, 我们知道parr存入的是数组的地址,譬如我们需要第二个数组的第三个元素,我们得先找到第一个数组,指针数组的形式是int * parr[]没错,当我们*parr是拿到的是第一个元素,也就是第一个数组的地址,当我们再+j,也就是代表我想访问第二个数组中的第几个元素,所以可以写成*(parr[i]+j),当然每种打印方式都是无高低地位之分的,高效正确地完成我们的像完成的就可以了。

int main()
{
	const char* arr[5] = {"abcdef", "bcdef", "hehe", "haha", "zhangsan"};
	for (int i = 0; i < 5; i++)
	{
		printf("%s\n", arr[i]);
	}
	return 0;
}

这段代码对于我们来说就已经很简单了,每次打印arr[]内的元素都是一串字符,而每串字符又可以表示成一个数组,这就是指针数组的定义方法及使用方法。

了解过这些后我们再来看看这些代码:

int arr[5];//一维数组
int *parr1[10];//parr1先和[]结合,所以parr1是指针数组
int (*parr2)[10];//parr2先和*结合,所以parr2是数组指针
int (*parr3[10])[5];//parr3先和[]结合,再和*结合,再和外面的[]结合,所以parr3是个可以存五个数组指针的数组

4. 数组传参和指针传参

我们讲过了数组指针和指针数组,知道了原来它们两个是可以结合的,那我们再回过头来,看一下我们最初就讲的数组传参,数组传入函数内都有几种形式呢?

//一维数组传参
void test1(int arr[])
{}
void test2(int arr[10])
{}
void test3(int* arr)
{}
void test4(int* arr[20])
{}
void test5(int** arr)
{}
int main()
{
	int arr[10] = { 0 };
	int* arr[20] = { 0 };
	return 0;
}

我们可以看到数组传参有这么多形式,可以直接传arr数组,也可以通过指针的方法传(之前讲过),也可以通过数组指针传,也可以通过二级指针的方式传,这些都可以实现数组的传参,当然建议还是选择简单的方式,毕竟太复杂的方式虽然可以实现,但是还是比较难懂。

//二维数组
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
 return 0;
}

大家注意二维数组除了二级指针外都可以进行传参,因为二级指针是用来存指针地址的指针,而二维数组与二级指针没有直接关系。

void print(int* ptr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d\n", *(ptr + i));
	}
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(p, sz);
	return 0;
}

一级指针传参形参可以用一级指针来接受,道理同数组名传参一样,数组名代表首元素地址,而接收时,就可以拿一级指针来接收。

void test(int *ptr)
{}
int main()
{
	int a = 10;
	int arr[] = { 0 };
	int* p = &a;

	test(&a);
	test(arr);
	test(p);
	return 0;
}
//二级指针传参
void test(int** ppa)
{}
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	int* arr[10];

	test(ppa);
	test(&pa);
	test(arr);
	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;
}

 大家注意arr是一个指针数组,里面存入的都是指针,所以当用数组名直接传参时候传入的是首元素地址,所以需要拿一个指针来接收,所以传arr是正确的。

5. 函数指针

我们讲过了数组指针、指针数组,但是再C语言里还有叫函数指针的,所以我们发现指针真是和谁都能结合,我们又发现是不是函数,数组这些能存入内存块的都是有地址的,所以才能和指针结合,那么函数指针又是个什么东西呢?怎么用呢?有什么用呢?

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

 我们发现当我们以%p的形式打印一个函数的地址时也能想打印数组、变量等获得一样的效果,都在屏幕上打印了十六进制的地址,那么函数确实在内存中是有地址的,那我们可不可以拿一个指针指向它呢?也来维护这块空间防止被改变,答案是肯定的,我们接下来就看看函数指针是怎么定义的。

void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

我们前面讲过了操作符优先级结合的问题,那么我们知道如果现在一个指针想指向一个函数,那么它首先一定要是一个指针,那么pfun1有括号,先和*结合,所以它是一个指针,再和外面的void ()结合,所以pfun1也就代表是一个函数指针,而pfun2先会和()结合,所以它是一个函数,只不过返回值是void*,也就是说pfun2是一个可以返回指针的函数。

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//printf("%p", &Add);
	//printf("%p", Add);
	int(*pf)(int, int) = &Add;//pf是用来存放函数的地址,pf就是函数指针变量
	int ret = Add(4, 5);
	printf("%d\n", ret);
	ret = (*pf)(4, 5);
	printf("%d\n", ret);

	return 0;
}

那么(*pf)(4,5)和Add(4,5)输出的答案是否一样呢?我们知道Add(4,5)就是计算4+5,而pf是指向Add的一个指针,那我们知道通过指针也可以访问到该空间,所以利用pf也可以找到Add函数,所以两种表达式计算的答案是一样的,而第二种表示方法就是利用函数指针实现的调用函数。

这下大家肯定大家会了如何观察许多操作符叠加在一起的情况,那我们再来看两个有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
typedef void(*pfun)(int);
int main()
{
	//这个代码是一次函数声明
	//signal有两个参数,第一个是int,第二个是void(*)()的函数指针类型
	//siganl返回类型依然是void(*)()的函数指针类型
	
	//void(*siganl(int, void(*)(int)))(int);
	pfun(*siganl(int, pfun))(int);

	return 0;
}

int main()
{
	//代码是一次函数调用,代码中把0强制类型转换为void(*)()的一个函数地址
	//*(void(*)())0解引用去调用0地址处的函数,被调函数无参数,返回类型是void
	(*(void(*)())0)();
	return 0;
}

6. 函数指针数组

又进入了一个新概念,函数指针数组,那我们还是照旧按照后缀来理解这个概念,首先它肯定是个数组,然后是用来存函数指针的数组,所以这个数组里的每个元素都指向一个函数。

那么它该如何定义呢?

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

我们知道整形数组的类型是int [],数组指针的类型是int(*)[],指针数组的类型是int*[],函数指针的类型是void(*)(),那么函数指针数组的类型该如何书写呢?是不是在函数指针后面再加上一个数组的标致[]就可以了,所以这个三个式子里只有parr1是正确的,首先[]的优先级要比*高,所以parr1先和[]结合称为数组,再和*结合,代表它存的是指针,最外面则是函数的返回类型和传参所用的括号,这就是函数指针数组的定义方法,那么它究竟有什么用处呢?

我们前面写项目时一个main函数里调用了许多的函数,当需要某些函数的功能时就需要调用它,那学过函数指针数组后可不可以直接用它把这些函数当作数组的元素存起来呢?答案是肯定的,那我们就以一个最简单的计算机来实现

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 meun()
{
	printf("***********************\n");
	printf("****1.Add*****2.Sub****\n");
	printf("****3.Mul*****4.Div****\n");
	printf("****0.exit*************\n");

}
int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		meun();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
            printf("输入你要计算的两个数字:");
            scanf("%d %d",&x,&y);
			Add();
			break;
		case 2:
            printf("输入你要计算的两个数字:");
            scanf("%d %d",&x,&y);
			Sub();
			break;
		case 3:
            printf("输入你要计算的两个数字:");
            scanf("%d %d",&x,&y);
			Mul();
			break;
		case 4:
            printf("输入你要计算的两个数字:");
            scanf("%d %d",&x,&y);
			Div();
			break;
		case 0:
			printf("退出");
			break;
		default:
			printf("error");
			break;
		}
	} while (input);
	return 0;
}

这就是一个简易计算机的实现,我们有没有发现加减乘除都被封装成了一个函数,然后用switch语句进行调用,但是学过函数指针数组后可不可以对它进行一定的优化呢?既然函数指针数组是用来存函数指针的,那么可不可以用指针指向这四个函数,然后再用函数指针数组把它们都存起来呢?如果可以,如何实现呢?

int main()
{
	int* arr[10];//整形指针数组
	//函数指针的数组
    //1.
	int (*pf1)(int, int) = &Add;
	int (*pf2)(int, int) = &Sub;
	int (*pf3)(int, int) = &Mul;
	int (*pf4)(int, int) = &Div;
    //2.
	int (*pfArr[4])(int, int) = { Add,Sub,Mul,Div };//pfArr - 函数指针数组
	return 0;
}

这样就可以实现利用函数指针数组存入函数指针了,注意12方式都可以,只不过2方法要更像数组一些,所以我们可以将它们放进刚刚的计算机里,看看会有什么变化。

int main()
{
	int input = 0;
	do
	{
		int x = 0;
		int y = 0;
		int ret = 0;
		meun();
		printf("请选择:");
		scanf("%d", &input);
		int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
		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;
}

我们只需要针对main函数里进行修改,将switch里的case都分装成函数指针数组的元素,这样0代表元素0,也就是退出,Add代表元素1,Sub代表元素2,Mul代表元素3,Div代表元素4,而当input输入时就可以针对input的值访问到pfArr里的元素,在把x,y传入。

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

好了,套娃又来了,学习了函数指针数组后,我们发现原来函数指针指针函数可以互相套,这次是在函数指针数组外部再讨一个指针,意思即创建一个指针指向函数指针数组,也就是说通过这个指针可以访问到函数指针数组里的元素,也当然可以进行修改删除等操作。

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int arr[10];
	int* arr[10];//整形指针数组
	int* (*pa)[10] = &arr;//整形指针数组的指针
	int (*pfArr[5])(int,int) = &Add;//指向函数的指针数组
	int (*(*ppfArr)[5])(int, int) = &pfArr;//指向函数指针数组的指针
	return 0;
}

我们通过定义方法就能发现指向函数指针数组的指针是在函数指针数组中的基础上外面又套了一层指针,也就是ppArr先和[5]结合,代表是一个数组,再和*结合代表是指针数组,再和外面的*结合,代表可以指向一个函数指针,也就可以存入pfArr的地址。

void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[10])(const char*) = &pfunArr;
 return 0;
}

这些就是函数、数组、指针之间的互相嵌套利用,需要大家下来细细品味,搞懂这些操作符之间优先级的关系。

8. 回调函数

这将又是一个新概念,回调函数,可能大家大多还没听过这个概念,我们先来看一下书上的概念:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一 个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或 条件进行响应。

为什么要讲回调函数呢?因为我们讲了函数指针等等,而回调函数就是针对函数指针的函数,通过函数指针调用的函数,那具体如何使用呢?

我们可以打开MSDN查qsort函数

 

 我们通过名字可以发现它是一个排序函数,返回值是vid,里面有很多参数,除了前面的一个void* base和size_t num和width,大家看后面是不是就是一个函数指针呢?所以我们发现函数指针用处还是很广的,其功能也是比较强大的。

那既然qsort是一个排序函数,如何排序呢?又如何调用的函数指针呢?我们接下来通过代码看:

void qsort(void* base, 	//void* - 无具体类型的指针
							//能够接收任意类型的指针
							//缺点:不能进行运算,不能加减整数,不能解引用
			size_t num,//待排序的元素个数 
			size_t width,//一个元素的大小,单位是字节 
			int(__cdecl* compare)(const void* elem1, const void* elem2));//cmp指向的是:排序时,用来比较两个元素大小的函数
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
void print(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print(arr, sz);
	return 0;
}

qsort里面的传参第一个base是待排数组的起始地址,也就是arr,后面的size_t定义的num和width一个代表代拍数组元素的个数,另一个width是宽度的意思,也就是每个待排元素的大小(size_t==unsigned int)。而最后一个函数指针,用来比较传入的两个元素的大小,怎么比呢?通过两个元素的相减,如果大于零说明第一个元素大于第二个元素,如果小于则反之,而我们看到有些返回值和参数的类型都用void*来表示,这是为什么?难道还有空这种数据类型?这其实是代表了可以传入任意类型,也可以返回任意类型,不对待排数据进行类型的限定,void*可以转换为任意类型,可以是int*可以是char*可以是float*,const的作用相信不用我多说了,比较函数传入参数不许随意改变,否则会影响排序的准确性。而将cmp_int传入到qsort函数里的操作就叫做回调函数。

typedef struct Stu 
{
	char name[20];
	int age;
}s;
int cmp_by_name(const void* e1, const void* e2)
{
	return strcmp(((s*)e1)->name, ((s*)e1)->name);
}
int cmp_by_age(const void* e1, const void* e2)
{
	return ((s*)e1)->age - ((s*)e2)->age;
}
int main()
{
	s arr[]= { {"张三",20}, {"李四",10}, {"王五",30} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//按照名字排序
	qsort(arr, sz, sizeof(arr[0]), cmp_by_name);
	//按照年龄排序
	qsort(arr, sz, sizeof(arr[0]), cmp_by_age);
	return 0;
}

 如果我们定义了一个结构体,相对结构体内的数据进行拍戏的话也可以用qsort排,当然对比的方法依旧是相减,如果是汉字的话减的就是转换成二进制码值,大家可以拿到自己的编译器上输入几个数据测试一下。

我们的冒泡排序也可以用回调函数的方法来实现:

void Swap(char* buf1, char* buf2, int width)
{
	for (size_t i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void BubbleSort(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2))
{
	for (size_t i = 0; i < num-1; i++)
	{
		for (size_t j = 0; j < num - 1 - i; j++)
		{
			if (cmp((char*)base+j*width,(char*)base+(j+1)*width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
void print(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
	print(arr, sz);
	return 0;
}

我们还是先封装一个BubbleSort函数,里面的传入的参数和刚刚的数组排序大致一样,只不过我们不止需要判断判断大小,还需要将他们进行交换,这里大家看到if判断里cmp调用的参数类型居然是char*,其实不需要惊讶,后面也乘了一个width,也就是还是相等于一个int类型的大小,其他的函数和普通的冒泡排序差不多。

9. 指针和数组面试题的解析

讲了这么多,最终还是需要落实在题目上,我们来一起看几道曾经对于数组和指针的面试题吧,相信完成这些试题大家对于这块的只是都会有一个很好的掌握。(题目的解析我都写在了后面,如果有不懂的可以翻看前面相关的内容,在这里就不过多赘述了)

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16
	printf("%d\n", sizeof(a+0));//4/8   a表示首元素地址,a+0还是首元素地址
	printf("%d\n", sizeof(*a));//4      a表示首元素地址,*a就是首元素 -> a[0]     *a == *(a+0) == a[0]
	printf("%d\n", sizeof(a+1));//4/8   a表示首元素地址,a+1表示第二个元素地址
	printf("%d\n", sizeof(a[1]));//4
	printf("%d\n", sizeof(&a));//4/8    &a - 取出整个数组的地址
	printf("%d\n", sizeof(*&a));//16    &a - 取出整个数组的地址,对数组解引用拿到的是数组,大小是16    *&a == a
	printf("%d\n", sizeof(&a+1));//4/8	跳过整个数组,但还是地址
	printf("%d\n", sizeof(&a[0]));//4/8	
	printf("%d\n", sizeof(&a[0]+1));//4/8	
	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	arr表示首元素地址,arr+0还是首元素地址
	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;
}
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));//随机值,没有初始化'\0'
	printf("%d\n", strlen(arr + 0));//随机值
	printf("%d\n", strlen(*arr));//*arr - 'a' - 97  非法访问   strlen认为传进来的'a'的ascii码值97就是地址 - error
	printf("%d\n", strlen(arr[1]));//同上
	printf("%d\n", strlen(&arr));//随机值   隐式类型转换
	printf("%d\n", strlen(&arr + 1));//随机值  比第一个随机值少6
	printf("%d\n", strlen(&arr[0] + 1));//随机值
	return 0;
}
int main()
{
	char arr[] = "abcdef";//a b c d e f \0
	printf("%d\n", sizeof(arr));//7
	printf("%d\n", sizeof(arr + 0));//4/8	arr是首元素地址
	printf("%d\n", sizeof(*arr));//1	'a'
	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;
}
int main()
{
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//6
	printf("%d\n", strlen(*arr));//*arr - 'a' - 97  非法访问   strlen认为传进来的'a'的ascii码值97就是地址 - error
	printf("%d\n", strlen(arr[1]));//arr[1] - 'b' - 98  非法访问   strlen认为传进来的'b'的ascii码值98就是地址 - error
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	return 0;
}
int main()
{
	const char* p = "abcdef";
	printf("%d\n", sizeof(p));//4/8
	printf("%d\n", sizeof(p + 1));//4/8
	printf("%d\n", sizeof(*p));//1
	printf("%d\n", sizeof(p[0]));//1
	printf("%d\n", sizeof(&p));//4/8 char**p
	printf("%d\n", sizeof(&p + 1));//4/8
	printf("%d\n", sizeof(&p[0] + 1));//4/8
	return 0;
}
int main()
{
	const char* p = "abcdef";
	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//5
	printf("%d\n", strlen(*p));//error
	printf("%d\n", strlen(p[0]));//error
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5
	return 0;
}
int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//48
	printf("%d\n", sizeof(a[0][0]));//4
	printf("%d\n", sizeof(a[0]));//16
	printf("%d\n", sizeof(a[0] + 1));//4/8	单独放在sizeof内部才算整个数组
	printf("%d\n", sizeof(*(a[0] + 1)));//4/8
	printf("%d\n", sizeof(a + 1));//16	a并没有单独放在sizeof内部,也没有&,所以a表示首元素(第一行)的地址,所以a+1是第二行的地址
	printf("%d\n", sizeof(*(a + 1)));//16	
	printf("%d\n", sizeof(&a[0] + 1));//16	第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1)));//16	*(&a[0]+1) -> *(&a[1]) -> a[1]
	printf("%d\n", sizeof(*a));//16
	printf("%d\n", sizeof(a[3]));//16	sizeof内部不参与运算,不会真的访问,假设a[3]存在,只是计算了一下类型属性(一维数组)的大小
	return 0;
}
int main()
{
	int a[] = { 1,2,3,4,5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));//2,5	
	return 0;
}
struct Test
{
	int Num;
	char* pcName;
	short sData;
	char cha[2];
	short sBa[4];
}* p;//大小为20个字节
int main()
{
	p = (struct Test*)0x00100000;
	printf("%p\n", p + 0x1);//0x00100014
	printf("%p\n", (unsigned long)p + 0x1);//0x00100001
	printf("%p\n", (unsigned int*)p + 0x1);//0x00100004
	return 0;
}
int main()
{
	int a[] = { 1,2,3,4 };
	int* ptr1 = (int*)(&a + 1);//4
	int* ptr2 = (int*)((int)a + 1);//2000000
	printf("%x,%x", ptr1[-1], *ptr2);//ptr[-1] == *(ptr-1)
	return 0;
}
int main()
{
	int a[3][2] = { (0,1),(2,3),(4,5) };//逗号表达式!TMD!
	int* p;
	p = a[0];
	printf("%d", p[0]);//1
	return 0;
}
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[3][2]);
	return 0;
}
int main()
{
	int aa[2][5] = { 1,2,3,4,5,6,8,7,9,10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10,5
	return 0;
}
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);//at
	return 0;
}
int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };//F P N E
	char*** cpp = cp;
	printf("%s\n", **++cpp);//POINT	自增改变的是自身的值,才会保留在第一个printf语句中
	printf("%s\n", *-- * ++cpp + 3);//ER
	printf("%s\n", *cpp[-2] + 3);//ST
	printf("%s\n", cpp[-1][-1] + 1);//EW
	return 0;
}

好了,关于指针的进阶我们就讲到这,相信这些内容如果全部掌握是需要点时间的,所以大家加油,我们下次再见。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值