字符串字面量
字符串字面量位於字面量池中,字面量池位於程序的常量區中
void show_address(const char* str) {
std::cout << reinterpret_cast<const void*>(str) << std::endl;
}
int main()
{
// 三者位於同一個地址上
show_address("Hello");
show_address("Hello");
show_address("Hello");
// C++中允許對字符串字面量取地址,即&"Hello" 得到的地址與上文相同
}
對於指針和數組,它們代表的含義不同
// pStr指針位於全局區中 指向位於常量區中的字符串字面量
const char* pStr = "Hello";
int main()
{
// strArr位於棧中 將數據從常量區拷貝到函數棧中
char strArr[] = "Hello";
}
std::string的內存分配
C++對std::string
的內部實現有如下約定
- 如果傳入的字符串字面量小於某閾值,那么該
std::string
內部在棧上分配內存(即短字符串優化——SSO);如果大於指定的閾值,那么將會根據傳入的字符串的尺寸,在堆上開辟相應的空間。不管是短字符串還是長字符串,在使用字符串字面量構建std::string
的時候,都會產生拷貝的操作 - 如果后續對
std::string
采用了“增”操作,那么將會采用double的形式進行擴容(雙倍擴容)
在通常情況下,若數據的長度小於等於15(還有一位是'\0'
結束符),那么會采用短字符串優化(這主要取決於不同庫的實現)
// MSVC中的實現
// length of internal buffer, [1, 16]:
static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);
std::string的結構
在MSVC-Release-x64的環境下,std::string
的大小是32B
using string = basic_string<char, char_traits<char>, allocator<char>>;
using _Alty = _Rebind_alloc_t<_Alloc, _Elem>;
using _Alty_traits = allocator_traits<_Alty>;
using _Scary_val = _String_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Simple_types<_Elem>,
_String_iter_types<_Elem, typename _Alty_traits::size_type, typename _Alty_traits::difference_type,
typename _Alty_traits::pointer, typename _Alty_traits::const_pointer, _Elem&, const _Elem&>>>;
_Compressed_pair<_Alty, _Scary_val> _Mypair;
std::string
采用std::allocator<char>
作為分配器,由_Compressed_pair
的EBO得,分配器並不會占用內存空間。該分配作用於std::_Is_simple_alloc_v<std::_Rebind_alloc_t<std::allocator<char>, char>>
為true
,因此std::string
的內存布局可以拆解如下
// std::string同一時間只可能是短字符串或長字符串
union _Bxty { // storage for small buffer or pointer to larger one
char _Buf[16];
char* _Ptr;
char _Alias[16]; // TRANSITION, ABI: _Alias is preserved for binary compatibility (especially /clr)
} _Bx;
std::size_t _Mysize = 0; // current length of string
std::size_t _Myres = 0; // current storage reserved for string
-
當
std::string
中記錄的是短字符串時,_Buf
代表棧上的字符串,如"Hello World"
是存儲在_Buf
數組中 -
當
std::string
中記錄的是長字符串時,_Ptr
代表指向堆上數據的指針,可通過該指針訪問數據
當我們調用c_str()
時,本質上是在調用如下方法
constexpr const value_type* _Myptr() const noexcept {
const value_type* _Result = _Bx._Buf;
// 判斷是否是長字符串
if (_Large_string_engaged()) {
_Result = _Unfancy(_Bx._Ptr);
}
return _Result;
}
constexpr bool _Large_string_engaged() const noexcept {
#if _HAS_CXX20
// 判斷當前函數調用是否發生在常量求值場合
if (std::is_constant_evaluated()) {
return true;
}
#endif // _HAS_CXX20
return _BUF_SIZE <= _Myres;
}
SSO與移動
若無特殊說明,本小節建立在MSVC-Release-x64的環境下進行分析,且源碼在便於理解的基礎上略有刪減。std::string
在Debug和Release模式下內存分配的機理不同(Debug模式下無短字符串優化等)
// 如果是MSVC-Debug-x64環境 那么會在堆上分配2次16B的內存
std::string name = "Hello World";
std::string newName = std::move(name);
下面進行源碼剖析
constexpr basic_string(basic_string&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t{}, _STD move(_Right._Getal())) // 標簽分發
{
// 根據優化等級選擇不同的分配器 在Release模式下取得_Fake_allocator 它是空類 不負責任何功能
_Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
// 拿走被移動對象中的數據
_Take_contents(_Right);
}
constexpr void _Take_contents(basic_string& _Right) noexcept {
// assign by stealing _Right's buffer
auto& _My_data = _Mypair._Myval2;
auto& _Right_data = _Right._Mypair._Myval2;
// We need to ask if pointer is safe to memcpy.
// size_type must be an unsigned integral type so memcpy is safe.
// _Elem must be trivial standard-layout, so memcpy is safe.
// We also need to disable memcpy if the user has supplied _Traits, since they can observe traits::assign and similar.
if constexpr (_Can_memcpy_val) {
#if _HAS_CXX20
if (!_STD is_constant_evaluated())
#endif
{
// 該宏判斷優化等級 Release模式下_ITERATOR_DEBUG_LEVEL為0
#if _ITERATOR_DEBUG_LEVEL != 0
if (_Right_data._Large_string_engaged()) {
// take ownership of _Right's iterators along with its buffer
_Swap_proxy_and_iterators(_Right);
} else {
_Right_data._Orphan_all();
}
#endif
// memcpy右值字符串中的數據
_Memcpy_val_from(_Right);
// 將右值字符串置回默認狀態
_Right._Tidy_init();
return;
}
}
// 下方代碼處理 when is unsafe to memcpy 的情況
// Codes...
}
void _Memcpy_val_from(const basic_string& _Right) noexcept {
// 添加偏移量 使memspy正常工作
const auto _My_data_mem =
reinterpret_cast<unsigned char*>(std::addressof(_Mypair._Myval2)) + _Memcpy_val_offset;
const auto _Right_data_mem =
reinterpret_cast<const unsigned char*>(std::addressof(_Right._Mypair._Myval2)) + _Memcpy_val_offset;
// 對數據進行拷貝 Debug和Release模式的不同會導致偏移量不同 但最終拷貝的是同一份數據
::memcpy(_My_data_mem, _Right_data_mem, _Memcpy_val_size);
}
由於MSVC中對將存儲數據的結構設計為union
,因此在::memcpy
的時候並不需要考慮是長字符串還是短字符串,直接對數據進行拷貝,然后再讀取的時候進行判定即可即可(即上文中提到的_Myptr()
以及_Large_string_engaged()
)
過時的COW
std::string_view與const std::string&
對於std::string
而言,當它從一個原生的c-style-string上構造時,都伴隨着內存分配(可能是堆也可能是棧);但對於std::string_view
而言,它內部只維護了一個原生指針和一個長度
const char* _Mydata;
std::size_t _Mysize;
這代表着std::string_view
在構造的時候,只是進行一次淺拷貝,同時進行一次O(n)復雜度的長度求值
constexpr basic_string_view(const char* _Ntcts) noexcept
: _Mydata(_Ntcts), _Mysize(_Traits::length(_Ntcts)) {}
const char* word = "Hello";
std::string_view sv1 = word;
std::string_view sv2 = word;
std::string s = word;

因此在對c-style-string進行操作時,為此構建一個std::string
是一個不值當的操作,我們需要的是一個“視圖”,即std::string_view
Example1
std::string extract_part(const std::string& bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
// do something...
}
盡管編譯器已經開啟了RVO,但上述代碼仍然包含了兩次std::string
對象的構造,若檢測的字符串是長字符串,那么這代表着高額的性能開銷
std::string_view extract_part(std::string_view bar) {
return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
// do something...
}
Problem1
但由於std::string_view
執行的是淺拷貝,所以也伴隨着dangling的問題
std::vector<std::string_view> elements;
// 若elem的生命周期短於elements 那么可能會訪問到已經被釋放的內存
void Save(const std::string& elem) {
elements.push_back(elem);
}
Problem2
std::map<std::string, int> frequencies;
int GetFreqForKeyword(std::string_view keyword) {
// 無法通過編譯 不存在std::string_view到std::string的隱式轉換
return frequencies.at(keyword);
}
Problem3
class Sink
{
public:
Sink(std::string_view sv) : str(std::move(sv)) {}
private:
std::string str;
};
- 對一個
std::string_view
,而言,std::move
它是無害但無用的 - 以
std::string_view
去構造std::string
,存在sv
在構建時內部指針懸空的風險
總結
- 考慮使用
std::string_view
代替const std::string&
- 函數傳參按值傳遞
std::string_view
即可,不需要pass-by-const-reference,也沒有移動操作
手撕簡易my_string
class my_string
{
protected:
std::size_t size;
char* pStr;
void init_null_impl() {
size = 0;
pStr = new char[1]{'\0'};
}
void init_impl(const char* newData) {
size = std::strlen(newData);
pStr = new char[size + 1];
strcpy_s(pStr, size + 1, newData);
}
void str_swap(my_string& _another) {
std::swap(pStr, _another.pStr);
std::swap(size, _another.size);
}
public:
my_string() {
init_null_impl();
}
my_string(const char* newData) {
if (newData == nullptr)
init_null_impl();
else
init_impl(newData);
}
my_string(const my_string& _copy) {
if (_copy.pStr == nullptr)
init_null_impl();
else
init_impl(_copy.pStr);
}
my_string(my_string&& _another) : size(_another.size), pStr(_another.pStr) {
_another.init_null_impl();
}
my_string& operator=(my_string _another) {
str_swap(_another);
return *this;
}
~my_string() {
delete[] pStr;
}
char operator[](std::size_t index) const { return pStr[index]; }
const char* c_str() const { return pStr; }
};