C++学习与总结-11/14篇

小结:

  • 使用nullptr而不是NULL
  • 巧用auto
  • for(auto &x : array) {
    **std::cout << x << std::endl;**
    
    }
  • 使用 {} 进行初始化
  • 模板可用using改名,可添加默认参数,可变长
  • 构造函数可以委托和继承
  • 关键字 overridefinal用于控制重载 default和delete用于控制编译器生成默认构造函数
  • enum class
  • 使用lambda表达式和捕获列表
  • std::function包装函数,将函数和函数指针作为对象处理
  • std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。
  • std::array
  • std::shared_ptr/std::unique_ptr/std::weak_ptr
  • std::regex
  • noexcept 修饰符
  • 字面量R

nullptr 与 constexpr

当需要使用 NULL 时候,请养成直接使用 nullptr 的习惯。

constexpr常量表达式 让用户显式的声明函数或对象构造函数在编译器会成为常数 从 C++ 14 开始,constexptr 函数可以在内部使用局部变量、循环和分支等简单语句

类型推导

C++ 11 引入了 autodecltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器

1
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);

注意auto 不能用于函数传参, auto 还不能用于推导数组类型:

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。 和sizeof相似:

有时候,我们可能需要计算某个表达式的类型,例如:

1
2
3
auto x = 1;
auto y = 2;
decltype(x+y) z; // z 是一个 int 型的

从 C++ 14 开始是可以直接让普通函数具备返回值推导

1
2
3
4
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}

区间迭代

1
2
3
4
int array[] = {1,2,3,4,5};
for(auto &x : array) {
std::cout << x << std::endl;
}

初始化列表

使用 {} 进行初始化

C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

1
2
3
4
5
6
7
8
9
#include <initializer_list>

class Magic {
public:
Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};

初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:

1
2
3
4
5
void func(std::initializer_list<int> list) {
return;
}

func({1,2,3});

模板增强

外部模板

1
extern template class std::vector<double>;  // 不在该编译文件中实例化模板

类型别名模板

1
using process = int(*)(void *); // 定义了一个返回类型为 int,参数为 void* 的函数指针类型,名字叫做 process

默认模板参数

1
2
3
4
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}

变长参数模板

1
template<typename... Ts> class Magic;

可以使用 sizeof... 来计算参数的个数

对参数进行解包 :

递归模板函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
template<typename T>
void printf(T value) {
std::cout << value << std::endl;
}
template<typename T, typename... Args>
void printf(T value, Args... args) {
std::cout << value << std::endl;
printf(args...);
}
int main() {
printf(1, 2, "123", 1.1);
return 0;
}

初始化列表展开

1
2
3
4
5
6
7
8
9
10
11
template<typename T, typename... Args>
auto print(T value, Args... args) {
std::cout << value << std::endl;
return std::initializer_list<T>{([&] {
std::cout << args << std::endl;
}(), value)...};
}
int main() {
print(1, 2.1, "123");
return 0;
}

面向对象增强

委托构造

C++ 11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};

int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}

继承构造

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++ 11 利用关键字 using 引入了继承构造函数的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}

显式虚函数重载

C++ 11 引入了 overridefinal 这两个关键字来防止上述情形的发生。

当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译。

final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

显式禁用默认函数

允许显式的声明采用或拒绝编译器自带的函数。

1
2
3
4
5
6
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}

强类型枚举

C++ 11 引入了枚举类(enumaration class),并使用 enum class 的语法进行声明:

1
2
3
4
5
6
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};

在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用 int)。

而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 << 这个算符来进行输出,可以收藏下面这个代码段:

1
2
3
4
5
6
#include <iostream>
template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

这时,下面的代码将能够被编译:

1
std::cout << new_enum::value3 << std::endl

Lambda表达式

Lambda 表达式的基本语法如下:

1
2
3
4
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
[ caputrue ] ( params ) opt -> ret { body; };

所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:

1. 值捕获

2. 引用捕获

3. 隐式捕获

总结一下,捕获提供了 Lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

  • [] 空捕获列表
  • [name1, name2, ...] 捕获一系列变量
  • [&] 引用捕获, 让编译器自行推导捕获列表
  • [=] 值捕获, 让编译器执行推导应用列表
  • 表达式捕获(C++ 14)
  • 泛型 Lambda (C++ 14) auto 关键字来产生意义上的泛型:

函数对象包装器

C++11 std::function 是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作,它也是对 C++中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的),换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>
#include <iostream>

int foo(int para) {
return para;
}

int main() {
// std::function 包装了一个返回值为 int, 参数为 int 的函数
std::function<int(int)> func = foo;

int important = 10;
std::function<int(int)> func2 = [&](int value) -> int {
return 1+value+important;
};
std::cout << func(10) << std::endl;
std::cout << func2(10) << std::endl;
}

std::bind 则是用来绑定函数调用的参数的,它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数,我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用

右值引用

传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据移动,浪费时间和空间。右值引用的出现恰好就解决了这两个概念的混淆问题

无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发,所以 std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。

新增容器

std::array

使用 std::array 保存在栈内存中,相比堆内存中的 std::vector,我们就能够灵活的访问这里面的元素,从而获得更高的性能;同时正式由于其堆内存存储的特性,有些时候我们还需要自己负责释放这些资源。

而第二个问题就更加简单,使用std::array能够让代码变得更加现代,且封装了一些操作函数,同时还能够友好的使用标准库中的容器算法等等,比如 std::sort

std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array 很简单,只需指定其类型和大小即可:

1
std::array<int, 4> arr= {1,2,3,4};

std::forward_list

std::forward_list 使用单向链表进行实现

C++11 引入了两组无序容器:std::unordered_map/std::unordered_multimapstd::unordered_set/std::unordered_multiset

元组 std::tuple

关于元组的使用有三个核心的函数:

  1. std::make_tuple: 构造元组
  2. std::get: 获得元组某个位置的值
  3. std::tie: 元组拆包

智能指针和引用计数

std::shared_ptr/std::unique_ptr/std::weak_ptr,使用它们需要包含头文件memory。

std::make_shared 就能够用来消除显示的使用 new,所以std::make_shared 会分配创建传入参数中的对象,并返回这个对象类型的std::shared_ptr指针。例如:

1
auto pointer = std::make_shared<int>(10);

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数,并通过get_count()来查看一个对象的引用计数。

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证了代码的安全:

是,我们可以利用 std::move 将其转移给其他的 unique_ptr

std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr 是否存在,expired() 方法在资源未被释放时,会返回 true,否则返回 false

正则表达式

C++11 提供的正则表达式库操作 std::string 对象,模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。

线程支持

std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含头文件,它提供了很多基本的线程操作,例如get_id()来获取所创建线程的线程 ID,例如使用 join() 来加入一个线程 等等

C++11 引入了 mutex 相关的类,其所有相关的函数都放在mutex 头文件中。

std::mutex 是 C++11 中最基本的 mutex 类,通过实例化 std::mutex 可以创建互斥量,而通过其成员函数 lock() 可以仅此能上锁,unlock() 可以进行解锁。但是在在实际编写代码的过程中,最好不去直接调用成员函数,因为调用成员函数就需要在每个临界区的出口处调用 unlock(),当然,还包括异常。这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类std::lock_gurad。RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。

在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分,例如:

1
2
3
4
5
6
7
8
9
void some_operation(const std::string &message) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);

// ...操作

// 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁
// 因此这个函数内部的可以认为是临界区
}

在并发编程中,推荐使用 std::unique_lock。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void block_area() {
std::unique_lock<std::mutex> lock(mtx);
//...临界区
}
int main() {
std::thread thd1(block_area);

thd1.join();

return 0;
}

std::future 则是提供了一个访问异步操作结果的途径

std::packaged_task 可以用来封装任何可以调用的目标,从而用于实现异步的调用

std::condition_variable 是为了解决死锁而生的。当互斥操作不够用而引入的。

其他

long long int 类型至少具备 64 位的比特数。

C++11 将异常的声明简化为以下两种情况:

  1. 函数可能抛出任何异常
  2. 函数不能抛出任何异常

并使用 noexcept 对这两种行为进行限制,例如:

1
2
void may_throw();           // 可能抛出异常
void no_throw() noexcept; // 不可能抛出异常

使用 noexcept 修饰过的函数如果抛出异常,编译器会使用 std::terminate() 来立即终止程序运行。

noexcept 还能用作操作符,用于操作一个表达式,当表达式无异常时,返回 true,否则返回 false

C++11 提供了原始字符串字面量的写法,可以在一个字符串前方使用 R 来修饰这个字符串,同时,将原始字符串使用括号包裹,例如:

1
std::string str = R"(C:\\What\\The\\Fxxk)";

C++11 引进了自定义字面量的能力,通过重载双引号后缀运算符实现: