C++11类的新特性:移动语义、default、delete、override详解

目录

[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 新增的关键字,用于显式声明重写父类虚函数。

它不会改变程序运行机制,而是通过编译期检查帮助程序员发现重写错误,提高代码的安全性和可维护性。

相关推荐
iCxhust1 小时前
C# 生成命令行程序 将hex格式烧录程序转换成bin烧录格式
开发语言·汇编·单片机·嵌入式硬件·c#·微机原理
Frank学习路上1 小时前
【C++】面试:面向对象与多态
c++·面试
xiaoshuaishuai81 小时前
C# 封装与继承
开发语言·c#
星辰_mya2 小时前
限流、漏斗桶和令牌桶的区别
java·开发语言·面试·架构·高并发
Shadow(⊙o⊙)2 小时前
信号1.0,信号概念、signal()处理、前后台进程、闹钟设置、初识信号三张表。
linux·运维·服务器·开发语言·c++
nazisami2 小时前
深入学习C++11
c++·c++11
(Charon)2 小时前
【C++ 面试高频:STL 容器 vector、map、unordered_map 总结】
开发语言·c++·面试
我是一颗柠檬2 小时前
【Java项目技术亮点】滑动窗口限流算法
java·开发语言·算法
Irissgwe2 小时前
二叉树进阶
数据结构·c++·算法·c·二叉搜索树