超详细的c进阶教程!手撕c指针

Posted 东条希尔薇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详细的c进阶教程!手撕c指针相关的知识,希望对你有一定的参考价值。

作者的码云地址:https://gitee.com/dongtiao-xiewei
后续作者会更新力扣的每日一题系列,原代码会全部上传码云,推荐关注哦,笔芯~

还像更深入地了解c语言?快来订阅作者的c语言进阶专栏!作者承诺本系列不会TJ!预计更新:指针,字符串处理,内存管理,结构体,预处理等等

C指针我相信,对于许多正在新手村发育的程序员来说,都是一道过不去的坎(包括我新手期呜呜呜TAT)。为了解决广大新手玩家在指针关卡卡关的问题,特地推出此攻略~

废话不多说,让我们开始吧!

指针快速入门

指针的概念

到底什么是指针?以下是度娘给出的定义:

指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

指针在c语言如此流行,也是因为指针可以有效的实现例如树,链表等数据结构。

听了度娘这么说,感觉还是一头雾水啊,说了个寂寞啊。。

别急,接下来我用生活中的例子给大家引出指针这一概念吧

我们可以把计算机内存看做一条街道上的一排房子,就像这样,每个房子都有自己的编号,如下图


每个房子都有且仅有一个门牌号与它对应,每个房子都可以容纳一口人家

对应到计算机科学中就是:

计算机中创建的每个变量(人家)都会在内存中占据一定的位置,每个内存位置都由一个特定的地址(门牌号)唯一确定

指针它就是一种能存储地址的变量。我们可以通过一些操作,利用指针找到某一个变量,去间接的操作它。

对应到计算机中就是这样


为什么一个格子对应一个字节呢?

指针大小

我们知道,我们使用的操作系统有32位和64位之分(在此电脑上点击右键后再点击属性查看)


操作系统位数的意义是:电脑可以使用多少根地址线

我们拿32位操作系统来举例:

32的机器对应32根地址线,假设每根地址线可以产生一个电信号(正电/负电),对应二进制中用1/0表示

32位就可以产生如下的电信号
注:一根地址线对应一个比特位,每8个比特位对应1个字节

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
....
11111111 11111111 11111111 11111111

我们简单的算一下:每根地址线可以产生2个电信号,32个地址就可以产生232个不同的电信号。

如果我们把每个字节表示成一个地址的话,可以表示232字节内存,按一个单位1024的比例扩大,算出可以操控4GB的空闲空间进行编址

问题来了,那为什么不拿其它单位来标识一个地址呢?

就拿1kb来举例,我们知道一个int变量大小仅有4b,如果我们拿1kb大小来标识一个地址,将会造成大量空间的浪费(本例浪费1020b左右)

按一个字节1b的大小来计算:

  • 32位操作系统下的指针变量大小为4b
  • 64位操作系统下的指针变量大小为8b
    可以简单写个程序来验证一下
int main()
{
	printf("%d\\n", sizeof(int*));//int*的指针变量
	printf("%d\\n", sizeof(char*));//可以用不同类型的指针变量来验证,是不是指针变量大小是固定的
	return 0;
}

VS2019设置系统的方法如下图


这里的x86代表我们使用的是32位平台

输出结果如下


64位平台上输出如下

指针操作符类型

可能又有好奇的小伙伴会问了,既然不同指针类型大小都一样,那为啥还要用这么多的指针类型啊?

哦对了,我们还是先介绍一下指针该怎么用吧~

指针的操作主要靠两个运算符:*—— 解引用操作符和 & ——取地址操作符

比如以下的代码

int a=10;
int* p=&a;
*p=20;
printf("%d\\n",a);

注:定义指针变量int p这里的星号表明p是一个指针变量,而不是解引用的意思*

我们通过画图解释这两个操作符

所以最后的输出结果就应该是20


当然,这里我们把int类型的变量存放在了int指针中,那可不可以把int a放在char* 类型的指针变量中呢?

int main()
{
	int a = 10;
	char* p = &a;
	return 0;
}

程序确实能编过去,但是报了一个警告

虽然这个程序可以编过去,不过我们最好还是把指针存放在相同类型的变量中,不然就可能导致一些意外:

比如还是本节第一个程序,只不过我们换成char*类型的变量,并将a的值修改到足够大

int a=0x11223344;
char* p=&a;
*p=0x55667788;
printf("%x\\n",a);

输出如下图

很显然,输出的值不是我们想要的值,为什么会造成这种错误呢

让我们调试一下,看看内存情况。

所以我们可以得出结论:

指针类型决定了向前或向后走一步或者操作的权限有多大

指针的运算

这一节全是概念性质的知识点,只需要记住就行了

指针±整数

我们通过打印数组的例子来说明这一概念

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

这个程序可以打印出每一个元素

所以,指针加减整数,可以表示跳过的元素个数

指针-指针

int main()
{
	int arr[10] = { 0 };
	int* parr1 = (arr + 9);
	int* parr2 = &arr[0];
	printf("%d\\n", parr2 - parr1);
	return 0;
}

输出结果

结论:指针减去指针的值的绝对值是两个指针中间的元素个数

指针的比较

我们知道,其实指针都是有一个十六进制的数字表示的,也算作一个值,所以理所当然的可以比较大小

int main()
{
	int arr[10] = { 0 };
	int* parr1 = (arr + 9);
	int* parr2 = &arr[0];
	if (parr1 > parr2)
		printf("haha\\n");
	else
		printf("hehe\\n");
	return 0;
}

结论:数组中的元素随着下标从小到大,其地址也由低到高

指针和数组的联系

我们平时在学习c语言的时候或者我们在看一些c语言教材的时候,我们发现作者通常会把指针和数组放在一起谈,这是为啥呢?指针和数组到底有什么关系呢?

用一个代码说明

int main()
{
	int arr[10] = { 0 };
	int* p1 = &arr[0];
	int* p2 = arr;
	printf("%p\\n", p1);
	printf("%p\\n", p2);
	return 0;
}

输出结果如下

这两个表示形式的地址是一模一样的

所以可以得出以下结论:数组名代表的是首元素地址

但是有以下两种特殊情况

  1. sizeof(arr)(其实arr未参加任何运算),这里的arr代表整个数组
  2. &arr,这里也代表的是取出整个arr数组的地址

用代码举例吧

int main()
{
	int arr[10] = { 0 };
	printf("%p\\n", arr);
	printf("%p\\n", &arr + 1);//检测跳过了一整个元素还是一个元素
	printf("%u\\n", sizeof(arr));//比较是否与首元素地址大小一样
	printf("%u\\n", sizeof(&arr[0]));
	return 0;
}

输出结果如下

数组的传参

我们知道,数组元素名代表的是首元素地址,所以以下两种传参方式都是可行的。

void test(int* p)//1
void test(int p[])//2
int main()
{
	int arr[10];
	test(arr);
	return 0;
}

数组传参的时候会退化成首元素的地址传给函数体

所以我们可以使用一个指针接受arr

当然,利用第二种数组的方式接受也可以,第二种方式更容易使萌新理解,我们不用管方块里面该填什么,随便你怎么填,要么不填,要么填一个大于原数组的数字,都无所谓

但是,这样传参,我们在有些使用场景下可能会出现一些问题

比如,我们要利用函数打印一个数组

void print_arr(int* arr)
{
	int sz = sizeof(arr) / sizeof(arr[0]);//我们必须要知道一个数组有多少元素才能打印,这是通用的计算方法
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

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

但是,程序的输出只打印了一个元素

其实是因为函数传进去的是一个指针,指针大小是固定的4或者8个字节,所以在函数体内计算数组大小永远是4/类型大小,结果是1,这个结果显然是错误的

所以,我们必须在函数外先算好数组元素个数,把个数参数传进去,才能达到我们想要的结果

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

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


这个结果就是正确的

[]操作符与指针

以下两种程序的效果完全等价

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);//第一种
		printf("%d ", *(arr + i));//第二种
	}
}

第二种的解释:arr代表首元素的地址,首先进行加法运算跳过某一些元素,最后在对其解引用找到数组中某一个元素的数字

二级或更高级指针

二级指针可以存放比它低一级指针变量的地址

int main()
{
	int a = 10;
	int b = 20;
	int* pa = &a;
	int* ppa = &pa;
	*ppa = &b;
	return 0;
}

字符指针和字符串

我们知道,有一种指针类型叫char*类型,它可以存放字符类型的地址

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

但是我们知道,c语言不像python,它是没有字符串类型的

但是,我们可以使用字符指针来处理字符串

在数组阶段,我们通常是使用数组来保存字符串的

char arr[]="hello world!";

我们同样可以使用字符指针来保存字符串

char* p="hello world!";

这种保存方式,是把整个字符串地址传进来了吗?

其实不是,我们简单用一个程序验证一下

int main()
{
	char* p = "hello world!\\n";
	printf("%c\\n", *p);
	return 0;
}

输出结果如下

结果是h,所以我们可以初步得出结论

字符指针在储存字符串时,本质还是把首元素地址储存起来,而存放空间又是连续的,所以可以通过这个指针找到整个字符串

指针数组和数组指针

至于为什么把它们放在一起,是因为它们名字里面每个字都一样,但是意义却完全不一样~

前者是数组,后者是指针

指针数组

顾名思义,指针数组就是一个能存放地址的数组,表示方式如下

int* parr[10];

这个程序就代表,一个名为parr的数组,其中包含10个元素,每个元素都是int*指针类型

我们可以这样分析:

把名字去掉,剩下的是int*[10],就是它的类型了~

指针数组的初始化方式举例

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	int* parr[4] = { &a,&b,&c,&d };
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", *parr[i]);
	}
	return 0;
}

可以打印出我们想要的结果

内存图:

总结:初始化只需要传进地址就行了,使用的时候记得解引用操作

数组指针

结论:数组指针是一种指向数组的指针
表示方式:

int(*p2)[10];

注意:由于括号的优先级最高p2不会先与[]结合,而是先与*结合,说明其是一个指针变量,然后指向是一个含10个整型元素的数组

数组指针的应用:二维数组传参

我们知道,二维数组在内存的方式也是连续存放的,一行元素存完后,接着立刻存储下一行

int arr[3][3];

这个二维数组的内存情况如下


我们就可以把二维数组的每一行看做一个一维数组的元素

于是我们就可以这样理解二维数组:看做一个一维数组,每一行的元素都是另外一个一维数组

基于以上的推测,于是我们就可以使用数组指针来传参

void test(int(*p)[3])
{

}

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

p就被看作一个指针,该指针指向一个有3个整型元素的数组

解引用方式:

*(*(p+1)+2)//等价于arr[1][2]

函数指针

其实,我们自己写的函数也是会使用特定的地址保存起来的

那函数的地址能不能保存呢?答案是当然可以,用函数指针就行了

函数指针包含以下几个成分:

  1. 指向的函数的返回值
  2. 指向的函数的参数类型

以下有两种可能会混淆的定义方式

void(*pfun1)()=test1;
void*pfun2()=test2;

第一种方式是正确的,因为变量必须要保存先与*结合,说明其是一个指针

使用方式

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

int main()
{
	int(*pfun)(int, int) = Add;
	//(返回类型)int(指针变量名称)(*pfun)(参数类型)(int, int) = (指向的函数)Add;
	int ret = (*pfun)(2, 5);
	//也可以进行简化
	int ret1 = pfun(2, 6);
	printf("%d,%d\\n", ret, ret1);
	return 0;
}

函数指针数组

类比指针数组,就是把每个元素换成一个函数指针了

声明方式

void(*parr[10])(int,int);

函数指针应用(回调函数)

定义:就是通过函数指针调用的函数,这个函数的参数通常是一个函数指针

例子:计算器

函数定义部分:

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

float Sub(float x, float y)
{
	return x - y;
}

float Mul(float x, float y)
{
	return x * y;
}

float Div(float x, float y)
{
	if (y == 0)
	{
		printf("error\\n");
		return 0.0
	}
	else
	{
		return x / y;
	}
	
}

不适用函数指针的话,代码将会变得非常冗余,(输入操作数的部分)

int main()
{
	int input = 0;
	float x = 0.0;
	float y = 0.0;
	do
	{
		printf("1. Add      2.Sub\\t");
		printf("3. Mul      4.Div\\n");
		printf("你要做什么?\\n");
		scanf("%d"

以上是关于超详细的c进阶教程!手撕c指针的主要内容,如果未能解决你的问题,请参考以下文章

超详细的C进阶教程!C语言文件操作

超详细的c进阶教程!C语言数据存储剖析

超详细的C进阶教程!C语言预处理

超详细的C进阶教程!动态内存管理

超详细的C进阶教程!深度解剖C语言自定义类型

搞定C语言指针,指针超详细讲解,及指针面试题