C语言学习记录—指针进阶

 第一章:字符指针

int main()
{
	char ch = 'w';
	char* pc = &ch;//*说明pc是指针,char说明pc指向的对象ch类型是char。char*是pc的类型
	*pc = 'b';//通过指针找到ch修改里面的值
	printf("%c\n", ch);
    
    char* p = "abcdef";//这里是把字符串首字母a的地址赋值给指针p
    printf("%s\n", p);
    //上面写法在某些编译器会报警告不安全。因为abcdef是常量字符串,常量字符串的意思就是不能被修改。
    //但是又把这个字符串的地址放到p里面,p的权限就变大了(因为p没有被修饰)。如果强行修改,程序就崩溃。
    //所以应该在要在*左边加上const,这样const就修饰指针*p,也就是限制了*p不能修改

    char arr[] = "abcdef";//这种写法才是把整个字符串放到数组里
	return 0;
}

练习题

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

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

	//内存中有一个字符串abcdef\0,这是一个常量字符串,放在内存只读数据区里(不能改)。
	//因为不能改,所以只需要存一份。所以p1和p2指向的是同一个地址
	if (p1 == p2)
		printf("p1 == p2\n");
	else
		printf("p1 != p2\n");

	//arr1和arr2是两个独立的数组,在内存中开辟了两块独立的空间,所以地址不同
	if (arr1 == arr2)
		printf("arr1 == arr2\n");
	else
		printf("arr1 != arr2\n");

	return 0;
}

第二章:指针数组

1. 定义

指针数组 - 是数组,是用来存放指针的数组。

int main()
{
	int* arr1[10];//存放整型指针的数组,数组每个元素的类型是int*(即int*类型的值)
	char* arr2[4];//存放字符指针的数组
    char* arr3[5];//一级字符指针的数组
    char** arr3[5];//二级字符指针的数组

	return 0;
}

2. 用指针数组模拟二维数组

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

	int* parr[3] = { arr1,arr2,arr3 };

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			//*(p+i)等价于p[i]
			printf("%d ", *(parr[i] + j));//起始地址加j,相当于向后平移j个元素
			//printf("%d ", parr[i][j]);//等价上方
		}
		printf("\n");
	}
	return 0;
}

第三章:数组指针

1. 定义

数组指针 - 是指针,指向数组的指针

int main()
{	
    //p1先跟[]结合,是个数组,有10个元素,p1是数组名,数组元素类型是int*。p1是指针数组
	int* p1[10];
	
    //p2先跟*结合,p2是指针,指向的是数组,数组元素10个,数组元素类型是int。p2是数组指针。
	//p2可以指向一个数组,该数组有十个元素,每个元素是int类型
	int(*p2)[10];
	//解释:p2先和*结合,说明p2是一个指针变量,然后指针指向的是一个大小为10个整型的数组。
	//所以p2是一个指针,指向一个数组,叫数组指针。
	//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p2先和*结合。
	return 0;
}

2. &数组名 VS 数组名

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

	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	//上方两个打印结果一样,说明数组名就是数组首元素地址
	
	printf("%p\n", arr);//012FF740
	printf("%p\n", arr + 1);//012FF744

	printf("%p\n", &arr[0]);//012FF740
	printf("%p\n", &arr[0] + 1);//012FF744
	
	printf("%p\n", &arr);//012FF740
	printf("%p\n", &arr + 1);//012FF768,增加了十六进制的28,也就是十进制的40字节

	int sz = sizeof(arr);
	printf("%d\n", sz);//40
	//数组名通常表示的都是数组首元素的地址
	//但是有2个例外
	//1. sizeof(数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
	//2. &数组名,这里的数组名表示的依然是整个数组,所以&数组名取出的是整个数组的大小


	//整形指针是用来存放整形的地址
	//字符指针是用来存放字符的地址
	//数组指针是用来存放数组的地址(即&arr就是取出整个数组的地址)
	int arr[10] = { 0 };
	int* p = arr;//将数组首元素地址存到p
	int(*p2)[10] = &arr;//*p2是指针,需要使用括号,指向一个有10个元素的数组(即[10]),每个元素的类型是int

	return 0;
}

3. 数组指针的使用

指向指针数组的指针
int main()
{
	char* arr[5] = { 0 };
	char* (*pc)[5] = &arr;
	return 0;
}

错误写法
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	
	int(*p)[] = &arr;//错误写法,元素个数不能省略
	int(*p)[10] = &arr;//正确写法

	return 0;
}

不推荐用法
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	
	int(*p)[10] = &arr;
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		//p是指向数组的(p存放的是整个数组的地址),*p的意思是通过数组地址找到数组
		//*p(对p解引用)其实就相当于数组名,数组名又是数组首元素的地址。
		//所以*p本质上是数组首元素的地址
		//int a = 10; int* p = &a; *p->a
		printf("%d ", *(*p + i));//此方法不合理,不推荐
	}
	

	//正常写法
	int* p = arr;//数组名是首元素地址,赋给整形指针p
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

常见用法
void print1(int arr[3][5], int r, int c)//数组形式
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print2(int(*p)[5], int r, int c)//指针形式
{
    //p指向的是二维数组的行,p加几就跳过几行。p+i是第i行的地址,对p+i解引用就是拿到第i行。
	//因为*(p+i) <=> *(arr+i) <=> arr[i] <=> p[i]
	//所以*p相当于每行的数组名(每一行是一个一维数组),而每行的数组名又相当于首元素地址。
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + 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);

	//int arr[3][5];
	//arr数组名表示数组首元素地址
	//二维数组首元素是第一行
	//此时arr表示数组第一行的地址
	//第一行是5个整型,每个元素为int的数组
	//所以arr是5个元素的整型数组的地址
	//总结:二维数组传参传的是首元素(二维数组的第一行)的地址,第一行是一个一维数组,按照一维数组的数组指针方式写即可
	return 0;
}

看看下面代码的意思:
int main()
{
	int arr[5];//整型数组
	int* parr1[10];//整形指针数组
	int(*parr2)[10];//数组指针

	int(*parr3[10])[5];//parr3先和[10]结合,它是数组。
    //parr3[10]拿走就剩下int(*)[5](这是一个数组指针)。所以parr3是存放数组指针的数组
	//它声明了一个长度为10的数组 parr3,其中每个元素都是一个指向长度为5的一维数组的指针。
	//int(*parr3[10]) 表示一个长度为10的数组,每个元素都是一个指向 int[5] 类型的指针。
    //也就是说,parr3 是一个指针数组,它可以存储指向长度为5的一维数组的指针。
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 2,3,4,5,6 };
	int arr3[5] = { 3,4,5,6,7 };
	int(*parr3[10])[5] = { &arr1,&arr2,&arr3 };
    //[5]表示每个元素都是一个指向长度为5的一维数组的指针,parr3[10]表示这个数组有10个元素
	//parr3[10]是一个数组,
	//*parr3[10]是一个指针数组,有10个元素
	//int(*parr3[10])[5],每个元素是指针且指向一个有5个元素的数组(即数组指针),且该数组类型为int。所以parr3是存放数组指针的数组
	return 0;
}

第四章:数组参数、指针参数

1. 一维数组传参

void test(int arr[])//参数写成数组形式。可行,元素个数可以不写
{}
void test(int arr[10])//可行
{}
void test(int* arr)//可行,数组名是首元素地址,首元素是int类型,所以可以用指针接收
{}
void test2(int* arr[20])//可行,用数组形式接收
{}
void test2(int** arr)//可行,因为首元素地址是int*的地址,所以用二级指针来存放一级指针变量的地址
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };//arr2有20个元素,每个元素是int*
	//arr2是数组名,也是首元素地址,首元素是int*的地址
	test(arr);
	test2(arr2);
}

2. 二维数组传参

void test(int arr[3][5])//可行,用数组形式接收
{}
void test(int arr[][])//不可行,形参的二维数组,行可以省略,列不能省略
{}
void test(int arr[][5])//可行,省略了行,但没省略列
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int* arr)//不可行,二维数组的数组名,表示首元素的地址,其实是第一行的地址,所以一维数组的地址不能放进一级指针
{}
void test(int* arr[5])//不可行,因为这里不是指针形式。这里是指针数组
{}
void test(int(*arr)[5])//可行,*arr是指针,指向的是5个元素,每个元素是int类型的数组
{}
void test(int** arr)//不可行,首元素地址是一个一维数组的地址,不能用二级指针接收
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

3. 一级指针传参

void print(int* p, int sz)//指针传参,实参是什么指针,形参就用什么指针接收
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;//arr是数组名,也是首元素的地址(即1的地址,是个整形)
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}


//如果函数的参数部分是指针,传参可以传什么
void print2(int* p)
{}
int main()
{
	int a = 10;
	int* ptr = &a;
	print2(&a);//如果函数的参数部分是指针,可以传地址
	print2(ptr);//可以传指针变量

	int arr[10];
	print2(arr);//可以传数组名
	return 0;
}

4. 二级指针传参

void test(int** ptr)//传的是二级指针,所以用二级指针接收
{
	printf("num = %d\n", **ptr);
}
int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
	test(pp);
	test(&p);//一级指针的地址传参,也可以用二级指针接收
	return 0;
}


//如果函数的形式参数是二级指针,调用函数的时候可以传什么实参
test(int** p)
{}
int main()
{
	int* p1;
	int** p2;
	int* arr[10];//指针数组
	test(&p1);
	test(p2);
	test(arr);//数组名是首元素地址,首元素是int*类型(即一级指针),所以一级指针的地址要用二级指针来接收
	return 0;
}

第五章:函数指针

1. 定义

函数指针 - 可以跟数组指针类比。数组指针是指向数组的指针。
函数指针就是指向函数的指针

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//&函数名 - 取出的是函数地址
	printf("%p\n", &Add);//函数名和&函数名打印的地址一样
	printf("%p\n", Add);
	//对于函数来说,&函数名和函数名都是函数的地址
	
	int(*pf)(int, int) = &Add;
	//*pf是指针,
	//最后一个()说明该指针指向函数,指向的函数的参数类型是(int, int)
	//第一个int是函数返回的类型
	return 0;
}

2. 函数指针有什么用

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	//通过整形指针来理解
	int a = 10;
	int* pa = &a;
	*pa = 20;
	printf("%d\n", *pa);
	
    //指针变量存了地址以后,可以通过地址找到该对象访问或修改
	//所以函数指针也是一样
    int(*pf)(int, int) = &Add;
    int ret = *(pf)(2, 3);
    printf("%d\n", ret);
	return 0;
}

 3. 函数指针的使用

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int ret = Add(2, 3);//直接调用

	int(*pf)(int, int) = &Add;
	int ret = (*pf)(2, 3);//通过函数地址间接调用
	//(*pf)相当于对函数指针解引用找到函数,然后调用函数
	//调用函数需要传参,(2, 3)是传参

	int ret = pf(2, 3);
	//第二种写法:可以省略*。
	//如果要加*必须放在括号里,即(*pf)。
	//但这里的*无实际作用,只是为了可读性,即pf是个指针。但带*是标准写法

	//为什么可以不写*
	//因为&Add和Add都可以表示函数的地址。
    //Add可以赋给pf,Add又可以直接调用函数,所以pf同样可以(因为pf保存也是函数地址)
	int(*pf)(int, int) = &Add;
	int(*pf)(int, int) = Add;
	int ret = Add(2, 3);
	int ret = pf(2, 3);
	printf("%d\n", ret);
	return 0;
}

4. 函数指针作用的示例

int Add(int x, int y)
{
	return x + y;
}
void calc(int(*pf)(int, int))//因为接收的是函数的地址,所以形参应该是函数指针
{
	int a = 3;
	int b = 5;
	int ret = pf(a, b);//通过函数指针调用Add函数
	printf("%d\n", ret);
}
int main()
{
	calc(Add);//函数名传参,函数名就是函数地址
	return 0;
}

5. 函数指针代码

代码1
int main()
{
	//代码1
	//以下代码是一次函数的调用,调用的是0作为地址处的函数。
	//1. 把0强制类型准换为:无参,返回类型是void的函数地址
	//2. 调用0地址处的这个函数
	
    ( *( void (*)() )0 )();
	
    //void(*p)();//p是函数指针
	//void(*)();//是一种函数指针类型
	//(void(*)())0;//0是整数,但前面是函数指针类型,说明要强制类型转换(将整形转换为函数指针类型),而函数指针类型存的是函数地址,所以可以认为0是个地址
	//这里是把0强制类型转换成函数指针类型,这时候(void(*)())0就是一个函数的地址
	//也就是0地址放一个函数,函数没有参数,返回类型为void
	//(*(void (*)())0);//这里是用第一颗*解引用找到函数
	//(*(void (*)())0)();//又因为这个函数没有参数,所以传参时什么都不传

	
	//它执行了一个函数指针的调用。代码中的(void (*)()) 表示一个函数指针类型,0 表示空指针。
	//整个表达式(*(void (*)())0)() 将空指针转换为函数指针,并调用该函数。


	return 0;
}

代码2
typedef unsigned int unit;//通过此方法重命名函数指针
typedef void(*)(int) pf_t;//此写法错误,要将pf_t放到*旁边
typedef void(*pf_t)(int);//把void(*)(int)类型重命名为pf_t。pf_t是一个类型
int main()
{
	//以下代码是一次函数声明,signal是函数名,。
	//声明的函数的第一个参数类型是int,
    //第二个参数类型是函数指针(该函数指针指向的函数参数是int,返回类型是void)。
	//signal函数的返回类型是一个函数指针(该函数指针指向的函数参数是int,返回类型是void)
	void ( *signal( int, void(*)(int) ) )(int);
	//signal先和后面的括号结合,所以是函数名
	//signal(int, void(*)(int));//后面的int和void(*)(int)是参数类型。
    //第二个类型是函数指针。这里是函数的声明,只需要写函数返回类型,函数名和参数类型
	//把上方代码去掉,还剩void(*)(int)这是一个函数指针类型,说明signal函数的返回类型是函数指针类型

	//上方代码不好理解,需要简化,通过typedef重命名
	pf_t signal(int, pf_t);//signal函数的参数第一个是int,第二个是函数指针类型,返回类型是函数指针类型
	return 0;
}

第六章:函数指针数组

1. 定义

把函数指针放在数组中,就是函数指针的数组

2. 函数指针数组用途

计算器示例
void menu()
{
	printf("***************************\n");
	printf("****  1. add   2. sub  ****\n");
	printf("****  3. mul   4. div  ****\n");
	printf("*******   0. exit   *******\n");
	printf("***************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);

	    //int x = 0;
	    //int y = 0;
	    //int ret = 0;
		//输入操作数代码放在这里会有问题
		//如果选择的不是1234,依然要输入操作数,这不符合逻辑,所以要放进switch里
		
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:>");//但是放进switch里有新问题,代码冗余。case1234中只有一句代码是不重复的
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

使用函数指针数组的实现:
//假设还想实现x&y,x^y,x|y,x&&y,x||y,x>>y,x<<y等功能
//需要在菜单函数增加选项,并增加对应的计算函数,还有主函数中的case选项也要增加
void menu()
{
	printf("***************************\n");
	printf("****  1. add   2. sub  ****\n");
	printf("****  3. mul   4. div  ****\n");
	printf("*******   0. exit   *******\n");
	printf("***************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	int x = 0;
	int y = 0;
	int ret = 0;
	int input = 0;
	//使用函数指针数组,可以轻易的增加或删除想要的计算功能  (转移表)
	int(*pfArr[])(int, int) = { 0,Add,Sub,Mul,Div };//0占位使用,为了让Add和下标1对齐

	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);//input是几就访问这个数组下标为几的元素
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} 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;
}
int main()
{
	//函数指针
	int(*pf)(int, int) = Add;

	//函数指针数组
	int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };

	//指向【函数指针数组】的指针
	int(*(*ppfArr)[5])(int, int) = &pfArr;
	//pfArr先和*结合,说明是指针。
	//该指针指向的是[5],说明指向了数组
	//数组的元素类型是int(*)(int, int),即函数指针
	return 0;
}

第八章:回调函数

1. 定义

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

2. 计算器(回调函数版本)

void menu()
{
	printf("***************************\n");
	printf("****  1. add   2. sub  ****\n");
	printf("****  3. mul   4. div  ****\n");
	printf("*******   0. exit   *******\n");
	printf("***************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
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);
	printf("%d\n", ret);
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

3. 复习冒泡排序

下方函数只能排序整型数组

//假设数组是0,1,2,3,4,5,6,7,8,9 该函数还是要两两比较,只是没有交换。
//并且还是要遍历9轮。但是如果第一轮遍历完发现一个都没有交换,说明这个数组已经是排序好了。
void bubble_sort(int arr[], int sz)
{
	int i = 0;//排序的轮数
	for (i = 0; i < sz - 1; i++)//总共进行元素个数sz-1轮排序
	{
		int j = 0;//元素下标
		for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

优化版本
void bubble_sort(int arr[], int sz)
{
	int i = 0;//排序的轮数
	for (i = 0; i < sz - 1; i++)//总共进行元素个数sz-1轮排序
	{
		int flag = 1;//假设数组是拍好序的
		int j = 0;//元素下标
		for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 0;//一旦有元素交换了,就要把flag置成0
			}
		}
		if (flag == 1)//如果flag还等于1,说明数组已经有序
		{
			break;
		}
	}
}
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);
	//打印排序好的数组
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

4. 演示qsort函数使用

#include <stdlib.h>
//使用库函数qsort - 可以排序任意类型的数据
void qsort(void* base, //要排序的数据的起始位置
		   size_t num, //待排序的数据元素个数
		   size_t width,//待排序数据元素的大小(单位是字节)
		   //因为不同数据比较方法不同,不仅限于>或<之类。所以要把比较方法提取出来
		   //e1是要比较的第1个元素,e2是要比较的第2个元素,e1和e2是要比较元素的地址
		   int(__cdecl* cmpare)(const void* elem1, const void* elem2)//函数指针-比较函数
		   );
__cdecl - 函数调用约定,按c的方式去调用,可以去掉


//比较两个整形元素
//e1指向一个整数,e2指向另外一个整数
//e1和e2都是指针,存的都是地址
int cmp_int(const void* e1, const void* e2)
{
	//void*指针是不能直接解引用
	//void*指针的作用
	//int a = 10;
	//char* pa = &a;//类型不匹配,编译器会报警告
	//void* pv = &a;//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);//升序
	//return (*(int*)e2 - *(int*)e1);//降序
    <0	The element pointed to by e1 goes before the element pointed to by e2
    0	The element pointed to by e1 is equivalent to the element pointed to by e2
    >0	The element pointed to by e1 goes after the element pointed to by e2
}
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]), cmp_int);//最后一个参数是把函数传给函数指针
	//打印排序好的数组
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

5. 使用qsort排序结构体

struct Stu
{
	char name[20];
	int age;
};
#include <string.h>
int cmp_stu_by_name(const void* e1, const void* e2)//比较名字
{
	//不能直接使用e1,因为它是void*类型,所以需要强制类型转换成结构体指针
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//两个名字是两个字符串,所以使用strcmp函数
	//strcmp函数返回值是大于0 等于0 小于0的数字
}
int cmp_stu_by_age(const void* e1, const void* e2)//比较年龄
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//两个名字是两个字符串,所以使用strcmp函数
}
void test2()
{
	//测试使用qsort来排序结构体
	struct Stu s[] = { {"zhangsan", 18},{"lisi", 30},{"wangwu", 25} };
	int sz = sizeof(s) / sizeof(s[0]);
	//qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);//名字字母升序
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);//年龄升序

}
int main()
{
	test2();

6. 使用回调函数,模拟实现qsort(采用冒泡的方式)

int cmp_int(const void* e1, const void* e2)
{
	return (*(int*)e1 - *(int*)e2);//升序
	//return (*(int*)e2 - *(int*)e1);//降序
}

//参考qsort函数重新设计冒泡排序
void Swap(char* buf1, char* buf2, int width)
{
	//该函数是把buf1指向位置和buf2指向位置 向后width宽度字节的内容交换
	int i = 0;
	//width是数据的宽度,也是一个数据所占字节
	for (i = 0; i < width; i++)//这里一次交换一个字节,如果是int,width=4,交换4次
	{
		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++)//总共进行元素个数sz-1轮排序
	{
		int flag = 1;//假设数组是拍好序的
		int j = 0;//元素下标
		for (j = 0; j < sz - 1 - i; j++)//每轮交换排序的次数比上一轮少1次。第一轮交换元素个数sz-1次(第一次i=0)
		{
			//base是void*,void* 是无具体类型的指针,所以不能解引用操作,也不能+-整数操作
			//所以要强制类型转换。如果用int*一次跳过太多字节,所以用char*提高精度
			//这里用下标j来乘以宽度就可以决定一次跳过几个字节
			//大于0说明前一个数比后一个数大,不符合升序,要交换
			//这里是调用比较函数,传的参数是代比较两个元素的地址
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				//交换
				//光有起始位置还不够,还需要知道宽度,即多少字节
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 0;//只要产生交换就要将flag置成0,避免下方跳出循环
			}
		}
		if (flag == 1)//如果flag还等于1,说明数组已经有序
		{
			break;
		}
	}
}

void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	//int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
	//把数组排成升序
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//最后一个参数是把函数传给函数指针
	//打印排序好的数组
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}


//使用自定义函数bubble_sort(模拟qsort)来排序结构体
int cmp_stu_by_name(const void* e1, const void* e2)//比较名字
{
	//不能直接使用e1,因为它是void*类型,所以需要强制类型转换成结构体指针
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//两个名字是两个字符串,所以使用strcmp函数
	//strcmp函数返回值是大于0 等于0 小于0的数字
}
int cmp_stu_by_age(const void* e1, const void* e2)//比较年龄
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;//两个名字是两个字符串,所以使用strcmp函数
}
void test4()
{
	
	struct Stu s[] = { {"zhangsan", 18},{"lisi", 30},{"wangwu", 25} };
	int sz = sizeof(s) / sizeof(s[0]);
	//bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);//年龄升序
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);//名字升序

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

第九章:指针和数组笔试题解析

一维数组

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16
	//sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节
	printf("%d\n", sizeof(a + 0));// 4/8
	//a不是单独放在sizeof内部,也没有取地址,所以a就是首元素地址,a+0还是首元素地址
	//是地址,大小就是4/8个字节。a <==> &a[0],a+0 <==> &a[0]+0
	printf("%d\n", sizeof(*a));// 4
	//a是首元素地址,对首元素地址解引用得到首元素,*a=1,sizeof(1)表示1个整型大小
	//a <==> &a[0],*a <==> *&a[0],*&a[0] <==> a[0]
	printf("%d\n", sizeof(a + 1));//4
	//a+1指向第二个元素的地址
	printf("%d\n", sizeof(a[1]));//4
	//计算的是第二个元素的所占内存空间的大小
	printf("%d\n", sizeof(&a));// 4/8
	//&a取出的是整个数组的地址
	printf("%d\n", sizeof(*&a));//16
	//&a(int (*)[4])取出整个数组的地址,数组指针解引用就访问整个数组,等价于sizeof(a)
	//或者理解为&和*抵消
	printf("%d\n", sizeof(&a + 1));// 4/8
	//&a取出整个数组的地址,+1跳过整个数组,但依然是地址
	//&a+1 是从数组a的地址向后跳过了一个(4个整型元素)数组的大小
	printf("%d\n", sizeof(&a[0]));// 4/8
	//&a[0]是第一个元素的地址,计算地址的大小
	printf("%d\n", sizeof(&a[0] + 1));// 4/8
	//计算第二个元素地址的大小
	return 0;
}

字符数组1

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));//6
	//数组名单独放在sizeof中,表示整个数组,计算的是整个数组的大小
	printf("%d\n", sizeof(arr + 0));// 4/8
	//arr+0是数组首元素地址
	printf("%d\n", sizeof(*arr));// 1
	//*arr是数组首元素,大小是1字节。*arr <==> *(arr+0) <==> arr[0]
	printf("%d\n", sizeof(arr[1]));// 1
	//计算第二个元素的大小
	printf("%d\n", sizeof(&arr));// 4/8
	//&arr取出整个数组的地址
	printf("%d\n", sizeof(&arr + 1));// 4/8
	//&arr取出整个数组的地址,+1跳过整个数组的地址
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8
	//第二个元素的地址
	
    //strlen
    //size_t strlen(const char* string);
    //strlen参数是指针,所以传过去的是地址
    printf("%d\n", strlen(arr));//随机值(>=6)
	//数组里没有\0,要找到/0才停下来
	printf("%d\n", strlen(arr + 0));//随机值
	//arr是首元素地址,+0还是首元素地址
	printf("%d\n", strlen(*arr));//野指针
	//arr是首元素地址,首元素地址解引用得到是首元素
	//此代码相当于strlen('a'),a的ASCII码值是97,所以又相当于strlen(97),相当于把97作为地址传给strlen
	//97这种地址不能随便传,而是空间开辟给你的时候,才是有效的。知道地址还不行,要分配给你才可以访问
	//这时97就变成野指针,程序报错
	printf("%d\n", strlen(arr[1]));//野指针
	//相当于strlen('b'),又相当于strlen(98)。同上方一样问题
	printf("%d\n", strlen(&arr));//随机值
	//&arr取出整个数组的地址,不过也是从首元素地址开始
	//虽然arr和&arr类型不一样,但都是传给strlen,所以都是从首元素开始
	printf("%d\n", strlen(&arr + 1));//随机值(随机值-6)
	//&arr取出整个数组,+1跳过整个数组,从数组后方开始找/0
	printf("%d\n", strlen(&arr[0] + 1));//随机值(随机值-1)
	//&arr[0]首元素地址,+1是第二个元素地址,从这开始向后找/0

   	return 0;
}

字符数组2

//strlen是求字符串长度的,关注的是字符串中的\0,计算的是\0之前出现的字符的个数
//strlen是库函数,只针对字符串
//sizeof只关注占用内存空间大小,不在乎内存中放的是什么
//sizeof是操作符
int main()
{
	char arr[] = "abcdef";// [abcdef/0]
	printf("%d\n", sizeof(arr));//7
	//数组名单独放到sizeof中,表示整个数组,求数组大小
	printf("%d\n", sizeof(arr + 0));// 4/8
	//arr是数组名,首元素地址,+0还是首元素地址
	printf("%d\n", sizeof(*arr));//1
	//arr是数组名,解引用得到首元素
	printf("%d\n", sizeof(arr[1]));//1
	//求第二个元素大小
	printf("%d\n", sizeof(&arr));// 4/8
	//&arr取出整个数组的地址
	printf("%d\n", sizeof(&arr + 1));// 4/8
	//&arr取出整个数组地址,+1跳过整个数组,即数组后面的地址
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8 
	//&arr[0]+1 第二个元素的地址,b的地址

	
	printf("%d\n", strlen(arr));//6
	//arr是数组名,是首元素地址
	printf("%d\n", strlen(arr + 0));//6
	//arr是数组名,也是首元素地址,+0还是首元素地址
	printf("%d\n", strlen(*arr));//报错
	//*arr得到的是首元素
	printf("%d\n", strlen(arr[1]));//报错
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	return 0;
}

字符指针

int main()
{
	char* p = "abcdef";//把首字符地址放到p里,内存里存放的是abcdef\0,这是一个常量字符串。p和字符串在内存中各自有自己的独立空间
	printf("%d\n", sizeof(p));// 4/8
	//p是指针变量
	printf("%d\n", sizeof(p + 1));// 4/8
	//p+1仍然是地址
	printf("%d\n", sizeof(*p));//1
	//对char*类型指针解引用,得到一个字符
	printf("%d\n", sizeof(p[0]));//1
	//p[0] <==> *(p+0)
	printf("%d\n", sizeof(&p));// 4/8
	//&p是二级指针
	printf("%d\n", sizeof(&p + 1));// 4/8
	printf("%d\n", sizeof(&p[0] + 1));// 4/8
	//p[0]是第一个字符,&取地址就是a的地址,+1是b的地址

	printf("%d\n", strlen(p));//6
	//p存放的是a的地址,向后找\0
	printf("%d\n", strlen(p + 1));//5
	printf("%d\n", strlen(*p));//报错
	//*p是a
	printf("%d\n", strlen(p[0]));//报错
	printf("%d\n", strlen(&p));//随机值
	//从指针p的位置开始向后找\0
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5
	//p[0]指向第一个元素,&取地址就是a的地址,+1是b的地址
	return 0;
}

二维数组

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//48
	//sizeof(数组名),计算整个数组大小
	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][0],+1表示第一行第二个元素的地址
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	//a[0]+1表示第一行第二个元素的地址,解引用就得到该整形元素
	printf("%d\n", sizeof(a + 1));// 4/8
	//a虽然是二维数组的地址,但是并没有单独放在sizeof内部,也没有取地址
	//a表示首元素地址,二维数组的首元素是它的第一行,a就是第一行的地址
	//a+1是第二行的地址
	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/8
	//a[0]是第一行数组名,&取地址就取出第一行的地址,+1指向第二行
	//所以(a+1) <==> (&a[0]+1)
	printf("%d\n", sizeof(*(&a[0] + 1)));//16
	//(&a[0] + 1)是第二行的地址,解引用得到第二行
	//*(a + 1) <==> *(&a[0] + 1)
	printf("%d\n", sizeof(*a));// 16
	//a表示首元素地址,二维数组首元素是第一行,对第一行的地址解引用,得到第一行
	printf("%d\n", sizeof(a[3]));//16
	//a[3]可以理解为第四行数组名,而sizeof(数组名)是计算数组大小
	//这里是根据二维数组的类型去分析,不是真的访问,本质上sizeof(a[3])等价于sizeof(a[0])
	//int a = 10;
	//sizeof(int);//这个直接告诉sizeof 是什么类型
	//sizeof(a);//这种是分析a的类型
	return 0;
}

总结:

数组名的意义:

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

第十章:指针笔试题

笔试题1

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);//跳过整个数组 后面的地址
	printf("%d,%d", *(a + 1), *(ptr - 1));//2 5
	//a是数组名,数组名是首元素地址,+1指向第二个元素
	//ptr是整形指针,指向数组后面,-1就向前移动一个整形,即数组最后一个元素
	return 0;
}

笔试题2

struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节(x86)
int main()
{
	printf("%p\n", p + 0x1);//0x100014
	//p是结构体指针,结构体指针+1跳过一个结构体(即20个字节),而20个字节用十六进制表示是14
	printf("%p\n", (unsigned long)p + 0x1);//0x100001
	//这里是把十六进制的100000转换成长整型,即十进制的1048576,+1就是1048577,再转换成十六进制就是100001
	printf("%p\n", (unsigned int*)p + 0x1);//0x100004
	//p被转换成整形指针,+1跳过4个字节,十进制的4用十六进制表示也是4,即100004
	return 0;
}

笔试题3

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	//&a取出整个数组的地址,+1跳过数组,再转换成整形指针
	int* ptr2 = (int*)((int)a + 1);
	//a是数组名,也是首元素地址,将这个值准换成整形
	//假设a=0x0012ff40,a+1=0x0012ff44,(int)a+1=0x0012ff41
	//将a的地址强制转换成整形后再+1,就是数值+1
	//再转换成指针,两个地址相差1,意味着向后移动了1个字节
	//又因为vs是小端存储模式,数组a在内存的存储情况如下:
	//01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
	//所以ptr2指向了01后面的00,而ptr是整形指针,向后访问4个字节,就是00 00 00 02
	//因为小端存储是倒序存放,所以将内存中的00 00 00 02取出后是02 00 00 00

	printf("%x,%x", ptr1[-1], *ptr2);//4  02000000
	//prt1[-1] <==> *(prt1+(-1)) <==> *(ptr1-1)
	//prt1指向数组后面,向前移动一个整形指向4,4在内存中是小端存储(即倒序存放),即04 00 00 00
	//取出来,要倒序,即00 00 00 04,但打印时前面的0省略
	return 0;
}

笔试题4

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	//这里是逗号表达式。逗号表达式会从左向右以此计算,整个表达式结果是最后一个表达式的结果
	//(0, 1)结果是1;(2, 3)结果是3;(4, 5)结果是5;
	//a数组实际结果是{ {1,3},{5,0},{0,0} }
	int* p;
	p = a[0];
	//a[0]是的第一行的数组名,它既没有放到sizeof内部,也没有&取地址,所以数组名表示首元素地址
	//即第一行第一个元素1的地址,即a[0][0]的地址,&a[0][0]
	printf("%d", p[0]);//1
	//p[0] <==> *(p+0)
	return 0;
}

笔试题5

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	//a是数组名,数组名是首元素地址,二维数组首元素是第一行,即第一行的地址。第一行是5个整型的一维数组
	//a的类型是int(*)[5]。p指向二维数组a第一行第一个元素的位置
	//p+1或对p解引用一次访问4个整型
	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[4][2]相当于a[3][3],两个指针相减得到指针和指针之间的元素个数,即4个
	//地址的结果是-4
	//10000000000000000000000000000100 - -4原码
	//11111111111111111111111111111011 - -4反码
	//11111111111111111111111111111100 - -4补码
	//把-4的补码当做地址打印ff ff ff fc
	//注意:整型地址+1加的是一个整型
	return 0;
}

笔试题6

int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);
	//&数组名取出整个数组,+1跳过整个数组,ptr1指向aa数组后面
	int* ptr2 = (int*)(*(aa + 1));
	//aa是数组名,是首元素地址,二维数组首元素是第一行,aa是第一行地址,+1跳过一行指向第二行
	//解引用第二行地址,得到第二行,也相当于拿到第二行数组名,*(aa + 1) <==> aa[1]
	//而aa[1]既没有放在sizeof内部,也没有取地址,所以又相当于第二行第一个元素的地址
	//在转换成int*,指向第二行第一个元素
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
	return 0;
}

笔试题7

int main()
{
	char* a[] = { "work","at","alibaba" };
	//a是字符指针数组,a存放的是work,at,alibaba首字符的地址
	char** pa = a;//等价于char* *pa,第二颗*说明pa是指针,第一颗*说明pa指向的对象是char*类型
	//a是数组名,数组名是首元素地址,即work首字符的地址
	pa++;//等价于pa+1 
	//pa指向的是char*,pa+1跳过一个char*
	printf("%s\n", *pa);//at
	return 0;
}

笔试题8

int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	//c是指针数组,c存放的是ENTER,NEW,POINT,FIRST首字符的地址
	char** cp[] = { c + 3,c + 2,c + 1,c };
	//cp是二级指针数组,存放的是c+3的地址(即FIRST);c + 2的地址(即POINT);c + 1的地址(即NEW);c的地址(即ENTER);
	char*** cpp = cp;
	//cpp存放的是cp的地址,cp是数组名,即首元素地址,即c+3
	printf("%s\n", **++cpp);//point
	//++cpp指向了cp数组的第二个元素,即c+2
	//第一次解引用得到了cp数组的内容c+2,第二次解引用得到c+2指向的内容point首字符p的地址
	printf("%s\n", *-- * ++cpp + 3);//er
	//cpp此时指向cp数组的第二个元素,即c+2,再++(即++cpp)就指向第三个元素,即c+1
	//*++cpp得到就是c+1,前面的--(即-- * ++cpp)就是让c+1再-1,得到c,而c指向的是enter首字符e的地址
	//对上面的地址(*-- * ++cpp)解引用,得到enter,在+3就是跳过3个字符,最后得到er
	printf("%s\n", *cpp[-2] + 3);//st
	//cpp此时指向cp数组的第三个元素,即c+1(此时c+1已经被改成c)
	//cpp-2(等价于* *(cpp-2))指向cp数组第一个元素,解引用得到c+3,再解引用得到first首字符f的地址,在+3就是跳过3个字符,最后得到st
	printf("%s\n", cpp[-1][-1] + 1);//ew
	//cpp[-1][-1]等价于*(*(cpp-1)-1)
	//cpp此时还是指向cp数组的第三个元素,即c+1(此时已经被改成c)
	//cpp-1指向cp数组的第二个元素,解引用得到c+2,再-1就是c+1,对c+1解引用得到new首字符n的地址,在+1就是跳过一个字符,最后得到ew
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值