【C++】新标准(11 14 17)


C++ 11有哪些新特性?

  • Lambda 表达式(匿名函数)
  • 引入了 auto 和 decltype 这两个关键字实现了类型推导
  • nullptr替代 NULL
  • 基于范围的 for 循环for (auto& i : res){}
  • 类和结构体的中初始化列表
  • std :: forward_list(单向链表)
  • 右值引用和std :: move语义

Lambda表达式

C++11的一大亮点就是引入了Lambda表达式。利用Lambda表达式,可以方便的定义和创建匿名
函数

1、声明Lambda表达式

[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下

  • capture list:捕获外部变量列表
  • params list:形参列表
  • mutable指示符:用来说用是否可以修改捕获的变量
  • exception:异常设定
  • return type:返回类型
  • function body:函数体

此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:

序号格式
1[capture list] (params list) -> return type {function body}
2[capture list] (params list) {function body}
3[capture list] {function body}
  • 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值
  • 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型:

如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类
型确定;
如果function body中没有return语句,则返回值为void类型

  • 格式3中省略了参数列表,类似普通函数中的无参函数。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool cmp(int a, int b)
{
    return  a < b;
}

int main()
{
    vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
    vector<int> lbvec(myvec);

    sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
    cout << "predicate function:" << endl;
    for (int it : myvec)
        cout << it << ' ';
    cout << endl;

    sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });  
    // Lambda表达式
    cout << "lambda expression:" << endl;
    for (int it : lbvec)
        cout << it << ' ';
}
//在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表
达式,
//我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。

2、捕获外部变量

Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可
以被该Lambda表达式使用)。那么,在哪里指定这些外部变量呢?Lambda表达式通过在最前面
的方括号[]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了
外部变量

#include <iostream>
using namespace std;

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; }; 
    f(); // 输出:123

    //或通过“函数体”后面的‘()’传入参数
    auto x = [](int a){cout << a << endl;}(123); 
}
  • 上面这个例子先声明了一个整型变量a,然后再创建Lambda表达式,该表达式“捕获”了a变量
  • ,这样在Lambda表达式函数体中就可以获得该变量的值。
  • 类似参数传递方式(值传递、引入传递、指针传递),在Lambda表达式中,外部变量的
  • 捕获方式也有值捕获、引用捕获、隐式捕获

2.1、值捕获

值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝
的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:123
}

这里需要注意的是,如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该
外部变量的值

2.2、引用捕获

使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&。如下:

int main()
{
    int a = 123;
    auto f = [&a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:321
}

从示例中可以看出,引用捕获的变量使用的实际上就是该引用所绑定的对象

2.3、隐式捕获

上面的值捕获和引用捕获都需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。
除此之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称
之为隐式捕获
。隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部
变量,[&]表示以引用捕获的方式捕获外部变量

// 隐式值捕获示例:
int main()
{
    int a = 123;
    auto f = [=] { cout << a << endl; };    // 值捕获
    f(); // 输出:123
}

// 隐式引用捕获示例:
int main()
{
    int a = 123;
    auto f = [&] { cout << a << endl; };    // 引用捕获
    a = 321;
    f(); // 输出:321
}

2.4、混合方式

上面的例子,要么是值捕获,要么是引用捕获,Lambda表达式还支持混合的方式捕获外部变量
,这种方式主要是以上几种捕获方式的组合使用。

到这里,我们来总结一下:C++11中的Lambda表达式捕获外部变量主要有以下形式:

  捕获形式    说明
[]不捕获任何外部变量
[变量名, …]默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获
,需要显示声明(使用&说明符)
[this]以值的形式捕获this指针
[=]以值的形式捕获所有外部变量
[&]以引用形式捕获所有外部变量
[=, &x]变量x以引用形式捕获,其余变量以传值形式捕获
[&, x]变量x以值的形式捕获,其余变量以引用形式捕获

3、修改捕获变量(mutable)

前面我们提到过,在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改
该外部变量,否则会引发编译错误
。那么有没有办法可以修改值捕获的外部变量呢?这是就
需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示
例:

int main()
{
    int a = 123;
    auto f = [a]()mutable { cout << ++a; }; // 不会报错
    cout << a << endl; // 输出:123
    f(); // 输出:124
}

4、Lambda表达式的参数

Lambda表达式的参数和普通函数的参数类似,那么这里为什么还要拿出来说一下呢?原因是在L
ambda表达式中传递参数还有一些限制,主要有以下几点:

  • 参数列表中不能有默认参数
  • 不支持可变参数
  • 所有参数必须有参数名

auto、decltype和decltype(auto)的用法

1 auto

  • auto被称作类型说明符,它可以让编译器替我们去分析表达式所述的类型,然后将所定义的
    变量声明为该类型。
  • 原理auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时
    效率的降低
    有点类似于模板类型推导
//auto在C++14中可以作为函数的返回值,因此auto AddTest(int a, int 
b)的定义是没问题的。
auto AddTest(int a, int b) {
    return a + b;
}
void func(auto x) {}            // auto不能定义函数参数
struct stu {
    auto var = 10;              // 非静态auto类型不能定义成员变量
    static const auto var = 10; // 编译通过
};

int main(){
    char x[3] = { 0 };
    auto y = x;
    auto z[3] = x;                 // auto不能作为数组类型
    std::vector<auto> v = { 1 };   // auto不能作为模板实例化类型
    return 0;
}
const int a1 = 10;
auto  b1= a1;       //b1的类型为int而非const int(去除const)
const auto c1 = a1;//此时c1的类型为const int

const int a2 = 10;
auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int
//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型

//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const 
int*
const auto l = i; //如果希望推断出的类型是顶层const的, 
那么就需要在auto前面加上const

//引用和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*
  • auto 让编译器通过初始值来进行类型推演,从而获得定义变量的类型,所以说 auto
    定义的变量必须有初始值
  • auto作为函数返回值时,只能用于定义函数,不能用于声明函数
  • auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
  • 使用auto要注意与复合类型的搭配①数组会被转换成首元素的指针,②引用会被去掉,③指针会被保留,④顶层const会被去掉,底层const会被保留

2 decltype

  • decltype被称作类型说明符,它的作用是选择并返回操作数的数据类型

随着程序越来越复杂,程序中用到的类型也越来越多,这种复杂性体现在两个方面:

  1. 一些类型难于“拼写”,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。
  2. 有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文寻求帮助
    1. 解决问题一,可以使用类型别名技术(typedef)。
    2. 解决问题二,可以使用auto和本文的主题:decltype。
//decltype + 变量 var
//decltype + 表达式 expr
//decltype + 函数名 func_name

// sum的类型就是函数f返回的类型
decltype(f()) sum = x;

2.1 工作原理

  • decltype并不会实际计算表达式的值,编译器分析表达式并得到它的类型。
  • 函数调用也算一种表达式,因此不必担心在使用decltype时真正的执行了函数,正如前
    例中的f()。

2.2 decltype + 变量

当使用decltype(var)的形式时,decltype会直接返回变量的类型(包括顶层const和引用),不会返回变量作为表达式的类型。

  • decltype加指针也会返回指针的类型。
  • decltype加数组,不负责把数组转换成对应的指针,所以其结果仍然是个数组。

总之decltype(var)完美保留了变量的类型。

const int ci = 0, &cj = ci;

// x的类型是const int
decltype(ci) x = 0;
// y的类型是const int &
decltype(cj) y = x;

2.3 decltype + 表达式

当使用decltype(expr)的形式时,decltype会返回表达式结果对应的类型。一个表达式的结果不是左值,就是右值

因此,decltype(expr)的结果根据expr的结果不同而不同:expr返回左值,得到该类型的左值引用;expr返回右值,得到该类型。

int i = 42, *p = &i, &r = i;

// r + 0是一个表达式
// 算术表达式返回右值
// b是一个int类型
decltype(r + 0) b;

// c是一个int &
//解引用运算符*作用于指针类型,得到了p指向的对象的左值(*p = 2很正确),p是指向int的指针,
//因此decltype(*p)得到的类型是int &。
decltype(*p) c = i; //解引用返回左值

当变量作为表达式时,返回的是该变量的一个左值形式(因为该表达式的结果可以作为赋值语句的左侧的值)。因此,使用decltype理应得到一个该类型的左值引用。但是decltype单独作用于对象,没有使用对象的表达式的属性,而是直接获得了变量的类型。要想获得变量作为表达式的类型,可以加一个括号:

int i = 42;

// 加了括号,变成了表达式
// 返回的是i的左值形式
// 因此ri的类型是int &
decltype((i)) ri = i;
int i = 42, *p = &i;

decltype((p)) temp = p;

解析:decltype作用的是表达式,§得到的是p的左值,所以temp一定是一个引用;p是指向int类型的指针,因此decltype得到的是指向int类型的指针的引用。

2.4 decltype + 函数

C++中通过函数的返回值和形参列表,定义了一种名为函数类型的东西。它的作用主要是为了
定义函数指针。

// 声明了一个函数类型
using FuncType = int(int &, int);

// 下面的函数就是上面的类型
int add_to(int &des, int ori);

// 声明了一个FuncType类型的指针
// 并使用函数add_to初始化
FuncType *pf = add_to;

int a = 4;

// 通过函数指针调用add_to
pf(a, 2);

我们可以使用decltype获得函数add_to的类型:

当使用decltype(func_name)的形式时,decltype会返回对应的函数类型,不会自动转换成相
应的函数指针。

decltype(add_to) *pf = add_to;

总结

  • decltype是为了解决复杂的类型声明而使用的关键字,称作decltype类型说明符。
  • decltype可以作用于变量、表达式及函数名
    • 作用于变量直接得到变量的类型
    • ②作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;
    • 作用于函数名会得到函数类型,不会自动转换成指针
  • decltype不会去真的求解表达式的值,可以放心使用。

为什么用nullptr替代 NULL

  • 在C语言中,NULL被定义为指针(void*)0,而在C++语言中,NULL则被定义为整数0
  • nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

存在对不同指针类型的函数重载,此时如果传入nullptr指针则仍然存在无法区分应实际调
用哪个函数,这种情况下必须显示的指明参数类型。

fun((char*)nullptr);//(函数重载)
 #ifdef __cplusplus
 #define NULL 0
 #else
 #define NULL ((void *)0)
 #endif
void fun(char* p) {
     cout << "char*" << endl;
 }
 void fun(int p) {
     cout << "int" << endl;
 }
   int main()
 {
     fun(NULL); // C++ 中为 0
     return 0;
 }
 //输出结果:int

基于范围的for循环

本例的x变量相当于传值参数。在循环内部更改 x 不会更改数组。但如果用 & 将 x
定义成传引用,对 x 的修改就会反映到数组中。可以使用 const 修饰符指定变量不能修改。

int array[] = {2, 4, 6, 8};
for(int &x : array){
 x++;//如果用 & 将 x 定义成传引用,对 x 的修改就会反映到数组中(对数组加1)
}
for(auto x : array){
 cout << x;
}
cout << endl;

C++左值引用和右值引用?

  • 右值引用和左值引用都是属于引用类型,无论是声明一个左值引用还是右值引用,都必须立即进行初始化。
  • 左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。

1 什么是左值和右值?

  • 左值: 就是有确定的内存地址、有名字的变量,可以被赋值,可以在多条语句中使用;
  • 右值: 没有名字的临时变量,不能被赋值,只能在一条语句中出现,如:字面常量和临时
    变量。

2 如何区分左值与右值?

看能不能对表达式取地址

能否用“取地址&”运算符获得对象的内存地址。

1.对于临时对象,它可以存储于寄存器中,所以是没办法用“取地址&”运算符;

2.对于常量,它可能被编码到机器指令的“立即数”中,所以是没办法用“取地址&”运
算符;

这也是C/C++标准的规定。

  • 返回左值的表达式:返回左值引用的函数、赋值、下标、解引用、前置递增/递减运算符++
    i。
  • 返回右值的表达式:返回非引用类型的函数、算术、关系、位、后置递增/递减运算符i++
    、lambda表达式。
int a = 5;
int b = 6;
int *ptr = &a; 
vector v1; 
string str1 = "hello " ;
string str2 = "world" ;
const int &m = 1 ;

a+b//临时对象
a++//右值,是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而
那份拷贝是临时对象(不可以对其取地址),故其是右值。
++a//左值,使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左
值。
v1[0]//调用了重载的[]操作符,而[]操作符返回的是一个int 
&,为持久对象(可以对其取地址)是左值。
string("hello")//临时对象(不可以对其取地址),是右值;
str1+str2//调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右
值;
str1//左值
*p//左值
#include <bits/stdc++.h>
using namespace std;

template<typename T>
void fun(T&& t)
{
    cout << t << endl;
}

int getInt()
{
    return 5;
}

int main() {

    int a = 10;
    int& b = a;  //b是左值引用
    int& c = 10;  //错误,c是左值不能使用右值初始化
    int&& d = 10;  //正确,右值引用用右值初始化
    int&& e = a;  //错误,e是右值引用不能使用左值初始化
    const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
    const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
    const int&& h = 10; //正确,右值常引用
    const int& aa = h;//正确
    int& i = getInt();  //错误,i是左值引用不能使用临时变量(右值)初始化
    int&& j = getInt();  //正确,函数返回值是右值
    fun(10); //此时fun函数的参数t是右值
    fun(a); //此时fun函数的参数t是左值
    return 0;
}

3 左值引用和右值引用

  • C++ 11引入右值引用,c++ 11中用 &表示左值引用,用&&表示右值引用。
int i = 2;//左值
int &a = i; //左值引用

int &&a = 10; //右值引用
  • 根据修饰符的不同,左值引用可分为:非const左值、const左值;右值引用可分为:非const右值、const右值。
int i = 10;//非const左值
const int j = 20;

//非const左值引用
int &a = i;//非const左值引用 绑定到 非const左值,编译通过
int &a = j;//非const左值引用 绑定到 const左值,编译失败
int &a = 5;//非const左值引用 绑定到 右值,编译失败

//const左值引用
const int &b = i;//const左值引用 绑定到 非const左值,编译通过
const int &b = j;//const左值引用 绑定到 const左值,编译通过
const int &b = 5;//const左值引用 绑定到 右值,编译通过

//非const右值引用
int &&c = 30;//非const右值引用 绑定到 右值,编译通过
int &&c = i;//非const右值引用 绑定到 const左值,编译失败(不能将一个右值引用绑定到左值上)

//const右值引用
const int &&d = 40;//const右值引用 绑定到 右值,编译通过
const int &&d = c;//const右值引用 绑定到 非const右值,编译通过
  • 非const的左值引用只能被绑定到非const左值
  • 非const的右值引用只能被绑定到非const右值

4 总结

  • 非const左值引用只能绑定到非const左值,不能绑定到const左值、非const右值和const右值;
  • const左值引用可以绑定到const左值、非const左值、const右值、非const右值。(不过常量左值所引用的右值在它的“余生”中只能是只读的。)
  • 非const右值引用只能绑定到非const右值;(不能将一个右值引用绑定到任何左值上
  • const右值引用可绑定到const右值和非const右值。
  • 右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。
  • 无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。

智能指针

智能指针详解

主要分为shared_ptr、unique_ptr和weak_ptr三种,使用时需要引用头文件。c++ 98中还有auto_ptr,基本被淘汰了,不推荐使用。而c++ 11中shared_ptr和weak_ptr都是参考的boost库中实现的。

智能指针的作用:是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

auto_ptr

采用所有权模式。(c++98的方案,cpp11已经抛弃)

auto_ptr<string> p1(new string("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所以权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存泄漏问题。

unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。采用所有权模式。

unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4: //#5
p4=p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr 将存在一段时间,编译器将禁止这么做,比如

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给pu3后就会被销毁。这种随情况而已的行为表明,unique_ptr优于允许两种赋值的auto_ptr.

注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标
准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

unique_ptr<string> ps1,ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1<< endl;

shared ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

  • use_count 返回引用计数的个数
  • unique 返回是否是独占所有权(use_count为1)
  • swap 交换两个 shared_ptr对象(即交换所拥有的对象)
  • reset 放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减
  • get 返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的.如
shared_ptr sp (new int(1)); //sp与sp.get()是等价的

weak_ptr

  • weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,进行该对象的内存管理的是那个强引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。

  • weak_ptr设计的目的是为配合shared_ptr而引入的一种智能指针,用来协助 shared_ptr 工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr.

class B;
class A {
public:
    shared_ptr<B> m_b;
};

class B {
public:
    shared_ptr<A> m_a;
};
int main() {
while (true)
  {
    shared_ptr<A> a(new A); //new出来的A的引用计数此时为1
    shared_ptr<B> b(new B); //new出来的B的引用计数此时为1
    a->m_b = b; //B的引用计数增加为2
    b->m_a = a; //A的引用计数增加为2
  }
 
  //由于指针存储在栈区,故b先出作用域,B的引用计数减少为1,不为0,所以堆上的B空间没有被释放
  //且B持有的A也没有机会被析构,A的引用计数也完全没减少
 
  //a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放
}

所以在使用基于引用计数的智能指针时,要特别小心循环引用带来的内存泄漏,循环引用不只是两方的情况,只要引用链成环都会出现问题。当然循环引用本身就说明设计上可能存在一些问题,如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用普通指针(或弱智能指针weak_ptr)即可
如果把其中一个改为weak_ptr就可以了,我们把类A里面的

shared_ptr<B> m_b 改为 weak_ptr<B>  m_b;

运行结果如下,这样的话,资源B的引用开始就只有1,当b析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时a析构时使A的计数减一,那么A的计数为0,A得到释放。

注意:不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问

a->m_b->print()
m_b是一个weak_ptr,应该先把它转化为shared_ptr,如:
shared_ptr p=a->b_m.lock(); p->print();

请添加图片描述
请添加图片描述

移动语义(std::move)

1 C++RAII机制

  • 保证在任何情况下,使用对象时先构造对象,最后析构对象

RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++ 语言的一种管理资源、避免泄漏的惯用法。利用的就是C++ 构造的对象最终会被销毁的原则。

如何使用RAII?

当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

2 C++11的移动语义

移动构造

我们可以定义一个右值引用将一个右值(将亡值)的内存资源获取。那么在拷贝构建一个对象时候,如果传入参数是一个右值,那么我们就可以直接引用这个右值,无需开辟资源深拷贝。这种做法称之为移动构造。
请添加图片描述

std::move

功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。

注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class MyString{
public:
	MyString(char* ptr = "")
	{
		_str = new char[sizeof(ptr)+1];
		strcpy(_str, ptr);
	}
	~MyString(){}
public:
	char* _str;
};

int main()
{
	MyString s1("Hello World!!!");//左值s1
	MyString&& s2 = std::move(s1);//右值引用s2
	s1 = MyString("你好,世界!!!");//s1仍然是左值
	cout << s1._str << "    " << s2._str << endl;
	system("pause");
	return 0;
}

完美转发

函数参数是右值时候,调用右值引用的函数时候,函数参数会丢失其右值属性。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int Func(int&& a)
{
	a = 20;//丢失了右值属性的右值引用
	return a;
}
int main()
{
	cout<<Func(10)<<endl;//纯右值
	system("pause");
}

这并不完美,我们需要函数参数在传参之后任然保持其原来的属性才完美。

所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。 这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

限定作用域枚举类型(enum class)

C++11引入了限定作用域的枚举类型,与普通枚举类型不同,限定作用域的枚举类型的成员名字在作用域外不可访问,所以这种枚举类型的名字可以和作用域外的名字重复。

enum class Color1{red,yellow,green};//限定作用域的枚举类型
enum struct Color2{ red, yellow, green };//限定作用域的枚举类型
enum Color3 { red, yellow, green };//不限定作用域的枚举类型
enum Color4 { red, yellow, green };//错误,重复定义了red, yellow, green

int main(void)
{

	Color1 color1 = Color1::red;
	Color2 color2 = Color2::red;
	Color3 color3 =red;
	system("pause");
	return 0;
}

定义枚举

定义枚举变量和定义结构体变量相同,有两种定义方法:

第一种:(定义枚举类型的同时定义枚举变量)
enum week{Mon = 1, Tues, Wed, Thurs}num;

第二种:(先定义枚举类型,再定义枚举变量)
enum week{Mon = 1, Tues, Wed, Thurs};
enum week num;

枚举类型与整型

  • 默认情况下,枚举成员值从0开始,依次加1,不过我们也可以为一个或几个枚举成员指定专门的值,没有显示提供初始值的枚举成员值等于之前枚举成员值加1。
enum Color { red=2, yellow, green };

int main(void)
{
	std::cout << red << std::endl;//2
	std::cout << yellow << std::endl;//3
	std::cout << green << std::endl;//4
	system("pause");
	return 0;
}
  • 不限定作用域的枚举类型能自动转换为整型,限定作用域的枚举类型不会进行隐式转换(不会自动转化为整型)。
enum Color1 { red=2, yellow, green };
enum class Color2 { red = 2, yellow, green };
int main(void)
{
	int i = red;
	int j = Color2::red;//错误
	system("pause");
	return 0;
}
  • 默认情况下限定作用域的枚举成员类型是int,不限定作用域的枚举类型则不存在默认类型,我们可以使用冒号更改枚举成员的整数类型。
enum Color1:long long { red=2, yellow, green };  //用 :更改枚举的整数类型
enum class Color2 { red = 2, yellow, green };
int main(void)
{
	auto color1 = red;
	auto color2 = Color2::red;
	std::cout << sizeof(color1) << std::endl;//8
	std::cout << sizeof(color2) << std::endl;//4
	system("pause");
	return 0;
}
  • 尽管枚举对象能够转换为整型,但是不能直接将整型值传给枚举形参
enum Color {red};

void printColor(Color color)
{
	std::cout << color << std::endl;
}

int main(void)
{
	printColor(0);//错误
	printColor(red);
	system("pause");
	return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇光_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值