C++11中常用的一些特性

目录

1、C++11

2、统一的列表初始化

2.1 {}初始化

2.2 std::initializer_list

3、声明

3.1 auto

3.2 decltype

3.3 nullptr

4、 范围for循环

5、STL中一些变化

新容器

array,

forward_list

6、右值引用和移动语义

6.1 左值引用和右值引用

6.2 左值引用与右值引用比较

6.3 右值引用使用场景和意义

6.4 完美转发

7、新的类功能

默认成员函数

类成员变量初始化

8、 可变参数模板

STL容器中的empalce相关接口函数:

9、 lambda表达式


1、C++11

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率.

2、统一的列表初始化

2.1 {}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Point {
    int _x; 
    int _y;
};
int main() {
    int x1 = 1;
    int x2{ 2 };
    int array1[]{ 1, 2, 3, 4, 5 };
    int array2[5]{ 0 }; 
    Point p{ 1, 2 };
    // C++11中列表初始化也可以适用于new表达式中 
    int* pa = new int[4]{ 0 };
    return 0; 
}

创建对象时也可以使用列表初始化方式调用构造函数初始化

class Date 
{
public:
    Date(int year, int month, int day) 
        :_year(year)
        ,_month(month) 
        ,_day(day) 
    {
        cout << "Date(int year, int month, int day)" << endl;
    } 
private:
    int _year;
    int _month;
    int _day;
};
int main() 
{
    Date d1(2022, 1, 1); // old style
    // C++11支持的列表初始化,这里会调用构造函数初始化 
    Date d2{ 2022, 1, 2 };
    Date d3 = { 2022, 1, 3 }; 
    return 0;
}

2.2 std::initializer_list

这是个什么类型呢,其实用花括号括起来的就是initializer_list

int main() 
{
    // the type of il is an initializer_list 
    auto il = { 10, 20, 30 };
    cout << typeid(il).name() << endl; 
    return 0;
}

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

让模拟实现的vector也支持{}初始化和赋值

namespace ltx
{
template<class T> 
class vector { 
public:
     typedef T* iterator;
     vector(initializer_list<T> l) 
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size(); 
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin(); 
         while (lit != l.end())
         {
             *vit++ = *lit++; 
         }
     }
     vector<T>& operator=(initializer_list<T> l) { 
         vector<T> tmp(l);
         std::swap(_start, tmp._start);
         std::swap(_finish, tmp._finish);
         std::swap(_endofstorage, tmp._endofstorage);
         return *this; 
     }
private:
     iterator _start;
     iterator _finish;
     iterator _endofstorage; 
 };
}

3、声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

        在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main() 
{
    int i = 10; 
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl; 
    cout << typeid(pf).name() << endl;
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; 
    //map<string, string>::iterator it = dict.begin();
    auto it = dict.begin(); 
    return 0;
}

3.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

// decltype的一些使用使用场景 
template<class T1, class T2> 
void F(T1 t1, T2 t2)
{
    decltype(t1 * t2) ret;
    cout << typeid(ret).name() << endl; 
}
int main() 
{
    const int x = 1; 
    double y = 2.2;
    decltype(x * y) ret; // ret的类型是double 
    decltype(&x) p;      // p的类型是int* 
    cout << typeid(ret).name() << endl; 
    cout << typeid(p).name() << endl;
    F(1, 'a'); 
    return 0;
}

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

4、 范围for循环

这个东西其实我们已经用了很多了,底层其实用的就是迭代器实现的。

5、STL中一些变化

新容器

C++11中的一些几个新容器有array,forward_list,unordered_set,unordered_map,但是实际最有用的是unordered_map和unordered_set。

array,

这个容器其实就是一个静态数组,和原生的静态数组不一样,他对越界的有了更为全面的处理,但是其实并没有必要,先入为主的原生静态数组已经深入人心了,而且,这个东西使用起来并不如vector。

forward_list

这个容器的底层使用单链表实现的,相比于list他节省了一些空间,其他的操作啥的,和STL中的其他容器没有什么区别

6、右值引用和移动语义

6.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main() 
{
    // 以下的p、b、c、*p都是左值 
    int* p = new int(0); 
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用 
    int*& rp = p;
    int& rb = b; 
    const int& rc = c; 
    int& pvalue = *p;
    return 0; 
}

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main() 
{
    double x = 1.1, y = 2.2; 
    // 以下几个都是常见的右值
    10;
    x + y; 
    fmin(x, y);
    // 以下几个都是对右值的右值引用 
    int&& rr1 = 10;
    double&& rr2 = x + y; 
    double&& rr3 = fmin(x, y);
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 
    10 = 1;
    x + y = 1; 
    fmin(x, y) = 1;
    return 0; 
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

6.2 左值引用与右值引用比较

左值引用总结:

        1. 左值引用只能引用左值,不能引用右值。

        2. 但是const左值引用既可引用左值,也可引用右值。

右值引用总结:

        1. 右值引用只能右值,不能引用左值。

        2. 但是右值引用可以move以后的左值。

6.3 右值引用使用场景和意义

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

namespace ltx
{
    bit::string to_string(int value) 
    {
        bool flag = true;
        if (value < 0)
        {
            flag = false; 
            value = 0 - value;
        }
        bit::string str; 
        while (value > 0)
        {
            int x = value % 10; 
            value /= 10;
            str += ('0' + x);
        }
            if (flag == false)
        {
            str += '-';
        }
        std::reverse(str.begin(), str.end()); 
        return str;
    } 
}
int main() 
{
    // 在bit::string to_string(int value)函数中可以看到,这里
    // 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷 
    贝构造)。
    ltx::string ret1 = ltx::to_string(1234); 
    ltx::string ret2 = ltx::to_string(-1234);
    return 0; 
}

右值引用和移动语义解决上述问题:

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

// 移动构造 
string(string&& s) 
 :_str(nullptr)
 ,_size(0)
 ,_capacity(0) 
{
    cout << "string(string&& s) -- 移动语义" << endl; 
    swap(s);
}
int main() 
{
    ltx::string ret2 = ltx::to_string(-1234); 
    return 0;
}

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

但是注意,移动构造匹配的是右值参数,这些右值,会被匹配成将亡值,所以将他们的数据换走,并不会产生什么影响,

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

不仅仅有移动构造,还有移动赋值:

在ltx::string类中增加移动赋值函数,再去调用ltx::to_string(1234),不过这次是将
ltx::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string& operator=(string&& s) 
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl; 
    swap(s);
    return *this; 
}
int main() 
{
    bit::string ret1;
    ret1 = bit::to_string(1234); 
    return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为ltx::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

6.4 完美转发

模板中的&& 万能引用

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。 
template<typename T>
void PerfectForward(T&& t) 
{
    Fun(std::forward<T>(t)); 
}
int main() 
{
    PerfectForward(10);           // 右值 
    int a;
    PerfectForward(a);            // 左值 
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b);            // const 左值 
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

7、新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:

        1. 构造函数

        2. 析构函数

        3. 拷贝构造函数

        4. 拷贝赋值重载

        5. 取地址重载

        6. const 取地址重载

        最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

        如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

类成员变量初始化

        C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化

强制生成默认函数的关键字default:

        C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

禁止生成默认函数的关键字delete:

        如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

8、 可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 
template <class ...Args>
void ShowList(Args... args) 
{}

STL容器中的empalce相关接口函数:

template <class... Args>
void emplace_back (Args&&... args)

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?

int main() 
{
    // 下面我们试一下带有拷贝构造和移动构造的ltx::string,再试试呢 
    // 我们会发现其实差别也不到,emplace_back是直接构造了,push_back 
    // 是先构造,再移动构造,其实也还好。
    std::list< std::pair<int, ltx::string> > mylist;
    mylist.emplace_back(10, "sort");
    mylist.emplace_back(make_pair(20, "sort"));
    mylist.push_back(make_pair(30, "sort"));
    mylist.push_back({ 40, "sort"});
    return 0; 
}

9、 lambda表达式

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}

1. lambda表达式各部分说明

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:
        在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

2. 捕获列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

注意:

a. 父作用域指包含lambda函数的语句块

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

        比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
        比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空。

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

f. lambda表达式之间不能相互赋值,即使看起来类型相同

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

孟婆的cappucino

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

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

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

打赏作者

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

抵扣说明:

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

余额充值