
目录
[1.1 默认的移动构造和移动赋值](#1.1 默认的移动构造和移动赋值)
[1.2 成员变量声明时给缺省值](#1.2 成员变量声明时给缺省值)
[1.3 default](#1.3 default)
[1.3.1 default的诞生背景](#1.3.1 default的诞生背景)
[1.3.2 default可以修饰的函数](#1.3.2 default可以修饰的函数)
[1.4 delete](#1.4 delete)
[1.4.1 delete的诞生背景](#1.4.1 delete的诞生背景)
[1.4.2 delete的基本用法](#1.4.2 delete的基本用法)
[1.4.3 delete到底做了什么](#1.4.3 delete到底做了什么)
[1.4.4 delete的应用场景](#1.4.4 delete的应用场景)
[1.4.5 delete可以修饰的函数](#1.4.5 delete可以修饰的函数)
[1.4.6 delete与重载](#1.4.6 delete与重载)
[1.5 final](#1.5 final)
[1.6 override](#1.6 override)
[1.6.1 为什么需要 override](#1.6.1 为什么需要 override)
[1.6.2 override的作用](#1.6.2 override的作用)
1.1 默认的移动构造和移动赋值
在C++98的时候,类中会有6个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址重载、const取地址重载,其中最重要的是前4个,后2个用处不大,默认成员函数就是我们不写编译器会生成的函数。在C++11中,由于右值引用的出现,类中就新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
移动构造函数默认生成的条件:没有声明移动构造函数且没有声明析构函数、拷贝构造函数、赋值运算符重载的任意一种,那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数的功能:对于内置类型成员采用浅拷贝的方式,对于自定义类型成员,编译器会检测这个成员是否实现移动构造,如果实现了就调用移动构造函数,如果没有实现就调用拷贝构造函数。
移动赋值运算符重载默认生成的条件:没有声明移动赋值运算符重载且没有声明析构函数、拷贝构造函数、赋值运算符重载的任意一种,那么编译器会自动生成一个默认移动赋值运算符重载。由此可见,移动构造和移动运算符重载的默认生成条件一模一样。
默认生成移动赋值运算符重载的功能:对于内置类型成员采用浅拷贝的方式,对于自定义类型成员,编译器会检测这个成员是否实现移动赋值,如果实现了就调用移动赋值,如果没有实现就调用拷贝赋值。由此可见,移动赋值和移动构造的功能十分相似。
示例:移动构造和移动赋值的实现代码
cpp
#include <iostream>
using namespace std;
class Student
{
public:
Student(const string& name, const string& address, int id, int age)
:_name(name)
,_address(address)
,_id(id)
,_age(age)
{}
// 移动构造
Student(Student&& s)
:_name(std::move(s._name))
,_address(std::move(s._address))
,_id(s._id)
,_age(s._age)
{}
// _name(s._name) _address(s._address)
// 只是拷贝构造,不是移动构造,在这里 s 引用的对象是右值属性
// 但是s是左值属性,对于它的成员也是左值属性,所以会调用拷贝构造
// 要想移动构造,必须move
// 移动赋值
Student& operator=(Student&& s)
{
if (this != &s)
{
_name = std::move(s._name);
_address = std::move(s._address);
_id = s._id;
_age = s._age;
}
return *this;
}
private:
string _name;
string _address;
int _id;
int _age;
};
int main()
{
Student tmp("张三", "天津", 222, 18);
Student s(std::move(tmp));
return 0;
}
1.2 成员变量声明时给缺省值
成员变量在声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表使用这个缺省值进行初始化。
cpp
#include <iostream>
using namespace std;
class Student
{
public:
Student(const string& name, int age)
// 对于没显示走初始化列表的变量,
// 编译器会隐式用缺省值在初始化列表中初始化
:_name(name)
,_age(age)
{}
void Print_info()
{
cout << _name << " " << _address << " " << _id << " " << _age << endl;
}
private:
// 声明时给缺省值
string _name = "张三";
string _address = "xxxxxx";
int _id = 0;
int _age = 18;
};
int main()
{
Student s("李四", 20);
s.Print_info();
return 0;
}
1.3 default
1.3.1 default的诞生背景
例如:类中提供了拷贝构造,编译器就不会生成移动构造了,C++98中的写法就成为下列形式。
cpp
class Student
{
public:
Student(const string& name, const string& address, int id, int age)
:_name(name)
,_address(address)
,_id(id)
,_age(age)
{}
// 移动构造
Student(Student&& s)
:_name(std::move(s._name))
,_address(std::move(s._address))
,_id(s._id)
,_age(s._age)
{}
private:
string _name;
string _address;
int _id;
int _age;
};
由于C++98中的这种写法就很麻烦,所以C++11提出了一个关键字 default ,其中被 default 修饰的函数就是在告诉编译器:这个函数我需要,但是实现由编译器完成。
cpp
class Student
{
public:
Student(const string& name, const string& address, int id, int age)
:_name(name)
,_address(address)
,_id(id)
,_age(age)
{}
Student(Student&& s) = default;
private:
string _name;
string _address;
int _id;
int _age;
};
编译器默认生成的移动构造函数会自动对成员执行移动构造;而手写移动构造函数时,右值引用参数本身是左值,必须使用
std::move将成员显式转换为右值,否则会退化为拷贝构造。
还有一种场景也是没有default的弊端:
我们实现了一个带参但不是全缺省的构造函数,在某些场景下我们还需要无参的构造函数。
cpp
// 有参的构造函数这里就不再写了
// 这里看起来是定义了一个空的无参构造函数
// 但可能让别人误解,写这段代码的人是不是
// 忘记了实现这个函数
class A
{
public:
A() {}
};
// 这样写含义明确,就不会让人误解
class A
{
public:
A() = default;
};
1.3.2 default可以修饰的函数
默认构造 -> 无参的构造函数
析构函数
拷贝构造
拷贝赋值
移动构造
移动赋值
cpp
class A
{
public:
// 默认构造
A() = default;
// 拷贝构造
A(const A& a) = default;
// 拷贝赋值
A& operator=(const A& a) = default;
// 移动构造
A(A&& a) = default;
// 移动赋值
A& operator=(A&& a) = default;
// 析构函数
~A() = default;
};
1.4 delete
1.4.1 delete的诞生背景
在C++11之前,如果想禁止拷贝,通常这样写
cpp
class A
{
private:
// 这里只是声明,它们的定义是在其他源文件
A(const A& a);
A(A&& a)
A& operator=(const A& a);
A& operator=(A&& a);
};
将构造和赋值函数私有化,不让外界调用,只要调用就会发生编译错误,进而达到禁止拷贝的目的。虽然达到了目的,但是会产生两个问题。
问题1:语义不明确
别人看到这份代码,别人不知道你是:故意禁止、忘了实现、以后准备实现。
问题2:错误信息不友好
错误信息: is private
实际上你的目的根本不是访问控制,而是:禁止调用
于是C++11 引入了 关键字 delete。
1.4.2 delete的基本用法
cpp
class A
{
public:
A() = default;
A(const A&) = delete;
};
此时:
cpp
A a;
A b(a);
直接报错:use of deleted function
非常清晰
1.4.3 delete到底做了什么
很多人以为 = delete 相当于 private 其实不是。例如上面的那个基本用法,构造函数和赋值函数依旧在public中,但不能被调用,也就是说private是权限问题,而delete是函数不存在的问题,向外界表面该函数不存在。
1.4.4 delete的应用场景
最经典的用途就是禁止拷贝,对于C++中 istream 和 ostream 对象,它们是不能拷贝的,但能被引用,主要是因为缓冲区的存在,如果能被拷贝,那么会导致缓冲区刷新不在预期结果内。
1.4.5 delete可以修饰的函数
delete可以修饰任何函数,如普通函数、成员函数、构造函数、析构函数等等。
1.4.6 delete与重载
cpp
void func(int)
{
cout << "int" << endl;
}
void func(double) = delete;
func(10); // 正常
func(3.14); // 编译错误,由于最佳匹配,func(3.14) 会匹配void func(double) = delete;然后报错
1.5 final
final 是C++11引入的一个关键字,它和类的继承体系有关。
它的作用只有两个:
1. 禁止类被继承
2. 禁止虚函数被重写
示例1:禁止类被继承
cpp
class Base final
{
};
// 编译错误,Base被final修饰,无法被继承
// 可以把final修饰的类理解为最终类,不能再作为父类
class Derived : public Base
{
};
示例2:禁止虚函数被重写
cpp
class Base
{
public:
// 这里可以理解为:到此为止,继承体系中的任何子类都不能再重写这个函数
virtual void Print() final
{
}
};
// 编译错误,父类中Print函数被final修饰,无法被重写
class Derived : public Base
{
public:
void Print() override
{
}
};
总结:
final只能修饰类和虚函数,对于类,使其成为最终类,不能被继承;对于虚函数,使其成为最终函数,不能被重写。不能修饰普通函数,因为普通函数不存在重写。
1.6 override
1.6.1 为什么需要 override
在C++中,子类可以重写父类的虚函数,实现运行时多态。
例如:
cpp
class Base
{
public:
virtual void Print()
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
void Print()
{
cout << "Derived" << endl;
}
};
这里的 Derived::Print() 成功重写了父类的虚函数。
然而在实际开发中,由于函数名、参数列表等细节写错,很容易导致重写失败。
例如:
cpp
class Base
{
public:
virtual void Print()
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
void Print(int)
{
cout << "Derived" << endl;
}
};
程序员的本意是重写Print(),却误写成了Print(int),此时编译器不会报错。因为这两个函数根本不是同一个函数。对于这种错误往往很难排查。
1.6.2 override的作用
为了解决这个问题,C++11 引入了 override 关键字。
cpp
class Derived : public Base
{
public:
void Print() override
{
cout << "Derived" << endl;
}
};
含义:我希望这个函数重写父类中的虚函数,请编译器帮我在父类中检查是否存在对应的虚函数。
override的检查机制:编译器看到被override修饰的函数,会在父类中查找函数名相同、参数列表相同、const 属性相同的函数,上面的条件必须全部满足,才能编译通过,否则编译出错。
总结:override 是 C++11 新增的关键字,用于显式声明重写父类虚函数。
它不会改变程序运行机制,而是通过编译期检查帮助程序员发现重写错误,提高代码的安全性和可维护性。