C语言入门-指针

目录

前言

1.指针(一)

1.1内存和地址

​编辑

1.2 指针变量

什么类型的变量能存放地址?这个变量可以通过地址来访问地址下的数据。答案就是指针变量。

两个重要操作符&和*

1.3 指针类型

指针在内存空间的大小

指针类型的作用

1.4 const

const修饰变量

1.5 指针运算

指针+-整数

指针-指针

 void*  指针

1.6 野指针

概念:野指针就是指针指向的位置随机的,不可知的。

成因

 如何避免野指针(空指针介绍)

1.7 assert介绍

1.8 指针的使用

strlen

模拟实现strlen

函数递归实现strlen

传值和传址调用

2.指针(二)

2.1 数组名的新认识

2.2 使用指针访问数组

2.3 一维数组传参本质

2.4冒泡排序

2.5 二级指针

2.6 指针数组

 指针数组引入

 用指针数组模拟二维数组

3.指针(三)

3.1 字符指针

3.2 数组指针

3.3 二维数组传参本质

3.4 函数指针

函数指针数组

转移表(函数指针数组的简单应用)

4.指针(进阶一)

4.1 回调函数

4.2 qsort

qsort介绍和使用

用冒泡排序模拟实现qsort

 5.结尾


前言

C语言基础:初步入门,会函数,数组即可。

指针一:初步介绍指针相关知识,比如指针变量,指针类型,指针如何使用,指针作为函数参数。

指针二:围绕数组,主要讲指针与一维数组。包括如何用指针访问数组,二级指针,冒泡排序,还有指针数组。

指针三:难度较高,围绕指针,说明各种类型的指针,从字符指针引入,到数组指针,函数指针,到函数指针数组。

指针(进阶一):了解回调函数的概念,并学会使用排序函数qsort,最后模拟用冒泡排序模拟实现qsort。

指针(进阶二):待更。


1.指针(一)

1.1内存和地址

  先引入一个生活案例,假设你住在一个宿舍楼,你住的宿舍水龙头坏了。此时,你需要去楼下填表,届时就会用修理师傅上门。修理师傅是如何找上门的?是因为你留了房间号,他就知道那间水龙头坏了。那么这里的房间号类比到计算机,这就是指针。

为什么学习指针,先要了解内存?因为指针是用来访问内存的。内存编码==指针==地址。

0x开头的数字默认为16进制,如0x11223344。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int a = 10;
	printf("%p\n", &a);
	return 0;
}

 上述变量创建

int a = 10;//本质是向内存申请4个字节的空间。int的大小是4个字节。

1.2 指针变量

什么类型的变量能存放地址?这个变量可以通过地址来访问地址下的数据。答案就是指针变量。

两个重要操作符&和*

int* p= &a;

p是变量名,p前面加上*,*说明p是指针变量。int* p说明p的类型为int*型(后面会再提到)

int说明 p指向的对象是int 类型。

&a------& 取地址操作符,上述语句,表示把a的地址取出来赋值给指针变量p。

 首先,上述运行结果表示,a的地址确实赋值给到了p。

其次,说明解引用操作符---,*p 间接访问了a,并通过赋值*p=0,改变了a的值。因此,解引用操作符,通过p的地址,找到变量a,这为我们提供了另一个改变变量的途径。

1.3 指针类型

(type)* p

1.p是变量名,*说明p是指针变量,p的类型是type* ,p指向的对象是type类型。

2.这里的 type 是泛指,如1.2所说的int,可以是 short,long,float, double等。

指针在内存空间的大小

为什么结果统一是4呢?

接下来我们谈一下地址如何产生的。CPU和内存要用线连起来,这里我们关心地址总线,在32位机器里,每根线有0/1,一根线有两种可能,那么一共有2^32种可能,每种就代表一个地址。所以我们得出结论,地址是32个0/1二进制序列。储存这样的地址要32个比特位(=4个字节),所以类型大小是4个字节。这是X86下进行,X64下为8个字节。

结论:指针变量大小为4/8个字节。指针类型大小跟平台(X86 X64)有关。

指针类型的作用

看来上面的内容,感觉指针类型大小很统一,这里先给结论,具体请看1.5指针运算->指针与整数的运算。

结论:指针类型决定了指针进行解引用操作时,它能访问多少个字节,也可以说指针类型决定了指针访问权限。

1.4 const

const修饰变量

在声明变量语句前加上const修饰,变量的值不可修改了。

const修饰指针变量

int * const p;

int const * p;

const int * p;

以下讨论这三种情况并给出相应结论。

结论:

const 在*左,本身可改,指向不能改。如p1,p3。

const 在*右,本身不可改,指向可改。如p2。

1.5 指针运算

指针+-整数

指针加减整数结果仍为指针。因为数组在内存中连续存放,随下标增长地址由低变高。

我们下面以一维数组举例。

 每次p+1,地址加4,相当与走了四个字节的长度(1个int的大小)。

 这次p+1,地址加1,等于走了一个字节(1个char类型的大小)。

对于 int* p;p+1就跳过4个字节。

对于 char* p;p+1就跳过1个字节。

结论:p+1 ->跳过 1*sizeof(type);

           p+n ->跳过 n*sizeof(type);

指针-指针

1.两个指针必须指向一段连续空间,比如一个一维数组。

2.指针-指针运算,减完的结果的绝对值是之间相差元素个数

3.不存在指针+指针运算。

指针是地址,指针减指针相当于地址减地址,地址是16进制,减出来结果必然是多少字节,在换算成相应类型的元素个数。指针加指针相当于地址加地址,16进制数加16进制数,结果无法想象,因此特定条件可以用指针与指针减法满足,但指针之间的加法运算不存在。

 void*  指针

  void* 可以接受任何类型的指针,但不可解引用(void*访问权限未知),不能进行上述的指针运算。

1.6 野指针

概念:野指针就是指针指向的位置随机的,不可知的。

成因

1.指针未初始化。

2.指针越界访问

3.指针向已经释放空间的内存访问。

 如何避免野指针(空指针介绍)

创建指针变量,在不确定赋值时,习惯给它赋值成空指针NULL

#define NULL ((void* )0)  //NULL具体意义为0(0x00000000)的地址。

int* p = NULL;//0也是地址,但这个地址我们无法使用,否则会报错。

注意:程序只能访问分配给自己的内存单元,否则就是非法访问了,不能随便访问内存空间的。

小心越界访问。指针只访问已经申请内存的空间,不能超出范围访问。

指针变量不再使用后,最终赋值为NULL(空指针)。

1.7 assert介绍

头文件:assert.h

assert不是函数,是类函数宏,这个宏被称为"断言"。

这个宏接受一个表达式,assert(表达式),若表达式结果为真(非0),程序将会继续执行下去,否则会在屏幕中写入错误信息,显示哪个表达式没通过。

#include<stdio.h>
#include<assert.h>


int main()
{
	assert(0);//表达式为假,会报错指明错误文件位置。
	return 0;
}

assert能 自动标识文件和出问题的行号 (见上图),还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果程序都没问题了,那么把#define NDEBUG  语句在 #include<assert.h>前,会禁用所有assert语句。

#include<stdio.h>
#include<assert.h>
int main()
{
    int* p = NULL;
    assert(p != NULL);
    return 0;
}

 

1.8 指针的使用

strlen

strlen函数,相信大家已经不陌生了。

头文件:string.h

功能:计算字符串的长度。

网站:cplusplus.com

size_t strlen ( const char * str );

类型 size_t 无符号整型。

从参数知,它接受字符串的起始地址,然后统计字符串'\0'之前的字符数。

 

 结果符合。

模拟实现strlen

那么如何模拟实现strlen?

我们可以从起始位置,遍历字符串,如果字符!='\0'那么继续执行,否则结束循环返回个数。

#include<stdio.h>
#include<string.h>//调用库函数strlen需引用的头文件。
size_t my_strlen(const char* p);//函数声明
int main()
{
	char arr[] = "Hello World!";
	size_t ret = strlen(&arr[0]);
	printf("%zd\n", ret);
	printf("%zd\n", my_strlen(&arr[0]));//将起始地址传给my_strlen.
	return 0;
}

size_t my_strlen(const char* p)//接受一个字符串的起始地址
{
	size_t ret = 0;//创建变量储存循环次数,也就是字符数
	while (*p != '\0')//每次判断
	{
		p++;//指针往后走,检查一下个字符
		ret++;//循环一次,'\0'前的字符数加一。
	}
	return ret;//返回'\0'前的字符数。
}

 

 结果与strlen函数调用一致。

函数递归实现strlen

#include<stdio.h>
#include<string.h>//调用库函数strlen需引用的头文件。
size_t my_strlen(const char* p);//函数声明
int main()
{
	char arr[] = "Hello World!";
	size_t ret = strlen(&arr[0]);
	printf("%zd\n", ret);
	printf("%zd\n", my_strlen(&arr[0]));//将起始地址传给my_strlen.
	return 0;
}

size_t my_strlen(const char* p)//接受一个字符串的起始地址
{
	if (*p != '\0')
	{
		p++;
		return 1 + my_strlen(p);
	}
	else
		return 0;
}


 

传值和传址调用

问题:如何创建函数来实现变量交换?

//在主函数中这样交换变量
​​​​#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d , b = %d\n", a, b);
	int temp = a;//创建临时变量交换两个数。
	a = b;
	b = temp;
	printf("交换后:a = %d , b = %d\n", a, b);
}

 可能的想法:把交换部分的代码封装成函数不就可以了吗。结果真能如此吗?

为什么做不到交换的效果呢?我们来调试一下。

1.x和y确实交换了,但a和b没有交换。因为swap函数在创建局部变量x,y时向内存申请空间,分别把a,b的值拷贝了一份,地址不同。交换x,y做不到交换a,b的效果。

那么要在swap函数操作main函数中的局部变量a,b就要指针变量为参数,通过指针解引用的操作来交换a,b。

#include<stdio.h>
void swap(int* x,int* y)
{
	int temp = *x;//创建临时变量交换两个数。
	*x = *y;
	*y = temp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d , b = %d\n", a, b);
	swap(&a, &b);//传a和b的地址。
	printf("交换后:a = %d , b = %d\n", a, b);
}

 指针间接访问,a与b交换了。

总结:

上面的操作,分别叫函数的传值调用和传址调用。函数调用过程形参是实参的一份临时拷贝。如果只想操作变量存储的数据就进行传值调用,若想通过形参来改变实参就必须用指针的形式,用解引用操作符* ,这是传址调用。

2.指针(二)

2.1 数组名的新认识

先下结论:数组名是数组首元素的地址。

两个例外:

1.sizeof(arr)中,arr表示整个数组。

2.&arr,arr表示整个数组。

数组名是数组首元素的地址,即arr=&arr[0];上面展示所有的&arr[0]均可替换为arr。

图示结果符合结论。

接下来说明的两个例外。

sizeof(arr)中,arr表示整个数组。

sizeof(),单独放一个数组名,才代表整个数组。如sizeof(arr+0),这个arr就理解为首元素的地址了。

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	/*printf("&arr[0] = %p\n", &arr[0]);
	printf("    arr = %p\n", arr);*/
	printf("sizeof(arr) = %zd", sizeof(arr));
	return 0;
}

 这里的arr表示是整个数组,这个数组有10个int 的元素,10*4,结果是40(字节)。

事实上,可以这么理解,sizeof(),这个单目操作符(),无论放入类型还是变量,都会判断()为什么类型并返回结果。我们知道数组是有类型的,去掉数组名就是它的类型,int [10]就是这个数组的类型,sizeof(int [10])结果是40,很好理解吧。

&arr,arr表示整个数组。

以上面的 int arr[10]举例

一、arr是数组名,也是数组首元素的地址,类型是int* 。

二、&arr 是这个数组的地址,类型是int* [],这是我们后面要提到的数组指针,这里先留个印象。

arr和&arr的地址是一样的,但由于指针类型不同,+1后移动的字节不同。因为arr是地址,arr可以执行指针的运算,但arr不是指针变量,不能进行赋值和自增等运算。

arr+1跳过了4个字节。

&arr+1 跳过了40个字节(整个数组),(E-C)*16+(4-0)=40。

因为类型的不同,指针+-1跳过的字节不同。1.5指针运算

2.2 使用指针访问数组

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	//输⼊
	
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//输⼊
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p + i);
		//scanf("%d", arr+i);//可以这样写
	}
	//输出
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
        //printf("%d ",*(arr+i));
	}
	return 0;
}

 arr是地址,可以赋值给指针变量。

所以arr+i和p+i等价。先前学习数组时,我们用[]操作符,即arr[i]来访问数组元素。

那么p[i]是否也能用来访问数组元素?

p[i]换成*(p+i)也行,本质上,p[i]==*(p+i)。事实上,用arr[i],编译器会理解成*(arr+i)

*(arr+i)==arr[i]==i[arr]--->(不推荐),右边两个都会转换成*(arr+i),所以写成任意形式效果是一样的。

结论

数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

数组在内存中连续存放,指针+偏移量可以很好访问整个数组。

2.3 一维数组传参本质

​

//数组形式作为函数参数
//void print_arr(int arr[], int sz)
//{
//	int i = 0;
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", arr + i);
//	}
//	return;
//}

//指针形式作为函数参数​
void print_arr(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
	return;
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	return 0;

}

 前面说过arr是地址,所以无论是前面的数组形式还是指针形式,本质都是把地址传给函数。

一维数组传参的本质是传递首元素的地址。

学习数组和函数的两个问题

1.为什么元素个数要在main函数(主调函数)中计算?

2.为什么在被调函数中修改数组,能改变原来的数组?

第一个问题:

sz预期是10,但结果是1,与预期不符。

因为print_arr中的arr是指针变量,类型是int* ,sizeof(int* )=4,sizeof(arr[0])=4,结果必然是一。

main函数中sizeof(arr),arr表示整个数组;但是print_arr中arr是形参,本质为指针变量。

第二个问题:

因为传参传的是地址,操作的是同一个数组,不存在创建一个新数组拷贝原来的数组对应数据的说法。

2.4冒泡排序

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; 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;
			}
		}
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", arr[i]);
	}
	return 0;

}

 冒泡排序的核心思想:两两相邻元素比较

分析:

{9,8,7,6,5,4,3,2,1,0}

要把这数组排成升序,先看第一个数和第二个数

9比8大所以交换

{8,9,7,6,5,4,3,2,1,0} //第一次比较

{8,7,9,6,5,4,3,2,1,0} //第二次比较

{8,7,6,9,5,4,3,2,1,0} //第三次比较

...............

{8,7,6,5,4,3,2,1,0,9}//第九次比较,确定了最大值9

至此,一趟冒泡排序结束了。

但还剩8个数待排序,于是要进行第二躺冒泡排序。

{7,6,5,4,3,2,1,0,8,9}//第二趟冒泡排序,确定了次大值8

第二趟进行了8对数的比较。

经过上面分析,10个数的降序数组要排成升序,要进行9躺冒泡排序。

第一趟比较9对数

第二趟比较8对数......

第九趟比较1对数

因此,我们可以给上面的代码进行解释。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;//趟数
	for (i = 0; i < sz-1; i++)//循环sz-1次,即进行sz-1趟冒泡排序
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)//进行第i+1趟的冒排,就要减少i对
		{
			if (arr[j] > arr[j + 1])//结果有右比左大,不满足就交换。
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
	for (i = 0; i < sz; i++)//遍历打印
	{
		printf("%d\n", arr[i]);
	}
	return 0;

}

下面试着用冒泡排序写一个排序函数吧

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void bubble_sort(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (p[j] > p[j + 1])
			{
				int tmp = p[j];
				p[j] = p[j + 1];
				p[j + 1] = tmp;
			}
		}
	}
}
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);
	for (int i = 0; i < sz; i++)//遍历打印
	{
			printf("%d\n", arr[i]);
	}
	return 0;
}

2.5 二级指针

  pp==&p;p==&a;

 前面提过指针指向类型决定了指针+1是跳过多少大小的字节。

同样 pp是二级指针,指向类型是指针类型。pp+1就跳过4/8字节。

2.6 指针数组

 指针数组引入

了解指针数组前,先回顾我们已经学过的数组类型。

整型数组--->存放整型的数组。

字符数组--->存放字符的数组。

由此我们可以推出,指针数组是存放指针的数组,本质上还是数组,只不过数组内元素数据类型是指针。

其实,区分函数指针,指针函数,指针数组,数组指针,指针函数,函数指针数组到底是什么。简单理解最后两个字理解为名词,前面理解为形容词。比如,指针数组翻译为指针的数组,即存放指针的数组。再比如说,函数指针数组,理解为函数的指针的数组(也可以理解为专门存放函数地址的数组,是一类特殊的指针的数组),存放函数的指针的数组具体看3.4-函数指针

 先创建一个数组

int main()
{
	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* arr[3] = { arr1,arr2,arr3 };
	return 0;
}

 

 arr存放了三个一维数组的地址。

 用指针数组模拟二维数组

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>


int main()
{
	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* arr[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
	}
	return 0;
}

 第一层for循环嵌套,访问arr数组三个元素,arr三个元素又是地址再通过下标引用操作符访问各个数组的元素。

1.为什么可以用arr[i][j]的形式?

前面提过arr[i]被编译器理解为*(arr + i)。

所以arr[i][j],编译器会转化成    *(*(arr + i) + j)。(arr+i)是arr数组元素的地址,解引用后找到对应数组的地址,在通过偏移访问arr1,arr2,arr3的元素。

2.指针数组等价与二维数组吗?

不等于。

第一,二维数组虽然分行列,但仍然在内存中连续储存。上面的指针数组arr中三个一维数组的地址不一定连续。

第二,指针数组可以存其它类型的地址,不一定是数组的地址。

以下是arr1,arr2,arr3的整形数组分配的内存,显然不连续。

 

 二维数组在内存中连续存放

3.指针(三)

3.1 字符指针

指针有一种类型 char*


int main()
{
	char ch = 'Y';
	char* p = &ch;
	*p = 'Q';
	return 0;
}

 字符数组和字符串

int main()
{
	char arr[] = "hello world";

	//这两种写法有什么区别?
	
	char* p1 = "hello world";
	char* p2 = arr;
	return 0;
}

 arr是一个字符数组,数组意味着数组的元素可以修改。

char* p1="hello world";C语言内置数据类型都没有能存放字符串的,这句话意思是把字符串的首元素地址赋值给了p1。这种字符串为常量字符串,它不能被修改。

arr数组会向内存申请空间,把这些字符连续存放在栈区。

常量字符串是存储在静态存储区,只是把地址传给了p1。

字符数组的内容可修改,常量字符串的内容不可修改。

 思考一下下面的问题


#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指向的是⼀个同一个常量字符串。C语言会把常量字符串存储到单独的⼀个内存区域(静态区)。因此,几个指针指向同⼀个字符串的时候,它们实际会指向同⼀块内存。

3.2 数组指针

数组指针是什么?指针,存放的是数组的地址,能够指向数组的指针变量。

回忆学过的指针类型 :char*   int*   double*。

那么如何创建一个简单的数组指针?

[]下标引用操作符优先级高于*解引用操作符。

p是一个变量名。

由于优先级 (*p)就是一个指针变量。

int (*p)[5] 就是一个简单数组指针。

p的类型是int (*)[5]

*p即p指向的类型为int [5]

Q1:p1,p2,谁是数组指针和指针数组。

#include<stdio.h>

int main()
{
	int* p1[10];
	int(*p2)[10];
	return 0;
}

 p1是指针数组,p2是数组指针。

强调一遍:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

数组指针初始化

 数组指针加一跳过了40个字节符合指针运算。存放整个数组的地址用数组指针。

3.3 二维数组传参本质

前面提过一维数组的数组名首元素的地址,二维数组同样满足。

学习二维数组的时候,提到二维数组的元素是一维数组。

二维数组的首元素看做是一个一维数组,每个元素都是一维数组。那么二维数组首元素的地址就是第一行一维数组的地址

void print(int a[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 ", a[i][j]);
		}
	}
}


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

 前面提一维数组的本质时,形参可以写成数组形式也可以写成指针形式。

根据数组名是数组⾸元素的地址,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀ 维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。说明二维数组传参本质也是传递了地址,只不过时数组的地址。

void print(int (*a)[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 ", a[i][j]);
		}
	}
}


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

 无论写成数组形式还是指针形式都是传地址。

结论:二维数组作为形参,既可以写成数组形式,也可以写成数组指针形式。

3.4 函数指针

函数有地址吗?


#include<stdio.h>
void test()
{
	printf("hehe\n");
}

int main()
{
	printf(" test = %p\n", test);
	printf("&test = %p\n", &test);
	return 0;
}

 类比数组,函数名就是函数的地址,那么&函数名也可以获得函数地址,结果也是函数的地址。

函数名和&函数名都是函数的地址,没区别。

什么类型的变量能存储函数的地址,没错,就是函数指针变量。

类比数组指针的写法,如下

int (*paff)(int x, int y);
//或者 int (*paff)(int,int);

//形参名可以省略。
//(*paff)为指针变量
//int 表明函数的返回类型。
//(int ,int) 表明函数有两个参数,都是int类型

 可以通过函数指针来调用函数,以下两种写法均可。

函数指针数组

如果把函数指针储存在一个数组,那么这个数组就叫做函数指针数组。

int (*puf[5])(int,int)

 puf先和[]结合,说明puf是一个数组,*表明这是一个指针数组,外面int (int,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;
}
int (*paff[4])(int, int) = { ADD,SUB,MUL,DIV };

转移表(函数指针数组的简单应用)

#include<stdio.h>
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 (*paff[5])(int, int) = { 0,ADD,SUB,MUL,DIV };

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:
		case 2:
		case 3:
		case 4:
			printf("请输入两个数:>");
			scanf("%d%d", &x, &y);
			ret = paff[input](x, y);
			printf("结果是: %d \n", ret);
			break;
		default:
			printf("输入错误\n");
		case 0:
			break;
		}
	} while (input);
}

4.指针(进阶一)

4.1 回调函数

回调函数是一个通过函数指针调用的函数。

#include<stdio.h>​
typedef int(* Cacl)(int, 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 cacl(Cacl p)
{
	int x = 0; int y = 0;
	printf("请输入两个数:");
	scanf("%d%d", &x, &y);
	int ret = p(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;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请输入:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			cacl(ADD);
			break;
		case 2:
			cacl(SUB);
			break;
		case 3:
			cacl(MUL);
			break;
		case 4:
			cacl(DIV);
			break;
		default:
			printf("输入错误\n");
		case 0:
			break;
		}
	} while (input);
	return 0;
}

 这里main函数根据输入,将不同的算术函数的地址传递给cacl函数,cacl函数内部通过这个函数指针调用函数。其中,这些主调函数通过传函数指针在另一个函数调用相应的函数,那么这就称作回调函数。

回调函数不是有主调方直接调用,而是在满足条件下由另一方调用,用作对该事件响应。

4.2 qsort

qsort介绍和使用

网站:legacy.cplusplus.com

qsort
库函数
头文件: stdlib.h
采用的快速排序。

qsort共有四个参数
1.void* base 是一个无具体类型的指针,作为形参,可以接受任何类型的参数。这里可以是待排序数组的首元素地址
2.size_t num  待排序的个数,size_t是无符号整形。接受一个无符号整形。
3.size_t size 传入数组中元素类型大小,单位字节。
4.int (*cmp)(const void* e1,const void* e2)    这里是函数指针作为参数,接受int (*)(const void* e1,const void* e2)这类函数类型的指针。因为不知道比较的元素是什么类型,而且也不希望修改它,所以它是void*用const修饰。
下面我们要学习如何创建这个函数。
我们从这个函数指针类型可以知道,这个函数返回类型是int,有两个形参都是void* 指针,可以认为数组中两个相邻之间的元素的指针。
假设比较整型:
可以这样*((int*)e1) - *((int*)e2),注意强制类型转换,因为void* 不能解引用。
如果像上面一样写,那么
表达式结果   >0   交换
表达式结果<=0    不交换
最后会排序结果是升序数组。如果要降序,只需调换e1和e2的位置。

#include<stdio.h>
#include<stdlib.h>//qsort包含的头文件
int cmp_int(const void* e1, const void* 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(int), cmp_int);
	//1.把数组首元素的地址传过去,2.整个数组都要排序(传数组的元素个数),3.数组元素的类型大小用sizeo计算并传参,4.自行构造比较整形的函数然后传函数指针。
	
	//打印观察
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

int cmp_int(const void* e1, const void* e2)
{
	//强制类型转换成int* ,再解引用!!!
		return *((int*)e1) - *((int*)e2);
}

用冒泡排序模拟实现qsort

回顾一下冒泡排序

qsort可以排序任意类型的数据,尝试思考下面代码要修改哪些部分

//该函数只能接受整型指针,也就是对int 的数据排序
void bubble_sort(int* arr, int sz)
{
	//趟数
	int i = 0;
	for (i = 0; i < sz-1; i++)//第一次for循环可以不用修改
	{
		//一趟内部两两比较
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)//第二层for循环也不用更改
		{
			//两个整型元素可以直接使用>比较
			//两个字符串,两个结构体元素是不使用>比较的!!!
            //但我们作为使用者,明确知道要排序的是什么数据类型,所以我们可以自己定义比较函数作为判断条件
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

参数改造理解:

1.void*的指针,无具体类型的指针,它可以作为函数参数接受任何类型的指针。作为bubble_sort的第一个参数。

2.排序要清楚待排的元素个数,int sz 可以保留,但个数非负,写成size_t更严谨。

3.传了指针和排序的元素个数,知道起始位置,但在函数内部不知道它的多少字节。所以必须知道一个元素多大!第三个参数是size_t size。

4.用户知道自己排序的数据类型,根据函数类型要求自定义函数。第四个参数要函数指针为形参。int (*cmp)(const void* e1,const void* e2)。

基本框架有了,

void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
//趟数
	int i = 0;
	for (i = 0; i < sz-1; i++)
	{
		int j = 0;//一趟内部相邻两两比较
		for (j = 0; j < sz - 1 - i; j++)
		{
			if(cmp())//这里不再只是整型,用cmp比较;
            ;//非完整代码
		}
	}
}

void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
	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)
			{
				//交换两个元素。
				/*
				不能这样写了
				int tmp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=tmp;
				*/
				//接下来通过一个字节一个字节的交换
				swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
				//传两个相邻元素的地址,还要传它们原本的类型大小
			}
		}
	}
}

void swap(char* p1, char* p2, size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++)//类型多少个字节则交换多少次。
	{
		//char 大小一个字节,用它作为中间变量,来起到交换作用。
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
	return;
}

 最后的代码

#include<stdio.h>
#include<stdlib.h>//qsort包含的头文件
//函数声明
void swap(char* p1, char* p2, size_t width);
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2));
int cmp_int(const void* e1, const void* e2);
int main()
{
	int arr[] = { 9,11,7,60,8,4,5,6,16,10 };//把这个数组排成升序。
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数
	bubble_sort(arr, sz, sizeof(int), cmp_int);
	
	//打印观察
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

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

void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
	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)
			{
				//交换两个元素。
				/*
				不能这样写了
				int tmp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=tmp;
				*/
				//接下来通过一个字节一个字节的交换
				swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
				//传两个相邻元素的地址,还要传它们原本的类型大小
			}
		}
	}
}

void swap(char* p1, char* p2, size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++)//类型多少个字节则交换多少次。
	{
		//char 大小一个字节,用它作为中间变量,来起到交换作用。
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
	return;
}

 

 5.结尾

有错指正,私信,谢谢了。

随缘补充内容。

指针进阶二待更。

  • 32
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值