【C语言】指针进阶

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

现在带领大家探讨指针更深层次的内容

字符指针

  1. 单引号引起的字符实际上是ascll值
  2. 双引号引起的字符串实际上是代表了一个指向无名数组起始地址的字符指针,该数组由双引号之间的字符加’\0’组成

我们来看

int main()
{
	char* pa = "abcdef";  //相当于const char* pa = "abcd"
	
	char arr[] = "abcdef";
	char* pb =  arr;	
	return 0;
}

我们先看这里的pa,实际上存储的是字符串"abcdef"首字符的地址放在pa中

再来看pb,实际上是存储了字符数组arr的首地址到pb中,即pb指向arr数组的首元素

那两者有什么区别?

  1. 类似于pa这样的直接指向无名数组的,后面的无名数组是常量字符串。不能被修改。
  2. 通过建立字符数组,用指针指向首元素的,可以通过解引用修改元素,类似于pb
int main()
{
	char* pa = "abcd";  //相当于const char* pa = "abcd"
	*pa = 'w';			//这里会异常,因为是常量指针
	printf("%c", *pa);	//程序会崩,运行下面的需要注释这2行

	char arr[] = "abcd";
	char* pb = arr;
	*pb = 'w';
	printf(arr);
	return 0;
}

我们用代码说话,大家可以去尝试编写代码,就可以清楚的认识到这2种的区别了

char* p = "abd";
可以理解为数组,但我们要明白这种和数组的区别在于是常量字符串

ok我们清楚了这些,我们来看一个面试题,请问下面应该输出什么

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    const char *str3 = "hello world.";
    const char *str4 = "hello world.";
    
    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数组名不一样,就是2个不同的数组,开辟了是2块不同的空间。我们的str1与str2指向的是不同空间的首地址,故str1与str2不相同

我们再来分析str3与str4
在这里插入图片描述
通过前面的学习,我们知道了这里指向的是常量字符串,不能被更改,所以这里的常量字符串只有一块空间,假设起始地址为0x11223344
那str3与str4指针都是存储的这块地址,指针就是地址,我们比较指针就是在比较地址,两者当然相同。但是如果比较的是&str3与&str4呢

数组指针

我们在学习数组指针之前,先复习一下指针数组

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

ok我们来探讨,数组指针是数组还是指针?
是指针!
我们数组指针,是指向数组的指针,这点需要牢记


> 我们怎么来记住数组指针的写法呢?
>  首先我们应该是一个指针	 	(*p)
>  再应该是指向的数组			(*p)[10] 
>  数组每一个元素是int类型  		int*p)[10]

我们值得注意的是

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

现在大家应该知道取整个数组的地址,应该用谁接收了吧

&数组名与数组名

理解更深层次前,我们先来复习一下简单的知识

数组名大多数情况下都是数组首元素的地址
但是有2个例外

  1. sizeof(数组名) —sizeof里面单独放一个数组名的时候,计算的是整个数组的大小,代表的也是整个数组。
  2. &数组名 —这里的数组名代表了是整个数组,值得注意的是,取出来的地址与首元素的地址一样,但意义不一样。

我们来看一下这些代码每次跳过多少字节

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr+1);//4

	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0]+1);//4

	printf("%p\n", &arr);
	printf("%p\n", &arr+1);//40
	return 0;
}

在这里插入图片描述
分别跳过4字节 4字节 40字节

使用数组指针

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0;
}

上面的是最容易理解的数组指针的使用,接下来我们来看数组指针的下一步使用

int main()
{
	int arr[] = { 1,2,3,4,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int (*p)[5] = &arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d", *((*p) + i));
		//printf("%d ", (*p)[i]);
	}
	return 0;
}

注意我们的2次解引用分别代表什么
而我们直接使用,是先解引用出了首元素的地址

printf("%d ", (*p)[i]);

我们这样来打印数组,可读性太差了,不推荐使用

我们再来看数组指针作为形参的用法

当我们的二维数组作为实参的时候

我们知道

  1. 二维数组的数组名,代表首元素地址
  2. 二维数组的首元素是第一行
  3. 首元素的地址就是第一行的地址,是一个一维数组

二维数组可以理解为是存放一维数组的数组

比如我们的

int arr[][3] =  {1,2,3,4,5,6,7,8,9};
arr+0 指向第一行
arr+1 指向第二行
arr+2 指向第三行

我们将arr作为实参传给函数的时候,我们应该用数组指针来接收,这样就用到了我们的数组指针,第一次解引用访问这一行,第二次解引用是拿到这一行的首元素地址

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


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

这里值得注意的是

printf("%d ",  *( * (arr + i) + j));
printf("%d ", arr[i][j]);

相信看了之前的初阶指针,就可以知道这里是为什么转换的

ok我们学习了数组指针,我们来看一下这是什么意思

int (*parr3[10])[5];

我们看到这题先不要慌张,先看优先级括号里面的内容
(*parr3【10】)
由于*操作符的优先级特别低,parr3先和[10]结合,构成数组,ok这就是你的主体,剩下的就是我们的元素类型了

int (*) [5],即整体是数组,数组里面的每一个元素都是数组指针类型的

在这里插入图片描述

数组,指针传参

一维数组传参

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

总结:一维数组传参的时候,形参可以是数组,可以是指针类型(本质都是指针),当形参是指针的时候,要注意指针类型的使用

二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}

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

总结:二维数组传参,形参可以是数组也可以是指针,如果是数组,行可以省略,列不能省略。如果是指针,实参是第一行的地址,形参就应该是数组指针

我们在传参的时候,最好使用地址传参,因为你地址传参,无非就是4/8字节的大小,可以减少内存的销毁,这一点在结构体传参中尤为重要

一级指针传参

#include <stdio.h>
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;
	int sz = sizeof(arr)/sizeof(arr[0]);
	//一级指针p,传给函数
 	print(p, sz);
    return 0;
}

这里值得注意的是,这样的传参并不能改变实参的内容,main与print是2块不同的函数栈帧,print函数里的p是实参的临时拷贝,如果想改变实参就要使用二级指针

二级指针传参

#include <stdio.h>
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;
}

二维数组,你如果传参arr过去,是一行的地址
形参用int* p接收的话,看似能打印出来整个二维数组
但是这是编译器自动给你转化了

Int * 这块空间,不管来什么值,都是当作整型指针看待
由于你的一行地址与首元素地址同名并且数组连续所以你可以打出来

函数指针

整数指针—指向整数的指针
字符指针—指向字符的指针
数组指针—指向数组的指针

那函数指针是不是指向函数的指针?函数有指针吗?
有的!想想看你的函数栈帧

我们用代码来验证

void test()
{
 	printf("hehe\n");
}

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

在这里插入图片描述

我们可以看见函数是有地址的
而且对于函数来说,数组名与&数组名是一样的,都是代表的是函数的地址

那么对于函数指针,我们应该怎么写?还是一样的道理
首先是一个指针—(*p)
再是一个函数—(*p)()
函数参数写上—(*p)(int x, int y)
返回类型—int (*p)(int x, int y)

p就是我们的变量名

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

int main()
{
	int (*p)(int, int) = add;
	//add与&add一样的,并且p=add,所以可以不用解引用
	int ret =  p(1, 2);
	printf("%d", ret);
	return 0;
}

在上面的代码里

p就是你的add
所以你p前面的*是摆设,可加可不加,加多少无所谓

题目分析
当我们学到这的时候,我们可以借用《C陷阱和缺陷》里面的代码帮助我们理解

( * ( void (*) () ) 0) ();

我们看这些复杂代码要学会从左至右按顺序按优先级去看代码,将他们一个一个抽离出来

一眼扫过去,我们就可以看见

void (*) ()		将其抽离出来

这不就是我们刚学的函数指针类型吗,先抽离出来看剩下的

( * ( ) 0) ();

我们再来看0前面的括号是什么意思?括号里面放类型是强制类型转换
!原来是将0强制转换为函数指针类型。即在0处放了一个函数,函数返回类型是void,没有参数

那剩下的这些不就是调用函数吗

( *  ) ();

整体来说就是将0强制类型转换成一个函数指针,该函数返回类型是void,没有参数,并调用整个函数

ok我们有了这个题目的基础,再来看下一个题目

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

我们用之前的方法,抽离成2个模块

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

这一看就是一个函数,但是为什么少了返回类型呢?那么接下来的模块就是它的返回类型

void (*   )(int);

当我们写成这样,你一看就知道什么意思了

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

但是事与愿违,当有指针的符号的时候,我们的变量名得与指针在一起,这是语法规则

大家如果觉得这样写函数比较麻烦,我们可以typedef一下

函数指针数组

要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

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

答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。

int sub(int x, int y)
{
	return x - y;
}

int div(int x, int y)
{
	return x /y;
}

int mul(int x, int y)
{
	return x * y;
}

//函数指针数组
int main()
{
	int (*arr[4])(int, int) = { add,sub,div,mul };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ",arr[i](3, 2));
	}
	return 0;
}

通过我们得函数指针数组,我们可以更便捷得访问调用函数,这点体现在转移表中

指向函数指针数组的指针

指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针

void test(const char* str)
{
	 printf("%s\n", str);
}

int main()
{
	 //函数指针pfun
	 void (*pfun)(const char*) = test;
	 //函数指针的数组pfunArr
	 void (*pfunArr[5])(const char* str);
	 pfunArr[0] = test;
	 //指向函数指针数组pfunArr的指针ppfunArr
	 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
	 return 0;
}

我们觉得每一步不好写的话,可以逐渐从第一步开始改造

这里我知道有这个东西即可

回调函数与qsort与void指针

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

对于我们的模拟计算器项目
我们之前的函数指针数组,是来解决case语句过多的情况下
这里的回调函数,就可以解决case语句冗余的情况下

谈起回调函数,我们不得不提起我们的qsort函数

qsort—是库函数,用来排序,使用的是快速排序的方法

我们来看C库里是怎么使用qsort的,头文件是#include <stdlib.h>
在这里插入图片描述

在这里插入图片描述

我们一个一个的来看参数,第一个为什么是void类型的?
因为开发者并不知道你要排序的是什么类型的数组

我们来看一下void类型的指针

int main()
{
	int a = 0;
	void* p = &a;	//void* 无具体类型指针,接收任意地址
	*p = 1;			//err void* 不能解引用操作,编译器不知道是什么类型
	*(int*)p = 1;	//right
	return 0;
}

当我们要使用的时候,强制类型转换即可

这样下来,我们的前3个参数就知道是什么干什么了的,我们来看最后一个参数

在这里插入图片描述
这个函数是我们自己写的函数,即需要自己写出来,返回值规定为int
>0是元素1大于元素2
=0是元素1等于元素2
<0是元素1小于元素2

为什么要我们自己写比较函数呢?
因为每个类型之间的比较不相同,开发者不能确定你要比较什么类型

  1. 比较整数 >=<
  2. 比较字符串 strcmp
  3. 比较结构体 指定类型比较

排序int的qsort

int cmp(const void* pa, const void* pb)
{
	return *(int*)pa - *(int*)pb;
}

int main()
{
	int arr[] = { 1,2,4,3,41,3,8,2,3,744,0 };
	int sz = sizeof(arr) / sizeof(*arr);
	qsort(arr, sz, sizeof(int), cmp);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

思考一下如果是排序2个字符串呢?我们来看一下strcmp的返回值
在这里插入图片描述
我们发现跟我们的自定义函数一样,于是我们可以直接写

int cmp_str(const void* pa, const void* pb)
{
	return strcmp(*(char*)pa, *(char*)pb);
}

如果我们比较一个字符数组里面的字符串呢

int cmp_str(const void* pa, const void* pb)
{
	return (*(char*)pa - *(char*)pb);
}

int main()
{
	char arr[] = "dwfazcff";
	int sz = strlen(arr);
	qsort(arr, sz, sizeof(char), cmp_str);
	printf(arr);
	return 0;
}

字符类型的本质是ascll值,直接来减即可

我们在写自定义比较函数的时候,要注意比较的是什么?
qsort默认是升序,如果要降序呢?

我们用我们熟悉的冒泡排序来模拟一下我们的qsort

int cmp(const void* pa, const void* pb)
{
	return *(int*)pa - *(int*)pb;
}

void swap(char* pa, char* pb,size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char a = *pa;
		*pa = *pb;
		*pb = a;
		pa++;
		pb++;
	}
}

void sort(void* base,
  size_t sz, 
 size_t width, 
 int (*cmp)(const void* pa, const void* pb))
{
	size_t i = 0;
	size_t j = 0;
	for (i = 0; i < sz-1; i++)
	{
		int flat = 1;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width,
			   (char*)base + (1+j) * width)>0)
			{
				flat = 0;
				swap((char*)base + j * width, 
				(char*)base + (1 + j) * width,width);
			}
		}
		if (flat)
		{
			break;
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值