C语言编程要点指针和内存分配上Read.docx
《C语言编程要点指针和内存分配上Read.docx》由会员分享,可在线阅读,更多相关《C语言编程要点指针和内存分配上Read.docx(16页珍藏版)》请在冰豆网上搜索。
![C语言编程要点指针和内存分配上Read.docx](https://file1.bdocx.com/fileroot1/2023-7/10/c840b8b3-75bf-4fd4-b5e4-faf7196aa2c3/c840b8b3-75bf-4fd4-b5e4-faf7196aa2c31.gif)
C语言编程要点指针和内存分配上Read
C语言编程要点---第7章指针和内存分配(上)
指针和内存分配
指针为C语言编程提供了强大的支持——如果你能正确而灵活地利用指针,你就可以直接切入问题的核心,或者将程序分割成一个个片断。
一个很好地利用了指针的程序会非常高效、简洁和精致。
利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild”pointer),即指向一个错误位置的指针,你的数据就危险了——存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。
此后可能发生的事情取决于这样两点:
第一,内存中的数据被破坏的程度有多大;第二,内存中的被破坏的部分还要被使用多少次。
在有些情况下,一些函数(可能是内存分配函数、自定义函数或标准库函数)将立即(也可能稍晚一点)无法正常工作。
在另外一些情况下,程序可能会终止运行并报告一条出错消息;或者程序可能会挂起;或者程序可能会陷入死循环;或者程序可能会产生错误的结果;或者程序看上去仍在正常运行,因为程序没有遭到本质的破坏。
值得注意的是,即使程序中已经发生了根本性的错误,程序有可能还会运行很长一段时间,然后才有明显的失常表现;或者,在调试时,程序的运行完全正常,只有在用户使用时,它才会失常。
在C语言程序中,任何野指针或越界的数组下标(out-of-boundsarraysubscript)都可能使系统崩溃。
两次释放内存的操作也会导致这种结果。
你可能见过一些C程序员编写的程序中有严重的错误,现在你能知道其中的部分原因了。
有些内存分配工具能帮助你发现内存分配中存在的问题,例如漏洞(leak,见7.21),两次释放一个指针,野指针,越界下标,等等。
但这些工具都是不通用的,它们只能在特定的操作系统中使用,甚至只能在特定版本的编译程序中使用。
如果你找到了这样一种工具,最好试试看能不能用,因为它能为你节省许多时间,并能提高你的软件的质量。
指针的算术运算是C语言(以及它的衍生体,例如C++)独有的功能。
汇编语言允许你对地址进行运算,但这种运算不涉及数据类型。
大多数高级语言根本就不允许你对指针进行任何操作,你只能看一看指针指向哪里。
C指针的算术运算类似于街道地址的运算。
假设你生活在一个城市中,那里的每一个街区的所有街道都有地址。
街道的一侧用连续的偶数作为地址,另一侧用连续的奇数作为地址。
如果你想知道RiverRd.街道158号北边第5家的地址,你不会把158和5相加,去找163号;你会先将5(你要往前数5家)乘以2(每家之间的地址间距),再和158相加,去找RiverRd.街道的168号。
同样,如果一个指针指向地址158(十进制数)中的一个两字节短整型值,将该指针加3=5,结
果将是一个指向地址168(十进制数)中的短整型值的指针(见7.7和7.8中对指针加减运算的详细描述)。
街道地址的运算只能在一个特定的街区中进行,同样,指针的算术运算也只能在一个特定的数组中进行。
实际上,这并不是一种限制,因为指针的算术运算只有在一个特定的数组中进行才有意义。
对指针的算术运算来说,一个数组并不必须是一个数组变量,例如函数malloc()或calloc()的返回值是一个指针,它指向一个在堆中申请到的数组。
指针的说明看起来有些使人感到费解,请看下例:
char*p;
上例中的说明表示,p是一个字符。
符号“*”是指针运算符,也称间接引用运算符。
当程序间接引用一个指针时,实际上是引用指针所指向的数据。
在大多数计算机中,指针只有一种,但在有些计算机中,指向数据和指向函数的指针可以是不同的,或者指向字节(如char。
指针和void*指针)和指向字的指针可以是不同的。
这一点对sizeof运算符没有什么影响。
但是,有些C程序或程序员认为任何指针都会被存为一个int型的值,或者至少会被存为一个long型的值,这就无法保证了,尤其是在IBMPC兼容机上。
注意:
以下讨论与Macintosh或UNIX程序员无关;
最初的IBMPC兼容机使用的处理器无法有效地处理超过16位的指针(人们对这种结论仍有争议。
16位指针是偏移量,见9.3中对基地址和偏移量的讨论)。
尽管最初的IBMPC机最终也能使用20位指针,但颇费周折。
因此,从一开始,基于IBM兼容机的各种各样的软件就试图冲破这种限制。
为了使20位指针能指向数据,你需要指示编译程序使用正确的存储模式,例如紧缩存储模式。
在中存储模式下,你可以用20位指针指向函数。
在大和巨存储模式下,用20位指针既可以指向数据,也可以指向函数。
在任何一种存储模式下,你都可能需要用到far指针(见7.18和7.19)。
基于286的系统可以冲破20位指针的限制,但实现起来有些困难。
从386开始,IBM兼容机就可以使用真正的32位地址了,例如象MS-Windows和OS/2这样一些操作系统就实现了这一点,但MS—DOS仍未实现。
如果你的MS—DOS程序用完了基本内存,你可能需要从扩充内存或扩展内存中分配更多的内存。
许多版本的编译程序和函数库都提供了这种技术,但彼此之间有所差别。
这些技术基本上是不通用的,有些能在绝大多数MS-DOS和MS-WindowsC编译程序中使用,有些只能在少数特定的编译程序中使用,还有一些只能在特定的附加函数库的支持下使用。
如果你手头有能提供这种技术的软件,你最好看一下它的文档,以了解更详细的信息.
7.1. 什么是间接引用(indirection)?
对已说明的变量来说,变量名就是对变量值的直接引用。
对指向变量或内存中的任何对象的指针来说,指针就是对对象值的间接引用。
如果p是一个指针,p的值就是其对象的地址;*p表示“使间接引用运算符作用于p”,*p的值就是p所指向的对象的值。
*p是一个左值,和变量一样,只要在*p的右边加上赋值运算符,就可改变*p的值。
如果p是一个指向常量的指针,*p就是一个不能修改的左值,即它不能被放到赋值运算符的左边,请看下例:
例7.1一个间接引用的例子
#include
int
main()
{
inti;
int*p;
i=5;
p=&i; /*now*p==i*/
/* %PisdescribedinFAQVII.28*/
printf("i=%d,p=%P, *p=%d\n",i,P,*p);
*p=6; /*sameasi=6*/
printf("i=%d,p=%P, *p=%d\n",i,P,*P);
return0; /*seeFAQXVI.4*/}
}
上例说明,如果p是一个指向变量i的指针,那么在i能出现的任何一个地方,你都可以用*p代替i。
在上例中,使p指向i(p=&i)后,打印i或*p的结果是相同的;你甚至可以给*p赋值,其结果就象你给i赋值一样。
请参见:
7.4什么是指针常量?
7.2. 最多可以使用几层指针?
对这个问题的回答与“指针的层数”所指的意思有关。
如果你是指“在说明一个指针时最多可以包含几层间接引用”,答案是“至少可以有12层”。
请看下例:
int i=0;
int *ip0l=&d;
int **ip02=&ip01;
int ***ip03=&ip02;
int ****ip04=&dp03;
int *****ip05=&ip04;
int ******ip06=&ip05;
int *******ip07=&ip06;
int ********ip08=&ip07;
int *********ip09=&ip08;
int **********ip10=&ip09;
int ***********ipll=&ip10;
int ************ip12=&ipll;
************ip12=1; /*i=1*/
注意:
ANSIC标准要求所有的编译程序都必须能处理至少12层间接引用,而你所使用的编译程序可能支持更多的层数。
如果你是指“最多可以使用多少层指针而不会使程序变得难读”,答案是这与你的习惯有关,但显然层数不会太多。
一个包含两层间接引用的指针(即指向指针的指针)是很常见的,但超过两层后程序读起来就不那么容易了,因此,除非需要,不要使用两层以上的指针。
如果你是指“程序运行时最多可以有几层指针”,答案是无限层。
这一点对循环链表来说是非常重要的,因为循环链表的每一个结点都指向下一个结点,而程序能一直跟住这些指针。
请看下例:
例7.2一个有无限层间接引用的循环链表
/*Wouldrunforeverifyoudidn'tlimitittoMAX*/
#include
structcirc_list
{
char value[3]; /*e.g.,"st"(incl'\0')*/
structcirc_list *next;
};
structcirc_list suffixes[]={
"th",&.suffixes[1],/*Oth*/
"st",&.suffixes[2],/*1st*/
"nd",&suffixes[3],/*2nd*/
"rd",&suffixes[4],/*3rd*/
"th",&.suffixes[5],/*4th*/
"th",&.suffixes[6],/*5th*/
"th",&suffixes[7],/*6th*/
"th",&suffixes[8],/*7th*/
"th",&suffixes[9],/*8th*/
"th",&suffixes[0],/*9th*/
};
#defineMAX20
main()
{
inti=0;
structcirc_list *p=suffixes;
while(i<=MAX){
printf("%ds%\n",i,p->value);
++i;
p=p->next;
}
}
在上例中,结构体数组suffixes的每一个元素都包含一个表示词尾的字符串(两个字符加上末尾的NULL字符)和一个指向下一个元素的指针,因此它有点象一个循环链表;next是一个指针,它指向另一个circ_list结构体,而这个结构体中的next成员又指向另一个circ_list结构体,如此可以一直进行下去。
上例实际上相当呆板,因为结构体数组suffixes中的元素个数是固定的,你完全可以用类似的数组去代替它,并在while循环语句中指定打印数组中的第(i%10)个元素。
循环链表中的元素一般是可以随意增减的,在这一点上,它比上例中的结构体数组suffixes要有趣一些。
请参见:
7.1什么是间接引用(indirection)?
7.3. 什么是空指针?
有时,在程序中需要使用这样一种指针,它并不指向任何对象,这种指针被称为空指针。
空指针的值是NULL,NULL是在中定义的一个宏,它的值和任何有效指针的值都不同。
NULL是一个纯粹的零,它可能会被强制转换成void*或char*类型。
即NULL可能是0,0L或(void*)0等。
有些程序员,尤其是C++程序员,更喜欢用0来代替NULL。
指针的值不能是整型值,但空指针是个例外,即空指针的值可以是一个纯粹的零(空指针的值并不必须是一个纯粹的零,但这个值是唯一有用的值。
在编译时产生的任意一个表达式,只要它是零,就可以作为空指针的值。
在程序运行时,最好不要出现一个为零的整型变量)。
注意:
空指针并不一定会被存为零,见7.10。
警告:
绝对不能间接引用一个空指针,否则,你的程序可能会得到毫无意义的结果,或者得到一个全部是零的值,或者会突然停止运行。
请参见:
7.4什么时候使用空指针?
7.10NULL总是等于0吗?
7.24为什么不能给空指针赋值?
什么是总线错误、内存错误和内存信息转储
7.4. 什么时候使用空指针?
空指针有以下三种用法:
(1)用空指针终止对递归数据结构的间接引用。
递归是指一个事物由这个事物本身来定义。
请看下例:
/*Dumbimplementation;shouldusealoop*/
unsignedfactorial(unsingedi)
{
if(i=0||i==1)
{
return1;
}
else
{
returni*factorial(i-1);
}
}
在上例中,阶乘函数factoriai()调用了它本身,因此,它是递归的。
一个递归数据结构同样由它本身来定义。
最简单和最常见的递归数据结构是(单向)链表,
链表中的每一个元素都包含一个值和一个指向链表中下一个元素的指针。
请看下例:
structstring_list
{
char*str;/*string(inthiscase)*/
structstring_list *next;
};
此外还有双向链表(每个元素还包含一个指向链表中前一个元素的指针)、键树和哈希表等许多整洁的数据结构,一本较好的介绍数据结构的书中都会介绍这些内容。
你可以通过指向链表中第一个元素的指针开始引用一个链表,并通过每一个元素中指向下一个元素的指针不断地引用下一个元素;在链表的最后一个元素中,指向下一个元素的指针被赋值为NULL,当你遇到该空指针时,就可以终止对链表的引用了。
请看下例:
while(p!
=NULL)
{
/*dOsomethingwithp->str*/
p=p->next;
}
请注意,即使p一开始就是一个空指针,上例仍然能正常工作。
(2)用空指针作函数调用失败时的返回值。
许多C库函数的返回值是一个指针,在函数调用成功时,函数返回一个指向某一对象的指针;反之,则返回一个空指针。
请看下例:
if(setlocale(cat,loc_p)==NULL)
{
/*setlocale()failed;dosomething*/
/*...*/
}
返回值为一指针的函数在调用成功时几乎总是返回一个有效指针(其值不等于零),在调用失败时则总是返回一个空指针(其值等于零);而返回值为一整型值的函数在调用成功时几乎总是返回一个零值,在调用失败时则总是返回一个非零值。
请看下例:
if(raise(sig)!
=0){
/*raise()failed;dosomething*/
/*...*/
}
对上述两类函数来说,调用成功或失败时的返回值含义都是不同的。
另外一些函数在调用成功时可能会返回一个正值,在调用失败时可能会返回一个零值或负值。
因此,当你使用一个函数之前,应该先看一下它的返回值是哪种类型,这样你才能判断函数返回值的含义。
(3)用空指针作警戒值
警戒值是标志事物结尾的一个特定值。
例如,main()函数的预定义参数argv是一个指针数组,它的最后一个元素(argv[argc])永远是一个空指针,因此,你可以用下述方法快速地引用argv中的每一个元素:
/*
Asimpleprogramthatprintsallitsarguments.
Itdoesn'tuseargc("argumentcount");instread.
ittakesadvantageofthefactthatthelast
valueinargv("argumentvector")isanullpointer.
*/
#include
#include
int
main(intargc,char**argv)
{
inti;
printf("programname=\"%s\"\n",argv[0]);
for(i=l;argv!
=NULL;++i)
printf("argv[%d]=\"%s\"\n",i,argv[f]);
assert(i==argc); /*seeFAQXI.5*/
return0; /*seeFAQXVI.4*/
}
请参见:
7.3什么是空指针?
7.10NULL总是等于0吗?
20.2程序总是可以使用命令行参数吗?
7.5. 什么是void指针?
void指针一般被称为通用指针或泛指针,它是C关于“纯粹地址(rawaddress)”的一种约定。
void指针指向某个对象,但该对象不属于任何类型。
请看下例:
int *ip;
void *p;
在上例中,ip指向一个整型值,而p指向的对象不属于任何类型。
在C中,任何时候你都可以用其它类型的指针来代替void指针(在C++中同样可以),或者用void指针来代替其它类型的指针(在C++中需要进行强制转换),并且不需要进行强制转换。
例如,你可以把char*类型的指针传递给需要void指针的函数。
请参见:
7.6什么时候使用void指针?
7.27可以对void指针进行算术运算吗?
15.2C++和C有什么区别?
7.6. 什么时候使用void指针?
当进行纯粹的内存操作时,或者传递一个指向未定类型的指针时,可以使用void指针。
void指针也常常用作函数指针。
有些C代码只进行纯粹的内存操作。
在较早版本的C中,这一点是通过字符指针(char*)实现的,但是这容易产生混淆,因为人们不容易判断一个字符指针究竟是指向一个字符串,还是指向一个字符数组,或者仅仅是指向内存中的某个地址。
例如,strcpy()函数将一个字符串拷贝到另一个字符串中,strncpy()函数将一个字符串中的部分内容拷贝到另一个字符串中:
char*strepy(char'strl,constchar*str2);
char*strncpy(char*strl,constchar*str2,size_tn);
memcpy()函数将内存中的数据从一个位置拷贝到另一个位置:
void*memcpy(void*addrl,void*addr2,size_tn);
memcpy()函数使用了void指针,以说明该函数只进行纯粹的内存拷贝,包括NULL字符(零字节)在内的任何内容都将被拷贝。
请看下例:
#include"thingie.h" /*definesstructthingie*/
structthingie*p_src,*p_dest;
/*...*/
memcpy(p_dest,p_src,sizeof(structthingie)*numThingies);
在上例中,memcpy()函数要拷贝的是存放在structthingie结构体中的某种对象op_dest和p_src都是指向structthingie结构体的指针,memcpy()函数将把从p_src指向的位置开始的sizeof(stuctthingie)*numThingies个字节的内容拷贝到从p_dest指向的位置开始的一块内存区域中。
对memcpy()函数来说,p_dest和p_src都仅仅是指向内存中的某个地址的指针。
请参见:
7.5什么是void指针?
7.14什么时候使用指向函数的指针?
7.7. 两个指针可以相减吗?
为什么?
如果两个指针向同一个数组,它们就可以相减,其为结果为两个指针之间的元素数目。
仍以本章开头介绍的街道地址的比喻为例,假设我住在第五大街118号,我的邻居住在第五大街124号,每家之间的地址间距是2(在我这一侧用连续的偶数作为街道地址),那么我的邻居家就是我家往前第(124-118)/2(或3)家(我和我的邻居家之间相隔两家,即120号和122号)。
指针之间的减法运算和上述方法是相同的。
在折半查找的过程中,同样会用到上述减法运算。
假设p和q指向的元素分别位于你要找的元素的前面和后面,那么(q-p)/2+p指向一个位于p和q之间的元素。
如果(q-p)/2+p位于你要找的元素之前,下一步你就可以在(q-p)/2+p和q之间查找要找的元素;反之,你可以停止查找了。
如果两个指针不是指向一个数组,它们相减就没有意义。
假设有人住在梅恩大街110号,我就不能将第五大街118号减去梅恩大街110号(并除以2),并以为这个人住在我家往回第4家中。
如果每个街区的街道地址都从一个100的倍数开始计算,并且同一条街的不同街区的地址起址各不相同,那么,你甚至不能将第五大街204号和第五大街120号相减,因为它们尽管位于同一条街,但所在的街区不同(对指针来说,就是所指向的数组不同)。
C本身无法防止非法的指针减法运算,即使其结果可能会给你的程序带来麻烦,C也不会给出任何提示或警告。
指针相减的结果是某种整类型的值,为此,ANSIC标准头文件中预定义了一个整类型ptrdiff_t。
尽管在不同的编译程序中ptrdiff_t的类型可能各不相同(int或long或其它),但它们都适当地定义了ptrdiff_t类型。
例7.7演示了指针的减法运算。
该例中有一个结构体数组,每个结构体的长度都是16字节。
如果是对指向结构体数组的指针进行减法运算,则a[0]和a[8]之间的距离为8;如果将指向结构体数组的指针强制转换成指向纯粹的内存地址的指针后再相减,则a[0]和aL8]之间的距离为128(即十六进制数0x80)。
如果将指向a[8]的指针减去8,该指针所指向的位置并不是往前移了8个字节,而是往前移了8个数组元素。
注意:
把指针强制转换成指向纯粹的内存地址的指针,通常就是转换成void*类型,但是,本例将指针强制转换成char