⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 第7章 指针和内存分配.txt

📁 我自己整理的c语言教程 来自 c语言之家
💻 TXT
📖 第 1 页 / 共 3 页
字号:
C语言编程常见问题解答  
 

 
发表日期:2003年9月30日      已经有2414位读者读过此文 
 


                                 第7章  指针和内存分配

    指针为C语言编程提供了强大的支持——如果你能正确而灵活地利用指针,你就可以直接切入问题的核心,或者将程序分割成一个个片断。一个很好地利用了指针的程序会非常高效、简洁和精致。
    利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild”pointer),即指向一个错误位置的指针,你的数据就危险了——存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。
    此后可能发生的事情取决于这样两点:第一,内存中的数据被破坏的程度有多大;第二,内存中的被破坏的部分还要被使用多少次。在有些情况下,一些函数(可能是内存分配函数、自定义函数或标准库函数)将立即(也可能稍晚一点)无法正常工作。在另外一些情况下,程序可能会终止运行并报告一条出错消息;或者程序可能会挂起;或者程序可能会陷入死循环;或者程序可能会产生错误的结果;或者程序看上去仍在正常运行,因为程序没有遭到本质的破坏。
    值得注意的是,即使程序中已经发生了根本性的错误,程序有可能还会运行很长一段时间,然后才有明显的失常表现;或者,在调试时,程序的运行完全正常,只有在用户使用时,它才会失常。    
    在C语言程序中,任何野指针或越界的数组下标(out-of-bounds array subscript)都可能使系统崩溃。两次释放内存的操作也会导致这种结果。你可能见过一些C程序员编写的程序中有严重的错误,现在你能知道其中的部分原因了。
    有些内存分配工具能帮助你发现内存分配中存在的问题,例如漏洞(leak,见7.21),两次释放一个指针,野指针,越界下标,等等。但这些工具都是不通用的,它们只能在特定的操作系统中使用,甚至只能在特定版本的编译程序中使用。如果你找到了这样一种工具,最好试试看能不能用,因为它能为你节省许多时间,并能提高你的软件的质量。
    指针的算术运算是C语言(以及它的衍生体,例如C++)独有的功能。汇编语言允许你对地址进行运算,但这种运算不涉及数据类型。大多数高级语言根本就不允许你对指针进行任何操作,你只能看一看指针指向哪里。
    C指针的算术运算类似于街道地址的运算。假设你生活在一个城市中,那里的每一个街区的所有街道都有地址。街道的一侧用连续的偶数作为地址,另一侧用连续的奇数作为地址。如果你想知道River Rd.街道158号北边第5家的地址,你不会把158和5相加,去找163号;你会先将5(你要往前数5家)乘以2(每家之间的地址间距),再和158相加,去找River Rd.街道的168号。同样,如果一个指针指向地址158(十进制数)中的一个两字节短整型值,将该指针加3=5,结
    果将是一个指向地址168(十进制数)中的短整型值的指针(见7.7和7.8中对指针加减运算的详细描述)。    
    街道地址的运算只能在一个特定的街区中进行,同样,指针的算术运算也只能在一个特定的数组中进行。实际上,这并不是一种限制,因为指针的算术运算只有在一个特定的数组中进行才有意义。对指针的算术运算来说,一个数组并不必须是一个数组变量,例如函数malloc()或calloc()的返回值是一个指针,它指向一个在堆中申请到的数组。
    指针的说明看起来有些使人感到费解,请看下例:
    char *p;    
    上例中的说明表示,p是一个字符。符号“*”是指针运算符,也称间接引用运算符。当程序间接引用一个指针时,实际上是引用指针所指向的数据。    
    在大多数计算机中,指针只有一种,但在有些计算机中,指向数据和指向函数的指针可以是不同的,或者指向字节(如char。指针和void *指针)和指向字的指针可以是不同的。这一点对sizeof运算符没有什么影响。但是,有些C程序或程序员认为任何指针都会被存为一个int型的值,或者至少会被存为一个long型的值,这就无法保证了,尤其是在IBM PC兼容机上。

    注意:以下讨论与Macintosh或UNIX程序员无关;

    最初的IBM PC兼容机使用的处理器无法有效地处理超过16位的指针(人们对这种结论仍有争议。16位指针是偏移量,见9.3中对基地址和偏移量的讨论)。尽管最初的IBM PC机最终也能使用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 <stdio.h>
int
main()
{
    int i;
    int  * p ;
    i = 5;
    p = & i;         / *  now  * p = = i  * /
    / *   %Pis described in FAQ VII. 28 * /
    printf("i=%d, p=%P,   * p= %d\n" , i, P,  *p); 
    * p = 6;        / *  same as i = 6  * /
    printf("i=%d, p=%P,   * p= %d\n" , i, P,  *P);
    return 0;       / *  see FAQ XVI. 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一个有无限层间接引用的循环链表

/ *  Would run forever if you didn't limit it to MAX  * /
# include <stdio. h> 
struct circ_list
{
    char        value[ 3 ];         /*  e.g.,"st" (incl '\0')  */
    struct circ_list    *  next;
}; 
struct circ_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  * / 
};
# define  MAX  20

main()
{
    int i = 0;
    struct circ_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是在<stddef.h>中定义的一个宏,它的值和任何有效指针的值都不同。NULL是一个纯粹的零,它可能会被强制转换成void*或char*类型。即NULL可能是0,0L或(void*)0等。有些程序员,尤其是C++程序员,更喜欢用0来代替NULL。
    指针的值不能是整型值,但空指针是个例外,即空指针的值可以是一个纯粹的零(空指针的值并不必须是一个纯粹的零,但这个值是唯一有用的值。在编译时产生的任意一个表达式,只要它是零,就可以作为空指针的值。在程序运行时,最好不要出现一个为零的整型变量)。

   注意:空指针并不一定会被存为零,见7.10。
    警告:绝对不能间接引用一个空指针,否则,你的程序可能会得到毫无意义的结果,或者得到一个全部是零的值,或者会突然停止运行。

    请参见:
    7.4  什么时候使用空指针?
    7.10 NULL总是等于0吗?    
    7.24 为什么不能给空指针赋值? 什么是总线错误、内存错误和内存信息转储?

    7.4  什么时候使用空指针?   
    空指针有以下三种用法:
    (1)用空指针终止对递归数据结构的间接引用。
    递归是指一个事物由这个事物本身来定义。请看下例:
    /*Dumb implementation;should use a loop */
    unsigned factorial(unsinged i)
    {
        if(i=0 || i==1)
        {
           return 1;
        }
        else
        {
            return i * factorial(i-1);
        }
    }

    在上例中,阶乘函数factoriai()调用了它本身,因此,它是递归的。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -