学习C++11和C++14

参考的资料《高速上手C++11/14》

  1. 大致过一遍内容,把重点内容仔细理解。

1. 一些被弃用的内容

弃用不是废弃,而是避免使用,为了兼容性,可能会永久保留的内容。

  1. 如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被启用。(google编码规范中也提到过,要么主动声明,要么不允许生成);
  2. 不允许char* str = "Hello World !!"将字符串常量赋值给char*,应该用 const char *或者auto
  3. 异常说明应该用noexcept。(异常没看过
  4. 应该使用static_cast,reinterpret_cast,const_cast来类型转换
  5. C++提供std::bind和std::function参数绑定

1.1 与C的兼容性

  1. 在编写C++时,尽量避免使用void*
  2. 不得不使用C时,使用 extern “C” 这种特性,实现C++和C代码分离编译,再统一链接
// foo.h
#ifdef __cpulsplus
extern "C"{
#endif

int add(int x, int y);

#ifdef __cplusplus
}
#endif

// foo.c
int add(int x, int y){
	return x+y;
}

// main.cpp
#include "foo.h"
int main(){
	add(1, 2);
	return 0;
}
->>>>>
gcc -c foo.c	// 编译出foo.o
g++ main.cpp foo.o -o main	// 将C++代码和.o文件链接起来

2. 语言可用性的强化

2.1 nullptr和constexpr

nullptr 取代 NULL 因为NULL有时会被视作0。
constexpr 修饰函数,函数显式的返回一个常数。

2.2 类型推导auto和decltype

for(std::vector<int>::const_iterator iter = vec.cbegin(); iter != vec.cend(); ++iter)
---->>>
for(auto iter = vec.cbegin(); iter != vec.cend(); ++iter)

auto不可以做为函数传参->可以使用函数模板进行重载

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

auto x = 1;
auto y = 2;
decltype(x+y) z;

2.3 尾返回类型

// 传统C++
template<typename R, typename T, typename U>
R add(T x, U y){
	return x+y;
}// 程序员需要明确指出返回类型;

// 尾返回类型C++11
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
	return x+y;
}
// C++14	nice~~~
template<typename T, typename U>
auto add(T x, U y){
	return x+y;
}

2.4 区间迭代

基于范围的for循环

for(auto &i : arr)	// std::vector arr(5, 100)

2.5 初始化列表

C++ 11 也可以使用初始化列表 std::initializer_list

  1. 初始化列表构造函数
#include 

class Magic{
public:
	// 初始化列表构造函数
	Magic(std::initializer_list<int> list);
};

Magic magic = {1, 2, 3, 4, 5};
std::vector<int> v = {1, 2, 3, 4, 5};
  1. 普通函数形参
void foo(std::initializer_list<int> list);
foo({1, 2, 3});
  1. 初始化任意的对象
struct A{
	int a;
	double b;
};
A a {1, 1.0};

struct B{
	B(int _a, double _b):a(_a), b(_b){}
private:
	int a;
	double b;
};
B b {2, 2.0};

2.6 模板增强

???

类型别名模板

// 传统
typedef int (*process)(void*)	// 定义一个返回类型为int,参数为void*的函数指针类型,名字叫做process
// C++11
using process = int(*)(void*);

// 传统
template<typename T, typename U>
class SuckType;
typedef SuckType<std::vector, std::string> NewType;	// 非法,模板
// C++11
using NewType = SuckType<std::vector, std::string>;	(模板类使用,T = vector, U = string)

2.7 面向对象增强

  1. 委托构造:构造函数可以在同一个类中一个构造函数调用另一个构造函数
class Base{
public:
	int value1_;
	int value2_;
	Base() {
		value1_ = 1;
	}
	Base(int value) : Base() {	// 委托Base()
		value2_ = 2;
	}
}
  1. 继承构造,已经理解
  2. 显式虚函数重载:使用 override 关键字显式的告诉编译器进行重载。final 关键字 为了防止类被继续继承或者虚函数被继续重载
class Base{
	virtual void foo() final;
};
class SubClass1 final: public Base {
};	// 合法
class SubClass2: public SubClass1 {
};	// 不合法
class SubClass1: public Base{
	void foo();
};	// 不合法
  1. 显式使用或者禁用默认函数,可以显式的声明采用或者拒绝编译器自带的函数
class Magic {
  public:
  	Magic() = default;	// 显式声明 "使用" 编译器生成构造
  	Magic& operator=(const Magic&) = delete;	// 显式声明 "拒绝" 编译器生成构造
}
  1. 强类型枚举,不能被隐式的转换成整数,不能与整数比较,不能与不同枚举类型的枚举值进行比较
enum class new_enum : unsigned int {	// 枚举类
	value1,
	value2 = 100
};

3. 语言运行期的强化

3.1 Lambda表达式

提供了一种类似匿名函数的特性,即需要一个函数,但是又不想费力去命名一个函数的情况下去使用。
语法:

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
	// 函数体
}

捕获列表 参数的一种类型,因为lambda表达式内部函数体在默认情况下是不能够使用函数外部的变量的,这时候捕获列表可以起到传递外部数据的作用。

// 1. 值传递,前期是变量可以拷贝,在lambda表达式被创建时拷贝
void learn_lambda_func_1() {
	int value_1 = 1;
	auto copy_value_1 = [value_1] {
		return value_1;
	};
	value_1 = 100;
	auto stored_value_1 = copy_value_1();
	// stored_value_1 == 1, value_1 == 100;
	// 因为copy_value_1在创建时就保存了一份value_1的拷贝。
}

// 2. 引用捕获,引用捕获保存的是引用变化
void learn_lambda_func_2() {
	int value_2 = 1;
	auto copy_value_2 = [&value_2] {
		return value_2;
	};
	value_1 = 100;
	auto stored_value_2 = copy_value_2();
	// stored_value_2 == 100, value_2 == 100;
	// 因为copy_value_2保存的是引用
}

// 3. 隐式捕获 
[] 空捕获列表
[name1, name2, ...] 捕获一系列变量
[&] 引用捕获,让编译器自行推导捕获列表
[=] 值捕获,让编译器自行推导捕获列表

// 4. 表达式捕获,允许捕获的成员用任意的表达式进行初始化,即允许了右值捕获
#include 
#include 
int main() {
	auto important = std::make_unique<int>(1);
	auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
		return x+y+v1+(*v2);
	};
	
	std::cout << add(3,4) << std::endl;
	return 0;
}

// 5. 泛型Lambda 使用auto
auto add = [](auto x, auto y) {
	return x+y;
};

add(1, 2);
add(1.1, 2.2);

3.2 函数对象包装器std::function

Lambda表达式的本质是一个函数对象,当Lambda表达式的捕获列表为空时,可以作为一个函数指针进行传递,eg:

#include 
using foo = void(int);
void functional(foo f) {
	f(1);
}

int main() {
	auto f = [](int value) {
		std::cout << value << std::endl;
	};

	functional(f);	// 函数指针调用
	f(1);	// lambda 表达式调用
	return 0;
}

从上面代码可以看到,可以将函数指针传递调用,或者直接调用函数表达式,在C++11中统一了这些概念,将可以被调用的对象类型,统称为可调用类型。

std::function 通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作。eg:

#include 
#include 

int foo(int para){
	return para;
}

int main() {
	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;	// 10
	std::cout << func2(10) << std::endl;	// 1+10+10 = 21
}

std::bind / std::placeholder
std::bind 解决的问题:将部分调用参数提前绑定到函数身上成为一个新的对象,然后等参数齐全后,完成调用。eg:

int foo(int a, int b, int c){
	...;
}
int main() {
	// 将参数1,2绑定到函数foo上,但是使用std::placeholders::_1来为第一个参数占位
	auto bindFoo = std::bind(foo, std::placeholders::_1, 1, 2);
	// 调用bindFoo时只需要提供第一个参数就可以了
	bindFoo(1);
}

3.3 右值引用

  1. 左值、右值
    左值定义:可以理解为赋值符号左边的值,准确的说是表达式后依然存在的持久对象;
    右值定义:右边的值,表达式后就不再存在的临时对象;
    我觉得把左值可以理解为,有名字的变量或者对象并且长期存在的,右值就是没名字的常量或者变量,临时变量或者对象,函数返回的临时变量/对象
int a = 1;	// a为左值,1为右值
A getNewA(){return A();}
A a = getNewA();	// a是左值,在当前位置会长久存在,getNewA()返回的是右值,临时对象,复制一份给a之后就会被自动析构
  1. 左值引用、右值引用
左值引用使用的符号是&,如:
int a = 1;	// a是一个左值
int &b = a;	// 左值引用
int &b = 1;	// 编译错误,左值引用,但是1是一个右值,
右值引用使用的符号是&&,如:
int&& a = 1;	// 将 1 取了个别名
int b = 1;
int&& c = b;	// 不能将一个左值复制给一个右值引用
A&& a = getNewA();	// getNewA()的返回值是右值(临时变量)
/*getNewA()返回的右值本来在表达式语句结束后,其生命结束,而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,可以理解为将这个临时变量提出来取了个名字。*/
  1. 常量左值引用。左值引用只能绑定左值,右值引用只能绑定右值,但是常量左值引用是个例外。常量左值引用,可以绑定非常量左值、常量左值、右值(绑定右值的时候,可以把右值的生命期延长,但是只能读不可以修改)
const int& a = 1;	// 常量左值引用绑定右值,不会报错
const A& a = getNewA();	// 不会保证报错

将亡值:即将被销毁、却能够被移动的值。可以理解为:临时的值能够识别、同时又能够被移动。

  1. c++11提供了 std::move 这个方法将左值参数无条件的转换为右值,这样可以能够方便的获得一个右值临时对象,如:
#include 
#include 

void reference(std::string& str) {...}	// str左值
void reference(std::string&& str) {...}	// str右值

int main() {
	std::string lv1 = "string,";	// lv1左值
	std::string&& rv1 = std::move(lv1);	// 将lv1暂时变成右值???
	const std::string& lv2 = lv1+lv1;	// 合法,常量左值引用,记住lv2不可以被修改
	std::string&& rv2 = lv1+lv2;	// 合法,右值引用延长临时对象生命周期
	// rv2是一个左值,右值引用lv1;
}
  1. 移动语义,和拷贝进行区别
class MyString {
public:
	MyString(const char* cstr = nullptr){
		if(nullptr != cstr) {
			m_data = new char[strlen(cstr)+1];	// strlen不计算\0
			strcpy(m_data, cstr);
		}
		else {
			m_data = new char[1];
			*m_data = '\0';
		}
	}
	// 拷贝构造
	MyString(const MyString& str) {	// 引用肯定不是空指针
		m_data = new char[strlen(str.m_data)+1];	// strlen不计算\0
		strcpy(m_data, str.data);
	}
	// 移动构造
	MyString(MyString&& str) : m_data(str.m_data){
		str.m_data = nullptr;
	}
private:
	char* m_data;
};

int main() {
	vector<MyString> vec;
	vec.push_back(MyString("hello"));
};

MyString 类中有构造函数、拷贝构造函数、移动构造函数,观察后两个的实现,拷贝构造用的是常量左值引用,移动构造用的是右值引用,而在main函数中的 MyString(“hello”) 是一个临时对象,编译器会优先调用移动构造函数。这样就不用调用拷贝构造函数,拷贝一份MyString(“hello”)到vec数组中,然后再调用析构函数。而是直接把MyString(“hello”) 移动到vec中。
另外的例子

MyString str1("hello");	// 左值
MyString str2("world");	// 左值
MyString str3(str1);	// 调用拷贝构造函数
MyString str4(std::move(str2));	// str::move将str2转比成右值,调用移动构造函数
  1. 完美转发
    C++11引入的两个概念:
  • 模板函数对右值引用参数的推导:向一个模板函数传递一个左值实参,对应形参是右值引用,编译器会把该实参推导为左值引用;
  • 引用折叠:由于C++不允许“引用的引用”,编译器将下图的实参类型进行推导。其中,前3种情况会转化为左值引用,后1种情况会转化为右值引用。只要形参和实参有一个是左值,推导后实参类型都是左值引用
    学习C++11和C++14_第1张图片
    例子:
void reference(int& v) {std::cout << "左值" << std::endl;}
void reference(int&& v) {std::cout << "右值" << std::endl;}

template <typename T>
void pass(T&& v) {	//一个声明的右值引用其实是一个左值
	std::cout << "普通传参:";
	reference(v); // 始终调用 reference(int& )
}

int main() {
	std::cout << "传递右值:" << std::endl;
	pass(1); // 1是右值, 但输出左值
	std::cout << "传递左值:" << std::endl;
	int v = 1;
	pass(v); // r 是左引用, 输出左值
	return 0;
}

就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用),引入完美转发的概念。使用std::forward

template <typename T>
void pass(T&& v) {
	std::cout << "普通传参:";
	reference(v);
	std::cout << "std::move 传参:";
	reference(std::move(v));
	std::cout << "std::forward 传参:";
	reference(std::forward<T>(v));
}

int main() {
	std::cout << "传递右值:" << std::endl;
	pass(1);
	std::cout << "传递左值:" << std::endl;
	int v = 1;
	pass(v);
	return 0;
}
// 输出结果
传递右值:
普通传参:左值引用
std::move 传参:右值引用
std::forward 传参:右值引用
传递左值:
普通传参:左值引用
std::move 传参:右值引用
std::forward 传参:左值引用

综上可以看出,如果1. 引用折叠后的不管是1还是v都是左值引用;2. std::move(v)后,都是右值引用;3. std::forward(v)将完美转发(传递)原来的引用属性。

4. 对标准库的扩充:新增容器

4.1 std::array

对比std::vector:1.std::vector保存在堆内存中,而std::array保存在栈内存中;2. 封装了一些函数
std::array使用时,指定类型和大小(大小必须是常数表达式)。

std::array<int, 4> arr = {1, 2, 3, 4};
std::sort(arr.begin(), arr.end());

4.2 std::forward_list

std::list 被认为是双向链表,可以实现恒定时间(不用遍历)的插入和擦除,并在两个方向上迭代;
主要缺点就是不能直接访问元素位置,需要线性搜索。
std::forward_list 是单链表,不需要双向迭代的时候,比std::list空间利用率更高。

4.3 无序容器

std::unordered_map / std::unordered_multimap
std::unordered_set / std::unordered_multiset
换句话说,即使存进去是有序的,输出也是无需的。

std::unordered_map<int, std::string> u = {
	{1, "1"},
	{2, "2"},
	{3, "3"}
};
for (const auto it : u)
	std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
// 输出
Key:[2] Value:[2]
Key:[3] Value:[3]
Key:[1] Value:[1]

4.4 元组 std::tuple: 用来存放不同类型的数据

  1. std::make_tuple(char, double, string) 构造元组
  2. std::get(name) 获得元组name位置key的值
  3. std::tie 用法见下面代码
#include 
#include 

auto get_student(int id)
{
	if (id == 0)
		return std::make_tuple(3.8, 'A', "张三");
	if (id == 1)
		return std::make_tuple(2.9, 'C', "李四");
	return std::make_tuple(0.0, 'D', "null");
}

int main()
{
	auto student = get_student(0);
	std::cout << "ID: 0, "
	<< "GPA: " << std::get<0>(student) << ", "
	<< "成绩: " << std::get<1>(student) << ", "
	<< "姓名: " << std::get<2>(student) << '\n';
	
	double gpa;
	char grade;
	std::string name;
	// 元组进行拆包
	std::tie(gpa, grade, name) = get_student(1);
	std::cout << "ID: 1, "
	<< "GPA: " << gpa << ", "
	<< "成绩: " << grade << ", "
	<< "姓名: " << name << '\n';
}
  1. std::get<>中除了放常量(一定要是常量,不然编译不通过),还可以放类型
std::tuple<double, char, std::string, double> t(3.8, 'A', "张三", 2.0);
std::get<std::string>(t);
std::get<double>(t); // 非法, 引发编译期错误
  1. 如何处理get< 变量 >(),使用 boost::variant 配合变长模板参数
#include 

template <size_t n, typename... T>
boost::variant<T...> _tuple_index(size_t i, const std::tuple<T...>& tpl) {
	if (i == n)
		return std::get<n>(tpl);
	else if (n == sizeof...(T) - 1)
		throw std::out_of_range("越界.");
	else
		return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(i, tpl);
}

template <typename... T>
boost::variant<T...> tuple_index(size_t i, const std::tuple<T...>& tpl) {
	return _tuple_index<0>(i, tpl);
}

int i = 1;
std::cout << tuple_index(i, t) << std::endl;
  1. 元组合并与遍历
// 1. 合并元组, 使用std::tuple_cat
auto newTuple = std::tuple_cat(get_student(1), std::move(t));
// 2. 遍历元组
# 知道tuple的长度
template <typename T>
auto tuple_len(T &tpl) {
	return std::tuple_size<T>::tpl;
}
# 遍历
for(int i = 0; i != tuple_len(new_tuple); ++i)
	std::cout << tuple_index(i, new_tuple) << std::endl;

5. 对标准库的补充

5.1 RAII和引用计数

引用计数是为了防止内存泄漏而产生的,基本想法是对于动态分配的对象,进行引用计数,每当增加一次对象的引用,那么引用对象的引用计数就会增加一次,每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
RAII资源获取即初始化技术对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间。

5.2 智能指针

C++11 引入智能指针,使用引用计数的概念,自动释放内存。

  1. std::shared_ptr
  2. std::unique_ptr
  3. std::weak_ptr

6. 正则表达式库

6.1 正则表达式

正则表达式描述了一种字符串的匹配模式,一般用于实现三个功能:

  1. 检查一个串是否包含某种形式的子串
  2. 将匹配的子串替换
  3. 从某个串中取出符合条件的子串

特殊字符
学习C++11和C++14_第2张图片
限定字符
学习C++11和C++14_第3张图片

正则表达式 [a-z]+\.txt
[a-z]表示小写字母中任意一个字母,+ 表示多次匹配
.表示匹配单个字符,但是我们这里就要用.越来的含义时,就是用\转义符 \.就表示.
所以[a-z]+\.txt匹配名字为小写字母的.txt文件

6.2 正则表达式库函数

c++ 11提供的正则表达式操作是对 std::string 的,用std::regex进行初始化,通过std::regex_match进行匹配,从而产生std::smatch

#include 
#include 
#include 

int main() {
	std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};	// 字符串数组
	std::regex txt_regex("[a-z]+\\.txt");	// 初始化一个正则表达式
	for (const auto &fname: fnames)
		std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
// 在 C++ 中 `\` 会被作为字符串内的转义符,为使 `\.` 作为正则表达式传递进去生效,需要对 `\` 进行二次转义,从而有 `\\.`

7. 语言级线程支持

7.1 std::thread

std::thread用于创建一个执行的线程实例,所以他是一切并发编程的基础,使用时需要包含 头文件

#include 
#include 
void foo() {...}
int main() {
	std::thread t(foo);
	t.join;
	return 0;
}

7.2 std::mutex 和 std::unique_lock

std::mutex 初始化一个互斥量, 使用std::lock函数上锁,std::unlock解锁,还有模板类std::lock_guard实现RAII的功能。

void some_operation(const std::string &message) {
	static std::mutex mutex_;
	std::lock_guard<std::mutex> lock(mutex_);
	// ...操作
	// 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁
	// 因此这个函数内部的可以认为是临界区
}

由于 C++保证了所有栈对象在声明周期结束时会被销毁,所以这样的代码也是异常安全的。无论 some_operation() 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 unlock()

std::unique_lock
std::unique_lock 则相对于 std::lock_guard 出现的, std::unique_lock 更加灵活, std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权)的方式管理 mutex 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用std::unique_lock

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

7.3 std::future 和std::packaged_task

7.4 std::condition_variable

8 其他

8.1 long long int

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

8.2 noexcept 修饰和操作

C++有一套完整的异常处理机制,但是没有接触过,后续总结一下。

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

  1. 函数可能抛出任何异常
  2. 函数不能抛出任何异常
    并且使用noexcept对这两种行为进行限制。
void may_throw();	// 可能抛出异常
void no_throw() noexcept;	// 不可能抛出异常???什么情况不会抛出异常

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

8.3 字面量

原始字符串字面量

传统的C++:C:\\What\\The\\Tranditional 
C++11: R"(C:\\What\\The\\Tranditional)";

你可能感兴趣的:(C++语言,c++,c++11)