原创
最近更新: 2021/11/03 23:45

C++学习笔记 之 C++11新特性

系列目录

C++学习笔记 之 基础篇

C++学习笔记 之 面向对象篇

C++学习笔记 之 C++11新特性


参考资料

C++11教程:C++11新特性大汇总

移动语义

指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

分为移动赋值运算符移动构造函数,一般而言,这比拷贝效率更高

左值和右值

左值

可以出现在赋值符号(=)左边(也可以出现在右边)。有名称的变量,可以被寻址。

(纯)右值/将亡值

只能出现在赋值符号右边,没有名称,不能被寻址。

一般而言,右值的作用是给左值赋值,或作为表达式的一部分。当相关运算结束之后,右值的相关资源就会被销毁,即为“将亡”。

譬如字面值1、函数返回值等匿名变量

右值引用

&&用于右值引用,一般是与移动语义配合使用,其他场合一般不使用,可以简单看作是一种特殊的数据类型。

详见“move()函数”一节。

int num = 10;
int &b = num; //正确,&用于左值引用
int &c = 10; //报错,&不可用于右值引用
int &&c = 10; //正确,&&用于右值引用
int &&c = num; //报错,&&不可用于左值引用

基本上第一行和第四行可以看做是相当的用法,一个变量成为右值的引用之后,其作用相当于左值。

move()函数

move()函数可以接收左值或者右值作为参数,但会将作为输入参数的左值强制转换为右值引用,于是就能够根据参数是左值或者右值调用不同的构造函数的重载。

move()函数本身并没有任何作用,只有搭配了合适的重载函数才有意义。

std::move()实际上是static_cast<T&&>()的简单封装。

class Person{
public:
    int p;
    Person(int p){
        this->p = p;
    }
	//一般的拷贝构造函数
    Person(const Person& p){
        this->p = p.p + 1;
    }
	//传入的参数为右值引用时调用这个构造函数
    Person(const Person&& p){
        this->p = p.p - 1;
    }
};
int main() {

    Person* p = new Person(100);
    Person* p2 = new Person(move(*p));
    Person* p3 = new Person(*p);

    cout << p->p << endl;//100
    cout << p2->p << endl;//99
    cout << p3->p << endl;//101

    return 0;
}

智能指针

智能指针主要用于解决指针使用中的两个主要问题:

  • 内存泄漏:用指针new了一个对象之后,忘记进行delete,或者由于程序出现异常等问题而跳过了delete代码。

  • 重复释放:多个指针指向同一个内存空间,如果都进行delete,就会出错。

内存泄漏问题有一些解决方法,如设置一个计数器,new的时候+1,delete的时候-1,可以检测内存泄漏的情况。

C++实现了各种智能指针,可以在一定程度上解决上述问题。

智能指针本质上是对普通指针进行了包装成一个类,析构函数实现了自动释放指向的内存,同时禁止拷贝和赋值,只能通过move()函数实现资源的传递。

std::auto_ptr

是C++98的方案,在C++11中被废除,因为会有以下潜在的内存崩溃问题:

auto_ptr<string> p1 (new string ("qwerty"));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。

std::unique_ptr

用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权。

unique_ptr 不允许左值在右边直接赋值,要用std::move 进行临时右值的赋值,并将原指针置为 nullptr,因此避免了 auto_ptr 的问题。

总之, C++不允许 unique_ptr 指向的内存被多个指针引用。

#include <memory>

//使用了类模板,需要提前指明指针指向的类
std::unique_ptr<int> p1();
std::unique_ptr<int> p2(nullptr);//空指针初始化
std::unique_ptr<int> p3(new int);//非空初始化
std::unique_ptr<int> p5(p4);//错误,不能直接进行拷贝
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数
std::unique_ptr<string> p6;
p6 = unique_ptr<string>(new string ("You"));//正确,因为右边是临时右值
//std::move()会将控制权转移,原来的指针会变为空

std::shared_ptr

在实现上采用的是引用计数机制,多个 shared_ptr 智能指针可以共同使用同一块堆内存。

只有引用计数为 0 时,堆内存才会被自动释放。

#include <memory>

std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr
//以下两行效果相同
std::shared_ptr<int> p3(new int(10));
std::shared_ptr<int> p3 = std::make_shared<int>(10);

//拷贝构造函数
std::shared_ptr<int> p4(p3);
std::shared_ptr<int> p4 = p3;
//移动构造函数
std::shared_ptr<int> p5(std::move(p4));
std::shared_ptr<int> p5 = std::move(p4);
//用普通指针初始化
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误,同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常

std::weak_ptr

weak_ptr类型指针通常不单独使用,只能和 shared_ptr 类型指针搭配使用。

可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息。

  • weak_ptr 可以通过 shared_ptr 赋值来初始化
shared_ptr<A> ptr_a(new A());

cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1
weak_ptr<A> wk_ptr_a = ptr_a;//可以使用赋值
cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出不变,weak_ptr不影响引用计数

if (!wk_ptr_a.expired()){//expired()用来检测是否过期
	wk_ptr_a.lock()->print();//lock()可以用来获取指向的对象
}

weak_ptr 是用来解决 shared_ptr 环形引用时死锁造成的内存泄漏问题,详见weak_ptr浅析

如果两个 shared_ptr 指向的对象里面各存在一个 shared_ptr 成员变量相互引用,那么这两个对象空间的引用计数永远不可能下降为0,资源永远不会释放。而 weak_ptr 本身不会增加引用计数,可以用在这个场景中。


auto和decltype

auto n1 = 10;         //编译器根据10判断n1是int
decltype(10) n2 = 99; //编译器根据10判断n2是int,和99无关

auto关键字表示自动类型推导,编译器在编译过程中会根据赋值表达式的右值来确定auto的类型。

decltype表示声明类型,根据括号中的表达式确定变量的类型,不需要赋值操作。

是一种语法糖,可以简化代码,尤其是一些难以书写的类型,譬如函数指针类型。


lambda 匿名函数

//[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 {函数体;};

//用auto自动识别类型,实际上的类型是main()::<lambda(int, int)>
auto l = [](int a, int b) -> int{return a + b;}
int c = l(1,2);
//上面两行等价于int c = [](int a, int b) -> int{return a + b;}(1,2);

//用lambda函数作为比较器
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; });
外部变量格式 功能
[] 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
[=] 只有一个 = 等号,表示以值传递的方式导入所有外部变量;
[&] 只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
[val1,val2,...] 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
[&val1,&val2,...] 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
[val,&val2,...] 以上 2 种方式还可以混合使用,变量之间没有前后次序。
[=,&val1,...] 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this] 表示以值传递的方式导入当前的 this 指针。

可变参数模板

variadic template

这玩意很复杂,深入讨论见侯捷 - C++新标准-C++11/14 - P15

用于配合可变参数函数使用,用于参数类型不限、数量不限的情况。

可以用于递归的情况,每层递归减少一个参数。当参数减少到一定程度,会调用重载的参数特化的版本。

//参数特化的情况,用于递归结束条件
//当函数只有一个参数,编译器会调用这个,而不是下面的版本
template<typename T>
void printX(const T& firstArg){
    cout << "The last one: " << firstArg << endl;
}

//一般情况下编译器会调用这个版本
//注意这里省略号...的用法!!!
template<typename T, typename... Types>
void printX(const T& firstArg, const Types&... args){
    //可以用sizeof...(Types)或sizeof...(args)获取参数个数
    cout << firstArg << "\t" << sizeof...(Types) << "\t" << sizeof...(args) << endl;
    printX(args...);//注意这里省略号...的用法
}

int main() {
    printX(1,1.2,'a',"asd",2,3);
    return 0;
}

其他更新

统一初始化

扩展了花括号初始化的适用范围,可以作为容器和类的初始化方式,也可作为参数和返回值进行传递。

编译器看到花括号括起来的初始化列表,会构造一个std::initializer_list<T>。进行初始化时,编译器会将元素逐一地传给构造函数。

int values[] {1,2,3};
vector<string> v {"a","b","c"};
complex<double> c {1.0,3.0};
int j = {1};
int i{};//i会被初始化为0
int* p{};//p会被初始化为nullptr

nullptr

以往C++使用0或者NULL表示空指针,会出现void*和int类型混杂的现象。

nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。

遍历元素的for循环

python和Java里面都有,不多说了。

for (int i : {1,2,3,4,5}) {
	cout << ch;
}

vector<int> vec;
for (int& i : vec) {//应当注意引用传递的情况
	i *= 3;
}

constexpr

用来修饰函数,编译器会在编译期执行,只留下返回值。用于优化程序。

函数体内只能含有一个return语句(一行)。

C++14后,也可以包含分支、循环语句等。

=default, =delete

用于类的构造函数、析构函数上。=default表示提醒编译器创建默认函数;=delete禁止编译器创建默认函数。

  • 显然,=default没什么用。
  • 理论上,你可以禁止各种构造函数和析构函数,但是后果自负。
class A {
public:
    A(int x){}//一般情况下,手动定义构造函数后,编译器就不会创建默认构造函数
    A() = default;//可以用default关键字提醒编译器创建默认构造函数,等价于A(){};
	//禁止拷贝和赋值
	A(const A&) = delete;//禁止编译器创建默认拷贝构造函数
	A &operator=(const A&) = delete;//禁止编译器创建默认拷贝赋值函数
	//~A() = default;默认析构函数,写不写都一样
};

评论区