指针的进阶

指针进阶

我们在初阶时就已经接触过指针,了解了指针的相关内容,有:

  • 指针定义:指针变量,用于存放地址。地址唯一对应一块内存空间。
  • 指针大小:固定32位平台下占4个字节,64位8个字节。
  • 指针类型:类型决定指针±整数的步长及指针解引用时访问的大小。
  • 指针运算:指针解引用,指针±整数,指针-指针,指针关系运算。

本章节在此基础上,对C语言阶段指针进行更深层次的研究。

字符指针

字符指针,存入字符的地址,类型为char*

字符指针的作用

  • 指向单个字符
char ch = 'w';
char* pc = &ch;
*pc = 'a';

这里指针pc就是ch的地址,指向字符ch。
字符地址的类型是字符指针类型(char*)。

  • 指向字符串首字符
char* pstr = "hello world";
printf("%s\n", pstr);//hello world
printf("%s\n", pstr+1);// ello world
printf("%c\n", *(pstr+1));//e

pstr就是字符串首字符的地址,指向字符’h’。
pstr+1指向第二个字符’e’,以此类推。
打印字符串,遇到’\0’停止打印。
"hello world"是常量字符串,无法修改,其实应该加const,但是舍弃const这个字符串还是不可以修改。

有如下例题

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

str1与str2多是字符串首字符地址,str3与str4也是字符串首字符的地址。
str1 == str2实际上是地址的比较
str1和str2是普通的数组,是在内存上开辟了两块空间不过存放了一样的数据。
str3和str4指向常量字符串,存放在内存的常量区,是不可被修改且具有唯一性即常量区只存放一个。所以str3和str4指向的都是同一个字符串。
所以str1 != str2,str3 == str4。

总结

  • 常量字符串不可被修改,存放在内存的常量区。
  • 具有唯一性即常量区只存放一个。

指针数组

指针数组的定义

指针数组是一个存放指针的数组

int* arr1[10];整型指针数组
char* arr2[4];字符指针数组
char** arr3[5];二级字符指针数组

指针数组的使用

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c =30;
	int d = 40;
	int* arr[4] = {&a, &b, &c, &d};
	//arr是数组,有四个元素,每个元素多是int*
	for (int i=0; i<4; i++)
	{
		printf("%d ", *arr[i]);
	}
	return 0;
}

int main()
{
	int arr1[] = {1, 2, 3, 4, 5};
	int arr2[] = {2, 3, 4, 5, 6};
	int arr3[] = {3, 4, 5, 6, 7};
	int* p[] = {arr1, arr2, arr3};
	int i = 0;
	for (i=0; i<3; i++)
	{
		int j = 0;
		for (j=0; j<5; j++)
		{
			//printf("%d ", *(*(p+i)+j));
			printf("%d ", p[i][j]);
		}
		printf("\n");
	}
	return 0;
}

int main()
{
	const char* p[] = {"abcd", "bcde", "cdef", "defg"};
	int i = 0;
	for (i=0; i<4; i++)
	{
		//printf("%s\n", p[i]);
		//printf("%s\n", *(p+i));
		int j = 0;
		for (j=0; j<4; j++)
		{
			//printf("%c ", p[i][j]);
			printf("%c", *(*(p+i)+j));
		}
		printf("\n");
	}
	return 0;
}

总结 - 很重要

p[i] == *(p+i)
p[i][j] == *(*(p+i)+j) == *(p+i)[j] == *(p[i]+j)

数组指针

数组指针就是数组的地址,指向数组的指针。

数组指针的定义

int arr[10] = {0};
//1.
int* pa = arr;
//2.
int* par = &arr;
//3.
int* parr[10] = {arr, arr+1};
//4.
int (*parrr)[10] = &arr;

1.pa是整型指针,指向数组首元素。
2.&arr是数组指针类型, 而par是整型指针类型,报错。
3.parr是指针数组,每个元素多是int*,[]比*优先级高。
4.parrr是数组指针,指向的数组有10个元素,每个元素为int。

总结

  • 去掉名字就是类型
  • 字符地址就是字符指针类型,整型地址就是整型指针类型etc.
  • int[10] - 整型数组类型
  • int* - 整型指针类型
  • int*[10] - 指针数组类型
  • int(*)[10] - 指针数组类型

数组名和&数组名

&数组名是整个数组的地址
sizeof(数组名)是整个数组的大小,单位是byte
除了以上两种情况,所有其他数组名多是首元素地址

	int arr[10] = {0};
	//arr是数组首元素的地址
	//&arr[0]:首元素的地址
	//&arr:整个数组的地址
	//有两种情况是整个数组
	//1.&arr
	//2.sizeof(arr),单位是字节
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr);
	printf("%p\n", arr+1);
	printf("%p\n", &arr[0]+1);
	printf("%p\n", &arr+1);
	return 0;

数组指针的使用

遍历数组,使用数组或是指针作形参接收就行了。且所谓的用数组接收仅是理解层面,本质上都是指针。

void print1(int arr[], int sz)
{
	int i = 0;
	for (i=0; i<sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

void print2(int* p, int sz)
{
	int i = 0;
	for (i=0; i<sz; i++)
	{
		printf("%d ", *(p+i));
	}
}

void print3(int (*p)[10], int sz)
{
	int i = 0;
	for (i=0; i<sz; i++)
	{
		//printf("%d ", (*p)[i]);
		printf("%d ", *(*p+i));
	}
}

int main()
{
	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int sz = sizeof(arr) / sizeof(arr[0]);
	//print1(arr, sz);
	//print2(arr, sz);
	print3(&arr, sz);
	return 0;
}

下面是二维数组传参

#define ROW 3
#define COL 5

void print(int (*p)[5], int row, int col)
{
	int i = 0;
	for (i=0; i<row; i++)
	{
		int j = 0;
		for (j=0; j<col; j++)
		{
			//printf("%d ", p[i][j]);
			printf("%d ", *(*(p+i)+j));
		}
		printf("\n");
	}
}

void print2(int arr[][COL], int row, int col)
{
	int i = 0;
	for (i=0; i<row, i++)
	{
		for j = 0;
		for (j=0; j<col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[ROW][COL] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7};
	print1(arr, ROW, COL);
	print2(arr, ROW, COL);
	return 0;
}

例题

//1.
int arr[5];
//2.
int *pa1[5];
//3.
int (*pa2)[10];
//4.
int (*pa3[10])[5];

1.整型数组,int[5] - 整型数组类型
2.指向数组,int*[5] - 指向数组类型
3.数组指针, int()[10] - 数组指针类型
4.pa3是一个数组,数组里有10个元素,每个元素类型为int(
)[5] - 数组指针类型

数组传参 指针传参

一维数组传参

void test1(int arr1[])
{}

void test1(int arr1[10])
{}

void test1(int* arr1)
{}

void test2(int* arr2[20])
{}

void test2(int* arr2[])
{}

void test2(int** arr2)
{}

int main()
{
	int arr1[10] = {0};
	int* arr2[20] = {0};
	test1(arr1);
	test2(arr2);
	return 0;
}

以上一维数组传参均可以

二维数组传参

void test(int arr[3][5])
{}

void test(int arr[][5])
{}

void test(int (*p)[5])
{}

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

一级指针传参

void print(int* p, int sz)
{
	int i = 0;
	for (i=0; i<sz; i++)
	{
		printf("%d ", *(p+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]);
	print(p, sz);
	return 0;
}

思考:
当一个函数的参数部分为一级指针的时候,函数可以接收哪些参数?

int a = 10;
test(&a);
int arr[10] = {0};
test(arr);
int* p = &a;
test(p);

二级指针传参

void test(int** p)
{}

int main()
{
	int a = 10;
	int* pa = &a;
	int** paa = &pa;
	int* arr[10];
	test(&pa);
	test(arr);
	test(paa);
	return 0;
}

函数指针

函数指针的定义

函数指针:存放函数地址的指针
函数名 == &函数名

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

int main()
{
	//int (*p)(int, int) = Add;
	int (*p)(int, int) = &Add;
	//int ret = (*p)(3, 5);
	int ret = p(3, 5);
	printf("%d\n", ret);
	return 0;
}

例题

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

void()()是函数指针类型,把0强制类型转换为函数指针类型,也就是把0强制转换为指针,指针中存放着指向的函数的地址,((void()())0)就是指针所指向的函数,就相当于p,也就是p(),这里无参数。
首先这是个函数,函数的两个参数类型为int,void(
)(int),函数的返回类型为void (*)(int)。

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

函数指针数组

函数指针数组是一个数组,数组每一个元素多是函数指针类型。

函数指针数组的定义

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

函数指针数组的使用

利用函数指针数组实现计算器,以简化调用过程。

转移表

//计算器实现1.0
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 input = 0;
	do{
	menu();
	printf("请选择:>");
	scanf("%d", &input);
	switch(input)
	{
		case 0:
			printf("退出成功\n");
			break;
		case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = div(x, y);
			printf("%d\n", ret);
			break;
		default:
			printf("请重新选择\n");
			break;
	}
	} while(input);
	return 0;
}

我们看到上面代码特别繁琐,我们使用函数指针数组实现计算器。

//计算器实现2.0
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 input = 0;
	int (*parr[5])(int, int) = {0, add, sub, mul, div};
	do{
	menu();
	printf("请选择:>");
	scanf("%d", &input);
	if (input >= 0 && input <= 4)
	{
		if (input == 0)
		{
			printf("退出成功\n");
		}
		else
		{
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			int ret = parr[input](x, y);
			printf("%d\n", ret);
		}
	}
	else
	{
		printf("请重新选择\n");
	}
	}while (input);
	return 0;
}

函数指针数组实现不同选择情况下,通过函数地址“跳转”到不同的函数的功能。
这样的函数指针数组成为转移表。(跳转功能)

回调函数
switch语句的性能要优于if语句,所以我们这里不舍弃switch语句。

//计算器实现3.0
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 (*p)(int, int), int x, int y)
{
	printf("请输入操作数:");
	scanf("%d %d", &x, &y);
	int ret = p(x, y);
	printf("%d\n", ret);
}

int main()
{
	int x = 0;
	int y = 0;
	int input = 0;
	do{
	menu();
	printf("请选择:>");
	scanf("%d", &input);
	switch(input)
	{
		case 0:
			printf("退出成功\n");
			break;
		case 1:
			Calc(add, x, y);
			break;
		case 2:
			Calc(sub, x, y);
			break;
		case 3:
			Calc(mul, x, y);
			break;
		case 4:
			Calc(div, x, y);
			break;
		default:
			printf("请重新选择\n");
			break;
	}
	}while (input);
	return 0;
}

通过函数指针调用的函数叫做回调函数,回调函数即使第三方调用函数的参数也在其中被调用。
若想在调用函数中随条件变化而调用不同的函数,就必须使用回调函数的方法:调用函数中使用函数指针,指向不同函数。回调函数在大型工程中显得非常方便。

指向函数指针数组的指针

顾名思义,指向函数指针数组的指针存放函数指针数组的地址。

void test1(char* p)
{}

void test2(char* p)
{}

int main()
{
	void (*par[10])(char*) = {test1, test2};
	void (*(*parr)[10])(char*) = &par;
	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=sz-1; j>i; j--)
		{
			if (arr[j] < arr[j-1])
			{
				int tmp = arr[j];
				arr[j] = arr[j-1];
				arr[j-1] = tmp;
			}
		}
	}
}

int main()
{
	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int sz = sizeof(arr) / sizeof(arr[0]);
	Bubble_sort(arr, sz);
	for (int i=0; i<10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

与冒泡排序作对比发现,冒泡排序仅需起始地址和元素个数即可,暗含了其他信息。由于过度具体化,冒泡排序只能排序整型数组,且比较函数过于简单无需单独列出。

因为qsort排序可适用于多种类型如浮点型,字符型,自定义类型的数据,故无法规定具体类型,所以需要多个参数去描述元素的基本信息。
qsort之所以能够适应多种数据,是因为参数void* base再搭配上num和size就描述出任意一种类型。

为什么将参数base的类型定义为void*呢?如下述代码所示。

int a = 10;
char* p1 = &a;//类型不兼容
float f = 1.2f;
char* p2 = &f;//类型不兼容
void* p1 = &a;
void* p2 = &f;

确定类型的地址之间直接赋值会提示类型不兼容,强制转化也可能会导致精度丢失。

故使用无(具体)类型void*,又称通用类型,即可以接收任意类型的指针,但是无法进行指针运算(解引用,±整数等)。

void* p1;
void* p2;
p++;//error
*p;//error
p1-p2;//error
p1 > p2;//error

1.base:用于存入数据的起始地址。类型定义为void*,可接受任意类型的指针。
2.num:待排序的元素个数。
3.size:元素大小,单位是byte。

明确了排序的起始位置,元素个数和元素大小,貌似已经够了。但是并无法排序所有类型,因此必须自定义一个抽象的比较函数指定元素的比较方式。

4.cmp:比较函数,用于指定元素的比较方式。

  • *p1小于*p2,返回值小于0。
  • *p1等于*p2,返回值等于0。
  • *p1大于*p2,返回值大于0。

5.p1 p2:进行比较的两个元素的地址。

qsort可以说是一个半库函数半自定义函数。自定义在于其函数最后一个参数为比较函数,该函数内部实现自由,但返回值必须按照规定返回相应的数值。

小结
需要qsort函数排序各种类型的数据。

  • 故base起始地址不可为固定的指针类型,只能用void*。
  • 既然是通用类型还要明确比较元素的个数和大小。
  • 最后,排序最核心的比较大小,为适应不同的类型元素必须自定义专门的比较函数。

qsort实现冒泡排序

//比较函数:整型
#include <stdlib.h>
int int_cmp(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

int main()
{
	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), int_cmp);
}

比较函数int_cmp不需要传参,作为回调函数由qsort直接调用。比较函数的传参过程由qsort内部实现。

qsort实现结构体排序

#include <stdlib.h>

struct Stu
{
	char* name;
	int age;
	float score;
};

void score_cmp(const void* p1, const void* p2)
{
	//升序
	return ((struct Stu*)p1) -> score - ((struct Stu*)p2) -> score;
	//降序
	return ((struct Stu*)p2) -> score - ((struct Stu*)p1) -> score;
}

void name_cmp(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1) -> name, ((struct Stu*)p2) -> name);
}

int main()
{
	struct Stu s[3] = {{"zhangsan", 22, 66.2f}, {"lisi", 23, 99.9f}, {"wangwu", 24, 86.5f}};
	int sz = sizeof(s) / sizeof(s[0]);
	//按成绩排序
	qsort(s, sz, sizeof(s[0]), score_cmp);
	//按名字排序
	qsort(s, sz, size(s[0]), name_cmp);
}

由此可得,提取出一个比较函数,具体交换的方式由qsort内部实现。

模拟实现qsort

用qsort的函数逻辑,实现冒泡排序。

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

int name_cmp(const void* p1, const void* p2)
{
	return strcmp((struct Stu*)p1 -> name, (struct Stu*)p2 -> name);
}

int score_cmp(const void* p1, const void* p2)
{
	return (struct Stu*)p1 -> score - (struct Stu*)p2 -> score;
}

void Swap(const char* p1, const char* p2, int size)
{
	int i = 0;
	for (i=0; i<size, i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

void my_bubble_sort(void* base, size_t num, size_t size, int (*p)(const void*, const void*))
{
	int i = 0;
	for (i=0; i<num-1; i++)
	{
		int j = 0;
		for (j=num-1; j>0; j--)
		{
			if (p((char*)base+size*(j-1), (char*)base+size*j) > 0)
			{
				Swap((char*)base+size*(j-1), (char*)base+size*j, size);
			}
		}
	}
}

void print1(int arr[], int sz1)
{
	int i = 0;
	for (i=0; i<sz1; i++)
	{
		printf("%d ", arr[i]);
	}
}

void print2(struct Stu* p, int sz2)
{
	int i = 0;
	for (i=0; i<sz2; i++)
	{
		printf("%s ", (p+i) -> name);
	}
}

void print3(struct Stu* p, int sz2)
{
	int i =0;
	for (i=0; i<sz2; i++)
	{
		printf("%f ", (p+i) -> score);
	}
}

struct Stu
{
	char* name;
	int age;
	float score;
}

int main()
{
	int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	my_bubble_sort(arr, sz1, size(arr[0]), cmp);
	struct Stu s[3] = {{"wangwu", 22, 45.6}, {"lisi", 23, 98.7}, {"zhaosan", 25, 65.3}};
	int sz2 = sizeof(s) / sizeof(s[0]);
	my_bubble_sort(s, sz2, sizeof(s[0]), name_cmp);
	my_bubble_sort(s, sz2, sizeof(s[0]), score_cmp);
	print1(arr, sz1);
	print2(s, sz2);
	print3(s, sz2);
	return 0;
}

地址统一强转为char*,以最小字节单位一个字节进行比较和交换,使代码更具有普适性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值