测试如下代码:
#include
#include
int main() {
char str[] = "String!\0 This is a string too!";
std::string sss(str);
std::cout << "str:" << sss << std::endl
<< "size:" << sss.size() << std::endl
<< "capacity:" << sss.capacity() << std::endl;
std::cout << "c_str len:" << strlen(sss.c_str())
<< "\ndata() len:" << strlen(sss.data());
return 0;
}
输出结果:
str:String!
size:7
capacity:15
c_str len:7
data() len:7
思考:
str.c_str()
和str.data()
返回值带有字符串结束符,为什么?第一个问题与 ‘\0’ 关系不大,但是有助于让我更深层的理解std::string 对象。下面就从这三个问题开始,通过源码的角度,找到问题答案。本文阅读源码版本为 gcc9.2。并且只看最新版本c++11以上的版本(带有宏_GLIBCXX_BEGIN_NAMESPACE_CXX11)的代码。
头文件定义在stringfwd.h,如下:
// stringfwd.h 文件
......
template,
typename _Alloc = allocator<_CharT> >
class basic_string;
......
typedef basic_string string;
可以看到,string就是basic_string,继续找到basic_string模板类,basic_string.h文件:
// basic_string.h文件
......
template
class basic_string {
......
struct _Alloc_hider : allocator_type // TODO check __is_final
{
_Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
: allocator_type(__a), _M_p(__dat) {}
pointer _M_p; // 实际的数据指针.
};
_Alloc_hider _M_dataplus;
size_type _M_string_length; // 数据长度
enum { _S_local_capacity = 15 / sizeof(_CharT) };
union {
_CharT _M_local_buf[_S_local_capacity + 1];
size_type _M_allocated_capacity;
};
......
}
从代码看,对象成员结构如下图:
_M_dataplus结构体对象只有一个成员变量_M_p,是指向字符串数据的指针,_M_string_length是字符串的长度。_M_local_buf[]和_M_allocated_capacity是一个共同体,也就是说共用一块16 bytes内存。当字符串比较小的时候(size<15),会将要存储的字符串存放在_M_local_buf[]中,此时capcity的大小15,当要存储的字符串长度size > 15时,存放字符串的存储空间将由分配器分配,并将其容量存放在_M_allocated_capacity中。其过程下面小结将会介绍。
basic_string对象的构造有很多,具体如何构造可以查阅cppreference,里面有很多示例。
拷贝其代码如下:
#include
#include
#include
#include
#include
int main()
{
{
// string::string()
std::string s;
assert(s.empty() && (s.length() == 0) && (s.size() == 0));
}
{
// string::string(size_type count, charT ch)
std::string s(4, '=');
std::cout << s << '\n'; // "===="
}
{
std::string const other("Exemplary");
// string::string(string const& other, size_type pos, size_type count)
std::string s(other, 0, other.length()-1);
std::cout << s << '\n'; // "Exemplar"
}
{
// string::string(charT const* s, size_type count)
std::string s("C-style string", 7);
std::cout << s << '\n'; // "C-style"
}
{
// string::string(charT const* s)
std::string s("C-style\0string");
std::cout << s << '\n'; // "C-style"
}
{
char mutable_c_str[] = "another C-style string";
// string::string(InputIt first, InputIt last)
std::string s(std::begin(mutable_c_str)+8, std::end(mutable_c_str)-1);
std::cout << s << '\n'; // "C-style string"
}
{
std::string const other("Exemplar");
std::string s(other);
std::cout << s << '\n'; // "Exemplar"
}
{
// string::string(string&& str)
std::string s(std::string("C++ by ") + std::string("example"));
std::cout << s << '\n'; // "C++ by example"
}
{
// string(std::initializer_list ilist)
std::string s({ 'C', '-', 's', 't', 'y', 'l', 'e' });
std::cout << s << '\n'; // "C-style"
}
{
// 重载决议选择 string(InputIt first, InputIt last) [with InputIt = int]
// 这表现为如同调用 string(size_type count, charT ch)
std::string s(3, std::toupper('a'));
std::cout << s << '\n'; // "AAA"
}
}
这里选取典型的几个构造函数,深入探索。
使用字符串构造:
// 如:std::string s("123")
basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a) {
_M_construct(__s,
__s ? __s + traits_type::length(__s) : __s + npos);
}
// 如:std::string s("C-style string", 7);
basic_string(const _CharT* __s, size_type __n, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a) {
_M_construct(__s, __s + __n);
}
使用另一个string对象构造:
// 如:std::string str(s);
basic_string(const basic_string& __str)
: _M_dataplus(_M_local_data(), _Alloc_traits::_S_select_on_copy(
__str._M_get_allocator())) {
_M_construct(__str._M_data(), __str._M_data() + __str.length());
}
// 如:std::string str(s,1,2);
basic_string(const basic_string& __str, size_type __pos, size_type __n)
: _M_dataplus(_M_local_data()) {
const _CharT* __start =
__str._M_data() + __str._M_check(__pos, "basic_string::basic_string");
_M_construct(__start, __start + __str._M_limit(__pos, __n));
}
可以看到无论调用哪个构造函数,其内部都会执行的操作为:
_M_dataplus
—— 指针初始化。_M_construct
—— 申请空间与数据拷贝。指针初始化:是将其内部指针指向对象内栈空间_M_local_buf[]
,代码如下:
// 强转指针
pointer _M_local_data() { return pointer(_M_local_buf); }
// 赋值给 _M_p
struct _Alloc_hider : allocator_type // TODO check __is_final
{
_Alloc_hider(pointer __dat, const _Alloc& __a = _Alloc())
: allocator_type(__a), _M_p(__dat) {}
pointer _M_p; // The actual data.
};
空间申请与数据拷贝:是将数据拷贝到本地内存中,如果对象内栈空间_M_local_buf[]
不够,则重新分配空间。传入参数是字符串的起始地址和字符串的终止地址。
代码如下:
template
template
void basic_string<_CharT, _Traits, _Alloc>::_M_construct(
_InIterator __beg, _InIterator __end, std::forward_iterator_tag) {
......
size_type __dnew = static_cast(std::distance(__beg, __end)); // 计算字符串长度
if (__dnew > size_type(_S_local_capacity)) { // 大于对象内栈空间容量,分配空间
_M_data(_M_create(__dnew, size_type(0)));
_M_capacity(__dnew);
}
// Check for out_of_range and length_error exceptions.
__try {
this->_S_copy_chars(_M_data(), __beg, __end); // 字符串数据拷贝
}
__catch(...) {
_M_dispose();
__throw_exception_again;
}
_M_set_length(__dnew); // 设置大小
}
......
template
void basic_string<_CharT, _Traits, _Alloc>::_M_construct(size_type __n,
_CharT __c) {
if (__n > size_type(_S_local_capacity)) {
_M_data(_M_create(__n, size_type(0)));
_M_capacity(__n);
}
if (__n) this->_S_assign(_M_data(), __n, __c);
_M_set_length(__n);
}
_M_construct()
函数步骤可以分为:分配空间、字符串拷贝、设置长度三个步骤。
分配空间: 可以看到,如果字符串长度大小大于对象内栈内存_S_local_capacity
时候,会重新申请内存,否则就使用对象内栈内存。说明一下,_M_construct
有好几个重载函数,这里只列出两个,其他的重载函数大体思路是一样的。
重新申请内存情况下会调用 _M_data()
, _M_create()
, _M_capacity()
函数:
void _M_data(pointer __p) { _M_dataplus._M_p = __p; } // 指针赋值
template
typename basic_string<_CharT, _Traits, _Alloc>::pointer
basic_string<_CharT, _Traits, _Alloc>::_M_create(size_type& __capacity,
size_type __old_capacity) {
if (__capacity > max_size())
std::__throw_length_error(__N("basic_string::_M_create"));
if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) { // 如果先前空间大小扩充二倍可以存放的话,就扩充2倍
__capacity = 2 * __old_capacity;
// Never allocate a string bigger than max_size.
if (__capacity > max_size()) __capacity = max_size();
}
// NB: Need an array of char_type[__capacity], plus a terminating
// null char_type() element.
return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);
}
void _M_capacity(size_type __capacity) { _M_allocated_capacity = __capacity; } // 内存容量赋值
字符串拷贝: 内存申请好之后就进行数据拷贝。
template
static void _S_copy_chars(_CharT* __p, _Iterator __k1, _Iterator __k2) {
for (; __k1 != __k2; ++__k1, (void)++__p)
traits_type::assign(*__p, *__k1); // These types are off.
}
设置长度: 设置字符串长度的值_M_set_length()
void _M_set_length(size_type __n) {
_M_length(__n);
traits_type::assign(_M_data()[__n], _CharT()); // 设置结束符'\0'
}
void _M_length(size_type __length) { _M_string_length = __length; }
注意:存入内存空间的字符串会在这里添加字符串结束符’\0’。
先解答问题二
问题二:为什么字符串的后半部分“This is a string too!”没哟存到sss中,std::string对象中能不能不能存储c语言中的字符串结束符’\0’?
通过上一节的存储与构造,我们发现,在拷贝赋值的过程,并没有对’\0’的限定。
再次查看构造函数:
// 如:std::string s("123")
basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
: _M_dataplus(_M_local_data(), __a) {
_M_construct(__s,
__s ? __s + traits_type::length(__s) : __s + npos);
}
我们知道_M_construct()函数为字符串数据拷贝与指针赋值,这里传入的参数为字符串起始地址和结束地址,而这里结束地址使用的是__s + traits_type::length(__s)
,traits_type
的数据类型其实是char_traits<_CharT>
,查看其char_traits<_CharT>::length()
代码:
static _GLIBCXX17_CONSTEXPR size_t
length(const char_type* __s)
{
size_t __i = 0;
while (!eq(__s[__i], char_type()))
++__i;
return __i;
}
通过char_type()
的值来判断字符串结束,而char_type()
实就是char()
,其值为0,也就是字符串结束符’\0’,所以traits_type::length(__s)
获取的的长度只是获取到了第一个’\0’的位置长度,所以后面的字符串不再存储。
如果想存储上面的整个字符串,可以如下:
#include
#include
int main() {
char str[] = "String!\0 This is a string too!";
std::string sss(str,30); // 或者 std::string sss(std::begin(str),std::end(str));
std::cout << "str:" << sss << std::endl
<< "size:" << sss.size() << std::endl
<< "capacity:" << sss.capacity() << std::endl;
std::cout << "c_str len:" << strlen(sss.c_str())
<< "\ndata() len:" << strlen(sss.data());
return 0;
}
输出结果:
str:String! This is a string too!
size:30
capacity:30
c_str len:7
data() len:7
从字符串输出结果,size大小可以看到保存了完整的字符串,而c_str和data()长度仍为7则是因为字符串内保存了’\0’,strlen()
函数遇到’\0’就计算其长度了。
问题一:为什么sss的size=7,capacity=15?std::string对象中存储结构是怎么样的?
通过 string 的对象结构与对象构造可知,size表示字符串长度,capacity表示容量,由问题二的解答可知,sss只会保存"String"字符串,所以size=7,其长度小于对象内部栈空间size=15,所以字符串存储在了内部栈空间中,capacity=15。
问题三:std::string 对象转换为c指针时函数str.c_str()
和str.data()
返回值带有字符串结束符,为什么?
c_str()和data()函数代码如下:
const _CharT* c_str() const _GLIBCXX_NOEXCEPT { return _M_data(); }
const _CharT* data() const _GLIBCXX_NOEXCEPT { return _M_data(); }
就是返回指向字符串指针。
回到构造函数时调用的函数_M_construct()
,其中的设置长度函数_M_set_length()
代码如下:
void _M_set_length(size_type __n) {
_M_length(__n);
traits_type::assign(_M_data()[__n], _CharT()); // 设置结束符'\0'
}
在设置长度的时候自动将其最后一个字节设置了’\0’。所以返回的字符串指针的结尾处有’\0’。
如果想看string类如何使用,可以参考博客 std::string详解