C语言进阶--指针(C语言灵魂)

news2024/11/26 5:50:37

目录

1.字符指针

2.指针数组

3.数组指针

4.数组参数与指针参数

4.1.一维数组传参

4.2.二维数组传参

4.3.一级指针传参

4.4.二级指针传参

5.函数指针

6.函数指针数组

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

8.回调函数

qsort函数

9.指针和数组笔试题

10.指针笔试题


前期要点回顾:

  1. 指针是个变量,用来存放地址,而地址则唯一标识一块内存空间;
  2. 内存会划分为小的内存单元,每个内存单元都有一个编号,这个编号就称为地址,而地址也就是所谓的指针,内存编号=地址=指针;
  3. 指针或地址要进行存储,则可以存放到指针变量中去;
  4. 指针的大小是固定的4/8个字节(32位平台/64位平台);
  5. 指针是有类型的,指针的类型决定了指针的+或-整数的步长,指针解引用操作时的权限;
  6. 指针+-整数,指针-指针,指针的关系运算; 

前期知识回顾:C语言深度解析--指针

1.字符指针

字符串字面量,或称为常量,或称为字面量。其含义是在程序执行过程中保持不变的数据。从本质而言,C语言把字符串字面量作为字符数组来处理。当C语言编译器在程序中遇到长度为n的字符串字面量时,它会为字符串字面量分配长度为n+1的内存空间。这块内存空间将用来存储字符串字面量中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是一个所有位都为0的字节,因此用转义字符\0来表示。

注意:不要混淆空字符'\0'和零字符'0'。空字符的码值为0,而零字符则有不同的码值(ASCII中为48)。

既然字符串字面值是作为数组来存储的,那么编译器会把它看作是char*类型的指针。char* p="abc";这个赋值操作不是复制"abc"中的字符,而是使p指向字符串的第一个字符。试图改变字符串字面量会导致未定义的行为,会导致程序崩溃或运行不稳定。

字符串字面量与字符常量:只包含一个字符的字符串字面量不同于字符常量。字符串字面量"a"是用指针来表示的,这个指针指向存放字符"a"(后面紧跟空字符)的内存单元。字符常量'a'是用整数来表示的。

字符数组和字符指针:char date[]="June 14";  char* date="June 14";                                             在声明为数组时,就像任意数组元素一样,可以修改存储在date中的字符;声明为指针时,date指向字符串字面值,是不可以修改的;
在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序执行期间指向其他字符串

字符指针,即指向字符的指针。有两种不同的运行场景,其一:指向字符(可以改变字符常量);其二:指向字符串(不可以改变字符串常量)。

 char *p = "I am happy";

场景一:指向字符(可以改变字符常量)

int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'a';

	printf("%c\n",ch);

	return 0;
}

场景二:指向字符串(不可以改变字符串常量)

int main()
{
	const char* p = "abcdef";//把字符串常量"abcdef"的首字节的地址放到p中

	//字符串常量"abcdef",不能进行修改,要加const进行保护
	//*p = 'w';//错误的,不能修改
	
	printf("%c\n",*p);//a

	printf("%s\n",p);//abcdef

	return 0;
}

代码const char* p = "abcdef";不是把字符串abcdef放进字符指针p里面,而是把字符串abcdef首字符的地址放到p中去。

字符指针和数组的功能相类似。当我们对字符指针进行解引用时,得到的是字符串的首元素;当我们对数组名进行解引用时,得到的是数组的首元素。

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p = arr;

	printf("%d\n",*p);//1

	return 0;
}

案例分析:

int main()
{
	const char* p1 = "abcdef";
	const char* p2 = "abcdef";

	char arr1[] = "abcdef";
	char arr2[] = "abcdef";

	if (p1 == p2)
	{
		printf("p1==p2\n");
	}
	else
	{
		printf("p1!=p2\n");
	}

	if (arr1 == arr2)
	{
		printf("arr1==arr2\n");
	}
	else
	{
		printf("arr1!=arr2\n");
	}

	return 0;
}

 

对于常量字符串, C/C++会把常量字符串存储到文字常量区,其内容在程序运行期间会一直存在,不会释放。且在文字常量区只有一份拷贝,不会出现相同的变量和常量的不同拷贝。因此,当几个指针指向同一个字符串时,它们实际会指向同一块内存。所以p1和p2共同指向字符串常量“abcdef”,不会出现对同一内容的不同拷贝。而当我们用相同的常量字符串去初始化不同的数组时就会开辟不同的内存块。在C语言中,一般将数组的创建存放在栈区,而栈区所存放的内容一般不同于文字常量区。

2.指针数组

指针数组,即存放指针的数组。指针数组本身是个数组,是个指针数组,是若干个相同类型的指针变量构成的集合。

int* p1[10];//整型指针的数组

因为优先级的关系,p1要先和[ ]结合,说明p1是个数组,然后再与*结合说明数组p1的元素是指向整型数据的指针。元素分别为p1[0],p1[1],...,p1[9],相当于定义了10个整型指针变量,用于存放地址单元。

 案例一:

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* p1 = &a;
	int* p2 = &b;
	int* p3 = &c;

	int* arr[3] = {&a,&b,&c};//arr就是一个指针数组

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		//printf("%d ", *(arr[i]));
		printf("%d ",*(*(arr+i)));
	}

	return 0;
}

案例二:

int main()
{
	int arr1[5] = {1,2,3,4,5};
	int arr2[5] = {6,7,8,9,10};
	int arr3[5] = {11,12,13,14,15};

	int* parr[3] = {arr1,arr2,arr3};//数组名表示首元素的地址

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

	return 0;
}

3.数组指针

数组指针,即指向数组的指针,专门用来存放一个数组的地址。数组指针本身是个指针,指向一个数组,加1跳过一个数组,即指向下一个数组。常用于二维数组。

int(*p)[10];

解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,所以p是一个指针,指向一个数组,叫数组指针。

int main()
{
	int arr[10] = {0};

	printf("%p\n",arr);//数组名是数组首元素的地址

	printf("%p\n",&arr[0]);

	printf("%p\n",&arr);//类型是int(*p)[10]

	int(*p)[10] = &arr;
	//p是一个指针,指向了数组,所以p叫作数组指针,它的类型是:int(*)[10],表示的是整个数组的地址(注意:&数组名取出的是整个数组的地址),所以p等价于&arr

	printf("%p\n",p);

	return 0;
}

数组名该怎么理解?

通常情况下,我们所说的数组名都是指数组首元素的地址
两个例外:

  1. sizeof(数组名),这里的数组名表示的是整个数组,sizeof(数组名)计算的是整个数组的大小
  2. &数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址 

那数组指针如何使用?                                                              

案例一:一维数组的打印

//形参写成数组的形式
void print1(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);
	}

	printf("\n");
}

//形参写成指针的形式
void print1(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",*(arr+i));
	}

	printf("\n");
}

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	//写一个函数打印arr数组的内容

	int sz = sizeof(arr) / sizeof(arr[0]);
	print1(arr,sz);

	return 0;
}
//形参写成数组指针的形式
//不是推荐写法
void print1(int(*p)[10], int sz)
{
	int i = 0;

	for (i = 0; i < sz; i++)
	{
		//*p相当于数组名,数组名又是首元素的地址,所以*p就是&arr[0]
		printf("%d ",*(*p+i));//p相当于&arr,解引用*p得arr
	}
	printf("\n");
}

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};

	//写一个函数打印arr数组的内容
	int sz = sizeof(arr) / sizeof(arr[0]);

	print1(&arr,sz);//传过去的是整个数组的地址,对应的形参是数组指针

	return 0;
}

案例二:二维数组的打印(数组指针的主要用途)

//形参写成数组的形式
void print2(int arr[3][5], int c, int r)
{
	int i = 0;

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

	printf("\n");
}

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

	//写一个函数,打印arr数组
	print2(arr,3,5);

	return 0;
}
//形参写成指针数组的形式
//p相当于a
void print2(int (*p)[5],int c,int r)
{
	int i = 0;
	for (i = 0; i < c; i++)
	{
		int j = 0;
		for (j = 0; j < r; j++)
		{
			//p+i指向的是第i行
			//*(p+i)相当于拿到了第i行的数组名
			//数组名表示首元素的地址,*(p+i)就是第i行第1个元素的地址
			printf("%d ", * (*(p + i) + j));
		}
		printf("\n");
	}
	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,表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址

	return 0;
}

在二维数组中

注意:

  1. arr[i]相当于*(arr+i)
  2. arr[i][j]相当于*(*(arr+i)+j)       

小结:数组指针与指针数组的区别

指针数组

  1. 是个数组,由若干个相同类型的指针构成的集合
  2. int* p[10];//数组p有10个int*类型的指针变量构成,分别是p[0]-p[9]

数组指针

  1. 是个指针,指向一个数组,+1跳过一个数组
  2. int(*p)[10];//p是个指针,指向数组的指针,p+1指向下一个数组,跳过10个整型
int main()
{
	int arr[5];//arr是一个整型数组,每个元素是int类型,有5个元素

	int* parr1[10];//parr1是一个数组,数组包含10个元素,每个元素的类型是int*

	int(*parr2)[10];//parr2是一个指向数组的指针,指向的是一个包含10个int元素的数组,每个元素的类型是int,parr2的类型是int(*)[10]

	int(* parr3[10])[5];//parr3要先和[10]结合,所以parr3是个数组,数组有10个元素,每个元素的类型是:int(*)[5]的数组指针
    //parr3是存放数组指针的数组
}

4.数组参数与指针参数

4.1.一维数组传参

//形参写成数组的形式:
void test(int arr[10]) {}

void test(int arr[]) {}//形参部分的数组大小可以省略

void test(int arr[100]) {}//不建议,但是没错


//形参写成指针的形式:
//数组名在传参时,本质上传的数组首元素的地址,可以写一个指针来接收它
void test(int* p) {}

int main()
{
	int arr[10] = {0};

	test(arr);//一维数组如何传参

	return 0;
}
//形参写成数组的形式
void test2(int* arr[20]) {}

void test2(int* arr[]) {}


//形参写成指针的形式
void test2(int** arr) {}//数组名表示首元素的地址,而首元素是int*类型,所以首元素的地址是int*变量的地址,因此要用二级指针int**来接收其地址

int main()
{
	int* arr2[20] = {0};

	test2(arr2);

	return 0;
}

4.2.二维数组传参

//形参写成数组的形式
void test(int arr[3][5]) {}

void test(int arr[][5]) {}//行可以省略,但是列不可以省略;因为二维数组本质上是连续存放的,第一行后面跟着第二行,第二行的后面跟着第三行,以此类推

void test(int arr[][]) {}//err,列不能省略


//形参写成指针的形式
//数组名传参时,本质上传的是数组首元素的地址,而二维数组的首元素是第一行,所以首元素的地址也就是第一行的地址,而能用于接收第一行地址的就是数组指针,所以形参是数组指针类型
void test(int(*p)[5]) {}

void test(int **arr) {}//err,二级指针不能匹配一维数组的地址

int main()
{
	int arr[3][5] = {0};

	test(arr);

	return 0;
}

注意:

  1. 二级指针是用来存放一级指针的地址的
  2. 数组指针是用来存放数组的地址的  

4.3.一级指针传参

//一级指针接收
void test(int* ptr,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",*ptr);
		ptr++;
	}
}

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]);
	test(p,sz);//p是一级指针

	return 0;
}

思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

//test函数能接收什么参数
void test(int* ptr) {}

int main()
{
	int a = 10;
	int* p = &a;
	int arr[10];

	test(&a);//变量的地址
	test(p);//指针变量
	test(arr);//一维数组名

	return 0;
}

4.4.二级指针传参

//二级指针接收
void test(char** ppc) 
{
	**ppc = 'v';
	printf("a = %c\n", **ppc);
}

int main()
{
	char a = 'w';
	char* pa = &a;
	char** ppa = &pa;//ppa就是一个二级指针

	test(ppa);

	return 0;
}

思考:当一个函数的参数部分为二级指针的时候,函数能接收什么参数?  

void test(char** ppc) {}

int main()
{
	char ch = 'a';
	char* pc = &ch;
	test(&pc);//一级指针地址

	char** ppc = &pc;
	test(ppc);//二级指针


	char* arr[4];
	test(arr);//指针数组,数组名表示首元素的地址,而首元素是char*类型,所以首元素的地址是char*类型,因而要用二级指针char**来接收其地址

	return 0;
}

5.函数指针

函数指针,即指向函数的指针,是存放函数地址的指针。

函数指针是指向函数的指针变量。函数指针首先应是指针变量,只不过该指针变量指向函数。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数。函数指针有两个用途:调用函数和做函数的参数。

函数指针的声明方法:

返回值类型 ( *指针变量名) ([形参列表]);

  1. 返回值类型说明函数的返回类型;
  2. (指针变量名 )中的括号不能省,括号改变了运算符的优先级。若省略整体则成为一个函数说明,说明了一个返回的数据类型是指针的函数;
  3. 形参列表表示指针变量指向的函数所带的参数列标
int Add(int x, int y)
{
	return x + y;
}

int test(char* str)
{

}

int main()
{
	printf("%p\n",&Add);//取函数的地址
	printf("%p\n", Add);//取函数的地址
	//两种方式都叫取函数的地址,意义是相同的


	int(*pf)(int, int) = Add;//pf就是函数Add的指针变量,它的类型是int (*)(int,int)

	int (*pt)(char*) = test;//pt就是函数test的指针变量


	//int ret = Add(2,3);
	//int ret = (*pf)(2, 3);//*pf表示解引用,相当于Add

	int ret = pf(2, 3);//*的存在没什么意义,可有可无;若要加*,则一定要加上括号


	printf("%d\n",ret);

	return 0;
}

小结:

  1. 函数有自己的地址,函数名或&函数名就是函数的地址。在对函数取地址时,&可加可不加;
  2. 在使用函数指针调用函数时,*可有可无;若要加*,则一定要加上括号

案例一:

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

详解:

  1. void (*)()是函数指针类型
  2. ( void (*)() )是强制类型转换
  3. ( void (*)() )0:对0进行强制类型转换
  4. (* ( void (*)() ) 0):0是函数的地址,对函数地址0进行解引用相当于调用这个函数
  5. void ( * ( void (*)() ) 0 )():调用这个函数时,不需要传入参数,返回值类型为void                    

总结:首先是把0强制类型转换为函数指针类型,这就意味着0地址处放一个返回值类型是void的无参的一个函数;然后调用0地址处的这个函数。

案例二:

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

详解:

  1. signal先和()结合,说明signal是函数名
  2. signal函数的参数,第一个是int类型,第二个是void(*)(int)的函数指针类型
  3. signal函数的返回值类型也是:void(*)(int)的函数指针类型

总结:signal是一个函数的声明。

对案例二进行简化:

void ( *signal ( int, void (*)(int) ) ) (int);类似于:void(*)(int) signal(int,void(*)(int)),但是不能这样写,主要是便于理解。

//进行简化
typedef void(*pf_t)(int);//给函数指针类型void(*)(int)重新起名字叫:pf_t

//typedef void(*)(int) pfun_t;//err,pfun_t不能写在后面

pf_t signal(int, pf_t);

6.函数指针数组

函数指针数组,即函数指针是个数组,存放的是函数指针,也就是函数的地址。前提:这些函数的参数类型、返回类型一致。

int(* pf[4])(int,int):pf首先是和[]结合,说明pf是个数组,类型为:int(*)(int,int)

当我们对同一类型的函数进行多次函数调用时,我们会发现非常繁琐,比如下面的代码实现:

int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;

而当我们使用函数指针数组来进行函数调用时,就可以实现化繁为简,如下所示

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 (*pf[4])(int, int) = {Add,Sub,Mul,Div};//4可以省略

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		//int ret = pf[i](8, 2);
		int ret = (*pf[i])(8, 4);
		printf("%d\n",ret);
	}

	return 0;
}

函数指针数组的用途:转移表

案例:简单计算器模拟实现

初阶版:

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;
	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("ret=%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret=%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret=%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret=Div(x, y);
			printf("ret=%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
		
	} while (input);

	
	return 0;
}

当我们编译完整个程序可以发现,整个程序的代码量是非常冗余的。在switch分支语句中,存在着大量相同的代码,这为程序的可读性带来一些不必要的麻烦。那如何对初阶版的程序进行改进呢?这时就要想到我们的函数指针数组。

进阶版:

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;
	int x = 0;
	int y = 0;
	int ret = 0;

	//转移表
	int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);

		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret=%d\n",ret);
		}

	} while (input);


	return 0;
}

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

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

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

int main()
{
    //数组指针
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int(*p)[10] = &arr;

	int* arr2[5];
	int* (*p2)[5] = &arr2;

	//函数指针
	int (*pf)(int, int) = &Add;

	//函数指针数组
	int (*pfarr[4])(int, int) = {0};

	int(*(*p3)[4])(int,int)=&pfarr;//p3和*结合,说明p3是个指针,指针指向的是含有4个元素的数组,数组的每个元素类型是:int(*)(int,int)的函数指针
	//p3是一个指向函数指针数组的指针
	//*p3-->pfarr
	//(*p3)[i]-->pfarr[i]

}

案例:

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(*pfarr[4])(int, int) = {Add,Sub,Mul,Div};

	//指向函数指针数组的指针
	int(*(*p3)[4])(int, int) = &pfarr;

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		//int ret=(*p3)[i](4, 4);
		int ret = p3[0][i](4, 4);//*(p3+0)等价于p3[0]

		printf("%d\n",ret);
	}

	return 0;
}

8.回调函数

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

在C语言中,回调函数只能使用函数指针实现。回调函数的使用可以大大提升编程的效率,这使得它在现代编程中被非常多地使用。同时,有一些需求必须要使用回调函数来实现。最著名的回调函数有C标准库中的快速排序函数qsort。

案例一:

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

void print_hehe(void (*p)())
{
	if (1)
	{
		(*p)();
		//p();
	}
}

int main()
{
	//test();

	print_hehe(test);

	return 0;
}

案例二:对<简单计算器模拟实现>的程序进行回调函数设计

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 = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d",&x,&y);
	//ret = pf(x, y);
	ret = (*pf)(x, y);

	printf("ret=%d\n",ret);

}

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;

	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函数

模拟实现整型冒泡排序算法:

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

int main()
{
	int arr[] = {9,8,7,6,5,4,3,2,1,0};

	int sz = sizeof(arr) / sizeof(arr[0]);
	
	//排序,为升序
	bubble_sort(arr, sz);

	//打印
	print_arr(arr, sz);

	return 0;
}

如何实现不同数据类型的排序呢?qsort函数

qsort函数详解:
头文件:#include<stdlib.h>
void qsort(void* base, size_t num, size_t size, int (*comparator) (const void*, const void*));
base:存放待排序数据的起始位置,指向要排序的数组的第一个元素的指针
num:待排序数组的元素个数,由base指向的数组中元素的个数
width:一个元素的字节大小,数组中每个元素的大小,以字节为单位

int comparator ( const void * elem1, const void * elem2 ):用于比较两个元素的函数,即函数指针(回调函数)
如果compar返回值小于0(< 0),那么p1所指向元素会被排在p2所指向元素的左面;
如果compar返回值等于0( = 0),那么p1所指向元素与p2所指向元素的顺序不确定;
如果compar返回值大于0( > 0),那么p1所指向元素会被排在p2所指向元素的右面。
comparator:比较函数
elem1,elem2:待比较的两个元素的地址

基于qsort函数来模拟实现整型冒泡排序算法:

//使用qsort排序整数
int cmp_int(const void* e1, const void* e2)
{
	//void*类型的指针是不能直接进行解引用的
	//void*可以用于接收任何类型的地址

	//if (*(int*)e1 > *(int*)e2)
	//{
	//	return 1;
	//}
	//else if (*(int*)e1 == *(int*)e2)
	//{
	//	return 0;
	//}
	//else
	//{
	//	return -1;
	//}

	//简化版
	return (*(int*)e1 > *(int*)e2);//两个整型相减
}

void test()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };

	//排序为升序
	int sz = sizeof(arr) / sizeof(arr[0]);

	//要求qsort函数的使用者,自定义一个比较函数cmp_int
	qsort(arr,sz,sizeof(arr[0]),cmp_int);

	print_arr(arr, sz);
}


int main()
{
	test();

	return 0;
}

基于qsort函数来模拟实现结构体成员变量排序算法:

//使用qsort排序结构体
struct Stu
{
	char name[20];
	int age;
	double score;
};

//升序
//比较年龄大小
int cmp_stu_by_age(const void*e1,const void*e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

//降序
//int cmp_stu_by_age(const void* e1, const void* e2)
//{
//	return ((struct Stu*)e2)->age - ((struct Stu*)e1)->age;
//}

//比较姓名首字母大小
int cmp_stu_by_name(const void* e1, const void* e2)
{
	//strcmp用于比较两个字符串并根据比较结果返回整数,基本形式为strcmp(str1, str2)
	//若str1 = str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
	return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name);
}

void test()
{
	struct Stu arr[3] = { {"zhangsan",20,55.5},{"lisi",30,88},{"wangwu",10,90} };

	int sz = sizeof(arr) / sizeof(arr[0]);

	//要求qsort函数的使用者,自定义一个比较函数
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);

	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);

}

int main()
{
	test();

	return 0;
}

模拟实现qsort(采用冒泡的方式) :

比较整数:

//比较大小
int cmp_int(const void* e1, const void* e2)
{
	//if (*(int*)e1 > *(int*)e2)
	//{
	//	return 1;
	//}
	//else if (*(int*)e1 == *(int*)e2)
	//{
	//	return 0;
	//}
	//else
	//{
	//	return -1;
	//}

	return (*(int*)e1 > *(int*)e2);//两个整型相减
}

//交换数据
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 Swap(void* buf1, void* buf2, int width)
//{
//	//一次交换一对字节
//	int i = 0;
//	for (i = 0; i < width; i++)
//	{
//		char tmp = *(char*)buf1;//先强制类型转换,再解引用
//		*(char*)buf1 = *(char*)buf2;
//		*(char*)buf2 = tmp;
//		((char*)buf1)++;
//		((char*)buf2)++;
//	}
//}

//使用回调函数,模拟实现qsort(采用冒泡的方式)
void bubble_sort(void* base, int num, int width, int(*cmp)(const void* e1, const void* e2))
{
	int i = 0;
	for (i = 0; i < num - 1; i++)//趟数
	{
		int j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			//width:一个元素的字节大小,数组中每个元素的大小,以字节为单位
			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]);
	}
}

//比较整型
void test()
{
	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);

	print_arr(arr, sz);

	return 0;
}

int main()
{
	test();

	return 0;
}

比较结构体成员变量:

//比较大小
int cmp_int(const void* e1, const void* e2)
{
	//if (*(int*)e1 > *(int*)e2)
	//{
	//	return 1;
	//}
	//else if (*(int*)e1 == *(int*)e2)
	//{
	//	return 0;
	//}
	//else
	//{
	//	return -1;
	//}

	return (*(int*)e1 > *(int*)e2);//两个整型相减
}

//交换数据
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 Swap(void* buf1, void* buf2, int width)
//{
//	//一次交换一对字节
//	int i = 0;
//	for (i = 0; i < width; i++)
//	{
//		char tmp = *(char*)buf1;//先强制类型转换,再解引用
//		*(char*)buf1 = *(char*)buf2;
//		*(char*)buf2 = tmp;
//		((char*)buf1)++;
//		((char*)buf2)++;
//	}
//}

//使用回调函数,模拟实现qsort(采用冒泡的方式)
void bubble_sort(void* base, int num, int width, int(*cmp)(const void* e1, const void* e2))
{
	int i = 0;
	for (i = 0; i < num - 1; i++)//趟数
	{
		int j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			//width:一个元素的字节大小,数组中每个元素的大小,以字节为单位
			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;
	double score;
};

//升序
//比较年龄大小
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 test()
{
	struct Stu arr[3] = { {"zhangsan",20,55.5},{"lisi",30,88},{"wangwu",10,90} };

	int sz = sizeof(arr) / sizeof(arr[0]);

	//qsort(arr,sz,sizeof(arr[0]),cmp_stu_by_age);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);

}

int main()
{
	
	test();

	return 0;
}

9.指针和数组笔试题

一维数组:

int main()
{
	int a[] = {1,2,3,4};

	//数组名是什么?
	//数组名通常来说是数组首元素的地址
	//两例外:1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
	//       2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址,+1则跳过整个数组


	printf("%d\n",sizeof(a));//16,sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小

	printf("%d\n", sizeof(a+0));//4,数组名通常来说是数组首元素的地址,a+0是数组第一个元素的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*a));//4,a表示数组首元素的地址,*a表示对数组首元素的地址进行解引用,即得到数组的第一个元素,sizeof(*a)是计算第一个元素的大小

	printf("%d\n", sizeof(a+1));//4,a表示数组首元素的地址,a+1表示第二个元素的地址,sizeof(a+1)就是第二个元素地址的大小,是地址大小就为4/8

	printf("%d\n", sizeof(a[1]));//4,计算的是第二个元素的大小

	printf("%d\n", sizeof(&a));//4,&a取出的是整个数组的地址,数组的地址也是地址,是地址大小就为4/8

	printf("%d\n", sizeof(*&a));//16,*&a等价于a,而sizeof(a)计算的是整个数组的大小

	printf("%d\n", sizeof(&a+1));//4,&a取出的是整个数组的地址,+1则跳过整个数组,产生的是元素4后面位置的地址,是地址大小就为4/8

	printf("%d\n", sizeof(&a[0]));//4,&a[0]取出的是数组第一个元素的地址,是地址大小就为4/8

	printf("%d\n", sizeof(&a[0]+1));//4,&a[0]+1取出的是数组第二个元素的地址,是地址大小就为4/8

	return 0;
}

字符数组:

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };//[a b c d e f]

	//sizeof() 和 strlen() 的主要区别在于:
	//sizeof() 是一个运算符,而 strlen() 是一个函数。
	//sizeof() 计算的是变量或类型所占用的内存字节数,而 strlen() 计算的是字符串中字符的个数。
	//sizeof() 可以用于任何类型的数据,而 strlen() 只能用于以空字符 '\0' 结尾的字符串。
	//sizeof() 计算字符串的长度,包含末尾的 '\0',而strlen() 计算字符串的长度,不包含字符串末尾的 '\0'。

	
	printf("%d\n", sizeof(arr));//6,sizeof只计算所占内存空间的大小,不在乎是否存在\0

	printf("%d\n", sizeof(arr + 0));//4,数组名表示数组首元素的地址,arr+0是数组第一个元素的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*arr));//1,arr表示数组首元素的地址,*arr表示对数组首元素的地址进行解引用,即得到数组的第一个元素,sizeof(*arr)是计算第一个元素的大小

	printf("%d\n", sizeof(arr[1]));//1,arr[1]是数组的第二个元素,大小为1个字节

	printf("%d\n", sizeof(&arr));//4,&arr取出的是整个数组的地址,数组的地址也是地址,是地址大小就为4/8

	printf("%d\n", sizeof(&arr + 1));//4,&arr取出的是整个数组的地址,+1则跳过整个数组,产生的是字符'f'后面位置的地址,是地址大小就为4/8

	printf("%d\n", sizeof(&arr[0] + 1));//4,&arr[0]+1取出的是数组第二个元素的地址,是地址大小就为4/8



	//strlen()函数只能用于计算以空字符 '\0' 结尾的字符串的长度,如果字符串中没有空字符,则 strlen() 函数的行为是未定义的
	//strlen()它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0')
	//strlen只能用char*做参数,且必须是以'\0'结尾的

	
	printf("%d\n", strlen(arr));//随机值19,arr数组中没有\0,所以strlen函数会从首元素开始依次向后寻找\0,统计\0之前出现的字符个数

	printf("%d\n", strlen(arr + 0));//随机值19,arr+0是数组首元素的地址,strlen函数还会继续从开头往后找\0,统计\0之前出现的字符个数

	//printf("%d\n", strlen(*arr));//错误,没有提供一个有效的地址,arr是数组首元素的地址,*arr是数组的首元素,‘a’-97

	//printf("%d\n", strlen(arr[1]));//错误,同上,'b' - 98

	printf("%d\n", strlen(&arr));//随机值19,&arr取出的是整个数组的地址,该值等于数组首元素的地址,所以strlen函数会从首元素开始依次向后寻找\0,统计\0之前出现的字符个数

	printf("%d\n", strlen(&arr + 1));//随机值13,&arr是整个数组的地址,+1则跳过整个数组,产生的是字符'f'后面位置的地址,所以strlen函数会从该地址开始继续往后找\0,统计\0之前出现的字符个数

	printf("%d\n", strlen(&arr[0] + 1));//随机值18,&arr[0] + 1 是数组第二个元素的地址,所以strlen函数会从第二个元素的地址开始继续往后找\0,统计\0之前出现的字符个数

	return 0;
}
int main()
{
	char arr[] = "abcdef";//[a b c d e f \0]


	printf("%d\n", sizeof(arr));//7,计算的是整个数组的大小,要包含\0

	printf("%d\n", sizeof(arr + 0));//4,数组名是数组首元素的地址,arr+0仍是数组首元素的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*arr));//1,arr表示数组首元素的地址,*arr表示对数组首元素的地址进行解引用,即得到数组的第一个元素,sizeof(*arr)是计算第一个元素的大小

	printf("%d\n", sizeof(arr[1]));//1,arr[1]是数组的第二个元素,大小是1个字节

	printf("%d\n", sizeof(&arr));//4,&arr取出的是数组的地址,数组的地址也是地址,是地址大小就为4/8

	printf("%d\n", sizeof(&arr + 1));//4,&arr是整个数组的地址,+1则跳过整个数组,产生的是字符'\0'后面位置的地址,是地址大小就为4/8

	printf("%d\n", sizeof(&arr[0] + 1));//4,&arr[0]+1是数组第二个元素的地址,是地址大小就为4/8



	printf("%d\n", strlen(arr));//6,arr为数组首元素的地址,从该位置开始依次向后访问直到遇到\0则结束,统计\0之前出现的字符个数

	printf("%d\n", strlen(arr + 0));//6,arr+0是数组首元素的地址,strlen函数会继续从开头往后找\0,统计\0之前出现的字符个数

	//printf("%d\n", strlen(*arr));//错误,没有提供一个有效的地址,arr是数组首元素的地址,*arr是数组的首元素,‘a’-97
	
	//printf("%d\n", strlen(arr[1]));//错误,没有提供一个有效的地址,arr[1]是数组的第二个元素,'b' - 98

	printf("%d\n", strlen(&arr));//6,&arr取出的是整个数组的地址,该值等于数组首元素的地址,所以strlen函数会从首元素开始依次向后寻找\0,统计\0之前出现的字符个数

	printf("%d\n", strlen(&arr + 1));//随机值12,&arr是整个数组的地址,+1跳过整个数组,产生的是字符'\0'后面位置的地址,所以strlen函数会从该位置开始继续往后找\0,统计\0之前出现的字符个数

	printf("%d\n", strlen(&arr[0] + 1));//5,&arr[0]+1是数组第二个元素的地址,所以strlen函数会从该位置开始继续往后找\0,统计\0之前出现的字符个数

	return 0;
}
int main()
{
	char* p = "abcdef";//[a b c d e f \0]


	printf("%d\n", sizeof(p));//4,p是个指针变量,计算的是指针变量的大小,而指针变量p中存放的是首字符'a'的地址

	printf("%d\n", sizeof(p + 1));//4,p+1存放的是字符'b'的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*p)); //1,*p其实就是字符'a',占一个字节

	printf("%d\n", sizeof(p[0]));//1,p[0]->*(p+0)->*p,*p其实就是'a',占一个字节

	printf("%d\n", sizeof(&p));//4,&p是指针变量p在内存中的地址,是地址大小就为4/8

	printf("%d\n", sizeof(&p + 1));//4,&p+1是跳过p之后下一个位置的地址

	printf("%d\n", sizeof(&p[0] + 1));//4,&p[0]是'a'的地址,&p[0]+1就是‘b’的地址,是地址大小就为4/8



	printf("%d\n", strlen(p));//6,p中存放的是a的地址,则从a的地址开始向后数,直到遇到\0则结束,统计\0之前出现的字符个数

	printf("%d\n", strlen(p + 1));//5,p中存放的是a的地址,p+1存放的是b的地址,则从b的地址开始向后数,直到遇到\0则结束,统计\0之前出现的字符个数

	//printf("%d\n", strlen(*p));//错误,*p的值是a,存放的不是一个地址,应该提供一个有效的地址
	
	//printf("%d\n", strlen(p[0]));//错误,p[0]的值是a,存放的不是一个地址,应该提供一个有效的地址

	printf("%d\n", strlen(&p));//随机值3,&p是取指针p的地址,从&p开始往后读取

	printf("%d\n", strlen(&p + 1));//随机值11,&p+1是跳过p之后下一个位置的地址,然后从该地址往后读取

	printf("%d\n", strlen(&p[0] + 1));//5,从字符'b'的地址开始向后数

	return 0;
}

总结: sizeof()与strlen()函数的异同

sizeof()简介:

在C语言中,sizeof() 是一个判断数据类型或者表达式长度的运算符,其作用就是返回一个对象或者类型所占的内存字节数。其返回值类型为size_t,一般定义为typedef unsigned int size_t;它有两种语法形式:sizeof(type_name);//sizeof(类型);sizeof object;//sizeof对象;

int i;
sizeof(i);//ok
sizeof i;//ok
sizeof(int);//ok
sizeof int;//err

sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的不同对象其sizeof值都是一致的。sizeof对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式进行计算。sizeof也可以对一个函数调用求值,其结果是函数返回类型的大小,函数并不会被调用。

strlen()函数简介:

strlen所作的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0')。函数原型为:size_t strlen(const char *string);其中size_t实际上unsigned int,一般定义为typedef unsigned int size_t。当用数组作为实际参数时,strlen不会测量数组本身的长度,而是返回存储在数组中的字符串的长度。
两者差异:

  1. sizeof() 是一个运算符,而 strlen() 是一个函数;
  2. sizeof() 计算的是变量或类型所占用的内存字节数,而 strlen() 计算的是字符串中字符的个数;
  3. sizeof() 可以用于任何类型的数据,而 strlen() 只能用于以空字符 '\0' 结尾的字符串;
  4. sizeof() 计算字符串的长度,包含末尾的 '\0',而strlen() 计算字符串的长度,不包含字符串末尾的 '\0';
  5. sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以'\0'结尾的;
  6. 数组做sizeof的参数不退化,传递给strlen就退化为指针。

二维数组:

int main()
{
	int a[3][4] = { 0 };

	printf("%d\n", sizeof(a));//48,sizeof(数组名)计算的是整个数组的大小,单位是字节3*4*4 = 48

	printf("%d\n", sizeof(a[0][0]));//4,第一行第一个元素的大小

	printf("%d\n", sizeof(a[0]));//16,a[0]是第一行的数组名,sizeof(a[0])就是第一行的数组名单独放在sizeof内部,计算的是第一行的大小

	printf("%d\n", sizeof(a[0] + 1));//4,a[0]作为第一行的数组名,并没有单独放在sizeof内部,也没有被取地址
	//所以a[0]就是数组首元素的地址,就是第一行第一个元素的地址,a[0]+1就是第一行第二个元素的地址,是地址大小就为4/8
	//a[i]+1就是元素a[i][1]的地址

	printf("%d\n", sizeof(*(a[0] + 1)));//4,*(a[0] + 1))表示的是第一行第二个元素

	printf("%d\n", sizeof(a + 1));//4,a表示首元素的地址,而a是二维数组,那么首元素的地址就是第一行的地址
	//所以a表示的是二维数组第一行的地址,a+1就是第二行的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*(a + 1)));//16,对第二行的地址解引用访问到就是第二行
	//*(a+1) -> a[1]
	//sizeof(*(a + 1))等价于sizeof(a[1])

	printf("%d\n", sizeof(&a[0] + 1));//4,a[0]是第一行的数组名,&a[0]取出的就是第一行的地址,&a[0] + 1 就是第二行的地址,是地址大小就为4/8

	printf("%d\n", sizeof(*(&a[0] + 1)));//16,对第二行的地址解引用访问到就是第二行

	printf("%d\n", sizeof(*a));//16,a就是首元素的地址,就是第一行的地址,*a就是第一行
	//*a - > *(a+0) -> a[0]

	printf("%d\n", sizeof(a[3]));//16,类型是int[4],只需要知道其对应的类型,并不需要知道其是否在内存中开辟了空间

	return 0;
}

总结:指针与二维数组

int a[3][4];
a[i]是二维数组a[3][4]中第i行的数组名,而数组名又代表首元素的地址,所以a[i]等价于&a[i][0]
a[i]+1就是元素a[i][1]的地址;a[i]+j就是元素a[i][j]的地址
*(a[i]+j)就是数组元素a[i][j]的间接引用形式

数组名a就是一维数组元素a[0](即二维数组的第0行)的地址
a+1就是一维数组元素a[1](即二维数组的第1行)的地址
a+i就是一维数组元素a[i](即二维数组的第i行)的地址

*(a+i)就代表a+i所指向的数组元素a[i],因此*(a+i)与a[i]在本质上是相同的
既然a[i]是a[i][0]的地址,那么*(a+i)也是a[i][0]的地址
*(a+i)+1是数组元素a[i][j]的地址,而*(a+i)+j是数组元素a[i][j]的地址
*(*(a+i)+1)就是*(a+i)+j所指向的数组元素a[i][j]的间接引用形式

行指针:指向二维数组中某一行的指针,如a和a+i。将行指针的值加1,将会指向下一行
元素指针:指向二维数组中某个元素的指针,如a[i]和a[i]+j,*(a+i)和*(a+i)+j。将元素指针的值加1,将会指向下一个元素

二维数组a的有关指针

  1. a:0行首地址
  2. a[0],*(a+0),*a:0行0列元素地址
  3. a+1,&a[1]:1行首地址
  4. a[1],*(a+1):1行0列元素a[1][0]的地址
  5. a[1]+2,*(a+1)+2,&a[1][2]:1行2列元素a[1][2]的地址
  6. *(a[1]+2),*(*(a+1)+2),a[1][2]:1行2列元素a[1][2]的值

10.指针笔试题

题一 :

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

	return 0;
}

分析:

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };

	int* ptr = (int*)(&a + 1);
	//&a表示对整个数组取地址,对应的类型是:int(*)[5]
	//&a+1则跳过整个数组,来到元素5末尾的位置
	//(int*)表示强制类型转换,将(&a+1)对应的int(*)[5]类型,强制转换为int*类型
	//最后将强制类型转换后的结果赋值给int*类型的指针变量ptr

	printf("%d,%d", *(a + 1), *(ptr - 1));//2 5
	//*(a+1)
	//a表示数组首元素的地址,+1则指向第二个元素的地址,对其进行解引用则得到第二个元素2
	//*(ptr-1)
	//ptr现在指向的是元素5末尾的位置,ptr-1则表示要向前移动一个int*指针的大小
	//此时来到了元素4末尾的位置,同时也是元素5起始的位置,对其进行解引用则得到元素5

	return 0;
}

题二:

//由于还没有学习结构体,这里告知结构体的大小是20个字节
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;

//假设p的值为0x100000,如下表达式的值分别是多少?
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);

	return 0;
}

分析:

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;
	//0x100000等价于0x001000000,0x表示16进制数
	//指针p的类型为(struct Test*),它的大小为对应的结构体变量的大小,为20字节

	printf("%p\n", p + 0x1);//00100014
	//0x1等价于0x00000001,是个十六进制数,转换成对应的十进制数则为1
	//结构体指针+1,则要加上这个类型变量的大小,则加20
	//十进制20对应的十六进制数则为14,所以会变成0x00100014

	printf("%p\n", (unsigned long)p + 0x1);//00100001
	//(unsigned long)是强制类型转换,是将结构体指针变量p强制转换为unsigned long类型,则从指针类型变化为无符号的长整型
	//0x00100000转换成对应的unsigned long类型数则为1048576
	//0x1等价于0x00000001,是个十六进制数,转换成对应的十进制数则为1
	//二者相加:1048576+1=1048577
	//转换成对应的十六进制数则为0x00100001

	printf("%p\n", (unsigned int*)p + 0x1);//00100004,将结构体指针强制转换为整型指针,整型指针+1就是+4
	//(unsigned int*)是强制类型转换,是将结构体指针变量p强制转换为unsigned int*类型,由原来的20字节大小变为现在的4字节大小
	//0x1等价于0x00000001,是个十六进制数,转换成对应的十进制数则为1
	//结构体指针+1,则要加上这个类型变量的大小,则加4
	//十进制4对应的十六进制数也为4,所以相加会变成0x00100004

	return 0;
}

题三:

int main()
{
	int a[4] ={1,2,3,4};
	int* ptr1 = (int*)(&a + 1);//4
	int* ptr2 = (int*)((int)a + 1);//2000000
	printf("%x,%x", ptr1[-1], *ptr2);

	return 0;
}

分析:

int main()
{
	int a[4] = {1,2,3,4};

	int* ptr1 = (int*)(&a + 1);
	//&a表示整个数组的地址,&a+1则跳过整个数组,来到元素4末尾的位置,它的类型是int(*)[4]
	//(int*)表示强制类型转换,将(&a+1)对应的int(*)[4]类型,强制转换为int*类型
	//最后将强制类型转换后的结果赋值给int*类型的指针变量ptr1

	int* ptr2 = (int*)((int)a + 1);
	//假设是小端字节序,则四个元素的存储的形式则分别为:(低地址)01 00 00 00,02 00 00 00,03 00 00 00,04 00 00 00(高地址)
	//而数组名表示数组首元素的地址,则a指向的内容为01 00 00 00,假设a的地址为0x00000020,将其强制转化为int类型,则将16进制转化为十进制,则对应的十进制数字为32,+1则变为33
	//而33对应的16进制数为0x00000021,说明此时的a只是往后跳转了一个字节,即从01开头跳转到00开头
	//又将其强制转化为int*类型,则此时a指向的内容为00 00 00 02,所以ptr2指向的内容为00 00 00 02

	printf("%x %x\n",ptr1[-1],*ptr2);//4 2000000
	//ptr1[-1]等价于*(ptr-1),而ptr-1则将其位置跳转到元素4地址的开头,ptr[-1]则表示取出其中的元素4,并以16进制的形式打印,而十进制4和对应的十六进制数相同,即为0x00000004
	//对ptr2进行解引用,所得到的内容即是00 00 00 02,又因为这是小端字节序,将其以16进制打印则为0x02000000

	return 0;
}

题四:

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);

	return 0;
}

分析:

int main()
{
	int a[3][2] = {(0,1),(2,3),(4,5)};
	//逗号表达式:它将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值
	//第一个表达式(0,1)的结果为1,第二个表达式(2,3)的结果为3,第三个表达式(4,5)的结果为5,所以二维数组存放的内容为:{{1,3},{5,0},{0,0}}

	int* p;
	p = a[0];
	//a[0]表示第0行的数组名,而数组名又表示首元素的地址,也就是a[0][0]的地址
	//将a[0][0]的地址赋值给指针变量p

	printf("%d",p[0]);//1
	//p[0]等价于*(p+0)=*p,对指针p进行解引用就得到a[0][0]的值,而a[0][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[4][2]);

	return 0;
}

分析:

int main()
{
	int a[5][5];
	//a是一个5*5的数组

	int(*p)[4];
	//p是一个指针数组,指向的是含有4个元素的数组

	p = a;
	//a的类型:int*[5]
	//p的类型:int*[4]
	//数组名a就是一维数组元素a[0](即二维数组的第0行)的地址
	//将数组a的地址赋值给p

	printf("%p %d\n",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);//FFFFFFFC -4
	//p[4][2]等价于*(*(p+4)+2)
	//p+1表示要越过含有4个元素的数组
	//p[4][2]表示跳过了4行含有4个元素的数组,并来到了第5行的第三个元素
	//a[4][2]表示跳过了4行含有5个元素的数组,并来到了第5行的第三个元素

	//指针-指针结果为指针之间的距离(用数组元素的个数来度量)
	//-4的原码:10000000 00000000 00000000 00000100
	//-4的反码:11111111 11111111 11111111 11111011
	//-4的补码:11111111 11111111 11111111 11111100将其以%p的形式进行打印,得ff ff ff fc

	return 0;
}

题六:

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

分析:

int main()
{
	int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};

	int* ptr1 = (int*)(&aa + 1);
	//&aa表示整个数组的地址,&aa+1则跳过整个二维数组,来到元素10末尾的位置
	//(int*)表示强制类型转换,将(&a+1)对应的类型,强制转换为int*类型
	//最后将强制类型转换后的结果赋值给int*类型的指针变量ptr1

	int* ptr2 = (int*)(*(aa + 1));
	//数组名aa就是一维数组元素aa[0](即二维数组的第0行)的地址,aa+1就是一维数组元素aa[1](即二维数组的第1行)的地址
	//*(aa+1)表示对第1行的地址进行解引用,相当于拿到了第1行的数组名,等价于aa[1],相当于元素6的地址
	//(int*)表示强制类型转换,将(*(aa + 1))对应的类型转换为int*类型
	//最后将强制类型转换后的结果赋值给int*类型的指针变量ptr2

	printf("%d %d",*(ptr1-1),*(ptr2-1));//10 5
	//ptr1-1表示指向了元素10开头的位置,对其进行解引用则得到元素10
	//ptr2-1表示指向了元素5开头的位置,对其进行解引用则得到元素5

	return 0;
}

题七:

int main()
{
	char* a[] = { "work", "at", "alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);

	return 0;
}

分析:

int main()
{
	char* a[] = {"work","at","alibaba"};
	//a是一个数组,存放3个char*类型的元素,每个元素分别为字符串:work\0; at\0; alibaba\0 对应的首元素的地址

	char** pa = a;
	//因为a是数组名,数组名表示首元素的地址,而首元素是char*类型,因此要用二级指针来接收

	pa++;
	//pa指向a的首元素地址,也就是“work”的w的地址
	//pa++,跳过一个char*类型的字符串,此时的pa指向“at”的a的地址

	printf("%s\n",*pa);//at

	return 0;
}

题八:

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

分析:

int main()
{
	char* c[] = {"ENTER","NEW","POINT","FIRST"};
	//             c+0    c+1    c+2     c+3

	char** cp[] = {c+3,c+2,c+1,c};
	//{"FIRST","POINT","NEW","ENTER"}
	//  cp+0    cp+1    cp+2   cp+3

	char*** cpp = cp;
	//cpp指向(cp+0)的起始地址

	printf("%s\n",**++cpp);//POINT
	//cpp先++,此时的cpp从(cp+0)的位置移动到(cp+1)的位置,再进行两次**;第一次解引用找到(cp+1)所指向空间的内容(c+2),第二次解引用找到(c+2)所指向空间的地址,即字符串POINT的地址

	printf("%s\n",*--*++cpp+3);//ER
	//注意:cpp所指向的位置是(cpp+1)
	//cpp先++,此时的cpp从(cp+1)的位置移动到(cp+2)的位置,进行第一次解引用找到(cp+2)所指向空间的内容(c+1);再进行--,则从(c+1)变为c,此时(cp+2)的空间存放的内容是c,第二次解引用找到c所指向空间的地址,即字符串ENTER的地址
	//再进行+3,则从字符串ENTER的起始地址E向后跳转3个字节到E的位置

	printf("%s\n",*cpp[-2]+3);//ST
	//cpp[-2]等价于*(cpp-2),则*cpp[-2]+3即为**(cpp-2)+3
	//注意:cpp所指向的位置是(cpp+2),cpp[-2]并不会改变cpp的指向
	//cpp-2将cpp从(cpp+2)的位置向前移动到(cpp+0)的位置,再进行两次解引用
	//第一次解引用找到cpp所指向空间的内容(c+3),第二次解引用找到(c+3)所指向空间的地址,即字符串FIRST的地址
	//再进行+3,则从字符串FIRST的起始地址F向后跳转3个字节到S的位置

	printf("%s\n",cpp[-1][-1]+1);//EW
	//cpp[-1]等价于*(cpp-1),cpp[-1][-1]等价于*(*(cpp-1)-1)
	//注意:cpp所指向的位置是(cpp+2),cpp[-1]并不会改变cpp的指向
	//cpp-1,首先从(cp+2)的位置向前移动到(cp+1)的位置,进行第一次解引用找到(cp+1)所指向空间的内容(c+2),再进行-1,则从(c+,2)变为(c+1)
	//进行第二次解引用找到找到(c+1)所指向空间的地址,即字符串NEW的地址
	//再进行+1,则从字符串NEW的起始地址E向后跳转1个字节到E的位置

	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/649884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《机器学习公式推导与代码实现》chapter10-AdaBoost

《机器学习公式推导与代码实现》学习笔记&#xff0c;记录一下自己的学习过程&#xff0c;详细的内容请大家购买作者的书籍查阅。 AdaBoost 将多个单模型组合成一个综合模型的方式早已成为现代机器学习模型采用的主流方法-集成模型(ensemble learning)。AdaBoost是集成学习中…

python获取热搜数据并保存成Excel

python获取百度热搜数据 一、获取目标、准备工作二、开始编码三、总结 一、获取目标、准备工作 1、获取目标&#xff1a; 本次获取教程目标&#xff1a;某度热搜 2、准备工作 环境python3.xrequestspandas requests跟pandas为本次教程所需的库&#xff0c;requests用于模拟h…

牛客网基础语法51~60题

牛客网基础语法51~60题&#x1f618;&#x1f618;&#x1f618; &#x1f4ab;前言&#xff1a;今天是咱们第六期刷牛客网上的题目。 &#x1f4ab;目标&#xff1a;对每种的循环知识掌握熟练&#xff0c;用数学知识和循环结合运用熟练&#xff0c;对逻辑操作符运用熟练。 &am…

接口测试入门神器 —— Requests

起源 众所周知&#xff0c;自动化测试是软件测试爱好者毕生探索的课题。我认为&#xff0c;只要把 接口测试 做好&#xff0c;你的自动化测试就至少成功了一半。 应部分热情读者要求&#xff0c;今天跟大家一起了解 python 接口测试库- Requests 的基本用法并进行实践&#x…

【跑实验03】如何可视化GT边界框,如何选择边界框内部的边界框,如何可视化GT框和预测框,如何定义IoU阈值下的不同边界框?

文章目录 一、如何可视化GT边界框&#xff1f;二、GT框和预测框的可视化三、根据IoU阈值来选择 一、如何可视化GT边界框&#xff1f; from PIL import Image, ImageDrawdef draw_bboxes(image, bboxes, color"red", thickness2):draw ImageDraw.Draw(image)for bbo…

精雕细琢,Smartbi电子表格软件重构、新增、完善

Smartbi SpreadSheet电子表格软件自发布以来&#xff0c;我们一直关注着用户的诉求&#xff0c;也在不断地对产品进行改进和优化&#xff0c;确保产品能够持续满足用户需求。经过一段时间的努力&#xff0c;产品在各方面都有了明显的改进&#xff0c;接下来&#xff0c;让我们一…

全网最详细的postman接口测试教程,一篇文章满足你

1、前言   之前还没实际做过接口测试的时候呢&#xff0c;对接口测试这个概念比较渺茫&#xff0c;只能靠百度&#xff0c;查看各种接口实例&#xff0c;然后在工作中也没用上&#xff0c;现在呢是各种各样的接口都丢过来&#xff0c;总算是有了个实际的认识。因为只是接口的…

不写单元测试的我,被批了 ,怎么说?

我是凡哥&#xff0c;一年CRUD经验用十年的markdown程序员&#x1f468;&#x1f3fb;‍&#x1f4bb;常年被誉为职业八股文选手 最近在看单元测试的东西&#xff0c;想跟大家聊聊我的感受。单元测试这块说实在的&#xff0c;我并不太熟悉&#xff0c;我几乎不写单元测试&…

k8s 的 Deployment控制器

1. RS与RC与Deployment关联 RC&#xff08;Replication Controller&#xff09;主要作用就是用来确保容器应用的副本数始终保持在用户定义的副本数。即如果有容器异常退出&#xff0c;会自动创建新的pod来替代&#xff1b;而如果异常多出来的容器也会自动回收。K8S官方建议使用…

JDBC BasicDAO详解(通俗易懂)

目录 一、前言 二、BasicDAO的引入 1.为什么需要BasicDAO&#xff1f; 2.BasicDAO示意图 : 三、BasicDAO的分析 1.基本说明 : 2.简单设计 : 四、BasicDAO的实现 0.准备工作 : 1.工具类 : 2.JavaBean类 : 3.BasicDAO类 / StusDAO类 : 4.测试类 : 一、前言 第七节内容…

一文读懂物联网平台如何搞定80%以上的物联网项目

太卷了&#xff01;一套物联网平台就能搞定80%以上的项目&#xff1f;&#xff01; 在刚刚结束的AIRIOT4.0物联网平台发布会上&#xff0c;航天科技控股集团股份有限公司智慧物联事业部总经理田淼给出答案。 在主题演讲环节&#xff0c;田总以【80%的物联网项目服务商都会面临…

分组函数group by使用技巧

一、需求&#xff1a;获取销售版本组合 颜色&#xff08;属性名&#xff09; (黑色&#xff0c;白色…) 属性值集合 Datapublic static class ItemSaleAttrsVo{private Long attrId;private String attrName;//当前属性有多少种版本&#xff1a;黑色,白色,蓝色&#xff0c;这里…

奇妙敏捷之旅·青岛站,真的太酷啦!

高手的世界里&#xff0c;一块小小的积木&#xff0c;也能立刻感受敏捷的乐趣&#xff01; 2023奇妙敏捷之旅青岛站&#xff0c;希望将理论知识、实践应用融入互动过程&#xff0c;实现思维的交流、碰撞以及面对面的沟通。因此&#xff0c;大家看到的奇妙敏捷之旅的现场&#…

Linux:课后习题及其答案

第一章 Linux系统初步了解 Q1&#xff1a;简述Linux系统的应用领域 Linux服务器、嵌入式Linux系统、软件开发平台、桌面应用 Q2&#xff1a;Linux系统的特点 开放性、多用户、多任务、良好的用户界面、设备独立性、丰富的网络功能、可靠的系统安全、良好的可移植性 Q3&#…

oracle19c rac、nfs部署教程

本文基于19c进行部署&#xff0c;使用centos7.9,nfs做共享存储 一、首先进行网络规划和配置 该实验一共三台机器一台是nfs,另外两台做rac 1.每台机器至少2块网卡,网卡名字必须一样&#xff08;publicip和VIP使用的是同一张网卡&#xff0c;privilegeip是另外一张网卡改ip 仅做r…

C语言中断言库与断言函数assert()的用法总结

断言库与断言函数的相关使用总结&#xff01; 断言函数的使用断言函数及断言库总结#define NDEBUG 断言函数在实现常见算法中的使用 断言函数的使用 话不多说&#xff0c;先来个例子感受一番断言函数assert()到底有什么功能。 由上面例子可知&#xff0c;assert()函数中在z的…

快速开发框架:一招解锁企业流程化管理!

在流程化管理时代&#xff0c;什么样的平台可以帮助企业实现高效率发展&#xff1f;在信息化爆炸式发展的今天&#xff0c;有很多企业期望能通过专用的快速开发框架实现提质增效发展。目前&#xff0c;低代码技术平台是较为盛行的平台&#xff0c;拥有易操作、灵活、增效等优势…

AntDB数据库灾备方案介绍

AntDB灾备方案(双中心方案) AntDB数据库节点分布于两个机房&#xff0c;并使用 patroni etcd 组件进行HA管理&#xff0c;主节点故障时能自动切换。切换时优先选择同机房的同步slave节点选举为new master。 部署图中相关组件说明&#xff1a; ⚫patroni&#xff1a;通过参…

Revit中楼梯该怎么画?包教包会!

绘制楼梯是室内装修中必不可少的一部分&#xff0c;因为楼梯的安装不仅仅是为了连接不同楼层&#xff0c;更是装饰整个室内空间的重要组成部分。 在楼梯的绘制过程中&#xff0c;需要结合实际情况进行设计&#xff0c;包括楼层高度、楼梯数量、台阶宽度、扶手高度等因素&#…

AntDB数据冷热分离方案

数据冷热分离 数据的存在价值&#xff0c;在于其被使用的程度&#xff0c;即被查询或更新的频率。在不同的业务系统中&#xff0c;人们对处于不同时期的数据有着不同的使用需求。比如&#xff0c;在网络流量行为分析系统中&#xff0c;客户会对最近一个月公司发生的安全事件和…