標籤:

std::string 有什麼缺點?

為什麼說C/C++不適合做web開發? - 姚冬的回答 指出 C++ 的 std::string 功能很差,我印象中在知乎上也不是第一次看到類似的評論了。


我覺得最大的缺點是 std::string 是「位元組串」而非「字元串」。

至於缺少 split、format、join 等,都可以通過與標準庫的其它函數組合實現。但它「位元組串」的屬性,讓人用起來膽戰心驚。


根據題主的引用,說一些淺見,各位大神求別噴我……

C++沒有語言內置的 String 類型,只能靠std::string這個極其簡陋的字元串類,大概是所有C++框架里功能最差的string 類了。

很多人抱怨過std::string沒有split函數。仔細想想其實感覺split函數簽名很難去統一,畢竟return一個split過的容器在沒有RVO的情況下是很慢的,但如果讓用戶傳一個空容器引用的話則看起來更加逗比。

C++沒有正則表達式,做個簡單的字串匹配都要自己寫,所以當年perl一出來,所有人就毫不猶豫地放棄用c++寫CGI了。

C++11 已經有正則表達式了std::regex,早在VS2010開始已經支持了。

C++沒有GC,大量的字元串處理,數據處理,所有內存都要程序員自己管理,是非常容易出錯的,緩衝溢出,內存泄漏是分分鐘都會發生。如果CGI是進程隔離的,那麼跑不了幾個並發請求,如果是共享進程,那麼就會總是崩潰。

其實C++11的 std::shared_ptr就當可以當GC來用吧,std::make_shared也很方便嘛。也是早在VS2010開始就支持了。

那麼std::string你簡單用一下final繼承(別和我爭論什麼繼承std::string的壞處),注意別增加Virtual Function也是可以擴展一下的。另final 是C++11特性。

但是記得構造函數一定要補全,這本身雖然比較麻煩,但C++11 也已經支持構造函數復用了,using一下就行了。

實在不行你只能用C++03的話你可以template一下來複用拷貝構造嘛,如下,然後剩下的你自己慢慢加就好了

class my_string : std::string
{
public:
my_string()
{

}

template &
my_string(const T rhs)
: std::string(rhs)
{

}

template &
my_string operator=(T rhs)
{
std::string::operator=(rhs);
return *this;
}
};


沒有常用的format,trim等函數,mfc的CString比它好用的多,QT的QString就更好用了。

一次想用trim,只好用boost的,本以為只用包含一個頭文件,結果編譯不通過,原來還引用了其它頭文件,最後只為了一個trim函數添加了五六個boost頭文件


很多搞C++的人有一個缺點,就是看不到需求的變化,總覺得在目前的條件下能hack進去完事就行了。然而std::string並不能hack。

譬如說,我知道這個object的ref count在這裡只有1(也不寫個assert,就是猜的),於是我可以亂改他,雖然本來object設計成immutable的每次都要創建新的也不要緊。於是就給這個object加了一個friend class——於是後面需求變更了,就創造了一個爛屁股。

我覺得天朝的IT公司(特別是那些不好好做code review的)應該成立一個制度,如果一個人的代碼的屁股太臭還被擦乾淨了,本人應該直播用砂紙擦屁股一次。該協議不包含在勞動合同裡面,終身有效。


不知道這麼多答主都是怎麼想的……既然跟大家的意見都不一致,那麼我就不指望有多少個贊了,所以只簡略的回復:

1、std::basic_string 的設計並不好,在 STL 中屬於最差的那一類。原因不是它的功能不夠強大、提供的方法不夠多,而是相反——它的方法太多了,純粹是冗餘。下面會繼續說。

2、同屬線性容器,vector 的方法為什麼這麼少?因為有 algorithm。事實上,諸如 find / find_if / substr 之類的演算法,字元串與其它線性容器並沒有區別,何苦來自己提供一套?要說優化,有特化有偏特化,用得著搞成成員函數?事實上,BOOST庫提供了相當多的字元串演算法,都不是成員函數。

3、成員函數是一種介面承諾,這東西越少越好。「簡單點,笨蛋!」成員函數過多,有兩方面的困難:一是難以變更,內部機制的優化因此而不可行,很難有針對特定結構改寫內部邏輯的機會;二是違背C++的原則:「不為無意義的事付出的代價。」為了穿越DLL邊界,在動態鏈接時string之類的標準類都是在CRT中顯式實例化的,也就是說,它的每一個方法,無論你用還是不用,都佔用了你的代碼空間。

4、其它懶得寫了,總結:這東東過度設計了。

最近在STM32上碼東西,不得己又「發明」了一個 string 類,十分不爽。


對我來說,std::string幾乎無缺點,完全符合我的胃口:最簡化模塊設計;

我先來說下我對std::string的理解:以字元作為元素的vector特化版本;在std::string中,沒0字元結尾這個概念,也能裝入這種數據;一定要牢記,這是個容器,擁有容器的一切特徵。

現在開始根據大家的槽點,一一解決:

1,不支持split,find substr即可解決,何況還有幾個加強版的find, 雖然在C++11中,你可以使用更通用的閉包版本來解決,但他的確提供了幾個方便又好理解的加強版:find_first_of, find_last_of, find_first_not_of, find_last_not_of;

2,不支持正則表達式,麻煩你告訴我那個C++的字元串類本身就支持正則表達式,我觀摩下;如果你需要這個功能,C++11你可以使用std::regex, 不支持的你可以找個第三庫,比如說Boost!

3,不支持文本處理,這是容器,他支持迭代器呀,比如說你想把所有字母變成小寫,Just:transform(s.begin(), s.end(), s.begin(), tolower); C++ 11中,配合lamada表達式簡直好用極了,如果還不過癮,你還可以構建一個stringstream,是不是爽到爆;

4,效率問題,是的,C++ Standard只是定義了string的介面,具體的實現並沒要求,所以一些string實現並沒採用reference counting的方式,但這又有什麼關係,引用計數的實現只是加速了複製和拷貝,並且這還是有限制的,必須是const reference,如果的確是const,那一個const shared_ptr&是不是就解決了這個問題(題外話:如果你還有一點點C++程序員的節操,還在乎性能,請盡量使用const);此外如果你真的真的真的那麼在乎效率,這是個容器呀,他支持allocator,你可以實現一個自己的內存模型,想怎麼高效就怎麼高效;

最後:如果我說我近幾年一直把string當做數據緩衝來用,會不會有人吐槽我;好吧,這真的是最好的數據緩衝了,特別是對於字元型數據,一經擁有,別無所求!


std::string最大的問題就是它被設計成了非只讀的,這個特點,導致所有使用引用計數,COW等方式實現的std::string都有無數的bug。

而其它語言像java, javascript, c#, python等語言顯然認識到這是個坑,所以,這些語言裡邊的string都擁有隻讀特性。


不支持以下class member function:

.split()

.upper()

.lower()

.isdigit()

.isalphabet()

.isalphnum()

.join()

.find()

.replace()

.rstrip()

.lstrip()

.strip()

其實這些都是很容易包裝的。自從c++終於學會了 for (auto each : container) 之後,接下來需要首要解決的就是徹底刪除c兼容的string char類,完全實現python string方法。

etc.

我相信這種第三方庫肯定是有的


其實string還好說。C++的string雖然功能有點簡陋,但split一類的功能自己可以構建一套。

資源管理也不是問題,我記得大部分的標準庫實現裡面的string都是copy-on-write的,更何況現在還有rvalue、move語義那套東西,你大可以直接傳值,應當不會有嚴重的性能損失。


居然沒有string.format

這是當時從vb轉c++最驚的一個設計,代碼庫到底是為它自己的封裝性服務,還是開發者?

你看其他語言都有,就知道這個設計有多糟糕了


唯一的缺點可能是:不同編譯器實現差別較大,因為標準只規定介面,不要求實現細節。比如c_str方法可能是非常數時間。


std::string算上重載有130+的成員函數,但它卻沒有一個split,這真的說得過去嗎?


印象中c++之父說過c++不偏袒任何一種編程範式

所以不能吐槽std::string不帶split,trim,format等成員函數

我隨便網上搜了這幾個函數,自己寫個stringutils.cpp文件放進去就可以了

std::vector& split(const std::string s, const std::string d)
{
std::vector& v;
char *str = new char[s.size()+1];
strcpy(str, s.c_str());
while (char *t = strsep(str, d.c_str()))
v.push_back(t);
delete[] str;
return v;
}

std::string ltrim(std::string s)
{
if (s.empty()) return s;
std::string::const_iterator iter = s.begin();
while (iter != s.end() isspace(*iter++));
s.erase(s.begin(), --iter);
return s;
}

std::string rtrim(std::string s)
{
if (s.empty()) return s;
std::string::const_iterator iter = s.end();
while (iter != s.begin() isspace(*--iter));
s.erase(++iter, s.end());
return s;
}

std::string trim(std::string s)
{
ltrim(s);
rtrim(s);
return s;
}

bool startsWith(const std::string str, const std::string prefix)
{
return prefix.size() &<= str.size() std::equal(prefix.cbegin(), prefix.cend(), str.cbegin()); } bool endsWith(const std::string str, const std::string suffix) { return suffix.size() &<= str.size() std::equal(suffix.crbegin(), suffix.crend(), str.crbegin()); } std::string::size_type indexOf(const std::string str, const std::string substr) { return str.find(substr); } std::string toUpper(const std::string str) { std::string upper(str.size(), ""); std::transform(str.cbegin(), str.cend(), upper.begin(), ::toupper); return upper; } std::string toLower(const std::string str) { std::string lower(str.size(), ""); std::transform(str.cbegin(), str.cend(), lower.begin(), ::tolower); return lower; } std::string format(const char *fmt, ...) { va_list ap; va_start(ap, fmt); int len = vsnprintf(nullptr, 0, fmt, ap); va_end(ap); std::string buf(len+1, ""); va_start(ap, fmt); vsnprintf(buf[0], buf.size(), fmt, ap); va_end(ap); buf.pop_back(); return buf; } #define log(fmt, ...) (std::cout &<&< format(fmt, ##__VA_ARGS__) &<&< " ")


沒有split和format


因為stl容器靠的是通用的演算法。導致string作為字元串的專用性不太夠。像什麼大小寫啊(大小寫轉換 忽略大小寫的比較) 字元串格式化啊(這個能用流完成。但是其他的string庫一般也提供了一個format函數)。但是string這種東西直接把長度替你管理了。你不用閑的沒事就判斷一下字元串再操作是否就溢出了,H這就不知道幸福到哪裡去了。


我個人的看法,有些人或存在 證實性偏見_百度百科,雖然std::string 並不完美,但是大多時候是足夠你使用的。

std::string 是動態擴容的,當容量不足以存儲足夠的字元時,會引發allocate 函數實現容量的翻倍,std::string::capacity() 可以獲得當前string 對象的容量大小;std::string 須經 std::string::c_str() 或者 std::string::data() 獲得一個 const char * ,這與 C 交互非常方便,另一個方面來說,string 的存儲空間是連續的。 所以在設計上 std::string 和 std::vector 幾乎一致。也就是

typedef std::vector& fuckstring;

這並不是 string 的實現,標準庫對string的實現做了很大的努力,都是 basic_string 模板類的一種實例。

完整實現 C++11 的編譯器及其標準庫需要實現如下4中類型的字元串類。

在Windows 中, 分別是 ASCIl (及其衍生的雙位元組), UTF16LE,UTF16LE,UTF32LE。

在POSIX 中 分別是 UTF8,UTF32LE,UTF16LE,UTF32LE。

typedef basic_string& string;
typedef basic_string& wstring;
typedef basic_string& u16string;
typedef basic_string& u32string;

目前 Visual Studio 2015, Clang 3.7 GCC 5.2 均完整實現了這些類。

C++ 11 編碼轉換非常方便 ,可以使用 std::codecvt ,libstdc++ 在這方面做的比較差,但是目前的 GCC 5.x 已經支持了,所以編譯器還是跟進好。

對於 C++ 來說,資源的釋放尤為重要,而我們使用 C++ 的 string 的時候基本上並不需要手動釋放內存,實際上這是C++ 一個核心的特性支持的, RAII ,下面是一個使用 RAII 的簡單例子:

#include &
#include &
class FuckSomeone{
private:
int *p=nullptr;
public:
FuckSomeone(size_t counts=10){
if(counts==0) return;
p=new int[counts];
}
~FuckSomeone()
{
delete []p;
}
int *Get() const{
return p;
}
};

int FunckFunction()
{
FuckSomeone so(100);
return 0;
}

在 FuckFunction 中,定義了一個局部變數 so, 然後,當退出 FuckFunction 時,函數的調用已經完成,p 也就被釋放了,clang++ -std=c++11 -S fuck.cc 結果如下:

.text
.def _Z13FunckFunctionv;
.scl 2;
.type 32;
.endef
.globl _Z13FunckFunctionv
.align 16, 0x90
_Z13FunckFunctionv: # @_Z13FunckFunctionv
.Ltmp0:
.seh_proc _Z13FunckFunctionv
# BB#0:
subq $56, %rsp
.Ltmp1:
.seh_stackalloc 56
.Ltmp2:
.seh_endprologue
movl $100, %eax
movl %eax, %edx
leaq 48(%rsp), %rcx
movq %rcx, 40(%rsp) # 8-byte Spill
callq _ZN11FuckSomeoneC2Ey
movq 40(%rsp), %rcx # 8-byte Reload
callq _ZN11FuckSomeoneD2Ev
xorl %eax, %eax
addq $56, %rsp
retq
.Ltmp3:
.seh_endproc

.def _ZN11FuckSomeoneC2Ey;
.scl 2;
.type 32;
.endef
.section .text,"xr",discard,_ZN11FuckSomeoneC2Ey
.globl _ZN11FuckSomeoneC2Ey
.align 16, 0x90
_ZN11FuckSomeoneC2Ey: # @_ZN11FuckSomeoneC2Ey
.Ltmp4:
.seh_proc _ZN11FuckSomeoneC2Ey
# BB#0:
subq $72, %rsp
.Ltmp5:
.seh_stackalloc 72
.Ltmp6:
.seh_endprologue
movq %rcx, 64(%rsp)
movq %rdx, 56(%rsp)
movq 64(%rsp), %rcx
movq $0, (%rcx)
cmpq $0, 56(%rsp)
movq %rcx, 48(%rsp) # 8-byte Spill
jne .LBB1_2
# BB#1:
jmp .LBB1_3
.LBB1_2:
movq $-1, %rax
movl $4, %ecx
movl %ecx, %edx
movq 56(%rsp), %r8
movq %rax, 40(%rsp) # 8-byte Spill
movq %r8, %rax
mulq %rdx
seto %r9b
movq 40(%rsp), %r8 # 8-byte Reload
cmovoq %r8, %rax
movq %rax, %rcx
movb %r9b, 39(%rsp) # 1-byte Spill
callq _Znay
movq 48(%rsp), %rcx # 8-byte Reload
movq %rax, (%rcx)
.LBB1_3:
addq $72, %rsp
retq
.Ltmp7:
.seh_endproc

.def _ZN11FuckSomeoneD2Ev;
.scl 2;
.type 32;
.endef
.section .text,"xr",discard,_ZN11FuckSomeoneD2Ev
.globl _ZN11FuckSomeoneD2Ev
.align 16, 0x90
_ZN11FuckSomeoneD2Ev: # @_ZN11FuckSomeoneD2Ev
.Ltmp8:
.seh_proc _ZN11FuckSomeoneD2Ev
# BB#0:
subq $56, %rsp
.Ltmp9:
.seh_stackalloc 56
.Ltmp10:
.seh_endprologue
movq %rcx, 48(%rsp)
movq 48(%rsp), %rcx
cmpq $0, (%rcx)
movq %rcx, 40(%rsp) # 8-byte Spill
je .LBB2_4
# BB#1:
movq 40(%rsp), %rax # 8-byte Reload
movq (%rax), %rcx
cmpq $0, %rcx
movq %rcx, 32(%rsp) # 8-byte Spill
je .LBB2_3
# BB#2:
movq 32(%rsp), %rax # 8-byte Reload
movq %rax, %rcx
callq _ZdlPv
.LBB2_3:
jmp .LBB2_4
.LBB2_4:
addq $56, %rsp
retq
.Ltmp11:
.seh_endproc

聰明的編譯器會在退出函數棧時調用析構函數。

實際上現代的 C++ 寫法大多是這種搞法,你可以去翻看一些經典的C++ 項目,如 LLVM, Android ART,你會發現這樣的用法數不勝數。

廢了這麼多話,也是想讓題主知道,C++ 是怎樣做到資源安全的。

std::string 確實不完美,很多在其他語言中的字元串操作函數,std::string 的過於簡陋,比如 split .C++ 的操作更類似於真實的內存情景。


我想說的是,這些功能 想要 自己封一套就可以。。。。。想怎麼實現就怎麼實現。。。。出了什麼問題也好自己定位修改。別人封裝好的 用了 出了問題 發現被人不能提供這個功能或者知道性能瓶頸而無法修改 才是真的要哭


在可以控制指針的語言里,有string事一個糟糕的設計。


編譯器實現差異,比如有些實現為 copy on write 的會產生一些你預計不到的延遲,不過所有實現版本都是慢慢慢


無法作為dll介面參數


缺點就是實現代碼太長,因為用typedef vector& string 再勉強一點加一個find就是現在的string。

某些腦殘粉的邏輯就是:儘管C++的string什麼都不支持,它還是沒有缺點。


同樣是一個字元的連續數組,為什麼C++的就差?這算歧視嗎?


string 聲明之後,當你第一次輸入的數據就決定了這個對象的大小,貌似是一個「固定容器」。

例如:

std::string str;

std::cin&>&>str;

str[str.size( )-1]="";

這時候

str.size()還等於原來的大小,並沒有減少1個。


推薦閱讀:

new一個String對象的時候,如果常量池沒有相應的字面量真的會去它那裡創建一個嗎?我表示懷疑。
C++ 如何從函數中返回數組的引用並且該數組包含 10 個 string 對象?

TAG:STL | C | string |