左值引用和右值引用:
左值引用关联的是左值,左值是什么?
有名称的内存空间,可多次访问到的。可获取其地址值。左值可出现在赋值运算符左侧,故而称为左值,当然可以出现在右侧,但右值不能出现在赋值运算符左侧。
左值引用相当是为左值起了个别名,获取到左值的存储空间。
右值引用关联的是右值:
右值是临时的,不能对其使用地址运算符,可出现在赋值运算符右侧的,故而称为右值。
由于压根无法获取右值的存储空间,因而右值引用会将右值存储到特定的位置处。即便无法直接访问该右值,但能通过右值引用来访问该右值,且可对该右值引用取地址。
想想看,之前没有右值引用,那么为什么要加上个右值引用呢?由于右值引用对应的是右值,而右值是临时值,因此可通过形参突出对一个临时变量所做的操作不同于对于左值类型的变量所做的工作!针对不同的情况做不同的事!为了实现这个需要有种新的标识符来表示对临时变量的引用!下面我们就来谈谈对临时变量和左值类型的变量操作的差异!即移动语义和复制语义!
移动语义和复制语义:
移动语义:
新的对象接管原对象的数据或是原对象指针指向的空间,同时对于原对象修改指针指向的空间。(避免两次释放同一个内存空间)
复制语义:
深度复制,对于包含指针的对象的复制,为避免悬空指针,分配新的空间后再复制数据。
避免移动数据,只是修改记录,转移所有权,转移过程中可能会修改实参的值,从这里可以看出至少要获得实参吧,为此至少得是引用类型,可是左值引用我们已经让其执行深度复制,为此我们需要新的引用类型的表示,这正是右值引用的用武之处!通过右值引用,我们可以执行一种新的不同于复制构造函数的操作,不再深度复制,而是使新的对象窃取实参中指针变量指向的空间(值),改变实参中指针变量的指向的空间(值)。
对于编译器来说,需要一种规则告知他何时执行复制、何时执行移动。这就是右值引用的作用之处。右值引用类型的形参匹配右值实参,左值引用形参匹配左值实参。为此,可将构造函数分为两种:复制构造函数、移动构造函数。赋值运算符函数和移动赋值运算符函数。
在头文件utility中,提供了move()方法可将左值强制转化为右值,使得可以调用形参为右值类型的函数!这么做可能有时候我们不希望执行深度复制,而是以效率为主,希望调用移动函数,为此我们只需将实参进行类型转化即可。
#include<utility>
#include<iostream>
class Useless {
int n;
char *p;
public:
Useless() {
n = 0;
p = nullptr;
}
Useless(int m,char c) :n(m) {
p = new char[n];
int i;
for ( i = 0; i < n-1; i++)
p[i] = c;
p[i] = '\0';
std::cout << " construct one parameter" << std::endl;
}
Useless(Useless & copy);
Useless(Useless && mv);
~Useless();
Useless operator=(const Useless & copy);
Useless operator=(Useless && mv);
Useless operator+(const Useless &o);
void show();
};
Useless::Useless(Useless & copy)
{
n = copy.n;
p = new char[n];
int i;
for ( i = 0; i < n-1; i++)
{
p[i] = copy.p[i];
}
p[i] = '\0';
std::cout << " constructor copy\n";
}
Useless::Useless(Useless && mv)
{
n = mv.n;
p = mv.p;
mv.n = 0;
mv.p = nullptr;
std::cout << " constructor move\n";
}
Useless::~Useless()
{
delete[]p;
std::cout << "destory!\n";
}
Useless Useless::operator=(const Useless & copy)
{
if (this == ©)
{
return *this;
}
delete[] p;
n = copy.n;
p = new char[n];
int i;
for ( i = 0; i < n-1; i++)
{
p[i] = copy.p[i];
}
p[i] = '\0';
std::cout << " copy = function\n";
return *this;
}
Useless Useless::operator=(Useless && mv)
{
if (this == &mv)
return *this;
delete[]p;
n = mv.n;
p = mv.p;
mv.n = 0;
mv.p = nullptr;//临时变量一定会被释放的但我们在释放前已经保存了数据成员并修改了将释放的数据
std::cout << "move = function\n";
return *this;
}
Useless Useless:: operator+(const Useless &o)
{
std::cout << "in + fucntion\n";
Useless temp;
temp.n = o.n + n;
temp.p = new char[temp.n];
int i;
for ( i = 0; i < n; i++)
temp.p[i] = p[i];
for ( i = n; i < n + o.n; i++)
temp.p[i] = o.p[i - n];
temp.p[i] = '\0';
std::cout << "leaving + function\n";
return temp;
}
void Useless::show()
{
std::cout << n << " " << p << std::endl;
}
int main()
{
Useless one(6,'c');
one.show();
Useless two(one);
two.show();
Useless three(one + two);//移动完成后 才释放临时对象one+two
three.show();
//two = std::move(three);
two = static_cast< Useless &&> (one);
two.show();
//one.show();
std::cin.get();
return 0;
}
C++11中为类新增的功能:
1、特殊成员函数(6个!默认构造函数、复制构造函数、移动构造函数、赋值运算符、移动赋值运算符、析构函数)
编译器可在需要的时候(实在没得用的时候)为我们提供默认的复制构造函数、移动构造函数、赋值运算符函数、移动赋值运算符函数,如何理解没得用?我们知道复制构造函数和移动构造函数的区别在于左值引用还是右值引用,如果自定义了一个复制构造函数,但我们的实参是临时对象,也是可用自定义的复制构造函数的(生成一个临时对象)。相对的,我们如果定义了移动构造函数,对于左值类型的实参可是可以使用移动构造函数的。只有当什么函数都没有定义的时候,编译器会根据实参的类型为我们提供最合适的方法。但凡是定义了一种构造方法,没定义的默认构造方法都失效。
构造函数为类成员初始化时将继续调用类成员对象的构造函数,根据为类成员提供的参数,来决定调用类成员对象的何种构造方法!(有什么能用的就用,实在没法了报错)
2、开启默认方法和禁用默认方法
default 使得可以使用编译器提供的默认方法!可以调用默认构造方法(但是default只能用于6个方法:默认构造函数、复制构造函数、移动构造函数、赋值运算符、移动赋值运算符、析构函数)
禁用方法(禁用编译器提供的方法或是禁止参数发生类型转化,进行精确匹配时可选用的方法)
比如:在这里,当实参为5时,编译器将报错!
3、委托构造函数
多个构造函数,函数体执行的内容有重叠时,为减少代码冗余,可在一个构造函数中调用另一个构造函数,但是需要遵循其语法:需要被调用的写在调用之前,使用成员初始化列表表达!
4、派生类继承基类的构造函数
语法:using 基类名::基类名(构造函数名称),使得基类的构造方法在派生类中可用,甚至当派生类对象初始化时参数与实参不匹配,可调用与基类参数匹配的构造方法。
5、管理虚方法
基类中声明了虚函数,但是在派生类中声明该虚函数时,特征标与基类不同,此时,会隐藏基类中的方法(隐藏就是无法通过派生类对象再来调用该方法),若是使用基类方法的参数类型,编译器报错,这种类型不一致的问题,是否可能是人为错误呢?(虚函数是需要在派生类中被重新定义的,但现在确没有被定义而是被隐藏了,与使用虚函数初衷不否啊,为了防止是人为将类型写错,此时我们可以让编译器为我们做个检查,不允许虚函数被隐藏而是必须在派生类中重定义)!在派生类中以override修饰虚函数!这样一来表达我们的目的是覆盖,因而可以检查基类是否有此类型的参数。
除了override修饰词外,还有final,不过这个是用在基类的虚函数上,以表示不允许覆盖此虚函数!
lambda表达式:
函数对象有三种:函数符 、函数指针、lambda表达式。
lambda表达式是一种定义和应用函数的数学系统,使得无需给函数命名,对于使用函数对象的函数可使用lambda表达式(匿名的函数定义),与普通的函数定义相比存在一些差异!
当函数仅由一条返回语句组成,可以省去返回类型(通过return判断返回类型);但函数定义包含多条语句需使用返回类型后置语法。
在写下面一个例子的时候,第一次见到在函数内部定义类!
#include<iostream>
#include<vector>
#include<algorithm>
#include<ctime>
#include<cstdlib>
const int size1 = 20;
const int size2 = 100 * size1;
const int size3 = 100 * size2;
bool f3(int x) {
return x % 3 == 0;
}
bool f12(int x)
{
return x % 12 == 0;
}
using namespace std;
int main()
{
srand(time(0));
vector<int > v(size1);
generate(v.begin(), v.end(), rand);
int c3 = count_if(v.begin(), v.end(), f3);
int c12 = count_if(v.begin(), v.end(), f12);
cout << "c12 " << c12 << " c3 " << c3 << endl;
v.resize(size2);
//srand(time(0));
generate(v.begin(), v.end(), rand);
class test {
int val;
public:
test(int v):val(v){}
bool operator()(int x) {
return x%val == 0;
}
};
c12 = count_if(v.begin(), v.end(), test(12));
c3 = count_if(v.begin(), v.end(), test(3));
cout << "c12 " << c12 << " c3" << c3 << endl;
v.resize(size3);
generate(v.begin(), v.end(), rand);
c12 = count_if(v.begin(), v.end(), [](int x) {return x % 12 == 0; });
c3 = count_if(v.begin(), v.end(), [](int x) {return x % 3 == 0; });
cout << "c12 " << c12 << " c3" << c3 << endl;
cin.get();
return 0;
}
为何使用lambda表达式?
距离:
函数定义和使用越近越好,函数指针显然达不到这个要求,函数符可以,因为可在函数内部定义函数符,lambda显然更直接。
简洁:
lambda比较繁琐,需要每次都写出来函数的定义,但是我们可以给lambda表达式赋予一个名字,之后可使用该名字表示该函数定义。使用方法如下:
lambda其他功能:
lambda可以访问所在作用域内的所有变量并且可以指定以值访问变量还是以引用访问变量,即在[]内标明。=以值访问,&引用访问。不特殊写明变量名称,则是对所有变量都以该方式访问。
int c10 = 0;
int c5 = 0;
for_each(v.begin(), v.end(), [&](int x) {c10 += (x % 10 == 0); c5 += (x % 5 == 0); });
包装器:
function包装器:模板具体化时,能以统一的方式处理多个相似的函数形式。
函数指针、函数对象、lambda表达式都是可调用的类型,即便函数参数、返回类型相同,但由于采取的形式不同,故而在具体化时所表达的形式不同,这导致实例化模板时,明明都是一样的调用特征标(返回类型和函数参数),却需要创建多个实例,效率低。为了解决这一个问题,functional头文件中的模板类function,根据调用特征标创建的function对象可以统一表示函数指针、函数符、lambda。对包装器赋值的来源可以是相同调用特征标的函数指针、函数符、lambda表达式。
。