免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 10465 | 回复: 11
打印 上一主题 下一主题

变长字符串的内存管理 [复制链接]

论坛徽章:
1
荣誉版主
日期:2011-11-23 16:44:17
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-07-04 14:28 |只看该作者 |倒序浏览
下面的文字是我在看一本书的时候,感觉这部分描述的不错,
所以就把它译了过来,希望能对大家有帮助。
英语不太好,所以有些地方可能会翻译的不好,大家体谅。

写一个C函数,它从一个I/O设备读取一个字符串,然后返回给调用者。字符串的长度不受限制而且不能事先取得它的长度。

这个问题表述了一个常见的编程问题,也就是,在事先不知道一个值的总长度时怎么去读取这个变长的值。这里有几个方法来解决这个问题,
它们的每一个都有自己的权衡。

方法1:静态内存

这里有一个方法来实现帮助函数:

  1. const char *
  2. get_string()
  3. {
  4.         static char buf[10000]; /* Big enough */
  5.         /* Read string inot buf... */
  6.         return buf;
  7. }
复制代码

这个方法的好处是比较简单,但是它也有很多严重的缺点。

返回的字符串可能比你期望的要长。无论你指定buf什么样的长度,它仍然可能太小。如果实际的字符串太长,你或者超出数组的范围最终导致代码悲惨的失败,或者必须擅自截断那个字符串。

对于较短的字符串,这个函数因为buf数组的大部分没有用到而浪费了内存。每一次get_string调用覆盖了前一次调用的结果。如果调用者想保留前一次调用的字符串,在再一次调用这个函数之前,他必须把上一次的字符串做一份拷贝。

这个函数不是可重入的。如果多个线程同时调用get_string,一个线程会覆盖另一个线程的结果。

方法2:静态指针指向的动态内存

这是第二种实现get_string函数的方法:

  1. const char *
  2. get_string()
  3. {
  4.         static char * result = 0;
  5.         static size_t rsize = 0;
  6.         static const size_t size_of_block = 512;
  7.         size_t rlen;
  8.         rlen = 0;
  9.         while (data_remains_to_be_read() ) {
  10.                 /* read a block of data... */
  11.                 if ( rsize - rlen < size_of_block ) {
  12.                         rsize += size_of_block;
  13.                         result = realloc(result, rsize);
  14.                 }
  15.                 /* append block of data to result... */
  16.                 rlen += size_of_block;
  17.         }
  18.         return result;
  19. }
复制代码

这个方法使用指向动态内存的一个静态指针,不断地增长缓冲区用来容纳数据。使用动态内存除去了字符串任意长度的限制,当然另一方面它
仍然有前一个方法的问题:每一次调用仍然覆盖前一次调用的结果,函数不是可重入的。这个版本也浪费了相当大数量的内存,因为它永远消
耗最坏情况下的内存数量(它曾经读取的最长字符串的长度)。

方法3:调用者分配的内存

在这个方法中,我们让调用者负责提供容纳字符串的内存。

  1. size_t
  2. get_string(char * result, size_t rsize)
  3. {
  4.         /* read at most rsize bytes into result... */
  5.         return number_of_bytes_read;
  6. }
复制代码

这个就是被UNIX操作系统的read系统调用所采纳的方法。它解决了大部分的问题。包括它是可重入的,不会内存溢出,不会擅自的截断数据。
(潜在的浪费内存的数量在调用者的控制之下。)

弊端在于如果字符串比提供的缓冲区更长,调用者必需保持调用直到所有的数据被读取。(如果我们假定数据源隐含在调用的线程中,那么多
个线程的重复调用是可重入的。)

方法4:返回指针指向的动态内存

在这个方法中,get_string动态的分配一个足够大的缓冲区来容纳结果,并返回一个指向缓冲区的指针。

  1. char *
  2. get_string()
  3. {
  4.         char * result = 0;
  5.         size_t rsize = 0;
  6.         static const size_t size_of_block = 512;
  7.         while (data_remains_to_be_read) {
  8.                 /* read a block of data... */
  9.                 rsize += size_of_block;
  10.                 result = realloc(result, rsize);
  11.                 /* append block of data to result... */
  12.         }
  13.         return result;
  14. }
复制代码

这个几乎和第二个方法一样(它们之间的区别是get_string没有使用静态数据)。它几乎解决了所有的问题:函数是可重入的,对于结果字符
串没有一个隐含的大小限制。对于长字符串并不要求多次的函数调用(但是动态分配增加了一些开销)。

这个方法的主要缺点是它让调用者负责释放分配的内存:

  1. /* ... */
  2. {
  3.         char * result;
  4.         result = get_string();
  5.         /* Use result... */
  6.         free(result);

  7.         /* ... */
  8.         result = get_string();
  9.         /* ... */
  10. }        /* Bad news, forgot to deallocate last result! */
复制代码

在这里,调用者从一个块中返回并没有释放由get_string返回的字符串。被result占用的内存从不会被收回。这种类型的重复错误注定会不可
避免的摧毁调用者。最终,调用者内存溢出并被操作系统终止运行,或者,在一个嵌入式系统,调用者可能会锁定机器。

论坛徽章:
0
2 [报告]
发表于 2006-07-04 22:21 |只看该作者
方法3和方法4稍加改动就可避免以上弊端.

对方法3, 通常的作法是调用者先传入空内存. 函数返回所需内存大小. 调用者再安需分配内存, 然后二次调用函数.

  1. size_t BufferSz=0;
  2. get_string(NULL, &BufferSz);
  3. char* Buffer = new char[BufferSz];
  4. size_t BytesRead = get_string(Buffer, BufferSz);
复制代码


方法4中, 调用者处理完缓冲区的字符串后应当再调用释放函数release_string(char*). 这样 (a) 资源的分配与释放匹配 (b) 保持各组件的独立. 函数可有多种内存分配方法, 动态分配, 静态内存, 缓存等. 调用者释放字符串后, 函数可保留这个内存, 当有多个调用时可作为缓存来提高速度.

论坛徽章:
1
荣誉版主
日期:2011-11-23 16:44:17
3 [报告]
发表于 2006-07-05 09:22 |只看该作者
原帖由 Alligator27 于 2006-7-4 22:21 发表
方法3和方法4稍加改动就可避免以上弊端.

对方法3, 通常的作法是调用者先传入空内存. 函数返回所需内存大小. 调用者再安需分配内存, 然后二次调用函数.
[code]
size_t BufferSz=0;
get_string(NULL, &Bu ...

请教一下,对你说的方法3还不是很明白。
第一次调用时get_string是怎么获得它需要的空间大小的,
比如read函数。

论坛徽章:
0
4 [报告]
发表于 2006-07-05 09:42 |只看该作者
原帖由 lenovo 于 2006-7-5 09:22 发表

请教一下,对你说的方法3还不是很明白。
第一次调用时get_string是怎么获得它需要的空间大小的,
比如read函数。

我说的是调用者与被调用函数的责任划分. 调用者应当不知道底层的细节. 比如read, 函数本身也许需要分配一块内存, 多次I/O, 再把结果传给调用者. 不管它怎么实现, 目的是减轻调用者的负担.

论坛徽章:
0
5 [报告]
发表于 2006-07-05 09:55 |只看该作者
在一本书看看过类似的讨论,好像最后倾向的是动态分配内存

论坛徽章:
1
荣誉版主
日期:2011-11-23 16:44:17
6 [报告]
发表于 2006-07-05 15:42 |只看该作者
原帖由 Alligator27 于 2006-7-5 09:42 发表

我说的是调用者与被调用函数的责任划分. 调用者应当不知道底层的细节. 比如read, 函数本身也许需要分配一块内存, 多次I/O, 再把结果传给调用者. 不管它怎么实现, 目的是减轻调用者的负担.

我明白了,不过这样会影响效率,
所以read没有这样做,
对吧?

论坛徽章:
0
7 [报告]
发表于 2006-07-05 23:24 |只看该作者
原帖由 lenovo 于 2006-7-5 15:42 发表

我明白了,不过这样会影响效率,
所以read没有这样做,
对吧?

我觉得效率是一个方面. 软件的框架也许是考虑的因素.
具体地说, read是很底层的IO函数. 它的主要目的是隐蔽更底层的硬件, 比如disk, tty, network, pipe等. 效率与逻辑交给上一层, 比如fread.
而get_string()的例子应当是架构上更上一层. 它清楚要读的内容, 从而决定用哪种内存分配. 对不变的东西, 比如configuration, locale, cpuid等, 完全可以用静态内存. 对不能确定长度的stream, 也许只有选择方法3. 对多次重复, 并有众多消费者的情况, 缓存显然是有用的.

论坛徽章:
0
8 [报告]
发表于 2006-07-05 23:44 |只看该作者
说得虚

论坛徽章:
0
9 [报告]
发表于 2008-10-11 21:47 |只看该作者
方法3需要在read完所有的数据以前read内部的那个对象一直保持着缓存直到read完,这个句法上感觉不好。(如果是某个类的 .read(),.close()方法就自然得多,可惜这里是c)
方法4感觉调用者职责不清,不太喜欢.

论坛徽章:
0
10 [报告]
发表于 2008-10-12 19:55 |只看该作者
原来牛人看的都是英文版的资料,强!
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP