1、列表初始化(了解其用法)
2、范围for循环
3、final 与 override
- final:定义最终类或修饰虚函数,是该类和虚函数不能被继承
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
- 注意区分:如果基类定义纯虚函数,则子类必须重写纯虚函数。
4 、新增加容器—静态数组array、forward_list(单链表)以及unordered_xx系列
5、变量类型推导
-
auto
可以使用auto来推导变量的实际类型,auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。 -
typeid
只能查看类型不能用其结果类定义类型。int a = 10; cout<<typeid(a).name()<<endl;
-
decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型。比如:1、推演表达式类型作为变量的定义类型 int a = 10; int b = 20; decltype(a+b) c;// 用decltype推演a+b的实际类型,作为定义c的类型 2、推演函数返回值的类型 int add(int x1, int x2) { return x1 + x2; } int main() { //如果没有带参数,推导函数的类型 cout << typeid(decltype(add)).name() << endl; //如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数 cout << typeid(decltype(add(10, 20))).name() << endl; system("pause"); return 0; }
6、默认成员函数控制
-
default:可以让编译器显示生成某个默认成员函数。
-
delete:可以让编译器禁止生成某个默认成员函数。
A() = default;//让编译器显示生成默认构造函数 A(const A&) = delete;// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
7、右值引用
-
左值:可以被修改的对象,或可以取地址的对象
如:const int a = 10; a 不能被修改但是能取地址,所以 a 是一个左值
-
右值:
①纯右值:常量,基本类型表达式的返回值(如:100,a+b)
②将亡值:函数按照值的方式进行返回的临时自定义对象、表达式返回的临时自定义对象。(如:string s1,s2; s3 = s1+s2;当中的s1+s2就会返回一个临时自定义对象拷贝构造s3) -
左值引用:左值引用就是给左值取别名。左值引用只可以引用左值,但是 const 左值引用既可以引用左值也可以引用右值。
int a = 10; const int& a = 10;//const 引用右值 int b = 20; const int& c = b;//const 引用左值
-
右值引用:右值引用就是给右值取别名。右值引用只可以引用右值,但是可以使用move 将左值的属性改为右值进行右值引用。
int b = 100; int&& a = 10;//右值引用引用右值 int&& c = move(b);//右值引用通过move()引用左值
7.1 移动语义(移动构造,移动赋值)
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式。移动语义主要是右值引用带来的移动构造和移动赋值。移动构造与移动赋值也属于默认成员函数,在C++11中,编译器会为类默认生成一个移动构造、移动赋值,该移动构造、移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造、移动赋值。对于不涉及资源管理的类来说,移动构造、移动赋值并起不了什么作用。
在之前写的 MyString 类的时候,我们重载了operator+()这个函数:
class String//其他函数已舍去
{
public:
.......
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
........
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3 = s1 + s2;
return 0;
}
在我们执行 s3 = s1 + s2 这句代码时候,首先会调用 operator+()这个函数,由于strRet是个局部变量,出了作用域就销毁了,所以返回值是 string 而不是 string& 。而此时真正返回的值是一个strRet 通过拷贝构造出的一个临时对象,临时对象构造完以后 strRet 就被销毁 了。这个临时对象是一个右值(右值中的将亡值)。然后通过 s3 = 临时对象 调用拷贝构造函数,这个临时对象可以传参给拷贝构造函数(因为const 左值引用可以引用右值)。完成这些后进行拷贝构造,拷贝构造完成后这个临时对象就会被析构。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完 全相同的对象,对于空间是一种浪费,这样做还增加了拷贝的次数。如何解决这个问题呢?–》使临时对象被充分的利用起来?****移动构造和移动赋值可以解决这个问题:
String(String&& s)
: _str(nullptr)
{
swap(_str, s._str);
}
int main()
{
String s1("hello");
String s2("world");
String s3 = s1 + s2;//这里是移动构造
return 0;
}
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造 临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临 时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即 可,既省了空间,又大大提高程序运行的效率。当然还有移动赋值,原理同上面相同。
String& operator=(String&& s)
{
_str = nullptr;
swap(_str, s._str);
return *this;
}
int main()
{
String s1("hello");
String s2("world");
String s3;
s3 = s1 + s2;//这里用的是移动赋值
return 0;
}
右值引用的应用主要在函数参数(移动构造、移动赋值)与函数的返回值(临时对象),都在上面那个例子体现。应用在函数参数还有一些其他场景,容器的插入:
移动构造带来的副作用:move将s1转化为右值后,在实现s2的拷贝时就会使用移动 构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串(s1会被置空),要避免这种情况的产生:
int main()
{
string s1("hello");
string s2(move(s1));
return 0;
}
7.2完美转发
函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相 应实参是右值,它就应该被转发为右值。但是,模板传参过程中,实参的右值属性就会被丢失,都会被处理成左值。如何解决这个问题?
完美转发可以解决,C++11通过forward函数来实现完美转发:
7.3 总结右值引用的作用:
-
①:实现移动语义(移动构造与移动赋值)
-
②:给中间临时变量取别名。
string s1("hello"); string s2(" world"); stirng&& s4 = s1 + s2//s4就是s1和s2拼接完成之后结果的别名
-
③:实现完美转发 (通过 forward 实现)
8、 lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法,如果待排序元素为自定义类型,需要用户定义排序时的比较规则。随着C++语法的发展,开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法, 都要重新去 写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了 极大的不便。因此,在C11语法中出现了lambda表达式。
struct Goods
{
string _name;
double _price;
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, { "菠萝", 1.5 } };
//第一种用法
auto ret = [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ret);
//第二种写法,不用获取返回值直接用
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; });
for (int i = 0; i < 4; i++)
{
cout << gds[i]._name << ":" << gds[i]._price << endl;
}
system("pause");
return 0;
}
lambda 表达式语法:
lambda 表达式实现原理
-
实际在底层编译器对于lambda表达式的处理方式,完全和仿函数一样。即:如果定义了一 个 lambda 表达式,编译器会自动生成一个名字叫做lambda_uuid ( uuid是一个字符串生成技术,它可以使每个 lambda 表达式都生成一个名字唯一的类),在该类中重载了operator(),lambda 表达式自身或用 auto 接受的返回值其实是一个对象,可以像仿函数那样使用。
struct add { int operator()(const int& a, const int& b) { return a + b; } }; int main() { //仿函数用法 add tmp1; tmp1(10, 20); //lambda表达式用法 auto tmp2 = [](const int& a, const int& b)->int{return a + b; }; tmp2(10, 20); return 0; }
可以看出 lambda 表达式用法与仿函数的用法并无差别。