【C语言指针详解】你对C语言指针了解多少?

注:本博客对指针变量类型的意义,const修饰指针,指针运算,assert断言,指针的使用和传址调用等指针基础知识讲解较少

​​​​​​​

废话不多说我们之间开始

 

如果有错误请大佬指正,谢谢 

内存和地址

在讲内存和地址之前,我们先举个例子

假设你有一把酒店某一房间的钥匙,并且房间没有编号,所以你需要一个一个试一下,在几百个,这样效率太低了。如果给房间编上号,便可根据钥匙找到。

 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,内存的相关知识我们在数组中讲过,就不在赘述了,那这些内存空间如何高效的管理呢?

其实也是把内存划分为一个个的内存单元,每个内存单元的大小取一个字节。

 下面此图是之前数组博客讲到内存的相关知识

一个bit位可以存储一个2进制的位1或者0

 

  如何理解编址

 

 1.内存被划分为一个个的单元,一个内存单元的大小是1个字节

2.每个内存单元的都给一个编号,这个编号就是地址,C语言中把地址又称为:指针

编号==地址==指针

···
 

指针变量和地址

1.创建一个变量a,并赋值为10

2.在内存上申请4个字节的空间,存放10 

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

&为取地址操作符,按位与需要两个操作数

上述地址以16进制展示

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

 

 

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	/*printf("%p\n", &a);*/
	int* pa = &a;//pa是指针变量-存放地址-地址又被称为指针,指针变量是用来存放指针的。

	return 0;
}

int *是pa的类型

 

  这3种写法没有区别 

  

 

   

 

 指针变量的大小

 

 

 

 那么指针类型的意义是什么呢?

指针的解引⽤
//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}

//代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

 

 指针+-整数


#include <stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	return 0;
}

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

void* 指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。

 const修饰指针:

const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#include <stdio.h>
int main()
{
 int m = 0;
 m = 20;//m是可以修改的
 const int n = 0;
 n = 20;//n是不能被修改的
 return 0;
}

此时n不能被修改,但是我们倘若取到n的地址便就能够修改n的值了。

const修饰指针变量
这里希望大家自己研究一下挺有意思的。

指针运算,assert断言和传址调用 不做讲解

看到这里了必须加油

数组名的理解

我们之间概括一下这个:

数组名是首元素的地址,但是有两个例外

  sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,
单位是字节
  &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素
的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。

在这里就不写代码解释了,就是使用void*类型的指针兼容任何数据,使用其他类型的指针得选择相应的数据,否则会报警告。

使用指针访问数组:

冒泡排序

这个应该是写过好多次了,所以直接上手写他

void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//假设这⼀趟已经有序了
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				flag = 0;//发⽣交换就说明,⽆序
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
		if (flag == 1)//这⼀趟没交换就说明已经有序,后续⽆序排序了
			break;
	}
}
int main()
{
	int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

 

 ⼀维数组传参的本质

⾸先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给⼀个函
数后,函数内部求数组的元素个数吗?

 

我们发现在函数内部是没有正确获得数组的元素个数。 数组名是数组⾸元素的地址;那么在数组传参 的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。
所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写
sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函
数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

 

 ⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
所以存放指针变量的地址的变量就是指针。

 

*ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa *ppa 其实访问的就是 pa .
int b = 20 ;
*ppa = &b; // 等价于 pa = &b;

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .
**ppa = 30 ;
// 等价于 *pa = 30;
// 等价于 a = 30;

指针数组

指针数组是存放指针的数组。

 

 指针数组的每个元素是地址,⼜可以指向⼀块区域。

加油兄弟们完成5分之2了

指针数组模拟二维数组

这里类似于我们之后在讲解二维数组传参本质时的前一种用法,我们在这里写一下等到了那里就不写了哈。

#include <stdio.h>
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数组中
	int* parr[3] = { arr1, arr2, arr3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

 

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数
组中的元素。 上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。

在这里可能我们的解释没有太详细,因为后面会讲到。 

字符指针变量

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}

上述代码是一般使用支付指针变量时的用法

我们继续看下面的代码:

#include <stdio.h>
int main()
{
 const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 printf("%s\n", pstr);
 return 0;
}
代码 const char* pstr = "hello bit."; 特别容易让人以为是把字符串 hello bit
到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中。
 
我们再看一个笔试题:

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

 这里附上详细解释:

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

数组指针变量

顾名思义数组指针变量是一种指针变量

整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

 数组指针变量的解释

 

 

 数组指针变量怎么初始化:

过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,实参是⼆维数组,形参也写成⼆维数组的形式。

⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维
数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

 

所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀ 维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类
型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。

#include <stdio.h>
void test(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} };
	test(arr, 3, 5);
	return 0;
}

 

 //总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式

函数指针变量

函数指针创建的举例: 

函数指针类型解析:

    

//函数指针变量怎样使用呢?
//通过函数指针调用指针指向的函数
#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*pf)(int, int) = Add;

	printf("%d\n", (*pf)(5, 8));
	printf("%d\n", pf(9, 10));
	return 0;
}

注:两端有趣的代码我们在回顾中详细讲了,大家可以看一下

回顾

 

 

上图涉及的知识点有:

1.认识函数指针类型

2.知道强制类型转换

3。通过函数指针调用函数的方式

 

 

 

5分之3狠狠拿下

函数指针数组

函数指针数组的⽤途:转移表
举例:计算器的⼀般实现:

 那么函数指针数组的作用是什么呢?

#include <stdio.h>
//实现一个计算器:能够计算整数的加法减法乘法除法

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

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

}

 此时代码虽然运行并且结果正确,但却有自己的局限性 

在这里如果我们想要实现& |  ^  <<  >>  &&  || 我们可以看到写到菜单中和switch语句是很繁琐的,还有switch中的每一个选择中每次函数调用都会有重复的代码

如何解决呢?没错,此时就要用到我们的函数指针数组的方式

此时注意,函数指针数组的元素与菜单并不对应,因此改为这样。

#include <stdio.h>
//实现一个计算器:能够计算整数的加法减法乘法除法

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

}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		//函数指针数组的方式解决
		int(*pf[])(int, int) = { NULL,Add,Sub,Mul,Div };
		printf("请输入你的选择:");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret=pf[input](x, y);
			printf("%d\n", ret);
			break;
		}
		else
		{
			printf("选择错误,请重新选择\n");
		}
	} while (input);
	
	return 0;

}

 运行一下我们看结果

这样写的好处是以后可以随意增加别的函数 

上述代码的函数指针数组我们称为转移表

回调函数

回调函数是什么?
回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

对于上面那个繁琐的计算机实现,就是那个不用函数指针数组的,我们怎样更改呢?我们直接上正确代码:

#include <stdio.h>
//实现一个计算器:能够计算整数的加法减法乘法除法

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

}
void calc(int(*pf)(int, int))
{
	int ret = 0;
	int x, y;
	printf("请输入操作数:");
	scanf("%d%d", &x, &y);
	ret = pf(x, y);
	printf("ret=%d\n", ret);
}
int main()
{
	int input = 0;
	
	do
	{
		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");
			break;
	}
	} while (input);
	
	return 0;

}

qsort的用法

我们在这里 

 使⽤qsort函数排序整型数据

#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
int main()
{
	int arr[] = { 1,9,0,2,3,6,4,7,5,8 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	qsort(arr, sz, sizeof(int), cmp);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

 

 使⽤qsort排序结构数据

2个结构体的数据进行比较时,不能直接使用>=<来比较

可以按照结构体的某种类型的数据比较

比较姓名时需要用到strcmp函数,具体详情如下:

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student
{
	char name[50];//名字
	int age;//年龄
};
int cmp_name(const void* e1, const void* e2)
{
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
	//return strcmp((*(struct Student*)e1).name, (*(struct Student*)e2).name); 这样写也可以

}
void test1()
{
	struct Student s1[] = { {"yangzihao",18},{"mayun",40},{"liuqiangdong",45} };
	int sz = sizeof(s1) / sizeof(s1[0]);
	qsort(s1, sz,sizeof(s1[0]),cmp_name);
}
int main()
{
	//因为结构体有很多元素,所以我们写成封装函数形式
	test1();
	//test2();
}

 接下来我们实现排序结构体年龄,这个比上述排序姓名要简单一些。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student
{
	char name[50];//名字
	int age;//年龄
};
int cmp_name(const void* e1, const void* e2)
{
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
	//return strcmp((*(struct Student*)e1).name, (*(struct Student*)e2).name); 这样写也可以

}
int cmp_age(const void* e1, const void* e2)
{
	return ((struct Student*)e1)->age - ((struct Student*)e2)->age;
}
void test1()
{
	struct Student s1[] = { {"yangzihao",18},{"mayun",40},{"liuqiangdong",45} };
	int sz = sizeof(s1) / sizeof(s1[0]);
	qsort(s1, sz,sizeof(s1[0]),cmp_name);
}
void test2()
{
	struct Student s1[] = { {"yangzihao",18},{"mayun",40},{"liuqiangdong",45} };
	int sz = sizeof(s1) / sizeof(s1[0]);
	qsort(s1, sz, sizeof(s1[0]), cmp_age);
}
int main()
{
	//因为结构体有很多元素,所以我们写成封装函数形式
	//test1();
	test2();
}

qosort函数的模拟实现:

我们在模拟实现之前先来复习一下冒泡排序:

#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;
			}
		}
	}
}
void 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[] = { 1,4,3,2,5,7,6,9,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	print_arr(arr,sz);
    return 0;
}

 但是此时程序中的bubble_sort函数中的参数已经设计死了 

为了方便我们接下来写代码,我们把此时主函数中的代码封装到test(1)中。

qosort是一个库函数,可以直接使用

qsort的实现是使用快速排序算法来排序的

 

 此时要想把cmp_int传给qsort函数时,要关注上述图4中的函数的参数是什么样的,否则不能传给qsort

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

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i<sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
void test2()
{
	//升序
	int arr[] = { 1,4,3,2,5,7,6,9,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr,sz,sizeof(arr[0]),cmp_int);
	print_arr(arr, sz);
}
int main()
{
	//test1();
	test2();
	return 0;
}

 接下来我们就开始模拟实现qsort函数

改造函数的前提还是使用冒泡排序

首先要知道躺数和每一趟对数比较是不会改变的,就是和之前冒泡排序一样

我们先看看之前的冒泡排序实现中哪些需要改变:

 为了能够接收任意可能得指针类型,设计为void*

void类型的指针就像一个垃圾桶一样,谁的地址都能接收。

base是一个无具体类型的指针,他只知道他指向的一个数据从这里开始,但是他不知道他要排序的有几个元素。冒泡排序必须要知道元素个数,因为我们模拟qsort函数是从冒泡思想出发的,因此我们要传上要排序的元素个数,又因为元素个数不能是负数,因此元素个数的类型我们定义为size_t

size_t---------无符号整型

知道了元素的个数,还不知道元素是什么类型的,因此需要传参元素类型的大小

不同类型排序的比较不同,因此在参数中,我们需要用一个函数指针来指向另一个函数,让这个函数来实现不同类型排序的比较  在这里不知道是什么类型的指针,所以函数指针的写法为 

int (*cmp)(void*p1,void*p2)

这个函数指针指向的函数只是比较p1,p2指向的元素,不会修改p1 p2指向的内容

因此加上const进行限制 int (*cmp)(const void*p1,const void*p2)

到这里我们讲解了函数的所用的4个参数。

void shuai(void* base,size_t sz,size_t width,int (*cmp)(const void*p1,const void*p2))
{
	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)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}

那如何把这两个地址传给cmp呢?

用一个函数Swap 元素比较过程中,只告诉两个元素的起始地址也不够,元素的类型不知道,但是知道元素的宽度

因此此时元素交换有3个参数

我们不知道一个元素大小,因为我们知道宽度,所以我们一个字节一个字节交换。

因此此时Swap函数的具体实现为:

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

OK,接下来我们发一个整体代码

#include <stdio.h>
#include <stdlib.h>
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++;
	}
}
void shuai(void* base,size_t sz,size_t width,int (*cmp)(const void*p1,const void*p2))
{
	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)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i<sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

void test3()
{
	//设计和实现函数,这个函数能够排序任意类型的数据
	int arr[] = { 1,4,3,2,5,7,6,9,8,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	shuai(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
}
int main()
{
	//test1();
	//test2();
	test3();
	return 0;
}

 此时还是进行整数的排序,接下来我们将进行实现结构体的排序

至于为什么将地址转化为char*,其实很简单,因为不知道元素的大小,所以一个字节一个字节进行比较,进而进行交换。

结构体和数组的初始化都用{ }

此时结构体就相当于一个数据类型,这个在之前的博客中我们提到过

结构体之间的比较不能直接用大于或小于号比较

 废话少说,上代码。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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++;
	}
}
void shuai(void* base,size_t sz,size_t width,int (*cmp)(const void*p1,const void*p2))
{
	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)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}
struct Stu
{
	char name[20];
	int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
	//p1 p2指向的是两个结构体对象的
	strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void test4()
{
	struct Stu arr[] = { {"zhangsan",18},{"lisi",35},{"wangwu",15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	shuai(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	//打印arr数组的内容
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", arr[i].name, arr[i].age);
	}
}
int main()
{
	//test1();
	//test2();
	/*test3();*/
	test4();
	return 0;
}

 那次是是按名字来排序的,那能不能按照年龄来排序呢?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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++;
	}
}
void shuai(void* base,size_t sz,size_t width,int (*cmp)(const void*p1,const void*p2))
{
	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)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}
struct Stu
{
	char name[20];
	int age;
};
int cmp_stu_by_age(const void* p1, const void* p2)
{
	//p1 p2指向的是两个结构体对象的
	return ((struct Stu*)p1)->age-((struct Stu*)p2)->age;
}
void test4()
{
	struct Stu arr[] = { {"zhangsan",18},{"lisi",35},{"wangwu",15} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//shuai(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	shuai(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	//打印arr数组的内容
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", arr[i].name, arr[i].age);
	}
}
int main()
{
	//test1();
	//test2();
	/*test3();*/
	test4();
	return 0;
}

 

此时调试我们得到width的值为24 

整型指针+1跳过4个字节

 要跳过结构体的大小,最好将其转换为char*

假设一个元素是7个字节,那么该如何做呢,此时只能是一个字节一个字节的来

拥有这个函数指针,我们把不同数据比较的函数抽离了出来,当我们排序的时候,两个元素比较的方法一定会有差异。 因此我们得出结论,如果脱离了函数指针,那么这个函数指针就无法实现了。

快看完了老铁

sizeof和strlen的对比

sizeof

sizeof,sizeof计算变量所占内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。

sizeof只关注占用内存空间的大小,不在乎内存中放什么数据。

#include <stdio.h>
int main()
{
	int a = 10;
	printf("%zd\n", sizeof(a));
	printf("%zd\n", sizeof(int));
	//printf("%zd\n", sizeof int);error错误
	printf("%zd\n", sizeof a);

 a所占内存空间的大小是4个字节,用int所创建的变量的大小就是4个字节,int类型的长度是4个字节

int是类型,是个模具,用这个模具在内存空间中套出一块空间大小就是4个字节。

#include <stdio.h>
int main()
{
	//int a = 10;
	//printf("%zd\n", sizeof(a));
	//printf("%zd\n", sizeof(int));
	printf("%zd\n", sizeof int);error错误
	//printf("%zd\n", sizeof a);

	int arr1[4] = { 0 };
	char arr2[4] = { 0 };
	printf("%zd\n", sizeof(arr1));
	printf("%zd\n", sizeof(int[4]));

	printf("%zd\n", sizeof(arr2));
	printf("%zd\n", sizeof(char [4]);
	return 0;
}

 strlen

strlen 是C语言库函数,功能是求字符串长度,函数原型为:

int main()
{
	char arr[] = "abcdef";
	size_t len = strlen(arr);
	printf("%zd\n", len);
	return 0;
}

                                                                                                                                                                         strlen找的就是\0之前

#include <stdio.h>
int main()
{
	char arr[20] = "abcdef";
	size_t len = strlen(arr);
	printf("len=%zd\n", len);//6

	size_t sz = sizeof(arr);
	printf("sz=%zd\n", sz);//20
	return 0;
}

 我们再看下面这个代码,这个代码我们只知道开辟了一分空间arr(a  b  c),他的前面和后面我们都不知道,所以用strlen来求字符串长度时,会一直往后找\0,所以结果是随机值。

#include <stdio.h>
int main()
{
	//char arr[20] = "abcdef";
	//size_t len = strlen(arr);
	//printf("len=%zd\n", len);//6

	//size_t sz = sizeof(arr);
	//printf("sz=%zd\n", sz);//20

	char arr[] = { 'a','b','c' };
	printf("%zd\n", strlen(arr));
	return 0;
}

#include <stdio.h>
int main()
{
	
	char arr[6] = "abcdef";
	printf("%zd\n", sizeof(arr));//6
	printf("%zd\n", strlen(arr));//error 错误

	return 0;
}

  

 sizeof不挑类型,strlen只针对字符串

数组名的理解

数组名一般表示首元素的地址,但是有两个例外

1.sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节

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

#include <stdio.h>

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16 计算的是整个数组的大小
	printf("%d\n", sizeof(a + 0));//4/8 数组首元素地址,具体大小看环境
	printf("%d\n", sizeof(*a));//4 首个元素
	printf("%d\n", sizeof(a + 1));//4/8  第二个元素的地址
	printf("%d\n", sizeof(a[1]));//4   第二个元素
	printf("%d\n", sizeof(&a));//4/8 取出的是整个数组的地址,但也仅仅是个地址
	printf("%d\n", sizeof(*&a));//16 int(*)[4]=&a;
	//&a的类型是数组指针,int(*)[4],*&a就是对数组指针解引用访问一个数组的大小,是16个字节
	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;
}

#include <stdio.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));//6
	printf("%d\n", sizeof(arr + 0));//4/8  第一个元素地址
	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;
}

#include <stdio.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));//随机值
	printf("%d\n", strlen(arr + 0));//随机值
	//printf("%d\n", strlen(*arr));//error
	//printf("%d\n", strlen(arr[1]));//error
	printf("%d\n", strlen(&arr));//随机值
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//随机值
	return 0;
}

#include <stdio.h>
int main()
{
	char arr[] = "abcdef";	//7个元素
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));
	return 0;
}

此时注意,应该使用%zd打印,否则vs编译器就会给出警告

因为前面的讲解已经很详细了,所以在此就一带而过了 

#include <stdio.h>
int main()
{
	char arr[] = "abcdef";	//7个元素
	printf("%zd\n", sizeof(arr));//7  之前讲的已经很清楚了
	printf("%zd\n", sizeof(arr + 0));//4/8 地址
	printf("%zd\n", sizeof(*arr));//1
	printf("%zd\n", sizeof(arr[1]));//1
	printf("%zd\n", sizeof(&arr));//4/8
	printf("%zd\n", sizeof(&arr + 1));//4/8
	printf("%zd\n", sizeof(&arr[0] + 1));//4/8  
	return 0;
}

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abcdef";
	printf("%zd\n", strlen(arr));//6
	printf("%zd\n", strlen(arr + 0));//arr+0数组首元素的地址,6
	//printf("%zd\n", strlen(*arr));// 传递的是字符a也就是97,error
	//printf("%zd\n", strlen(arr[1]));//error
	printf("%zd\n", strlen(&arr));//6
	printf("%zd\n", strlen(&arr + 1));//随机值
	printf("%zd\n", strlen(&arr[0] + 1));//5
	return 0;
}

printf("%zd\n", strlen(&arr));//6

此时这条语句会报警告

 因为strlen的参数是char*的,而&arr取出的是整个数组的地址

#include <stdio.h>
int main()
{
	char* p = "abcdef";//一级指针变量
	printf("%zd\n", sizeof(p));//4/8
	printf("%zd\n", sizeof(p + 1));//4/8
	printf("%zd\n", sizeof(*p));//1
	printf("%zd\n", sizeof(p[0]));//1 p[0]--->*(p+0)-*p
	printf("%zd\n", sizeof(&p));//4/8 &p也是地址,是指针变量的地址,二级指针
	printf("%zd\n", sizeof(&p + 1));//4/8  &p+1是指向p指针变量后面的空间,也是地址
	printf("%zd\n", sizeof(&p[0] + 1));//4/8 p[0]是数组的第一个元素,&地址后加1,变为b的地址
	return 0;
}

//int a=10;
//int* p = &a;
//p + 1;//p跳过一个整型

//char* p
//char* *q=&p;
//q+1//跳过char* 的变量

 

#include <stdio.h>
int main()
{
	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 p[0]----*(p+0)-->*p
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5
	return 0;
}

#include <stdio.h>
int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a)); 
	printf("%d\n", sizeof(a[0][0]));
	printf("%d\n", sizeof(a[0]));
	printf("%d\n", sizeof(a[0] + 1));
	printf("%d\n", sizeof(*(a[0] + 1))); 
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(*(a + 1)));
	printf("%d\n", sizeof(&a[0] + 1));
	printf("%d\n", sizeof(*(&a[0] + 1))); 
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a[3]));
	return 0;
}

 这是我们假设想象的二维数组,真正的二维数组存储是连续的,就是第一行之后紧接着存储第二行,第二行后面紧接着第三行。

 

#include <stdio.h>
int main()
{
	//注意:二维数组的首元素就是第一行
	//二维数组也是数组,之前对数组名的理解也是适合的
	//
	int a[3][4] = { 0 };
	printf("%zd\n", sizeof(a)); //48
	printf("%zd\n", sizeof(a[0][0]));//4
	printf("%zd\n", sizeof(a[0]));//16  4*4 是第一行这个一维数组的数组名,数组名单独放在
	//sizeof内部了  计算的是第一行的大小,单位是16个字节
	printf("%zd\n", sizeof(a[0] + 1));//4/8 a[0]第一行这个一维数组的数组名,
	//这里表示数组首元素,也就是a[0][0]的地址,a[0]+1是a[0][1]的地址

	printf("%zd\n", sizeof(*(a[0] + 1)));//4  a[0][1]
	printf("%zd\n", sizeof(a + 1));//4/8  a是二维数组的数组名,但是没有&,也没有单独放在
	//sizeof内部 所以这里的a是数组首元素的地址,应该是第一行的地址,a+1是第二行的地址
	printf("%zd\n", sizeof(*(a + 1)));//16 第二行的地址解引用,相当于拿到了第二行
	//第二行是一个一维数组的地址进行解引用,拿到了整个一维数组

	//另一种理解方式*(a + 1)相当于==>a[1],第二行的数组名单独放在sizeof中
	//计算的是第二行的大小
	printf("%zd\n", sizeof(&a[0] + 1));//4/8 &a[0]是第一行的地址,&a[0]+1就是第二行的地址
	printf("%zd\n", sizeof(*(&a[0] + 1))); //16
	 // int(*p)[4]=&a[0] + 1
	printf("%zd\n", sizeof(*a));//16 
	//这里的a是第一行的地址,*a就是第一行,sizeof(*a)计算的就是第一行的大小
	//*a----->*(a+0)------>a[0]
	printf("%zd\n", sizeof(a[3]));//16 
	// 我们在这里的第一反应都是越界了,但事实是这里不存才越界
	//sizeof内部的表达式不会真实计算的
	//计算的是第四行的大小
	return 0;
}

 在这里,我们再次强调数组名的理解:

 

  数组和指针笔试题解析

 题目一:

#include <stdio.h>

int main()
{
	int a[5] = { 1,2,3,4,5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
	return 0;
}

&a取出的是整个元素的地址。 

 题目二:

//题⽬2
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?

struct Test
{
	
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

 

 

如果改为%x之后打印出来的结果的0就会省略掉

 题目三:

#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数组中存的值是不是和我们预想的不太一样

那是因为数组初始化赋值是用了小括号,若按照小括号这种思路其实就是按照逗号表达式存放的·

 

 题目四:

//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
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个int类型的元素

 

内存中的地址不存在原码反码补码,所以打印内存中的地址,就是打印补码,这样不太准确,以16进制打印补码

数组随着下标的增长,地址是由低到高变化的。

  题目五:


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

 &aa代表整个数组,+1跳过整个数组

aa代表首元素调用,+1代表第二行的首元素的地址

 

 题目六:

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

 

题目七:

#include <stdio.h>
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的理解: cpp+1,cpp指向的值变化,然后两次解引用,第一次解引用得到c+2的地址,第二次解引用得到point的地址

所以第一次打印出的字符串为point

对*--*cpp+3详细的解释:此时我们用画图来直观地看一下:

 对*cpp【-2】+3详细的解释,我们同样用图直观的表现出来:

   对cpp[-1][-1]+1,我们同样用上述图的方法来表示:

 

   恭喜你,也谢谢你支持我的文章,如果觉得文章不错,就点赞收藏加评论。

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值