[请回答C++] C++11&&类默认函数&&final&&override&&可变参数模板&&emplace

BingWallpaper


image-20220929151808023

默认成员函数控制

严格来说C++一个类中应该有几个默认成员函数呢?

原先我们认为有六个:

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

实际上现在在C++中对于空类编译器会生成一些默认的成员函数:

  1. 构造函数
  2. 拷贝构造函数
  3. 运算符重载
  4. 析构函数
  5. &取地址重载(用处不大)
  6. const取地址的重载(用处不大)
  7. 移动构造
  8. 移动赋值

如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

自动生成移动

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:( 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。)

  1. 如果没有自己实现移动构造,且析构函数、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行浅拷贝或者值拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
		{}
private:
	allen::string _name;//自定义的string中有移动构造
	int _age = 0;
};
int main()
{
	Person s1;
	Person s3 = std::move(s1);
	return 0;
}

image-20220518172844378

只要没有实现移动构造的前提下,实现任何一个其他比如析构,就会变成调用拷贝构造

  1. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数,拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  2. **如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。 **

因此C++11对自定义生成类型的成员变量非常的友好,默认成员函数都会恰当处理自定义类型成员

class AbaAba
{
private:
	std::stack<int> _pushST;
	std::stack<int> _popST;
};

类似于这样的类是不需要自己主动显式实现其他默认函数的

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

默认生成的移动构造函数:

对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动构造就调用它的移动构造,否则就调用它的拷贝构造。

默认生成的移动赋值重载函数:

对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动赋值就调用它的移动赋值,否则就调用它的拷贝赋值。

类成员变量初始化

C++11允许在类定义的时候给成员变量初始缺省值,默认生成的构造函数会使用这些缺省值初始化

private:
	allen::string _name;
	int _age;

所以为了弥补这一点,C++11支持类成员变量的初始化

private:
	//非静态成员变量,可以在成员声明时给缺省值
	cl::string _name = "张三"; //姓名
	int _age = 20;             //年龄
	static int _n; //静态成员变量不能给缺省值

显式缺省函数default

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本(强制生成),用=default修饰的函数称为显式缺省函数。

class Person
{
public:
	Person() = default;
	Person(Person&& p) = default;//VS2013不支持
	Person(const Person& p)=default
	~Person()=default

private:
	allen::string _name;
	int _age = 0;
};

删除默认函数delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数

之前用的很多了就不举例了,但是有一个注意点

⚠️注意:避免删除函数和explicit一起使

final和override

之前在继承多态的时候已经提到过,这里就点一下重点

final

final: 修饰类,则该类不能继承**

class Pokemon final
{
public:
    void Move();
}:

class pikachu : public Pokemon  //err
{
}

final:修饰虚函数,表示该虚函数不能再被继承

class Car
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }  //虚函数
};
override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car
{
public:
	virtual void Drive(char ch){}
};

class Benz :public Car 
{
public:
    virtual void Drive(int i) override 
    { 
        cout << "Benz-舒适" << endl; 
    }
};

一般纯虚函数,才是要求子类强制重写,如果子类不重写,子类依旧是抽象类,则不能被实例化

可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。

template <class ...Args>
void ShowList(Args... args)
{
    cout << sizeof...(args) << endl;//支持
    
    //注意这个可变参数模板不是随意使用的,是由使用场景限制的
	//for (int i = 0; i < sizeof...(args); ++i)
	//{
		//cout<<args[i]<<" "; // 不支持
	//}
}

模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 )个模板参数,而args则是一个函数形参参数包。

模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。

现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。

参数包的展开方式

但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

一般我们会通过递归或逗号表达式来展开参数包。

递归展开

递归展开参数包的方式如下

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:

//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}

编写无参的递归终止函数

我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//递归终止函数
void ShowList()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}

这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。

  • 但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
  • 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。

鉴于此,我们可以将展开函数和递归调用函数的函数名改为ShowListArg,然后重新编写一个ShowList函数模板,该函数模板的函数体中要做的就是调用ShowListArg函数展开参数包。比如:

//递归终止函数
void ShowListArg()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
	cout << value << " "; //打印传入的若干参数中的第一个参数
	ShowListArg(args...); //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
	ShowListArg(args...);
}

这时无论外部调用时传入多少个参数,最终匹配到的都是同一个函数了。

除了编写无参的递归终止函数,也可以编写带参数的递归终止函数来终止递归

这里编写带一个参数的递归终止函数:

//递归终止函数
template<class T>
void ShowListArg(const T& t)
{
	cout << t << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
	cout << value << " "; //打印传入的若干参数中的第一个参数
	ShowList(args...);    //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{
	ShowListArg(args...);
}

这样一来,在递归调用过程中,如果传入的参数包中参数的个数为1,那么就会匹配到这个递归终止函数,这样也就结束了递归。但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。

但该方法有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

判断参数包中参数的个数(不可行!)

既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数个数为0时就终止递归呢?比如:

//错误示例
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印传入的若干参数中的第一个参数
	if (sizeof...(args) == 0)
	{
		return;
	}
	ShowList(args...);    //将剩下参数继续向下传
}

这种方式是不可行的,原因如下:

函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。

而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数要求至少传入一个参数。

这里编写的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

除非下面这种修改方式

//一定要补一个空参数包,这样才能停下来
void ShowList()
{
	cout << endl;
}

template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	if (sizeof...(args) == 0)
	{
		return;
	}
	ShowList(args...);
}

int main()
{
	ShowList();
	ShowList(9011);
	ShowList(9012, 'A');
	ShowList(9012, 'A', string("言之命至"));

	return 0;
}
逗号表达式

如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了。比如:

//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... }; //列表初始化
	//打印参数包中的各个参数
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	return 0;
}

这里只要都是整形的话就可以用这种方法初始化,但是如果其中有一个不是整形就不可以

调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。

通过逗号表达式展开参数包

虽然我们不能用不同类型的参数去初始化一个整型数组,但我们可以借助逗号表达式。

🍁 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
🍁 将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。
🍁 将处理参数包中参数的动作封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如:

//处理参数包中的每个参数
template<class T>
void PrintArg(const T& t)
{
	cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
	cout << endl;
}

说明:

我们这里要做的就是打印参数包中的各个参数,因此处理函数当中要做的就是将传入的参数进行打印即可。

可变参数的省略号需要加在逗号表达式外面,表示需要将逗号表达式展开,如果将省略号加在args的后面,那么参数包将会被展开后全部传入PrintArg函数,代码中的{(PrintArg(args), 0)…}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}。

如果想要支持传入0个参数,也可以写一个无参的ShowList函数。

//支持无参调用
void ShowList()
{
	cout << endl;
}
//处理函数
template<class T>
void PrintArg(const T& t)
{
	cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
	cout << endl;
}

实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的。

//支持无参调用
void ShowList()
{
	cout << endl;
}
//处理函数
template<class T>
int PrintArg(const T& t)
{
	cout << t << " ";
	return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArg(args)... }; //列表初始化
	cout << endl;
}

emplace

image-20220519101919545

在有着可变参数的基础上面,emplace就是借助可变参数的一个STL容器接口

image-20220519102041563

在vector的末尾插入一个新元素,就在其当前最后一个元素之后。这个新元素是使用args作为其构造函数的参数来构造的。

和push_back相同点

在这个情况下,emplace_back和push_back的效果是一样的

	std::list< std::pair<int, char> > mylist;

	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');

这里的emplace_back是不支持大括号方式的初始化的,因为push_back知道data的类型emplace_back的构造是可变参数包形式被编译器推成了Initializer_list,所以不可以推出,可以用小括号直接传构造pair的参数,这是一种参数包的方式

emplace的优势

🌿 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
🌿 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
🌿 除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。

std::list<Date> lt;
Date d1 = { 2022, 5, 18 };
lt.push_back(d1);
lt.push_back(Date(2022,5,19));
lt.push_back({2022,5,19});

lt.emplace_back(d1);
lt.emplace_back(Date(2022, 5, 19));
//lt.emplace_back({2022, 5, 19});//err
lt.emplace_back(2022, 5, 19);//该方式效率最高

分析emplace调用方式

image-20220519123038051

emplace系列接口的工作流程如下:

👑 先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。

👑 然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。

👑 在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。

👑 将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中。

下面可以看到emplace的第三种参数包方式效率是最高的

	std::list< std::pair<int, allen::string> > mylist;
	std::pair<int, allen::string> kv(20, "sort");
	mylist.emplace_back(kv);					// 左值
	mylist.emplace_back(std::pair<int, allen::string>(20, "sort")); // 右值
	mylist.emplace_back(10, "sort");			// 参数包

image-20220519130811917

emplace接口意义

emplace的调用形式

由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

📖 如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数

📖 如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数

📖 如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数

也就是说:

  • 传入左值对象,需要调用构造函数+拷贝构造函数。
  • 传入右值对象,需要调用构造函数+移动构造函数。
  • 传入参数包,只需要调用构造函数。

当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。

实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。

emplace的意义究竟在何处

emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。

但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的

emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

测试用例
namespace allen
{
	class string
	{
	public:
		//构造函数
		string(const char* str = "")
		{
			cout << "string(const char* str) -- 构造函数" << endl;

			_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;
	};
}

测试一组emplace_back

int main()
{
	list<pair<int, allen::string>> mylist;
	
	pair<int, allen::string> kv(1, "左值");
	mylist.emplace_back(kv);                              //传左值
	cout << endl;
	mylist.emplace_back(pair<int, allen::string>(2, "右值")); //传右值
	cout << endl;
	mylist.emplace_back(3, "参数包");                      //传参数包
	return 0;
}

image-20220929122535495

测试一组push_back

int main()
{
	list<pair<int, allen::string>> mylist;
	
	pair<int, allen::string> kv(1, "one");
	mylist.push_back(kv);                              //传左值
	cout << endl;
	mylist.push_back(pair<int, allen::string>(2, "two")); //传右值
	cout << endl;
	mylist.push_back({ 3, "three" });                  //列表初始化
	return 0;
}

image-20220929122929631

参考资料:https://blog.csdn.net/chenlong_cxy/article/details/126807356?spm=1001.2014.3001.5502

参考资料:https://blog.csdn.net/chenlong_cxy/article/details/126780535

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值