C++11新特性——总结



1. 列表初始化的使用

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}

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.C++11新声明

1. auto

C++11中将auto用于实现自动类型推断。要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。通过auto自动类型推断获得的变量可以使用typeid( )函数获得实际类型

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

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

	auto a = 1, b = 2;
	auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同

auto不能作为函数的参数不能直接用来声明数组

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a){}
// 此处代码编译失败,auto不能推导数组
auto b[] = {4,5,6};

2. decltype

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

int main()
{
    const int x = 1;
    double y = 2.2;
    int z = 0;

    decltype(x * y) ret; // ret的类型是double
    decltype(&x) p;  // p的类型是int*
    decltype(z) tmp;// z的类型是int

    cout << typeid(ret).name() << endl;//double
    cout << typeid(p).name() << endl;//int *
    cout << typeid(tmp).name() << endl;//int

    return 0;
}

在定义模板的时候特别好用,因为只有等到模板被实例化时才能确定类型。

template<class T, class U>
void func(T t, U u)
{
    ...
    decltype(T*U) tu;
    ...
}

3.返回类型后置

C++新增的一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型。让我们能够使用 decltype 来指定模板函数的返回类型。

template<class T, class U> -> decltype(T*U)
auto func(T t, U u)
{
    ...
    decltype(T*U) tu;
    ...
}

这里解决的问题是,在编译器遇到func的参数列表前, T和U还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做。

4.模板别名:using =

新语法可用于模板部分具体化,但是typedef不能

typedef array<double, 12> arrd;
typedef array<int, 12> arri;
typedef array<std::string, 12> arrstr;

arrd gallons;// gallons is type array<double, 12>
arri days;   // days    is type array<int, 12>
arrst months;// months  is type array<std::string, 12>

/*新语法*/
template<class T>
using arrtype = array<T, 12>;//template to create multiple aliases

arrtype<double> gallons;     //gallons is type array<double, 12>
arrtype<int> days;           //days    is type array<int, 12>
arrtype<std::string> months; //months  is type array<std::string, 12>

5. nullptr

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

为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr==0为true

6. explicit

C++11引入了关键字explicit,以禁止单参数构造函数导致的自动转换:

class Plebe
{
    ...
    Plebe (int);
    explicit Plebe(double);
    ...
};

intm main()
{
    Plebe a, b;
    a= 5;          //allowed
    b = 0.5;       //unallowed
    b = Plebe (0.5);//显式转换,allowed
}

7.类成员初始化

在类声明中,可使用等号大括号版本的初始化,但不能使用圆括号版本的初始化。

8. 管理虚方法 override 和 final

class A
{
    int a;
public:
    Action(int i = 0) : a(i) {}
    int val() const {return a;}
    virtual void show(char ch) const { cout << val() << ch << endl;}
};

class B: public A
{
public:    
    B(int i = 0) : A (i) {}
    virtual void show(char * ch) const {cout << val() << ch << "!" << endl; }
};

由于类B定义的是show(char * ch)而不是show(char ch),将对B对象隐藏show(char ch),因此下面代码报错:

B b(10);
b.show('b');//faild

在C++11中,可使用虚说明符override指出您要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。下面的函数重写将生成一条编译错误:

virtual void show(char * ch) const override {cout << val() << ch << "!" << endl; }

如果想禁止派生类覆盖特定的虚方法,为此可在参数列表后面加上final。例如,下面的代码禁止A的派生类重新定义函数show(char ch):

virtual void show(char ch) const final{ cout << val() << ch << endl;}

3.基于范围的for循环

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for(auto& e : array)//e为引用类型,用于修改数组元素
		e *= 2;
	for(auto e : array)
		cout << e << " ";//2,4,6,8,10
	return 0;
}

 注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。for循环迭代的范围必须是确定的。

4.右值引用

1、概念理解

  • 左值:表示可寻址。是保存在内存中,具有确切地址,并能取地址,进行访问,修改等操作的表达式(变量)。
  • 右值:表示可读不可寻址。是保存在内存中,或者寄存器中,不知道也无法获得其确切地址,在得到计算表达式结果后就销毁的临时表达式。包括字面常量、诸如x+y表达式以及函数返回值。
  • 右值引用:表示即将过期但还没过期的值。

右值是能够赋值给左值,但是左值不能赋值给右值。

右值引用主要作用是解决:(1)大对象在作为函数返回值返回时的深度拷贝问题(2)智能指针时将其他unique_ptr通过move()移动后可以赋值给unique_ptr,(3)大对象之间的快速复制

    int &a = 1;//错误,1是右值,a是左值引用,无法直接初始化,需要用左值初始化;
    int a = 1; //正确,a是左值不是左值引用,可以被赋值;
    int const &a = 1;//正确,a是常量左值引用,右值可以绑定到常左值引用
    int &&a = 1;//正确,这是C++11中的右值引用,a为左值;

2、函数的返回值一般是右值,也可能是左值:

  • 当返回值为函数内部定义的局部变量时,函数返回值为右值,因为函数内部该变量已被销毁。
  • 当返回值为指针类型或引用类型的参数时,或者全局变量时,函数返回值为左值
    //返回右值的常规函数
    int fun(){//函数中a是局部变量,会销毁,所以返回值是右值,只能临时使用。
        int a = 1;
        return a;
    }
    //返回左值的函数
    int &fun(int &a){//函数将输入的引用返回,从始至终都是这个a
        a++;
        return a;
    }
    //以上函数可以这么用
    fun(a) = 4;//因为fun返回左值,所以fun可以被赋值
    //错误的返回左值案例
    int &fun(){//不要将局部变量的引用返回,因为返回后局部变量被销毁
        int a = 3;
        return a;
    }

3、移动语义std::move()

  • move作用是可以将一个左值转换成右值引用,从而可以调用C++11的拷贝构造函数。由于在C++11中的移动构造函数的入参是右值引用,因此当传入左值时,无法调用该移动构造函数,需要借助move将左值转换成右值引用。
  • 使用move后,原来的指针unique_ptr指针转让所有权变成空指针,让你能够将一个unique_ptr赋给另一个unique_ptr

4、完美转发std::forward()

完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。
因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性。这样右值引用在传递过程中就能够保持右值的属性。

void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)  // 万能引用
{
    Func(std::forward<T>(t));  // 根据参数t的类型去匹配合适的重载函数
}
int main()
{
    int a = 4;  // 左值
    PerfectForward(a);

    const int b = 8;  // const左值
    PerfectForward(b);

    PerfectForward(10); // 10是右值

    const int c = 13;
    PerfectForward(std::move(c));  // const左值被move后变成const右值

    return 0;
}

运行结果如下:

5.新增容器


用橘色圈起来是C++11中的几个新容器,但是实际最有用的是unordered_mapunordered_set

6.类的新功能

1、类默认生成函数

任何一个类,在什么都不写的时候,都会默认生成6个成员函数
默认构造函数析构函数拷贝构造函数赋值重载函数普通对象取地址const对象取地址

C++11中类新增了2个默认成员函数,使其变为了8个默认成员函数。新增的这两个成员函数就是移动构造函数移动赋值函数

如果我们没有自己实现没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器才自动生成默认移动赋值移动赋值函数。如果我们自己实现了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数复制赋值运算符。

class Person {
public:
    Person(const char* name="", int age = 0){ ... };
    Person (const Person& p) ;//复制构造函数
    Person(Person&& p);//移动构造函数
private:
    char* _name;
    int _age;
}

//复制构造函数
Person::Person(const Person& p)
{
    _name = new char[20];
    strcpy(_name, p._name);
    _age = p._age;
}

//移动构造函数
//移动构造函数/移动赋值运算符的参数不能是const引用,因为这个方法修改了源对象
Person::Person(Person&& p)
{
    _name = p._name;
    p._name = nullptr;//修改了原对象
    _age = p._age;
}

通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数,将初始化分成了两组。使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数。

2、defult  / delete

  • delete关键字除了释放被new开辟的空间外,delete关键字还可以修饰默认生成的成员函数,让其禁止默认生成
  • default关键字,假设要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了在类成员函数的后面提供defult,会让该成员函数强制默认生成
class Person {
public:
    Person(const char* name="", int age = 0){ ... };
    Person (const Person& p) = delete;//禁止生产默认复制构造函数
    Person(Person&& p) =default;//强制默认生成移动构造函数
    Person& operator =(Person&&/p) = default;//强制默认生成移动赋值重载函数
private:
    lb::string _nane;
    int _age;
}
void test()
{
    Person s1;
    Person s2 = s1;//调用拷贝(此处被禁止了)
    Person s3 = std::move (s1);//调用移动构造
    Person s4;
    s4 = std:: move(s2);//调用移动赋值
}

7.Lambda表达式

Lambda 表达式是一个匿名函数,即没有函数名的函数。

1、Lambda表达式的语法规则

[capture-list] (parameters) mutable -> return-type { statement}

  •  [capture-list] : 捕捉列表,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针
  • (parameters):参数列表,如果不需要参数传递,则可以连同()一起省略。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->returntype:返回值类型,声明函数的返回值类型,没有返回值时此部分可省略。也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体,在函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

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

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};
	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };
	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
		cout << a << " " << b << endl;
	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;
	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量

2、为什么引入Lambda表达式?

如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array + sizeof(array) / sizeof(array[0]));
	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), std::greater<int>());
	return 0;
}

但是如果待排序的元素为自定义类型,则需要用户自定义排序时的比较规则

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};
struct ComparePriceLess//按照水果价格降序排序
{
	bool operator()(const Goods& gl, const Goods& gr){
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater//按照水果价格生序排序
{
	bool operator()(const Goods& gl, const Goods& gr){
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,	3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());//降序排序
	sort(v.begin(), v.end(), ComparePriceGreater());//升序排序
}

引入Lambda表达式后,上述排序代码可以写成如下形式:

vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,    3 }, { "菠萝", 1.5, 4 } };
//降序排序
sort(v.begin(), v.end(), [](Goods gl, Goods gr)->bool {return gl._price<gr._price});
//升序排序
sort(v.begin(), v.end(), [](Goods gl, Goods gr)->bool {return gl._price>gr._price});

3、Lambda表达式原理

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象

class Rate
{
public:
	Rate(double rate)
		:_rate(rate)
	{}
	double operator()(double money, int year){
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	// lambda
	auto r2 = [=](double monty, int year)->double {return monty * rate * year;};
	r2(10000, 2);
	return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

8.包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那我们为什么需要包装器呢?先看例子:ret = func(x) ;

上面的代码中func可以是仿函数对象,可以是函数指针,也可以是lambda表达式. . .这些对象都可以调用,在实现模板时就会对一个模板进行多次的实例化,导致了模板的效率底下。

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)//普通函数
{
	return i / 2;
}
struct Functor//仿函数
{
	double operator()(double d)
	{
		return d / 3;
	}
};
void test20()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 仿函数
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}

由代码的执行结果我们可以看到,count有三份,说明我们实现了3个不同的useF函数模板,主要是由于程序中定义的几种模式的函数,都满足函数模板中调用的函数参数与变量参数之间的使用关系,因此在实际运行模板函数时,分别调用几种不同的函数

  但是如果我们使用包装器将三个函数进行包装,对应的模板只会实现一份。包装格式如下:

function<返回类型(参数类型)> 包装器名称=包装函数

#include<iostream>
#include<functional>
using namespace std;

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
int f(int a) {//普通函数
	return a + 0;
}
struct Functor {//仿函数
public:
	int operator() (int a) {
		return a + 1;
	}
};
class Plus//类成员函数
{
public:
	static int plusi(int a) {
		return a + 2;
	}
	double plusd(double a) {
		return a + 3;
	}
};
void test21()
{
	// 函数名(函数指针)
	std::function<int(int)> func1 = f;
	cout << useF(func1, 11) << endl;
	// 函数对象
	std::function<int(int)> func2 = Functor();
	cout << useF(func2, 11) << endl;
	// lamber表达式
	std::function<int(int)> func3 = [](const int a) {return a + 5; };
	cout << useF(func3, 11) << endl;
}

在使用了包装器后,模板参数就不再出现实例化多份的情况了。

9.智能指针

C++11——智能指针_oywLearning的博客-CSDN博客

10.原子操作

C++并发编程 | 原子操作std::atomic_oywLearning的博客-CSDN博客_std::atomic

11.线程库std::thread

C++多线程编程之thread类详解_oywLearning的博客-CSDN博客_c++ thread

12.可变参数模板

可变参数模板 使得编程者能够创建这样的 模板函数 模板类 ,即可 接受可变数量的参数
例如要编写一个函数,它可接受任意数量的参数,参数的类型只需是 cout 能显示的即可,并将
参数显示为用逗号分隔的列表,其函数模板如下:
//为多个参数的情况定义的函数
template<typename T,typename... Arg>
void show_list(T value,Arg... arg)
{
 cout<<value<<",";
 show_list(arg...);//递归调用多个变量的show_list版本,最后调用单个变量的show_list版本
}
//为一个参数的情况定义的
template<typename T>
void show_list(T value)
{
 cout<<value<<endl;
}
//为没有参数的情况定义的
void show_list(){}
要创建可变参数模板,需要理解几个要点:
  • 模板参数包
  • 函数参数包
  • 展开参数包
  • 递归
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值