内存访问模式
如果你想了解更多,你应该阅读计算机组成和编译原理。
Sizeof关键字
sizeof关键字由编译器用来计算基于字节的特定类型数据的长度。例如:
sizeof=1;sizeof=4;sizeof的值是编译时计算的,所以可以认为是常量!
指针是什么?
我们知道,C语言中的数组是指一种类型,具体分为int型数组、double型数组、char型数组等等。
同样,指针的概念一般是指一类数据类型,比如int指针类型、double指针类型、char指针类型等等。
一般我们用int类型存储一些整数数据,比如int num = 97,我们也用char存储字符:char ch = 'a '。
我们还必须知道,任何程序数据加载到内存后,内存中都有它们的地址,也就是指针。
为了在内存中存储数据的地址,我们需要一个指针变量。
所以指针是程序数据在内存中的地址,指针变量是用来存储这些地址的变量。
以我个人的理解,指针可以理解为int integer,只不过它存储的数据是内存地址,不是普通数据。我们通过这个地址值访问数据,假设它是P,这意味着数据存储位置是内存的第P个字节。
当然不能像int类型的数据那样做各种加减乘除运算,这是编译器不允许的,因为这样的错误是很危险的!
图2是指针的描述。指针的值是数据存储地址。因此,我们说指针指向数据存储位置。
指针长度
我们用这种方式来定义指针:
Type*p;我们说p是指向type的指针,type可以是任意类型。除了char,short,int,long等基本类型外,还可以是指针类型,比如int *,int **,或者更高级别的指针,或者结构,类,函数。所以,我们说:
Int *是指向Int类型的指针;
Int **,即 *,是指向int *类型的指针,即指向指针的指针;
Int ***,也就是 *,是指向Int **类型的指针,也就是指向指针的指针;
...我想你应该明白
Struct xxx *是指向Struct xxx类型的指针;
其实说这么多,只是希望看到指针的时候不要被int ***,之类的东西吓到。前面说过,指针是指向某个类型的指针。我们只看最后一个*号,前面那个正好是类型。
细心的人应该会发现,在“什么是指针”一节中,已经指出指针的长度等于CPU位数,大多数CPU都是32位,所以我们说指针的长度是32位,也就是4字节!注意:任何指针的长度都是4字节,不管是什么指针!
所以:
Type*p;izeof的值为4,类型可以是任何类型、字符、整数、长整型、结构、类、整数* *...
以后看到任何sizeof、sizeof、sizeof都不要在意。写全4。只要是指针,长度就是4字节。千万不要被类型迷惑!
为什么程序中的数据有自己的地址?
要理解这个问题,需要从操作系统的角度去认识内存。
电脑修理工眼中的内存是这样的:内存在物理上是由一组DRAM芯片组成的。
换句话说,内存是一个大的线性字节数组。每个字节都有固定的大小,由8个二进制位组成。
最重要的是每个字节都有一个唯一的数字,从0开始,到最后一个字节结束。
如上图,这是一个256-m的内存,总共256x1024x1024 = 268435456字节,所以它的地址范围是0 ~268435455。
因为内存中的每个字节都有一个唯一的数字。
所以程序中使用的变量、常数、偶数函数等数据在加载到内存中时都有自己唯一的编号,这个编号就是这个数据的地址。
指针就是这样形成的。
下面用代码解释
#include <stdio.h> intmain { char ch = 'a'; intnum = 97; printf; //ch的地址:0028FF47 printf; //num的地址:0028FF40 return0; }变量和内存
为简单起见,上面例子中的局部变量int num = 97用来分析内存中变量的存储模型。
已知num的类型是int,占用4字节内存空,其值为97,地址为0028FF40。我们从以下几个方面来分析。
1.内存数据
内存中的数据是与变量值对应的二进制,一切都是二进制的。
97的二进制是:00000000000000000000000000000110000。然而,当使用小端模式进行存储时,较低的数据存储在较低的地址,因此绘图是相反的。
2.内存数据的类型
内存的数据类型决定了该数据占用的字节数以及计算机将如何解释这些字节。
Num属于int类型,因此它将被解释为整数。
3.内存数据的名称
内存的名称就是变量名。内存数据本质上是用地址来标识的,不存在内存的名字这种东西。这只是高级语言提供的一种抽象机制,方便我们对内存数据进行操作。
而且在C语言中,并不是所有的内存数据都有名字,比如malloc申请的堆内存就没有。
4.内存数据地址
如果一个类型占用的字节数大于1,则其变量的地址就是地址值最小的字节的地址。
所以num的地址是0028FF40。存储器的地址用于识别该存储块。
5.内存数据的生命周期
Num是主函数中的局部变量,所以主函数启动时分配给栈内存,主执行结束时死亡。
如果一段数据一直占用他的内存,那么我们说它是“活的”,如果它占用的内存被回收,数据就会“消亡”。
C语言的程序数据会根据其定义的位置、数据类型、修改的关键字等因素确定其生命周期特征。
本质上,我们的程序使用的内存在逻辑上会分为堆栈区、堆区、静态数据区和方法区。
不同领域的数据有不同的生命周期。
无论未来计算机硬件如何发展,内存容量都是有限的,所以清楚地了解程序中每个程序数据的生命周期是非常重要的。
指针操作
更多的采访会带这种东西:
Type*p;p++;然后问你p的值变了多少?
其实也可以认为是在测试编译器的基础知识。所以p的值不是+1那么简单,编译器实际上是在p上加了sizeof。
看一段代码的测试结果:
从观察结果可以看出,它们的生长结果是:
2(sizeof)4(sizeof)4(sizeof)8(sizeof)4(sizeof)8(sizeof)12(sizeof)看,附加值是的大小吗?其他结构、类等。,不验证你。有兴趣的话,自己验证一下。
让我们汇编这样一段代码,看看编译器是如何添加指针的:
编译结果:
请注意注释部分的结果,我们可以看到piv值显示为4),然后是16)。
指针变量和指向关系
用来保存指针的变量是指针变量。
如果指针变量p1存储变量num的地址,可以说p1指向变量num,也可以说p1指向num所在的内存块。这种指向关系通常由图中的箭头表示。
自由函数不能也不能将p设置为空。像下面这样的代码将有一个内存段错误:
因为,第一次自由操作后,P所指向的内存已经释放,但是P的值没有改变,自由函数无法改变这个值。当P所指向的内存区域再次被释放时,这个地址就变成了非法地址,会导致段错误
但是下面的代码不会有这样的问题:
因为p的值被编程为空,所以自由函数检测到p为空,并将直接返回它,没有任何错误。
在这里顺便给大家讲一个释放内存的小窍门,可以有效避免忘记设置指针空带来的各种内存问题。这个方法是自定义一个内存释放函数,但是传入的参数不知道指针,只知道指针的地址。在此功能中设置空,如下所示:
调用my_free后,p的值变为0,多次调用free后不会报错!
还有一种方法也很有效,就是定义一个FREE宏,在宏中设置它空。例如
指向空,或者什么都不要指向。
错误指针
指针变量的值为空,或者是未知的地址值,或者是当前应用程序无法访问的地址值。这样的指针是不好的指针。
它们不能被取消指向,否则程序会有运行时错误,这将导致程序意外终止。
任何指针变量都必须确保在寻址之前指向有效且可用的内存块,否则会出错。
错误的指针是C语言错误最常见的原因之一。
下面的代码就是错误的示例。voidopp { int * p = NULL* p = 10//哎呀!无法处理空值}
void foo { int * p;* p = 10//哎呀!无法寻址未知地址}
void bar { int * p =1000;* p = 10//哎呀!不能将指针指向不属于该程序内存的地址
空*型指针
由于void的类型为空,void*的指针仅保存指针值,但会丢失类型信息。我们不知道它指向什么类型的数据,只指定这个数据在内存中的起始地址。
要想完整提取出指向的数据,程序员必须对指针进行正确的类型转换,然后求解指针。因为,编译器不允许void*类型的指针被直接去指。
虽然字面意思是void空,但是void指针的含义并不是空指针的含义,它指的是上面提到的NULL指针。
Void指针实际上意味着指向任何类型的指针。任何类型的指针都可以直接赋给void指针而无需强制转换。
例如:
Type a, *p=&a;(Type等于char, int, struct, int*…)void*pv;pv=p;如前所述,void指针的优点是任何指针都可以直接赋值给它,这在某些场合非常有用,所以有些操作对于任何指针都是一样的。空指针最常用于内存管理。最典型最知名的就是标准库的免费功能。其原型如下:
voidfree;自由函数的参数可以是任意指针。没人见过自由参数中的指针需要强到void*?
malloc,calloc,realloc函数的返回值也是一个void指针。因为内存分配,你只需要知道分配的大小,然后返回新分配内存的地址。指针的值是地址。不管返回什么样的指针,结果其实都是一样的,因为所有的指针实际上都是32位长度,它的值就是内存的地址。指针类型仅供编译器查看。如果malloc函数设置为下面的原型,那就完全没有问题了。
char*malloc;实际上设置为
Type*malloc;是完全正确的,使用void指针的原因,其实前面说过,void指针就是任意指针,所以设计更严谨,更符合我们的直观理解。如果你用我前面提到的指针概念来理解童鞋,你一定会明白这一点。
结构和指针
结构指针有特殊的语法:->:符号
如果p是结构指针,p->:的方法访问结构的成员
typedef struct{char name;intage;float score;}Student;intmain{Student stu = {"Bob ",19,98.0 };学生* ps = & amp斯图;
ps->。年龄= 20岁;ps->。得分= 99.0;printf;返回0;}
数组和指针
1.当数组名是正确的值时,它是第一个元素的地址。
intmain{intarr = {1,2,3};int * p _ first = arrprintf;//1 return 0;}
2.指向数组元素的指针支持递增和递减操作。
intmain{intarr = {1,2,3};int * p = arrfor{printf;} return0}
3.p= p+1意味着让P指向最初指向的同一类型存储块的下一个相邻存储块。
在同一个数组中,元素的指针可以相减,指针之间的差等于下标之间的差。
4、p == *
p == *+ m)
5.当使用sizeof作为数组名称时,将返回整个数组占用的内存字节数。当数组名赋给指针时,sizeof运算符用于返回指针的大小。
这就是为什么当把一个数组传递给一个函数时,需要用另一个参数传递数组元素的个数。
intmain{intarr = {1,2,3};int * p = arrprintf=%d ",sizeof);//sizeof= 12 printf= % d ",sizeof);//sizeof=4
返回0;}
函数和指针
参数和函数指针
在C语言中,参数是通过值传递给形式参数的,也就是说函数中的形式参数是实际参数的副本,形式参数和实际参数只是上面的相同值,而不是相同的内存数据对象。
这意味着这种数据传输是单向的,即从调用者传输到被调函数,但被调函数不能修改传输的参数来达到返回的效果。
void change{a++; //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。}intmain{intage = 19;change;printf; //age = 19return0;}有时我们可以使用函数的返回值来返回数据,这在简单的情况下是可能的。
但是,如果返回值有其他用途,或者要返回多个数据,则返回值无法求解。
传递指向变量的指针可以很容易的解决上面的问题。
void change{++; //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,//会直接去内存中找到age这个数据,然后把它增1。}intmain{intage = 19;change;printf; //age = 20return0;}让我们举另一个用函数交换两个变量的值的老式例子:
#include<stdio.h>voidswap_bad;voidswap_ok;int main { inta = 5;int b = 3;swap_bad;//不能互换;swap _ ok;//okreturn 0;}
//写voidswap_bad{ intt;t = a;a = b;b = t;}
//正确写法:通过指针void swap _ ok { intt;t = * pa* pa = * pb* Pb = t;}
使用typedef的好处是可以用一个短的名字来表示一个类型,而不是一直使用长的代码,这样不仅使代码更加简洁易读,还避免了代码键入容易出错的问题。强烈建议您使用typedef来定义复杂的结构,如结构和指针。
每个函数本身也是一种程序数据。一个函数包含多个执行语句。经过编译,本质上是多条机器指令的集合。
程序装入内存后,函数的机器指令存储在一个特定的逻辑区域:代码区。
由于存储在内存中,函数也有自己的指针。
在C语言中,当函数名是正确的值时,就是这个函数的指针。
void echo{printf;}int main{void = echo; //函数指针变量指向echo这个函数p;//通过函数的指针p调用函数,相当于echoecho;返回0;}
常量和指针
const修改谁?谁也一样?
如果const后跟一个类型,则跳过最近的原子类型,并修饰以下数据。
如果const后面是数据,直接修改数据。
intmain{inta = 1;intconst * p1 = & ampa .//const后面跟*p1,本质上是数据a,所以修改*p1。通过p1,不能修改a的值。const int * p2 = &:a;//const后面是int类型,所以跳过int,修改*p2,效果同上
int * constp3 = NULL//const后面是数据p3。也就是说,指针p3本身是常量。
constint * constp4 = & ampa .//a的值不能通过p4来改变,p4本身就是constitution const * const P5 = &;a .//效果同上
返回0;
} typedefinet * pint _ t;//将int* type包装为pint_t,那么pint_t现在就是一个完整的原子类型
intmain{
inta = 1;constpint _ tp1 = & ampa .//同样,const跳过pint_t类型,修改p1。指针p1本身是const pint _ tconstp 2 = & amp:a;//const直接修改p,同上
返回0;
}
深度复制和轻度复制
如果两个程序单元通过复制它们共享的数据的指针来工作,这是一个浅层复制,因为要访问的数据没有被复制。
如果复制访问的数据,每个单元都有自己的副本,并且对目标数据的操作不受彼此的影响,称为深度复制。
//测试机器是否处于小端模式。如果是,则为真;否则为假
//这种方法的依据是,C语言中一个对象的地址是这个对象占用的字节中地址值最小的字节的地址。
boolisSmallIndain{unsignedintval = 'A';unsignedchar* p = &val; //C/C++:对于多字节数据,取地址是取的数据对象的第一个字节的地址,也就是数据的低地址return * p = = ' A}
1.《指针的指针 还没搞懂C语言指针?这里有最详细的纯干货讲解(附代码)》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《指针的指针 还没搞懂C语言指针?这里有最详细的纯干货讲解(附代码)》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/guoji/1688969.html