• 1. 从源码解析std::string与’\0’的关系
  • 1.1. 背景
  • 1.2. std::string 对象的存储结构
  • 1.3. std::string 对象的构造
  • 1.4. 解答最初三个问题
  • 1.5. 备注


1. 从源码解析std::string与’\0’的关系


1.1. 背景

测试如下代码:

#include <bits/stdc++.h>
#include <iostream>
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

思考:

  • 问题一:为什么sss的size=7,capacity=15?std::string对象中存储结构是怎么样的?
  • 问题二:为什么字符串的后半部分“This is a string too!”没哟存到sss中,std::string对象中能不能不能存储c语言中的字符串结束符’\0’?
  • 问题三:std::string 对象转换为c指针时函数str.c_str()str.data()返回值带有字符串结束符,为什么?

第一个问题与 ‘\0’ 关系不大,但是有助于让我更深层的理解std::string 对象。下面就从这三个问题开始,通过源码的角度,找到问题答案。本文阅读源码版本为 gcc9.2。并且只看最新版本c++11以上的版本(带有宏_GLIBCXX_BEGIN_NAMESPACE_CXX11)的代码。


1.2. std::string 对象的存储结构

头文件定义在stringfwd.h,如下:

// stringfwd.h 文件
    ......
  template<typename _CharT, typename _Traits = char_traits<_CharT>,
           typename _Alloc = allocator<_CharT> >
    class basic_string;

    ......
  typedef basic_string<char>    string;

可以看到,string就是basic_string,继续找到basic_string模板类,basic_string.h文件:

// basic_string.h文件

    ......
    template <typename _CharT, typename _Traits, typename _Alloc>
    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;
        };
        ......
    }

从代码看,对象成员结构如下图:

java的string源码 string源码分析_字符串


_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中。其过程下面小结将会介绍。


1.3. std::string 对象的构造

basic_string对象的构造有很多,具体如何构造可以查阅cppreference,里面有很多示例。
拷贝其代码如下:

#include <iostream>
#include <cassert>
#include <iterator>
#include <string>
#include <cctype>
 
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<charT> 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));
    }

可以看到无论调用哪个构造函数,其内部都会执行的操作为:

  1. 初始化变量_M_dataplus —— 指针初始化。
  2. 函数_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 <typename _CharT, typename _Traits, typename _Alloc>
template <typename _InIterator>
void basic_string<_CharT, _Traits, _Alloc>::_M_construct(
    _InIterator __beg, _InIterator __end, std::forward_iterator_tag) {
  
    ......

    size_type __dnew = static_cast<size_type>(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 <typename _CharT, typename _Traits, typename _Alloc>
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 _CharT, typename _Traits, typename _Alloc>
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 <class _Iterator>
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’。


1.4. 解答最初三个问题

先解答问题二
问题二:为什么字符串的后半部分“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 <bits/stdc++.h>
#include <iostream>
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’。


1.5. 备注

如果想看string类如何使用,可以参考博客 std::string详解