C语言相关概念和易错语法(4)(指针)

1.指针类型与内存的关系

我们知道,计算机有64根地址线,通过各自不同的通电状态可以表示2的64次方个地址,每个地址对应一个字节的内存。这些地址信息的产生是在硬件层面实现的,因此不需要任何内存来存储这些地址信息。我们说的指针是将一些我们需要的地址信息提取出来存到变量里,方便我们后续快速找到这块内存。

指针有不同的类型,如char*和int*,但这些指针的本质是存储地址的变量,所以变量的大小相同,64位8字节,32位4字节。区别在于访问内存时一次性访问的大小不同,由指针对应的类型决定,int对应的指针是4字节,char对应的指针是1字节。

为什么int*存储的是1个地址,一次性访问4个字节,不是每个字节都有对应的地址吗?其实int*存储的是对应int变量的最低地址,int开辟了4个字节的空间,int*存储的的是int开辟的4个字节对应地址的最低地址(起始地址)。

可以试着用上面的知识解释下面的代码。

125d9b341bb44629aa4684fe1c4bbe88.png

使用指针必须初始化,否则变量存储的是随机值,属于野指针,不知道指向哪一块空间。如果实在不知道应该存储哪个元素的地址,就使用NULL初始化,NULL是空指针,本质上是0,0也是地址,但不能访问,否则报错。

这里顺便区分一下,null == NUL == '\0'和NULL有本质上的不同

 

2.const和指针之间的关系

(1)因为const修饰的字符串在内存中不会重复存储

如const char* p1 = "Hello,world!";

const char* p2 = "Hello,world!";

const char* p3 = "Hello,world!";

所以p1,p2,p3存储的地址都相同

(2)*与const位置不同造成结果的不同:

const int* p、int const * p:它们完全等价,相当于const修饰的是*p,使得存入的地址p无法通过解引用方式修改对应内存中存储的值

注意:使用char* p = "abc"相当于p是"abc"的数组名,但默认字符串不可修改,即*p会报错

int* const p:相当于修饰p,意思是p只能存入一个值,如果你写了int* const p = &a,则后续你写int* const p = &b就会报错

注意:在指针中*在不同的地方可以有不同的理解方式,如int* p应该理解为变量名为p,类型是int*,说明我们理解的时候是把int和*结合起来当作一个类型来考虑。而在这里const修饰指针(const int* p、int const * p)的理解中,我们把*与p结合起来理解(*p),让它表示解引用的意思,但const int*却又表示p的类型,应该把*和const int结合起来。这个情况会贯穿整个指针的学习,我们按照自己的理解方式去理解就好,指针的书写形式终究只是一种表达形式,明白指针背后计算机的运行逻辑最为重要,通过不同的方式理解主要是防止混淆。

3.指针-指针 运算

指针进行减运算,得到的不是两个十六进制的地址数字的差值,而是两个指针之间元素的个数,但注意的是,指针指的是对应元素最低地址,因此注意指针1-指针2中指针1对应的元素在范围之外

7796657a65874577956629dfbda4be5e.png

我们看到指针2指向元素1,指针1指向4,当指针1-指针2时,中间的差值是3,包括指针2指向的元素但不包括指针1指向的元素

通过指针之间的减法,我们可以知道指针也是可以比较大小的(地址大小),对应地址高的则大,反之则小

前提条件:指针之间的元素都是经过自己初始化了的,即两个指针指向同一块空间

注意:指针减法也可能为负数,低地址减去高地址,大小就是中间的元素个数,但前面要加负号

4.一些容易遗忘的初始化知识点

(1)全局变量、静态变量不初始化默认为0,而局部变量默认为随机值。随机值是由编译器决定的

(2)数组不完全初始化的使用过程中,会把没被初始化的内容默认初始化0,所以当我们写出 int arr[5] = { 0 }这样的代码时,相当于利用不完全初始化的规则来实现完全初始化的效果。

5.断言assert的使用

在包含头文件<assert.h>之后,使用assert(表达式)检查,如果表达式结果为真就不执行,为假执行,直接中断程序并报错。使用#define NDEBUG禁用

这要和我们平时使用的循环和分支语句的判断做区分,循环和分支语句的判断是为真执行,为假不执行,但也可以使用逻辑反操作!来调整,具体要看使用需求

一般来说,release会对代码做出优化,有的在debug起效的在release不会起效,assert就是这样。

但assert在Linux的release下也能起作用。

6.数组和指针的关系

(1)数组的本质的理解

数组本质上就是多个连续存放的数据(元素之间没有任何多余空间,这些元素紧密相连),可以通过指针对它们进行修改、查找等一系列操作。理解层面上,你完全可以把数组想象成一种存储数据的表达形式,这样就有下面的变换:arr[n] ==  *(arr + n) == *(n + arr) == n[arr]

同样,对于常量字符串"Hello,world"用char* p = "Hello,world"来存储时,存储的是'H'的地址,也就是字符串的首元素地址,我们不仅可以用p来访问数组,还可以把"Hello,world"整体当作数组名(首元素地址)来访问,于是下面等式成立:

p[n] == *(p + n) == *(n + p) == n[p] == "Hello,world"[n] == *("Hello,world" + n) == *(n + "Hello,world") == n["Hello,world"]。

这部分内容第一次接触会很难接受,但数组的本质就是这样,arr[]不过是一种表达形式,并没有想象中那么特殊,在编译器编译过程中,数组也都会被当作数据加指针的形式进行访问和操作的。

(2)二维数组的储存方式

二维数组本质上是以一维数组为元素的数组,所以二维数组传参传的就是一维数组的地址(首元素地址),创建 int arr[3][4]就用int(*p)[4](数组指针)来接收,p每加1就跳一行4个元素,如果要读取某一行的具体的某个元素,就需要先对p解引用得到一行元素的首元素地址,再解引用得到单个元素。

为什么对数组指针p再解引用就得到单个元素的地址呢?我们知道对于数组arr来说,&arr就是取出整个数组的地址,要用数组指针来接收;而arr只是首元素地址,用整型指针(或字符指针等)来接收即可。所以有如下等式成立:

*(&arr) == arr;

我先取出arr整个数组的地址,再对它解引用,&和*互为逆过程,所以最后会得到arr,arr就是首元素地址。对于这里来说也是这样,int(*p)[4]中的p相当于 p = &arr;按照上面的等式,*p == *(&arr) == arr;所以对数组指针再进行解引用得到的就是对应数组首元素的地址了。(这里有点绕,需要好好消化)

下面用代码来举例


#include <stdio.h>

int main()
{


	int arr[3][4] = { {1,2,3,4} ,{0,2,4,6},{1,3,5,7} };

	int(*p)[4] = arr;

	printf("%d ", **p);
	printf("%d ", *((*p) + 1));
	printf("%d ", *((*p) + 2));
	printf("%d ", *((*p) + 3));

	printf("\n");

	printf("%d ", **(p + 1));
	printf("%d ", *(*(p + 1) + 1));
	printf("%d ", *(*(p + 1) + 2));
	printf("%d ", *(*(p + 1) + 3));

	printf("\n");

	printf("%d ", **(p + 2));
	printf("%d ", *(*(p + 2) + 1));
	printf("%d ", *(*(p + 2) + 2));
	printf("%d ", *(*(p + 2) + 3));

	printf("\n");


	return 0;
}


运行结果是

66c4ecd938484206833d92fd07fb0010.png

7.最大公约数和最小公倍数的实现方式

(1)最大公约数

辗转相除法是求最大公约数的一个很稳定,不用动脑子的办法,很适合交给计算机去做。

描述语言来讲:随便在两个数中挑个作为被除数,另一个作为除数,被除数除以除数得到被整除的值和一个余数。忽略那个整除值,让原本的除数作为被除数,余数作为除数,在进行除法运算,以此类推。总有一个时候,两数相除的余数是0,此时那个除数就是最大公约数。

例如126和9的最大公约数的求法:

9作为被除数,126作除数,两数相除得余数9;

126作被除数,9作为除数,相除得余数0;

那么这个除数9就是它们的最大公约数。

在这个例子中我们发现,最开始的除数和被除数没有要求,就算我让较小的数作被除数,在一次运算后会自动调整过来的。

(2)最小公倍数

两数相乘再除以最大公约数就是最小公倍数了。掌握前者这个就很简单了。

8.sizeof和strlen的使用区别

(1)sizeof

我们要注意,sizeof上是一种C语言自带的关键字和操作符,计算的是对应表达式的大小而不去关心这个表达式内部到底是什么。所以sizeof(int)可以直接计算int类型的大小,就算你没有为它创建变量。但是注意,当计算类型大小或写了一个表达式时,不能写成sizeof int或sizeof a=b+c,这样违反语法,除此之外你都可以省略括号,如sizeof arr。(arr是数组)

sizeof内部表达式不执行,所以sizeof( a = b + 1 )本质上是不会改变a的值的,包括a++,--b这种表达式都不会执行。在这种情况下,a = b + c并不是赋值运算。b和c是int类型,b+c也是int类型,int数据放到short里面发生截断,最终表达式是short类型,起决定性的是等号左边的变量的类型,sizeof计算的就是short的大小。

effe427082bb46b1a7cc242517e6080a.png

下面还有一组代码可以帮我们加深印象

bd787f98212e411884c7195321215d0b.png

(2)strlen

和sizeof相比,strlen是专门用来求字符串的长度的库函数,传入的参数是一个指针,由该地址对应的字符为起始点向后统计直到\0为止,这里注意一定要传指针,strlen(a),strlen(1)会直接报错。当然,传指针可以不用关心传过去的指针类型,只要是地址,传过去都会被转换为char*,因为strlen设计时的形参就是char*。

01400c232f994d25bdfb213ca1f1f55b.png

这里三种类型的指针只是决定了指针+1跳过多少字节,它们存储的地址是同一块,因此strlen处理它们的时候没有任何区别。

9.const修饰一个类型后代表一个新的类型

我们知道const可以修饰int,char,int*等等,分别代表不同含义,修饰之后const int是一种新的类型,和int不一样,所以调用用const修饰的变量时,可以考虑使用强制类型转换将const int转为int之后再使用,const int*,const char等都是如此。

10.针对二维数组相关指针含义的理解

建立在二维数组实质上是数据加指针的理解上,我们应该集中去区分一些表达式的含义:

先看一段代码,尝试去解释它们


#include <stdio.h>

int main()
{
	int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };

	printf("%d ", **arr);
	printf("%d\n", arr[0][0]);

	printf("%d ", *(*arr + 1));
	printf("%d\n", arr[0][1]);

	printf("%d ", *(*(arr + 0) + 2));
	printf("%d\n", arr[0][2]);

	printf("%d ", *(arr[0] + 3));
	printf("%d\n", arr[0][3]);

	printf("%d ", (*(arr + 1))[0]);
	printf("%d\n", arr[1][0]);

	printf("%d ", (*(&arr[2] - 1))[1]);
	printf("%d\n", arr[1][1]);

	printf("%d ", *(*(&((&arr[0])[2]) - 1) + 3));
	printf("%d\n", arr[1][3]);

	printf("%d ", *(*(&*(arr + 1) + 1) + 1));
	printf("%d\n", arr[2][1]);

	return 0;
}

结果发现相邻表达式确实是等价的

e70b33fe76454cea84ed1fd646254233.png

对于二维数组,我们理解为将指针划为不同等级,它们对数组有不同的访问权(一次性访问大小)。其中数组指针权能较高,它指向整个数组首元素地址,+1会跳过整个数组;整型指针权能较低,+1会跳过1个int类型的大小,所以我们利用数组指针用来确定行,整型指针用来确定某一行的某一个元素。

由这篇文章的第6个知识点,我们可以通过*和&对数组类型进行转换,使他们能一次性访问不同的大小。

[  ]本质上就是一次解引用操作,如arr[1] == *(arr+1)

这部分内容有点绕,这里我只是分享了一下我的理解,如果有更好的理解方式,欢迎分享!

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 36
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值