深入理解C++11:C++11新特性解析与应用之 融入实际应用
对于C++这样的语言来说,除了具有普适、通用等特性外,在一些实际应用方面也是不容忽视的,
对齐支持
数据对齐
在了解为什么数据需要对齐之前,我们可以回顾一下打印结构体的大小这个C/C++中的经典案例。先看代码:
#include <iostream>
using namespace std;
struct HowManyBytes{
char a;
int b;
};
int main(){
cout<<"sizeof(char):"<<sizeof(char)>>endl;
cout<<"sizeof(int):"<<sizeof(char)>>endl;
cout<<"sizeof(HowManyBytes):"<<sizeof(HowManyBytes)>>endl;
cout<<endl;
cout<<"offset of char a:"<<offsetof(HowManyBytes,a)>>endl;
cout<<"offset of int b:"<<offsetof(HowManyBytes,b)>>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<iostream>
using namespace std;
//自定义的ColorVector,拥有32字节的数据
struct ColorVector{
double r;
double g;
double b;
double a;
};
int main(){
//使用C++11中的alignof来查询ColorVector的对齐方式
cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;
return 1;
}
在如上代码,我们使用了C++11标准定义的alignof函数来查看数据的对齐方式。从结果上看,可以看出ColorVector在实验机上依然是对齐到8字节的地址边界上。
c++ alignof(ColorVector):8
现在的计算机通常会支持许多向量指令,而ColorVector正好是4组8字节的浮点数数据,很有潜力改造为能直接操作的向量数据。这样一来,为了能够高效地读写ColorVector大小的数据,我们最好能将其对齐到32字节的地址边界上。
下面代码中,我们利用C++11新提供的修饰符alignas来重新设定ColorVector的对齐方式。
#include <iostream>
using namespace std;
//自定义的ColorVector,对齐到32字节的边界
struct alignas(32)ColorVector{
double r;
double g;
double b;
double a;
};
int main(){
//使用C++11中的alignof来查询ColorVector的对齐方式
cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;
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 <iostream>
using namespace std;
class InComplete;
struct Completed{};
int main(){
int a;
long long b;
auto& c=b;
char d[1024];
//对内置类型和完整类型使用alignof
cout<<alignof(int)<<endl<<alignof(Completed)<<endl; //4,1
//对变量、引用或者数组使用alignof
cout<<alignof(a)<<endl<<alignof(b)<<endl<<alignof(c)<<endl<<alignof(d)<<endl;//4,8,8,1
//本句无法通过编译,Incomplete类型不完整
//cout<<alignof(Incomplete)<<endl;
}
使用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 <iostream>
using namespace std;
struct alignas(alignof(double)*4) ColorVector{
double r;
double g;
double b;
double a;
};
//固定容量的模板数组
template<typename T>
class FixedCapacityArray{
public:
void push_back(T t){/*在data中加入t变量*/}
//...
//一些其他成员函数、成员变量等
//...
char alignas(T) data[1024]={0};
//int length=1024/sizeof(T);
};
int main(){
FixedCapacityArray<char> arrCh;
cout<<"alignof(char):"<<alignof(char)<<endl;
cout<<"alignof(arrCh.data):"<<alignof(arrCh.data)<<endl;
FixedCapacityArray<ColorVector> arrCV;
cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;
cout<<"alignof(arrCV.data):"<<alignof(arrCV.data)<<endl;
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 <iostream>
#include <memory>
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<<alignof(p)<<endl;
}
尝试将vec中的内容按alignof(double)*4的对齐值进行对齐(不过在编写本书的时候,我们的编译器还没有支持std:: align这个新特性,因此代码仅供参考)
(.....剩下的这个部分不是特别懂,放在以后把相关的知识学完后再补)
通用属性
语言扩展到通用属性
随着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语言制定者决定增加了通用属性这个特性。
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<top;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]]。
预定义的通用属性
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<iostream>
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<<"Returned"<<endl;//无法执行该句
return 1;
}
代码清单中,Func调用后的打印语句永远不会执行,因为Func被声明为[[noreturn]].不过由于函数作者的疏忽,忘记了i==0时,Func运行结束时还是会返回mian的。在我们的实验机上,编译运行该例子会在运行时发生"段错误"。当然,具体的错误情况可能会根据编译器和运行时环境的不同而有所不同。不过总的来说,程序员必须审慎使用[[noreturn]].
另外一个通用属性[[carries_dependency]] 则跟并行情况下的编译器优化有关。事实上,[[carries_depency]] 主要是为了解决弱内存模型平台上使用memory_order_consume内存顺序枚举问题。
memory_order_consume的主要作用是保证对当前 "原子类型数据" 的读取操作先于所有之后关于该原子变量的操作完成,但它不影响其他原子操作的顺序。要保证这样的"先于发生" 的关系,编译器往往需要根据memory_model枚举值在原子操作间构建一系列的依赖关系,以减少在弱一致性模型的平台上产生内存栅栏。不过这样的关系则往往会由于函数的存在而被破坏。比如下面的代码:
tomic<int*> a;
...
int*p=(int*)a.load(memory_order_consume);
func(p);
上面的代码中,编译器在编译时可能并不知道func函数的具体实现,因此,如果要保证a.load先于任何关于a(或是p)的操作发生,编译器往往会在func函数之前加入一条内存栅栏。然而,如果func的实现是:
void func(int*p){
//... 假设p2是一个atomic<int*>的变量
p2.store(p,memory_order_release)
}
由于p2.store使用了memory_order_release的内存顺序,因此,p2.store对p的使用会被保证在任何关于p的使用之后完成。这样一来,编译器在func函数之前加入的内存栅栏就变得毫无意义,且影响了性能。同样的情况也会发生在函数返回的时候。
解决的方法就是使用[[carries_dependency]]。该通用属性既可以标识函数参数,又可以标识函数的返回值。
当标识函数的参数时,它表示数据依赖随着参数传递进入函数,即不需要产生内存栅栏。
而当标识函数的返回值时,它表示数据依赖随着返回值传递出函数,同样也不需要产生内存栅栏。
下面是相关的例子:
#include <iostream>
#include <atomic>
using namespace std;
atomic<int*> p1;
atomic<int*> p2;
atomic<int*> p3;
atomic<int*> p4;
//定义了4个原子类型
void func_in1(int*val){
cout<<*val<<endl;
}
void func_in2(int*[[carries_dependency]] val){
p2.store(val,memory_order_release); //p2.store对p的使用会被保证在任何关于p的使用之后完成。
cout<<*p2<<endl;
}
[[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<<*p_ptrl<<endl; //L2
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<<*p_ptr2<<endl;
}
在代码中,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 支持
字符集、编码和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编码方式。
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 <iostream>
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<<utf8<<endl;
cout<<utf16<<endl;
cout<<utf32<<endl;
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 <iostream>
using namespace std;
int main(){
cout<<R"(hello,\n
world)"<<endl;
return 0;
}
输出如下,可以看到'\n'并没有被解释为换行。
hello,\n
world
而对于Unicode的字符串,也可以通过相同的方式声明。声明UTF-8、UTF-16、UTF-32的原生字符串字面量,将其前缀分别设为u8R、uR、UR就可以了。不过有一点需要注意,使用了原生字符串的话,转义字符就不能使用了,这会给想使用\u或者\U的方式写Unicode字符的程序员带来一定影响。下面看代码:
#include <iostream>
using namespace std;
int main(){
cout<<u8R"(\u4F60,\n
\u597D)"<<endl;
cout<<u8R"(你好)"<<endl;
cout<<sizeof(u8R"(hello)") <<"\t"<<u8R"(hello)"<<endl;
cout<<sizeof(uR"(hello)") <<"\t"<<uR"(hello)"<<endl;
cout<<sizeof(UR"(hello)") <<"\t"<<UR"(hello)"<<endl;
return 0;
}
运行结果如下:
![结果.png-19.6kB][1]
可以看到,当程序员试图使用\u将数字转义为Unicode的时候,原生字符串会保持程序员所写的字面值,所以这样的企图并不能如愿以偿。而借助文本编辑器直接输入中文字符,反而可以在实验机的环境下在文件中有效地保存UTF-8的字符(因为编辑器按照UTF-8编码保存了文件)。程序员应该注意到编辑器使用的编码对Unicode的影响。而在之后面的sizeof运算符中,我们看到了不同编码下原生字符串字面量的大小,跟其声明的类型是完全一致的。(我没有看出是完全一致的)
此外,原生字符串字面量也像C的字符串字面量一样遵从连接规则。代码如下:
#include <iostream>
using namespace std;
int main(){
char u8string[] =u8R"(你好)""=hello";
cout<<u8string<<endl; //输出"你好=hello"
cout<<sizeof(u8string)<<endl; //15
return 0;
}
代码中的原生字符串字面量和普通的字符串字面量会被编译器自动连接起来。整个字符串有2个3字节的中文字符,以及8个ASCII字节,加上自动生成的\0,字符串的总长度为15字节。与非原生字符串字面量一样,连接不同前缀的(编码)的字符串有可能导致不可知的结果,所以程序员总是应该避免这样使用字符串。
(问:在程序设计时,如何保证数据对齐)