【C++11】类的新功能 | 可变参数模板

news2025/2/13 2:16:28

文章目录

  • 一.类的新功能
    • 1.默认成员函数
    • 2.类成员变量初始化
    • 3.强制生成默认函数的关键字default
    • 4.禁止生成默认函数的关键字delete
    • 5.继承和多态中final与override关键字
  • 二.可变参数模板
    • 1.可变参数模板的概念
    • 2.可变参数模板的定义方式
    • 3.参数包的展开方式
      • ①递归展开参数包
      • ②逗号表达式展开参数包
    • 4.STL容器中的emplace相关接口函数

一.类的新功能

1.默认成员函数

八个默认成员函数

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

然后再编写一个简单的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;         //年龄
};

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

int main()
{
	Person s1("张三", 21);
	Person s2 = std::move(s1); //想要调用Person默认生成的移动构造

	return 0;
}

上述代码中用一个右值去构造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;
}

说明一下:

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

2.类成员变量初始化

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

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

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

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

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

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

class Person
{
public:
	//拷贝构造函数
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
private:
	cl::string _name; //姓名
	int _age;         //年龄
};

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

int main()
{
	Person s; //没有合适的默认构造函数可用

	return 0;
}

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

class Person
{
public:
	Person() = default; //强制生成默认构造函数

	//拷贝构造函数
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
private:
	cl::string _name; //姓名
	int _age;         //年龄
};

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

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

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

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

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

class CopyBan
{
public:
	CopyBan()
	{}
private:
	CopyBan(const CopyBan&) = delete;
	CopyBan& operator=(const CopyBan&) = delete;
};

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

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

final修饰类

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

class NonInherit final //被final修饰,该类不能再被继承
{
	//...
};

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

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.可变参数模板的概念

可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

  • 在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
  • 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

本篇博客只讲解函数模板的可变参数。

2.可变参数模板的定义方式

函数的可变参数模板定义方式如下:

template<classArgs>
返回类型 函数名(Args… args)
{
  //函数体
}

例如:

template<class ...Args>
void ShowList(Args... args)
{}

说明一下:

  • 模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0N ( N ≥ 0 )个模板参数,而args则是一个函数形参参数包。
  • 模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。

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

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', string("hello"));
	return 0;
}

我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如:

template<class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl; //获取参数包中参数的个数
}

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

特别注意,语法并不支持使用args[i]的方式来获取参数包中的参数。比如:

template<class ...Args>
void ShowList(Args... args)
{
	//错误示例:
	for (int i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " "; //打印参数包中的每个参数
	}
	cout << endl;
}

因此要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般我们会通过递归或逗号表达式来展开参数包。

3.参数包的展开方式

①递归展开参数包

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

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

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

//展开函数
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判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

②逗号表达式展开参数包

通过列表获取参数包中的参数

数组可以通过列表进行初始化,比如:

int a[] = {1,2,3,4}

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

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

这时调用ShowList函数时就可以传入多个整型参数了。比如:

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	return 0;
}

但C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用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...}

这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入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;
}

4.STL容器中的emplace相关接口函数

emplace版本的插入接口

C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

在这里插入图片描述

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

在这里插入图片描述

注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

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

比如:

int main()
{
	list<pair<int, string>> mylist;
	pair<int, string> kv(10, "111");
	mylist.push_back(kv);                              //传左值
	mylist.push_back(pair<int, string>(20, "222"));    //传右值
	mylist.push_back({ 30, "333" });                   //列表初始化

	mylist.emplace_back(kv);                           //传左值
	mylist.emplace_back(pair<int, string>(40, "444")); //传右值
	mylist.emplace_back(50, "555");                    //传参数包
	return 0;
}

emplace系列接口的工作流程

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

  1. 先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。
  2. 然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。
  3. 在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。
  4. 将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中。

emplace系列接口的意义

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

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

总结一下:

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

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

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

emplace接口的意义:

  • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
  • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。
  • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。
    验证

如果要验证我们上述对emplace系列接口的说法,需要借助一个深拷贝的类,下面模拟实现了一个简化版的string类,类当中只编写了我们需要用到的成员函数。

代码如下:

namespace cl
{
	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;
	};
}

由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。

下面我们用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数。比如:

int main()
{
	list<pair<int, cl::string>> mylist;

	pair<int, cl::string> kv(1, "one");
	cout << endl;
	cout << endl;
	mylist.emplace_back(kv);                              //传左值
	cout << endl;
	mylist.emplace_back(pair<int, cl::string>(2, "two")); //传右值
	cout << endl;
	mylist.emplace_back(3, "three");                      //传参数包
	return 0;
}

在这里插入图片描述

说明一下:

  • 模拟实现string的拷贝构造函数时复用了构造函数,因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。
  • 为了更好的体现出参数包的概念,因此这里list容器中存储的元素类型是pair,我们是通过观察string对象的处理过程来判断pair的处理过程的。

这里也可以以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑。比如:

int main()
{
	list<pair<int, cl::string>> mylist;

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

在这里插入图片描述


码文不易,本章到此结束,还请多多支持哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/864062.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟展览馆有哪些优势?如何打造自己的虚拟展览馆

引言&#xff1a; 随着科技的不断创新与发展&#xff0c;虚拟展览馆作为一种全新的文化体验方式&#xff0c;正逐渐引起人们的关注。虚拟展览馆以其便捷、创新、可定制的特点&#xff0c;为参观者提供了前所未有的沉浸式体验。 一&#xff0e;什么是虚拟展览馆&#xff1f; 虚…

MongoDB【无敌详细,建议收藏】

"探索MongoDB的无边之境&#xff1a;沉浸式数据库之旅" 欢迎来到MongoDB的精彩世界&#xff01;在这个博客中&#xff0c;我们将带您进入一个充满创新和无限潜力的数据库领域。无论您是开发者、数据工程师还是技术爱好者&#xff0c;MongoDB都将为您带来一场令人心动…

修改element-plus主题色

修改element-plus主题色 前提&#xff1a;要安装按需引入和自动导入插件 ​npm install -D unplugin-vue-components unplugin-auto-import文章目录 修改element-plus主题色一、安装插件二、新建一个element的覆盖scss文件三、配置 一、安装插件 npm install -D unplugin-vu…

重启服务器引发的Docker异常

公司使用云服务器需要硬盘扩容&#xff0c;服务器重启才生效。 重启以后发现拉取远程镜像的命令登录失败了&#xff01; 然后发现找不到容器和镜像列表了&#xff0c;但是容器都启动了。 查看docker运行状态都是正常的 systemctl is-active docker systemctl status docker.…

原生JS手写扫雷小游戏

场景 实现一个完整的扫雷游戏需要一些复杂的逻辑和界面交互。我将为你提供一个简化版的扫雷游戏示例&#xff0c;帮助你入门。请注意&#xff0c;这只是一个基本示例&#xff0c;你可以根据自己的需求进行扩展和改进。 思路 创建游戏板&#xff08;Grid&#xff09;&#xff1…

Vue [Day7] 综合案例

核心概念回顾 state&#xff1a;提供数据 getters&#xff1a;提供与state相关的计算属性 mutations&#xff1a;提供方法&#xff0c;用于修改state actions&#xff1a;存放异步操作 modules&#xff1a;存模块 功能分析 https://www.npmjs.com/package/json-server#ge…

企业运单管理教程

随着电子面单的普及&#xff0c;以及商务寄件稳步增加&#xff0c;快递公司为了留住商企客户&#xff0c;推出了月结协议模式寄件。企业可以根据寄件量大小&#xff0c;和快递公司签订月结协议&#xff0c;运费每月结算一次&#xff0c;还能根据自身的寄件量&#xff0c;向快递…

评述6种室内定位技术的底层原理及未来展望

从古至今&#xff0c;人类始终关心一个颇具哲学意味的问题——“我在哪里”。从千年前的人类在夜空下遥望星河&#xff0c;到依靠经验和模糊的观测绘制的初具现代化意味的地图&#xff0c;再到近现代人类在计算机技术、无线通信技术甚至空间技术的帮助下&#xff0c;不断探索更…

ORA-04031

ORA-04031&#xff1a;unable to allocate 3264 bytes of shared memory 1、问题背景 修改SGA重启数据库后报错系统内存&#xff1a;8G 原SGA大小&#xff1a;3G 修改后SGA大小&#xff1a;5G数据库可以正常重启&#xff0c;但是trance日志一直在报错 2、解决办法 调整shared…

湘大 XTU OJ 1148 三角形 题解(非常详细):根据题意朴素模拟+观察样例分析需要计算几轮 具体到一般

一、链接 1148 三角形 二、题目 题目描述 给一个序列&#xff0c;按下面的方式进行三角形累加&#xff0c;求其和值。 比如序列为 1,2,3,4,5 1 2 3 4 53 5 7 98 12 1620 2848输入 有多组样例。每个样例的第一行是一个整数N(1≤N≤100),表示序列的大小&…

uni-app实现图片上传功能

效果 代码 <uni-forms-item name"ViolationImg" label"三违照片 :"><uni-file-picker ref"image" limit"1" title"" fileMediatype"image" :listStyles"listStyles" :value"filePathsL…

docker通用镜像方法,程序更新时不用重新构建镜像

docker通用镜像方法&#xff0c;程序更新时不用重新构建镜像。更新可执行文件后&#xff0c;重新启动容器就可运行。 功能 1、在demo目录下添加脚本文件start.sh&#xff0c;里面执行demo.jar文件。 2、将demo目录映射到镜像下的 /workspace目录。 3、Dockerfile文件中默认…

CDN(Content Delivery Network)内容分发网络

从DNS域名系统到CDN内容分发网络 DNS什么是DNS直接使用DNS的缺点 CDNCDN加速过程使用CDN的优势 DNS 什么是DNS 输入域名www.baidu.com后&#xff0c;浏览器先检查缓存和本地Host文件&#xff0c;看有没有对应的ip地址&#xff0c;有则直接使用&#xff0c;没有就会向本地DNS服…

ubuntu安装nginx以及php的部署

目录 1.安装依赖包 2.安装nginx 3.编译nginx 4.启动nginx 5.访问nginx 6.增加源地址 7.安装php 8.配置php-fpm 9.修改权限 10.配置nginx里的php 11.启动php-fpm 12.配置php文件以及权限 13.登陆查看 1.安装依赖包 apt-get install gcc apt-get install libpcre3 l…

C++:模板初阶以及string类使用

C&#xff1a;模板初阶以及string类使用 模板的简单认识1.泛型编程2.函数模板模板的原理图函数模板格式函数模板实例化非模板函数和模板函数的匹配原则 3.类模板类模板的定义格式类模板的实例化 string1.string简介2.string常用的接口 题目练习1.字符串相加2.字符串里面最后一个…

MySQL REGEXP_SUBSTR() 函数

MySQL 8.0 的 REGEXP_SUBSTR()函数从一个字符串获取和指定模式匹配的子串并返回。默认情况下&#xff0c;REGEXP_SUBSTR()函数执行不区分大小写的匹配。 REGEXP_SUBSTR() 语法如下&#xff1a; REGEXP_SUBSTR (expression, pattern [, position[, occurrence[, match_type]]])…

服务质量(QoS)监控工具

企业网络的使用正变得越来越复杂&#xff0c;由于越来越依赖企业网络进行广泛的应用&#xff0c;网络格局发生了翻天覆地的变化。语音、视频和数据网络的融合增加了更多的变量来确定网络的行为。不同的应用程序以不同的方式影响网络&#xff0c;这要求完全控制网络带宽的使用&a…

年薪50W+的测试大佬都在用这个:Jmeter 脚本开发之——扩展函数

很多同学&#xff0c;都问我&#xff1a;“老师&#xff0c;我的 jmeter 里面&#xff0c;怎么没有 MD5 函数&#xff0c;base64 函数也没有&#xff0c;我是不是用了假的 jmeter&#xff1f;” 哈哈哈&#xff0c;不是的。jmeter 的函数&#xff0c;有自带函数和扩展函数两大…

Python数据分析实战-列表字符串、字符串列表、字符串的转化(附源码和实现效果)

实现功能 str([None,master,hh]) ---> [None,"master","hh"] ---> "None,master,hh" 实现代码 import re import astx1 str([None,master,hh]) print(x1)x2 ast.literal_eval(x1) print(x2)x3 ",".join(str(item) for item…

【C++奇遇记】智能的函数探幽

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集 数据库专栏 初阶数据结构 &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如…