C++11 学习笔记

1 CppReference

2 alignof(expression) alignas(type or expression)

alignof 和 alignas 用于内存对齐,提供了更灵活的操作,由于该操作之前在各个系统中基本都有实现,这里做了统一.注意 alignas 的参数必须是类型或者 2 的 n 次幂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constexpr int bufmax = 1024;
char buffer1[bufmax];
alignas(int) char buffer2[bufmax]; //对齐到 sizeof(int) 倍
alignas(8) char buffer3[bufmax]; //对齐到 8 的整数倍
// alignas(3) char buffer3[bufmax]; //编译错误,必须对齐到 2 的 n 次幂

auto ai1 = alignof(buffer1); // ai1 的推断结果为 m 类型 unsigned long
auto ai2 = alignof(buffer2);
auto ai3 = alignof(buffer3);

cout<<"alignof(buffer) : "<<ai1<<endl
<<"and ai type is : "<<typeid(ai1).name()<<endl
<<"buffer addr : 0x"<<ios_base::hex<<reinterpret_cast<uint64_t>(buffer1)<<endl
<<"alignof(buffer2) : "<<ai2<<endl
<<"buffer2 addr : 0x"<<ios_base::hex<<reinterpret_cast<uint64_t>(buffer2)<<endl
<<"type name int : "<<typeid(bufmax).name()<<endl;

3 Using 声明别名

using 和 typedef 的使用在类型声明上基本一致,但是 typedef 无法使用在模板定义中 一般t 后缀表示该类型是另一个类型的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
class TestUsing
{
typedef T value_type;
public:
T add(T a, T b) { return a + b; }
};

template <typename T>
using NewName = TestUsing<T>;
typedef TestUsing<int> NewName2;
// template <typename T>
// typedef TestUsing<T> NewName2; //编译错误 typedef不可用于模板, 即定义出的新类型不能使模板。

注意:不允许在类型别名前加修饰符(如 unsigned)

1
2
using Char2 = char;
unsigned Char2 testval; //(BE147) error: expected initializer before ‘testval’

4 初始化

4.1 初始化器的四种格式

  1. X a1{v}; //列表初始化
  2. X a2 = {v}; //列表初始化
  3. X a3 = v;
  4. X a4(v);

4.2 列表初始化可以防止窄化转换,这意味着

  1. 整型和浮点型数据不会向存不下它的方向转化 例如 float=>double double!=>float char=>int int!=>char
  2. 整型和浮点型两种类型的数据不能相互转化 float!=>int int!=>float

4.3 使用auto声明的对象不应该使用列表初始化,因为类型推断的结果可能是 initializerlist<T> (在初始化参数是列表的情况下)

4.4 使用空列表初始化会使用默认值对对象进行初始化,一般是 0 值,指针是 nullptr

4.4.1 char buffer[len]{} 将会把 buffer 中所有的变量初始化成 0 在 buffer 很大的时候可能会产生性能影响

4.5 如果变量没有指定初始化器则

  1. 全局变量、名字空间变量、局部 static 变量和 static 成员(统称为静态对象)将会执行相应数据类型的列表{}初始化
  2. 对于局部变量和自由存储上的对象(堆对象)将会使用默认构造函数(是用户自定义类型且存在默认构造函数)或不执行默认初始化(语言内置类型)

4.6 推断类型:auto 和 decltype()

c++提供了两种从表达式推断数据类型的机制,从编译器返回一个已知的表达式结果的类型

  1. auto 根据对象的初始化器推断对象的数据类型,对象可能是变量、const 或 constexpr
  2. decltype(expr) 推断的对象可能是函数的返回值的类型、类成员的类型和变量的类型

4.6.1 auto 的使用

  1. auto 可以作为初始化器类型的占位符,避免冗余的书写
  2. auto 可以抽象函数中的类型信息,在对象的类型发生变化的时候一定程度上避免函数逻辑受影响,例如 vector 类型替换成 list 类型的迭代循环工作,当然在较大范围内使用 auto 会影响代码可读性和提高错误定位的难度。

4.6.2 类型推断

  1. 我们可以为推断出的类型增加类型说明符和修饰符,比如&或 const
  2. 由于引用类型会自动解引用,所以推断出的类型永远不会是引用类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto const testValA = 100;
auto constexpr testValB = 100;

int main(int argc, char *argv[]) {
cout << "type name : " << typeid(decltype(testValA)).name() << endl;
auto x = decltype(testValA){10};
cout << "type name : " << typeid(decltype(x + 100)).name()
<< " the value is " << x << endl;
int a = 100;
int &ra = a;
auto autora = ra;
cout << "Is lvalue reference: " << is_lvalue_reference<decltype(autora)>::value << endl;
auto &autorra = ra;
cout << "Is lvalue reference: " << is_lvalue_reference<decltype(autorra)>::value << endl;
// auto autoval{100, 200, 300}; //error : direct-list-initialization of ‘auto’ requires exactly one element [-fpermissive]
auto autoval1{100};
cout<<"type of autoval1 : "<<typeid(autoval1).name()<<endl; // i
auto autoval2 = {100};
cout<<"type of autoval2 : "<<typeid(autoval2).name()<<endl; // St16initializer_listIiE
auto autoval3 = {100, 200, 300};
cout << "type of autoval3 : " << typeid(autoval3).name() << endl; // St16initializer_listIiE
}

4.7 左值和右值

对变量分类的两种属性

  • i: 有身份 在程序中有对象的名字或者存在指针\引用指向该对象, 这样我们可以在后面找到它
  • m: 可移动 能把对象移动出来(要看之后还会不会使用它,剩下的对象处于合法但未指定的状态)

根据这两种属性可以将对象分成

  • i => 泛左值
  • i&!m => 左值
  • m => 右值
  • !i&m => 纯右值
  • i&m => 特别值

4.8 对象生命周期

  • 构造函数结束-》析构函数执行
  • 分类:
    1. 自动对象:在函数中声明的对象,再起定义处被创建,超出作用域后销毁,大多数实现中存储在栈帧中
    2. 静态对象:在全局作用域、名字空间作用域中声明或在函数或类中以 static 声明的对象,整个程序运行过程中只被初始化一次,生命周期持续到程序结束,在程序执行过程中地址唯一
    3. 自由存储对象:通过 new 和 delete 直接控制生命周期的对象
    4. 临时对象:在计算中间结果或存放 const 实参引用的值的对象,生命周期视具体情况而定,如果该对象被绑定到一个引用上,则生命周期与该引用一致,否则与它所处的完整表达式一致
      1. 完整表达式:不是任何其他表达式的子表达式
    5. 线程局部对象:声明为 threadlocal 的对象随着线程的创建而创建,随着线程的销毁而销毁。
  • 数组元素和非静态类成员的生命周期由他们所属的对象决定

5 指针、数组与引用

5.1 指针

  • 注意无法将函数指针赋予void类型的指针
  • 想要使用void指针必须先把它转换成其他类型的指针 ,否则编译器不知道如何处理, 包括解引用和 ++
  • nullptr 值只能被赋予指针,不能被赋予数值类型,从而增加了安全性,尤其是当一组重载函数同一参数位置即可以接收整数参数也可以接收指针类型参数的时候不容易出现歧义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int testPtr() {
void (*userfun)(const vector<int> &) = user;
void *testPtr = userfun; //error: invalid conversion from ‘void (*)(const std::vector<int>&)’ to ‘void*’ [-fpermissive]
int *test = (int *)0; // 合法但是无法为每种类型实现一个 NULL 所以 c++中的指针实现为 0 或 0L
int *test = (int *)123; // 这个表达式合法 但是 int *test = 123; 不合法不存在合理的类型转换 123 是整型字面量
// int *test = (void *)0; // 在 C++中不合法,所以 C++中的 NULL 实现为 0 或 0L 但是 c 中的 NULL 实现为(void *)0

int a = 100;
int *pa = &a;
const int *pa1 = &a;
// *pa1 = 200; // error: assignment of read-only location ‘* pa1’ //指向的值是常量
int const *pa2 = &a;
// *pa2 = 300; // error: assignment of read-only location ‘* pa2’ //指向的值是常量
pa2 = pa;
int *const pa3 = &a;
*pa3 = 400;
pa3 = pa; // error: assignment of read-only variable ‘pa3’ //指针本身是常量
}

5.2 数组

  • 申请非自由存储对象数组的时候数组的长度必须是常量表达式(否则编译器无法确定在栈上或者静态存储区上分配内存空间的大小)
  • 注意在作为参数传递数组的时候形参即使写成数组形式,实际上也会被转换成相应的指针,sizeof 的结果会有所不同,所以尽量避免使用数组类型的形参
  • 假设存在内置数组 a 和数组有效范围内的整数 j 则以下表达式等效
    • a[j] = *(&a[0]+j) = *(a+j) = *(j+a) = j[a]
    • 注意 j[a]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int testArrayParam(uint8_t ta[100]) {
cout<<"sizeof(ta) in fun:"<<sizeof(ta)<<" typeid(ta):"<<typeid(ta).name()<<endl;
// sizeof(ta) in fun:8 typeid(ta):Ph
return ta[0];
}
int main(int argc, char *argv[]) {
uint8_t testArray[100];
uint8_t testArray2[100];
// testArray2 = testArray; // error: invalid array assignment
// uint8_t &rtestArray[100] = testArray; // error: declaration of ‘rtestArray’ as array of references
// uint8_t (&rtestArray)[] = testArray; // error: invalid initialization of reference of type ‘uint8_t (&)[] {aka unsigned char (&)[]}’ from expression of type ‘uint8_t [100] {aka unsigned char [100]}’
uint8_t (&rtestArray)[100] = testArray;
cout<<"sizeof(ta) in main:"<<sizeof(rtestArray)<<" typeid(ta):"<<typeid(rtestArray).name()<<endl;
// sizeof(ta) in main:100 typeid(ta):A100_h
testArrayParam(rtestArray);
// uint32_t intArray[2] = {1, 2, 3}; // error: too many initializers for ‘uint32_t [2] {aka unsigned int [2]}’

char testA[] = "abcd";
3[testA] = 'f'; // 不要惊讶 合法。
cout<<testA<<endl; // abcf

}

5.2.1 指针数组和多维数组的小细节

  • 二维数组在函数参数传递的时候必须要附带一维的大小,否则编译器会报错。
  • 指针在进行二维索引的时候 p[x][y] 实际上是做 *(*(p+x)+y)操作,这也说明了为什么二维数组指针需要携带一维维度,因为作为二维数组其实是一个元素是一维数组的一维数组,
void testArray2(int (*pArray)[3]) { return ; }
void testArray3(int **pArray) { return; }
int testArray1() {
int testPA1[] = {1, 2, 3};
int testPA2[] = {4, 5, 6};
int testPA3[] = {7, 8, 9};
int *testPA[] = { testPA1, testPA2, testPA3 };

int testAA[3][3] = {}
//hexo 这里貌似有注入 bug,这里不能写普通数组{1, 2, 3}, {4, 5, 6}, {7, 8, 9};
//这里数组间都插入了 4 个字节的缝隙,对齐到了 16 字节,不知道具体原因
testArray2(testAA);
// testArray3(testAA); // error: cannot convert ‘int (*)[3]’ to ‘int**’ for argument ‘1’ to ‘void testArray3(int**)’
// testArray2(testPA); // error: cannot convert ‘int**’ to ‘int (*)[3]’ for argument ‘1’ to ‘void testArray2(int (*)[3])’
testArray3(testPA);
}
1
2
3
4
5
int test[2][3];
// int *p = test; // error: cannot convert ‘int (*)[3]’ to ‘int*’ in initialization
// p[x][y] 执行的操作是 *(*(p+x)+y)
int *p = test[0];
p[3*1+2] = 1; // test[1][2]; // 对于二维数组也许这样是正确的打开方式, 明显容易出现歧义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int testArray() {
int testAA[3][3] = {};
int testPA1[] = {1, 2, 3};
int testPA2[] = {4, 5, 6};
int testPA3[] = {7, 8, 9};
int *testPA[] = { testPA1, testPA2, testPA3 };

cout<<"dump testAA[i]"<<endl;
for (int i = 0; i != 9; ++i)
cout<<" i : "<<i<<" val : "<<testAA[i]<<endl;

cout<<"dump testPA[i]"<<endl;
for (int i = 0; i != 3; ++i)
cout << "i : " << i << " val : " << testPA[i] << endl;
}
/*
dump testAA[i]
i : 0 val : 0x7ffea9337120
i : 1 val : 0x7ffea933712c testAA + sizeof(int) * 3 * 1
i : 2 val : 0x7ffea9337138
i : 3 val : 0x7ffea9337144 //out of array
i : 4 val : 0x7ffea9337150
i : 5 val : 0x7ffea933715c
i : 6 val : 0x7ffea9337168
i : 7 val : 0x7ffea9337174
i : 8 val : 0x7ffea9337180
dump testPA[i]
i : 0 val : 0x7ffea93370d0
i : 1 val : 0x7ffea93370e0
i : 2 val : 0x7ffea93370f0
*/

5.2.2 字符串

  1. 字面值

    一个很大的变化是字符串字面值在 C++11 中是 const char* 而且可能被存贮到了只读段,这意味这对字面值常量的修改会造成段错误。标准中使用 char*指针接收字面值常量会引发编译错误 gcc 实现中允许了这种操作.

    1
    2
    3
    4
    5
    int testStr() {
    char *str = "test string"; // warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings] 之前的版本是合法的 c++11 要求必须为 const char*
    str[2] = 'a'; // 这里在 gcc version 5.4.0 中崩溃了,应该是字符串字面值被存储到了只读存储区
    char strWritable[] = "test string"; //可修改 但是注意生命周期和上面的不同
    }
  2. 原始字符串

    由于一些类似正则表达式这样的需求,有时候需要频繁在字符串中插入\或者",在这种情况下原本的字符串常量书写变得十分复杂,原始字符串可以在字面量中插入\和"等字符,而不做特殊解释,简化了相关字符串的输入,在需要输入诸如)"这样的字符串序列的时候可以在引号和括号间插入若干字符序列,起到构造字符串字面量的作用,如下面 rawStr2 所示。

    1
    2
    3
    4
    int testStr() {
    const char *rawStr = R"(abcx123\\ "")"; // 原始字符串,转移符和引号按正常字符打印可以插入换行
    const char *rawStr2 = R"*ab(abcx123\\ "")*ab"; // 这里需要注意 "(...)" 格式的原始字符串字面量要求前面的*ab 和后面的*ab 需要匹配,序列被插入到了引号和括号之间,以避免结束部分和字符串内容重叠
    }
  3. 大字符
    • 前缀 L 表示宽字符字面值,通过 wchart 存储,但是编码格式依赖于编译器和具体环境
    1. unicode 编码字符串常量
      • 类型
        1. u8 表示使用 unicode8 格式进行编码 (无法用于 u8'',因为可能存储不下?)
        2. u 表示使用 unicode16 格式进行编码
        3. U 表示使用 unicode32 格式进行编码
      • 注意
        • 前缀 u 和 R 是有序且区分大小写的
        • 前缀 u 实际上已经透露了存储类型(char char16t char32t),所以无法同时使用 L 限定,而且使用的存储类型也不能和 wchart 混用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      int testStr() {
      const char *testc = "testc";
      const char *testu8 = u8"testu8";
      const char *testu8r = u8R"(testu8r)";
      // const char *testu = u"testu"; // error: cannot convert ‘const char16_t*’ to ‘const char*’ in initialization
      // const wchar_t *testu1 = u"testu"; // error: cannot convert ‘const char16_t*’ to ‘const wchar_t*’ in initialization
      const wchar_t *testw = L"testw";
      // const char16_t *testwc16 = testw; // error: cannot convert ‘const wchar_t*’ to ‘const char16_t*’ in initialization

      const char16_t *testu = u"testu";
      const char32_t *testU = U"testU";
      // const char16_t *testlu = Lu"testu"; // ‘Lu’ was not declared in this scope
      // const char16_t *testul = uL"testu"; // ‘uL’ was not declared in this scope
      const char16_t *testuR = uR"(testuR)";
      }

5.3 引用

5.3.1 引用的作用

  1. 不存在空引用或未初始化的引用,因此在代码中可以充当对象的别名,一直指向最开始初始化的那个对象 (非要有空值的话自定义 nullx 也可以 if(&r == &nullx)很反人类的用法)
  2. 语法形式和对象操作相同便于实现运算符重载 (避免了 &x+&y 这种诡异形式,本身的运算符重载定义也是语法错误, C++不允许对内置类型的运算符进行重载)
  3. 和指针一样存储地址,没有其他额外开销
1
2
3
4
5
6
static TestOperator&& operator+(const TestOperator *a, const TestOperator *b) // error: ‘TestOperator&& operator+(const TestOperator*, const TestOperator*)’ must have an argument of class or enumerated type
{ return TestOperator(); }
static int operator+(int a, int b) { // error: ‘int operator+(int, int)’ must have an argument of class or enumerated type
return a+b;
}

5.3.2 引用的分类

类型 说明
左值引用 可以改变值的对象
左值 const 引用 不可以改变值的对象
右值引用 在使用后无需保留的对象

5.3.3 左值引用

  • 引用不是对象,很多情况下没有任何存储空间分配
  • 不能创建引用数组
  • 提供给 T&的值必须是左值 ,提供给 const &T 的值不一定非得是左值,有时候不一定是 T 类型的值
  • 函数潜在的修改外部传入的引用值可读性比较差,应该尽量使用 const 引用,返回修改后的值或者在函数名中进行明确标注
  • 返回引用的函数可以用于赋值的左侧和右侧 比如 map 的索引运算符重载
1
2
3
4
5
6
int testReference() {
const int &a = 1.0; // 指向的临时变量的生命周期到引用的作用域结束
// const int &a{1.0}; // error: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
const int *p = &a;
cout<<p<<endl;
}

5.3.4 右值引用

  • 右值引用可以绑定到右值,但是不可以绑定到左值
  • 右值引用的主要意义在于指向临时对象,允许后续用户对其修改,并认为之后不在使用了。通过允许破坏性读取,来避免一些多余的拷贝操作,从而优化性能。
  • 由于右值引用使用过程中经常会有 staticcast<T&&>(x)这样的操作,所以标准库提供了简化函数 move(x)(因为只是为 x 穿件一个右值引用所以函数名 rval(x)可能更合适), move(x) = staticcast<T&&>(x)
1
2
3
4
5
6
7
8
9
10
11
12
string f() {}
int testReference2() {
string var;
string &r1{var};
// string &r2{f()}; // error: invalid initialization of non-const reference of type ‘@#$’ from an rvalue of type ‘#$%#’
// string &r3{"test_string"}; // error: invalid initialization of non-const reference of type ‘@#$’ from an rvalue of type ‘#$%#’

// string &&rr1{var}; // error: cannot bind ‘#$%$’ lvalue to ‘$%#’
string &&rr2{f()};
string &&rr3{"test_string"};
const string &&rr4{f()};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 新旧两种风格的 swap 比较
template<typename T>
void swap(T &a, T&b) {
T tmp{a};
a = b;
b = tmp;
}

template<typename T>
void new_swap(T &a, T&b) {
T tmp{static_cast<T&&>(a)};
a = static_cast<T&&>(b); // 可以使用 a = move(b); 替代
b = static_cast<T&&>(tmp);
}

void test() {
// 要是想支持右值 swap 还需要对函数进行重载
// new_swap(var,string("123")); // error: invalid initialization of non-const reference of type ‘@#$’ from an rvalue of type ‘@#$@’
}

5.3.5 引用的引用

引用的引用只可以通过 using 的别名结果获取或模板参数获取,而它实际的类型还是原始类型的引用类型,如 int&

  • using rri = int&&;
  • using lri = int&;
  • using rrrri = rri&& => int &&
  • using lrrri = rri& => int &
  • using rrlri = lri&& => int &
  • using lrlri = lri& => int &
  • 上述的规则是左值优先

6 结构体、枚举、联合

6.1 结构体

  • 成员在内存中的顺序一定和声明时候的顺序一致,但是大小却不一定一致(内存对齐)
  • 结构名从声明时出现开始就可以使用了,但是不可以构建对象,因为编译器无法确定其大小,可以声明他的指针,如链表节点的定义
  • 多个结构相互引用需要提前声明 struct List;
  • 为了兼容早期的 C 语言规定,C++允许在同一作用域内同时声明一个 struct 和非 struct,这时默认使用的是非 struct,当想引用 struct 的时候,需要使用 struct 关键字注明 class union enum 类似
  • struct 是一种特殊的 class,他的成员默认访问权限是 public
1
2
3
4
5
6
7
struct TestStruct { int a; };
int TestStruct(struct TestStruct s) { return 0; }

int main(int argc, char *argv[]) {
struct TestStruct s;
TestStruct(s);
}
1
2
3
4
5
6
7
8
9
struct TestStruct {
int x; int y;
TestStruct(int y,int x) : x(x), y(y) { }
};

int main(int argc, char *argv[]) {
struct TestStruct s{1,2};
cout<<"x:"<<s.x<<" y:"<<s.y<<endl; // x:2 y:1
}

6.1.1 普通旧数据 POD(Plain Old Data)

在一些底层或对性能要求比较高的模块中,我们更希望把对象当做纯数据来处理(内存中的连续序列),通常一些高级的语法工具(运行时多态,用户自定义拷贝语句)会让数据变得不纯粹,从而影响操作数据的效率,考虑执行一个拷贝 100 个元素的数组,需要对每个元素执行拷贝构造函数,对比 memcpy 可能只是一个移动机器指令。

  1. 为了确保结构是 POD 的,对象应该满足一下条件
    1. 不具有复杂的布局(比如含有 vptr)
    2. 不具有非标准拷贝语义(用户自定义)
    3. 含有一个最普通的默认构造函数
    4. 本身是内置类型或是 POD 对象数组
  2. POD 必须是属于下列类型的对象
    1. 标准布局类型
    2. 平凡可拷贝类型
    3. 具有平凡默认构造函数的类型
    1. 平凡类型具有一下属性
      1. 一个平凡默认构造函数
      2. 平凡拷贝和移动操作
      3. 当一个默认构造函数无须执行任何实际操作时,我们认为他是平凡的(使用=default 定义默认构造函数)
    2. 一个类型 不含有 以下情况则是 具有标准布局
      1. 含有一个非标准布局的非 static 成员或基类
      2. 含有 virtual 函数
      3. 含有 virtual 基类
      4. 含有引用类型的成员
      5. 其中的非静态数据成员有多种访问修饰符
      6. 阻止了重要的布局优化
        1. 在多个基类中都含有非 static 数据成员
        2. 在派生类和基类中都含有非 static 数据成员
        3. 基类类型与第一个非 static 数据成员的类型相同
    3. 除非在类型内包含非平凡的拷贝、移动操作或析构函数否则该类型就是 平凡可拷贝类型
    4. 让拷贝移动析构函数变得不平凡的原因
      1. 这些操作是用户自定义的
      2. 这些操作所属的类含有 virtual 函数
      3. 这些操作所属的类含有 virtual 基类
      4. 这些操作所属的类含有非平凡的基类或者成员
  3. 标准库 POD 类型的判断

    ispod 是一个标准库类型属性谓词,定义在 typetraits 中通过它可以检验类型是否为 POD

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    struct S0 {};
    struct S1 {int a;};
    struct S2 {int a; S2(int aa):a(aa){}}; // 不是默认构造函数
    struct S3 {int a; S3(int aa):a(aa){} S3(){}}; //是POD 用户自定义默认构造函数
    struct S4 {int a; S4(int aa):a(aa){} S4()=default;};
    struct S5 {virtual void f();}; //虚函数
    struct S6:S1{};
    struct S7:S0{int b;};
    struct S8:S1{int b;}; //不是POD数据既属于S1也属于S8
    struct S9:S0,S1 {};
    struct S10 {int a;int b; S10(int b,int a):a(a),b(b){}};
    void S5::f() {}
    template<typename T>
    void PrintPODType() {
    if (is_pod<T>::value)
    cout<<typeid(T).name()<<" is POD"<<endl;
    else
    cout<<typeid(T).name()<<" is *NOT* POD"<<endl;
    }

    int main(int argc, char *argv[]) {
    PrintPODType<S0>();
    PrintPODType<S1>();
    PrintPODType<S2>();
    PrintPODType<S3>();
    PrintPODType<S4>();
    PrintPODType<S5>();
    PrintPODType<S6>();
    PrintPODType<S7>();
    PrintPODType<S8>();
    PrintPODType<S9>();
    PrintPODType<S10>();
    }

    /*
    2S0 is POD
    2S1 is POD
    2S2 is *NOT* POD
    2S3 is *NOT* POD // 判断错误? 还是说 S3不是pod? 这种情况依赖实现?
    2S4 is POD
    2S5 is *NOT* POD
    2S6 is POD
    2S7 is POD
    2S8 is *NOT* POD
    2S9 is POD
    3S10 is *NOT* POD
    */


6.1.2 位域

程序中可以通过位域限定成员变量使用的位数,从而提供了对外部布局成员变量命名的方法(比如VM中是否位脏页只占1个位,可以手动做位操作但是使用位域可以增强可读性)。 关于位域需要注意一下几点

  1. 无法获取位域的地址
  2. 位域虽然可能节省了结构本身的内存占用,但是会显著增加操作它的二进制代码长度和时钟周期,位域本质是编译器生成的位逻辑运算
  3. 只可以使用整型和枚举类型声明位域
  4. 可以使用匿名位域占位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct PPN {
unsigned int PFN : 22;
int : 3;
unsigned int CCA : 3;
bool nonreachable : 1;
bool dirty : 1;
bool valid : 1;
bool global : 1;
};

int main(int argc, char *argv[]) {
PPN ppn{};
ppn.dirty = 1;
}
/*
Dump of assembler code for function main(int, char**):
0x000000000040354d <+0>: push %rbp
0x000000000040354e <+1>: mov %rsp,%rbp
0x0000000000403551 <+4>: mov %edi,-0x14(%rbp)
0x0000000000403554 <+7>: mov %rsi,-0x20(%rbp)
=> 0x0000000000403558 <+11>: movl $0x0,-0x10(%rbp)
0x000000000040355f <+18>: movzbl -0xd(%rbp),%eax
0x0000000000403563 <+22>: or $0x20,%eax
0x0000000000403566 <+25>: mov %al,-0xd(%rbp)
0x0000000000403569 <+28>: mov $0x0,%eax
0x000000000040356e <+33>: pop %rbp
0x000000000040356f <+34>: retq
End of assembler dump.
*/

6.2 联合体

union 是一种特殊的结构体,所有的成员都分配在同一个地址上,一个 union 实际占用的大小和它的最大成员相同,自然同一时刻只能保存一个成员的值。 union 有很多限制,因为比如复制的时候根本不知道该使用那个复制构造函数云云

  • 注意 如果联合中包含了具有用户自定义析构函数等被 delete 掉的函数,需要在适当的时候显示调用 例如
    1. 类内包含匿名联合其中包含 string s 对象,则在析构此类对象的时候考虑是否需要 s.~string();
    2. 上例类构造\设置对象的时候是否需要 new(&s) string(); 显式执行 string 构造函数

6.2.1 union 的限制

  1. union 不能含有虚函数
  2. union 不能含有引用类型的成员
  3. union 不能含有基类
  4. 如果 union 的成员含有用户自定义的构造函数,拷贝操作、移动操作或者析构函数则此类函数对union来说被delete掉了 union类型的对象不能含有这些函数
  5. 在 union 的所有成员中,最多只能有一个成员包含类内初始化器
  6. union 不能作为其他类的基类

6.2.2 匿名联合

在类中声明没有名字的联合体会生成一个匿名连个对象,在类的其他成员函数中可以直接使用匿名联合对象内的字段。可以搭配枚举tag来更安全的使用联合。

1
2
3
4
5
6
7
8
9
10
union U1 {
int a;
const char *p{""};
int test() {return this->a;}
};
int main(int argc, char *argv[]) {
U1 u1;
cout<<u1.test()<<endl; // 打印p指向的地址
// U1 u2{7}; //error: no matching function for call to ‘U1::U1(<brace-enclosed initializer list>)’
}

6.3 枚举

有两种类型的枚举

6.3.1 enum class 它的枚举值名字位于 enum 的作用域内(枚举名字可以通过枚举名::来明确限定不会和其他枚举内的枚举名重复),枚举值不会隐式的转换成其他类型。

  • 使用 staticcast<underlyingtype<EnumType>::type>(type) 把枚举转化成整型操作更靠谱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 enum class Light: uint8_t;
uint8_t ReadVal(Light &flag) {
return static_cast<uint8_t>(flag);
}
enum class Light: uint8_t { Red, Green, Yellow };
enum class Flag : uint8_t {
Nil = 0,
Red = 1,
Yellow = 2,
Blue = 4
};
constexpr Flag operator&(Flag var, Flag flag) { // if(f1 & Flag::Red) { //error: could not convert ‘operator&(f1, (Flag)1u)’ from ‘Flag’ to ‘bool’
return static_cast<Flag>(static_cast<char>(var)&static_cast<char>(flag));
}
constexpr Flag operator|(Flag var, Flag flag) {
return static_cast<Flag>(static_cast<char>(var)|static_cast<char>(flag));
}
int main(int argc, char *argv[]) {
// Light s1 = 1; // error: cannot convert ‘int’ to ‘Light’ in initialization
// uint8_t i1 = s1; // error: cannot convert ‘Light’ to ‘uint8_t {aka unsigned char}’ in initialization
// uint8_t i2 = Light::Red; // error: cannot convert ‘Light’ to ‘uint8_t {aka unsigned char}’ in initialization
// Light S2 = Red; // error: ‘Red’ was not declared in this scope
Light S3 = Light::Red;
// if (S3 == Flag::Red); // error: no match for ‘operator==’ (operand types are ‘Light’ and ‘Flag’)
if (S3 == Light::Red);
Flag f1 = Flag::Red | Flag::Blue;
if((f1 & Flag::Red) != Flag::Nil) {
cout<<"f1 has Red"<<endl;
}
switch(f1) { // warning: enumeration value ‘Blue’ not handled in switch [-Wswitch]
case Flag::Red:
cout<<"Red"<<endl;
case Flag::Yellow:
cout<<"Yellow"<<endl;
case Flag::Red&Flag::Yellow:
cout<<"Red&Yellow"<<endl;
}
Flag f2 {};
cout<<"f2: "<<(int)static_cast<uint8_t>(f2)<<endl;
f2 = static_cast<Flag>(1234); //超出范围
cout<<"f2: "<<(int)static_cast<uint8_t>(f2)<<endl;
}
/*
f1 has Red
f2: 0
f2: 210
*/


enum class AllocatorType { AllocatorX,AllocatorY };
void *operator new(size_t size, AllocatorType type) {
cout<<"new allocator type : "<<static_cast<underlying_type<AllocatorType>::type>(type)<<endl; //underlying_type 这种方式更优雅
}

6.3.2 plain enum 它的枚举值名字和枚举类型本身位于同一作用域,枚举值可以隐式的被转化成整数。

对于普通枚举类型,如果没有指定潜在类型,则无法先声明后定义 普通枚举可以匿名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// enum TestEnum1;  // error: use of enum ‘TestEnum’ without previous declaration
// enum TestEnum1 { TestEnumA,TestEnumB };
enum class TestEnum2;
enum class TestEnum2 { TestEnumA,TestEnumB };
enum Light : uint8_t;
uint8_t ReadVal(Light &flag) {
return static_cast<uint8_t>(flag);
}
enum Light: uint8_t { Red, Green, Yellow };
enum Flag : uint8_t {
Nil = 0,
// Red = 1, // error: redeclaration of ‘Red’
FlagRed = 1,
FlagYellow = 2, // error: redeclaration of ‘Yellow’
Blue = 4
};
constexpr Flag operator&(Flag var, Flag flag) { // if(f1 & Flag::Red) { //error: could not convert ‘operator&(f1, (Flag)1u)’ from ‘Flag’ to ‘bool’
return static_cast<Flag>(static_cast<char>(var)&static_cast<char>(flag));
}
constexpr Flag operator|(Flag var, Flag flag) {
return static_cast<Flag>(static_cast<char>(var)|static_cast<char>(flag));
}
int main(int argc, char *argv[]) {
// Light s1 = 1; // error: invalid conversion from ‘int’ to ‘Light’ [-fpermissive]
Light s1;
// s1 = 1; // error: invalid conversion from ‘int’ to ‘Light’ [-fpermissive]
uint8_t i2 = Light::Red;
Light S2 = Red;
uint8_t i1 = S2;
Light S3 = Light::Red;
// if (S3 == Flag::Red); // error: ‘Red’ is not a member of ‘Flag’
if (S3 == Light::Red);
Flag f1 = Flag::FlagRed | Flag::Blue;
if((f1 & Flag::FlagRed) != Flag::Nil) {
cout<<"f1 has Red"<<endl;
}
switch(f1) { // warning: enumeration value ‘Blue’ not handled in switch [-Wswitch]
case Flag::FlagRed:
cout<<"Red"<<endl;
case Flag::FlagYellow:
cout<<"Yellow"<<endl;
case Flag::FlagRed&Flag::FlagYellow:
cout<<"Red&Yellow"<<endl;
}
Flag f2 {};
cout<<"f2: "<<(int)static_cast<uint8_t>(f2)<<endl;
f2 = static_cast<Flag>(1234); //超出范围
cout<<"f2: "<<(int)static_cast<uint8_t>(f2)<<endl;
}

7 语句

语句是C++中的一个逻辑执行单元,一般以表达式+分号构成,也包括{}块,声明和for等执行流控制语句和 try 语句块

7.1 声明

声明的同时执行初始化器,这也就意味着在声明变量的时候初始化可以写的很复杂,在函数内部,由于声明是一个普通的语句,意味着它可以在需要的时候才出现,很大程度减小了未初始化变量存在的可能性,对于常量的初始化这有跟大帮助,可以通过经过一系列计算的结果初始化它,这同时也增加了程序的局部性,可读性更好。

1
2
3
4
5
6
class TestInit { public: TestInit() { cout<<"test init"<<endl; } };
int testinit() {
TestInit testobj;
return 1;
}
static int a = testinit(); //完全可以实现一个注册器

7.2 分支

对于判断的条件表达式,算数类型和指针类型可以隐式转换成 bool 类型,enum class 不可以隐式转换

7.2.1 switch

  • case 标签中出现的表达式必须是整型或枚举类型的常量表达式,switch 中一个值只能被case标签使用一次。
  • default 语句一般用于异常情况的处理或者默认情况的处理,但是对于枚举,如果没有 default 标签,则编译器会检查 switch 对没有出现的标签进行警告,在未来可能会扩展枚举值的情况,这样的警告可能会比较有用,所以在 switch 枚举值的时候可以考虑避免使用default
  • case 内可以声明变量,但是不能初始化变量,除非使用{}语句块,在块内初始化
constexpr int testconstfun(int v) {return v+1;}
int main(int argc, char *argv[]) {
constexpr int testval = 2;
int a = 1;
switch (a)
{
case 1:
cout<<"1"<<endl; break;
case testval:
cout<<"2"<<endl; break;
case testconstfun(testval):
cout<<"3"<<endl; break;
// case testconstfun(testval): // error: duplicate case value
// cout<<"can not"<<endl; break;
}

// double d = 1.0;
// switch (d) { // error: switch quantity not an integer
// case 1.0: // error: could not convert ‘1.0e+0’ from ‘double’ to ‘<type error>’
// cout<<"1.0"<<endl; break;
// }
int x = 1;
switch (x) {
case 3:
{
int zz = 1;
}
case 1:
int y;
// int z = 1; // error: jump to case label [-fpermissive] note: crosses initialization of ‘int z’
case 2:
cout<<"Y:"<<y<<endl; //可以使用但是这个值是未初始化的。
}
}
  • if 条件语句中声明的变量可以再if和else中使用
1
2
3
4
5
6
7
8
9
10
11
int get_val(int a) {
return a + 1;
}
int main(int argc, char *argv[]) {

if (int a = get_val(1)) {
cout<<"a in if: "<<a<<endl;
} else {
cout<<"a in else: "<<a<<endl;
}
}

7.3 循环语句

  • for 初始化语句,要么是个声明语句,要么是个表达式语句(不能是{}或者另一个执行流控制语句等)。

7.3.1 范围 for

  • for(T v:c) 可以理解成对于 c 中的每个 T 类型的 v 对象执行循环体, v 必须是个序列这意味着,要不 v 是内置数组类型,要不 v.begin()和 v.end()或者 begin(v)和 end(v)得到的是迭代器
  • 遍历方法的使用规则
    1. 优先尝试成员函数 begin()和 end()
    2. 在外层作用域寻找 begin()和 end()成员
    3. 遍历错误
  • 对于内置数组 T v[N] 编译器使用 v 和 v+N 代替 begin 和 end
  • <iterator> 为所有内置数组和标准库容器提供了 begin(c)和 end(c) 当然我们可以自己定义 begin 和 end 以适应自定义容器
  • for(T &v:c) 使用引用遍历序列,可以修改序列内对象的值,或避免对象拷贝
1
2
3
4
5
6
int main(int argc, char *argv[]) {
vector<int> testvec{1,2,3,4,5,6,7,8,9};
for(const int &v:testvec) {
cout<<v<<endl;
}
}

7.3.2 for 语句

  • for 的死循环可以用 for(;;) 无需 for(;true;) 似乎 while(true)更符合习惯
  • 以下三个循环等效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[]) {
vector<int> testvec{1,2,3,4,5,6,7,8,9};
cout<<"++++++++++1+++++++++++++"<<endl;
for(const int &v:testvec) { cout<<v<<endl; }

cout<<"++++++++++2+++++++++++++"<<endl;
for(auto i = begin(testvec); i != end(testvec); ++i) { cout<<*i<<endl; }

cout<<"++++++++++3+++++++++++++"<<endl;
{
auto i = begin(testvec);
while (i != end(testvec))
{
cout << *i << endl;
++i;
}
}
}

7.3.3 退出循环

  • 用于退出循环的语句有 break return throw goto 或者像 exit()这么间接的方式
  • break 用于跳出最内层循环或 switch
  • continue 语句用于跳转到本次循环的末尾,然后执行递增循环条件语句并检查循环条件

7.3.4 go 语句

  • go 标签的作用域是标签所处的函数,这意味着你可以跳进跳出块,但是限制是:
    1. 不能跳过初始化器
    2. 不能跳入异常处理程序
  • go 语句一个比较有意义的用法是跳出多层的循环语句,不需要逐层判断后逐层 break

8 表达式

因为有些地域的键盘不容易输入&等符号或者一些程序员不喜欢的原因 C++11 增加了一组关键字代替他们

and &&
andeq &=
bitand &
bitor  
compl -
not !
noteq !=
or  
oreq  
xor ^
xoreq ^=
  • 在开始运算前,尺寸小于 int 型的运算对象(如 bool char)会先转换成 int 类型
  • 关系运算符(= < 等)的结果是布尔类型
  • C++中 并没有明确规定 表达式中子表达式的求值顺序,并不能假定从左到右或是从右到左(|| && , 三个运算符明确了求值顺序 b=(a=2,a+1)结果一定是 3)
  • 注意函数调用中的逗号和逗号表达式的逗号是两回事儿
  • 对于接受左值运算对象的运算符来说,他的结果是一个表示该左值运算对象的左值 例如:
1
2
3
4
5
6
7
8
void testopt2() {
int x,y;
int j = x = y;
int *p = &++x;
int *q = &(x++); //error: lvalue required as unary ‘&’ operand x++返回的是++之前的临时变量,不是左值
int *p2 = &(x>y?x:y);
int &r = (x<y)?x:1; //error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’ 1 不是左值
}

8.1 C++提供了两种与常量有关的概念

  • constexpr: 编译时求值 主要用于编译时求值,优化性能
  • const: 不一定在编译时求值在作用于内不改变其值 主要用于接口中规定不可修改的部分
    • 用常量表达式初始化的 const 可以用在常量表达式中。

8.1.1 constexpr 常量表达式

使用在常量声明前,把常量存储在静态存储区,提高安全性。 要求: 不能包含编译时未知的值,也不能具有其他副作用, 可以由整数 浮点 枚举 地址(const char*)构成,可以结合任何不会修改状态的运算符(比如 + ?: [] 但是= 和 ++是不可以的) 作用:

  1. 明明常量使得代码易于维护
  2. 保护值不被修改
  3. C++要求数组的尺寸,case 标签 tmplate 值实参使用常量
  4. 嵌入式系统中只读存储器更廉价,
  5. 多线程中不存在竞争
  6. 提高性能
1
2
3
4
5
6
7
8
9
10
11
12
13
constexpr int TestFun(int testval) {
return testval * 2;
}

const int x = 7;
const string s = "asdf";
const int y = sqrt(x);

constexpr int TestVal = TestFun(2);
constexpr int xx = x;
// constexpr string ss = s; // error: constexpr variable cannot have non-literal type 'const string' (aka 'const basic_string<char>') //string 对象无法在编译期决定
// constexpr int yy = y; // error: constexpr variable 'yy' must be initialized by a constant expression // x 不是 constexpr

  • 使用在函数声明和定义前,制定常量表达式,在函数的所有参数均为常量表达式的时候函数返回一个常量表达式,如果参数非均为常量,则像普通函数一样工作避免了重复定义。
  • 含有 constexpr 构造函数的类称为字面值常量类型,构造函数必须足够简单才能被声明成 constexpr,简单意味着,构造函数的函数体为空,且所有的成员都是用潜在的常量表达式初始化的。(或者干脆不提供构造函数,完全使用列表初始化)
  • 注意 constexpr 的成员函数同时是 const 的
  • 在使用 constexpr 的时候任何对象都无法改变值或者造成什么其他影响(编译时使用微型函数式语言)
constexpr int isqrt_helper(int sq, int d, int a) { return sq <= a ? isqrt_helper(sq+d,d+2,a):d; }
constexpr int isqrt(int x) { return isqrt_helper(1,3,x)/2 - 1; }

struct Point {
int x,y,z;
constexpr Point (int px, int py) : x(px),y(py), z(0) { }
// constexpr Point (int px, int py) : x(px),y(py) { } // error: member ‘Point::z’ must be initialized by mem-initializer in ‘constexpr’ constructor
constexpr Point (int px, int py, int pz) : x(px),y(py),z(pz) { }
constexpr Point up(int d) { return {x,y,z+d}; }
constexpr Point move(int dx,int dy) { // There need not 'const'
// this->x += 100; //error: assignment of member ‘Point::x’ in read-only object
return {x+dx, y+dy, z};
}
};

/* //或者这样也可以
struct Point {
int x,y,z;
constexpr Point up(int d) { return {x,y,z+d}; }
constexpr Point move(int dx,int dy) { // There need not 'const'
return {x+dx, y+dy, z};
}
}; */

constexpr Point origo { 0, 0 };
constexpr int z = origo.x;
constexpr Point constarry[] = { origo,Point{1,1},Point{2,2},origo.move(1,2) };

int squareF(int x) { return x * x; }

constexpr int x = constarry[1].x;
// constexpr Point xy{1,squareF(4)}; // error: call to non-constexpr function ‘int squareF(int)’

constexpr int square(int x) { return x*x; }
constexpr int radial_distance(Point p) {
return isqrt(square(p.x) + square(p.y) + square(p.z));
}

constexpr Point p1 {10,20,30};
constexpr Point p2 {p1.up(20)};
constexpr int dist = radial_distance(p2);

int main(int argc, char *argv[]) {
const Point testpoint { 10,20,30 };
testpoint.move(100,200);
}

8.2 隐式转型

C++允许整数类型和浮点类型在表达式中混合使用,在可能的情况下值的类型会自动转化避免损失信息,但是有时候也会发生窄化类型转换

8.2.1 整型提升转换规则

  1. 如果一个 char,signed char, unsigned char, short int 或 unsigned short int 类型的原数值可以被 int 类型表示,则转换成 int 类型,否则转换成 unsigned int
  2. char16t,char32t,wchart 或者 平凡枚举类型(enum)的值被转换成下列第一个可以表示他们潜在类型原数值的类型: int, unsigned int, long, unsigned long, unsigned long long
  3. 位域如果可以被一个 int 型表示则 转换成 int 型,否则尝试转换成 unsigned int 型,如果 unsigned int 也无法表示该位域,则不会对它进行整型提升。
  4. bool 类型转换成 int 类型,false 转换成 0,true 转换成 1

8.2.2 类型转换

  • {}可以避免窄化类型转换
  • numericlimits 可以确保截断以一种可以移植的方式进行
  • 如果窄化类型转换确实无法避免,可以使用 narrowcast<>()
  1. 整数类型转换
    • 如果被转换成无符号整型,则保留低位,高位被截断
    • 如果被转换成有符号整型,则结果依赖于实现如 signed char sc = 1023 结果可能是 127 或-1 (gcc 是-1)
  2. 浮点数类型转换
    • 一个浮点值可以被转换成其他类型的浮点值,如果原值可以被完全目标类型表示,则结果和原值相同,如果原值在两个相邻的低精度目标值之间,则结果是他们之中的一个(gcc 是四舍五入),否则结果未定义。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    double testd = 0.1234567890123456;
    float testf = testd;
    cout<<setprecision(16)<<testd<<endl; // 0.1234567890123456
    cout<<setprecision(16)<<testf<<endl; // 0.1234567910432816 //这里从第 8 个有效数字开始被截断,四舍五入进了一位

    testd = 0.1234567810123456;
    testf = testd;
    cout<<setprecision(16)<<testd<<endl; // 0.1234567810123456
    cout<<setprecision(16)<<testf<<endl; // 0.123456783592701

    testd = 1E300;
    testf = testd;
    cout<<testd<<endl; // 1e+300
    cout<<testf<<endl; // inf
  3. 指针和引用类型转换
    • 任何指向对象的指针都能隐式地转换成 void *
    • 指向派生类的指针/引用都能隐式的转换成指向其可访问的且明确无二意的基类
    • 指向函数的指针和指向成员的指针不能隐式的转换成 void *
    • 求值结果为 0 的常量表达式可以转换成任何类型的或指向成员的空指针
    • 最好直接使用 nullptr
    • T*可以隐式的转换成 const T*, T& 可以隐式的转换成 const T&
    • TODO 补充指向成员的指针转换规则
  4. 布尔类型转换
    • bool 类型转换成 int 类型,false 转换成 0,true 转换成 1
    • 指针、整数、浮点数都能隐式的转换成 bool 类型。非 0 的值对应 true,0 值对应 false
  5. 浮点数和整数相互转换
    • 浮点数向整数转换则丢弃小数部分,如果源值无法被目标类型表示,则行为未定义
    • 整数向浮点数转换都是合法的但是如果目标浮点数的精度无法表示源整数,则会丢失精度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int main(int argc, char *argv[]) {                                                          
    int i = 1234567890;
    float f = i;
    cout<<"int "<<i<<" to float "<<f<<endl;

    double d = 123456789012345;
    i = d;
    cout<<"double "<<d<<" to int "<<i<<endl;

    d = 0.123456789012345;
    i = d;
    cout<<"double "<<d<<" to int "<<i<<endl;
    }

    /*
    test init
    int 1234567890 to float 1234567936
    double 123456789012345 to int -2147483648
    double 0.123456789012345 to int 0
    */


  6. 二元运算符的转化规则
    • 按照下面的顺序当出现两个对象其中一个是这个类型的时候另一个也转换成这个类型
    • long double > double > float
    • 如果到 float 都没有则先对两个操作数进行整形提升去除掉 char short 枚举 位域 和 bool 类型, 然后按下面的规则统一类型
    • unsigned long int 和 long long int, 如果 long long int 能够表示所有 unsigned long int 的值的时候 转换成 long long int 否则转换成 unsigned long long int , 这个规则中所有的类型去掉一个 long 的时候同样适用
    • 之后按照下面的顺序
    • long > unsigned > int
    • 有符号的和无符号的就尽量别混着用

8.3 new 和 delete

  • new 和delete分配和释放对象, new[]和 delete[]用于分配和释放数组
  • T *o = new T 和 T *o = new T()的区别,对于用户自定义类型,两者一样,对于内置类型,后者会把它初始成默认值(0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class TestA {                                                                                                                 
public:
int a;
TestA() : a{ }
};
int main(int argc, char *argv[]) {
TestA *a = new TestA();
TestA *b = new TestA;

int *d = new int();
int *c = new int;
}

/*
Dump of assembler code for function main(int, char**):
0x0000000000405c3d <+0>: push %rbp
0x0000000000405c3e <+1>: mov %rsp,%rbp
0x0000000000405c41 <+4>: push %rbx
0x0000000000405c42 <+5>: sub $0x38,%rsp
0x0000000000405c46 <+9>: mov %edi,-0x34(%rbp)
0x0000000000405c49 <+12>: mov %rsi,-0x40(%rbp)
=> 0x0000000000405c4d <+16>: mov $0x4,%edi
0x0000000000405c52 <+21>: callq 0x4042c0 <_Znwm@plt>
0x0000000000405c57 <+26>: mov %rax,%rbx
0x0000000000405c5a <+29>: mov %rbx,%rdi
0x0000000000405c5d <+32>: callq 0x405efe <TestA::TestA()>
0x0000000000405c62 <+37>: mov %rbx,-0x30(%rbp)
0x0000000000405c66 <+41>: mov $0x4,%edi
0x0000000000405c6b <+46>: callq 0x4042c0 <_Znwm@plt>
0x0000000000405c70 <+51>: mov %rax,%rbx
0x0000000000405c73 <+54>: mov %rbx,%rdi
0x0000000000405c76 <+57>: callq 0x405efe <TestA::TestA()>
0x0000000000405c7b <+62>: mov %rbx,-0x28(%rbp)
0x0000000000405c7f <+66>: mov $0x4,%edi
0x0000000000405c84 <+71>: callq 0x4042c0 <_Znwm@plt>
0x0000000000405c89 <+76>: movl $0x0,(%rax) //=======> 注意这里
0x0000000000405c8f <+82>: mov %rax,-0x20(%rbp)
0x0000000000405c93 <+86>: mov $0x4,%edi
0x0000000000405c98 <+91>: callq 0x4042c0 <_Znwm@plt>
0x0000000000405c9d <+96>: mov %rax,-0x18(%rbp)
0x0000000000405ca1 <+100>: mov $0x0,%eax
0x0000000000405ca6 <+105>: add $0x38,%rsp
0x0000000000405caa <+109>: pop %rbx
0x0000000000405cab <+110>: pop %rbp
0x0000000000405cac <+111>: retq
End of assembler dump.
*/


8.4 重载 new(放置语法)

  • new 运算符可以通过 void* operator new(sizet s, …);的方式进行重载,对象的大小的 new 运算符隐式计算的,所以返回的是 void 类型指针。调用的方式为 new(这里的内容被传到 sizet 后面当做形参),编译器根据常规的实参匹配规则确定哪个版本的 new 运算符函数将会被执行,但是每一个 operator new 函数都以 sizet 作为第一个实参, 头文件<new>中实现了 void*的重载版本,使用 void*指针指向的地址存储对象
  • 放置式 delete 即 void operator delete(void *p, …) 在对象构建失败的时候被调用,调用的重载版本根据对象 new 的时候使用的版本进行匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct BadClass {
BadClass() { throw std::runtime_error(""); }
};
enum class AllocatorType { AllocatorX,AllocatorY };
void *operator new(size_t size, AllocatorType type) {
cout<<"new allocator type : "<<static_cast<underlying_type<AllocatorType>::type>(type)<<endl;
return malloc(size);
}
void operator delete(void *p, AllocatorType type) {
free(p);
}
int main(int argc, char *argv[]) {
try {
BadClass *a = new(AllocatorType::AllocatorY) BadClass; // 在对象 *构建失败* 的时候会根据 new 的重载类型匹配相应的 delete 重载版本
// 这里因为构建失败(抛出了异常)所以调用了重载版本的 delete 具体见 http://en.cppreference.com/w/cpp/memory/new/operator_delete
} catch (const std::exception &) { }
int *i = new(AllocatorType::AllocatorY) int;
delete i; //这里没有构建失败,所以不会调用 delete 的重载版本,这个时候使用 C++的默认 delete 函数中的分配器释放了 malloc 分配的指针,程序崩溃
}

  • nothrow(nothrowt 的一个对象用于触发重载版本)的new和delete重载版本不会抛出异常
  • nothrow 只确保不抛出badalloc, 但是有可能抛出其他异常,比如在构造函数中出现的异常。

8.5 列表

  • 实现模型
    1. 如果{}列表被用作构造函数的实参,则其实现过程与使用()列表相似,除非列表的元素以值传递的方式传递给构造函数,否则不会拷贝列表的元素
    2. 如果{}列表被用于初始化一个聚合体(数组或没有提供构造函数的类)的元素,则列表的每个元素分别初始化聚合体中的一个元素,除非列表的元素以值传递的方式传递给构造函数,否则不会拷贝列表的元素
    3. 如果{}列表被用于构建一个intializerlist对象,则列表的每个元素分别初始化initializerlist的底层数组的一个元素,通常情况下我们把元素从initializerlist拷贝到实际使用它们的地方
  • 列表初始化命名变量的两种表现形式
    1. 限定列表,部分情况可以作为表达式,限定为某种类型,如T{…}意思是创建一个T类型的对象,并用{…}初始化它
    2. 未限定的列表{…},根据上下文确定, 注意 这里没有表明要创建的对象的类型, 所以他需要能够明确知道所用类型。使用上限于一下场景
      • 函数实参
      • 返回值
      • 赋值运算符 = += *=等
      • 下标 这个在 GCC里面不好使 规范里面 这里提到了 (9)这项
1
2
3
4
5
6
7
8
9
10
11
12
13
int test_f(int a) { return a; }

int test_initializer() {
int v {7};
int v2 = {7};
int testarray[5];
// testarray[{1}]; // error: array subscript is not an integer
v = {8};
v += {99};
// v = 7 + {10}; error: initializer list cannot be used on the right hand side of operator '+'
test_f({100});
return {11};
}

8.6 显示类型转换

很多时候C++的类型转换是隐式执行的,但是有些时候我们必须显示地转换类型 C++提供的显示类型转换操作如下:

  1. 构造,使用{}符号提供对新值类型安全的构造
  2. 命名转换,提供不同等级的类型转换:
    • constcast, 对某些声明为const的对象获取写入权限
    • staticcast, 反转一个定义良好的隐式类型转换
    • reinterpretcast, 改变位模式的含义
    • dynamiccast, 动态地检查类层次关系
  3. C风格的转换,提供命名的类型转换或其组合
  4. 函数化符号,提供C风格转换的另一种形式

8.6.1 构造

用值e构造一个类型为T的值可以表示为 T{e} T{v}的好处是只执行行为良好的类型转换,不会发生窄化或截断除非有明确的T类型构造函数接收此转换 显式构造的未命名对象是临时对象,生命周期限于表达式 T{} 表示为类型T的默认值

  • 内置类型初始化为0值
  • 用户自定义类型则执行默认构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DefaultSubClass {
public:
DefaultSubClass() { cout << "default sub" << endl; }
};

class DefaultInit {
private:
DefaultSubClass sub;
public:
DefaultInit() = delete;
DefaultInit(DefaultInit &org) {}
};

class BaseClass1 {
public:
void Hello() { cout << "hello baseclass1" << endl; }
};

class BaseClass2 {
public:
virtual void Hello() { cout << "hello baseclass2" << endl; }
};

class DerivedClass : public BaseClass2 {
private:
int x;
public:
virtual void Hello() { cout << "hello derived class x is " << x << endl; }
};

int main(int argc, char *argv[]) {
//DefaultInit def = DefaultInit{}; // error: call to deleted constructor of 'DefaultInit'
double a = 100.123; int b = static_cast<int>(a);
BaseClass2 c2;
// BaseClass1 c1 = static_cast<BaseClass1>(c2); // error: no matching conversion for static_cast from 'BaseClass2' to 'BaseClass1'
// BaseClass1 *pc1 = static_cast<BaseClass1 *>(&c2); // error: static_cast from 'BaseClass2 *' to 'BaseClass1 *', which are not related by inheritance, is not allowed

DerivedClass *pd1 = static_cast<DerivedClass *>(&c2);
DerivedClass *pd2 = dynamic_cast<DerivedClass *>(&c2); //如果不存在任何virtual函数则类似转换会报错 error: 'BaseClass2' is not polymorphic
DerivedClass d3;
BaseClass2 *pb1 = &d3;
DerivedClass *pd3 = dynamic_cast<DerivedClass *>(pb1);
DerivedClass *pd4 = dynamic_cast<DerivedClass *>(&c2);
pd4->Hello();

}

8.6.2 命名转换

显示类型转换也成为强制类型转换。命名转换的基本思想是令类型转换的含义更明显,让程序员表达他的真正意图

  • staticcast 执行关联类型之间的转换,包括各种内置类型之间的转换和有继承关系的用户类型之间的转换
  • reinterpretcast 处理非关联类型之间的转换
  • constcast 参与转换的类型仅在const修饰符及volatile修饰符上有所区别
  • dynamiccast 执行指针或者引用类型层次体系的类型转换,并执行运行时检查

8.6.3 C风格的转换

(T)e 比较任性没有检查机制

8.6.4 函数形式的转换

T(e) 对于内置类型该操作类似于(T)e.

9 函数

9.1 函数声明

struct S {
[[noreturn]] virtual inline auto f(const unsigned long int *const) -> void const noexcept
}

限定符说明:

函数名字   必选
参数列表 可以为空() 必选
返回类型 可以是void,可以使前置或后置形式(使用auto) 必选 
inline 表示一种愿望:通过内联函数体实现函数调用 可选
constexpr 表示当给定常量表达式作为参数时,应该可以在编译时对函数求值 可选
noexcept 表示函数不允许抛出异常 可选
链接说明 例如static 可选
[[noreturn] ] 该函数不会用常规的调用/返回机制返回结果 可选
virtual 成员函数,函数可以被派生类覆盖 可选
override 成员函数,函数必须覆盖基类中的一个虚函数 可选
final 成员函数,函数不能被派生类覆盖 可选
static 成员函数,函数不与某一特定对象关联 可选
const 成员函数,函数不能修改其对象的内容 可选

9.2 函数定义

9.2.1 一些特殊的函数

  • 构造函数 没有返回值,可以初始化基类对象和成员,无法获取其地址
  • 析构函数 不能被重载,无法获取其地址
  • 函数对象 不是函数 不能被重载
  • lambda 定义函数对象的一种简写方式

9.2.2 返回值

通常函数的返回值出现在函数声明最开始的地方,但是可以通过auto占位来吧返回类型后置,这样就方便通过参数类型来决定返回值类型(函数模板)

string to_string(int a);
auto to_string(int a) -> string; //和上面的等价

template<class T,class U>
auto product(const vector<T> &x, const vector<U> &y) -> decltype(x+y); // 后置返回类型以便于类型推断

9.2.3 inline函数

告诉编译器尽量使用内联方式执行函数,而非调用。 内联函数和函数内的静态变量可以获得一个*唯一的地址*不会因为内联变化函数地址。

9.2.4 constexpr 函数

如果调用干函数的参数全部为常量,则可以将constexpr函数当做常量表达式使用,在编译期对其求值 constexpr不允许有任何副作用,即只可以用局部变量

9.2.5 属性

属性可以置于C++语法的任何位置,通常情况下属性描述了位于它前面的语法实体的性质,属性也能出现在声明语句的开始位置

目前支持的属性看这里http://en.cppreference.com/w/cpp/language/attributes

1
2
[[noreturn]] void exit(int); //目前主要为提高可读性

9.2.6 标号的作用于是整个函数

9.2.7 参数传递

函数调用使用实参初始化形参,参数传递语义与初始化语义一致(拷贝初始化)

10 小细节

  • 抽象函数的调用需要使用指针或引用操作对象的主要原因是执行抽象任务的代码无法判断对象属于那个具体的实现,所以编译器对操作对象需要的空间大小一无所知,无法从栈上分配合理的空间。
  • 每个含有虚函数的类都含有自己的 vtbl 用于虚函数的调用,虚函数的调用会抽象成对 vtbl 指定索引的函数的调用。
  • 用 const 定义的常量必须在声明的时候初始化,因为后面无法再对其进行赋值
// const int a; // error: uninitialized const ‘a’ [-fpermissive]
Last Updated 2018-01-30 Tue 11:30.
Emacs 24.5.1 (Org mode 9.0.9)