string内存模型浅析

std::string我们平时经常会用到,但std::string在提供便捷的使用方法外,还隐含了许多意想不到的陷阱。接下来我们就由std::string的内存模型,探讨一下std::string的使用。下文如非特别说明,用string指代标准库的std::string.

首先看第一个示例。
int main()
{
std::string str1;
std::string str2 = "abc";
printf("str1 address = %p, str1.data address = %p\n",&str1, str1.data());
printf("str2 address = %p, str2.data address = %p\n",&str2, str2.data());
return 0;
}

使用GDB查看
(gdb) info locals
str1 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80e872c ""}}
str2 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80f0004 "abc"}}</std::allocator</std::allocator

str1的_M_p是一个全局静态变量。
(gdb) info symbol 0x80e872c
std::basic_string, std::allocator >::_Rep::_S_empty_rep_storage + 12 in section .bss
而str2的_M_p保存着str2的字符串。
(gdb) x /s 0x80f0004
0x80f0004: "abc"
从上我们可以看到,string的字符串地址保存在_M_p中。当入参为空的时,_M_p的地址指向一个为0的全局静态变量。

因此,要谨慎对待string的默认构造。当你调用了string的默认构造时,千万不要对_M_p的内容进行覆写。否则其他使用默认构造生成的string字符串,会输出非空数据。
int main()
{
std::string str1;
std::string str2;
snprintf((char*)str1.c_str(), 4, "abc");
printf("str1.data = %s\n", str1.c_str());
printf("str2.data = %s\n", str2.c_str());
return 0;
}

在这里str2的打印变成了“abc”
str1.data = abc
str2.data = abc

在_M_p中我们并没有看到保存字符串长度的数据,假设运行如下示例,输出结果又是什么?
int main()
{
char *str="abc\0def";
std::string str1;
str1.assign(str, 7);
std::cout << str1 <<std::endl;

}

输出的结果是“adbdef”。原因是string在内部维护了字符串有效长度,当前内存大小和引用计数3个变量,其中引用计数后面会再做详细介绍。

我们从gdb打印这3个变量。
str1 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x815a004 "abc"}}
(gdb) p str1._M_rep()
$1 = (std::basic_string, std::allocator >::_Rep *) 0x8159ff8
(gdb) p *(std::basic_string, std::allocator >::_Rep *) 0x8159ff8
$5 = {<std::basic_string, std::allocator >::_Rep_base> = {_M_length = 7, _M_capacity = 7, _M_refcount = 0},
static _S_max_size = 1073741820, static _S_terminal = 0 '\000', static _S_empty_rep_storage = {0, 0, 0, 0}}
可以看到字符串长度_M_length为7,当前内存大小_M_capacity为7,引用计数_M_refcount为0。
内存地址从低到高排布

内存打印也佐证了上述结论。

(gdb) x /8xw 0x8159ff8
0x8159ff8: 0x00000007 0x00000007 0x00000000 0x00636261
0x815a008: 0x00666564 0x00020ff9 0x00000000 0x00000000</std::basic_string</std::allocator

前文中提到了引用计数,这个变量是为实现cope-on-wirte写时拷贝技术,运用写时拷贝可以避免对相同内容的字符串频繁拷贝而导致性能的损耗。主要原理是当两个string对象使用同一份字符串时,两个string对象将共享同一块字符串地址,只有当字符串变更时,才重新开辟一块新的内存空间。
int main()
{
std::string str1 = "abc";
std::string str2 = str1;
return 0;
}

(gdb) info locals
str1 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80f0004 "abc"}}
str2 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80f0004 "abc"}}
(gdb) p str1._M_rep()
$1 = (std::basic_string, std::allocator >::_Rep *) 0x8159ff8
(gdb) p *(std::basic_string, std::allocator >::_Rep *) 0x8159ff8
$2 = {<std::basic_string, std::allocator >::_Rep_base> = {_M_length = 3, _M_capacity = 3, _M_refcount = 1},
static _S_max_size = 1073741820, static _S_terminal = 0 '\000', static _S_empty_rep_storage = {0, 0, 0, 0}}
我们看到,str1和str2的字符串指向同一个地址。而此时_M_refcount引用计数为1,表明str1和str2的字符串共享同一个地址。</std::basic_string</std::allocator</std::allocator

此时大家再思考一个问题,如果给str1和str2赋两个相同const (char*)字符串,它们还会指向同一地址吗?
答案是不会,因为引用计数必须要知道对象的生命周期,而const(char*)的生命周期无法确定。所以只有在两个对象间的赋值、拷贝构造和assign()等操作时才会延时拷贝,共享一个字符串地址,。

接下来再给大家看一个有意思的示例
int main()
{
std::string str1 = "abc";
std::string str2 = str1;
printf("str2[0] = %c", str2[0]);

return 0;
}

大家猜一下,str2的字符串地址还会和str1一样吗?

答案是两个地址不一样。
str1 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80f0004 "abc"}}
str2 = {static npos = 4294967295, _M_dataplus = {<std::allocator> = {<__gnu_cxx::new_allocator> = {}, },
_M_p = 0x80f001c "abc"}}
原因是c++标准库并没有规定operate[]和at等操作是否是只读方式,所以在使用operate[]和at的时候,为防止用户操作字符串内容,所以要重新开辟一个新的内存空间。迭代器也是同理。</std::allocator</std::allocator

但凡事有利必有弊。string使用如此便捷,那它是线程安全的吗?其实这个问题在stackoverflow上,已经有人帮我们回答了。
答案是又不是。
首先,C++11之前并没有对标准库做线程安全的要求。其次,在string中加入线程同步策略违背了c++的编程哲学“Don't pay for things you don't use”。所以string库本身不是线程安全的。但是在逻辑上,写时拷贝又是线程安全的。因为c++0x的草案中有规定,当瞒着用户实现写时拷贝,那么必须要处理竞态条件。所以在使用引用计数的时候,一定是原子操作,而且操作顺序是先复制字符串再对引用计数增减。

综上,C++是一门非常强大的语言,要熟练的使用需要对技术细节有深刻的理解。我综合了网上大多数人的建议,提供一个供大家参考的string使用策略。
短长度的字符串可以采用const (char*)的赋值方式避免写时拷贝。虽然引用计数是原子操作,但是对于cpu的算术指令来说,这个操作还是太重量级了。与其在读共享内存时,因线程同步而等待,还不如重新申请一个新内存。
而长长度的字符串建议使用写时拷贝。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值