c++11特性

0. 简介

在c++11标准中, 语言本身和标准库都增加了很多新内容. 里面的某些特性, 会让你在代码编写时更优雅.

 

我的环境:

系统: ubuntu16.04

g++版本: g++5.4.0

使用c++11特性编译时需加参数 -std=gnu++11  或  -std=c++11

 

1. 特性

1.1 nullptr

NULL可以直接赋值给bool, int, float等类型(有些会编译警告), 而nullptr赋值给这些类型会编译报错.

比起NULL宏来说, nullptr常量更利于函数的重载.

 1 #include <iostream>
 2 
 3 void f(int i)
 4 {
 5     std::cout << "i: " << i << std::endl;
 6     return;
 7 }
 8 
 9 void f(int *s)
10 {
11     std::cout << "s: " << s << std::endl;
12     return;
13 }
14 
15 int main(void)
16 {
17     //f(NULL);
18     f(nullptr);
19 
20     return 0;
21 }

 对于上面的代码, 使用NULL会编译报错(看到有些文章说会调用void f(int); 编译器不同, 还是有些差别的. 我是编译都不能通过). 使用nullptr会调用void f(int*s);

 

1.2 强类型枚举

在传统的c++枚举类型中, 程序员必须为同一作用域每一个枚举项设定一个唯一的名字. 否则就会出现重命名. 而且它们会被隐式转换为int类型, 无法拥有特定的用户定义类型.

如下代码就是传统c++枚举出现重命名的问题(代码编译报错).

 1 #include <iostream>
 2 
 3 enum E0
 4 {
 5     Val1,
 6     Val2
 7 };
 8 
 9 enum E1
10 {
11     Val,
12     Val1
13 };
14 
15 int main(void)
16 {
17     return 0;
18 }

 

c++11中引入强类型枚举的新类型, 不会将枚举常量暴露到外层作用域, 也不会隐式转换为int类型, 并且拥有用户指定的特定类型. 在c++11中, 传统的枚举类型也可以由用户指定特定类型.

如下代码可以正常编译. 枚举类型转换其它类型只能显式转换.

 1 #include <iostream>
 2 
 3 enum class E2 : bool
 4 {
 5     Val1,
 6     Val2,
 7 };
 8 
 9 enum class E3
10 {
11     Val,
12     Val1
13 };
14 
15 int main(void)
16 {
17     //bool k = E2::Val1; /* error */
18     bool b = static_cast<bool>(E2::Val2);
19     return 0;
20 }

 

1.3 类型推断

在c++11之前, auto关键字就已经存在, 之前的auto用于声明变量为自动变量, 自动变量意味着拥有自动的生命期. 就算不使用auto声明, 变量依旧默认是有自动的生命期.

int a = 10;            /* 拥有自动生命期 */
auto int b = 10        /* 拥有自动生命期 */
static int c = 01      /* 延长生命期 */

 

在c++11之前, 对auto的使用是极少的, 在c++11已经删除了这个用法, 取而代之的是全新的auto: 变量的自动类型推断.

 1 #include <iostream>
 2 #include <typeinfo>
 3 
 4 int main(void)
 5 {
 6     auto a = 20;
 7     auto b = 20LL;
 8     auto c = "hello world";
 9 
10     if (typeid(a) == typeid(int))
11     {
12         std::cout << "a is int" << std::endl;
13     }
14     if (typeid(b) == typeid(long long))
15     {
16         std::cout << "b is long long" << std::endl;
17     }
18     if (typeid(c) == typeid(const char *))
19     {
20         std::cout << "c is const char *" << std::endl;
21     }
22 
23     return 0;
24 }

 

上面代码输出:

a is int
b is long long
c is const char *

 

从输出可以看出, auto 推断出a为int类型, b 为long long类型, c为const char * 类型; 在编程时不建议这样使用auto, 直接写出变量的类型更加的清晰易懂.

auto的自动类型推断发生在编译期, 所以使用auto并不会造成程序运行时效率的降低. 而是否会造成编译期的时间消耗, 我认为是不会的, 在未使用auto时, 编译器也需要得知右操作数的类型, 再与左操作数的类型进行比较, 检查是否可以发生相应的转化, 是否需要进行隐式类型转换.

auto关键字正确的用法:

用于代替冗长复杂, 变量使用范围专一的变量声明.

 

 1 #include <string>
 2 #include <vector>
 3 
 4 int main(void)
 5 {
 6     /* 传统写法 */
 7     std::vector<std::string> vs1;
 8     for (std::vector<std::string>::iterator i = vs1.begin(); i != vs1.end(); i++)
 9     {
10         /* ... */
11     }
12 
13     /* 使用auto */
14     std::vector<std::string> vs2;
15     for (auto i = vs2.begin(); i != vs2.end(); i++)
16     {
17         /* ... */
18     }
19 
20     /* 配合范围的遍历操作更加简洁 */
21     std::vector<std::string> vs3;
22     for (auto &i : vs3)
23     {
24         /* ... */
25     }
26 
27     return 0;
28 }

从上面可以看出, 使用auto, 会使代码变得非常简洁.

 

  • 在定义模板函数时, 用于声明依赖模板参数的变量类型.
#include <iostream>

template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x * y;
    std::cout << v << std::endl;
}

int main(void)
{
    return 0;   
}

 

在模板函数中, 我们很难确定x * y的真正类型, 这样使用auto就很容易定义函数了.

 

  • 模板函数依赖于模板参数的返回值.

decltype 是检查实体的声明类型, 或表达式的类型和值类别的一个关键字.

返回值后置式声明语法也是c++11的特性.

#include <iostream>

template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(x * y)
{
    return x * y;
}

int main(void)
{
    return 0;
}

 

当模板函数的返回值依赖于模板的参数时, 我们无法在编译代码前确定模板参数的类型, 也无法知道返回值类型, 这时候可以使用auto.

atuo在这里只是返回值占位,  它只是为函数返回值占了一个位置, 真正的返回值是后面的decltype(_Tx * _Ty). 为什么要用返回值后置式声明语法呢? 

如果没有后置, 那么声明就是decltype(x * y) multiply(_Tx x, _Ty y), 而此时x, y还没有声明, 编译时无法通过的.

使用返回值后置式函数声明语法, 返回值类型的位置必须记做 "auto"

注意事项:

    • auto变量必须在定义时初始化.
    • 函数或模板参数不能被声明为auto
    • 定义在一个auto序列的变量, 必须始终推到成同一类型. 例如: 
1 auto a1 = 1, a2 = 12, a3 = 30;   /* 正确 */
2 auto a4 = 1, a5 = 1.1, a6 = 'k'; /* 错误, 没有推到成为同一类型 */
    •  auto仅仅是一个占位符, 它并不是一个真正的类型, 不能使用一些以类型为操作数的操作符, 如sizeof, typeid等.

 

1.4 基于范围的遍历操作

遍历容器时支持"foreach" 用法.

可以遍历c类型的数组, 初始化列表以及任何冲在了begin()和end()函数的类型.

写法非常简洁:

 1 #include <iostream>
 2 
 3 int main(void)
 4 {
 5     int arr[] = {1, 20, 9, 4, 111, 53, 23, 7};
 6     //for (const auto &val : arr)
 7     for (const int &val : arr)
 8     {
 9         std::cout << "val: " << val << std::endl;
10     }
11 
12     return 0;
13 }

 

1.5 委托和成员默认初始值

  • 一个构造函数可以调用该类中的其它构造函数来完成部分初始化任务(委托).
  • 声明成员时可以直接指定默认初始值.
 1 #include <iostream>
 2 
 3 class A
 4 {
 5 public:
 6     A(int new_number) : m_nNumber(new_number) { std::cout << "hello world" << std::endl;    }
 7     A() : A(42) {   std::cout << "hello world 2" << std::endl;  }
 8 
 9 private:
10     int m_nNumber;
11     int m_nValue = 10;
12 
13 };
14 
15 int main(void)
16 {
17     A a;
18     return 0;
19 }

 

1.6 初始化列表和统一初始化方式

允许非POD容器类型通过初始化列表完成构造.

无论是POD还是非POD类型都可以使用 obj = { ... }的方式来进行初始化. 对非POD类型{ ... }将自动匹配和调用构造函数. 因此对于 A(x)的构造函数可以这样写: A{x},  A a{x}. 在需要返回一个A类型对象时, 可以写: return{x}; (建议不要滥用该用法, 否则降低代码的可读性和可维护性)

 1 #include <iostream>
 2 #include <vector>
 3 #include <map>
 4 #include <initializer_list>
 5 
 6 class A
 7 {
 8 public:
 9     A(std::initializer_list<int> items)
10     : m_nNumber(*items.begin())
11     {};
12 
13 private:
14     int m_nNumber;
15 };
16 
17 
18 int main(void)
19 {
20     std::vector<int> vec1{1, 6, 22, 100, 8, 15, 10, 9};
21     std::vector<int> vec2 = {1, 6, 22, 100, 8, 15, 10, 9};
22     std::map<int, int> map1 = { {1, 2}, {3, 4}, {5, 6}, {7, 8} };
23 
24     A a1{1, 2};
25     A a2{1, 2, 5, 1000};
26 
27     return 0;
28 }

 

1.7 常量表达式 constexpr

值不会改变并且编译期就能得到计算结果的表达式.

 

  • constexpr 变量. 一定是一个常量. 而且必须用常量表达式初始化.

  • constexpr 函数 指能用于常量表达式的函数. 函数的返回类型以及所有形参类型都得是字面值类型, 而且函数体中必须有且只有一条return语句.

  • constexpr 构造函数. 用于生成constexpr对象以及constexpr函数的参数或返回类型. constexpr构造函数可以声明成=default或=delete的形式. constexpr构造函数必须即符合构造函数的要求, 又符合constexpr函数的要求. 所以constexpr构造函数一般是空的.constexpr构造函数必须初始化所有数据成员, 初始值或者使用constexpr构造函数, 或者是一条常量表达式.

 

允许constexpr函数的返回值并非一个常量. 当函数的实参是常量表达式时, 它的返回值也是常量表达式; 反之则不然.

 

 1 constexpr int func1()
 2 {
 3     return 100;
 4 }
 5 
 6 constexpr int func2(int val)
 7 {   /* 如果val是常量表达式, 则func2(val)也是常量表达式 */
 8     return val * func1();
 9 }
10 
11 int main(void)
12 {
13     constexpr int a = 20;        /* 20 是常量表达式 */
14     constexpr int b = a + 2;     /* a + 2 是常量表达式 */
15     constexpr int c = func2(100);
16 
17     //int i = 100;
18     //constexpr int d = func2(i); /* i 不是常量表达式, 所以func2(i)不是常量表达式, 该语句error */
19     return 0;
20 }

 

1.8 显式默认和禁用方法

用户可以通过"=default"后缀修饰符, 为每个类显式指定默认构造函数. 也可以通过"=delete"后缀修饰构造函数和赋值等操作(通常用来实现禁止复制的语义)

显式限定这些特殊方法增强了代码的可读性和可维护性.

 1 class A
 2 {
 3 public:
 4     A() = default;
 5     A(const A &a) = delete;
 6 };
 7 
 8 int main(void)
 9 {
10     A a;
11     //A b(a); /* 拷贝构造函数已经声明为delete, 无法调用. */
12 
13     return 0;
14 }

 

1.9 override 和 final 修饰符

override 修饰符用于标识指定的虚函数重载了基类中的同名虚方法(而不是定义一个新的同名虚方法). 该修饰符在编译时严格检查, 避免因为函数签名不同而导致重载失败.

final 修饰符可用于修饰类或方法. 在修饰类时, 它表示指定类是一个concrete类(不能再作为基类而被其它类继承). 

用于函数时, 表示此虚方法已经是最终实现, 任何派生类中重载这个方法的企图都将引发一个编译错误.

 1 #include <iostream>
 2 
 3 class A
 4 {
 5 public:
 6     A() = default;
 7     virtual ~A() {  std::cout << "~A" << std::endl; }
 8 
 9     virtual void func1() {  std::cout << "A::func1" << std::endl;   }
10     virtual void func2() final {    std::cout << "A::func2" << std::endl;   }
11 };
12 
13 class Base final
14 {
15 public:
16     Base() = default;
17     ~Base() = default;
18 };
19 
20 class B
21 : public A
22 //, public Base /* Base被final修饰, 不能再作为基类被继承 编译会失败*/
23 {
24 public:
25     B() = default;
26     virtual ~B() {  std::cout << "~B" << std::endl; }
27     virtual void func1() override { std::cout << "B::func1" << std::endl;   }
28     //virtual void func2() override {   std::cout << "B::func2" << std::endl;   } /* 基类已经禁止重载 编译会失败 */
29     //virtual void func3() override {   std::cout << "B::func3" << std::endl;   } /* 基类没有该函数 编译会失败 */
30 
31 };
32 
33 int main(void)
34 {
35     int override = 20;
36     int final;
37     final = 30;
38     std::cout << "override: " << override << std::endl;
39     std::cout << "final: " << final << std::endl;
40     A *a = new B();
41     a->func1();
42 
43     delete a;
44     a = nullptr;
45 
46     return 0;
47 }

 

通过上面的例子我发现final和override还可以被定义成变量.

使用override和final 有利于增强可读性, 定义和派生抽象类的利器. 它们在除了特定上下文中有特别的含义外, 仍然可以作为合法的标识符使用.

 

1.10 static_assert

静态断言, 编译时的断言检查. 如果为假, 编译器会打印错误信息.

下面列举几个使用的例子:

 1 #include <type_traits>
 2 
 3 class A
 4 {
 5 public:
 6     A() {}
 7     virtual ~A() {}
 8 private:
 9 };
10 
11 class B
12 : public A
13 {
14 public:
15     B() {}
16 };
17 
18 
19 int main(void)
20 {
21     /* 若B空类类型(无异于0大小位域的非静态数据成员, 无虚函数, 无虚基类, 且无非空基类和非联合类类型), 则为true */
22     static_assert(std::is_empty<B>::value, "error..."); /* 是否是空类 */
23 
24     /* 若B派生自A 或 为同一非联合类, 则为true */
25     static_assert(std::is_base_of<A, B>::value, ".......error........");
26 
27     /* POD 类型为true */
28     static_assert(std::is_pod<B>::value, ".......error........");
29 
30     /* 
31      * 是否是整数类型 bool 、 char 、 char8_t 、 char16_t 、 char32_t 、
32      * wchar_t 、 short 、 int 、 long 、 long long 类型,或任何实现定义
33      * 的扩展整数类型,包含任何有符号、无符号及 cv 限定的变体。则提供等
34      * 于 true
35      * */
36     static_assert(std::is_integral<int>::value, ".......error........");
37 
38     /* ... ... */
39 
40     return 0;
41 }

 

1.11 自定义字面值后缀

后缀名尽量以下划线开头(否则会报警告, 可能有些编译器会报错)

后座的参数只能是unsigned long long, long double, const char*或者const char* + size_t

 1 #include <iostream>
 2 
 3 unsigned long long operator"" _god(unsigned long long n)
 4 {
 5     return n * 100;
 6 }
 7 
 8 int main(void)
 9 {
10     auto n = 3_god;
11     std::cout << "n: " << n << std::endl;
12 
13     return 0;
14 }

 

使用不当会降低代码的可读性和可维护性

 

1.12 新的字符类型和字符串字面值定义

新增char16_t 和 char32_t 两种类型. 

新增 utf-8(u8), utf-16(u), utf-32(U)字符串字面值定义方法.

同时新增了对 引号(")和反斜杠(\)等特殊字符无需转换的裸字符串字面值定义方法(R), 该方法还可以与utf字面值前缀接合使用(u8R, uR, UR)

 1 #include <iostream>
 2 
 3 int main(void)
 4 {
 5     char16_t ch1;
 6     char32_t ch2;
 7 
 8     std::cout << u8"hello world" << std::endl;
 9     std::cout << u"hello world" << std::endl;
10     std::cout << U"hello world" << std::endl;
11     std::cout << R"("hello  world")" << std::endl;
12     std::cout << u8R"(hello"" \   world)" << std::endl;
13 
14     return 0;
15 }

 

1.13 模板的可变参数

为模板方法提供了类似普通函数的可变参数(...)支持.

 1 #include <iostream>
 2 
 3 void f()
 4 {
 5     std::cout << "f()" << std::endl;
 6 }
 7 
 8 template <typename T, typename... Args>
 9 void f(T t, Args... args)
10 {
11     /* 打印可变参数的长度(参数的个数) */
12     std::cout << "args: " << sizeof...(args) << std::endl;
13 
14     f(args...); /* 传入可变参数 */
15     return;
16 }
17 
18 int main(void)
19 {
20     f(1, 2, 3, "he", "");
21     return 0;
22 }

 

1.14 改进大于号解析

不必担心模板定义中的 ">>" 被解析成右移操作.

1 #include <vector>
2 
3 int main(void)
4 {
5     std::vector<std::vector<int>> vec1;  /* c++11支持 */
6     std::vector<std::vector<int> > vec2;
7     return 0;
8 }

 

1.15 lambda 表达式

[capture list] (parameter list) mutable或exception声明 ->return type {function body};
  • [capture list]

按值传递的参数默认是const的, 不可修改.

空 :   [] 没有任何capture

= : 函数体内可以使用lambda所在范围内所有可见的局部变量(包括lambda所在类的this), 并且是值传递方式.

& : 函数体内可以使用lambda所在范围内所有可见的局部变量(包括lambda所在类的this), 并且是引用传递方式.

this :函数体内可以使用lambda所在类中的成员变量

x :将x按值进行传递(默认是const, 不可修改)

&x : 将x按引用进行传递.

x, &y : 将x按值传递, b按引用传递.

=, &x, &y : 除x和y按引用传递外, 其它参数都按值进行传递.

&, x, y : 除x和y按值进行传递外, 其它参数都按引用进行传递.

 

  • (parameter list)

如同普通函数的参数列表.

 

  • mutable 或 exception 声明

这部分可以省略. 按值传递capture list时, 加上mutable修饰符后, 可以修改传递进来的拷贝(只是修改拷贝的副本, 而不是修改本身). 

exception声明用于指定函数抛出的异常, 如抛出整数类型的异常, 可以使用throw(int).

 

  • ->return type

标识函数返回值的类型, 当返回值为void, 或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时, 这部分可以省略.

 

  • {function body}

函数的实现, 函数体可以为空.

 

下面是对lambda表达式的简单使用:

 1 #include <iostream>
 2 
 3 int main(void)
 4 {
 5     int n = 5;
 6     int m = 5;
 7 
 8     auto f1 = [m]() mutable {   return ++m; };
 9     std::cout << "f1: " << f1() << std::endl;
10     std::cout << "m: " << m << std::endl;
11 
12     auto f2 = [&n]() {  return ++n; };
13     std::cout << "f2: " << f2() << std::endl;
14     std::cout << "n: " << n << std::endl;
15 
16     auto f3 = [=](char x) mutable ->int {   m = x; return m;    };
17     std::cout << "f3: " << f3('3') << std::endl;
18     std::cout << "m: " << m << std::endl;
19 
20 
21     return 0;
22 }

 

一个没有指定任何捕获的lambda函数, 可以显式的转换成一个具有相同声明形式的函数指针.

1 int main(void)
2 {
3     auto f4 = [](int x) {   return ;    };
4     void (*func_ptr)(int) = f4;
5 
6     return 0;
7 }

 

  注意: 使用引用捕获时, 若引用的是函数体内的局部变量, 那么lambda的生命周期不要超过所在函数体的生命周期.

 

1.16 右值引用和移动构造语义

右值引用解决了左值引用无法传递临时对象和常引用传递的对象只读的问题. 右值引用允许传递一个可变的临时对象引用.

移动构造使用移动而非赋值语义完成构造过程, 主要用于解决函数返回值时的大量深拷贝开销.

 

不能将一个右值引用直接绑定到一个变量上, 即使这个变量是右值引用类型也不行.

下面是对右值引用使用简单的例子: 

 1 #include <utility>
 2 
 3 int main(void)
 4 {
 5     int a = 30;
 6     int &b = a;
 7     //int &&c = a;          /* error 右值引用不能绑定到左值 */
 8     //int &d = a * 2;       /* erro   a * 2 是一个右值 */
 9     const int e = a * 2;    /* const 的引用可以绑定到一个右值 */
10     int && f = a * 2;       /* 右值引用可以绑定到右值 */
11     //int && g = f;         /* error 不能将一个右值引用绑定到一个变量上, 即使这个变量是右值引用类型也不行 */
12     int && h = std::move(f); /* ok */
13 
14 
15     return 0;
16 }

 

1 int r1 = 20;
2 int &&r2 = std::move(r1);

 

调用move就意味着承诺: 除了对r1赋值或销毁它以外, 将不能再使用它.

 

 1.17 标准库函数begin, end 和 容器内的cbegin, cend

 标准库的begin, end和容器中的成员begin, end功能相似,

 正确使用begin和end是将数组作为它们的参数. 可以方便的获取数组的尾指针和头指针.

 1 #include <stdio.h>
 2 #include <string.h>
 3 
 4 #include <iostream>
 5 #include <iterator>
 6 
 7 int main(void)
 8 {
 9      char s1[] = "hello world";
10      const char *psbeg = std::begin(s1);
11      char *psend = std::end(s1);
12      printf("psbeg:\t%p\ns1:\t%p\n", psbeg, s1);
13      /* end返回的是数组结尾的下一个地址 */
14      printf("psend - 1:\t%p\ns1[strlen(s1)]:\t%p\n", psend - 1, &s1[strlen(s1)]);
15 
16     return 0;
17 }

 

 

容器中新增了cbegin和cend, 类似容器中的begin, end返回指示容器第一个元素或最后一个元素下一个的迭代器. 不同的是不论容器对象本身是否是常量, cbegin, cend返回的是const_iterator.

1 #include <vector>
2 
3 int main(void)
4 {
5     std::vector<int> vec;
6     std::vector<int>::const_iterator it = vec.cbegin();
7 
8     return 0;
9 }

 

 

1.18 array 和 forward_list

  • array是固定大小的序列容器, 它包含以严格线性序列排序的特定数量的元素. 不通过分配器管理其元素的分配. 成员方法和vector容器有诸多类似之处. 就不再一一阐述了.
  • forward_list 是一个单向链表.
    • 无法使用反向迭代器. 只能从它得到const或non-const前项迭代器, 这些迭代器都不能解引用, 只能自增;
    • 没有可以返回最后一个引用的成员函数back();
    • 因为只能通过自增前面元素的迭代器来到达序列的终点, 所以push_back(), pop_back(), emplace_back()也无法使用.

forward_list的操作要比list容器更快, 而且占用更少的内存.

 

下面是array简单使用的例子:

 1 #include <iostream>
 2 #include <array>
 3 
 4 int main(void)
 5 {
 6     std::array<int, 5> arr1 = {1, 2};
 7 
 8     std::cout << sizeof(arr1) << std::endl;
 9     std::cout << arr1.size() << std::endl;
10     std::cout << "arr[1]: " << arr1[100] << std::endl;
11     for (auto &val : arr1)
12     {
13         std::cout << "val: " << val << std::endl;
14     }
15 
16     return 0;
17 }

 

 

1.19 string 和数值之间的转换

string和数值转换  转换类型
to_string(val) 把val转换成string
stoi(s, p, b) 返回s的起始子串(表示整数内容)的数值, 返回值类型分别是int, long, unsigned long, long long, unsigned long long. b 表示转换所用的基数, 默认值是10(十进制). p是size_t指针, 用来保存s中第一个非数值字符的下标, p默认为0(不保存下标)
stol(s, p, b)
stoul(s, p, b)
stoll(s, p, b)
stoull(s, p, b)
stof(s, p) 返回s的起始子串(表示浮点数内容)的数值, 返回值类型分别是float, double, long double. 参数p的作用和整数转换函数一样.
stod(s, p)
stold(s, p)

 

 

 

 

 

 

 

 

 

下面是简单使用例子:

 1 #include <iostream>
 2 #include <string>
 3 
 4 int main(void)
 5 {
 6     int a1 = 100;
 7     std::string s1 = std::to_string(a1);
 8 
 9     a1 = std::stoi("200", nullptr, 10);
10     int a2 = std::stoi("30");            /* 使用默认的p和b, 可以省略不写 */
11 
12     std::cout << "s1: " << s1 << std::endl;
13     std::cout << "a1: " << a1 << std::endl;
14     std::cout << "a2: " << a2 << std::endl;
15 
16     return 0;
17 }

 

 

1.20 无序关联容器

unordered_set, unordered_map, unordered_multiset, unordered_multimap

它们分别对应有序关联容器set, map, multiset, multimap.

这四个无序关联容器是使用哈希函数和关键字类型的==运算符来组织元素的.

除了哈希管理操作之外, 无序关联容器提供了与有序容器相同的操作(find, insert等), 意味着可以像操作set, map一样操作unordered_set, unordered_map

通常情况下, 无序关联容器可以获得更好的性能, 但也会占用更多的内存.

 

1.21 正则表达式 regex

正则表达式库非常的强大, 用于字符串匹配非常的方便. 这里只简单介绍c++中的使用.

正则表达式库组件
regex 表示有一个正则表达式类
regex_match 将一个字符序列与一个正则表达式匹配
regex_search 寻找第一个与正则表达式匹配的子序列
regex_replace 使用给定格式替换一个正则表达式
sregex_iterator 迭代适配器, 调用regex_search来遍历一个string中所有匹配的子串
smatch 容器类, 保存在string中搜索的结果
ssub_match string中匹配的子表达式的结果

 

 

 

 

 

 

 

 

 

 

 

 1 #include <iostream>
 2 #include <string>
 3 #include <regex>
 4 
 5 int main(void)
 6 {
 7     std::string str1[] = {"foo.txt", "b.txt", "abcd.txt", "txt", "a0.txt", "AAA.txt", "wt.txt"};
 8 
 9     std::regex txtRegex("[a-z]+\\.txt");
10 
11     std::regex baseRegex("([a-z]+)\\.txt");
12     std::smatch baseMatch;
13     for (const auto &s : str1)
14     {
15         if (std::regex_match(s, baseMatch, baseRegex))
16         {
17             std::cout << "baseMatch[0]: " << baseMatch[0] << std::endl;
18             std::cout << "baseMatch.szie: " << baseMatch.size() << std::endl;
19             if (baseMatch.size() == 2)
20             {
21                 std::string str = baseMatch[0].str();
22                 std::string base = baseMatch[1].str();
23                 std::cout << "str: " << str << std::endl;
24                 std::cout << s << " : " << base << std::endl;
25             }
26         }
27     }
28 
29     return 0;
30 }

 

 

1.22 noexcept异常说明 和 noexcept运算符

  noexcept说明符:

  • 知道函数不会抛出异常有助于简化调用该函数的代码.
  • 如果编译器确认该函数不会抛出异常, 它就能执行某些特殊的优化操作, 而这些优化操作并不适合可能出错的代码.

 

noexcept说明符出现的位置:

  • 函数的尾置返回类型之前. 
  • 函数指针的声明和定义中指定noexcept
  • 在成员函数中, noexcept说明符需要跟在const及引用限定符之后, 而在final, override或虚函数的=0 之前.

noexcept运算符:

返回值是一个bool类型的右值常量表达式, 用于表示给定的表达式是否会抛出异常. 和sizeof类似, noexcept也不会求其运算对象的值.

 1 #include <iostream>
 2 
 3 class Base
 4 {
 5 public:
 6     void func1() const noexcept {   return; }
 7     virtual void func2() {  return; }
 8     virtual void func3() noexcept = 0;
 9     virtual void func4() noexcept final {   return; }
10 };
11 
12 class Derive
13 : public Base
14 {
15 public:
16     virtual void func2() noexcept override {    return; }
17     virtual void func3() noexcept override {    return; }
18 };
19 
20 void f1() noexcept {    return; }
21 void f2() { return; }
22 
23 int main(void)
24 {
25     std::cout << noexcept(f1()) << std::endl;
26     std::cout << noexcept(f2()) << std::endl;
27     Derive d;
28     std::cout << noexcept(d.func2()) << std::endl;
29     std::cout << noexcept(2) << std::endl;
30 
31     return 0;
32 }

 

 

1.23 继承的构造函数

  • 派生类能够重用其直接基类定义的构造函数. 一个类只初始化它的直接基类, 出于同样的原因, 一个类也只能继承其直接基类的构造函数. 类不能继承默认, 拷贝和移动构造函数. 如果派生类没有定义这些构造函数, 则编译器将为派生类合成它们.
  • 和普通的using声明不一样, 一个构造函数的using声明不会改变该构造函数的访问级别(不管using声明出现在哪里, 基类的私有构造函数在派生类中还是私有构造函数; 受保护和公有的构造函数也是同样的规则)
  • 一个using声明语句不能指定explicit或constexpr. 如果基类的构造函数是explicit或constexpr, 则继承的构造函数也拥有相同的属性.
  • 当一个基类构造函数含有默认实参时, 这些实参并不会被继承. 相反, 派生类将获得多个继承的构造函数, 其中每个构造函数分别省略掉一个含有默认实参的形参. 例如: 如果基类有一个接受两个形参的构造函数, 其中第二个形参含有默认实参, 则派生类将获得两个构造函数: 一个构造函数接受两个形参(没有默认实参), 另一个构造函数只接收一个形参, 它对应于基类中最左侧的没有默认值的那个形参.
  • 如果基类含有多个构造函数, 则除了两个例外情况, 大多数时候派生类会继承所有这些构造函数.
    • 第一个例外是派生类可以继承一部分构造函数, 而为其它构造函数定义自己的版本.  如果派生类定义的构造函数与基类的构造函数具有相同的参数列表, 则该构造函数将不会被继承. 定义在派生类中的构造函数将替换继承而来的构造函数.
    • 第二个例外是默认, 拷贝和移动构造函数不会被继承.  这些构造函数按照正常规则被合成. 继承的的构造函数不会被作为用户定义的构造函数来使用, 因此, 如果一个类只含有继承的构造函数, 则它也将拥有一个合成的默认构造函数.

 

如果从多个基类中继承了相同的构造函数(形参列表完全相同), 这程序会产生错误, 这个类必须为该构造函数定义它自己的版本.

 1 #include <string>
 2 
 3 class Base1
 4 {
 5 public:
 6     Base1() = default;
 7     Base1(const std::string&)   {};
 8 };
 9 
10 class Base2
11 {
12 public:
13     Base2() = default;
14     Base2(const std::string&)  {};
15 };
16 
17 class Derive
18 : public Base1
19 , public Base2
20 {
21 public:
22     using Base1::Base1; /* 从Base1继承构造函数 */
23     using Base2::Base2; /* 从Base2继承构造函数 */
24     Derive() = default;
25     /*
26      * 因为从多个基类继承了相同的构造函数,
27      * 如果不定义一个可以接受string的构造函数, 则会报错
28      * */
29     Derive(const std::string&s): Base1(s), Base2(s) {}
30 };
31 
32 int main(void)
33 {
34     return 0;
35 }

 

 

1.24 智能指针

  • shared_pt 基于引用计数的智能指针, 会统计当前有多少个对象同时拥有该内部指针; 当引用计数降为0时, 自动释放.
  • weak_ptr 一种弱引用, 指向shared_ptr所管理的对象; weak_ptr只引用, 不计数.
  • unique_ptr 独占语义的智能指针, 资源只能唯一的被一个unique_ptr所占有, 当其离开作用域时自动析构.  

 

shared_ptr和unique_tr都支持的操作

shared_ptr<T> sp

unique_prt<T> up

空智能指针, 可以指向类型为T的对象.
p 将p用作一个条件判断, 若p指向一个对象, 则为true
*p 解引用p, 获得它指向的对象
p->mem 等价于(*p).mem
pget() 返回p中保存的指针. 

swap(p, q)

p.swap(q)

交换p和q中的指针

p.reset()

shared_ptr将引用计数递减, unique_ptr将释放p指向的对象

p.reset(q)

p.reset(nullptr)

shared_ptr将引用计数递减, unique_ptr将释放p所指的对象, 如果提供了内置指针q, 则将p指向这个对象.

 

 

 

 

 

 

 

 

 

 

 

 

shared_ptr的操作
make_shared<T>(args) 返回一个shared_ptr, 指向一个动态内存分配的类型为T的对象. 使用args初始化此对象
shared_ptr<T>p(q) p是shared_ptr q的拷贝; 此操作会递增q中的计数器. q中的指针必须能转换为T*
p = q p和q都是shared_ptr, 所保存的指针必须能相互转换. 此操作会递减p的引用计数, 递增q的引用计数;
p.unique()

若p.use_count()为1, 返回true; 否则返回false

p.use_count() 返回与p共享对象的智能指针数量(引用计数值)

 

 

 

 

 

 

 

 

unique_ptr的操作
unique_ptr<T, D> u(d) 空unique_ptr, 指向类型为T的对象, 用类型为D的对象d代替delete
u = nullptr 释放u指向的对象, 将u置为空
u.release() u放弃对指针的控制权, 返回指针, 并将u置为空

 

 

 

 

 

 

 

weak_ptr的操作
weak_ptr<T> wp 空的weak_ptr 可以指向类型为T的对象
weak_ptr<T> wp(sp) 与shared_ptr sp指向相同对象的weak_ptr. T必需能转换成sp指向的类型
wp = p p可以指向一个shared_ptr或weak_ptr. 赋值后wp 与 p共享对象
wp.reset() 将wp置为空
wp.use_count()

与wp共享对象的shared_ptr的数量

wp.expired() 若wp.use_count()为0, 返回true, 否则返回false
wp.lock() 如果expired为true, 返回一个空的shared_ptr; 否则返回一个指向wp的对象的shared_ptr

 

 

 

 

 

 

 

 

shared_ptr使用注意事项:

  • 不使用相同的内置指针初始化(或reset)多个智能指针.
  • 不delete get() 返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果使用了get()返回的指针, 记住当最后一个对应的智能指针销毁后, 这个指针就变为了无效指针了.
  • 如果使用智能指针管理的资源不是new分配的内存, 记住传递给它一个删除器.

 

下面是使用示例:

 1 #include <memory>
 2 
 3 class Dealloc
 4 {
 5 public:
 6     Dealloc() = default;
 7 
 8     //括号()操作符的重载
 9     void operator() (int* p )
10     {
11         if (p != nullptr)
12         {
13             delete p;
14             p = nullptr;
15         }
16     }
17 };
18 
19 void shared_ptr_func(void)
20 {
21     //1. 创建
22     std::shared_ptr<int> sp0; /* 空智能指针 */
23     std::shared_ptr<int> sp1 = std::shared_ptr<int>(new int(100));
24     std::shared_ptr<int> sp2 = std::make_shared<int>(int(10));
25 
26     //2. 自定义资源释放函数
27     {
28         auto sp4 = std::shared_ptr<int>(new int[5], [](int *p){ delete[ ] p; });
29     }
30     {
31         // 括号()操作符的重载
32         auto sp5 = std::shared_ptr<int>(new int(1000), Dealloc() );
33     }
34 
35     //3. 复制
36     auto sp6(sp1);
37     auto sp7 = sp1;
38 
39     //4. 获取保存的指针
40     int* pRaw = sp2.get();
41 
42     //5. 返回共享对象的智能指针数量.
43     long nCount1 = sp1.use_count();
44     long nCount2 = sp2.use_count();
45 
46     //6. 是否是独享
47     bool b1 = sp1.unique();
48     bool b2 = sp2.unique();
49 
50     //7. swap
51     sp1.swap(sp2);
52 
53     //8. reset 重置指向另一个对象, 原指针引用计数减一
54     sp1.reset(new int(20));
55 }
56 
57 void del(int *p) { delete p; }
58 
59 void unique_ptr_func(void)
60 {
61     std::unique_ptr<int> up0;
62     std::unique_ptr<int> up1(new int(20));
63     //std::unique_ptr<int> up2(up1); /* 不支持拷贝 */
64     std::unique_ptr<int> up2(up1.release());
65 
66     up2.reset(up0.release());
67 
68     //up0 = up1; /* error 不支持赋值 */
69 
70     std::unique_ptr<int, decltype(del) *> up3(new int(20), del);
71 }
72 
73 void weak_ptr_func()
74 {
75     std::shared_ptr<int> sp0 = std::make_shared<int>(int(10));
76     std::weak_ptr<int> wp0(sp0); /* 不会改变sp0的引用计数 */
77 
78     bool b1 = wp0.expired();
79     std::shared_ptr<int> sp1 = wp0.lock();
80     std::weak_ptr<int> wp1 = wp0.lock();
81 }
82 
83 int main(void)
84 {
85     shared_ptr_func();
86     unique_ptr_func();
87     weak_ptr_func();
88     return 0;
89 }

 

 

 

 

 

 

 

 

 

参考

[1] 自由真实个性 https://www.cnblogs.com/KunLunSu/p/7861330.html

[2] 白杨 http://www.baiy.cn/doc/cpp/comments_of_cxx0x.htm

[3] jiange_zh https://blog.csdn.net/jiange_zh/article/details/79356417

[4] DoubleLi  https://www.cnblogs.com/lidabo/p/7241381.html

[5] c++ primer 第五版

[6] 季末的天堂 https://www.cnblogs.com/jimodetiantang/p/9016826.html

[7] c++参考手册 https://zh.cppreference.com

 

 

posted @ 2019-08-16 13:11  丁方  阅读(2059)  评论(0编辑  收藏  举报