还在为指针发愁吗?C语言指针2万字+的总结,让你不再是小白。(建议收藏)

按着顺序,学习不迷路,最后附经典题目

前言

指针是C语言的灵魂,是C语言的精髓,也是一把双刃剑。用的好,就带来极大的便利和灵活,用不好,就带来极多的bug。下面,让我们好好学习指针。

初阶指针

什么是指针

我们知道,在这个“储存”了万物的世界,为了更快速的找到某个“东西”,人们给某个地方起了地址名称——大到亚洲非洲这样的地名,小到门牌编号。我们只要按照这个地址就可以找到某个“东西”。例如,你给女朋友寄一束鲜花,你在快递盒上标明了地址,快递员就能通过地址找到你女朋友的家。在计算机中,我们数据储存在内存中,内存被划分成很多分区域,划分的最小单位是一个字节,我们对每个字节进行编号,通过这个编号就能找到对应内存里储存的内容。指针就是内存中一个最小单元的编号,也就是地址。我们平时说的指针,通常指的是指针变量,用来存放地址的变量。

小结:指针是一个变量,用来存放地址。

指针变量 

初学C语言,我们知道不同的变量要用不同的类型声明定义,如整型我们一般用int,long,long long。浮点型我们用float,double。那么指针变量要怎么声明定义呢?我们又怎么得到一个变量的地址呢?我们又怎么用一个地址找到内存里面的内容呢?

总的来说,我们可以用&操作符来取出一个变量的地址,把这个地址放到一个变量里面,就成了指针变量。

举个例子:


#include <stdio.h>
int main()
{
	int a = 0;//在内存中开辟一块空间
	int* p = &a;//使用&操作符,取出a的地址
	   //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,
       //p就是一个之指针变量,他的类型就是int*
	return 0;
}

那现在有个问题,指针有多大呢?

要讲清楚指针有多大,就要明白地址线的工作原理,这里我就先不细说,要想搞清楚可以私信我。

这里我们只要记住在32位机器上(x86环境),任何指针的大小都是4个字节;在64位机器上(x64环境),任何指针大小都是8个字节。

指针有什么类型

我们知道,变量有int,float,char等等,那指针有没有类型呢?那是当然的。

如此代码所示,当变量a为int型时,将a取地址赋给p,p的类型就是int*。那么我们以此类推可以得到,当变量为char型时,其地址类型就是char*,当变量为float时,其地址类型就是float*。那可能有头铁的铁子会问,那int型的变量可以用char*接收吗?那肯定是不行的。轻则报警告,重则直接报错。那么指针有这么多类型的意义是什么?(请往下看到指针加减整数,这里讲解)

指针的解引用

我们可以通过*操作符(解引用操作符)找到地址所对应的内容。

举个例子:

#include <stdio.h>
int main()
{
	int a = 0;//在内存中开辟一块空间
	int* p = &a;//使用&操作符,取出a的地址
	   //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量,他的类型就是int*
	printf("%d", *p);//打印*p的值
	return 0;
}

 

 可以看到,我们通过*p找到了p所指的地址对应内存里的内容。

何为野指针

先上概念:野指针就是指针指向的位置是不可知的。想必很多初学者第一次看见这概念的时候,跟我有同样的想法:这是个啥玩意儿啊?我们直接上代码。

野指针的成因:

1.指针未初始化:

#include <stdio.h>
int main()
{ 
     int *p;//此处只是定义了int*p这个指针,指针未初始化,默认为随机值
    *p = 20;
     return 0; 
}

2.指针越界访问:

#include <stdio.h>
int main()
{
	int arr[5] = { 0 };//创建一个int型数组
	int* p = arr;//数组名为首元素地址
	int i = 0;//定义循环变量
	for (i = 0; i <= 6; i++)
	{
		//当指针指向的范围超出数组arr的范围时,(即i==6时),p就是野指针
		*p = i;
		p++;//p自增1
	}
	return 0;
}

3.指针指向的空间被释放:

void test(int* const p)
{
	;
}

#include <stdio.h>
int main()
{
	int arr[5] = { 0 };
	test(arr);//出了函数p就会被销毁
	*p = 10;//这里的p就是野指针
	return 0;
}

既然野指针这么讨人厌,那我们要怎么做能规避野指针的出现呢?

如何规避野指针:

1. 指针初始化,在创建指针的时候就对其初始化,如果不知道要初始化为什么就置为NULL(空指针)
2. 小心指针越界

3. 指针指向空间释放即使置NULL

4. 避免返回局部变量的地址,如在函数内部返回在函数创建的变量地址

5. 指针使用之前检查有效性,使用assert判断。

如何进行指针运算:

指针加减整数:

#include <stdio.h>
int main()
{
     int n = 10;
     char *pc = (char*)&n;//强制类型转换,把int*型转为char*
     int *pi = &n;
     printf("%p\n", &n);//&n为int*型,打印结果为005BF8C4,为所占内存的首字节地址
     printf("%p\n", pc);//pc为char*类型的指针,打印结果也为005BF8C4
     printf("%p\n", pc + 1);//pc为char*类型的指针,加了1打印结果为005BF8C5,指针往后走了一个字节(char的大小)
     printf("%p\n", pi);//pi为int*型,打印结果为005BF8C4,为所占内存的首字节地址
     printf("%p\n", pi + 1);//pi为int*型,打印结果为005BF8C8,指针往后走了4个字节(int型的大小)
     return  0; 
}

 由此可知,指针类型决定了指针加减整数时,往后走一步的步长,即加一往后走多少字节。

char*的步长为1,int*的步长为4等等。

指针减指针 

举个模拟实现库函数strlen的例子

#include<stdio.h>
int my_strlen(char* s) 
{
	char* p = s;//创建一个指针记录初始位置
	while (*p != '\0')
		p++;
	return p - s;//指针减指针得到两个指针之间的字节个数
}

int main()
{
	char s[6] = "abcde";
	int ret = my_strlen(s);//数组名为首元素地址传参
	printf("%d", ret);
	return 0;
}

 由此可知,指针减指针得到指针之间字节的个数。

指针和数组的爱恨情仇

还是老样子,我们直接上例子

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

 

我们看到,两个打印的结果是一样的,依此,我们可以得出结论,数组名表示数组首元素地址

(两种情况除外)这两种情况在这里不细说,想知道可以自行搜索或者在评论区提问或者私信我。

#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);//求出数组元素个数
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//p指针往后走i个整型,每走一步跳过4个整型,最后解引用操作找到指 
                                //针所指向的内存空间的内容
	}
	return 0;
}

 何为二级指针

我们平常所说的指针其实是指针变量,指针变量也是变量,那作为变量,指针也有他的地址,那储存一级指针的地址的指针就是二级指针。

上代码演示

#include <stdio.h>

int main()
{
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;
	printf("%p\n", pa);
	printf("%p\n", ppa);
	return 0;
}

 

不懂的铁子可以参考下面的图

 这里对ppa解引用(*ppa),找到的是pa,再次解引用就找到a,即**ppa找到a。

指针数组是个啥

相信很多初学者会有同一个疑问,指针数组他到底是指针还是数组。这里类比一下就清楚了。我们知道数组存放整型时,称为整型数组。用来存放字符串时,称为字符数组。类比过来,当数组用来储存指针时,则称为指针数组。即指针数组是数组。

 

进阶指针

字符指针

先看一段代码

#include <stdio.h>

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

这里思考一个问题:我们可以看到将一个字面常量字符串hello world赋值给了字符指针pstr ,这里是把一个字符串放到pstr中吗?

实际上并不是的,指针变量是用来存放地址的,所以实际上pstr里放的是字符串的首元素地址,通过这个首地址找到了字符串并打印出来。

我们再看一段代码

#include <stdio.h>
int main()
{
	char str1[] = "hello world.";
	char str2[] = "hello world.";
	const char* str3 = "hello world.";
	const char* str4 = "hello world.";
	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 呢?

那是因为我们在内存栈区不同空间中开辟了两个数组,一个是str1,储存内容为"hello world";另一个是str2,储存的内容为"hello world"。if(str1 == str2)判断的是数组首元素地址是不是相同。因为是在内存不同空间储存的,str1 和 str2指向的地址肯定是不同的。

为什么str3 and str4 are same呢?

那是因为我们在只读常量区开辟了一块空间,储存内容为"hello world"。然后把首元素h的地址赋给了str3 和 str4。即str3 和 str4都指向同一个地址,所以str3 和 str4肯定是相同的。

指针数组

我们在初阶指针学习了指针数组,我们在这里再次巩固一下。

先看一段代码

#include<stdio.h>

int main()
{
	int* arr1[10]; 
	char* arr2[4]; 
	char** arr3[5];
	int** arr4[3];
	int*** arr5[6];
	return 0;
}

 能否说出这是什么数组呢?

答案揭晓:

#include<stdio.h>

int main()
{
	int* arr1[10]; //一级整型指针数组
	char* arr2[4]; //一级字符指针数组
	char** arr3[5];//二级字符指针数组
	int** arr4[3];//二级整型指针数组
	int*** arr5[6];//三级整型指针数组
	return 0;
}

数组指针

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

我们知道,有整型指针,他是指向一个整型变量起始地址的指针。有字符指针,他是指向一个字符或者字符串起始地址的指针。依此类比,数组指针是一个指针,他是指向一个数组的起始地址的指针。

下面我们看一段代码

#include<stdio.h>

int main()
{
	int* p1[10];
	int(*p2)[10];
}

思考一下:p1和p2是什么?

答案揭晓:p1是指针数组,p2是数组指针。

为什么呢?

int ( * p )[ 10 ]: p先和 * 结合,说明 p 是一个指针变量,然后指着指向的是一个大小为 10 个整型的数组。所以 p 是一个 指针,指向一个数组,叫数组指针。
这里要注意: [] 的优先级要高于 * 号的,所以必须加上()来保证 p 先和 * 结合

int *p[10]:没有(),p和[]先结合,说明p是一个数组,数组有十个元素,每个元素类型是int*。所以p是一个 数组,储存了10个int*指针的数组。

&数组名VS数组名

int arr[10];

arr和&arr是啥?

我们知道arr是数组名,表示首元素地址,那么&arr是什么?

先看一段代码

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

运行结果如下 

我们可以看到,两个打印出来的结果是一样的,真的一样吗?可能很多新手就跳进坑里了。

我们再看一段代码

#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;
}

运行结果

 运行结果显示,arr+1相比于arr,增加了4个字节, 而&arr+1相比于&arr,增加了40个字节。实际上:&arr表示的是数组的地址,而不是首元素的地址,虽然他们的值是一样的。&arr的类型是int(*)[10],是一种数组指针,而arr的类型是int*,是一种整型指针。

数组指针的使用

看一段代码

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) 
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		j = 0;
		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;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		j = 0;
		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,11,12,13,14,15 };
	print_arr1(arr, 3, 5);
	//数组名arr,表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	print_arr2(arr, 3, 5);
	return 0;
}

    数组名arr,表示首元素的地址
    但是二维数组的首元素是二维数组的第一行
    所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    可以数组指针来接收,int(*arr)[5]表示这是一个数组指针,指向的是一个有五个元素,每个元素是      int 的数组。

下面巩固练习一下,下面四句代码是什么意思?

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

int arr[5]:他是一个数组,名为arr,有五个元素,每个元素是int型。

int * parr1 [ 10 ]:他是一个数组,名为parr,有10个元素,每个元素是int*型(指向一个整型的指针)
int ( * parr2 )[ 10 ]:他是一个指针,名为parr2,他指向一个数组,这个数组有10个元素,每个元素是int型
int ( * parr3 [ 10 ])[ 5 ]:他是一个数组,名为parr3,他又10个元素,每个元素是int(*)[5]的指针,这个指针指向一个数组,这个数组有五个元素,每个元素是int型。

前三个应该不难理解,下面是第四个的图解

数组参数和指针参数:我们不一样

一维数组传参

 下面看一段代码,看看传参过程中,哪个正确哪个错误。

#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?
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

 在test函数中,传的是arr数组名,即首元素(int型)地址,可以用数组(可以不指定大小)接收数组,也可以用int*型指针接收数组。所以void test(int arr[]){};void test(int arr[10]){};void test(int* arr){}都ok。

在test2中,传的是arr2数组名,即首元素(int*型)地址,可以用数组接收数组,也可以用指针接收数组,值得注意的是,首元素是int*的指针,要接收一个指针的地址应该用二级指针。所以voidtest2(int* arr[20]){};void test2(int** arr){}都ok。

二维数组传参

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);
}

void test(int arr[3][5]){}:用数组接收数组,参数齐全,当然可以。

void test(int arr[][]){}:用数组接收数组,但是省略了列,不可以。

void test(int arr[][5]){}:用数组接收数组,省略了行没有省列,当然可以。

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

void test(int* arr){}:二维数组传数组名为首元素地址,但是首元素是数组第一行,是一维数组的地址,用类型为int*的指针接收当然不行。

void test(int* arr[5]){}:这个参数是一个一维数组,用来接收二维数组当然不行。

void test(int(*arr)[5]){}:这是一个指针,指向一个数组,这个数组有5个元素,每个元素是int型。用一个数组指针接收二维数组首元素地址当然可以。

void test(int** arr){}:用二级指针接收一个一维数组地址,当然不行。

一级指针传参

看一段代码

#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;
}

这段代码很简单,不做解释,主要理解什么是一级

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

指针传参。

二级指针传参

#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;
}

也很简单,不做解释。

函数指针:要烧脑咯

先看一段代码

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

这两个地址是 test 函数的地址。通过这里我们知道,函数名就是函数的地址,可以省略取地址符号。

 下面pfun1和pfun2哪个有能力存放test函数的地址?

void test()
{
     printf("hehe\n");
}
void (*pfun1)();
void *pfun2();

答案是pfun1

pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参 数,返回值类型为void。
pfun2先和()结合,说明pfun2是函数,函数参数为空,返回值为void*。

读两段有趣的代码:解释一下代码表示的是什么意思

(*(void (*)())0)();

该代码是一次函数调用
调用0地址处的一个函数
首先代码中将0强制类型转换为类型为void (*)()的函数指针
然后去调用0地址处的函数

void (* signal( int, void(*)(int) ) )(int);


该代码是一次函数的声明
声明的函数名字叫signal
signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,该函数指针能够指向的那个函数的参数是int,返回类型是void
signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void

在这推荐一本书 C陷阱和缺陷》,这本书提及了这两段代码 C陷阱与缺陷-安德鲁·凯尼格-微信读书 (qq.com)

函数指针数组:要开始长脑子了

数组是一个存放相同类型数据的存 储空间,那我们已经学习了指针数组,把函数的地址存到一个数组中,那这个数组就叫函数指针数组。
下面哪个是函数指针数组呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
答案是 parr1 ,parr1 先和 [] 结合,说明 parr1 是数组,数组的内容是 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;
	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;
}

作为新手的我们肯首先想到的就是用分支循环结构。但是这段代码有明显的缺点,重复代码很多,每个case里只是调用的函数不一样,可以用函数指针数组来当作转移表,简化代码。

#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 (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int sz = sizeof(pf) / sizeof(pf[0]);
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input <= sz)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);

	return 0;
}

用函数指针数组的好处一是简化了代码,提高了效率;二是在后面对计算器功能增加时,只需要增加功能函数的声明定义和更改函数指针数组,可维护性高。

指向函数指针数组的指针:这到底是啥玩意儿啊

指向函数指针数组的指针是一个 指针 ,指针 指向一个 数组 ,数组的 元素都是 函数指针 ;
那我们要如何定义这个指针呢?
看下面一段代码
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)[5])(const char*) = &pfunArr;
	return 0;
}

我们刚开始定义一个指向函数指针数组的指针,可以从简单的函数指针开始定义,然后一步一步往后走。阅读代码的时候,要从内往外看,如ppfunArr因为有小括号,先和*结合,说明他是一个指针,再往外看看见[5],说明这个指针指向一个数组,这个数组有5个元素。然后把他们全部遮起来,剩下void(*)(const char*),说明这五个元素都是一个函数指针,这个函数指针的参数是(const char*),返回值是void。

回调函数:恭喜你,不再是小白了

先上定义:

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

光看定义可能晦涩难懂,那我们上一个经典用例:qsort

我们先了解一些qsort的用法

 

首先在使用qsort函数时必须得引入对应得头文件<stdlib.h> ,qsort函数是可以对任意类型的元素排序,我们可以看到该函数是有4个参数,第一个参数是接收数组地址(要对哪个地址的内容进行排序)的,第二个是参数是接收数组元素个数的,第三个参数是接收元素大小(单位字节),第4个参数是函数指针,这个函数指针指向的函数需要使用者自己定义实现,这个需要自己实现的函数是用来比较元素大小的,(需要排升序就是p1-p2,降序就是p2-p1)同时已经规定好了这个比较函数的返回值类型和参数类型,同时也说明了返回值的意义。p1如果大于p2就返回大于0的数,反之就返回小于0的数,如果两个数相等就返回0.

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void* p1, const void* p2) 
{
	return (*(int*)p1 - *(int*)p2);//因为void*的指针不能进行加减等运算,所以需要强制类型转换
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int i = 0;

	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);//int_cmp就是回调函数
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

 说简单点就是一个函数(qsort)调用另一个参数(int_cmp),被调用的函数(int_cmp)就是回调函数。

上面排序的是整型,那怎么排序其他类型呢?

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

struct Stu
{
	char name[20];
	int age;
};//创建一个结构体变量

//按照学生的年龄来排序
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//按照学生的名字来排序
int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test2()
{
	struct Stu s[3] = { {"zhangsan",16}, {"lisi", 17}, {"wangwu", 18} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);//按年龄排序
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);//按名字排序
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
}
int main()
{
	test2();
	return 0;
}

 

现在上难度,模拟实现一个qsort。

我们采取冒泡排序的算法,即前后一一对比,判断是否需要交换。

先看一段代码,理解一下什么是冒泡排序

#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
	//趟数
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		int j = 0;
		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;
			}
		}
	}
}
int main()
{
	int arr[5] = { 5,3,4,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

冒泡排序即拿第一个元素一一往后对比,判断是否需要与之交换位置,直到一趟冒泡排序结束,就不再排序这个元素。

我们发现,上面的冒泡排序有很大的弊端,就是只能排序整型类型,而qsort是能排序所有类型数据的,所有我们要对上面的代码进行改进。

我们直接上代码

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

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

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

void Swap(char* buf1, char* buf2, int width)
{
	//我们不知道将来要交换的元素到底又多大,所以我们采用一个字节一个字节进行交换,每个字节都交换了,就相当于整体交换了
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

//按照学生的年龄来排序
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}


void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const void* e2))
{
	//趟数
	size_t i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		size_t j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//我们不知道将来要排序的类型是什么,也不知道有多少字节,又基于空指针不能直接运算,所以我们将他强转为char*的指针,j*width找到那个元素地址
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				
			}
		}
	}
}


//使用我们自己写的bubble_sort函数排序整型数组
void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n\n");
}
//使用我们自己写的bubble_sort函数排序结构体数组
void test4()
{
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	for (int i = 0; i < sz; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	printf("\n");
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
	for (int i = 0; i < sz; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
}

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

 

到这都理解了,我们接下来上点经典题目

指针和数组题的解析 

一维数组

int a[] = {1,2,3,4};
1.printf("%d\n",sizeof(a));
2.printf("%d\n",sizeof(a+0));
3.printf("%d\n",sizeof(*a));
4.printf("%d\n",sizeof(a+1));
5.printf("%d\n",sizeof(a[1]));
6.printf("%d\n",sizeof(&a));
7.printf("%d\n",sizeof(*&a));
8.printf("%d\n",sizeof(&a+1));
9.printf("%d\n",sizeof(&a[0]));
10.printf("%d\n",sizeof(&a[0]+1));

1中sizeof单独放的是数组名所以1打印结果就是整个数组的大小,也就是16.
2中放的是数组名但是加了0,那就是数组首元素地址加0,还是数组首元素地址,所以2中计算的是地址的大小,那就是4或者8,
3中是对数组名解引用,数组名是数组首元素地址,所以对它解引用就是数组第一个元素,因此3中计算的大小就是4
4中是首元素地址加1,那还是地址,所以4中是地址大小,4或者8
5中数组元素,所以5中也是4
6中取的数组地址,本质还是地址,所以6中打印的也是4或8
7中解引用和取地址是可以抵消的,那就是相当于放的是数组名,那计算就是数组的大小,也就是16
8中是地址加1,那还是地址,所以是4或者8
9中是取地址,那还是地址,所以是4或者8
10中也是取地址加1,那还是地址,也就是4或者8

 字符数组

int main()
{
    char arr[] = { 'a','b','c','d','e','f' };
	1.printf("%d\n", sizeof(arr));
	2.printf("%d\n", sizeof(arr + 0));
	3.printf("%d\n", sizeof(*arr));
	4.printf("%d\n", sizeof(arr[1]));
	5.printf("%d\n", sizeof(&arr));
	6.printf("%d\n", sizeof(&arr + 1));
	7.printf("%d\n", sizeof(&arr[0] + 1));
	8.printf("%d\n", strlen(arr));
	9.printf("%d\n", strlen(arr + 0));
	10.printf("%d\n", strlen(*arr));
	11.printf("%d\n", strlen(arr[1]));
	12.printf("%d\n", strlen(&arr));
	13.printf("%d\n", strlen(&arr + 1));
	14.printf("%d\n", strlen(&arr[0] + 1));
    return 0;
}

1中单独放的是数组名,所以1中打印结果是数组的大小,就是6
2中是数组名加0,就是首元素地址加0,那就还是地址,所以是4或者8
3中数组名解引用也就是对数组首元素地址解引用就是数组首元素,所以3中大小是1
4中是数组第二个元素,所以就是1.
5中是取地址数组名,本质还是地址,所以打印结果是4或者8.
6中是数组地址加1,还是地址,所以打印结果是4或者8.
7中是取地址加1,还是地址,所以打印结果是4或者8.
8是求字符串长度,但是整个数组中没有\0,所以打印结果是随机值。
9是数组首元素地址加0,还是数组首元素地址,因为没有\0,所以还是随机值而且随机值还和8一样。
10是数组首元素地址解引用就是数组首元素,也就是字符a,字符a会转成对应的ASCII值97,也就是会传入97所在的地址,这个地址是不能所以访问的这样会造成程序崩溃。
11和10中的例子一样,传入字符b对应ASCII值98的地址,造成程序崩溃。
12是取数组地址,数组的地址和数组首元素地址是一样的,而且数组里没有\0,所以也是随机值,而且随机值和8中的例子是一样的。
13取数组地址加1跳过整个数组,指向的是紧接着数组元素f后面的地址,也是随机值,并且和12的随机值相差6.
14是首元素地址加1,指向第二个数组元素的地址,也是随机值

int main()
{
	char arr[] = "abcdef";
	1.printf("%d\n", sizeof(arr));
	2.printf("%d\n", sizeof(arr + 0));
	3.printf("%d\n", sizeof(*arr));
	4.printf("%d\n", sizeof(arr[1]));
	5.printf("%d\n", sizeof(&arr));
	6.printf("%d\n", sizeof(&arr + 1));
	7.printf("%d\n", sizeof(&arr[0] + 1));
	8.printf("%d\n", strlen(arr));
	9.printf("%d\n", strlen(arr + 0));
	10.printf("%d\n", strlen(*arr));
	11.printf("%d\n", strlen(arr[1]));
	12.printf("%d\n", strlen(&arr));
	13.printf("%d\n", strlen(&arr + 1));
	14.printf("%d\n", strlen(&arr[0] + 1));
	return 0;
}

1中放的是指针变量,指针变量的大小都是4或者8.
2中还是指针变量,所以结果是4或者8
3.是对指针解引用,p是指向常量字符串abcdef的首地址,所以也就是指向a,因此算的是a的大小也就是1.
4是等价于*(p+0)所以算的还是a的大小,还是1.
5是取地址p是对指针变量取地址,还是地址,所以是4或者8
6是对取地址加1还是地址,所以是4或者8
7是相当于取a的地址加1,还是取地址,所以是4或者8.
8.是求常量字符串的长度,所以是6
9是p+1地址处求字符串长度,也就是从字符串第二个字符起求字符串长度们就是5.
10.就是把第一个字符a转成ASCII值处的地址,也就是97处的地址,会造成程序崩溃。
11和10是相等的
12是对p取地址,通过这个地址可以找到p,p中放的是地址编号,地址编号以16进制表示,编号中可能可能会有0.所以是随机值。
13是对p取地址加1,这个地址位置也是随机的,不知道这个地址到底放着什么,打印结果也是随机值
14.取首元素地址加1就是第二个字符的地址,实际上就是计算从b起字符串的长度,也就是5

二维数组

int main()
{
	int a[3][4] = { 0 };
	1.printf("%d\n", sizeof(a));
	2.printf("%d\n", sizeof(a[0][0]));
	3.printf("%d\n", sizeof(a[0]));
	4.printf("%d\n", sizeof(a[0] + 1));
	5.printf("%d\n", sizeof(*(a[0] + 1)));
	6.printf("%d\n", sizeof(a + 1));
	7.printf("%d\n", sizeof(*(a + 1)));
	8.printf("%d\n", sizeof(&a[0] + 1));
	9.printf("%d\n", sizeof(*(&a[0] + 1)));
	10.printf("%d\n", sizeof(*a));
	11.printf("%d\n", sizeof(a[3]));
	return 0;
}

1中放的是数组名,所以计算的是二维数组的大小,也就是48
2中是第一行第一列的元素,所以计算的是元素大小,也就是4.
3中是a[0],a[0]是什么东西呢?我们知道如果想访问二维数组的第一行的元素,就是a[ 0 ][ j ] 访问第二行的元素就是a[ 1 ][ j ],二维数组是可以把每一行的元素可以当作一维数组来看的,实际上a[0],就是二维数组第一行首元素地址,第一行首元素的地址,类比一维数组,一维数组的首元素地址单独放在sizeof里是求整个数组的大小,第一行首元素地址单独放在sizeof中不就是求整个第一行的大小,也就是16.
4中并不是单独放入的a[0],类似于一维数组如果不是对a[0]取地址,就表示的不是第一行的地址而是第一行首元素的地址,这可以类比一维数组。a[0]+1是跳过一个int* 也就是指向第一行的第二个元素,本质还是地址。所以打印结果是4或者8.
5是对4解引用,也就是说5中放的是a[0][1],计算的是元素大小,也就是4
6是数组名加1,也就是数组首元素地址加1,二维数组的首元素是谁呢?首先我们把二维数组当作一维数组来看时,它的每一行就相当于一个元素,有3行,就有3个元素,那么首元素的地址就是第一行的地址,a+1就是第二个数组元素的地址,也就是第二行的地址,既然是地址也就是4或者8
7是对6解引用,6是指向第二行的地址,第二行的地址解引用就是a[1],也就是计算第二行的大小,也就是16
8.是&a[0]+1,&a[ 0 ]地址就是第一行首元素的地址,加一之后指向的是数组第一行第二个元素的地址,既然是地址那就是8或者4.
9是对8解引用,8是指向数组第二行地址, * ( &a[0] + 1)其实价于 *(a+0+1),也就是相当于a[1],所以就是求整个第二行的大小。
10.对数组名解引用就是等价于a[0],所以和例3一样计算的是整个第一行的大小,也就是16
11.求数组整个第四行的大小,但是a只有3行,这样会不会造成数组越界访问,其实sizeof中的表达式是不参与计算的,也就是没有运算效果,一个表达式,是有两个属性的,值属性和类型属性,sizeof是根据数据类型来计算大小,里面的值不会影响结果,同时里面的表达式式也没有操作效果。所以实际上这样写虽然是不符合规范的,但是不会造成数组越界。因为a数组前3行都是4个整型元素,哪怕是不存在第四行,但是还是沿用a[2]的数据类型,所以结果也是16

指针经典笔试题

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

取地址数组名表示的是取出整个数组的地址,想要保存整个数组的地址,应该用数组指针变量来保存,但是用强制类型转化,将其转转成了整型指针类型,那我们应该搞清楚ptr到底指向哪。首先取地址数组名+1,实际上是跳过整个数组,既然跳过了整个数组,那么ptr指向的就应该是数组最后一个元素往后一个整型的大小的位置,那么 ptr-1刚好就是5的位置,在对其解引用就是5,数组名是首元素地址,首元素地址加1是访问第二个数组元素,就是2。打印结果就是2,5。

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

 0x表示的是16进制数,p+0x1和p+1是差不多的,因为p中放的地址是16进制形式的,所以用0x。前面提到过指针加减整数是指向向前或者向后的地址,向前一步走或者向后一步走的每步距离多大是取决于指针变量的数据类型的,p是开始时struct Test*类型的,也给出了struct Test的大小是20字节的,所以加1就跳过20个字节,是以地址(16进制)为打印结果的,所以在原来的地址编号上在加上16就行了,16转为10进制就是20。我们看到第二个打印p被转成了无符号整型,也就是说p现在是整型。整型加1就是加1,也就是说p中放的地址编号加1。第三个打印是p被转成了无符号整型指针,也就是说加1是跳过一个无符号整型大小个字节,所以p中的地址编号加4。这个没办法运行演示,因为起始地址是随机的。

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);
	printf("%x,%x", ptr1[-1], *ptr2);
	return 0;
}
//程序运行结果是什么?

这个第一次做的话可能会有点难,首先我们知道以%x打印,就是以16进制进行打印,只是如果高位是0的话就省略不打印,如0x0012ff40,打印结果为12ff40。这是做这道题的前提知识。

&a+1是跳过整个数组的指向的是4紧接着后面的地址,-1在对其解引用就是对4访问,打印结果就是4,这个相对简单,不做图解。第二个打印首先将a(首元素地址)强制类型转化,也就是a转成了整型,整型加1就是加1,现在a对应的地址编号就是在原来的基础上加了1,在将其转化成整型指针,实际上就是在指向首元素地址的基础上往后偏移了一个字节。整型在计算机中都是以补码存储的,存储就会涉及存储顺序的问题,就是字节大小端。我们知道数组地址都是从低到高的,不同的机器采用的存储顺序是不同的,以vs x86环境为例,采用的是小端存储。就是低地址放低位,高地址放高位。

站在ptr2的位置,他是一个整形指针,解引用向后访问4个字节,即02000000,又因为%x不打印前面的0,所以结果为2000000。

 

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

数组中的元素是用小圆括号包起来了,并没有用花括号。所以数组中的是逗号表达式,a中实际上是放的1 ,3,5,并没有完全初始化,在之前的例题中有说过a[0]是第一行首元素的地址,也就是a[0][0]的地址,所以对p解引用就是a[0][0],打印结果就是1。

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;
}

 p是数组指针,指向有4个元素的一维数组,a是二维数组,但是把每行看做是一维数组,也就是说a是5个一维数组,每个一维数组都是有着5个元素。你也可以说a是有着5个元素的一维数组,每个元素都是a的每一行。现在把a给了p, p指向的是就是数组首元素地址,其实就是a[0][0]的地址,前面学的好的铁子们,看到这可能会有疑问,p的类型(int(*)[4])和a的类型(int(*)[5])不是不一样吗?这可以直接赋值么?是可以的,只是编译器会报警告,而且平时我们自己写代码不推荐这样写。

以%d的形式打印,结果是-4就不做解释;以%p的形式打印呢?我们知道,-4在内存中是以补码的形式进行储存的,-4的补码为11111111 11111111 11111111 11111100,化作16进制为0xFF FF FF FC 。所以打印结果为0xFF FF FF FC。

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));
	return 0;
}

 取地址数组名是取整个数组的地址,加1后跳过整个数组,patr1-1现在指向的就是数组的末尾,就是10,所以第一个打印结果就是10,二维数组数组名,就是数组首元素地址,把二维数组当作一维数组,每个元素都代表二维数组的每一行,所以aa是第一行的地址,aa+1是第二行的地址,单独的每行就是一维数组,一维数组的地址是有数组首元素的地址表示的,所以第二行的地址就是数组第二行首元素的地址,就是6的地址,将6的地址传给ptr2,指针加减整型已经说了好几次了,就指向往后一个整型大小的地址1,就是指向5,在对其解引用就是5,打印结果就是5。

#include <stdio.h>
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

a是指针数组,a中放的是3个字符串的地址。pa是二级指针,因为二级指针就是存放指针变量的地址,也就是存放地址的变量的地址。pa指向数组首元素地址,pa+1指向数组第二个元素的地址,数组第二个元素就是at的地址,对pa+1解引用就可以访问到at的地址,也就是说打印的结果就是at。

 

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

这题相对比较复杂,我们画图解释。

先把题目给的数组画出来

 ++cpp的结果

顺着地址我们找到了POINT

*--*++cpp+3的结果

先++cpp然后解引用找到了c+1这块空间然后进行--,这块空间内容变成c 。

 再次解引用找到E的地址加三打印就是ER

*cpp[-2]+3的结果

先cpp减2(不是自减)然后解引用找到c+3,再次解引用找到F的地址加三打印结果为ST

 

cpp[-1][-1]+1的结果

cpp[-1][-1]+1等价于(*(cpp-1)-1 )+1,先cpp减一(不是自减)解引用找到c+2,c+2减一为c+1,再次解引用找到N的地址加一打印为EW。

到这本章结束啦。

总结

1.指针加减整数,指针相减,是穿插在题目中,没有单独拿出来。指针是C语言的精华,知识点很多,短短一篇文章是囊括不完的,在平时编程中需要多练习,多总结。本人能力有限,也不是写地面面俱到。
2,数组首元素地址和数组地址,虽然它们对应的地址编号是相同的,但是&取地址数组名和数组名所表示的意义可是完全不同的。这点是需要注意的,数组名是表示首元素地址,sizeof(数组名)是求整个数组的大小,&数组名表示取出整个数组的地址。
3.二维数组是可以当作一维数组的,将二维数组当作一维数组,它的每行看作一个元素。a如果是二维数组,a[0]就是第一行元素的首地址,a+1是整个第二行的地址。
4.sizeof只是计算数据大小,里面的式子是没有运算效果的。
5.数组和指针传参以及回调函数等,需要去实践练习。

注释:以上仅仅表达个人看法,如有错误,请斧正。
 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

每天进步亿丢丢

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

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

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

打赏作者

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

抵扣说明:

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

余额充值