c++11 string u8_深入理解C++11:C++11新
一.數據對齊
在了解為什么數據需要對齊之前,我們可以回顧一下打印結構體的大小這個C/C++中的經典案例。先看代碼:
#include
using namespace std;
struct HowManyBytes{
char a;
int b;
};
int main(){
cout<>endl;
cout<>endl;
cout<>endl;
cout<
cout<>endl;
cout<>endl;
return 0;
}
結構體HowManyBytes由一個char類型成員a及一個int類型成員b組成。編譯上述的代碼,我們可以得到如下結果:
sizeof(char):1
sizeof(int):4
sizeof(HowManyBytes):8
offset of char a:0
offset of int b:4
很明顯,a和b兩個數據的長度分別是1字節和4字節,不過當我們使用sizeof來計算HowManyBytes這個結構體所占用的內存空間時,看到其值為8字節。其中似乎多出來了3字節沒有使用的空間。
通常情況下,C/C++結構體中的數據會有一定的對齊要求。這里成員b的偏移是4字節,而成員a只占用了1字節內存空間,這意味著b并非緊鄰a排列。事實上,在我們的平臺定義上,C/C++的int類型要求對齊到4字節,即要求int類型數據必須放在一個能夠整除4的地址上;而char要求對齊到1字節。這就造成了成員a之后的3字節空間被空出,通常我們也稱因為對齊而造成的內存留空為填充數據(padding data)。
在C++中,每個類型的數據除去長度等屬性外,都有一項“被隱藏”屬性,那就是對齊方式。對于每個內置或者自定義類型,都存在一個特定的對齊方式。對齊方式通常是一個整數,它表示的是一個類型的對象存放的內存地址應滿足的條件。在這里,我們簡單地將其稱為對齊值。
對齊的數據在讀寫上會有性能上的優勢。比如頻繁使用的數據如果與處理器的高速緩存器大小對齊,有可能提高緩存性能。而數據不對其可能造成一些不良的后果,比較嚴重的當屬導致應用程序退出。典型的,如在有的平臺上,硬件將無法讀取不按字對齊的某些類型數據,這個時候硬件會拋出異常(如bus error) 來終止程序。更為普遍的,在一些平臺上,不按照字對齊的數據會造成數據讀取效率低下。因此,在程序設計時,保證數據對齊是保證正確有效讀寫數據的一個基本條件。
通常由于底層硬件的設計或用途不同,以及編程語言本身在基本(內置)類型的定義上的不同,相同的類型定義在不同的平臺上會有不同的長度,以及不同的對齊要求。
在C++語言中,我們可以通過sizeof查詢數據長度,但C++語言卻沒有對對齊方式有關的查詢或者設定進行標準化,而語言本身又允許自定義類型、模板等諸多特性。編譯器無法完全找到正確的對齊方式,這會在使用時造成困難。代碼如下:
#include
using namespace std;
//自定義的ColorVector,擁有32字節的數據
struct ColorVector{
double r;
double g;
double b;
double a;
};
int main(){
//使用C++11中的alignof來查詢ColorVector的對齊方式
cout<
return 1;
}
在如上代碼,我們使用了C++11標準定義的alignof函數來查看數據的對齊方式。從結果上看,可以看出ColorVector在實驗機上依然是對齊到8字節的地址邊界上。
c++ alignof(ColorVector):8
現在的計算機通常會支持許多向量指令,而ColorVector正好是4組8字節的浮點數數據,很有潛力改造為能直接操作的向量數據。這樣一來,為了能夠高效地讀寫ColorVector大小的數據,我們最好能將其對齊到32字節的地址邊界上。
下面代碼中,我們利用C++11新提供的修飾符alignas來重新設定ColorVector的對齊方式。
#include
using namespace std;
//自定義的ColorVector,對齊到32字節的邊界
struct alignas(32)ColorVector{
double r;
double g;
double b;
double a;
};
int main(){
//使用C++11中的alignof來查詢ColorVector的對齊方式
cout<
return 1;
}
運行后,我們會得到如下結果:
c++ alignof(ColorVector): 32
正如我們所看到的,指定數據ColorVector對齊到32字節的地址邊界上,只需要聲明alignas(32)即可。
二.C++11的alignof和alignas
C++11在新標準中為了支持對齊,主要引入兩個關鍵字:操作符alignof、對齊描述符(alignment-specifier) alignas。操作符alignof的操作數表示一個定義完整的自定義類型或者內置類型或者變量,返回的值是一個std:: size_t類型的整型常量。如同sizeof操作符一樣,alignof獲得的也是一個與平臺相關的值。
#include
using namespace std;
class InComplete;
struct Completed{};
int main(){
int a;
long long b;
auto& c=b;
char d[1024];
//對內置類型和完整類型使用alignof
cout<
//對變量、引用或者數組使用alignof
cout<
//本句無法通過編譯,Incomplete類型不完整
//cout<
}
使用alignof很簡單,基本上沒有什么特別的限制。在上面代碼中,類型定義不完整的class InComplete是無法通過編譯的。其他的規則則基本跟大多數人想象的相同:引用c于其引用的數據b對齊值相同,數組的對齊值由其元素決定。
對齊描述符alignas,既可以接受常量表達式,也可以接受類型作為參數,比如:c++ alignas(double) char c; 效果跟 c++ alignas(alignof(double)) char c; 是一樣的。
注意 在C++11標準之前,我們也可以使用一些編譯器的擴展來描述對齊方式,比如GNU格式的attribute((aligned(8))) 就是一個廣泛被接受的版本。
我們在使用常量表達式作為alignas的操作符的時候,其結果必須是以2的自然數冪次作為對齊值。對齊值越大,我們稱其對齊要求越高;而對齊值越小,其對齊要求也越低。由于2的冪次的關系,能夠滿足嚴格對齊要求的對齊方式也總是能夠滿足要求低的對齊值的。
在C++11標準中規定了一個“基本對齊值”。一般情況下其值通常等于平臺上支持的最大標量類型數據的對齊值(常常是long double)。我們可以通過alignof(std::max_align_t)來查詢其值。而像我們在代碼中設定ColorVector對齊值到32字節(超過標準對齊)的做法稱為擴展對齊(extended alignment)。不過即使使用了擴展對齊,也并非意味著程序員可以隨心所欲。每個平臺上,系統能夠支持的對齊值總是有限的,程序中如果聲明了超過平臺要求的對齊值,則按照C++標準該程序是不規范的,這可能會導致未知的編譯時或者運行時錯誤。
對齊描述符可以作用于各種數據。具體來說,可以修飾變量、類的數據成員等,而位域(field)以及用register聲明的變量則不可以。代碼如下:
alignas(double)void f(); //錯誤:alignas不能修飾函數
alignas(double) unsigned char c[sizeof(double)]; //正確
extern unsigned char c[sizeof(double)];
alignas(float)
extern unsigned char c[sizeof(double)]; //錯誤:不同對齊方式的變量定義
C++11標準建議用戶在聲明同一個變量的時候使用同樣的對齊方式以免發生意外。不過C++11并沒有規定聲明變量采用了不同的對齊方式就終止編譯器的編譯。
下面代碼實現了一個固定容量但是大小隨著所用的數據類型變化的容器類型,如代碼所示:
#include
using namespace std;
struct alignas(alignof(double)*4) ColorVector{
double r;
double g;
double b;
double a;
};
//固定容量的模板數組
template
class FixedCapacityArray{
public:
void push_back(T t){/*在data中加入t變量*/}
//...
//一些其他成員函數、成員變量等
//...
char alignas(T) data[1024]={0};
//int length=1024/sizeof(T);
};
int main(){
FixedCapacityArray arrCh;
cout<
cout<
FixedCapacityArray arrCV;
cout<
cout<
return 1;
}
//編譯選項:clang++8-1-6.cpp-std=c++11
在本例中,FixedCapacityArray固定使用1024字節的空間,但由于模板的存在,可以實例化為各種版本。這樣一來,我們可以在相同的內存使用量的前提下,做出多種(內置或者自定義)版本的數組。對于arrCh,由于數組中的元素都是char類型,所以對齊到1就行了,而對于我們定義的arrCV, 必須使其符合ColorVector的擴展對齊,即對齊到8字節的內存邊界上。在這個例子中,起到關鍵作用的代碼是:
char alignas(T) data[1024]={0};
該句指示data[1024]這個char類型數組必須按照模板參數T的對齊方式進行對齊。
alignof(char):1
alignof(arrCh.data):1
alignof(ColorVector):32
alignof(arrCV.data):32
由于char數組默認對齊值為1,會導致data[1024]數組也對齊到1.這肯定不是編寫FixedCapacityArray的程序員愿意見到的。
在C++11標準引入alignas修飾符之前,這樣的固定容量的泛型數組有時可能遇到因為對齊不佳而導致的性能損失(甚至程序錯誤),這給庫的編寫者帶來了很大的困擾。而引入alignas能夠解決這些移植性的困難。
C++11對于對齊的支持并不限于alignof操作符及alignas操作符。在STL庫中,還內建了std::align函數來動態地根據指定的對齊方式調整數據塊的位置。該函數的原型如下:
void* align(std:: size_t alignment, std:: size_t size,void*&ptr,std:: size_t&space);
該函數在ptr指向的大小為space的內存中進行對齊方式的調整,將ptr開始的size大小的數據調整為按alignment對齊。代碼如下:
#include
#include
using namespace std;
struct ColorVector{
double r;
double g;
double b;
double a;
};
int main(){
size_t const size=100;
ColorVector* const vec=new ColorVector[size];
void*p=vec;
size_t sz=size;
void* aligned=align(alignof(double)*4,size,p,sz);
if(aligned!=nullptr)
cout<
}
嘗試將vec中的內容按alignof(double)*4的對齊值進行對齊(不過在編寫本書的時候,我們的編譯器還沒有支持std:: align這個新特性,因此代碼僅供參考)
(.....剩下的這個部分不是特別懂,放在以后把相關的知識學完后再補)
三.通用屬性
1.語言擴展到通用屬性
隨著C++語言的演化和編譯器的發展,人們常會發現標準提供的語言能力不能完全滿足要求。于是編譯器廠商或組織為了滿足編譯器客戶的需求,設計出一系列的語言擴展(language extension)來擴展語法。這些擴展語法并不存在于C++/C標準中,卻有可能擁有較多的用戶。
擴展語法中比較常見的就是"屬性"。屬性是對語言中的實體對象(比如函數、變量、類型等)附加一些的額外注解信息,其用來實現一些語言及非語言層面的功能,或是實現優化代碼等的一種手段。不同編譯器有不同的屬性語法。比如對于g++,屬性是通過GNU的關鍵字__attribute__來聲明的。程序員只需要簡單地聲明:
__attribute__((attribute-list))
即可為程序中的函數、變量和類型設定一些額外信息,以便編譯器可以進行錯誤檢查和性能優化等。代碼如下所示:
extern int area(int n) __attribute__((const));
int main(){
int i;
int areas=0;
for(i=0;i<10;i++){
areas+=area(3)*i;
}
}
//編譯選項:g++ -c 8-2-1.cpp
這里的const屬性告訴編譯器:本函數返回值只依賴于輸入,不會改變任何函數外的數據,因此沒有任何副作用。在此情況下,編譯器可以對area函數進行優化處理。area(3)的值只需要計算一次,編譯之后可以將area(3)視為循環中的常量而只使用其計算結果,從而大大提高了程序的執行性能。
事實上,在GNU對C/C++的擴展中我們可以看到很多不同的attribute屬性。常見的如format、noreturn、const和aligned等,具體含義和用法讀者可以參考GNU的在線文檔。
在Windows平臺上,我們會找到另外一種關鍵字__declspec。__declspec是微軟用于指定存儲類型的擴展屬性關鍵字。用戶只要簡單地在聲明變量時加上:
c++ __declspec(extended-decl-modifier)
即可設定額外的功能。以對齊方式為例,在C++11之前,微軟平臺的程序員可以使用__declspec(align(x)) 來控制變量的對齊方式,代碼如下:
__declspec(align(32)) struct Struct32{
int i;
double d;
};
代碼中,結構體Struct32被對齊到32字節的地址邊界,其起始地址必須是32的倍數。同樣的,微軟也定義了很多__declspec屬性,如noreturn、oninline、align、dllimport、dllexport等,具體含義和用法可以參考微軟網站上的介紹。
事實上,在擴展語言能力的時候,關鍵字往往會成為一種選擇。GNU和微軟只能選擇"屬性"這樣的方式,是為了盡可能避免與用戶自定義的名稱沖突。同樣,在C++11標準的設立過程中,也面臨著關鍵字過多的問題。于是C++11語言制定者決定增加了通用屬性這個特性。
2.C++11的通用屬性
C++11語言中的通用屬性使用了左右雙中括號的形式:
c++ [[attribute-list]]
這樣設計得好處是:既不會消除語言添加或者重載關鍵字的能力,又不會占用用戶空間的關鍵字的名字空間。
語法上,C++11的通用屬性可以作用于類型、變量、名稱、代碼塊等。對于作用于聲明的通用屬性,既可以寫在聲明的起始處,也可以寫在聲明的標識符之后。而對于作用于整個語句的通用屬性,則應該寫在語句起始處。而出現在以上兩種規則描述的位置之外的通用屬性,作用于哪個實體跟編譯器具體的實現有關。
第一個例子是關于通用屬性應用于函數的,具體如下:
[[attr1]] void func[[attr2]]();
這里,[[attr1]]出現在函數定義之前,而[[attr2]]則位于函數名稱之后,根據定義,[[attr1]]和[[attr2]]均可以作用于函數[func]。
[[attr1]] int array[[attr2]][10];
這跟第一個例子很類似,根據定義,[[attr1]] 和 [[attr2]] 均可以作用于數組array。下面這個例子比較復雜:
[[attr1]] class C[[attr2]]{}[[attr3]] c1[[attr4]], c2[[attr5]];
這個例子聲明了類C及其類型的變量c1和c2。本語句中,一共有5個不同的屬性。按照C++11的定義,[[attr1]] 和[[attr4]] 會作用于c1, [[attr1]]和[[attr5]] 會作用于c2,[[attr2]] 出現在聲明之后,僅作用于類C,而[[attr3]] 所作用的對象則跟具體實現有關。(其實這個地方沒看太明白,attr2和attr3具體是怎么作用的)
[[attr1]] L1:
switch(value){
[[attr2]] case1: //do something...
[[attr3]] case2: //do something...
[[attr4]] break;
[[attr5]] default: //do something...
}
[[attr6]] goto L1;
這里,[[attr1]] 作用于標簽L1,[[attr2]] 和[[attr3]] 作用于case 1和case 2表達式,[[attr4]] 作用于break, [[attr5]] 作用于default表達式,[[attr6]] 作用于goto語句。下面的for語句也是類似的:
[[attr1]] for(int i=0;i
//do something...
}
[[attr2]] return top;
這里,[[attr1]] 作用于for表達式,[[attr2]] 作用于return。下面是函數有參數的情況:
[[attr1]] int func([[attr2]] int i,[[attr3]] int j)
{
//do something
[[attr4]] return i+j;
}
[[attr1]] 作用于函數func,[[attr2]] 和[[attr3]] 分別作用于整型參數i和j,[[attr4]] 作用于return 語句。
事實上,在現有C++11標準中,只預定義了兩個通用屬性,分別是[[noreturn]] 和 [[carries_dependency]]。
3.預定義的通用屬性
C++11預定義的通用屬性包括[[noreturn]] 和 [[carries_dependency]] 兩種。
[[noreturn]] 是用于標識不會返回的函數的。這里必須注意,不會返回和沒有返回值的(void)函數的區別。
沒有返回值的void函數在調用完成后,調用者會接著執行函數后的代碼;而不會返回的函數在被調用完成后,后續代碼不會再被執行。
[[noreturn]] 主要用于標識那些不會將控制流返回給原調用函數的函數,典型的例子有:有終止應用程序語句的函數、有無限循環語句的函數、有異常拋出的函數等。通過這個屬性,開發人員可以告知編譯器某些函數不會將控制流返回給調用函數,這能幫助編譯器產生更好的警告信息,同時編譯器也可以做更多的諸如死代碼消除、免除為函數調用者保存一些特定寄存器等代碼優化工作。
下面代碼:
void DoSomething1();
void DoSomething2();
[[noreturn]] void ThrowAway(){
throw "expection"; //控制流跳轉到異常處理
}
void Func(){
DoSomething1();
ThrowAway();
DoSomething2(); // 該函數不可到達
}
由于ThrowAway 拋出了異常,DoSomething2永遠不會被執行。這個時候將ThrowAway標記為noreturn的話,編譯器會不再為ThrowAway之后生成調用DoSomething2的代碼。當然,編譯器也可以選擇為Func函數中的DoSomething2做出一些警告以提示程序員這里有不可到達的代碼。
不返回的函數除了是有異常拋出的函數外,還有可能是有終止應用程序語句的函數,或是有無限循環語句的函數等。事實上,在C++11的標準庫中,我們都能看到形如:
[[noreturn]] void abort(void) noexcept;
這樣的函數聲明。最常見的是abort函數。abort總是會導致程序運行的停止,甚至連自動變量的析構函數以及本該在atexit() 時調用的函數全都不調用就直接退出了。因此聲明為[[noreturn]] 是有利于編譯器優化的。
盡量不要對可能會有返回值的函數使用[[noreturn]]。下面代碼就是一個錯誤的例子:
#include
using namespace std;
[[noreturn]] void Func(int i){
//當參數i的值為0時,該函數行為不可估計
if(i<0)
throw "negative";
else if(i>0)
throw "positive";
}
int main(){
Func(0);
cout<
return 1;
}
代碼清單中,Func調用后的打印語句永遠不會執行,因為Func被聲明為[[noreturn]].不過由于函數作者的疏忽,忘記了i==0時,Func運行結束時還是會返回mian的。在我們的實驗機上,編譯運行該例子會在運行時發生"段錯誤"。當然,具體的錯誤情況可能會根據編譯器和運行時環境的不同而有所不同。不過總的來說,程序員必須審慎使用[[noreturn]].
另外一個通用屬性[[carries_dependency]] 則跟并行情況下的編譯器優化有關。事實上,[[carries_depency]] 主要是為了解決弱內存模型平臺上使用memory_order_consume內存順序枚舉問題。
memory_order_consume的主要作用是保證對當前 "原子類型數據" 的讀取操作先于所有之后關于該原子變量的操作完成,但它不影響其他原子操作的順序。要保證這樣的"先于發生" 的關系,編譯器往往需要根據memory_model枚舉值在原子操作間構建一系列的依賴關系,以減少在弱一致性模型的平臺上產生內存柵欄。不過這樣的關系則往往會由于函數的存在而被破壞。比如下面的代碼:
tomic a;
...
int*p=(int*)a.load(memory_order_consume);
func(p);
上面的代碼中,編譯器在編譯時可能并不知道func函數的具體實現,因此,如果要保證a.load先于任何關于a(或是p)的操作發生,編譯器往往會在func函數之前加入一條內存柵欄。然而,如果func的實現是:
void func(int*p){
//... 假設p2是一個atomic的變量
p2.store(p,memory_order_release)
}
由于p2.store使用了memory_order_release的內存順序,因此,p2.store對p的使用會被保證在任何關于p的使用之后完成。這樣一來,編譯器在func函數之前加入的內存柵欄就變得毫無意義,且影響了性能。同樣的情況也會發生在函數返回的時候。
解決的方法就是使用[[carries_dependency]]。該通用屬性既可以標識函數參數,又可以標識函數的返回值。
當標識函數的參數時,它表示數據依賴隨著參數傳遞進入函數,即不需要產生內存柵欄。
而當標識函數的返回值時,它表示數據依賴隨著返回值傳遞出函數,同樣也不需要產生內存柵欄。
下面是相關的例子:
#include
#include
using namespace std;
atomic p1;
atomic p2;
atomic p3;
atomic p4;
//定義了4個原子類型
void func_in1(int*val){
cout<
}
void func_in2(int*[[carries_dependency]] val){
p2.store(val,memory_order_release); //p2.store對p的使用會被保證在任何關于p的使用之后完成。
cout<
}
[[carries_dependency]] int*func_out(){
return(int*)p3.load(memory_order_consume); //p3.load對p的使用會被保證在任何關于p的使用之前完成。
}
void Thread(){
int* p_ptr1=(int*)p1.load(memory_order_consume); //L1
cout<
func_in1(p_ptr1); //L3
func_in2(p_ptr1); //L4
int*p_ptr2=func_out(); //L5
p4.store(p_ptr2,memory_order_release); //L6
cout<
}
在代碼中,L1句中,p1.load采用了memory_order_consume的內存順序,因此任何關于p1或者p_ptr1的原子操作,必須發生在L1句之后。
這樣一來,L2將由編譯器保證其執行必須在L1之后(通過編譯器正確的指令排序和內存柵欄)。
而當編譯器在處理L3時,由于func_in1對于編譯器而言并沒有聲明[[carries_dependency]]屬性,編譯器則可能采用保守的方法,在func_in1調用表達式之前插入內存柵欄。
而編譯器在處理L4句時,由于函數func_in2使用了[[carries_dependency]], 編譯器則會假設函數體內部會正確地處理內存順序,因此不再產生內存柵欄指令。
事實上func_in2中也由于p2.store使用內存順序memory_order_release, 因而不會產生任何的問題。
而當編譯器處理L5句時,由于func_out的返回值使用了[[carries_dependency]],編譯器也不會在返回前為p3.load(memory_order_consume) 插入內存柵欄指令去保證正確的內存順序。
而在L6行中,我們看到p4.store使用了memory_order_release, 因此func_out不產生內存柵欄也是毫無問題的。
與[[noreturn]]相同的是,[[carries_dependency]] 只是幫助編譯器進行優化,這符合通用屬性設計的原則。 當讀者使用的平臺是弱內存模型的時候,并且很關心并行程序的執行性能時,可以考慮使用 [[carries_dependency]]。
四.Unicode 支持
1.字符集、編碼和Unicode
無論是哪種狀態,計算機總是使用兩種不同的狀態來作為基本信息,即二進制信息。而要標識現實生活中更為復雜的實體,則需要通過多個這樣的基本信息的組合來完成。現在使用最為廣泛的ASCII字符編碼就出現了。
基本ASCII的字符使用了7個二進制位進行標識,這意味著總共可以標識128種不同的字符。這對英文字符(以為一些控制字符、標點符號等)來說綽綽有余,不過隨著計算機在全世界普及,非字符構成的語言(如中文)也需要得到支持,128個字符對于全世界眾多語言而言就顯得力不從心了。
通常情況下,我們將一個標準中能夠表示的所有字符的集合稱為字符集。通常,我們稱ISO/Unicode所定義的字符集為Inicode。在Unicode中,每個字符占據一個碼位(Code point)。Unicode字符集總共定義了1 114 112個這樣的碼位,使用從0到10FFFF的十六進制數唯一地表示所有的字符。不過不得不提的是,雖然字符集中的碼位唯一,但由于計算機存儲數據通常是以字節為單位的,而且出于兼容之前的ASCII、大數小段數段、節省存儲空間等諸多原因,通常情況下,我們需要一種具體的編碼方式來對字符碼位進行存儲。比較常見的基于Unicode字符集的編碼方式有UTF-8、UTF-16及UTF-32。
注意,事實上,現行桌面系統中,Windows內部采用了UTF-16的編碼方式,而Mac OS、Linux等則采用了UTF-8編碼方式。
2.C++11中的Unicode支持
在C++98標準中,為了支持Unicode,定義了“寬字符”的內置類型wchar_t. 不過不久程序員便發現C++標準對wchar_t的“寬度”顯然太過容忍,在Windows上,多數wchar_t被實現為16位寬,而在Linux上,則被實現為32位。事實上,C++98標準定義中,wchar_t的寬度是由編譯器實現決定的。理論上,wchar_t的長度可以是8位、16位或者32位。這樣帶來的最大的問題是,程序員寫出的包含wchar_t的代碼通常不可移植。
C++11引入以下兩種新的內置數據類型來存儲不同編碼長度的Unicode數據。
A char16_t: 用于存儲UTF-16編碼的Unicode數據。
B char32_t: 用于存儲UTF-32編碼的Unicode數據。
至于UTF-8編碼的Unicode數據,C++11還是使用8字節寬度的char類型的數組來保存。而char16_t和char32_t的長度則猶如其名稱所顯示的那樣,長度分別為16字節和32字節,對任何編譯器或者系統都是一樣的。此外,C++11還定義了一些常量字符串的前綴。在聲明常量字符串的時候,這些前綴聲明可以讓編譯器使字符串按照前綴類型產生數據。事實上,C++11一共定義了3種這樣的前綴:
u8表示UTF-8編碼
u表示為UTF-16編碼
U表示為UTF-32編碼
3種前綴對應著3種不同的Unicode編碼。一旦聲明了這些前綴,編譯器會在產生代碼的時候按照相應的編碼方式存儲。以上3種前綴加上基于寬字符wchar_t的前綴“L”, 及不加前綴的普通字符串字面量,算來在C++11中,一共有了5種方式來聲明字符串字面量,其中4種是前綴表達的。
不要將各種前綴字符串字面量連續聲明,因為標準定義除了UTF-8和寬字符字符串字面量同時聲明會沖突外,其他字符串字面量的組合最終會產生什么結果,以及會按照什么類型解釋,是由編譯器實現自行決定的。因此應該盡量避免這種不可移植的字符串字面量聲明方式。
C++11中還規定了一些簡明的方式,即在字符串中用'\u'加4個十六進制數編碼的Unicode碼位(UTF-16)來標識一個Unicode字符。比如'\u4F60' 表示的就是Unicode中的中文字符 "你",而'\u597D' 則是Unicode中的 "好"。此外,也可以通過'\U' 后跟8個十六進制數編碼的Unicode碼位(UTF-32)的方式來書寫Unicode字面常量。
下面代碼例子如下:
#include
using namespace std;
int main(){
char utf8[] =u8 "\u4F60\u597D\u597D\u554A";
char16_t utf16[] =u "hello";
char32_t utf32[] =U "hello equals\u4F60\u597D\u554A";
cout<
cout<
cout<
char32_t u2[] =u "hello"; //Error
char u3[] = U "hello"; //Error
char16_t u4=u8 "hello";
}
在本例中,我們聲明了3中不同類型的Unicode字符串utf8、utf16和utf32。由于無論對哪種Unicode編碼,英文的Unicode碼位都相同,因此只有非英文使用了"\u"的碼位方式來標志。
也就是說,一旦使用了Unicode字符串前綴,這個字符串的類型就確定了,僅能放在相應類型的數組中。
u2、u3、u4就是因為類型不匹配而不能通過編譯。
如果我們注釋掉不能通過的定義,編譯并運行,可以得到以下輸出:
你好啊
0x7fffaf087390
0x7fffaf087340
對應于char utf8[] =u8"\u4F60\u597D\u554A"這句,該UTF-8字符串對應的中文是“你好啊”。而對于utf16和utf32變量,我們本來期望它們分別輸出"hello" 及 "hello equals你好啊"。不過實驗機上我們都只得到了一串數字輸出。原因如下:
用戶要在自己的系統上看到正確的Unicode文字,還需要輸出環境、編譯器,甚至是代碼編輯器等的支持。我們可以按照編寫代碼、編譯、運行的順序來看看它們對整個Unicode字符串輸出的影響。
(剩下的這部分的UTF部分的介紹比較高級,暫時還理解不了那么多)
中間跳過一些章節
原生字符串字面量
原生字符串字面量(raw string literal)并不是一個新鮮的概念,在許多編程語言中,我們都可以看到對原生字符串字面量的支持。 原生字符串使用戶書寫的字符串 “所見即所得”,不再需要如'\t'、'\n'等控制字符來調整字符串中的格式,這對編程語言的學習和使用都是具有積極意義的。
順應這個潮流,在C++11中,終于引入了原生字符串字面量的支持。C++11中原生字符串的聲明相當簡單,程序員只需要在字符串前加入前綴,即字母R,并在引號中用使用括號左右標識,就可以聲明該字符串為原生字符串了。
#include
using namespace std;
int main(){
cout<
world)"<
return 0;
}
輸出如下,可以看到'\n'并沒有被解釋為換行。
hello,\n
world
而對于Unicode的字符串,也可以通過相同的方式聲明。聲明UTF-8、UTF-16、UTF-32的原生字符串字面量,將其前綴分別設為u8R、uR、UR就可以了。不過有一點需要注意,使用了原生字符串的話,轉義字符就不能使用了,這會給想使用\u或者\U的方式寫Unicode字符的程序員帶來一定影響。下面看代碼:
#include
using namespace std;
int main(){
cout<
\u597D)"<
cout<
cout<
cout<
cout<
return 0;
}
運行結果如下:
![結果.png-19.6kB][1]
可以看到,當程序員試圖使用\u將數字轉義為Unicode的時候,原生字符串會保持程序員所寫的字面值,所以這樣的企圖并不能如愿以償。而借助文本編輯器直接輸入中文字符,反而可以在實驗機的環境下在文件中有效地保存UTF-8的字符(因為編輯器按照UTF-8編碼保存了文件)。程序員應該注意到編輯器使用的編碼對Unicode的影響。而在之后面的sizeof運算符中,我們看到了不同編碼下原生字符串字面量的大小,跟其聲明的類型是完全一致的。(我沒有看出是完全一致的)
此外,原生字符串字面量也像C的字符串字面量一樣遵從連接規則。代碼如下:
#include
using namespace std;
int main(){
char u8string[] =u8R"(你好)""=hello";
cout<
cout<
return 0;
}
代碼中的原生字符串字面量和普通的字符串字面量會被編譯器自動連接起來。整個字符串有2個3字節的中文字符,以及8個ASCII字節,加上自動生成的\0,字符串的總長度為15字節。與非原生字符串字面量一樣,連接不同前綴的(編碼)的字符串有可能導致不可知的結果,所以程序員總是應該避免這樣使用字符串。
End.
作者:我是小居居
來源:簡書
本文均已和作者授權,如轉載請與作者聯系。
總結
以上是生活随笔為你收集整理的c++11 string u8_深入理解C++11:C++11新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大脚战场插件怎么关闭_PM工具栏插件:H
- 下一篇: 持续低烧的原因有哪些