在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为
C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没
有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,
第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140
个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语
言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
在C++98中,标准允许使用花括号{}
对数组元素进行统一的列表初始值设定。比如:
int array1[] = {1,2,3,4,5};int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=)也可不添加。列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
#include
#include
#include
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可,常见的类比如vector,list,map,set在C++11中都支持initializer_list类型参数的构造函数。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
编译器自己识别{}为initializer_list类型进行转化。
比如vector:
#include
template
class Vector {
public:
// ...
Vector(initializer_list l)
: _capacity(l.size())
, _size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size++] = e;
}
Vector& operator=(initializer_list l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
};
int main()
{
//自己实现的
Vector vv = { 1,2,3,4,5 };
Vector vv2 = vv;
//多个对象支持列表初始化
vectorv1 = {1,2,3,4,5};
list l1 = {1,2,3,4,5};
pairkv("left","左边");
mapdict = { {"insert","插入"},kv};
initializer_list ilt = {1,2,32,4,5};
}
注意:如果使用迭代器时报错,前面加上typename initializer_list
,这是在类模板中再去找他的内嵌类型,未实例化之前可能取不到。告诉编译器类模板实例化了之后再去调用类里面的迭代器。
cbegin(),cend(),emplace_back(),emplace().
定长数组,相比于变长数组vector。
优点:支持迭代器,更好的兼容STL容器。对于越界的检查。
std::array
template < class T, size_t N > class array;
int main()
{
arraya;
int a1[10];//数组
a1[14] = 0;//抽查行为,*(a+14)=0,越界可能不检查
a[14] = 0;//a.operator[](14)=0,对于函数的调用,肯定检查
return 0;
}
forward_list,单链表,支持头插头删(push_front() pop_front()),支持在节点后面插入删除,不支持尾插尾删和在节点之前的操作
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:
可以放在=左边的,变量或者解引用的指针,我们可以获取他的地址+可以对他赋值,称为左值。const修饰的左值可以取地址,但是不可以赋值。
只能放在=右边的,或者不能取地址的称为右值。表示数据的表达式如:字面常量,表达式返回值,传值返回函数的返回值,临时对象也是右值。
const int b=10;//常量,函数返回值等不能取地址的都是右值
int main()
{
//可以取地址对象就是左值
const int b = 10;//b是左值,可以取地址不能赋值
const int& r3=b; //左值引用
//右值
double x = 1.1, y = 2.2;
x + y;
double &&r4=fmin(x,y);
//右值引用就是右值的别名
int&& rr1 = 10;
//左值引用不能直接引用右值,得加上const
const int& r1 = x + y;
const int& r2 = 10;
const int& r3 = fmin(x,y);
const int& p1 = (10+20);
//void push_back(const T& x)这样传参,对面左值引用和右值引用均可传过去
//右值引用不能直接引用左值,但是可以右值引用move以后的左值
int d = 10;
int* p = &d;
int*&& rr1 = move(p);
int n = 10;
int&& p2 = move(n);
const int p = 20;
const int&& rr2 = move(p2);
//给右值取别名之后会改变存储位置,另开辟个空间存储右值,别名就是个左值了
cout<<&p2<
左值引用:传值传参会调用拷贝构造函数,作为参数基本完美解决问题;作为返回值以下问题就不完美只能解决部分问题,所以可用右值引用优化。
string& operator+=(char ch)
{
push_back(ch);
return *this;//处理作用域还在,就很完美
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;//出了作用域就会被销毁,传值返回会多一次拷贝构造,然后再析构,不完美。
//所以只能用传值返回是右值
}
提供一个移动构造,是右值只会去移动构造中,走最匹配的那个函数。
注意:C++11中将右值分为:自定义类型叫将亡值,或者纯右值。
//拷贝构造
string(const string &s)//左值
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
//移动构造,一种资源转移,避免了资源的些许浪费,少一层拷贝
string(string &&s)//右值(临时对象也是一种右值)
:str(nullptr)
{
this->swap(s);
return *this;
}
int main()
{
string s("hello world");
string s1 = s;
string s2 = move(s);//将s左值的属性修改为右值属性,赋予了别人将自己的资源拿走的权利
//单纯的move并不会对于s造成影响,当传给别人时就会有影响。
return 0;
}
//右值引用理解场景2
string to_string(int value)
{
string str;
while(value)
{
int val=value%10;
str+=('0'+val);
value/=10;
}
reverse(str.begin(),str.end());
return str;
}
int main()
{
string ret=to_string(1234);
}
首先,如果编译器不优化,str拷贝构造临时对象,临时对象(在main函数的栈帧中)作为to_string的返回值构造ret。优化之后,在to_string结束之前,用str构造ret,从两次拷贝构造优化为只有一次拷贝构造。
什么情况可优化?
当用临时对象去构造ret时,也就是有ret接收时会进行优化。当没有ret时,因为str在to_string函数结束之后要被销毁,必须得有一个临时对象作为返回值返回,还没人接收时就无法优化。
如果有了移动构造之后,
如果不考虑优化的存在,原来的两次拷贝构造变成一次拷贝构造,一次移动构造,因为str是左值调用一次拷贝构造产生临时变量,此时产生的拷贝的临时对象被认为是右值,调用移动构造。
优化之后,就变成了一次移动构造,鲁莽地直接将str认为是右值,
如果有ret接收,直接移动构造交给ret,完成一次资源转移。
如果没有ret接收,之前直接将ret视为右值的存在在to_string()函数销毁时作为函数返回值返回即可。
资源转移:有ret接收,就资源转移给ret,如果没ret接收,就资源转移给一个函数必有的函数返回值。
使得C++11 效率更高,当然,如果vv是静态或者是全局的,出了作用域还在,直接用左值返回就OK了。
//移动赋值
string& operator=(string&& s)
{
cout<<"转移资源"<
如果有移动赋值时,将str作为右值直接进行移动构造,资源转移给临时对象,再用临时对象移动赋值给ret,两次资源转移。将临时对象这个已经存在的对象交给ret这个已经存在的对象。
如果没有移动赋值函数,
to_string()
的str被识别为右值移动构造资源转移给临时对象,临时对象赋值走一遍深拷贝交给已经存在的ret。
//List C++11
void push_back (const value_type& val);
void push_back (value_type&& val);//新增
int main()
{
//string也有右值引用
list lt;
string s("11111111");
lt.push_back(s);//传参是左值,是深拷贝构造
cout<
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template
class List
{
typedef ListNode Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
// 只要右值引用,再传递其他函数调用,要保持右值属性,必须用完美转发,然后走的yuanwei::string的移动赋值,资源转移
//Insert(_head, x);//这个x退化为左值,就会去调用yuanwei::string中的深拷贝
Insert(_head, std::forward(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
//Node* newnode = new Node;
//newnode->_data = std::forward(x); // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
//new(&newnode->_data)T(x);
//定位new调用从拷贝构造->移动构造
new(&newnode->_data)T(std::forward(x));
//new是开空间+调用构造函数初始化
//stl中的容器的空间是从内存池来的,和malloc效果一样,只开空间不初始化也就不会调用构造函数,对已经存在的空间初始化,需要用定位new初始化空间,就像这个一样。
// prev newnode pos
prev->_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 pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List lt;
lt.PushBack("1111");
//lt.PushFront("2222");
return 0;
}
push_back VS emplace_back
//二者相比,右值版本不会更加高效,差不多
//左值版本 emplace 会更高效,因为他不存在深拷贝的问题
int main()
{
std::list>mylist;
//两次资源转移,先构造右值,再移动构造
mylist.push_back(make_pair(1,'A'));//一个类型参数
mylist.push_back({1,'a'});//pair支持{}初始化
//两次直接构造
mylist.emplace_back(make_pair(1,'a'));//整体作为单参数对象
mylist.emplace_back(1,'a');//多参数传值也支持
return 0;
}
emplace_back()是直接构造,pushback()是先构造对象,然后再进行资源转移.
原来的默认构造函数有6个,重要的有4个:析构函数,构造函数,拷贝构造函数,拷贝赋值函数.
新的是移动构造函数,移动赋值函数。这俩的使用规则是相同的,下面只介绍一个:
如果在类中,你没有实现移动构造函数,并且你没有实现你的析构函数,拷贝构造函数,拷贝赋值函数其中一个,那么会给你生成默认移动构造函数;他对于内置类型是采用值拷贝的方式,对于自定义类型,当自定义类型本身是提供移动构造函数的话,就调用。如果没有,就去调用拷贝构造函数。
同理移动赋值函数。
类内声明内置类型的时候给个缺省值
- C-函数指针void(*p)();
- C++98-仿函数/函数对象
- C++11-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 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++11 lambda表达式
auto priceLess = [](const Goods& g1, const Goods& g2){return g1._price < g2._price; };
sort(v.begin(), v.end(), priceLess);
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._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate; });
return 0;
}
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数**,**捕捉列表能够捕捉上下文中的变量供lambda函数使用。所以不能省略。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
//lambda实现两个数相加的功能
auto add1=[](int a,int b) ->int {return a + b; };
//调用方式
cout<
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this),如果指定某一个变量进行值传递,[a,b]指明出来
[&var]:表示引用传递指定的捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
d. 在块作用域以外的lambda函数捕捉列表必须为空。在全局中无法捕捉变量。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)();
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
lambda表达式,底层原理其实是被处理成一个lambda_uuid的一个仿函数类。
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);
//cout << typeid(r2).name() <
从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会**自动生成一个类,在该类中重载了operator()。**仿函数对象去调用operator()。
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎
么给,或者类型写起来特别复杂。C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identifification 运行时类型识别)。
C++98中确实已经支持RTTI:typeid
只能查看类型不能用其结果类定义类型。dynamic_cast
只能应用于含有虚函数的继承体系中运行时类型识别的缺陷是降低程序运行的效率。
int func(int a)
{
return a;
}
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a + b) c;
//C++98 const int ->int 会存在区别,decltype就不会存在
cout << typeid(c).name() << endl;
//声明函数指针类型
int(*pfunc1)(int) = func;
auto pfunc2 = func;
decltype(pfunc2) pfunc3 = func;//和auto配合使用
decltype(&func) pfunc4 = func;
map dict = { {"left","左边"}};
auto it = dict.begin();
//decltype的使用场景:要顶一个auto推导对象的拷贝
decltype(it) copyIt1 = it;
auto copyIt2 = it;
//vector无法通过编译
vector v;
v.push_back(it);
}
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。显示缺省函数。在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A
{
public:
A(int a): _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
删除默认函数。如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数.
比如:单例模式下,规定声明的对象只能有一个,这个对象是不允许被拷贝构造其他的对象的,C++98 的方法一是,让拷贝构造函数私有化,在类的外面就调用不到了,但是呢,在类的里面中的某个函数还是可以调用的,同时造成默认构造函数就无法生成了。如果类的里面也不让调用时,也就是方法二:在私有中,只声明但是不实现来防止拷贝。注意:如果不设置成私有的话,别人可以在类的外面实现拷贝构造函数,从而控制你提供的类做出修改。
C++11 就直接提供了关键字delete,在拷贝构造函数的后面加上:delete 函数就变成了已删除函数,也无论私有与否了。
避免删除函数和explicit一起使(explicit是防止函数进行隐式类型转换)
class A
{
public:
A(int a): _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
//A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0;
}
int printf ( const char * format, ... );//可变参数
//递归终止函数
template
void ShowList(const T& t)
{
cout << t <//Args模板参数包
void ShowList(T val, Args ... args)//args形参参数包
{
cout << typeid(val).name() << ":" << val << endl;
ShowList(args...);//递归依次到达下一个参数
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1,'A',string("sort"));
return 0;
}
//参数包,传一个或者多个
template
void PrintArg(T val)
{
T copy(val);
cout << typeid(T).name() << ":" << val << endl;
}
template
void ShowList(Args... args)
{
//{}列表初始化,开多大空间取决于可变参数个数,依次取出参数包
//但是函数没有返回值,创建数组需要元素有返回值,所以用逗号表达式,带个0
int arr[] = {(PrintArg(args),0)...};
//逗号表达式,0是返回值。
cout << endl;
}
//带返回值,不用逗号表达式
template
int PrintArg(T val)
{
T copy(val);
cout << typeid(T).name() << ":" << val << endl;
return 0;
}
template
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
//0是返回值
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1,'A',string("sort"));
return 0;
}
endl;
ShowList(args...);//递归依次到达下一个参数
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1,'A',string("sort"));
return 0;
}