当前位置:网络安全 > 【深入理解C语言指针(一)】

【深入理解C语言指针(一)】

  • 发布:2023-10-01 16:36

1.内存和地址

1.1内存

在讲内存和地址之前,我们先来看一个现实生活中的案例:
假设有一栋宿舍楼,你被安排在楼上。楼上有100个房间,但房间没有编号。你的一个朋友来和你一起玩。想要找到你,就得去各个房间。这是非常低效的。但是,如果我们按照楼层和该楼层的房间对每个房间进行编号,如:

? :201、202、203…
3…

有了房间号,如果你的朋友知道房间号,他们可以快速找到房间并找到你。

如果我们将上面的例子与计算进行比较呢?
我们知道,CPU(中央处理单元)在计算中处理数据时,会从内存中读取所需的数据,处理后的数据也会放回到内存中。当我们购买电脑时,电脑内存有8GB/16GB/32GB等,如何高效管理这些内存空间呢?
其实内存是分为内存单元的,每个内存单元的大小是1字节。
计算机中常用单位(补充):
一位可以存储1或0的二进制位

1 位 -
2 字节 - 字节
3 KB
4 MB
5 GB 6 TB
7 PB

1 1字节 = 8
2 1KB = 1024字节
3 1MB = 1024KB
4 1GB = 1024MB
5 1TB = 1024GB
6 1PB = 1024TB

其中,每个内存单元相当于一个学生宿舍。一个人类字节空间可以容纳 8 位,就像学生
居住的八个房间一样。人就是有点。每个存储单元还有一个编号(这个编号相当于宿舍房间的门号)。有了这个内存单元的数量,CPU就可以快速找到内存空间。

在生活中,我们也将门牌号称为地址。在计算机中,我们也称内存单元号为地址
在C语言中,地址有一个新名称:Pointer。所以我们可以理解为:
内存单元的编号==地址==指针

1.2 如何理解寻址

当CPU访问内存中的某个字节空间时,它必须知道这个字节空间在内存中的什么位置,而由于内存中的字节
很多,所以需要对内存进行寻址(就像有宿舍很多),需要给宿舍号一样)。

计算机中的寻址并不是记录每个字节的地址,而是通过硬件设计来完成的。

钢琴吉他上面没有写“杜里米在晃动”之类的信息,但演奏者仍然可以准确地找到每根弦的每个位置
。为什么是这样?因为厂家已经在乐器硬件层面设计好了,所有玩家都知道
。本质是达成共识!

硬件寻址也是如此

首先你要明白,计算机中有很多硬件单元,而且硬件单元必须协同工作。所谓协作
,至少需要彼此之间进行数据传输。但硬件与硬件是相互独立的,那么如何通信呢?答案很简单,用“线”将它们连接起来。 CPU和内存之间也有大量的数据交互,因此两者必须用线路连接。然而,今天我们关心的是一组名为 Address Bus 的线路。

我们可以简单理解为32位机有32条地址总线,每条线只有两种状态,表示0,1【电脉冲有无】,那么
一根线可以代表2种类型。也就是说,2条线可以代表4种含义,以此类推。 32条地址线可以代表2^32个含义,每个含义代表一个地址。地址信息被发送到存储器。在内存中可以找到该地址对应的数据,并通过数据总线将数据传送到CPU内部寄存器。

2。指针变量和地址

2.1 地址运算符 (&)

了解了内存和地址的关系,我们再回到C语言。 C语言中创建变量实际上就是在内存中申请空间,如:

#包括 
int main()
{int一个=10;返回  0;
}

例如上面的代码创建了一个整型变量a,并在内存中分配了4个字节来存储整数10,每个字节都有一个地址。上图中的4个字节分别是地址为:

0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73

那么我们怎样才能得到a的地址呢?
这里你要学一个运算符(&)——地址运算符

#包括 
int main()
{int a = 10;& a;//获取a的地址 printf ("%p\n",&a)返回 0;
}

按照我画的例子,会打印处理:006​​FFD70&a取出a
所占的4个字节中地址较小的字节的地址。

虽然整型变量占用4个字节,但是只要我们知道首字节地址,按照指令访问4个字节的数据是可行的。

2.2 指针变量解引用运算符 (*)

2.2.1 指针变量

那么我们通过地址运算符(&)得到的地址就是一个数值,如:0x006FFD70。有时候这个值也需要存储起来
方便后面使用,那么我们把这样的地址值存储在哪里呢?答案是:指针变量

⽐例如:

#包括 
int main()
{int a = 10;int *pa=& a ;//获取a的地址,存入指针变量pa return 0;
}

指针变量也是变量的一种。这种类型的变量用于存储地址。存储在指针变量中的值将被理解为地址。

2.2.2 如何拆解指针类型

我们看到pa的类型是int*。我们应该如何理解指针的类型呢?

int a = 10;
int*pa=& a;

这里pa左边写的是int**是在说明pa是一个指针变量,前面的int表示pa指向的是一个integer类型的对象。

那么如果有一个char类型变量ch,那么ch的地址应该放在什么类型的指针变量中呢?

charch='w';
pc = &ch;//pc的类型怎么写? 

2.2.3 取消引用运算符

我们保存了地址,以后会用到,那么怎么用呢?
在现实生活中,我们使用地址来查找可以存放或存放物品的房间。
其实C语言也是一样的。只要我们得到了地址(指针),我们就可以利用地址(指针)找到该地址(指针)所指向的对象。这里我们必须学习一个叫做解引用的操作符。操作员(*)。

#包括intmain() {int a = 100;int *pa=& a ;*pa = 0;返回  0;
}

上面代码的第7行使用了理解引用运算符。 *pa表示通过pa中存储的地址找到指向的空间,*pa其实就是a变量;所以*pa = 0,这个算子把a变成了0。
肯定有同学在想,如果这里的目的是把a变成0的话,写成a = 0 ;完了,为什么不用指针呢?其实a的修改就交给pa了。这样修改a就多了一种方式。写代码会更灵活,以后就能看懂了。

2.3 指针变量的大小

我们从前面的内容了解到,32位机假设有32条地址总线。每条地址线发出的电信号转换成数字信号,是1或0。然后我们取32条地址线产生的2。将基序列视为一个地址,那么一个地址是32位,需要4个字节来存储。
如果使用指针变量来存储地址,那么指针的大小必须是4字节的空间。
同样对于64位机器,假设有64条地址线,一个地址就是由64个二进制数组成的二进制序列。需要8个字节的空间来存储。指针的大小为8字节。

#包括 
//指针变量的大小取决于地址的大小
//32位平台上的地址为32位(即4个字节)
//64位平台上的地址为64位(即8字节)
int main(){printf("%zd\n", sizeof (* ));printf("%zd\n", 尺寸*));printf("%zd\n", 尺寸(int)  *));printf( "%zd\n",尺寸 (*));返回  0;
}

结论:

• 32位平台下地址为32个位,指针变量大小为4个字节
• 64位平台下地址为64个位,指针变量大小为8个字节
• 注意指针变量的大小和类型是相关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

3。指针指标类型的意义

铲斗指针的最大⼩和类型⽆关系,只要是铲斗指针,在同一个平台下,铲斗指针都是同类的,为什么还要有各种的铲斗类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习。

3.1卸载器的解引用

对⽐,下面2段代码,主要在调试时观察内存的变化。

//代码1
#包括 int main()
{intn=0x11223344;int *pi = &n; *pi = 0;返回0;
}
//代码2
#包括 
int main()
{intn=0x11223344;char *pc = (char*)&n;*pc = 0;返回0;
}

调试我们可以看到,代码1第n的4个字节全部改为0,但是代码2只是将n的第1字节改为0。
结论:指针解引的类型决定了,对指针解引用途的时候有极大的权限(一次能操作⼏个字节)。
⽐如: char* 的指针解引用途就只能访问 1 个字节,⽽ int* 的指针的解引用途可以访问 4 个字节。

3.2夹具±整数

先看一段代码,调试观察地址的变化。

#包括 
int main()
{
int n = 10;char *pc = ( char*)&n; 
int *pi = & n;printf("%p\n ",&n);
printf("%p\n",pc); 
printf("%p\n",pc+1);
printf("%p\n", pi); 
printf("%p\n", pi+1);
返回 0;
}

代码运的结果如下:

我们可以看出,char*类型的指针指针+1跳过了1个字节,int*类型的指针指针+1跳过了4个字节
这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针前方或者⾛一步距离有多远。

4。 const 修饰指针

4.1 const 修饰指针

变量是可以的,如果把变量的地址移动一个指针变量修改,通过指针变量的也可以修改这个变量。
但是如果我们希望一个变量加上一个指针变量,不能被修改,怎么做呢?这就是const的用途。

 #包括intmain(){int= 0;m = 20;//m可修改const int n =  0;n = 20;//n 不可修改返回0;
}

上述代码中的n不可修改。其实n本质上是一个变量,只不过被const修饰后,有语法上的限制。我们只要修改代码中的n,就不符合语法规则了。报错,导致无法直接修改n。

但是如果我们绕过n,使用n的地址,并修改n,我们就可以做到,尽管这样做违反了语法规则。

#包括 
int main()
{const int n =  ;printf("n = %d\n" , n);int*p  =&n;* p = 20;printf("n = %d\n ",n); 返回 0;
}

输出结果:

我们可以看到这个确实被修改了,但是我们还是要想一想,为什么n被const修饰呢?这是为了防止它被修改。如果p得到n的地址,它就可以修改n。这就打破了const的限制。这是不合理的。因此,即使 p 获得了 n 的地址,也不应该能够修改 n。那么接下来会发生什么呢?该怎么办?

4.2 const 修饰指针变为

我们看下面的代码来分析一下

#包括 
//代码1
空白测试1
{int n = 10;int 米=20;int *p = &n; *p=20; // 可以吗?p = &m; //可以吗?
}
空白test2
{//代码2int n = 10;int=20;const int* p =  &n;*p =  20;//可以吗?p = &  m; //可以吗?
}
voidtest3(){int n = 10;int 米=20;int*const p = &n ;*p = 20; //可以吗?p = &m ; //可以吗?
}
voidtest4()
{int n = 10;int 米=20;intconst * const p =  &n;*p = 20; //可以吗?p = &m;//还好吗
}
int main()
{//测试⽆const修饰的情况test1();//测试const放在*的左边情况test2();//测试const放在*的右边情况test3() ;//测试*的左右肩膀都有consttest4();返回0;
}

结论:const修饰指针指针的时机

• 如果const 放在 的左侧,它会修改指针指向的内容,确保指针所指向的内容不能被指针改变。但指针变量本身的内容是可变的。
• 如果const 放在
的右侧,它会修改指针变量本身,保证指针变量的内容不能被修改,但可以通过指针改变指针指向的内容。

5。指针运算

有三个基本的指针操作,即:
•指针±整数
•指针销钉
•指针关系操作

5.1指针±整数

因为数组在内存中是连续存储的,所以只要知道第一个元素的地址,就可以顺着线索找到后面的所有元素。

intarr[10]= {1,2,3,4,5 , 6,7,8,9,10};

#include//指针+-整数intmain(){int arr[10]={1,2,3,4,5,6,7 ,8,9,10};int*p = &arr [0];int=0; int sz = 尺寸 (arr)/尺寸(arr[] ]);(i=0;i<sz;++{printf("%d",*(p+ i));//p+i⾥这就是指针+整数}return 0;}

5.2指针-指针

//指针-指针
#包括 
intmy_strlen(char* s)
{char *p = s ;同时(*p  !='\0')p++;返回 p-s;}
int main()
{printf("%d\n",my_strlen ("abc")) ;返回0;
}

5.3 指针的关系运算

//指针的关系运算
#包括 
int main()
{intarr[10]=  {1,2,3,4 , 5,6,7,8,9,10};int  *p =&arr[0];int i  = 0;int sz  = 尺寸(arr)/ 尺寸(arr[0]);同时(p<arr+sz) //指针大小比较{printf("%d",*p );p+ +;}返回0;
}

6。野指针

概念:野指针意味着指针所指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 野指针产生的原因

1。指针未初始化

#包括 
int main()
{ int *p; //局部变量指针未初始化,默认为随机值*p=20;返回0; 
}

2。指针越界访问

#包括 
int main()
{intarr[10]=  {0};int *p =&arr[] ];int= 0;(i=0; i  <=11; i++) {//当指针指向的范围超过数组arr的范围时,p为野指针*(p++)  )=i;}返回 ;
}

3。释放指针所指的空间

#包括 
int* 测试()
{int n = 100;返回 &n;
}
int main(){int*p =测试 ();printf( "%d\n",*p);是转0;
}

6.2 如何避免野指针

6.2.1 指针初始化

如果你确切知道指针指向哪里,直接赋值即可。如果你不知道指针应该指向哪里,可以将 NULL 赋给指针。
NULL是C语言符号常量中定义的一个标识符,值为0,0也是一个地址,这个地址不能使用,读写这个地址都会报错。

 #ifdef__cplusplus#defineNULL0#其他#定义 NULL ((void) *)0)#endif

初始化如下:

#包括 
int main()
{intnum =10;int *p1 = & num ;int*p2 = NULL ;返回0;
}

6.2.2 指针越过边界时要小心

⼼程序在内存中申请的空间只能通过指针来访问。超出范围就无法访问。如果超过了,就是越界访问。

6.2.3 当不再使用指针变量时,立即将其设置为NULL,并在使用指针前检查有效性

当指针变量指向一个区域时,我们可以通过指针访问该区域。当我们以后不再使用这个指针访问该空间时,我们可以将指针设置为NULL。因为一个常见的规则是:只要指针为NULL,就不会被访问。同时可以在使用前判断该指针是否为NULL。

我们可以将野指针想象成野狗。单独留野狗是很危险的,所以我们可以找一棵树把野狗拴起来,这样会比较安全。其实我们可以及时给指针变量赋值NULL。这就像把野狗拴起来,暂时管理野指针一样。

可是,即使野狗被拴住了,我们也得绕着它走。我们不能戏弄野狗,这有点危险。指针也是如此。在使用它们之前,我们还必须检查它们是否为NULL,看看它们是否被捆绑在一起。如果野狗不能直接使用的话,我们就使用它。

int main(){intarr[10]=  {1,2,3,4 , 5,67,7,8,9,10};int  *p =&arr[0];对于(i=0;i <10;i++){*( p++)=  i;}//此时p已经越界,可以将p设置为NULLp = NULL;/ /下次使用时使用//...p = & arr[0] ;//让p再次获取地址if(p !=  NULL) //判断{//...}返回 0;
}

6.2.4 避免返回局部变量的地址

如果造成第三个野指针的例子。

7。断言

assert.h头文件定义了宏assert(),用于保证程序在运行时满足指定的条件。如果不满足要求,会报
错误。祝你好运。该宏通常称为 “断言”

断言(p !=NULL) ;

当程序运行到这行语句时,上面的代码验证变量 p 是否等于NULL。如果确实不等于NULL,则程序
将继续运行,否则将终止并给出错误信息。
assert() 该宏接受表达式作为参数。如果表达式为 true(返回值非零),assert() 将不会产生
效果,程序继续运行。如果表达式为 false(返回值为 0),assert() 会报错,将错误信息写入标准错误
stderr,并显示 ◆未通过的表达式,以及包含该表达式的文件名和行号。
assert()的使用对于程序员来说非常友好。使用assert()有几个好处:不仅可以自动识别文件和
有问题的行号,还有一个机制可以打开或关闭assert(),无需更改代码。如果已经确认程序没有问题,不需要做任何进一步的断言,只需在#include 语句前定义一个宏NDEBUG即可。

#定义NDEBUG
#包括 

然后,重新编译程序,编译器将禁用文件中所有assert() 语句。如果程序有问题,可以去掉这条#define NDBUG指令(或者注释掉),重新编译,重新启用assert()
句子。
assert()的缺点是,由于引入了额外的检查,增加了程序的运行时间。
一般我们可以在debug时使用,在release版本中选择禁用assert。在VS这样的集成开发环境中,在release版本中,直接进行了优化。这样,在debug版本中编写会帮助程序员排查问题,而在release版本中编写则不会影响用户使用程序时的效率。

8。使用指针和地址调用

8.1 按地址呼叫

学习指针的目的就是用指针解决问题,那么什么问题不能用指针解决呢?
例如:写一个函数来交换两个整型变量的值
经过一番思考,我们可能会写出这样的代码:

#包括 
void 交换1(int x, int y)
{int tmp = x;x = y;y =tmp;
}intmain()
{int a = 0;int  b = 0;scanf ( "%d %d",&a, &b); printf("交换前:a=%d b=%d\n", a, b);交换1 (a, b);printf  ("兑换后:a=%d b=%d\ n",a,b);返回 0;
}
?布Zw==/}

我们发现没有交换效应。为什么是这样?
我们调试一下试试?

我们发现main函数内部创建了a和b。 a的地址是0x00cffdd0,b的地址是0x00cffdc4。当调用
Swap1 函数时,a 和 b 被传递给 Swap1 函数。在 Swap1 函数内部创建了形参 x 和 y,用于接收 a 和 b 的值,但是 x 的地址是 0x00cffcec,y 的地址是 0x00cffcf0。同样,y的地址与b的地址不同,相当于x和y是独立的空间。那么在Swap1函数内交换x和y的值自然不会影响a和b。当 Swap1 函数调用完毕后,返回主函数。 A和b不能互换。当使用 Swap1 函数时,变量本身直接传递给函数。我们之前已经知道这种调用函数的方法。这称为 按值调用

结论:当实参传递给形参时,形参会创建一个单独的临时空间来接收实参,对形参的修改不会影响实参。所以Swap失败了。

我们该怎么办?
我们现在要解决的是,当调用Swap函数时,Swap函数内部操作的是主函数中的a和b,直接交换a和b的值。然后就可以使用指针了。在main函数中,将a和b的地址传递给Swap函数。 Swap函数通过地址间接操作main函数中的a和b。

#包括 
void 交换2(int*px,int*py) {inttmp =0;tmp = *px;* px = *py;* py = tmp;
}
int main()
{int a = 0;int  b = 0;scanf ( "%d %d",&a, &b); printf("交换前:a=%d b=%d\n", a, b);交换1 (&a,&b );printf("交换后:a=%d b=%d\n", a, b);返回0;
}

先看输出结果:

我们可以看到Swap2的执行已经顺利完成了任务。这里调用Swap2函数时,变量的地址被传递给函数。这种调用函数的方法称为:Pass 地址称为

8.2 strlen的模拟实现

//计数器模式intmy_strlen(constchar*str
{int计数=0;断言 (str);同时 (*str){计数++; str++;}返回计数;
}
int main()
{int长度=my_strlen("abcdef");printf("%d\n",长度);返回0;
}

相关文章