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

目录

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习[16]bash学习深入2---别名设置alias---history指令---环境配置相关

文章目录 前言1. alias2. history3. 环境配置相关总结 前言 linux学习15里面简单提了一下alias指令&#xff0c;就表明它是一个别名的作用&#xff0c;这节就展开来写一下。 同时上一节一笔带过的history指令&#xff0c;这一节也进行例子的演示记录。 最后是环境相关的配置&a…

常用API(String,ArrayList)

1:String类概述 String是字符串类型&#xff0c;可以定义字符串变量指向字符串对象String是不可变字符串的原因&#xff1f;1.String变量每次的修改都是产生并指向新的字符串对象。2.原来的字符串对象都是没有改变的&#xff0c;所以称不可变字符串。 2&#xff1a;String创建…

八股文总结

文章目录 项目介绍1.不动产项目项目难点机器学习算法调研图像提取算法调研数据集-ImageNetXceptionVGGInceptionDensenetMobilenet 系统流程图 2.图书项目技术栈ShiroMybatisMyBatis:Mybatis Plus: 面试问题 Java基础基本数据类型反射接口和抽象类异常代理模式1. 静态代理2. 动…

『DevOps最佳实践』使用Jenkins和Harbor进行持续集成和交付的解决方案

&#x1f4e3;读完这篇文章里你能收获到 全文采用图文形式讲解学会使用Harbor配置项目学会在Jenkins中配置Harbor推送权限使用Jenkins和Harbor进行持续集成的实践感谢点赞收藏&#xff0c;避免下次找不到~ 文章目录 一、准备工作1. 环境准备2. 修改Docker配置文件3. Docker登陆…

【SpringCloud】三、Nacos服务注册+配置管理+集群搭建

文章目录 一、认识Nacos1、安装2、服务注册和发现3、服务分级存储模型4、负载均衡策略--NacosRule5、服务实例的权重设置5、环境隔离namespace6、Eureka和Nacos的区别 二、Nacos配置管理1、统一配置管理2、微服务配置拉取3、配置热更新4、多环境配置共享 三、Nacos集群搭建1、初…

架构-嵌入式模块

章节架构 约三分&#xff0c;主要为选择题 #mermaid-svg-z6RGCDSEQT5AhE1p {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-z6RGCDSEQT5AhE1p .error-icon{fill:#552222;}#mermaid-svg-z6RGCDSEQT5AhE1p .error-text…

Apifox(1)比postman更优秀的接口自动化测试平台

Apifox介绍 Apifox 是 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台&#xff0c;定位 Postman Swagger Mock JMeter。通过一套系统、一份数据&#xff0c;解决多个系统之间的数据同步问题。只要定义好 API 文档&#xff0c;API 调试、API 数据 Mock、API 自…

利用腾讯云函数隐藏C2服务器

1、简介 腾讯云函数&#xff0c;可以为企业和开发者提供无服务器执行环境&#xff0c;无需购买和管理服务器&#xff0c;只需要在腾讯云上使用平台支持的语言编写核心代码并设置代码运行的条件&#xff0c;即可在腾讯云基础设施上弹性 安全地运行代码。 C2服务器所有流量通过腾…

AB32VG1:SDK_AB53XX_V061(4)蓝牙音频测试笔记

文章目录 1. 淘宝上两种开发板&#xff0c;有一种的蓝牙功能不正常2. 蓝牙音频测试2.1 《config.h》和《Boombox.setting》两个配置以哪个为准2.2 codeblocks更换链接库2.2.1 这样进入build options是错的2.2.2 build options正确打开方式 2.3.编译工程&#xff0c;下载运行2.3…

手撕学生管理系统超详解——【c++】

题目要求&#xff1a;设计一个学生成绩管理程序&#xff0c;实现按班级完成对学生成绩信息的录入和修改&#xff0c;并用文件保存。 实现按班级输出学生的成绩单;实现按学号和姓名进行查询&#xff0c;按平均成绩进行排序功能。 问题描述 该程序的目标是提供一个简单且易于使用…

Linux本地搭建GitLab服务器 - 内网穿透远程访问

文章目录 前言1. 下载Gitlab2. 安装Gitlab3. 启动Gitlab4. 安装cpolar内网穿透5. 创建隧道配置访问地址6. 固定GitLab访问地址6.1 保留二级子域名6.2 配置二级子域名 7. 测试访问二级子域名 转载自cpolar极点云文章&#xff1a;Linux搭建GitLab私有仓库&#xff0c;并内网穿透实…

为什么我们需要API接口?API接口的核心又是什么?

API&#xff08;Application Programming Interface&#xff09;是一种连接不同软件之间的标准化的接口&#xff0c;可以让不同软件间进行数据交互和通信。API接口的作用很多&#xff0c;以下是几个主要的原因&#xff1a; 1.提高软件系统的灵活性和可扩展性。API接口可以将不…

Banana Pi BPI-R3 Mini:2.5GbE 嵌入式路由器板,MTK7986方案

香蕉派 BPI-R3 Mini Banana Pi BPI-R3 Mini 是一款功能强大的 SBC 路由器板&#xff0c;专为需要高速网络功能的个人和企业而设计。这款路由器是广受欢迎的 Banana Pi R3 路由器板的小兄弟&#xff0c;配备了先进的功能&#xff0c;旨在提供可靠的性能&#xff0c;是需要可靠网…

2.MATLAB篇——基本操作与矩阵输入

>> cos(((12345)^5)^0.5)ans -0.3623>> help sinsin - 参数的正弦&#xff0c;以弧度为单位此 MATLAB 函数 返回 X 的元素的正弦。sin 函数按元素处理数组。该函数同时接受实数和复数输入。 对于 X 的实数值&#xff0c;sin(X) 返回区间 [-1, 1] 内的实数值。 对于…

离散数学题目收集整理练习(期末过关进度50%)

✨博主&#xff1a;命运之光 &#x1f984;专栏&#xff1a;离散数学考前复习&#xff08;知识点题&#xff09; &#x1f353;专栏&#xff1a;概率论期末速成&#xff08;一套卷&#xff09; &#x1f433;专栏&#xff1a;数字电路考前复习 ✨博主的其他文章&#xff1a;点击…

flutter DevTools(1)

在VSCODE中调试 第一步&#xff1a; ① 切换到 vscode 的调试和运行模式, ② 配置好 .vscode 中的启动项 launch.json ③ 共有四种模式 [1] debug : 模式编译产物适合纯 Flutter 侧代码的开发、调试 [2] profile : 的用来做性能分析和测试 [3] release : 的用于打包发布 [4]…

【C++】C++11新特性重点:可变参数+lambda

C11新特性第二篇重点 文章目录 上一篇的补充一、可变参数模板二、lambda函数总结 前言 上一篇我们重点讲解了右值引用移动语义&#xff0c;关于移动构造和移动赋值还有一些需要补充的知识&#xff1a; 如果你没有自己实现移动构造函数&#xff0c;且没有实现析构函数 、拷贝构…

RabbitMq消息堆积问题及惰性队列

消息堆积问题 当生产者发送消息的速度超过了消费者处理的速度&#xff0c;就会导致队列的消息堆积&#xff0c;知道队列存储消息达到上限。最早接受的消息&#xff0c;可能就会成为死信&#xff0c;会被丢弃&#xff0c;这就是消息堆积问题。 解决消费对接问题 1.增加更多的消…

【数据库一】MySQL数据库初体验

MySQL数据库初体验 1.数据库基本概念1.1 数据Data1.2 表1.3 数据库1.4 数据库管理系统1.5 数据库系统 2.数据库的发展3.主流的数据库介绍3.1 SQL Server&#xff08;微软公司产品&#xff09;3.2 Oracle &#xff08;甲骨文公司产品&#xff09;3.3 DB2&#xff08;IBM公司产品…

XSS—存储型xss

xss >跨站脚本攻击>前端代码注入>用户输入的数据会被当做前端代码执行。 原理&#xff1a;使用者提交的XSS代码被存储到服务器上的数据库里或页面或某个上传文件里&#xff0c;导致用户访问页面展示的内容时直接触发xss代码。 输入内容后直接在下方回显&#xff0c;回…