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






2.50/5 (3投票s)
2005年6月6日
6分钟阅读

78372

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::string
和 tstr
分配的空间比需要的多,以牺牲内存空间来换取速度)。
实现细节
以下行声明了一个堆栈分配的动态字符串,初始最大长度为 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_t
在 stdafx.h 中。 - 这里所说的一切也适用于 C 代码(对于那些仍然“被迫”使用它,或选择…的人)。
- 由于字符串是在堆栈上分配的(至少在创建时),因此你不能将其返回给调用者。如果你需要将动态字符串返回给调用者,你可以通过使用其他构造函数(即在这种情况下不要使用
tstrDecl
)来创建一个。 - 我的
tstr
类不计算引用(std::string
和CString
会)。我认为这可以毫无问题地添加。 - 我知道现在使用预处理器宏有点“不酷”…如果有人能找到一种不使用宏的好方法,我将很乐意更新这篇文章。
- 我没有在
tstr
中实现赋值运算符(以允许类似myTstr="abc"
的内容)。这是一个品味问题;我认为显式调用“set”比重新定义“=”更明确。如果你愿意,请随时添加它。 - 对于作为类数据成员(而不仅仅是局部变量)的字符串,可以通过创建一个辅助成员来保存字符缓冲区来应用相同的想法。但是,代码的可读性会降低,因为它需要使用两个不同的宏(一个用于类的声明,一个用于构造函数)。提供的源代码中包含了一个示例。如果你创建了大量的对象(无论是在堆栈上还是在堆上),这仍然可能是一个有趣的优化,可以为每个字符串数据成员节省一次堆分配。
迷你基准测试
提供的源代码(“tstrSample”)运行了一个迷你基准测试,以比较 CString
、std::string
和 tstr
的速度。它当然是为了展示 tstr
的表现优势!例如,如果使用一种经常重新分配字符串(多次增加其长度)的测试,那么 CString
的表现最好,这并不奇怪,因为我假设微软的那些人一定花了很多时间来优化 CString
的速度。我还没有优化 tstr
,即使我优化了,我也几乎没有机会做得比他们更好:)
因此,提供的测试只是声明了在堆栈上分配的 tstr
字符串,并且重新分配它们的频率很低(测试中有一个可以更改的设置,以查看它如何影响基准测试结果;查找 ReallocCondition
)。
我在这里不会给出基准测试结果,这并不重要。在“看起来像”真实生活的情况下,我认为 tstr
的速度比 CString
快 10% 到 15%。奇怪的是,std::string
常常远远落后,但我没有进一步研究原因。
我认为,重要的是这项功能可以实现在现有库中。
哲学问题:这篇文章有用吗?
我的一位哲学老师曾告诉我们
“请,请,不要试图表达你自己的想法。所有的想法很早就被表达过了,是由那些比你思考得更深入、写得更清楚的人。
堆栈分配的动态字符串现在对我来说非常有用,而且又如此“显而易见”,以至于我很难相信没有人之前想到过它。我仍然无法相信,但我在互联网上的搜索并不成功。我也问了几个朋友,也没有结果。所以,即使只是为了帮助传播这个想法,我想这篇文章也可以有用。
简而言之,如果你知道谁在我之前发现了这个东西并发表了,请给我发送一个链接,我会像往常一样更新这篇文章。