目录
- 1. 统一的列表初始化
- 1.1 {}初始化
- 1.2 std::initializer_list
- 2. 声明
- 2.1 auto
- 2.2 decltype
- 3. nullptr
- 4. 右值引用和移动语义
- 4.1 左值引用和右值引用
- 4.2 左右值引用比较
- 4.3 右值引用使用场景和意义
- 4.4 完美转发
- 5. 新的类功能
- 6. lambda表达式
- 6.1 语法
- 7. 包装器
- 7.1 bind
1. 统一的列表初始化
1.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
{
Point(int x = int(), int y = int()) :_x(x), _y(y) {
cout << "Point(int x = int(), int y = int()) :_x(x), _y(y)" << endl;
}
int _x, _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int x3 = { 3 };
int arr1[]{ 1, 2, 3, 4, 5 };
int arr2[5]{ 0 }; //全0
int arr3[5]{}; //全0
//以下三种写法等价
//都是调用构造函数
Point p(1, 2);
Point p1{ 1, 2 };
//需要注意的是下面这种写法实际是多参数构造函数的隐式类型转换
//若在构造函数前加上explicit关键字则无法进行转换
Point p2 = { 1,2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
int* pa1 = new int[4]{ };
Point* pp = new Point[2]{ {1,1},{2,2} };
return 0;
}
1.2 std::initializer_list
下面两种写法用的是同一个语法吗?
struct Point
{
int _x;
int _y;
};
Point p1 = { 1, 2 };
vector<int> v = { 1,2 };
不是同一个语法,v的花括号里可以无限添加元素,而p1括号里能放的元素个数则取决于它的构造函数的参数个数。
这里v后面的括号实际上是一个类类型,叫做initializer_list,它是一个类模板:
可以理解为是一个常量数组,括号里面的数据存储在常量区,不管存放多少数据,它所占空间的大小始终是8/16字节,因此不难推断该类中的成员包含两个指针,分别指向这块空间的开始的结束位置。
成员函数:
在C++11后,许多容器都新增了一个参数是initializer_list对象的构造函数。
凡是支持了能用initializer_list对象进行构造的容器,就拥有不定参数的构造能力。
2. 声明
2.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推导。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
//根据等号右边值的类型自动推导
//也就是说&i是什么类型p就是什么类型
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自动推导右边的类型方便书写
auto it = dict.begin();
return 0;
}
但是这要求定义变量的时候必须要进行初始化,若只想根据某个值的类型来定义一个变量并不想初始化时该怎么做呢?这时可以使用decltype。
2.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
template<class T>
class A {
private:
T _a;
};
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');
//也可以作为模板列表的实参
A<decltype(1)> aa;
return 0;
}
decltype(int) x;
这种写法报错,括号里不能是类型
3. nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4. 右值引用和移动语义
4.1 左值引用和右值引用
什么是引用?
引用就是给变量或者对象取别名,一起公用一块空间。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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
去引用,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
4.2 左右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
int main()
{
//左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
return 0;
}
函数形参若是接受变量或者对象引用的话,若不修改它则尽量将其声明为const,因为这样不仅能接收左值引用也能接收右值引用。
右值引用总结:
- 右值引用只能引用右值,无法引用左值
- 但是右值引用可以引用std::move()后的左值
int main() {
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
以下两个函数构成函数重载吗?
void func(const int& r)
{
cout << "void func(const int& r)" << endl;
}
void func(int&& r)
{
cout << "void func(int&& r)" << endl;
}
构成,并且不会引发调用歧义,左右值引用都可以调用第一个,若两个函数同时存在传递右值会调用第二个,也就是编译器会自动根据实参类型自动调用最匹配的那个。
4.3 右值引用使用场景和意义
在讨论它之前需要先讨论左值引用的场景和意义,左值引用主要用在:做函数形参和返回值,而意义是有效减少拷贝,提高效率。
但是下面这种情况,左值引用就不能很好的来解决:
string func() {
string str("hello world!");
//...
return str;
}
int main() {
string ret;
ret = func();
return 0;
}
由于是局部对象,出了函数作用域就会被释放销毁,因此无法使用左值引用,只能传值返回,而传值返回的过程是:首先用str先拷贝构造一个临时对象,再用这个临时对象去赋值拷贝ret。
这个过程产生了两次深拷贝,效率较低,那有没有一种方法可以直接让ret获取(而非拷贝)这个临时对象中的数据呢?这时就可以体现出右值引用的作用了,但右值引用并不是直接起作用而是通过类中新增的两个默认成员函数,移动构造和移动赋值间接体现出它的作用。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
分别与拷贝构造和赋值重载构成函数重载
临时对象是右值,因此会调用移动赋值,而这个临时对象的生命周期只有它所在的那一行,也就是说花费了一次深拷贝的代价结果就只有这一行有效,紧接着就会被销毁,这样就太浪费了,又因为这个临时对象里面的数据就是ret想要的,因此可以直接交换它的成员来达到减少拷贝的目的。
对于不同类型右值的叫法还有些许不同:
内置类型的右值叫做:纯右值
自定义类型的右值叫做:将亡值
上面那种接收返回值的写法并不推荐(如果是单纯想创建对象来接收返回值),因为不会触发编译器的优化,最好是写在一行:
同一行有连续的构造或者拷贝构造时会触发编译器的优化,在str销毁前直接拿str来拷贝构造ret,但此时若是手动实现了移动构造时,会触发编译器的二次优化:
str是左值为什么会匹配到右值引用版本的构造函数呢?由于str是个局部对象,出了它的作用域就会被销毁,符合临时对象的特征,因此编译器会直接把它当成右值-将亡值来处理,所以会调用右值引用版本的构造,在它销毁前把它的成员与ret进行交换,这样没有任何一次深拷贝的发生,大大提高了拷贝的效率。
所以第一种先定义对象,下面接收返回值的写法编译器也是优化了的,先移动构造一个临时对象,再用这个临时对象移动赋值给ret,但这两步都只是交换数据,没有深拷贝的发生。
有了右值引用在传值返回的地方就再也不用考虑拷贝的代价了,这是体现它价值的场景之一。
除此之外,c++11更新后给所有容器都新增了一个右值引用版本的插入接口:
…
该插入接口也可减少拷贝,提高效率。
4.4 完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
当实参是左值,它就会实例化一份左值引用版本的函数,若是右值,则会实例化一份右值版本的函数。
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; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(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;
}
那么在该函数中根据不同类型的参数t去调用函数Fun,会调用到对应匹配的吗?
结果是都去调用了左值引用版本的Fun函数,这是因为不管t是左值引用还是右值引用类型,它们的属性都是左值,也就是说可以取它们的地址,非const类型还可以修改它们。
左值引用的属性是左值可以理解,因为左值本来就可以取地址或者修改,那么被左值引用引用后,自然保持了左值的属性,但是右值是无法取地址和修改的,被右值引用引用后却可以取地址并且修改,其实这是编译器强制把它识别成了左值属性,如果不这么做就无法实现移动构造和移动赋值:
但此时又出现一个问题,不管t是左值还是右值引用,它的属性始终是左值属性,若就想让t保持它原有的属性去调用对应的Fun函数呢?这里就需要用到完美转发:
std::forward()是一个库函数,功能是在传参的过程中保留对象原生类型属性,t是左值引用就保持左值属性,是右值引用就保持右值属性。
有些场景需要让右值引用被识别成左值,比如上面的移动构造和赋值,而有些场景又需要保持它原有的属性继续向下传递,比如:
str虽然是右值引用,但属性是左值,因此会去调用拷贝构造构造cp而非去调用移动构造,这是一次是深拷贝,代价有些高,因此这种场景就需要让str保持它原有的右值属性,进而去调用移动构造减少拷贝的代价,因此合理的做法是完美转发一下即可:
5. 新的类功能
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动赋值重载函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
(默认移动赋值跟上面移动构造完全类似)如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
强制生成默认函数的关键字default:
C++11可以更好的控制要使用的默认函数。假设使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:提供了拷贝构造,就不会生成移动构造了,那么可以使用default关键字显式指定移动构造生成。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
//生成默认移动构造
Person(Person&& p) = default;
private:
lzh::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//强制不生成默认拷贝构造
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
6. lambda表达式
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());
}
若要对自定义类型按照不同的规则实现排序,则需要自己定义不同的仿函数来控制比较逻辑,若规则很多,那么就得根据对应的规则去定义很多仿函数类,尤其是命名相同的类,随着C++语法的发展,人们开始觉得上面的写法太复杂了,因此,在C++11语法中出现了Lambda表达式。
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
//定义lambda表达式对象
//价格降序
auto lessPrice = [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; };
sort(v.begin(), v.end(), lessPrice);
//也可直接传递
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate; });
return 0;
}
它的本质是个函数对象,底层是仿函数实现的。
6.1 语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
- lambda表达式各部分说明:
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数 总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量(对象),通过对象调用。
- 捕获列表说明:
捕捉列表描述了上下文中那些数据可以被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表达式之间不能相互赋值,即使看起来类型相同,底层是不同的类类型。
7. 包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
ret = func(x);
上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
为什么呢?
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;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
useF函数模板实例化了三份,而包装器可以很好的解决上面的问题:
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
//注意:若包装类的非静态成员函数时前面必须加上&
//静态成员函数可加可不加
return 0;
}
通过包装器对象调用时,上面的函数模板只会实例化一份函数,因为包装器统一了可调用对象的参数和返回值类型。
7.1 bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
template <class Fun, class... Args>
/* unspecified */ bind (Fun&& fun, Args&&... args);
// with return type (2)
template <class Ret, class Fun, class... Args>
/* unspecified */ bind (Fun&& fun, Args&&... args);
使用举例:
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<int()> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
///若要绑定成员函数,需要加上&
//同时非静态成员函数因为有this指针的存在
//在绑定时需要带上该类的对象或者对象指针
return 0;
}