以下内容为本人的烂笔头,如需要转载,请全文无改动地复制粘贴,原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/cxpQFmuoCBUASni7Wk3rvw
何苦为难数组?
欸,处理变长字符串时,如果是从 C 语言过来的同学,第一反应就是使用字符数组来充当缓冲,上演左腾右挪,然后就会出现频繁的重新分配内存过程,其中免不了会发生异常,那么需要如何抛出和处理异常信号才能保证已分配的资源被正确释放?
假设需求是,对输入的一个字符串执行拷贝,然后基于副本修改字符串,再在副本末尾添加新字符串:
#include <iostream>
#include <cstring>
void AppendStr(const char* s1, const char* s2)
{
char* copy = new char[strlen(s1) + 1];
memset(copy, 0, strlen(s1) + 1);
strcpy(copy, s1);
// modify copy
copy[0] = 'x';
// append another string to copy
char* copy2 = new char[strlen(copy)
+ strlen(s2) + 1];
memset(copy2, 0, strlen(copy) + strlen(s2) + 1);
strcpy(copy2, copy);
strcpy(copy2 + strlen(copy), s2);
delete[] copy;
copy = copy2;
std::cout << "new string: "
<< std::string(copy) << std::endl;
delete[] copy;
}
int main()
{
AppendStr("abc", "def");
return 0;
}
示例代码中,调用函数 AppendStr() 时输入两个字符串 s1 和 s2,拷贝 s1,然后在 s1 的副本上修改内容,将第二个字符串 s2 拼接到修改后的字符串 s1 的副本末尾。
编译执行看看输出:
new string: xbcdef
鉴于数组不能自动变更长度,所以在拼接字符串时必须专门重新申请更大的数组空间,而且修改字符串也需要先拷贝到副本空间才能修改,以避免破坏原始数据,所以存在多次的资源申请,这是性能隐患。
如果期间发生异常并展开当前运行栈,那么之前申请的资源就可能被泄漏。
此类未释放资源就退出上下文的场景,如果没有动用其他保命措施,比如智能指针或者 RAII 相关的资源管理技术,就势必要在栈内添加捕捉处理异常信号,然后释放资源。
上面的代码基本不考虑发生异常的情况,如何保证资源在发生异常时被正确释放呢?
按照上面的思路,还得这样子改改:
void AppendStr(const char* s1, const char* s2)
{
char* copy = new char[strlen(s1) + 1];
memset(copy, 0, strlen(s1) + 1);
strcpy(copy, s1);
try {
// modify copy
copy[0] = 'x';
throw std::runtime_error("something error");
// append another string to copy
char* copy2 = new char[strlen(copy)
+ strlen(s2) + 1];
memset(copy2, 0, strlen(copy) + strlen(s2) + 1);
strcpy(copy2, copy);
strcpy(copy2 + strlen(copy), s2);
delete[] copy;
copy = copy2;
} catch (const std::runtime_error& e) {
delete[] copy;
throw e;
}
std::cout << "new string: "
<< std::string(copy) << std::endl;
delete[] copy;
}
int main()
{
try {
AppendStr("abc", "def");
} catch (const std::exception& e) {
std::cerr << "catch err: "
<< e.what() << std::endl;
}
return 0;
}
修改后的代码中,在第一次拷贝字符串到副本空间后,直接抛出异常信号(something error)模拟发生异常的情况。try-catch 语句块负责捕捉异常,然后在语句块中释放已经申请的资源。
目前来看,判断分支还不多,但是随着分配资源的增多,判断分支也会偏向复杂,容易出错,有没有更优雅的处理方式?
且看 std::string
熟悉 OOP 的同学在处理字符串的时候,可能也会使用标准库提供的 std::string 类,于是上面的代码可改成这样:
void AppendStr(const std::string& s1, const std::string& s2)
{
std::string copy = s1;
// modify copy
auto it = copy.begin();
*it = 'x';
// append another string to copy
copy += s2;
std::cout << "new string: "
<< copy << std::endl;
}
int main()
{
try {
AppendStr("abc", "def");
} catch (const std::exception& e) {
std::cerr << "catch err: "
<< e.what() << std::endl;
}
return 0;
}
可见,使用字符数组时多次的内存申请、函数末尾的资源清理,以及添加的异常处理代码,在改为使用 std::string 之后都不需要再保留了。
因为这些行为在 std::string 类内部自带实现,其中应用了 RAII 的模式管理资源,和利用容器技术避免频繁申请资源,更关键的是这些繁琐的过程都被封装在类内。
作为工作繁忙的开发者来说,在了解个中原理的情况下,还是应该尽量使用 std::string 代替字符数组。