指针粗解.docx
- 文档编号:30663569
- 上传时间:2023-08-19
- 格式:DOCX
- 页数:25
- 大小:40.39KB
指针粗解.docx
《指针粗解.docx》由会员分享,可在线阅读,更多相关《指针粗解.docx(25页珍藏版)》请在冰豆网上搜索。
指针粗解
指针
壹、内存分配表
计算机中的内存都是编址的,就像你家的地址一样。
在程序编译或者运行的时候,系统(可以不关心具体是什么,可能是编译器,也可能是操作系统,实际上是二者合作的结果。
)开辟了一张表。
每遇到一次声明语句(包括变量的声明、函数的声明和传入参数的声明等等)都会开辟一个内存空间,并在表中增加一行纪录,记载一些对应关系。
Declaration
ID
Name
Address
Length
intnP;
1
nP
2000
2B
charmyChar;
2
myChar
2002
1B
int*myPointer;
3
myPointer
2003
2B
char*myPointer2;
4
myPointer2
2005
2B
贰、指针就是一个整数
指针,是一个无符号整数(unsignedint,因不致歧义下简称“整数”),它是一个以当前系统寻址范围为取值范围的整数。
32位系统的寻址能力(地址空间)是4GB(0~232-1bytes),二进制表示长度为32bit,也就是4B。
不难验证,在32位系统里,int类型也正好是4B(32-bit)长度,可以取遍上述范围。
同理,64位系统取值范围为0~264-1,int类型长度为8B。
例证就是程序1得到的答案和程序2的答案一致。
(不同机器可能需要调整一下pT的取值。
)
程序1
程序2
#include
main()
{
char*pT;
chart='h';
pT=&t;
putchar(*pT);
}
#include
main()
{
char*pT;
chart='h';
pT=(char*)1245048;
putchar(*pT);
}
程序2第6行加上“(char*)”是因为毕竟unsignedint和char*不是一回事,需要强制转换,否则会有个警告。
既然指针的实质是一个整数,为何不用unsignedint直接声明,或者统一用int*声明,而要用不同的类型后面加上一个“*”表示呢?
char*声明过的类型,一次访问1个sizeof(char)长度,double*声明过的类型,一次访问1个sizeof(double)长度。
在汇编里,没有数据类型这一概念,整数类型和指针就是一回事了。
不论是整数还是指针,执行自增的时候,都是将原值加一。
如果上文声明char*pT;,汇编语言中pT自增(INC)之后值为1245049,可是C语言中pT++之后pT值为1245049。
如果32位系统中,上文声明int*pT;,汇编语言中pT自增之后值为1245049,可是C语言中pT++之后pT值为1245052。
为什么DOS下面的TurboC,和Windows下的VC的int类型自增时的步进不一样长?
因为DOS是16位的,Windowsx86是32位的,int类型长度取决于操作系统的位长。
可以预见,在Windowsx64中编译,上文声明int*pT;,在执行pT++之后pT值为1245056。
那么,复杂的结构怎么分配空间呢?
C语言的结构体(汇编语言对应为Record类型)按顺序分配空间。
inta[20];
typedefstructst
{
doubleval;
charc;
structst*next;
}pst;
pstpT[10];
在32位系统下,内存里面做如下分配:
(单位:
H,16进制)
变量
2000
2001
2002
2003
2004
2005
2006
…
204C
204D
204E
204F
地址
a[0]
a[1]
…
a[19]
变量
2050
2051
…
2057
2058
2059
205A
205B
205C
205D
205E
205F
地址
pst.val
…
pst.c
pst.next
无效
无效
无效
这就说明了为什么sizeof(pst)=16而不是8。
编译器把结构体的大小规定为结构体成员中大小最大的那个类型的整数倍。
至于pT的存储,可以依例推得。
总长为160,此不赘述。
有个问题,如果执行pT++,答案是什么?
是自增16,还是160?
别忘了,pT是常量,不能加减。
所以,我们就可以声明:
typedefstructBiTree
{
intvalue;
structBiTree*LeftChild;
structBiTree*RightChild;
}BTree;
用一个整数,代表一棵树的结点。
把它赋给某个结点的LeftChild/RightChild值,就形成了上下级关系。
只要无法找到一个路径,使得A->LC/RC->LC/RC...->LC/RC==A(A泛指某一结点),这就构成了一棵二叉树。
反之就成了图。
叁、C的按值传递
C中函数调用是按值传递的,传入参数在子函数中只是一个初值相等的副本,无法对传入参数作任何改动。
但实际编程中,经常要改动传入参数的值。
这一点我们可以用一个小技巧,即传入参数的地址而不是原参数本身,当对传入参数(地址)取“*”运算时,就可以直接在内存中修改,从而改动原想作为传入参数的参数值。
#include
voidinc(int*val)
{
(*val)++;
}
main()
{
inta=3;
inc(&a);
printf("%d",a);
}
在执行inc(&a);时,系统在内存分配表里增加了一行“inc中的val”,其地址为新地址,值为&a。
操作“*val”,即是在操作a了。
肆、*和&运算
“*p”操作是这样一种运算,返回p的值作为地址之内存空间的取值。
“&p”则是这样一种运算,返回当时声明p时开辟的地址。
显然可以用赋值语句对内存地址赋值。
我们假设有这么一段内存地址空间,他们取值如下:
(单位:
H,16进制)
地址
0000
…
2000
2001
2002
2003
2004
…
3000
3001
3002
3003
…
取值
?
?
?
?
…
01
30
00
00
30
…
00
03
20
9A
…
然后,执行这么一段代码“int*p;”,假设开辟空间时p被分配3001H、3002H两个位置。
则p为2003H,*p为3001H。
(字节序为小尾序,Little-Endian,低位在低地址,Intel的x86系列CPU适用。
Motorola的PowerPC系列则采用大尾序,Big-Endian,高位在低地址,则上述p为0320H,*p需进一步查内存0320H处的存储值。
)
**p的值为多少?
**p=*(*(p))=*(*(2003H))=*(3000H)=0300H。
那么&&p、*(&p)和&(*p)又等于多少?
&&p=&(&(p))=&(3001H),此时出错了,3001H是个常数怎么可能有地址呢?
*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。
很多人误解了&和*,以为他们互为逆运算,即&*p=p。
实际上&*p=&(*p)=&(3001H),出错了,3001H是个常数怎么可能有地址呢?
我们再看看另类的*和&。
这里有两个地方要注意:
(1)在程序声明变量的时候的*,只是表明“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(type)长度”。
这点不要和(*)操作符混淆;
(2)在C++程序声明变量的时候的&,只是表明“它是一个引用,这个引用声明时不开辟新空间,它在内存分配表加入新的一行,该行内存地址等于和调用时传入的对应参数内存地址”。
这一点不要与“*”声明符和“&”操作符混淆。
伍、双重指针(指向指针的指针)
双重指针又是怎么一回事儿呢?
综合贰的BTree定义,和叁的说法。
对于一棵树,我们通常用它的根结点地址来表示这棵树。
正所谓“擒贼先擒王”,找到了树的根,其每个结点都可以找到。
但是有时候我们需要对树进行删除结点,增加结点操作,往往考虑到删除根结点,增加的结点取代原来的根结点作为新根结点的情况。
为了修改根结点这个“整数”,我们需要退一步,使用这个“整数”的内存地址,也就是指向这个“整数”的指针。
在声明时,我们用2个*号,声明指向指针的指针。
它的意思是“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(unsignedint)长度,其指向的内存地址所存储的值是一个整数,那个整数值指向某个内存地址,一次访问sizeof(BTree)长度。
”详见“数据结构.rar”有关“树”(06文件夹)的程序代码。
陆、指针数组、数组指针和指向函数的指针
指针数组:
就是一个整数数组,那个数组的各个元素都是整数,指向某个内存地址。
数组指针:
数组名本身就是一个指针,指向数组的首地址。
注意这是声明定长数组时,其数组名指向的数组首地址是常量。
而声明数组并使某个指针指向其值指向某个数组的地址(不一定是首地址),指针取值可以改变。
指向函数的指针:
从二进制角度考虑,数组名是该数组数据段首地址,函数名就是该代码段的首地址,可以用“int*fun()”。
在二进制层面,代码段和数据段什么区别?
没什么区别。
#include
voidinc(int*val)
{
(*val)++;
}
main()
{
void(*fun)(int*);
inta=3;
fun=inc;
(*fun)(&a);
printf("%d",a);
}
柒、几个问题
下面是关于指针的几个思考题,在文中或许没有提及,然而读者可以自行上机实现之。
1、声明一个指针之后,为什么要立即赋值为NULL?
如果不这么做,其初始值是多少?
2、声明为void*的指针,一次可以访问多大步长?
为什么很多程序在需要传入缓存(Buffer)首地址的时候都习惯在传入参数处声明为(void*Buffer)?
3、很多人都说C语言是一种面向过程的语言,因为它最多只有struct的定义,而没有class的概念。
看完本篇后,你认为C语言能成为面向对象的语言吗?
如果可以,请给出一个简单的表示。
4、指针和引用只有表示上的差异,而无实质的区别。
那么,Java、C#等语言里没有指针的概念,你认为这样的理解正确吗?
如果不赞同,请举出什么情况下传入的参数实际上是其指针而不是副本。
5、指针是一个指向内存地址的整数,如何用这个概念去理解Windows程序设计中的HANDLE(句柄)、RESOURCE(资源)和MESSAGE(消息)?
C语言中的指针专题
一、数组的指针、指针数组以及指向指针的指针
考虑数组的指针的时候我们要同时考虑类型和维数这两个属性。
换一句话,就是说一个数组排除在其中存储的数值,那么可以用类型和维数来位置表示他的种类。
A)一维数组 在c和c++中数组的指针就是数组的起始地址(也就第一个元素的地址),而且标准文档规定数组名代表数组的地址(这是地址数值层面的数组表示)。
例如:
inta[10];int*p;
p=&a[0]//和p=a是等价的:
因为a是数组名,所以他是该数组的地址,同时因为第一个元素为a[0],那么&a[0]也代表了该数组的地址。
但是我们是不是就说一个数组名和该数组的第一个元素的&运算是一回事呢?
在一维的时候当时是的,但是在高维的时候,我们要考虑到维数给数组带来的影响。
a[10]是一个数组,a是数组名,它是一个包含10个int类型的数组类型,不是一般的指针变量噢!
(虽然标准文档规定在c++中从int[]到int*直接转换是可以的,在使用的时候似乎在函数的参数为指针的时候,我们将该数组名赋值没有任何异样),a代表数组的首地址,在数字层面和a[10]的地址一样。
这样我们就可以使用指针变量以及a来操作这个数组了。
所以我们要注意以下问题:
(1)p[i]和a[i]都是代表该数组的第i+1个元素;
(2)p+i和a+i代表了第i+1个元素的地址,所以我们也可以使用 *(p+I)和*(a+I)来引用对象元素;(3)p+1不是对于指针数量上加一,而是表示从当前的位置跳过当前指针指向类型长度的空间,对于win32的int为4byte;
B)多维数组 对于二维数组a[4][6];由于数组名代表数组的起始地址,所以a(第一层)和第一个元素a[0][0]地址的数字是相同的,但是意义却是不同的。
对于该数组我们可以理解为:
a的一维数组(第一层),它有四个元素a[0]、a[1]、a[2]、a[3](第二层),而每个元素又含有6个元素a[0][0],a[0][1],a[0][2],a[0][3],a[0][4],a[0][5](第三层),…到此我们终于访问到了每个元素了,这个过程我们经历了:
a->a[0]->a[0][0]; 整体来讲:
a是一个4行5列的二维数组,a表示它指向的数组的首地址(第一个元素地址&a[0]),同时a[0]指向一行,它是这个行的名字(和该行的第一个元素的首地址相同(第一个元素为地址&a[0][0]))。
所以从数字角度说:
a、a[0]、&a[0][0]是相同的,但是他们所处的层次是不同的。
既然a代表二维数组,那么a+i就表示它的第i+1个元素*(a+i)的地址,而在二维数组中
∙(a+i)又指向一个数组,*(a+i)+j表示这个数组的第j+1个元素的地址,所以要访问这个元素可以使用*(*(a+i)+j)(也就是a[i][j])。
他们的示意图为(虚线代表不是实际存在的):
对照这个图,如下的一些说法都是正确的(对于a[4][6]):
∙a是一个数组类型,*a指向一个数组;
∙a+i指向一个数组;
∙a、*a和&a[0][0]数值相同;
∙a[i]+j和*(a+i)+j是同一个概念;
总结一下就是:
我们对于二维指针a,他指向数组a[0,1,2,3],使用*,可以使他降级到第二层次,这样*a就指向了第一个真正的数组。
对于其他的情况我们也可以采用相同的方式,对于其他维数和类型的数组我们可以采用相类似的思想。
说到指向数组的指针,我们还可以声明一个指针变量让它指向一个数组。
例如:
int(*p)[5];
这时p就是一个指针,要指向一个含有5个int类型元素的数组,指向其他的就会出现问题。
这个时候我们可以使用上面的什么东西来初始化呢?
我们可以使用*a,*(a+1),a[2]等。
原因很简单:
我们在一个二维的数组中,那么表达方式有上面的相互类似的意义呢?
只有 *a,*(a+1),a[2]等,
C)指针数组 一个指针数组是指一个数组中的每个元素都是一个指针,例如:
int*p[10];//而不能是int(*p)[10]
或者
char*p[10];
此时p是一个指针(数值上和&p[0]一样);在前面有intt[10];
int*pt=t;//使用pt指向t
那么这里我们用什么指向int*t[10]中的t呢?
我们要使用一个指针的指针:
int**pt=t;
这是因为:
在int*t[10]中,每个元素是指针,那么同时t又指向这个数组,数组上和&t[0]相同,也就是指向t[0],指向一个指针变量,可以说是一个指针的指针了,所以自然要用
int**pt;
D)指针的指针 一个指针变量内部可以存储一个值,这个值是另外一个对象的地址,所以我们说一个指针变量可以指向一个普通变量,同样这个指针变量也有一个地址,也就是说有一个东西可以指向这个指针变量,然后再通过这个指针变量指向这个对象。
那么如何来指向这个指针变量呢?
由于指针变量本身已经是一个指针了(右值),那么我们这里就不能用一般的指针了,需要在指针上体现出来这些特点,我们需要定义指针的指针(二重指针)。
int*p1=&i;int**p2=&p1;
综合以上的所有点,下面是我们常常看到一些匹配(也是经常出错的地方):
inta[3],b[2][3],c,*d[3];voidfun1(int*p);voidfun2(int(*p)[3]);voidfun3(int**p);voidfun4(intp[3]);voidfun5(intp[]);voidfun6(intp[2][3]);voidfun7(int(&p)[3]);
函数不会产生编译时刻的可能值(但逻辑上不一定都对)
15pt"> 函数 不会产生编译时刻的可能值(但逻辑上不一定都对) 6.75pt"> fun1 a,&a[i],*b,b[i],&b[i][j],&c,d[i] 14.25pt"> fun2 b,b+i, 14.25pt"> fun3 d 13.5pt"> fun4 a,&a[i],*b,b[i],&b[i][j],&c,d[i] 17.25pt"> fun5 a,&a[i],*b,b[i],&b[i][j],&c,d[i] 10.5pt"> fun6 b 16.95pt"> fun7 a 为什么可以有这样的搭配,原因如下: ∙对于fun1fun4fun5: 在编译器看来fun1,fun4,fun5的声明是一样,在编译时候,编译器把数组的大小舍去不考虑,只考虑它是一个指针,也就是说有没有大小说明是一样的,所以三者的形式都是fun1的形式(其实只要提供了int*指针就可以了); ∙对于fun7: 以上的解释对于引用是不适用的,如果变量被声明为数组的引用,那么编译器就要考虑数组的大小了,那么必须和声明一模一样(所以fun7就只有a合适); ∙对于fun2: p是一个指向一个含有3个元素的数组,这样b和b+i正好合适,而a却不是(它是指向a[0]的,不是指向这个数组的); ∙对于fun3: p是一个指针的指针,而d指向d[0],同时d[0]又是一个指针,所以d就是一个指针的指针。 但是b却不是(它是一个2*3的矩阵也就是年int[2][3]类型); ∙对于fun6,p是一个2*3的数组类型,和b恰好完全匹配; 二、函数指针、函数的指针参数以及返回指针的函数 A)函数指针 C++规定,一个函数的地址就是这个函数的名字。 我们需要指出的就是一个指针需要指定类型是为了后来的指针解析时候使用,通过指针有效快速访问对象。 那么对于函数的指针,它要表示出该函数的那些特性才能满足解析的唯一性呢? 答案就是一个函数的特性有它的参数列表和返回类型。 下面是一个函数指针的例子: int(*p)(intI,intj); 不能是 int*p(intI,intj), 这样就变成了返回指针的函数声明了。 在C++中处于对安全性的考虑,指针和它指向的对象要类型一致,也就说上面的指针所指向的函数的特性要和它一模一样: 例如指向intmin(intI,intj);是可以的。 但是指向intmin(doubleI,doublej);是不可以。 函数指针也和其他的指针一样,在使用的时候很怕发生"悬空",所以在使用的时候同样要判断有效性,或者在定义的时候就初始化。 int(*p)(intI,intj)=min;int(*p)(intI,intj)=&min;int(*p)(intI,intj)=0; B)函数的指针参数 函数指针可以作函数的参数: 例如我们有一个积分的算法,对于不同的数学函数可以进行积分(我们这里假设函数都是一元的);那么我们的算法接口可以定义为: template 这里的最后的参数是一个函数的指针,并且被设定缺省值为0。 这个函数返回一个值,同时需要一个参数。 假如加入我们有这样的一个函数: doubleline(doublex){returna*x+b;} 那么我就可以使用了。 函数指针还可以作为返回类型(注意不是函数! ! ,某个特定的函数是不可以作为返回类型的。 )假设: typedefint(*PF)(int);PFgetProcessMethod();//true C)返回指针的函数 一个函数的返回是函数的重要接口之一,c++的一个重要的强大的功能就是能够设计足够复杂和好用的用户自定义类型。 而同时处理和传递这些类型也是很麻烦的一件事情,我们不想把我们的时间都花在这些对于我们的实际工作没有很实质帮助的拷贝上,解决这个问题就要依赖我们的接口设计: c和c++都提供了相应的解决方案,在c++中我们可是使用引用,讲他们作为函数的实际参数,或者我们在函数的实际参数中使用一个指针等。 同样我们还可以使用一个函数返回一个指针: 但是这是一个很不好解决的问题! 我们首先容易出错的是: 将一个局部变量的地址传出来! 例如: UserType*Process(){ UserTypeut(param-list); //processut; return&ut;//} 这个变量在我们的函数结束的时候就被销毁了,尽管地址可以传出去,但是这个地址已经不存在了,已经不能使用的东西,在这个函数之外却不知道,难免要出错! 同时我还会有一个比较麻烦的问题: 使用new,又容易造成内存泄露 UserType*Process(){ UserTpye*put=newUserType(param-list); //processput; returnput;} 我们在函数内部使用了一个new,分配了一个空间,这样传出来也是可以! 就是说不会发生上面的问题了。 但是用户通常都会忘记在程序的外面在把
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 指针