![](https://img-blog.csdnimg.cn/4d3c5592f30f46698bb3adf42976e896.gif)
目录
1、 C++11简介
2、 统一的列表初始化
2.1 {}初始化
2.2 std::initializer_list
3、声明
3.1 auto和范围for
3.1decltype
3.3 nullptr
4、新容器
5、 右值引用
5.1左值引用和右值引用
5.2 左值引用与右值引用比较
5.3 左值和右值引用使用场景及意义
6、完美转发
7、新的类功能
7.1默认成员函数
7.2强制生成默认函数的关键字default:
7.2禁止生成默认函数的关键字delete:
C++11特性我分为【上】【下】两部分来写,本文介绍【上】
1、 C++11简介
从 C++0x 到 C++11 , C++ 标准 10 年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03 , C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中 约 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语言。相比较而言, C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习 。 C++11增加的语法特性篇幅非常多,这里没办法一一讲解,所以本文主要讲解实际中比较实用的语法。小故事:1998 年是 C++ 标准委员会成立的第一年,本来计划以后每 5 年视实际需要更新一次标准,C ++国际标准委员会在研究 C++ 03 的下一个版本的时候,一开始计划是 2007年发布,所以最初这个标准叫C++ 07 。但是到 06 年的时候,官方觉得 2007 年肯定完不成 C++ 07 ,而且官方觉得 2008年可能也完不成。最后干脆叫 C++ 0x 。 x 的意思是不知道到底能在 07 还是 08 还是 09 年完成。结果 2010年的时候也没完成,最后在 2011 年终于完成了 C++ 标准。所以最终定名为 C++11 。
2、 统一的列表初始化
2.1 {}初始化
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;
}
class Point
{
public:Point(int x = 0, int y = 0) : _x(x), _y(y){}private:int _x;int _y;
};int main()
{int x = 1;int y{ 2 };//c++11,没必要学,没意义//C++11才支持的花括号列表初始化(容器都可以用{}初始化,数组也适用)int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };vector<int> v1{ 1,2,3,4,5 };vector<int> v2 = { 1,2,3,4,5 };list<int> l1{ 1,2,3,4,5 };list<int> l2 = { 1,2,3,4,5 };map<string, int> m{ {"苹果", 1},{"西瓜",3},{"香蕉",2}};//map的每个对象都是pairmap<string, int> m1 = { {"苹果", 1},{"西瓜",3},{"香蕉",2} };//对于类的构造函数的初始化Point p1(1, 2); //正常情况下支持的Point p2{ 1, 2 };Point p3 = { 1, 2 };//c++11中的列表初始化也可用于new表达式int* pa = new int[4] {0};return 0;
}
2.2 std::initializer_list
①、类型(其为一个类模板)
template<class T> class initializer_list;
②、使用场景
//initializer_list的使用(两种写法)
auto i1 = { 10, 20, 30 }; //the type of i1 is an initializer_list
initializer_list<int> i2 = { 1, 2, 3 };//容器是如何支持这种花括号的列表初始化的呢?
//使模拟实现的vector也支持{}初始化和赋值
/*vector(initializer_list<T>l):_capacity(l.size()), _size(0)
{ //为了讲解,这里假设vector没用指针实现,而是用_capacity,_size实现_array = new T[_capacity];for (auto e : l)_array[_size++] = e;
}*/
//其他容器也类似
注:容器支持花括号列表初始化,本质是增加了一个initializer_list的构造函数,initializer_list可以接收{}列表
3、声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
3.1 auto和范围for
//auto和范围for(熟悉)->简化了代码的写法//auto不能做形参和返回值
//auto func(auto e)
//{}
int main()
{std::map<std::string, std::string> dict = { {"leverage","影响力"}, {"acre", "英亩"} };std::map<std::string, std::string>::iterator it1 = dict.begin();auto it2 = dict.begin();//用auto明显更方便书写了//注:这里当容器存的对象比较大时或这个对象要做深拷贝,如string//最好给引用和const,可以减少拷贝,提高效率for (const auto& e : dict) {//容器支持范围for原理:范围for会被编译器替换成迭代器,则支持迭代器就支持范围forcout << e.first << e.second << endl;//acre英亩\nleverage影响力}//注:auto生成的迭代器是可以当参数进行传参的//因为auto生成对象跟只用类型对象是一样的,即it1与it2是一样的,没有区别 //唯一区别:it2类型是编译器自动推导出来的//auto的优势就是可以把类型比较复杂的地方,简化代码的写法//除了STL的容器可以用范围for用法,数组也可以(原生指针也可以认为是天然迭代器,如vector//string等迭代器就是原生指针int a[] = { 1,2,3,4,5,6 };for (auto e : a){cout << e << " ";}cout << endl;
}
3.1decltype
关键字 decltype 将变量的类型声明为表达式指定的类型。
//类型推导,属于RTTI (run time type identification)【了解】
//程序运行时对象的类型识别
int main()
{int a = 10;int b = 20;double c = 10;auto d = a + b;auto e = a + c;//拿到类型名称的字符串cout << typeid(d).name() << endl; //intcout << typeid(e).name() << endl; //doublestring s;cout << typeid(s).name() << endl; //class std::basic_string<char,....>//若想定义一个跟d一样类型的对象//typeid(d).name() f; //报错,故用decltype//通过对象去推类型decltype(e) g;decltype(e) h;cout << typeid(g).name() << endl; //doublecout << typeid(h).name() << endl; //doublereturn 0;
}
3.3 nullptr
4、新容器
容器中的一些新方法如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。 比如提供了 cbegin 和 cend 方法返回 const 迭代器等等,但是实际意义不大,因为 begin 和 end也是可以返回 const 迭代器的,这些都是属于锦上添花的操作。实际上 C++11 更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本。但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?请看下面的右值引用和移动语义的讲解。另外 emplace还涉及模板的可变参数,也需要再继续深入学习后面的知识。
5、 右值引用
5.1左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以我们之前学习的引用就叫左值引用。无论左值引用还是右值引用,都是给对象取别名。不过左值引用主要给左值取别名,右值引用主要给右值取别名
// 以下的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;
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
// 以下几个都是常见的右值
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;
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x+y;
rr1 = 20;
rr2 = 5.5; // 报错
5.2 左值引用与右值引用比较
左值引用总结:
//左值引用不能直接引用右值,const左值引用既可以引用左值,也可引用右值
//int& e = 10;
//int& f = x + y;
const int& e = 10;
const int& f = x + y;
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。 当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值 。 C++11 中, std::move() 函数位于 头文件中,该函数名字具有迷惑性,它 并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义 。
//右值引用只能引用右值,不能引用左值,但可以引用move后的左值
//error C2440: “初始化”: 无法从“int”转换为“int &&”
//message : 无法将左值绑定到右值引用
//int&& mm = a;
int&& mm = move(a);
5.3 左值和右值引用使用场景及意义
void func1(string s)
{}
void func2(const string& s)
{}
int main()
{string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}
namespace mz
{string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}
int main()
{// 在mz::string to_string(int value)函数中可以看到,这里只能使用传值返回,// 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。string ret1 = mz::to_string(1234);string ret2 = mz::to_string(-1234);cout << ret1 << endl; //1234cout << ret2 << endl; //-1234return 0;
}
右值引用的使用场景:
场景1(简单应用)
template<class T>
void f(T& a)
{cout << "void f(const T& a)" << endl;
}template<class T>
void f(T&& a)
{cout << "void f(const T&& a)" << endl;
}int main()
{int x = 10;//f的左值右值引用的参数不同,故构成函数重载f(x); //void f(const T& a) ->匹配左值引用f(10); //void f(const T&& a) ->匹配右值引用return 0;
}
场景2
C++11又将右值区分为:纯右值和将亡值
纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象
以我们模拟实现的String为例,看看右值引用的应用场景
class String
{
public:String(const char* str = " "){_str = new char[strlen(str) + 1];strcpy(_str, str);}//s2(s1)String(const String& s){cout <<"String(const String& s)-拷贝构造-效率低" << endl;_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}//s3(右值-将亡值)String(String&& s):_str(nullptr){//传进来的是个将亡值,反正你都要亡了,我的目的是跟你有一样大的空间//不如把你的空间给我cout << "String(String&& s)-移动拷贝构造-效率高" << endl;swap(_str, s._str);}//s3 = s4String& operator=(const String& s){cout << "String& operator=(const String& s)-拷贝赋值-效率低" << endl;if (this != &s){char* newstr = new char[strlen(s._str) + 1];strcpy(newstr, s._str);delete[] _str;_str = nullptr;}return *this;}//s3 = 右值-将亡值String& operator=(String&& s){cout << "String& operator=(const String&& s)-移动赋值-效率高" << endl;swap(_str, s._str); //直接掠夺s的资源即可,反正你s都要亡了return *this;}//第二个层次的问题//s1 + s2String operator+(const String& s2){String ret(*this);//ret.append(s2._str); //因append我们没有模拟实现,故这里注释掉return ret; //返回的是右值}//s1 += s2String& operator+=(const String& s2){//this->append(s2);return *this; //返回的是左值}~String(){delete[]_str;}
private:char* _str;
};String f(const char* str)
{String tmp(str);return tmp; // 返回tmp拷贝的临时对象
}int main()
{String s1("左值");String s2(s1); //参数是左值//String s3(String("右值")); //编译器会优化掉String s3(f("右值-将亡值")); //参数是右值-将亡值(传给你用,用完我就析构了),但编译器也会优化掉String s4(move(s1)); String s5("zuo值");s5 = s1;String s6("s6");String s7("s7");String s8 = s6 += s7; //拷贝构造String s9 = s6 + s7; //移动构造return 0;
①、拷贝构造和移动拷贝构造的场景 (可以减少拷贝)
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己 。注:正常的左值引用时拷贝构造,而右值引用是移动拷贝构造
![]()
②、拷贝赋值和移动赋值的场景与①类似(也可以减少拷贝)
③、左值和右值做返回值的场景
operator+与operator+=的返回值
注:operator+返回的是一个右值,因为既有拷贝构造又有移动构造,编译器会选择最匹配的参数调用,用这个右值构造s9,就会匹配调用移动构造。这里其实是个移动语义。
总结:
以前我们写的函数总是避免用传值做返回值,因为会有深拷贝,但这里有右值引用后,会有移动构造和移动赋值减少拷贝,即不会深拷贝了,那你想传值返回就传值返回(很便利)
④、所有深拷贝类(vector/list/map/set..),都可以加两个右值引用做参数的移动拷贝和移动赋值
总结:
⑤、右值引用做函数参数,减少拷贝
⑥、左值引用和右值引用减少拷贝的对比
6、完美转发
模板中的&& 万能引用
解决:利用完美转发,std::forward 完美转发在传参的过程中保留对象原生类型属性
template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};
template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x));}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x));//复用Insert中可能丢失右值属性,故完美转发}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, const T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}
private:Node* _head;
};
int main()
{List<bit::string> lt;lt.PushBack("1111");lt.PushFront("2222");return 0;
}
7、新的类功能
7.1默认成员函数
7.2强制生成默认函数的关键字default:
class A
{
public:A(const int& a):_a(a){}//因为你写了拷贝构造,编译器就不会生成默认构造了//法一、自己写一个默认构造//法二、default:指定编译器显式的生成(C++11做法)A() = default; private:int _a = 10;
};int main()
{A aa1; //若不用default,则失败A aa2(aa); //成功return 0;
}
②、有拷贝构造导致的编译器无法生成移动构造
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:string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
7.2禁止生成默认函数的关键字delete:
class A
{
public:A() = default; //若要求A的对象不能拷贝和赋值(防拷贝)//C++98的做法:只给声明,不给实现,这样别人就无法拷贝对象//缺陷:导致链接不上,且别人可以再类外定义
// A(const int& a);
// A& operator=(const A& aa);//为解决上面缺陷,private限定,类外也无法定义
//private:
// A(const int& a);
// A& operator=(const A& aa);//C++11的做法:用delete定义为删除函数A(const int& a) = delete;A& operator=(const A& aa) = delete;A(const int& a):_a(a){}private:int _a = 10;
};int main()
{A aa1;A aa2(aa1);aa1 = aa2;return 0;
}
C++【下】链接:【C++】C++11【下】lambda表达式|thread线程库-CSDN博客