65.9K
CodeProject 正在变化。 阅读更多。
Home

将动态字符串分配在堆栈上

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.50/5 (3投票s)

2005年6月6日

6分钟阅读

viewsIcon

78372

downloadIcon

288

一篇关于 C++ 中堆栈分配动态字符串的文章。

问题所在

1. 字符缓冲区不方便且易出错

每当你想在 C++(或 C)中处理一个字符字符串时,都会出现几个问题。其中一个大问题与长度有关:这个字符串可以有多长?

厌倦了使用固定大小的字符缓冲区很容易,例如

char buf[200];

这会产生无休止的问题(和错误),因为无论缓冲区有多大,字符串溢出的那一天总会到来。在那一天,如果你写得好的话,你的字符串会被截断。在许多情况下,即使你的应用程序没有崩溃,它仍然是一个错误。你可能会丢失或损坏数据,这通常更糟。

2. 动态字符串速度较慢

解决这个最大长度问题的完美方法是使用动态分配的字符串。任何可用的 C++ 库都提供了一个类来处理动态字符串。通过使用这样的类,你可以摆脱最大长度问题,并且可以获得许多附加优势:你获得了一个漂亮的面向对象的接口,比标准的 strxxx 函数更友好;你获得了一个更安全的实现,等等。

但是凡事都有代价:动态字符串速度较慢。当然,因为它们需要分配堆内存,然后稍后释放它。

结果是速度和便利性之间的困境。如果你需要快速,你可能会不愿使用动态字符串。幸运的是,由于字符串操作通常不是通用应用程序中最耗时的任务,因此选择大多数时候都很容易。

想法

在大多数情况下,你的应用程序处理的每个字符字符串都有一个合理的 maximum length,在 90% 的时间里都会遵守。但由于另外 10%(或更少)的情况,你最终会使用动态字符串,它们速度较慢,但可以处理几乎任何长度……

那么为什么不调整动态字符串,使其在构造时接受一个堆栈分配的缓冲区,然后在需要时才扩展,成为堆分配的呢?

这样你就可以拥有动态字符串,同时在 90% 的情况下节省了堆上的第一次分配

简短的代码示例

这是一个非常简单的例子。我们分配一个字符串,其长度足够容纳 25 个字符,然后为其分配常量字符串。

首先,使用固定长度的字符缓冲区(请注意,下面的行不起作用

char name[25+1]; //+1 for final zero
strcpy(name,"John Lennon"); //no problem
strcpy(name,"Blondaux Georges Jacques Babylas"); //*** bug here; string overflowed

使用常用的动态字符串,它将变成(MFC 版本)

CString name('\0',25); //string's memory is allocated in the heap (time consuming)
name="John Lennon"; //no reallocation (fast)
name="Blondaux Georges Jacques Babylas"; //reallocated (time consuming)

或者(STL 版本)

//31 character size allocated in the heap (time consuming)
std::string name(25,'\0');
//no reallocation (fast)
name="John Lennon";
//reallocated (time consuming)
name="Blondaux Georges Jacques Babylas";

现在使用我自己的“堆栈分配动态字符串”实现,称为 tstr

//declares a stack-allocated dynamic string (fast)
tstrDecl(name,25);
//no reallocation, still on the stack (fast)
name.set("John Lennon");
//reallocation; now in the heap (time consuming)
name.set("Blondaux Georges Jacques Babylas");

请注意,我们只节省了第一次分配的时间。除此之外,事情的工作方式相同(唯一的区别是 std::stringtstr 分配的空间比需要的多,以牺牲内存空间来换取速度)。

实现细节

以下行声明了一个堆栈分配的动态字符串,初始最大长度为 25

tstrDecl(name,25);

这是它在实现(tstr.h)中的工作方式

class tstr {
public:
    //constructor; uses given buffer as non-dynamic buffer
    tstr(char* buf,size_t tlenofbuf);

    ...
};

// declaration of a local tstr that uses a stack allocated
// buffer (fast first allocation, dynamic increase possible)
#define tstrDecl(str,len) char str##_tbuf[(len)+_tstr_overx]; 
        tstr str(str##_tbuf,sizeof(str##_tbuf)/sizeof(char));

该宏声明一个辅助字符缓冲区(在堆栈上预留一些空间)。然后,它声明字符串对象,将其地址传递给构造函数。当然,你不需要了解这些就可以使用字符串。

有关更多详细信息,请查看提供的源代码。

注释/缺点

  • 这里所说的一切都适用于 Unicode 字符字符串。例如,人们可以定义使用 wchar_t 而不是 char 的字符串。提供的源代码在定义 _UNICODE 时可以使用 wchar_tstdafx.h 中。
  • 这里所说的一切也适用于 C 代码(对于那些仍然“被迫”使用它,或选择…的人)。
  • 由于字符串是在堆栈上分配的(至少在创建时),因此你不能将其返回给调用者。如果你需要将动态字符串返回给调用者,你可以通过使用其他构造函数(即在这种情况下不要使用 tstrDecl)来创建一个。
  • 我的 tstr 类不计算引用(std::stringCString 会)。我认为这可以毫无问题地添加。
  • 我知道现在使用预处理器宏有点“不酷”…如果有人能找到一种不使用宏的好方法,我将很乐意更新这篇文章。
  • 我没有在 tstr 中实现赋值运算符(以允许类似 myTstr="abc" 的内容)。这是一个品味问题;我认为显式调用“set”比重新定义“=”更明确。如果你愿意,请随时添加它。
  • 对于作为类数据成员(而不仅仅是局部变量)的字符串,可以通过创建一个辅助成员来保存字符缓冲区来应用相同的想法。但是,代码的可读性会降低,因为它需要使用两个不同的宏(一个用于类的声明,一个用于构造函数)。提供的源代码中包含了一个示例。如果你创建了大量的对象(无论是在堆栈上还是在堆上),这仍然可能是一个有趣的优化,可以为每个字符串数据成员节省一次堆分配。

迷你基准测试

提供的源代码(“tstrSample”)运行了一个迷你基准测试,以比较 CStringstd::stringtstr 的速度。它当然是为了展示 tstr 的表现优势!例如,如果使用一种经常重新分配字符串(多次增加其长度)的测试,那么 CString 的表现最好,这并不奇怪,因为我假设微软的那些人一定花了很多时间来优化 CString 的速度。我还没有优化 tstr,即使我优化了,我也几乎没有机会做得比他们更好:)

因此,提供的测试只是声明了在堆栈上分配的 tstr 字符串,并且重新分配它们的频率很低(测试中有一个可以更改的设置,以查看它如何影响基准测试结果;查找 ReallocCondition)。

我在这里不会给出基准测试结果,这并不重要。在“看起来像”真实生活的情况下,我认为 tstr 的速度比 CString 快 10% 到 15%。奇怪的是,std::string 常常远远落后,但我没有进一步研究原因。

我认为,重要的是这项功能可以实现在现有库中。

哲学问题:这篇文章有用吗?

我的一位哲学老师曾告诉我们

“请,请,不要试图表达你自己的想法。所有的想法很早就被表达过了,是由那些比你思考得更深入、写得更清楚的人。

堆栈分配的动态字符串现在对我来说非常有用,而且又如此“显而易见”,以至于我很难相信没有人之前想到过它。我仍然无法相信,但我在互联网上的搜索并不成功。我也问了几个朋友,也没有结果。所以,即使只是为了帮助传播这个想法,我想这篇文章也可以有用。

简而言之,如果你知道谁在我之前发现了这个东西并发表了,请给我发送一个链接,我会像往常一样更新这篇文章。

© . All rights reserved.