• C++11 ——— 类的新功能


    类的新功能

    默认成员函数

    八个默认成员函数

    在C++11之前,一个类中有如下六个默认成员函数:

    • 构造函数。
    • 析构函数。
    • 拷贝构造函数。
    • 拷贝赋值函数。
    • 取地址重载函数。
    • const取地址重载函数。

    其中前四个成员函数最重要,后面两个成员函数一般不会用到,这里“默认”的意思就是你不写编译器会自动生成。在C++11标准中又增加了两个默认成员函数,分别是移动构造函数和移动赋值重载函数。

    默认移动构造和移动赋值的生成条件

    C++11中新增的移动构造函数和移动赋值函数的生成条件如下:

    • 移动构造函数的生成条件:没有自己实现移动构造函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。
    • 移动赋值重载函数的生成条件:没有自己实现移动赋值重载函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。

    也就是说,移动构造和移动赋值的生成条件与之前六个默认成员函数不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成。

    特别注意: 如果我们自己实现了移动构造或者移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值。

    默认生成的移动构造和移动赋值会做什么?

    • 默认生成的移动构造函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动构造就调用它的移动构造,否则就调用它的拷贝构造。
    • 默认生成的移动赋值重载函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动赋值就调用它的移动赋值,否则就调用它的拷贝赋值。

    验证默认生成的移动构造和移动赋值所做的工作

    要验证默认生成的移动构造和移动赋值确实做了上述工作,这里需要模拟实现一个简化版的string类,类当中只编写了几个我们需要用到的成员函数。

    代码如下:

    namespace cl
    {
    	class string
    	{
    	public:
    		//构造函数
    		string(const char* str = "")
    		{
    			_size = strlen(str); //初始时,字符串大小设置为字符串长度
    			_capacity = _size; //初始时,字符串容量设置为字符串长度
    			_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
    			strcpy(_str, str); //将C字符串拷贝到已开好的空间
    		}
    		//交换两个对象的数据
    		void swap(string& s)
    		{
    			//调用库里的swap
    			::swap(_str, s._str); //交换两个对象的C字符串
    			::swap(_size, s._size); //交换两个对象的大小
    			::swap(_capacity, s._capacity); //交换两个对象的容量
    		}
    		//拷贝构造函数(现代写法)
    		string(const string& s)
    			:_str(nullptr)
    			, _size(0)
    			, _capacity(0)
    		{
    			cout << "string(const string& s) -- 深拷贝" << endl;
    
    			string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
    			swap(tmp); //交换这两个对象
    		}
    		//移动构造
    		string(string&& s)
    			:_str(nullptr)
    			, _size(0)
    			, _capacity(0)
    		{
    			cout << "string(string&& s) -- 移动构造" << endl;
    			swap(s);
    		}
    		//拷贝赋值函数(现代写法)
    		string& operator=(const string& s)
    		{
    			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
    
    			string tmp(s); //用s拷贝构造出对象tmp
    			swap(tmp); //交换这两个对象
    			return *this; //返回左值(支持连续赋值)
    		}
    		//移动赋值
    		string& operator=(string&& s)
    		{
    			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    			swap(s);
    			return *this;
    		}
    		//析构函数
    		~string()
    		{
    			//delete[] _str;  //释放_str指向的空间
    			_str = nullptr; //及时置空,防止非法访问
    			_size = 0;      //大小置0
    			_capacity = 0;  //容量置0
    		}
    	private:
    		char* _str;
    		size_t _size;
    		size_t _capacity;
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    然后再编写一个简单的Person类,Person类中的成员name的类型就是我们模拟实现的string类。

    代码如下:

    class Person
    {
    public:
    	//构造函数
    	Person(const char* name = "", int age = 0)
    		:_name(name)
    		, _age(age)
    	{}
    	//拷贝构造函数
    	Person(const Person& p)
    		:_name(p._name)
    		, _age(p._age)
    	{}
    	//拷贝赋值函数
    	Person& operator=(const Person& p)
    	{
    		if (this != &p)
    		{
    			_name = p._name;
    			_age = p._age;
    		}
    		return *this;
    	}
    	//析构函数
    	~Person()
    	{}
    private:
    	cl::string _name; //姓名
    	int _age;         //年龄
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    虽然Person类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数Person类都实现了,因此Person类中不会生成默认的移动构造和移动赋值,可以通过下面的代码来验证:

    int main()
    {
    	Person s1("张三", 21);
    	Person s2 = std::move(s1); //想要调用Person默认生成的移动构造
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码中用一个右值去构造s2对象,但由于Person类没有生成默认的移动构造函数,因此这里会调用Person的拷贝构造函数(拷贝构造既能接收左值也能接收右值),这时在Person的拷贝构造函数中就会调用string的拷贝构造函数对name成员进行深拷贝。

    如果要让Person类生成默认的移动构造函数,就必须将Person类中的拷贝构造、拷贝赋值和析构函数全部注释掉,这时用右值去构造s2对象时就会调用Person默认生成的移动构造函数。

    • Person默认生成的移动构造,对于内置类型成员age会进行值拷贝,而对于自定义类型成员name,因为我们的string类实现了移动构造函数,因此它会调用string的移动构造函数进行资源的转移。
    • 而如果我们将string类当中的移动构造函数注释掉,那么Person默认生成的移动构造函数,就会调用string类中的拷贝构造函数对name成员进行深拷贝。

    要验证Person类中默认生成的移动赋值函数可以用下面的代码,验证方式和上面验证移动构造的方式是一样的。

    int main()
    {
    	Person s1("张三", 21);
    	Person s2;
    	s2 = std::move(s1); //想要调用Person默认生成的移动赋值
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    说明一下:

    • 我们在模拟实现的string类的拷贝构造、拷贝赋值、移动构造和移动赋值函数中都打印了一条提示语句,因此可以通过控制台输出判断是否调用了对应的函数。
    • 由于VS2013没有完全支持C++11,因此上述代码无法在VS2013当中验证,需要使用更新一点的编译器进行验证,比如VS2019。

    类成员变量初始化

    默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。比如:

    class Person
    {
    public:
    	//...
    private:
    	//非静态成员变量,可以在成员声明时给缺省值
    	cl::string _name = "张三"; //姓名
    	int _age = 20;             //年龄
    	static int _n; //静态成员变量不能给缺省值
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意: 这里不是初始化,而是给声明的成员变量一个缺省值。

    强制生成默认函数的关键字default

    C++11可以让我们更好的控制要使用的默认成员函数,假设在某些情况下我们需要使用某个默认成员函数,但是因为某些原因导致无法生成这个默认成员函数,这时可以使用default关键字强制生成某个默认成员函数。

    例如,下面的Person类中实现了拷贝构造函数:

    class Person
    {
    public:
    	//拷贝构造函数
    	Person(const Person& p)
    		:_name(p._name)
    		, _age(p._age)
    	{}
    private:
    	cl::string _name; //姓名
    	int _age;         //年龄
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这时如下代码就无法编译成功了,因为Person类中编写了拷贝构造函数,导致无法生成默认的构造函数,因为默认构造函数生成的条件是没有编写任意类型的构造函数,包括拷贝构造函数。

    int main()
    {
    	Person s; //没有合适的默认构造函数可用
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时我们就可以使用default关键字强制生成默认的构造函数,如下:

    class Person
    {
    public:
    	Person() = default; //强制生成默认构造函数
    
    	//拷贝构造函数
    	Person(const Person& p)
    		:_name(p._name)
    		, _age(p._age)
    	{}
    private:
    	cl::string _name; //姓名
    	int _age;         //年龄
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    说明一下: 默认成员函数都可以用default关键字强制生成,包括移动构造和移动赋值。

    禁止生成默认函数的关键字delete

    当我们想要限制某些默认函数生成时,可以通过如下两种方式:

    • 在C++98中,可以将该函数设置成私有,并且只用声明不用定义,这样当外部调用该函数时就会报错。
    • 在C++11中,可以在该函数声明后面加上=delete,表示让编译器不生成该函数的默认版本,我们将=delete修饰的函数称为删除函数。

    例如,要让一个类不能被拷贝,可以用=delete修饰将该类的拷贝构造和拷贝赋值。

    class CopyBan
    {
    public:
    	CopyBan()
    	{}
    private:
    	CopyBan(const CopyBan&) = delete;
    	CopyBan& operator=(const CopyBan&) = delete;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    说明一下: 被=delete修饰的函数可以设置为公有,也可以设置为私有,效果都一样。

    继承和多态中final与override关键字

    final修饰类

    被final修饰的类叫做最终类,最终类无法被继承。比如:

    class NonInherit final //被final修饰,该类不能再被继承
    {
    	//...
    };
    
    • 1
    • 2
    • 3
    • 4

    final修饰虚函数

    final修饰虚函数,表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错。比如:

    //父类
    class Person
    {
    public:
    	virtual void Print() final //被final修饰,该虚函数不能再被重写
    	{
    		cout << "hello Person" << endl;
    	}
    };
    //子类
    class Student : public Person
    {
    public:
    	virtual void Print() //重写,编译报错
    	{
    		cout << "hello Student" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    override修饰虚函数

    override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错。比如:

    //父类
    class Person
    {
    public:
    	virtual void Print()
    	{
    		cout << "hello Person" << endl;
    	}
    };
    //子类
    class Student : public Person
    {
    public:
    	virtual void Print() override //检查子类是否重写了父类的某个虚函数
    	{
    		cout << "hello Student" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
  • 相关阅读:
    视觉SLAM十四讲-第三讲三维刚体运动
    LeetCode160:相交链表
    给你的R语言再次提速
    触控笔哪个牌子好用?平板电脑触控笔品牌排行榜
    vue2两个数组嵌套循环返回的新数组item顺序要一致
    [SpringBoot系列]SSMP整合小项目
    【机器学习9】前馈神经网络
    CnOpenData学者长期科研服务陪伴计划启动!
    如何将代码以高亮的方式:插入到 word 文件中
    本地部署 EmotiVoice易魔声 多音色提示控制TTS
  • 原文地址:https://blog.csdn.net/chenlong_cxy/article/details/126780535