
2.4.3 动态分配函数
第二种内存管理模型(由被调用者分配,由调用者释放)是由ISO/IEC TR 24731-2定义的动态分配函数实现。ISO/IEC TR 24731-2定义了许多标准C字符串处理函数的替代品,这些替代品使用动态分配的内存,以确保不会发生缓冲区溢出。因为使用这样的函数需要引入随后的释放缓冲区的额外调用,所以这些函数更适用于新的开发,而不是改造现有代码。
在一般情况下,因为在ISO/IEC TR 24731-2中描述的函数,总是自动调整缓冲区大小以容纳所需的数据,所以这些函数更好地确保了不会发生缓冲区溢出问题。但是,使用动态内存分配的应用程序,可能会遭受拒绝服务攻击,因为其中的数据会一直存在,直到内存耗尽。它们也更容易出现动态内存管理错误,这也可能导致安全漏洞。
例2.1可以使用动态分配函数实现,如例2.7中所示。
例2.7 使用函数getline()从stdin中读入数据
01 #define __STDC_WANT_LIB_EXT2__ 1 02 #include <stdio.h> 03 #include <stdlib.h> 04 05 void get_y_or_n(void) { 06 char *response = NULL; 07 size_t len; 08 09 puts("Continue? [y] n: "); 10 if ((getline(&response, &len, stdin) < 0) || 11 (len && response[0] == 'n')) { 12 free(response); 13 exit(0); 14 } 15 free(response); 16 }
此程序对于任何输入都具有已定义的行为,包括一个假定,即假定一个非常长的、需要耗尽所有可用内存才能容纳的行,应被视为一个“no”回应。因为对getline()函数动态地分配response缓冲区,所以程序必须调用free()来释放已分配的内存。
ISO/IEC TR 24731-2允许在不相应地打开文件的情况下定义流。这种类型的流从内存缓冲区取得输入或把输出写入到内存缓冲区。例如,GNU C库使用这些流来实现sprintf()和sscanf()函数。
与内存缓冲区相关的流和与外部文件关联的文本文件流,具有相同的操作。此外,数据流的方向也是用完全相同的方式确定的。
你可以明确地使用fmemopen()、open_memstream()或open_wmemstream()函数创建一个字符串流。这些函数允许你对字符串或内存缓冲区执行I/O操作。fmemopen()和open_memstream()函数在<stdio.h>中被声明,如下所示。
1 FILE *fmemopen( 2 void * restrict buf, size_t size, const char * restrict mode 3 ); 4 FILE *open_memstream( 5 char ** restrict bufp, size_t * restrict sizep 6 );
open_wmemstream()函数是在<wchar.h>中定义的,并具有以下签名:
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
fmemopen()函数打开一个流,使你可以读取或写入指定的缓冲区。open_memstream()函数打开一个面向字节的流来写入一个缓冲区,而open_wmemstream()函数创建一个面向宽字符的流。当用fclose()关闭流或用fflush()刷新流时,bufp和sizep被更新,以包含缓冲区的指针及其大小。只要没有进一步的输出流发生,这些值仍然有效。如果执行额外的输出,必须再次刷新流来存储新的值,才能再次使用它们。一个空字符被写入缓冲区的末尾,但它存储在sizep中的size值中不包括它。
通过调用fmemopen()、open_memstream()或open_wmemstream()创建的一个与内存缓冲区相关联的流的输入和输出操作,发生在内存缓冲区的范围内,受限于实现。对于用open_memstream()或open_wmemstream()打开的流的情况,内存区域动态增长,以适应必要的写操作。对于输出,在刷新或关闭操作期间,数据从函数setvbuf()提供的缓冲区移动到内存流。如果没有足够的内存来增长内存区域,或者操作需要访问相关内存区域以外的地方,相关的操作失败。
例2.8中的程序在第6行打开一个流来写入到内存。
例2.8 打开一个流来写入内存
01 #include <stdio.h> 02 03 int main(void) { 04 char *buf; 05 size_t size; 06 FILE *stream; 07 08 stream = open_memstream(&buf, &size); 09 if (stream == NULL) { /* handle error */ }; 10 fprintf(stream, "hello"); 11 fflush(stream); 12 printf("buf = '%s', size = %zu\n", buf, size); 13 fprintf(stream, ", world"); 14 fclose(stream); 15 printf("buf = '%s', size = %zu\n", buf, size); 16 free(buf); 17 return 0; 18 }
在第10行把字符串“hello”写入到流,并且在第11行刷新该流。fflush()的调用更新buf和size,以便第12行的printf()函数输出:
buf = 'hello', size = 5
在第13行把字符串".world"写入流后,在第14行关闭流。关闭流的同时也更新buf和size,以便第15行的printf()函数输出:
buf = 'hello, world', size = 12
size是缓冲区的累计(总数)大小。open_memstream()函数提供了一个更安全的写入内存机制,因为它采用了根据需要动态分配内存的方法。但是,它确实要求调用者来释放分配的内存,如例子的第16行所示。
在安全关键的系统中,往往是不允许动态分配的。例如,MISRA标准要求,“不得使用动态堆内存分配”[MISRA 2005]。一些安全关键系统在初始化过程中可以利用动态内存分配,但在操作过程中不允许。例如,航空电子软件在初始化飞机时可以动态地分配内存,但在飞行过程中不允许。
动态分配函数从广泛应用的现有实现中取得,许多这类函数都包含在POSIX中。