C++11 新特性
自动类型推导 auto
基本语法
在C++11之前,auto关键字用来指定存储期。在新标准中,它的功 能变为类型推断。
auto现在成了一个类型的占位符,通知编译器去根据初始化代码推 断所声明变量的真实类型。
各种作用域内声明变量都可以用到它。例如,名空间中,程序块中,或是for循环的初始化语句中。
在没有auto以前,遍历一个容器需要这样来书写一个迭代器:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> vc = { 1,2,3,4,5,6 }; for (vector<int>::iterator it = vc.begin(); it != vc.end(); it++) { cout << *it << " "; } return 0; }
有了auto之后,可以写出如下代码:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> vc = { 1,2,3,4,5,6 }; for (auto it= vc.begin(); it != vc.end(); it++) { cout << *it << " "; } return 0; }
auto 与 const
先看一段代码:
int x = 0; const auto n = x; auto f = n; const auto& r1 = x; auto& r2 = r1;
1.第 2 行代码中,n 为 const int,auto 被推导为 int。
2.第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。
3.第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
4.第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。
总结:
- 1.当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
- 2.当类型为引用时,auto 的推导结果将保留表达式的 const 属性。
auto 的高级用法
auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型:
int x = 0; auto *pt1 = &x; //pt1 为 int *,auto 推导为 int auto pt2 = &x; //pt2 为 int*,auto 推导为 int* auto &r1 = x; //r1 为 int&,auto 推导为 int auto r2 = r1; //r2 为 int,auto 推导为 int
auto 的限制
- 1.auto 不能在函数的参数中使用
auto funcName(int v1,int v2) { return v1 + v2; }
- 注意:C++11的时候不能,C++14开始可以让普通函数具备返回值推导
- 2.auto 不能作用于类的非静态成员变量**(就是没有static关键字修饰的成员变量)**
class Student { protected: auto name; //错误 int age; //正确 };
3.auto 不能作用于模板参数
template <typename T> class Student{ //dosomething: }; int main(){ Student<int> s1; Student<auto> s2 = s1; //错误 return 0; }
- 4.auto 不能用于推导数组类型
自动类型推导 decltype
decltype,在C++中,作为操作符,用于查询表达式的数据类型。
decltype在C++11标准制定时引入,主要是为泛型编程而设计,以解决泛型编程中,由于有些类型由模板参数决定,而难以(甚至不可能)表示的问题。
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。有时候,我们可能需要计算某个表达式的类型。
lambda表达式如果我们想要使用它的类型我们就需要使用decltype。
auto num1 = 100; auto num2 = 200; decltype(num1 + num2) num3;
num3的类型就是num1 + num2 最终的结果的类型。
在泛型编程中,可能需要通过参数的运算来得到返回值的类型:
#include <iostream> using namespace std; template <typename R, typename T, typename U> R add(T t, U u) { return t + u; } int main() { int a = 1; float b = 2.0; auto c = add<decltype(a + b)>(a, b); cout << c << endl; return 0; }
下面的代码正常吗?
template <typename T, typename U> decltype(t + u) add(T t, U u) { return t + u; }
C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在,所以报错!
正确写法:
template <typename T, typename U> auto add(T t, U u)->decltype(t+u) //尾随返回类型 { return t + u; }
C++14开始可以直接写成:
template <typename T, typename U> auto add(T t, U u) { return t + u; }
右值引用
基本概念
C++98 中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以 提高程序的可读性。
比如交换两个变量的值,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
void change(int& n1, int& n2) { int temp = n1; n1 = n2; n2 = temp; }
为了提高程序运行效率,C++11 中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int func1(int v1, int v2){ return v1 + v2; } int main() { const int&& num1 = 10;//右值示例 //引用函数返回值,返回值是一个临时变量,为右值 int&& num2 = func1(10, 20); cout << num1 << endl; cout << num2 << endl; return 0; }
右值与左值
- 1.一般认为:左值可放在赋值符号的左边,右值可以放在赋值符号 的右边;或者能够取地址的称为左值,不能取地址的称为右值
- 2.左值也能放在赋值符号的右边,右值只能放在赋值符号的右边
int num = 100; //函数的返回值结果为引用 int& returnNum(){ return num; } int main() { int num2 = 10; int num3 = num2; int* p = new int(0); //num2和num3,p和*p都是左值,说明:左值既可放在=的左侧,也可放在=的右侧 const int num4 = 30; //num4 = num1; //特例:num4 虽然是左值,但是为const常量,只读不允许被修改 cout << &num4 << endl; //num4可以取地址,所以num4严格来看也是左值 //num3 + 2 = 200; //编译失败:因为num3+2的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值 returnNum() = 111; return 0; }
移动语义
转移语义可以将资源(堆、系统对象等)从一个对象转移到另一个对象,这样可以减少不必要的临时对象的创建、拷贝及销毁。移动语义与拷贝语义是相对的,可以类比文件的剪切和拷贝。在现有的 C++ 机制中,自定义的类要实现转移语义,需要定义移动构造函数,还可以定义转移赋值操作符。
以 string 类的移动构造函数为例:
MyString(MyString&& str) { cout << "move ctor source from " << str.data << endl; len = str.len; data = str.data; str.len = 0; str.data = NULL; }
和拷贝构造函数类似,有几点需要注意:
- 参数(右值)的符号必须是 &&
- 参数(右值)不可以是常量,因为我们需要修改右值
- 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
标准库函数 std::move 可以将左值变成一个右值。
编译器只对右值引用才能调用移动构造函数,那么如果已知一个命名对象不再被使用,此时仍然想调用它的移动构造函数,也就是把一个左值引用当做右值引用来使用,该怎么做呢?
用 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
列表初始化
在 C++98 中,标准允许使用花括号 {} 对数组元素进行统一的列表初始值设定。比如:
int arr1[] = {1,2,3,4,5}; int arr2[100] = {0};
但对于一些自定义类型却不行,例如:
vector<int> vc{1,2,3,4,5};
在 c++98 中是无法编译成功的,只能够定义 vector 对应之后通过循环进行插入元素达到这个目的。
C++11 扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include<iostream> #include<vector> #include<map> using namespace std; class ClassNum { public: ClassNum(int n1 = 0, int n2 = 0) : _x(n1), _y(n2) {} private: int _x; int _y; }; int main() { int num1 = { 100 };//定于内置类型 int num2{ 3 };//也可以不加= //数组 int arr1[5] = { 1,3,4,5,6 }; int arr2[] = { 4,5,6,7,8 }; //STL中的容器 vector<int>v{ 12,2 }; map<int, int>mp{ {1,2},{3,4} }; //自定义类型初始化 ClassNum p{ 1, 2 }; return 0; }
For each
C++11 引入了基于范围的迭代写法,拥有了能够写出像 Python 类似的简洁的循环语句。就拿最常用的 vector 遍历举例:
int main() { vector<int> v1={ 1,2,3,4,5,6 }; for(auto i : v1)//范围for { cout << i << “ ”; } return 0; }
如果用 for each 则需要传入一个处理函数,下面案例用 lambda 实现该函数:
输入案例:
abcstringabc a
输出案例:
2
#include <iostream> #include <algorithm> using namespace std; int getCharNum(string s, char c) { int count = 0; for_each(s.begin(), s.end(), [&](char ch) { if (c == ch) { count++; } }); return count; } int main() { string s1; char ch; cin >> s1; cin >> ch; cout << getCharNum(s1, ch) << endl; return 0; }
Lambda表达式
lambda 表达式实际上是一个匿名类函数,在编译时会将表达式转换为匿名类函数。
语法
[capture-list] (parameters) mutable -> return-type { statement} [捕获列表](参数)->返回值{ 函数体 };
1.[capture-list]: 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。(不能省略)
2.(parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同 () 一起省略。(没有参数可以省略)
3.mutable: 默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空),mutable 放在参数列表和返回值之间。(可以省略)
4.->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。(有没有返回值都可以省略)
5.{statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的 变量。(不能省略)
捕获列表说明
捕捉列表描述了上下文中那些数据可以被 lambda 使用,以及使用的方式传值还是传引用。
1.[a,&b]: 其中 a 以复制捕获而 b 以引用捕获。
2.[this]: 以引用捕获当前对象(*this)。
3.[&]: 若存在,以引用捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象。
4.[=]: 若存在,以复制捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象。
5.[]: 不捕获,大部分情况下不捕获就可以了。
在 lambda 函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
C++11 中最简单的 lambda 函数为:[]{}; 该lambda函数不能做任何事情,没有意义!
#include<iostream> using namespace std; void(*FP)(); //函数指针 int main() { //最简单的lambda表达式, 该lambda表达式没有任何意义 [] {}; //省略参数列表和返回值类型,返回值类型由编译器推导为int int num1 = 3, num2 = 4; //省略了返回值类型,无返回值类型 auto fun1 = [&num1, &num2](int num3) {num2 = num1 + num3; }; fun1(100); cout << num1 << " " << num2 << endl; //捕捉列表可以是lambda表达式 auto fun = [fun1] {cout << "great" << endl; }; fun(); //各部分都很完善的lambda函数 auto fun2 = [=, &num2](int num3)->int {return num2 += num1 + num3; }; 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; //编译失败--->提示找不到operator=() //auto fun3 = [&num1,&num2](int num3) {num2 = num1 + num3;}; //fun1 = fun3; //允许使用一个lambda表达式拷贝构造一个新的副本 auto fun3(fun); fun(); //可以将lambda表达式赋值给相同类型的函数指针 auto f2 = [] {}; FP = f2; FP(); return 0; }
函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了 operator() 运算符的类对象。
从使用方式上来看,函数对象与 lambda 表达式完全一样。
#include <iostream> using namespace std; 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.6; Rate r1(rate); double rd = r1(20000, 2); cout << rd << endl; //lambda auto r2 = [=](double money, int year)->double {return money * rate * year;}; double rd2 = r2(20000, 2); cout << rd2 << endl; return 0; }
在 C++98 中,对一个数据集合中的元素进行排序,可以使用 sort 方法。
#include <iostream> #include <algorithm> #include <functional> using namespace std; int main() { int array[] = { 3,6,9,5,4,7,0,8,2,1 }; //默认按照小于比较,排出来结果是升序 sort(array, array + sizeof(array) / sizeof(array[0])); //如果需要降序,需要改变元素的比较规则 sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>()); return 0; }
如果待排序元素为自定义类型,需要用户定义排序时的比较规则。
#include <iostream> #include <algorithm> #include <functional> using namespace std; 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[] = { { "苹果", 5.1 }, { "橙子", 9.2 }, { "香蕉", 3.6 }, {"菠萝",9.6} }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare()); for (int i = 0; i < 4; i++) { cout << gds[i].name << " " << gds[i].price << endl; } return 0; }
有了 lambda 表示,代码就可以写成如下:
#include <iostream> #include <algorithm> #include <functional> using namespace std; struct Goods{ string name; double price; }; int main() { Goods gds[] = { { "苹果", 5.1 }, { "橙子", 9.2 }, { "香蕉", 3.6 }, {"菠萝",9.6} }; sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)->bool { return l.price < r.price; }); for (int i = 0; i < 4; i++) { cout << gds[i].name << " " << gds[i].price << endl; } return 0; }