经典c 教程(ch7 指针和内存分配---下)

来源:互联网 发布:ug nx 8.0编程电子书 编辑:程序博客网 时间:2024/05/22 04:52

715  数组的大小可以在程序运行时定义吗?
   
不。在数组的定义中,数组的大小必须是编译时可知的,不能是在程序运行时才可知的。例如,假设i是一个变量,你就不能用i去定义一个数组的大小:
    char   array[i];    /*(notvalidc */
   
有些语言支持这种定义,但C语言不支持。如果C语言支持这种定义,栈就会变得更复杂,调用函数的开销就会更大,而程序的运行速度就会明显变慢。
   
如果数组的大小在编译时是可知的,即使它是一个非常复杂的表达式,只要它在编译时能被计算出来,你就可以定义它。
   
如果你要使用一个在程序运行时才知道其大小的数组,你可以说明一个指针,并且调用malloc()calloc()函数从堆中为这个数组分配内存空间。以下是一个拷贝传给main()函数的argv数组的例子:   

7.15 在动行时确定大小的数组,使用了指针和
malloc()
/*
A silly program that copies the argv array and all the pointed-to
strings. Just for fun, it also deallocates all the copies.
*/
# include
# include

int
main (int argc, char* * argv)
{
    char* * new_argv;
    int i;
    /*
    Since argv[0] through argv [argc] are all valid, the
    program needs to allocate room for argc + 1 pointers.
    */
    new_argv = (char* * ) calloc(argc + l, sizeof (char * ));
    / * or malloc ((argc +1) * sizeof (char * ) ) * /
    printf ("allocated room for %d pointers starting at %P\n", argc + 1, new_argv);
    /*
    now copy all the strings themselves
    (argv[0] through argv[argc-l])
    */
    for (i = 0;i
    {
        / * make room for '\0' at end, too * /
        new_argv [i]= (char* ) malloc(strlen(argv[i]) + l);
        strcpy(new_argv[i], argv[i]);
        printf ("allocated %d bytes for new_argv[%d] at %P",
                    "copied\"%s\"\n",
                    strlen(argv[i]) + l, i, new_argv[i], new_argv[i]) ;
    }
    new_ argv [argc] = NULL:
    /*
    To deallocate everything, get rid of the strings (in any
    order), then the array of pointers. If you free the array
    of poiners first, you lose all reference to the copied
    strings.
    */
    for (i = 0;i
        free(new_argv[i]);
        printf ("freed new_argv[%d] at %P\n" , i, new_argv[i]) ;
        argv[i]=NULL; /*
习惯,见本例后面的注意
*/
    }
     free(new_argv)
    
     printf("freed new_argv itself at %P\n"
new_argv)

     return 0
  /*请参见164 */
}   
   
注意:为什么例75
在释放了new_argv数组中的每个元素之后,还要将这些元素赋值为NULL?这是一种在长期实践的基础上形成的习惯。在释放了一个指针之后,你就无法再使用它原来所指向的数据了,或者说,该指针被悬挂起来了,它不再指向任何有用的数据。如果在释放一个指针之后立即将它赋值为NULL,那么,即使程序再次使用该指针,程序也不会出错。当然,程序可能会间接引用这个空指针,但这种错误在调试程序时就能及时被发现。此外,
 
程序中可能仍然有一些该指针原来的拷贝,它们仍然指向已被释放的那部分内存空间,这种情况在C程序中是很自然的。总之,尽管上述这种习惯并不能解决所有问题,但确实有作用。

    请参见:
    7
16malloc()函数更好还是用calloc()函数更好?
    7
20 什么是栈
(stack)?
    7
21 什么是堆
(heap)?
    7
22 两次释放一个指针会导致什么结果
?   
    9
为什么用const说明的常量不能用来定义一个数组的初始大小
?
      
   
7
16 malloc()函数更好还是用calloc()函数更好?       
   
函数malloc()calloc()都可以用来分配动态内存空间,但两者稍有区别。
   
    malloc()
函数有一个参数,即要分配的内存空间的大小:
   
    Void *malloc(size_t size);   
    calloc()
函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小:
   
    void  *calloc(size_t numElements
size_t sizeOfElement)

   
如果调用成功,函数malloc()calloc()都将返回所分配的内存空间的首地址。
    malloc()
函数和calloc()函数的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存空间曾经被分配、释放和重新分配,则其中可能遗留各种各样的数据。也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常运行,但经过一段时间后(内存空间已被重新分配)可能会出现问题。
    calloc()
函数会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为零;如果你是为指针类型的元素分配内存,那么这些元素通常(但无法保证)会被初始化为空指针;如果你是为实数类型的元素分配内存,那么这些元素可能(只在某些计算机中)会被初始化为浮点型的零。
    malloc()
函数和calloc()函数的另一点区别是calloc()函数会返回一个由某种对象组成的数组,但malloc()函数只返回一个对象。为了明确是为一个数组分配内存空间,有些程序员会选用calloc()函数。但是,除了是否初始化所分配的内存空间这一点之外,绝大多数程序员认
为以下两种函数调用方式没有区别:

    calloc(numElements
sizeOfElement)
    malloc(numElements *sizeOfElement)

   
需要解释的一点是,理论上(按照ANSIC标准)指针的算术运算只能在一个指定的数组中进行,但是在实践中,即使C编译程序或翻译器遵循这种规定,许多C程序还是冲破了这种限制。因此,尽管malloc()函数并不能返回一个数组,它所分配的内存空间仍然能供一个数组使用(realloc()函数来说同样如此,尽管它也不能返回一个数组)
   
总之,当你在calloc()函数和malloc()函数之间作选择时,你只需考虑是否要初始化所分配的内存空间,而不用考虑函数是否能返回一个数组。

    请参见:
    7
两个指针可以相减吗?为什么?
    7
把一个值加到指针上意味着什么
?   
    7
10 NULL总是等于0?

    717  怎样说明一个大于64KB的数组?
   
保守的回答是,为了程序的可移植性,你不能说明这样一个数组。ANSIISOC标准规定编译程序能处理的单个对象的大小不得超过(32KB-1)个字节。
   
为什么64KB是一种上限呢?因为16位指针的最大寻址空间是64KB
   
在有些环境中,你可以直接说明一个大于64KB的数组,这种说明是有效的,不会引起任何问题;在另外一些环境中,你不能说明这样大的一个数组,但你可以调用函数malloc()calloc()从堆中分配一块这样大的内存空间。
   
IBMPC兼容机上,这种限制更严格。在这种情况下,你至少要使用大数据存储模式(见本章开头部分的介绍)。此外,你还要调用函数malloc()calloc()"far"变体来分配内存空间。例如,为了分配一块70000字节的缓冲区,在BorlandCBorlandC++中,你可以使用以下函数调用形式:   
    far char *buffer
farmalloc(70000L)

   
MicrosoftCMicrosoftC++中,你可以使用以下函数调用形式:
    far char *fbuffer=fmalloc(70000L)

   
注意:70000L尾部的L用来指明该常量是一个long int型常量。一个int型常量的长度为16位,其中还包括一位符号位,因此存不下70000这个值。
   
请参见:
    7
18 farnear之间有什么区别?  
    7
21 什么是堆
(heap)?
    9
3为什么要小心对待位于数组后面的那些元素的地址呢
?

  
7
18  farnear之间有什么区别?
    在本章的开头部分曾经介绍过,基于IBMPC兼容机的一些编译程序使用两种指针,这两种指针就是此处要讨论的far指针和near指针。near指针的长度为16位,可寻址空间为64KBfar指针的长度为32位,可寻址空间为1MBnear指针可在长度为64KB的段中工作,这种段有两种,一种供函数地址使用,即程序段;一种供数据地址使用,即数据段。
   far指针含一个16位的基地址(即段地址)和一个16位的偏移量,将基地址乘以16(16进制下右移一位)后再与偏移量相加,即可得·far指针所指向的地址,因此far指针的实际长度是20位。例如,如果一个far指针的段地址为0x7000,偏移量为0x1224,则该指针指向地址0x71224;如果一个far指针的段地址为0x7122,偏移量为0x0004。则该指针也指向地址0x71224
   
在编译你的程序之前,你必须指示编译程序使用哪种存储模式。如果使用小代码存储模式,指向函数地址的指针将被默认为near指针,也就是说,所有的函数都要在同64KB的段中;如果使用大代码存储模式,指向函数地址的指针将被默认为far指针。同样,对小和大数据存储模式来说,指向数据地址的指针也将分别被默认为near指针和far指针。除了默认的near指针或far指针之外,你仍然可以明确地将指向函数或数据的指针说明为near指针或far指针。   
    far
指针工作起来相对慢一些,因为每次访问一个{ar指针时,都要将数据段或程序段的段寄存器中的数据交换出来。far指针的算术运算和比较也有些反常,例如,前文例子中提到的两个far指针指向同一个地址,但两者比较的结果却不相同。如果你的程序使用了小数据和小代码存储模式,那么它运行起来就相当快;否则,它的运行速度就会明显变慢。

   
除了上述内容之处,你的编译程序对near指针和far指针往往还有一些细节性的规定,请阅读编译程序文档,以了解更详细的信息。

    请参见:
    7
19什么时候使用far指针?

    719  什么时候使用far指针?
   
当使用小代码或小数据存储模式时,也许程序的大部分函数或数据能装入同一个程序段或数据段中,但仍有一部分函数或数据无法装入,这时,你就要为这些剩余的函数或数据另外分配内存空间,并显式说明相应的far函数或far指针来使用这部分内存空间。在使用小代码存储模式时绝大多数函数被限制在64KB的程序内,但far函数可以使用64KB的程序段外的内存空间(许多C库函数都被显式说明为far函数,因此无论程序使用哪种类型的代码存储模式,这些函数都能正常工作)。同样far指针能使用64KB的数据段外的内存空间,并且通常都和farmalloc()这样的函数一起使用,以管理64KB的数据段以外的堆。

    请参见;   
    7
18farnear之间有什么区别
?
    7
21什么是堆
(heap)?
  
   
7
20 什么是栈(stack)?
   
栈是存放函数的所有动态局部变量及函数调用和返回的有关信息的一块内存。调试程序提供了一种功能,能够显示一个已被调用的函数的列表,这是调试程序时一种很有用的手段。
   
栈的内存管理严格遵循先进后出的顺序,即释放栈中对象所占内存时的顺序刚好与给这些对象分配栈中内存时的顺序相反,这一点正是实现函数调用所需要的。从栈中分配内存效率特别高,C编译程序能产生如此优质的代码的原因之一,就是充分利用了栈。
   
较早版本的C语言曾提供这样一个函数,程序员可以用它从栈中分配内存,而该函数返回时会自动释放所分配的内存。这种函数调用是非常危险的,因此版本较新的C语言中不再提供这个函数。

    请参见:
    7
15 数组的大小可以在程序运行时定义吗?
    7
2l 什么是堆(heap)?

    721 什么是堆(heap)?
   
堆是供malloc()calloc()realloc()等函数获取内存空间的一块内存。
   
从堆中获取内存比从栈中获取内存要慢得多,但是堆的内存管理却比栈灵活得多,任何时候你都可以从堆中获取内存,而且在释放堆中对象所占的内存时,你可以按任意顺序进行。数据对象使用栈中内存(如动态局部变量)比使用堆中内存会使程序运行更快,但是有时使用堆中内存可以改善一种算法,例如使它更快,或者鲁棒性(强壮性)更好,或者更灵活。因此,使用堆还是栈需要折衷考虑。
   用来存放递归数据结构的内存几乎都要从堆中获取。用来存放字符串的内存通常也从堆中获取,尤其是对那些在程序运行时可能出现的很长的字符串。
   
从堆中获取的内存不会被自动释放,因此你必须调用free()函数释放这种内存。如果从堆中获取的内存在使用完后没有被释放,这部分内存在程序结束之前会一直被占用,这种情况被称为内存漏洞。如果内存漏洞发生在一个循环中,你很快就会用光堆中的内存(此时内存分配函数会返回一个空指针)。在有些环境中,如果一个程序没有将它所用过的内存释放掉,这部分内存在该程序结束之后仍然会一直被占用。
    注意:在调试程序时很难发现内存漏洞,但可以借助内存分配工具来发现它。   
   有些编程语言不支持用户程序释放堆中的内存,而是采用了一种自动内存垃圾的方式。这种方式会导致一些很严重的性能问题,并且实现起来也要困难得多,程序的开销往往也更多。C语言也有一些自动内存垃圾的函数,但用这些函数来管理内存是非常危险的。这是一个涉及到设计编译程序的问题,此处不再详细讨论。

    请参见:
    7
4什么时候使用空指针?
    7
20什么是栈
(stack)?

   
7
22  两次释放一个指针会导致什么结果?
   
如果你释放了一个指针,又重新给它分配内存,然后再次释放它,这样做是安全的。
   
注意;准确地说,当我们说释放一个指针时,实际上指的是释放该指针所指向的内存,而不是指针本身,因为指针本身不会因此而有任何变化。然而,C程序员通常都把释放一个指针所指向的内存简短地说成是释放一个指针。
   
当你释放了一个指针后,所释放的内存可能刚好会被重新分配给另外一个指针,在这种情况下,你就可以再次使用原来那个指针,并且可以再次释放这个指针,当然,这种情况极其罕见。请看下例:
    
# include

int
main(int argc, char* * argv)
{
    char* * new_argv1;
    char* * new_argv2;
    new_argvl = calloc(argc +1, sizeofCchar * )) ;
    free(new-argvl) ; /* free once */ .
    new_argv 2 = (char* * ) calloc (argc+1, sizeof(char*));
    if (new_argvl == new_argv2) {
        /*
        new-argvl accidentally points to freeable mem
        */
        free(new_argvl) ; /* freed twice */
    } else {
        free(new_argv2);
    }
    new_argvl = calloc(argc + l, sizeof(char*));
    free(new_argvl) ; /* freed once again */
    return ();
}

 
   
上例没有什么实际意义,它只用来说明在什么样的情况下两次释放一个指针是安全的。在上例中,第一次给指针new_argv1分配一块能拷贝argv数组的内存后,立即将其释放,然后将同样大小的一块内存分配给指针new_argv2。因为此时第一块内存已经是空闲的,所以calloc()函数可能会刚好将第一块内存分配给指针Hew_argv2。在这种情况下;new_argvlnew_argv2将指向同一块内存,它们在程序中是等价的。因此,在if语句的第一个子句中,free()函数再次释放了new_argvl,其作用和释放new_argv2是相同的。上例说明,在合法的前提下,一个指针可以被释放任意多次。
   
然而,如果你释放了一个指针,在没有重新给它分配内存之前再次释放它,会导致什么结果呢?请看下例: 

  void caller(
...
)
    {
        void *p

        /* 
...  */
        callee(p)

        free(p)

    }
    void callee(void *p)
    {
        /* 
...  */
        free(p)'
        return;
    }

   
在上例中,caller()函数将指针p传递给callee()函数后就释放了p,而callee()函数也释放了p,这样,p所指向的内存就被连续释放了两次。ANSIC
标准不知道怎样处理这种操作方式,因此这会导致难以预料并且通常都是破坏性的结果。
    在编写内存分配和释放函数时,可以让它们检查哪些内存正在被使用或已经被释放,但为了使这些函数工作起来更快,通常都没有这样做。当用free()函数释放一个指针时,它总是认为该指针所指向的内存是由malloc()函数或calloc()函数分配的,并且分配之后还从没有被释放过。free()函数将计算这块内存的大小,并且更新其中的数据结构,不管这块内存是否已经被释放过。如果这块内存已经被释放过,那么free()函数就很可能会将某些信息写到一个错误的位置上去,从而使你的程序中出现一个野指针,从此你的程序就陷入困境了(请参见本章开头部分的介绍)
   
为了有效地防止两次释放一个指针,首先要仔细编写程序,其次要利用一些内存分配工具来检查程序中是否存在这种错误。

    请参见:
    7
21 什么是堆(heap)?
    7
24 为什么不能给空指针赋值?什么是总线错误、内存错误和内存信息转储
?
    7
26 free()函数是怎样知道要释放的内存块的大小的?

    723 NULLNUL有什么不同?
    NULL
是在h>头文件中专门为空指针定义的一个宏。NULASCII字符集中第一个字符的名称,它对应于一个零值。C语言中没有NUL这样的预定义宏,但有些程序员喜欢定义这样一个宏。
   
注意;ASCII字符集中,数字0对应于十进制值80,不要把数字0'\0'(NUL)的值混同起来。
    NULL可以被定义为(void *)0,而NUL可以被定义为'\0'NULLNUL都可以被简单地定义为0,这时它们是等价的,可以互换使用,但这是一种不可取的方式。为了使程序读起来更清晰,维护起来更容易,你在程序中应该明确地将NULL定义为指针类型,而将NUL
 
义为字符类型。
   
请参见:
    7
3什么是空指针?

    724  为什么不能给空指针赋值?什么是总线错误、内存错误和内存信息转储?
   
这些都是程序中存在野指针或越界下标等严重错误时系统所报告的出错消息。
    “null pointer assignment”
是一个MS-DOS程序执行完毕后可能报告的一条出错消息。有些MS-DOS程序会分配一小块内存给空指针,让空指针也能指向内存中的某个地址,如果这些程序试图往这块内存中写入数据,就会覆盖掉原来由编译程序写在其中的数据。当程序执行完毕时,由编译程序生成的一部分代码会检查这块内存中的数据,如果发现原来的数据被修改了,就会报告"null’pointerassignment”这条消息。这条消息告诉你程序中存在野指针或越
 
界下标,但你无法判断究竟错误出在程序中的哪一部分。有些调试程序和编译程序会提供一些支持,帮助你找出错误。   
    “Bus error
core dumped”"Memory faultcore dumped”是运行在UNIX下的程序可能报告的两条出错消息,它们往往出现在程序进行读或写的过程中,都说明程序中存在野指针或越界下标,但所涉及的问题不仅仅局限于空指针。这两条出错消息对程序员更友好,因为“core dumped”这段消息告诉程序员已经将一个名为"core”的文件写入当前目录中,该文件记录了程序出错时栈和堆中的所有信息。借助于调试程序,程序员可以通过“core”文件找出程序

 
中的野指针或越界下标。尽管这种方式并不告诉程序员出现野指针或越界下标的原因,但它能有效地帮助程序员解决问题。需要注意的是,如果程序未获得写当前目录的许可,就无法产生“core”文件,也就不会报告"core dumped”这段消息。
   
注意;“core”是指磁芯,因为运行第一代UNIX系统的硬件使用磁芯而不是硅片作为随机存取存贮器。
   
能帮助你发现内存分配错误的工具,有时同样能帮助你找出程序中的野指针和越界下标。
 
在最好的情况下,这些工具几乎能找出所有这类错误。

    请参见:
    7
3 什么是空指针?

    725  怎样确定一块已分配的内存的大小?
    实际上你无法确定。free()函数能做到这一点,但你的程序无法知道free()函数所用的技巧。尽管通过反汇编库函数你可能会发现这种技巧,但是在不同版本的编译程序中,这种技巧可能会有所不同,因此这样做没有什么意义?

    请参见:
    7

来源:http://www.eefocus.com/andysun001/blog/10-06/192020_ae600.html#articletop
原创粉丝点击