11C++11通用为本,专用为末_2
8. 显式转换操作符
C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显式的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
class TestString
{
public:
explicit TestString(const int strLen)
//explicit TestString(const int strLen)
{
m_len = strLen;
m_data = new char[m_len + 1];
memset(m_data, m_defChar, m_len);
m_data[m_len] = 0;
}
char m_defChar = '0';
int m_len = 10;
char* m_data = nullptr;
};
void showStr(TestString str)
{
cout << str.m_data << endl;
}
int main()
{
TestString s1(3);
showStr(s1);
//如果构造函数上添加了 explicit 关键字,则不能进行隐式类型构造,以下两句会编译报错
//showStr(3); //编译报错
//showStr('a'); //编译报错
getchar();
return 0;
}
在 c++11 中,标准将 explicit 的使用范围扩展到了自定义的类型转换操作符上,以支持所谓的 “显示类型转换”。explicit 关键字作用于类型转换操作符上,意味着只有在直接构造目标类型或显式类型转换的时候可以使用该类型。
9. 初始化列表
9.1 普通变量
在 c++11 中,自动变量和全局变量的初始化在 C++11 中被丰富了。程序员可以使用以下几种形式完成初始化工作:
-
等号 “=” 加上赋值表达式, 比如 int a = 3 + 4;
-
等号 “=” 加上花括号式的初始化列表, 比如 a = {3 + 4};
-
圆括号式的表达式列表, 比如 int a(3 + 4);
-
花括号式的初始化列表, 比如 int a{3 + 4};
而后两种形式也可以用于获取堆内存 new 操作符中, 比如:
int* i = new int(1);
double* d = new double{1.2f};
9.2 容器也支持列表初始化
vector<int> v{ 1, 3, 5 };
map<int, float> m = { {1, 1.00f}, {2, 2.00f} };
9.3 自定义类型使用列表初始化
标准模板库中容器对初始化列表的支持源自 <initializer_list> 头文件,并且声明一个以 initializer_list<T> 模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化。
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
enum Gender {BOY, GIRL};
class People
{
public:
People(initializer_list<pair<string, Gender>> l)
{
for (const auto &i : l)
{
m_data.push_back(i);
}
}
private:
vector<pair<string, Gender>> m_data;
};
void test()
{
People ship2012 = { {"Rock", BOY}, {"Lily", GIRL} };
}
同样的,函数的参数列表也可以使用初始化列表。
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
void func(initializer_list<int> iv)
{
for (const auto &i : iv)
{
cout << i << "\t";
}
cout << endl;
};
void test()
{
func({});
func({ 1, 3 ,5 ,7 });
}
9.4. 使用列表初始化,可以防止类型收窄
类型收窄一般指一些可以使得数据比哪壶啊或者精度丢失的隐式类型转换。可能导致类型收窄的典型情况如下:
-
从浮点数隐式类型转换为整型数。
-
从高精度浮点数转为低精度浮点数,比如从 long double 隐式地转换为 double, 或者从 double 转为 float。
-
从整型(或者非强类型的枚举)转化为浮点型,如果整型数大到浮点数无法精确地表示,则可以视为类型收窄。
-
从整型(或者非强类型的枚举)转化为较低长度的整型,比如 unsigned char = 1024, 1024 明显不能被一般长度为 8 位的 unsigned char 所容纳,所以也可以视为类型收窄。
类型收窄可以简单地理解为新类型无法表示原有类型数据的值的情况。事实上,发生类型收窄通常也是危险的,应引起程序员的注意。因此,在 C++11 中,使用初始化列表进行初始化的数据编译器是会检查其是否发生类型收窄的。
const int x = 1024;
const int y = 10;
char a = x; //C++98 写法, 收窄,但可以通过编译,编译器警告
char* b = new char(1024); //C++98 写法, 收窄,但可以通过编译,编译器警告
//char c = { x }; //收窄, 无法通过编译
char d = { y }; //未收窄,可以通过编译
//unsigned char e{ -1 }; //收窄, 无法通过编译
float f{ 7 }; //未收窄,可以通过编译
//int g{ 2.0f }; //收窄, 无法通过编译
float h = 1.2l; //未收窄,可以通过编译
10. POD类型
POD 是英文中 Plain Old Data 的缩写。通常用于说明一个类型的属性,尤其是用户自定义类型的属性。Plain 表示了 POD 是个普通的类型,在 c++ 中常见的都是这样的属性,而不像一些存在着虚函数虚继承的类型那么特别。而 Old 则体现了器与 C 的兼容性,比如可以用最老的 memcpy() 函数进行复制, 使用 memset() 进行初始化等。当然,这样的描述都太过笼统,具体地, c++11 将 POD 划分成两个基本概念的合集,即:平凡的(trivial)和标准布局的(standard layout)。
我们先来看一下平凡的定义。通常情况下,一个平凡的类或者结构体应该符合以下定义:
-
拥有平凡的默认构造函数和析构函数 —— 平凡的默认构造函数就是说构造函数“什么都不干”。通常情况下,不定义类的构造函数,编译器就会为我们生成一个平凡的默认构造函数。
-
拥有平凡的拷贝构造函数和移动构造函数
-
拥有平凡的拷贝赋值运算符和移动赋值运算符
-
不能包含虚函数以及虚基类
以上 4 点虽然看似复杂,不过在 c++11 中,我们可以通过一些辅助的类模板来帮助我们进行以上属性的判断。
//template <typename T> struct std::is_trivial;
struct Trivial1 {};
struct Trivial2
{
public:
int a;
private:
static int b;
};
struct NonTrivial1
{
NonTrivial1() :c(11) {}
int c;
};
void test()
{
cout << "is_trivial<Trivial1>::value: " << is_trivial<Trivial1>::value << endl; //1
cout << "is_trivial<Trivial2>::value: " << is_trivial<Trivial2>::value << endl; //1
cout << "is_trivial<NonTrivial1>::value: " << is_trivial<NonTrivial1>::value << endl; //0
cout << "is_pod<Trivial1>::value: " << is_pod<Trivial1>::value << endl; //1
cout << "is_pod<Trivial2>::value: " << is_pod<Trivial2>::value << endl; //1
cout << "is_pod<NonTrivial1>::value: " << is_pod<NonTrivial1>::value << endl; //0
}
POD 包含的另外一个概念是标准布局。标准布局的类或者结构体应该符合以下定义:
(1)所有非静态成员有相同的访问权限(public、private、protected)
//成员 a 与成员 b 拥有不同的访问权限,因此该结构体不是标准布局
//如果去掉 private 关键字的话,那么该结构体就符合标准布局的定义了
struct Test
{
public:
int a;
private:
int b;
}
(2)在类或者结构体继承时,满足以下两种情况之一:
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类
- 基类有非静态成员,而派生类没有非静态成员
(3)类中第一个非静态成员的类型与其基类不同
(4)没有虚函数和虚基类
(5)所有非静态数据成员均符合标准布局类型,其基类也符合标准布局。这是一个递归的定义。
以上 5 点构成了标准布局的含义,最为重要的应该是前两条。
struct B1{};
struct B2{};
struct D1 : B1
{
B1 b;
int i;
};
struct D2 : B1
{
B2 b;
int i;
};
int main()
{
D1 d1;
D2 d2;
cout << "is_standard_layout<D1>::value: " << is_standard_layout<D1>::value << endl; //0
cout << "is_standard_layout<D2>::value: " << is_standard_layout<D2>::value << endl; //1
cout << "is_pod<D1>::value: " << is_pod<D1>::value << endl; //0
cout << "is_pod<D2>::value: " << is_pod<D2>::value << endl; /1
return 0;
}
那么使用 POD 用什么好处呢?
(1)字节赋值,代码中我们可以安全地使用 memset 和 memcpy 对 POD 类型进行初始化和拷贝等操作。
(2)提供对 C 内存布局兼容。 C++ 程序员可以与 C 函数进行相互操作,因为 POD 类型的数据在 C 与 C++ 间的操作总是安全的。
(3)保证了静态初始化的安全有效。静态初始化在很多时候能够提供程序的性能,而 POD 类型的对象初始化往往更加简单。
11. 非受限联合体(略)
12. 用户自定义字面值(略)
13. 内联名字空间(略)
14. 模板的别名
在 c++98 中, 使用 typedef 为类型定义别名。在 c++11 中使用 using 同样也可以定义类型的别名。
#include <iostream>
#include <type_traits>
using namespace std;
void test()
{
cout << "is_same<uint, UINT>::value: " << is_same<uint, UINT>::value << endl; //1
}
使用模板编程时, using 的语法比 typedef 更灵活。
//这里我们模板式地使用了 using 关键字, 将 std::map<T, char*> 定义为了一个 MapString 类型,之后我们还可以使用类型参数对 MapString 进行类型的实例化,而使用 typedef 将无法达到这样的效果。
template<typename T> using MapString = std::map<T, char*>;
MapString<int> numberString;
15. 一般化的 SFINEA 规则
在 c++ 模板中有一条著名的规则, 即 SFINEA - Substitution failure is not an error,中文直译即为“匹配失败不是错误”。更为确切的说,这条规则表示的是对重载的模板的参数进行展开的时候,如果展开导致了一些不匹配,编译器并不会报错。