浅谈C++11新特性
Posted _Camille
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈C++11新特性相关的知识,希望对你有一定的参考价值。
相较于C++98/03,C++11带来了数量可观的新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。
C++11
前言
C++11能更好的用于系统开发和库开发,语法更加简化,更加稳固和安全,可以很大程度上提升开发效率
一、列表初始化
1.C++98
可以使用花括号 {} 对数组元素进行统一的列表初始值设定,但对于自定义类型却无法使用 {} 进行初始化。
int arr[] = { 1, 2, 3, 4, 5 };//C++98支持
vector<int> iv = { 1, 2, 3, 4, 5 };//C++98不支持无法通过编译
而C++11扩大了用花括号 {} 括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户自定义类型,使用初始化列表时,可添加 = ,也可不添加,究其原因是C++11底层增加了initializer_list类型。
2.C++11内置类型和自定义类型的列表初始化
int x = { 1 };
int x1{ 1 };
int x2{ 1 + 2 };
int arr[]{1, 2, 3, 4, 5};
int* arr1 = new int[5]{1, 2, 3, 4, 5};//动态数组
vector<int> iv{ 1, 2, 3, 4, 5 };
class rec
{
public:
rec(int x = 0, int y = 0) :
_l(x), _w(y)
{}
private:
int _l;
int _w;
};
int main()
{
rec r{ 1, 2 };
return 0;
}
多个对象的列表初始化:
多个对象想要支持列表初始化,需要给该类(模板类)添加一个带有initialler_list类型参数的构造函数即可。
initialler_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()以及size()。
#include <initializer_list>
template <class Type>
class SeqList
{
public:
SeqList(const initializer_list<Type> &list) :
size(0), capacity(list.size())
{
base = new Type[capacity];
for (const auto&e : list)
base[size++] = e;
}
private:
Type *base;
size_t capacity;
size_t size;
};
int main()
{
SeqList<int> sq{ 1, 2, 3, 4, 5 };
return 0;
}
二、变量类型推导
当我们定义变量时,原则上必须先给出变量的实际类型,编译器才允许定义,但有些情况下我们可能不会事先知道实际类型怎么给,或者他的类型比较复杂,在C++11中,我们可以使用auto关键字来根据变量初始化表达式类型推导变量的实际类型。
auto x = 10;
int x1 = 10;
函数参数不可以使用auto来推导
decltype类型推导
如果有时需要根据表达式运行完成后的结果的类型进行推导,因为在编译期间,代码不会运行,所以此时的auto也就无能为力。而decltype是根据表达式的实际类型推演出定义变量时所用的类型。
int a = 10;
int b = 5;
decltype(a + b) c;
cout << typeid(c).name() << endl;
也可以推导函数返回值的类型
int add(int a, int b)
{
return a + b;
}
int main()
{
cout << typeid(decltype(add(1,2))).name() << endl;
return 0;
}
三、范围for循环
vector<int>iv{ 1, 2, 3, 4, 5 };
for (auto & e : iv)
{
cout << e << " ";
}
四、final与override
这两个关键字是帮助用户检测是否进行函数重写。
final:修饰虚函数,表示该虚函数不能再被继承;
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
五、智能指针
这块内容单独写一篇博客总结
六、新增加容器
静态数组array、forward_list、unordered系列
七、默认成员函数控制
在C++中对于一个空的类,编译器会自动生成一些默认成员函数,例如构造,析构,拷贝构造,赋值重载,移动构造,移动拷贝构造等等,如果类中已经显示定义了,那么编译器将不会生成默认函数,在C++11中程序员可以自行控制是否需要编译器生成这些默认函数。
1.显示缺省函数
在C++11中,可以在默认函数定义或者声明时加上=的default,从而显示的只是编译器生成该函数的默认版本,他、用=default修饰的函数称为显示缺省函数。
class A
{
public:
A() = default;//等同于A(){}
A(int a) :_a(a)
{}
private:
int _a;
};
int main()
{
A a1();//调用default修饰的默认构造
A a2(1);
return 0;
}
2.删除默认函数
刚好和default相反,在C++11中,在该默认函数声明上加上=delete即可限制该函数的生成,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
A(const A&) = delete;//禁止生成默认的拷贝构造
注意要避免删除函数和explicit(显示转换)一起使用
八、右值引用
1.what右值引用?
引用可以参考之前的博客引用
使用引用可以提高程序的可读性,为了提高程序运行效率,C++11引入了右值引用,右值暨等号右边的值,右值只能作为等号右边,而不能跑到等号左边。右值引用也是别名,但其只能对右值引用。
int a = 10;//a为左值
int &b = a;//常规引用
const int &c = 10;//常引用
int &&d = 10;//右值引用
int add(int a, int b)
{
int value = a + b;
return value;
}
int main()
{
int && ret = add(10, 20);
return 0;
}
2.左右值的区分
一般可以认为:可以放在等号左边(也能放右侧),或者能够取地址的称为左值,只能放在等号右边,或者不能取地址的称为右值。
区分:
1.普通类型的变量,可以取地址,为左值;
2.const修饰的常量,不可修改,只读类型,理论上应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),所以C++11认为其是左值;
3.如果表达式的运行结果是一个临时变量或者对象,认为是右值;
4.如果表达式运行结果或单个变量是一个引用则认为是左值;
总结:
1.不能简单根据等号两边来判断左右值;
2.能得到引用的表达式一定能够作为引用,否则就用常引用;
C++11对右值进行了严格的区分:
C语言中的纯右值:a+b,100;
将亡值。比如表达式的中间结果,函数按照值的方式返回。
注意:普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值,C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
3.移动语义
解决按值返回对象的缺陷(空间浪费问题)
移动语义暨将一个对象中资源移动到另一个对象中的方式。
举个栗子:
namespace ljl
{
class String{
public:
String(char* str = "")
{
if (str == nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String & s):
_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
String operator+(const String &s)
{
char* tmp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(tmp, _str);
strcpy(tmp + strlen(_str), s._str);
String strret(tmp);
return strret;
}
private:
char *_str;
};
}
请注意上述代码中的重载+的代码
引入C++11中移动构造进行改进:将strret中资源转移到临时对象中。
String(String && s) :_str(s._str)
{
//右值引用的移动语义
s._str = nullptr;
}
因为临时对象也是右值,所以继续调动移动构造,构造s3,节省了相对的2个空间,而且提高了程序运行效率。
注意:移动构造函数的参数千万不能设置为const类型,否则资源不会进行转移;在C++11中,编译器会默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显示定义自己的移动构造。
4.右值引用引用左值
在有些场景下,可能需要用右值去引用左值实现移动语义,当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。在C++11中,std::move()函数位于头文件中,他并不搬移任何东西,只有一个功能就是将一个左值强制转换为右值引用,然后实现移动语义。
误用实例:
string s1("hello");
string s2(move(s1));
string s3(s2);
move将s1转化为右值后,在实现s2的拷贝时就会调用移动构造,此时s1的资源就会被转移到s2中,s1称为无效字符串。
正确使用实例:
class Person
{
public:
Person(char * name, char* sex, int age)
:_name(name), _sex(sex), _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _sex(p._sex)
, _age(p._age)
{}
/*
Person(Person&& p)
:_name(p._name)
,_sex(p._sex)
,_age(p._age)
{}
*/
Person(Person && p)
:_name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
private:
String _name;
String _sex;
int _age;
};
Person GetP()
{
Person p("ljl", "male", 18);
return p;
}
}
int main()
{
ljl::Person p(ljl::GetP());
return 0;
}
分析:
5.完美转发
完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另外一个函数。
完美就是,函数模板在向其他函数传递自身形参时,如果相应实参是左值,他就应该被转发为左值,如果相应实参是右值,它就应该被转发为右值。
其目的就是保留在其他函数针对转发而来的参数的左右值属性进行不同处理(例如参数为左值是拷贝语义,而当参数为右值时进行移动语义)
C++11通过forward函数来实现完美转发。
6.右值引用的应用
1.实现移动语义;
2.给中间临时变量取别名;
3.实现完美转发。
九、lambda表达式
1.C++98
在C++98中如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法(需要引用算法头文件#include< algorithm>)。
int main()
{
int arr[] = { 4, 3, 5, 2, 1 };
int n = sizeof(arr) / sizeof(arr[0]);
for (auto& e : arr)
cout << e << " ";
cout << endl;
sort(arr, arr + n);//默认升序
for (auto& e : arr)
cout << e << " ";
cout << endl;
sort(arr, arr + n, greater<int>());//利用仿函数进行降序排序
for (auto& e : arr)
cout << e << " ";
cout << endl;
return 0;
}
但是当待排序元素为自定义类型时,需要用户定义排序时的比较规则:
struct Goods
{
string _name;
double _price;
};
struct Compare1
{
bool operator()(const struct Goods &g1, const struct Goods &g2)
{
return g1._price < g2._price;
}
};
int main()
{
Goods gds[] =
{
{ "苹果", 2.1 },
{ "香蕉", 3 },
{ "橙子", 2.5 },
};
int n = sizeof(gds) / sizeof(gds[0]);
sort(gds,gds+n,Compare1());
return 0;
}
由上我们可以看到,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
2.lambda表达式
说白了lambda表达式省略了我们之前自定义的仿函数类。
sort(gds, gds + n, [](const struct Goods &g1,
const struct Goods &g2)->bool
{return g1._price < g2._price; });
上面的代码和C++98的效果一样。
表达式语法:
匿名的函数对象
int main()
{
auto fun = [](int a, int b)->int {return a + b; };
cout << fun(10, 20) << endl;
cout << typeid(fun).name()<<endl;
return 0;
}
注:返回值类型在明确的情况下,可以省略由编译器推导,在無返回值时刻直接省略。如果不需要传递参数,则可以将()直接省略。因此最简单的lambda表达式为[]{}。
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
捕获列表说明:
捕获列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。
[var]:表示值传递方式捕捉变量var;
int main()
{
int x = 10;
int y = 20;
auto fun = [x, y]//需要捕获这两个变量,除非变量是全局的
{
return x + y;
};
cout << fun(x,y) << endl;
return 0;
}
[&var]表示引用传递捕捉变量var;
[&]:表示引用传递捕捉所有父作用域中的变量(包括this);
[this]:表示值传递方式捕捉当前的this指针;
class A
{
public:
int fun(int x, int y)
{
auto f = [this](int x, int y)->int
{
return this->a + this->b + x + y;
};
return f(x, y);
}
private:
int a = 1;
int b = 2;
};
int main()
{
A a;
cout << a.fun(10, 20) << endl;
return 0;
}
注:
父作用域包含lambda函数的语句块;
语法上捕捉列表可有多个捕捉项组成,并以逗号分隔;
捕捉列表不允许变量重复传递,否则就编译错误;
在块作用域以外的lambda函数捕捉列表必须为空;
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会出错;
lambda表达式之间不能相互赋值。
十、线程库
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差,C++11中最重要的特性就是对线程进行支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入原子类的概念,要是用标准库中的线程,必须包含 < thread > 头文件。
thread th;//创建一个没有关联任何线程函数的线程
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,暨么有启动任何线程; |
thread(fun,args1,agrs2…) | 构造一个线程对象,并关联线程函数fun,args1和args2为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程 |
join() | 该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程“死活”就与主线程无关 |
解释:
1.当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
①.函数指针;
②.lambda表达式;
③.函数对象。
void thread_fun(C++11特性之std:call_once介绍