文章目录
- 😍C++11优势
- 😎 列表初始化
- 😁变量类型推导
- 👌为什么需要类型推导
- 👍decltype类型推导(了解)
- 😜final 与 override
- final
- 🤞override
- ❤️默认成员函数控制
- 🤩显示缺省函数
- 😐删除默认函数(禁止调用)
- 😍右值引用与移动语义
- 😯什么是左值?什么是左值引用
- 😹什么是右值?什么是右值引用
- 😜左值引用与右值引用比较
- 🎈右值引用使用场景和意义
- 🧨完美转发
- 🍕新的类功能
- 🚕lambda表达式
- 💕lambda表达式语法
- 😃可变参数列表(先学会基本特性)
- 🥱包装器
😍C++11优势
相比C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
😎 列表初始化
在C++11中可以直接这样初始化:
内置类型的列表初始化:
int x1 = {10};
int x2{10};//建议使用原来的
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};//这种初始化就很友好,不用push_back一个一个插入
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
自定义类型的列表初始化:
class Point
{
public:
Point(int x = 0, int y = 0): _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Pointer p = { 1, 2 };
Pointer p{ 1, 2 };//不建议
return 0;
}
多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。
注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "66666666666" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//C++11容器都实现了带有initializer_list类型参数的构造函数
vector<Date> vd = { { 2022, 1, 17 }, Date{ 2022, 1, 17 }, { 2022, 1, 17 } };
return 0;
}
😁变量类型推导
👌为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,
但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂
int main()
{
short a = 32670;
short b = 32670;
// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
short c = a + b;
std::map<std::string, std::string> m{ {"apple", "苹果"}, {"banana","香蕉"} };
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
// 使用迭代器遍历容器, 迭代器类型太繁琐 可以使用auto
//std::map<std::string, std::string>::iterator it = m.begin();
auto it = m.begin();
👍decltype类型推导(了解)
为什么需要decltype
**auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。**但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如
1、推演表达式类型作为变量的定义类型
int a = 10, b = 20;
decltype(a + b)c;
cout << typeid(c).name() << endl;
- 推演函数返回值的类型
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
cout << typeid(Add(1, 2)).name() << endl;
return 0;
}
😜final 与 override
final
final修饰类的时候,表示该类不能被继承
class A final //表示该类是最后一个类
{
private:
int _year;
};
class B : public A //无法继承
{
};
final修饰虚函数时,这个虚函数不能被重写
class A
{
public:
virtual void fun() final//修饰虚函数
{
cout << "this is A" << endl;
}
private:
int _year;
};
class B : public A
{
public:
virtual void fun()//父类虚函数用final修饰,表示最后一个虚函数,无法重写
{
cout << "this is B" << endl;
}
};
🤞override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class A
{
public:
virtual void fun()
{
cout << "this is A" << endl;
}
private:
int _year;
};
class B : public A
{
public:
virtual void fun() override
{
cout << "this is B" << endl;
}
};
❤️默认成员函数控制
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
🤩显示缺省函数
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指定编译器生成该函数的默认版本(默认成员函数),用=default修饰的函数称为显式缺省函数。
class A
{
public:
A() = default;//让编译器默认生成无参构造函数
A(int year) //这样不写缺省值的时候,就不需要自己在去实现一个默认的无参构造函数
:_year(year)
{}
void fun()
{
cout << "this is A" << endl;
}
private:
int _year;
};
😐删除默认函数(禁止调用)
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class A
{
public:
A() = default;
A(int a) : _a(a)
{}
//C++11
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator=(const A&) = delete;
private:
int _a;
//C++98,设置成private就可以了
A(const A&) = delete;
A& operator=(const A&) = delete;
};
😍右值引用与移动语义
传统的C++就有引用,称为左值引用,C++11后,出了右值引用。无论是左值引用还是右值引用,都是给对象取别名(与对象共享一片空间)
😯什么是左值?什么是左值引用
左值是一个表示数据的表达式(如变量名和解引用的指针),**我们可以获取它的地址,也可以对它赋值,左值可以出现在赋值符号的左边,右值不可以出现在左边。**左引用加const修饰,不能对其赋值,但可取地址,是一种特殊情况。左值引用就是给左值取别名。
//以下都是左值
int* p = new int[10];
int a = 10;
const int b = 20;
//对左值的引用
int*& pp = p;
int& pa = a;
const int& rb = b;
左值:
1、可以取地址
2、一般情况下可以修改(const修饰时不能修改)
😹什么是右值?什么是右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值、传值返回函数的返回值(不能是左值引用返回)等,右值可以出现在赋值符号的右边,但是不能出现在左边。右值引用就是给右值取别名。
double x = 1.1, y = 2.2;
//常见右值
10;
x + y;
add(1, 2);
//右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double && rr3 = add(1, 2);
//右值引用一般情况不能引用左值,可使用move将一个左值强制转化为右值引用
int &&rr4 = move(x);
//右值不能出现在左边,错误
10 = 1;
x + y = 1.0;
add(1, 2) = 1;
move:当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
😜左值引用与右值引用比较
左值引用总结:
1 左值引用只能引用左值,不能引用右值
2.const左值引用既可引用左值,也可引用右值
// 左值引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
//const左值引用既可以引用左值,也可以引用右值
const int& ra3 = 10;
const int& ra4 = a;
右值引用总结:
1.右值引用只能引用右值,一般情况下不能引用左值
2.右值引用可以引用move以后的左值
int a = 10;
int b = 20;
//不能引用左值
//int&& rr1 = a;
int&& rr2 = 10;
int&& rr3 = move(a);//强制转换为右值引用
🎈右值引用使用场景和意义
左值引用既可以引用左值,可以引用右值,为什么C++11还要提出右值引用?因为左值引用存在短板,下面我们来看看这个短板以及右值引用是如何弥补这个短板的!
🧨完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
void Func(int x)
{
cout << x << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
Func(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
我们先来了解万能引用
1、模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
2、模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
3、但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
4、我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用我们下面学习的完美转发
C++11通过forward函数来实现完美转发
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(t);//没有使用forward保持其右值的属性,退化为左值
Func(forward<T>(t));
}
int main()
{
PerfectForward(1);//右值
int a = 10;
PerfectForward(a);
PerfectForward(move(a));
const int b = 20;
PerfectForward(b);
PerfectForward(move(b));
return 0;
}
右值引用的对象,再作为实参传递时,属性会退化为左值,只能匹配左值引用。使用完美转发,可以保持他的右值属性
🍕新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
1.构造函数
2.析构函数
3.拷贝构造函数
4.拷贝赋值重载
5.取地址重载
6.const取地址重载
重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11新增了两个:移动构造函数和移动赋值逸算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,目没有实现析构函数。接贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
C++对于自定义类型成员变量非常的友好,默认成员函数都会恰当处理自定义类型成员
🚕lambda表达式
之前我们要比较自定义类型的一个大小,需要自己实现一个类,并写上仿函数,这样有点复杂。
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
return 0;
}
之前自己写的过于复杂,随着lambda的推出,写这种比较大小排序就比较简单了。
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)
->bool
{
return l._price < r._price;
});
return 0;
}
上面的写法,相当于是把函数直接写到sort的第三个位置上,接下来我们来看一下lambda的语法。
💕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函数不能做任何事情。
int main()
{
[] {};//最简单的lambda表达式上面也不做
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=] {return a + 3; };
// 省略了返回值类型,无返回值类型
//引用传递捕捉a 和 b变量
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl;
// 各部分都很完善的lambda函数
//引用方式捕捉b,值传递捕捉其他所有变量
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;
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
2.捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl;
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
int x = 10;
auto add_x = [x, =](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
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);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
😃可变参数列表(先学会基本特性)
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
//可变参数,你传int,char,还是自定义都会自动给你推导
可以包含0-任意个参数
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;//计算个数
}
int main()
{
ShowList(1, 2, 3);
ShowList(1, 'a');
ShowList(1, 'A', string("sort"));
return 0;
}
如果我们要对其取值,如何取?
//需要加上结尾函数
/像套娃
void ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);//不断调用自己,直到最后参数为空,调用上面的结尾函数
}
接下来我们在看看可变参数在列表初始化的应用
template<class ...Args>
void ShowList(Args... args)
{
int arr[ ] = { args... };//可变参数初始化列表
cout << endl;
}
我们这里列表初始化内部都是一样的数据,如果我们要传不一样的数据,该如何实现?
C++11,利用逗号表达式调用例外一个函数,最后的0留给数据。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
// 列表初始化
// {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 2, 3);
ShowList(1, 'a');
ShowList(1, 'A', string("sort"));
return 0;
}
也可以给模板函数设置一个返回值
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
// 列表初始化
//也可以给模板函数设置一个返回值
int arr[] = { PrintArg(args)... };
cout << endl;
}
可变参数包结合完美转发的好处:
直接就是普通构造函数的形式,不存在移动构造或者拷贝构造,节省空间
🥱包装器
函数包装器器其实就是函数指针,用了包装器之后,函数模板只会实例化一次,这里我们了解其用法即可。
可调用对象的类型:函数指针、仿函数(函数对象)、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 func(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(func, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl;
return 0;
}
这里我们可以看到静态变量count,每次的地址都不一样,说明函数模板实例化了3次。
我们可以通过包装器只让函数模板实例化一次
int main()
{
// 函数名 生成一个函数包装器,f1就是函数指针 == double (*f1)(double)
std::function<double(double)> f1 = func;
cout << useF(f1, 11.11) << endl;
// 函数对象
std::function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
// lamber表达式
std::function<double(double)> f3 = [](double d)->double{ return d / 4; };
cout << useF(f3, 11.11) << endl;
return 0;
}
可以看到count的值是累加的,说明函数模板只实例化了一次
std::function包装各种可调用的对象,统一可调用对象类型,并且指定了参数和返回值类型。
为什么有std:function,因为不包装前可调用类型存在很多问题:
1、函数指针类型太复杂,不方便使用和理解
2、仿函数类型是一个类名,没有指定调用参数和返回值。得去看operator()的实现才能看出来。3、lambda表达式在语法层,看不到类型。底层有类型,基本都是lambda_uuid,也很难看