C语言_进阶_指针

友友们:
今天我们将开启对不同指针的深度解析!!
Are you ready?<( ̄︶ ̄)↗[GO!]
在这里插入图片描述

本章重点

  • 字符指针
  • 数组指针
  • 指针数组
  • 数组传参和指针传参
  • 函数指针
  • 函数指针数组
  • 指向函数指针数组的指针
  • 回调函数
  • 指针和数组面试题的解析

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定的4/8个字节(32位平台/64位平台)。
3.指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
4.指针的运算。

这个章节,我们继续探讨指针的高级主题。

1.字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:

int main()
{
    char ch = 'w';
    char *pc = &ch;//将ch的地址存放在pc中
    *pc = 'w';//解引用pc得到的是ch所对应的值
    return 0;
}

还有一种使用方式如下:

int main()
{
	char* pstr = "hello bit.";
	printf("%c\n", *pstr);
	return 0;
}

问题: 这里是把一个字符串放到pstr指针变量里了吗?
在这里插入图片描述
根据打印结果我们可以知道代码 char* pstr = "hello bit."本质是把字符串 hello bit. 首字符地址放到了pstr中。
解析: 根据最上面的常规写法我们可以知道char *pstr存的是地址,而这里相当于把char ch="hello bit."和char *pstr=&ch结合起来,所以根据本质可以知道存的是首元素地址。
在这里插入图片描述

注意:上述代码中,使用%s输出时,会从arr所指向的首元素地址开始,依次输出字符,直到遇到 '\0',最终输出整个字符串 hello bit 。
补充:

  • %c 用于输出单个字符型数据
  • %s 用于输出以 ‘\0’ 结尾的字符串,即找到\0之前的所有字符

有这样的一道面试题:

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    char *str3 = "hello bit.";
    char *str4 = "hello bit.";
    if(str1 ==str2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
       
    if(str3 ==str4)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
       
    return 0;
}

这里最终输出的是:
在这里插入图片描述分析如下:str1和str2开辟的是两块不同的空间,这里不是比较两个字符串的内容,str1指向一块空间首元素的地址,str2指向另一块空间首元素的地址,两块空间不同,那自然地址也就不相同了;str3放的是h的地址,str4放的也是h的地址,既然都是h的地址,那么自然也就相同了(str3和str4里面的hello bit为常量字符串不能更改,既然不能被改的字符串一样,所以在内存中没有必要存两份,大家一起共用就行了)

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。但是用相同的常量字符串去初始化不同的数组的时候就会开辟不同的内存块。所以str1和str2不同,str3和str4不同。

2.指针数组

在《指针》章节我们也学了指针数组,指针数组是一个存放指针(也就是地址)的数组。

这里我们再复习一下,下面指针数组是什么意思?

int* arr1[10]; //存放整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的!数组

在这里插入图片描述画图解释:在这里插入图片描述解析: a b c 为数组名,指的是数组首元素的地址,数组arr存放的是地址,也就是a中1,b中2,c中3的地址;他们是int * 类型;由于a b c一共有三个数组所以定义变量i来确定外面大循环的趟数为3趟(也就是3行),所以arr[i]指的是i=0时为第一行数组的地址,i=1时为第二行数组的地址…;然后确定里面小循环的次数为5次,通过解引用 *arr[0]可以找到其所对应的值,因为每个数组里面有5个元素,这里arr指向首元素地址,所以再定义变量j,通过+j来移动指针依次找到后面的值,例如第一行中j=0时指向1,j=1时指向2…(这里i和j指的是下标)然后对(arr[i]+j)进行解引用即可遍历出整个数组。

3.数组指针

3.1数组指针的定义

数组指针是指向数组的指针。(是一个指针,而不是数组)

在这里插入图片描述这里举个例子来理解上面标注的部分,也就是为什么int(*parr)[10]不能写成int *parr[10],为什么这里要外加一个(),原因如下:

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

请区分一下p1和p2

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
  • p1 是一个数组,数组名为 p1,它包含 10 个元素,每个元素的类型是 int*,即指向整型的指针,因此 p1 是一个指针数组
  • p2 是一个指针,它指向一个包含 10 个整型元素的数组,所以 p2 是一个数组指针
  • 注意二者写法上的问题就是有无()

举个例子怎么写数组指针:

double* d[5];
double* (*pd)[5] = &d;//pd就是一个数组指针

这里定义了一个double* d[5];要我们写&d前面的怎么写?
首先写pd前面加上* 表示pd是一个指针,然后写[5]表明这个指针指向的是一个数组,里面的元素类型为double * 类型

3.2&数组名VS数组名

我们看一段代码:
在这里插入图片描述可见数组名和&数组名打印的地址是一样的,但是他们表达的意义不一样!
原因如下:

在这里插入图片描述根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

补充:
1.整形指针+1跳过四个字节大小
2.数组指针+1跳过整个数组的大小(这里int类型4个字节,有10个元素,所以占40个字节)

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

  • sizeof(数组名)-数组名表示整个数组,计算的是整个数组大小,单位是字节
  • &数组名-数组名表示整个数组,取出的是整个数组的地址

3.3数组指针的使用

数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址
看代码:
在这里插入图片描述注意:这样写不好,首先这里我们拿到的是整个数组的地址,而for循环中的*p相当于拿到数组名,也就是数组首元素的地址,然后通过+(下标)i来依次拿到后面的元素。
在这里插入图片描述

一个数组指针的使用:

法一:
在这里插入图片描述
法二:在这里插入图片描述注意:二维数组首元素的地址指的是第一行的地址! 由于这里第一行有五个元素,相当于一维数组int a[5],存放的是&a,也就是int*p=&a(指针数组);所以这里传过去拿数组指针接收为int(*p)[5];然后for循环中的打印为什么是这样的呢?因为二维数组p指向的是第一行的地址,p+i(下标)指向第几行第几行,然后通过 解引用 * (p+i)找到某一行的数组名,由于我们知道第一行的数组名为arr[0],第二行数组名为arr[1],第三行数组名为arr[2];拿到数组名就相当于拿到数组首元素的地址,也就是拿到某一行里面第一个数字的地址,再通过+j来找到某一行里面第几个数字的地址,然后通过解引用 *( *(p+i))+j)找到里面地址对应的具体的值
上述整体代码如下:

void print1(int arr[3][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 ", arr[i][j]);
		}
		printf("\n");
	}
}
//这里p是一个数组指针
void print2(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} };
	print1(arr, 3, 5);
	print2(arr, 3, 5);//arr数组名表示数组首元素的地址
	return 0;
}

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

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

分析如下图:
在这里插入图片描述

4.数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1一维数组传参

#include <stdio.h>
void test(int arr[])ok?✔(里面参数无意义可以不写)
{}
void test(int arr[10])ok?{}
void test(int *arr)ok?✔(写成指针形式)
{}
void test2(int *arr[20])ok?✔(数组传参,数组接收)写成指针数组
{}
void test2(int **arr)ok?✔(写成二级指针)
{}
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test(arr);
 test2(arr2);/arr2是一个存放int*类型的数组
}

4.2二维数组传参

void test(int arr[3][5])ok?✔
{}
void test(int arr[][])ok?×(二维数组传参行可以省略,列不能)
{}
void test(int arr[][5])ok?✔
{}
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。
void test(int *arr)ok?×(第一行是5个整型的地址,不能拿一级指针接收)
{}
void test(int* arr[5])ok?×(这里写成数组,而不是指针,那更不行了)
{}
void test(int (*arr)[5])ok?✔(指针指向5个整型元素的数组)
{}
void test(int **arr)ok?×(不能写成二级指针,因为传过去的不是二级指针,是第一行的地址,所以只能写成一维数组的指针)
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);二维数组首元素地址指的是第一行的地址,传过去的也就是第一行的地址
}

4.3一级指针传参

void print(int* ptr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(ptr + i));//这里的i为下标
	}
}
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]);
	//p是一级指针
	print(p, sz);
	return 0;
}

思考:

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

比如:

void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?

test1接收的参数如上图:
test2接收的参数如下图:
在这里插入图片描述

4.4二级指针传参

在这里插入图片描述注意:标红部分第一个解引用找到的是pa的地址,再进行一层解引用找到的是a的值,然后调用test函数时,对a进行了修改。
思考:

当函数的参数为二级指针的时候,可以接收什么参数?

void test(int** p2)
{
	**p2 = 20;
}
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;//二级指针是存放一级指针的地址
	//把二级指针进行传参
	test(ppa);传二级指针变量
	test(&pa);传一级指针变量的地址

	int* arr[10] = { 0 };
	test(arr);传存放一级指针的数组
	printf("%d", a);
	return 0;
}

5.函数指针

函数指针是指向函数的指针,存放函数地址的指针。
在这里插入图片描述上面两个地址一样我们可以知道:

  • 数组名 != &数组名
  • 函数名 == &函数名

那我们的函数的地址要想保存起来,怎么保存?

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//函数指针-存放函数地址的指针
	//&数组名-取到的就是函数的地址
	//pf就是一个函数指针变量
	int (*pf)(int, int) = &Add;
}

pf与*结合,表示它是一个指针,后面这个小括号表示指向一个函数,()里面指的是传过去参数的类型,说明这是一个函数指针,然后这个函数的返回值类型为int

下面我们做一个练习:

void test(char* str)
{

}
int main()
{
	pt = &test;

	return 0;
}

如何定义这个pt?
在这里插入图片描述

根据上面的解析就能够很快正确的写出!!!

观察下面一段代码,分析其解引用:
在这里插入图片描述这里划红线的( * pt )表示的是对指针变量pt进行解引用;这里说明用函数指针去调用这个Add函数,那如何通过pt去调用这个函数呢,那就是对pt进行解引用
在这里插入图片描述根据这个观察可以发现上面红线的那个*是摆设

int Add(int x, int y)
{
  return x + y;
}
int main()
{
	//int (*pt)(int,int) = &Add;//由于&函数名=函数名,所以可以写成
	int (*pt)(int, int) = Add;//这里是将函数名的地址存放到pt里面
	int ret = (*pt)(3, 5);//1 这里就是解引用这个函数然后传参
	int ret = Add(3, 5);//2
	int ret = pt(3, 5);//3
	printf("%d", ret);
	return 0;
}
上面1 2 3三种写法都可以!!!是等价的

5.1阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

代码1:
从0开始分析,0是一个数字(也是一个变量名),这里要将0强制转换为函数指针类型(所以要一个括号强转),那么0就被看作是一个函数地址;所以转换后为(void(*)())0;这里 ( * )指的是指针,然后后面这个()表示的是函数,结合起来就是指针指向函数,函数的返回类型为void,总的()里面的就是函数指针类型;然后这里是调用0地址处的函数,所以对其进行解引用,并用()括起来,因为函数是void无返回类型,也就是无参,所以后面传参不用写值,所以直接一个();
总结:

void(*) () -函数指针类型
( void(*) () )0 -0进行强制类型转换,被解释为一个函数地址
*( void(*) () )0 -0地址进行了解引用操作
(*void(*) ()0 )()-调用0地址处的函数

代码2:
在这里插入图片描述根据上面分析我们可以总结道:

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

为了便于理解代码2,我们可以这样写:

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

注意:上面这样写语法是错误的!!!
那么我们怎么改写呢?简化如下:

typedef-对类型进行重定义
typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名为pfun_t
所以就能写成:
pfun_t signal(int, pfun_t);//这就是对刚刚那个错误的进行修改

6.函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:

整型指针:int*
整型指针数组:int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
在这里插入图片描述如上图所示:pfArr先和 [] 结合,说明pfArr是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。这里也就是说数组里面有2个函数指针 ,这个()里面的int,int指的是函数的2个参数类型为int类型。

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

例子:(计算器)

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;
	//计算器-计算整型变量的加、减、乘、除
	do {
		menu();
		int x = 0;
		int y = 0;
		int ret = 0;
		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;
}

我们发现这样写代码过于繁长,过于冗长
使用函数指针数组的实现:

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;
	//计算器-计算整型变量的加、减、乘、除
	do {
		menu();
		//pfArr就是函数指针数组
		//转移表
		int (*pfArr[5])(int, int) = { NULL,add,sub,mul,div };//通过下标去访问
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数>:");
			scanf("%d %d", &x, &y);
			ret = (pfArr[input])(x, y);
			printf("ret=%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出程序!\n");
			break;
		}
		else
		{
			printf("选择错误!\n");
		}
		
	} while (input);
	return 0;
}

注意:这段代码每次循环开始时先调用 menu 函数展示操作菜单,接着定义了一个函数指针数组 pfArr,它可以存放指向具有两个整型参数且返回整型值的函数的指针。这里初始化数组的元素,将 add、sub、mul、div 这几个函数的地址依次放入数组对应位置(索引为 1 到 4 的位置,索引 0 位置设为 NULL),以便后续通过下标方便地调用对应的函数(也就是说当用户根据提示的菜单输入1时,说明用户是要进行add的加法运算,然后这样对应数组中下标1,也就是add)。

  • input是我们要输入的东西,也就是一个下标,我们根据下标找到某个数组名的元素,这个元素恰好是某个数组名的地址,然后去调用这个地址所对应的函数
  • input 是一个用户输入的整数,用于选择要执行的操作。通过 pfArr[input],可以根据用户的输入从函数指针数组中获取相应的函数指针。例如,如果 input 的值为 1,则 pfArr[input] 等价于 pfArr[1],即获取到数组中的第二个元素,也就是指向 add 函数的指针
  • 函数调用 (pfArr[input])(x, y)
    在获取到相应的函数指针后,使用 (pfArr[input])(x, y) 的形式来调用该函数指针所指向的函数,并传递参数 x 和 y。例如,如果 pfArr[input] 指向 add 函数,那么 (pfArr[input])(x, y) 就相当于调用 add(x, y),执行加法运算并返回结果

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

指向函数指针数组的指针是一个指针指针指向一个数组,数组的元素都是 函数指针 ;
在这里插入图片描述如何定义?

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

补充一个小知识:
在这里插入图片描述

8.回调函数

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

这句话简单理解如下图:
在这里插入图片描述举个例子:上面计数器的写法可以利用回调函数去这样写

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 calc(int (*pf)(int,int)) 
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	return pf(x, y);
}
int main()
{
	int input = 0;
	//计算器-计算整型变量的加、减、乘、除
	do {
		menu();
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:

			ret=calc(add);//函数名就是函数的地址,所以直接将函数传进去
			printf("ret=%d\n", ret);
			break;
		case 2:

			ret = calc(sub);
			printf("ret=%d\n", ret);
			break;
		case 3:
			ret = calc(mul);
			printf("ret=%d\n", ret);
			break;
		case 4:
			ret = calc(div);
			printf("ret=%d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}
	} while (input);
	return 0;
}

8.1演示一下qsort函数的使用:

qsort指的是快速排序的意思,下面解释一下qsort的使用

  • 这里使用的都是升序

排序整型数据(实现冒泡排序)

void qsort (void* base,//base中存放的是待排序数据中第一个元素(对象)的地址
            size_t num,//排序数据元素的个数 
            size_t size,//排序数据中一个元素的大小,单位为字节(也就是传过去的元素是什么类型,传int型就是4)
            int (*compar)(const void*,const void*));//compar是用来比较待排序数据中的两个元素的函数
            返回大于0的数字,则说明第一个元素大于第二个元素;返回0,表示第一个元素等于第二个元素;返回的
            如果是小于0的数字,则说明第一个元素小于第二个元素

代码代码如下:

int compar_int(const void*e1, const void*e2)//注意返回类型一定是int
{
	return *(int*)e1 - *(int*)e2;//这里先强转为int*型,然后再解引用找到所对应的数字,再做减法
}

void print(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]);
	//排序
	qsort(arr, sz, sizeof(arr[0]), compar_int);
	//打印
	print(arr, sz);
	return 0;
}

使用qsort函数排序结构体数据

这里是根据年龄来排序
在这里插入图片描述这里是根据名字来排序
在这里插入图片描述补充一个知识:strcmp的比较是按照从左到右的顺序,依次比较对应字母的ASCLL码值(比较的是对应位置的字符,不要误以为比较的是长度)

  • 那么如何实现降序呢

在这里插入图片描述
只需要改变compar_int函数里面的逻辑,原先前一个数比后一个数大的话,返回的是一个比0大的数,现在返回一个比0小的数,那么就可以实现反向的排序
补充e1-e2
对于 qsort 函数的比较函数 compar,它接收两个 const void * 类型的指针,这两个指针指向要比较的元素。它的返回值决定了元素的排序顺序

  • 如果 compar 返回小于 0 的值,第一个元素会被排在第二个元素之。(第一个元素<第二个元素,实现降序
  • 如果 compar 返回 0,两个元素被认为相等。(第一个元素=第二个元素)
  • 如果 compar 返回大于 0 的值,第一个元素会被排在第二个元素之。(第一个元素>第二个元素,实现升序

补充e2-e1
当 *pb 大于 *pa 时,*pb - *pa 为正,qsort 会将 b 指向的元素排在 a 指向的元素之前,实现降序排序。
当 *pb 小于 *pa 时,*pb - *pa 为负,qsort 会将 b 指向的元素排在 a 指向的元素之后。
当 *pb 等于 *pa 时,结果为 0,qsort 会认为它们相等,不改变它们的顺序。
在这里插入图片描述

总结来说就是:
第一个元素与第二个元素相减实现的是升序
第二个元素与第一个元素相减实现的是降序

模仿qsort函数来实现冒泡排序的通用算法

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 bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
	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)
			//强制转换为char*类型是因为我们不知道这个元素是什么类型,base是首元素的地址,然后+j*width是因为可以根据下标和宽
			//度来跳过几个字节,实现对哪两个数的比较
			{
				//交换
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

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

void print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
		int sz = sizeof(arr) / sizeof(arr[0]);
		//排序
		bubble_sort(arr, sz, sizeof(arr[0]), compar_int);
		//打印
		print(arr, sz);
	return 0;
}

运行结果:
在这里插入图片描述

9.指针和数组笔试题解析

sizeof(数组名)-计算的是整个数组的大小
&数组名-数组名表示的是整个数组,取出的是整个数组的地址
除此之外,所有的数组名都是数组首元素的地址
sizeof求所占空间大小
注意
sizeof计算指针(地址)的大小,而不是它所指向的数据的大小。在大多数系统中,指针的大小取决于系统的地址空间,通常是 4 字节(32 位系统)或 8 字节(64 位系统)

  • 一维数组
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16 
//因为数组a有4个元素,每个元素4个字节 4*4=16
printf("%d\n",sizeof(a+0));//4/8 
//因为a+0是第一个元素的地址,sizeof(a+0)计算的是地址的大小,地址也就是指针大小要看平台是32/64位平台
printf("%d\n",sizeof(*a));//4
//这里a不满足sizeof和&两种情况,所以这里表示的是首元素的地址,然后解引用找到第一个元素1,1是整数(整型)所以为4
printf("%d\n",sizeof(a+1));//4/8
//这里a+1是第二个元素的地址,sizeof(a+1)计算的是地址的大小,地址也就是指针大小要看平台是32/64位平台
printf("%d\n",sizeof(a[1]));//4
//a[1]通过下标找到的是第二个元素2,计算的是第二个元素的大小
printf("%d\n",sizeof(&a));//4/8
//这里&a虽然是整个数组的地址,但也是地址,sizeof(&a)计算的是一个地址的大小,也得看平台是32/64
printf("%d\n",sizeof(*&a));//16
//&a--int(*p)[4]=&a,数组指针解引用找到的是这个数组,sizeof计算的是整个数组的大小为16(简单来说&和*可以抵消)
printf("%d\n",sizeof(&a+1));//4/8
//&a取出整个数组的地址,但开始时它指向的是首元素的位置上的地址,+1表示跳过整个数组指向另一块空间的起始位置的地址,也就是4后面这个空间的地址
printf("%d\n",sizeof(&a[0]));//4/8
//a[0]找到的是第一个元素1,然后&a[0]取出的是1的地址,所以sizeof计算的是地址的大小,看平台是32/64
printf("%d\n",sizeof(&a[0]+1));//4/8
//同理这里取出的是第二个元素2的地址,计算的是地址的大小
  • 字符数组
//字符数组
char arr[] = {'a','b','c','d','e','f'};//放了6个字符[]中为6
printf("%d\n", sizeof(arr));//6
//sizeof(数组名)计算的是整个数组的大小,所以6*1=6(注意这里没有\0噢,""里面才有一个默认\0)
printf("%d\n", sizeof(arr+0));//4/8
//这里arr是首元素的地址,+0相当于没加,还是首元素‘a’的地址,所以要看平台是32/64
printf("%d\n", sizeof(*arr));//1
printf("%d\n", sizeof(arr[1]));//1
//这两个计算的都是单个元素的大小,char类型所占空间为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
//上面三个计算的都是地址

补充:
strlen是根据地址一直往后数,知道找到\0才停止,求字符串长度
printf("%d\n", strlen(arr));//随机值
//这里数组名指的是首元素的地址,所以从第一个元素开始往下找,直到找到\0为止,由于后面空间不知道什么时候有\0,所以为随机值
printf("%d\n", strlen(arr+0));//随机值
//这里和上面同理,因为arr表示首元素的地址,+0还是表示首元素的地址...
printf("%d\n", strlen(*arr));//×
//这里传过去的地址是有问题的,因为strlen传的是地址,这里*arr找到的是第一个元素'a',它所对应的值是97,这里相当于把97当作一个地址传过去是会出问题的
printf("%d\n", strlen(arr[1]));//×
//这里和上面同理,传的是第二个元素'b',也就是将98当作一个地址传过去,这个地址是不合法的
printf("%d\n", strlen(&arr));//随机值
//这里&arr取的是整个数组的地址,但指针指向的位置任然是首元素那,因此和最上面两个一样,从第一个元素出发,一直找到\0为止
printf("%d\n", strlen(&arr+1));//随机值/随机值-6
//这里和上面一样,跳过一个数组,从'f'后面的那个位置地址出发,一直找到\0,所以也是一个随机值,那至于为什么另一个答案-6呢,是因为和arr里面六个字符差6(和上面一个随机值相比)
printf("%d\n", strlen(&arr[0]+1));//随机值/随机值-1
//这里是从'b'这个地址开始往后面找

上面可见\0非常重要!!!

char arr[] = "abcdef";
//这里面放了 a b c d e f \0  7个元素
printf("%d\n", sizeof(arr));//7
//sizeof(数组名)求的是整个数组的大小,七个元素都是char类型,各占1个字节,所以7*1=7
printf("%d\n", sizeof(arr+0));//4/8
//arr数组名表示首元素的地址,sizeof求地址要看平台是32/64位
printf("%d\n", sizeof(*arr));//1
//arr表示首元素的地址,也就是a的地址,对其解引用找到a,类型为char,占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
//上面三个求的都是地址,要根据平台来判断
printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr+0));//6
//arr数组名表示首元素的地址,也就是从第一个元素开始数,一直到找到\0为止
printf("%d\n", strlen(*arr));//×
printf("%d\n", strlen(arr[1]));//×
//这两个传过去的不是地址,只能将传过去的97和98看作地址,是非法的
printf("%d\n", strlen(&arr));//6
//&arr取出的仍然是数组起始位置的地址,也就是从起始位置开始数
printf("%d\n", strlen(&arr+1));//随机值
//这里&arr刚开始指向的是起始位置的地址,+1表示跳过整个数组指向\0之后位置的地址,由于后面是未知的,所以是一个随机值
printf("%d\n", strlen(&arr[0]+1));//5
//&arr[0]取出的是第一个元素的地址,+1指向的是第二个元素的地址,也就是从第二个元素开始数
char *p = "abcdef";
学了指针我们可以知道,这里表示将首字符'a'的地址存放在指针变量p里面
//这里空间里面放的是a  b c d e f \0  p里面存放的是a的地址
printf("%d\n", sizeof(p));//4/8
//sizeof计算的是指针变量的大小
printf("%d\n", sizeof(p+1));//4/8
//p本来是a的地址,p+1变成了b的地址,计算的仍然是指针的大小
printf("%d\n", sizeof(*p));//1
//p指向a,*p指向的就是a这个值,所占空间为1
printf("%d\n", sizeof(p[0]));//1
//求的是字符a所占的空间大小,这里p[0]和有解引用*(p+0)的写法是等价的
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p+1));//4/8
printf("%d\n", sizeof(&p[0]+1));//4/8
//&p取出的是P的地址,p[0]表示的是第一个元素,&p[0]取第一个元素的地址,sizeof求的是指针的大小
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p+1));//5
//p 指向字符串 "abcdef" 的首字符 'a',p存放的是a的地址,第一个从a开始数,第二个从b开始数
printf("%d\n", strlen(*p));//×
printf("%d\n", strlen(p[0]));//×
//p[0]和*p是一个道理,指的是第一个元素,不是地址,是不合法的
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p+1));//随机值
//这里的p本来就是一个指针变量,取出p的地址,我们压根就不知道p它自己的地址是什么,里面有什么
printf("%d\n", strlen(&p[0]+1));//5
//取出第一个元素的地址,+1指向第二个元素,也就是从b开始数
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
//十二个元素,每个元素都是int整型,所以12*4=48
printf("%d\n",sizeof(a[0][0]));//4
//a[0][0]第一行第一个元素所占空间的大小
printf("%d\n",sizeof(a[0]));//16
//sizeof(a[0])表示数组名a[0]单独放在sizeof内部,a[0]表示整个第一行,sizeof(a[0])计算的就是第一行的大小,
//第一行有4个元素,一个元素四个字节
printf("%d\n",sizeof(a[0]+1));//4/8
//这里a[0]不是单独放在sizeof内部,也没有&数组名,所以表示的是首元素的地址,指向的是第一行第一个元素的地址,
//a[0]+1就是一维数组的第二个元素的地址,即 a[0][1] 的地址,sizeof计算的是指针的大小,所以要看是32/64平台
printf("%d\n",sizeof(*(a[0]+1)));//4
//a[0]+1就是一维数组的第二个元素的地址,即 a[0][1] 的地址,对其解引用找到第一行第二个元素,是一个整型,大小为4个字节
printf("%d\n",sizeof(a+1));//4/8
//二维数组名a没有单独放在sizeof内部,也没有&数组名,表示的是二维数组首元素的地址,二维数组首元素的地址表示第
//一行一维数组的地址,+1表示跳过一行,指向第二行的地址(a + 1 指向的是数组 a 的第二行的起始地址,也就是 &a[1])
printf("%d\n",sizeof(*(a+1)));//16
//由上可知a+1表示第二行的地址,对其解引用找到第二行,求的是第二行的大小,第二行4个元素,每个元素占4个字节;*(a+1)其实就等价于a[1]
printf("%d\n",sizeof(&a[0]+1));//4/8
//a[0]表示第一行数组名,&a[0]取出的是第一行整个数组的地址,+1表示指向第一行之后的空间,也就是第二行的地址
printf("%d\n",sizeof(*(&a[0]+1)));//16
//由上可知&a[0]+1表示第二行的地址,对其解引用,找到的是第二行,计算的是第二行的大小,第二行4个元素,每个元素占4个字节,4*4=16
printf("%d\n",sizeof(*a));//16
//这里二维数组数组名表示的是首元素的地址,因为a没有单独写在sizeof里面,也没有&,二维数组首元素的地址表示第一
//行一维数组的地址,解引用求的是第一行的大小
printf("%d\n",sizeof(a[3]));//16
//根据a[0]表示第一行;a[1]表示第二行...可以推测出a[3]表示第四行的数组名,这里虽然没有第四行,但是我们可以根
//据类型推测出a[3]-->int [4],这里不会去访问第四行,但是我们可以根据其类型去算出其大小
注意sizeof()内部的表达式是不算的

注意:二维数组的数组名 a 指向该二维数组的首行,也就是第一个一维数组起始地址
在这里插入图片描述因为我们常见的int arr[5]一维数组,访问里面的元素是arr[i],i的取值范围是0~4
所以这里我们可以理解 数组名[0]表示的是第一行的数组名
补充
注意sizeof()内部的表达式是不算的
在这里插入图片描述

  • s = a + 6 是一个赋值表达式,但由于 sizeof 操作符的特性,这个表达式不会被执行。sizeof 只关心表达式结果的类型,而不关心表达式的结果
  • a + 6 的结果类型是 int,因为 a 是 int 类型,与 6 相加后结果还是 int 类型
  • 将 int 类型的值赋给short类型的 s,结果仍然是short类型,因为 s short 类型,所以 sizeof(s = a + 6) 实际上是计算 short 类型的大小。所以结果为2

下面printf结果仍然为5,是因为sizeof里面的表达式不参与运算

总结: 数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

9.1指针笔试题

笔试题1:

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

在这里插入图片描述注意:这里强制类型转换是因为&a取出的是整个数组的地址,是一个int(*)[5]指针数组,强转为一个整型指针,然后-1表示减去4个字节,才能向左移动一位

笔试题2:

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;//这里结构体+*表示结构体指针,P是指针变量
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
 printf("%p\n", p + 0x1);//0X10014
 //P是一个指针变量,指针加1表示跳过这个结构体,上述可知结构体大小为20个字节,所以这里+1表示+20,0X表示16进制,所以0X100000+0x1=0x100014(这里14表示十六进制,1*16^1+4*16^0=14;
 printf("%p\n", (unsigned long)p + 0x1);//0X10001
 //p原本是指针类型,这里强转为整型类型(无符号长整型),整型+1加的就是1,结果为0X100001
 printf("%p\n", (unsigned int*)p + 0x1);//0X10004
 //强转为无符号整型指针,指针+1表示跳过一个无符号整型也就是4个字节,结果为0X10004
 return 0;
}

考察的是:指针类型决定了指针的运算!指针+1取决于指针类型,也就是加多少个字节。(int 整型指针+4个字节;结构体指针+20个字节…)

笔试题3:

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;
}
结果为42000000
//%x打印的没有0X,而是直接打印有效数字(也就是0X和数字之前的0都会省略)

在这里插入图片描述

  • 注意:整型+1是直接加1,假设原先a的地址为0X0012ff44,先强转为int整型变成了整型数字0X0012ff44,再+1变成数字0X0012ff45,然后再强转为整型指针(也就是地址0X0012ff45),由于地址加为0X0012ff45之后正好指向的就是内存地址里面00那个位置的地址,所以如图所示
  • 学了数据的存储我们可以知道:
    在大多数现代计算机系统中,内存是按字节编址的,这意味着每个字节都有一个唯一的地址,并且相邻字节地址差 1

笔试题4:

#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;
}
答案为1

在这里插入图片描述

笔试题5:

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;
}
结果为fffffffc和-4

在这里插入图片描述

笔试题6

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;
}
答案为105

在这里插入图片描述

笔试题7:

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

在这里插入图片描述

笔试题8:

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

在这里插入图片描述
终于结束了指针…学习不易,仍需努力!( ゚д゚)つBye

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值