大家好,我是小康。
你有没有过这样的经历?写了一大堆代码,明明逻辑没问题,程序跑得却像蜗牛一样慢。特别是当你在处理大量数据,往容器里疯狂塞东西的时候。
如果你经常和 C++ 的 vector、list 这些容器打交道,那么今天这篇文章绝对值得你花几分钟时间——因为我要告诉你一个小技巧,它能让你的代码不仅写起来更爽,还能跑得更快!
它就是容器的"隐藏技能":emplace_back()
。
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
"又是一个新函数?我的push_back不香了吗?"
别急,咱们先来看个例子,感受一下这两者的区别:
#include <vector>
#include <string>
class Person {
public:
Person(std::string name, int age) : m_name(name), m_age(age) {
printf("构造了一个人:%s, %d岁\n", name.c_str(), age);
}
// 拷贝构造函数
Person(const Person& other) : m_name(other.m_name), m_age(other.m_age) {
printf("拷贝构造了一个人:%s\n", m_name.c_str());
}
private:
std::string m_name;
int m_age;
};
int main() {
std::vector<Person> people;
printf("=== 使用push_back ===\n");
people.push_back(Person("张三", 25));
people.clear(); // 清空容器
printf("\n=== 使用emplace_back ===\n");
people.emplace_back("李四", 30);
}
运行这段代码,你会看到这样的输出:
=== 使用push_back ===
构造了一个人:张三, 25岁
拷贝构造了一个人:张三
=== 使用emplace_back ===
构造了一个人:李四, 30岁
看出区别了吗?
- 使用
push_back
时,我们先创建了一个临时的 Person 对象,然后 vector 把它拷贝到容器里 - 而使用
emplace_back
时,我们直接传入构造 Person 所需的参数,vector 直接在容器内部构造对象
结果就是:push_back
额外调用了一次拷贝构造函数,而emplace_back
没有!
"所以emplace_back就是直接传构造函数参数?"
没错!这就是它最大的特点。
push_back(x)
需要你先构造好一个对象x,然后把它放进容器emplace_back(args...)
则是直接把构造函数的参数 args 传进去,在容器内部构造对象
这个差别看似小,实际上在性能上却能带来很大的提升,尤其是当:
- 对象构造成本高(比如有很多成员变量)
- 拷贝成本高(比如内部有动态分配的内存)
- 你需要插入大量对象时
"来点实际的例子!"
好的,我们来看一个真正能展示差异的例子。我们创建一个拷贝成本真正很高的类,这样才能看出 emplace_back 的威力:
#include <vector>
#include <string>
#include <chrono>
#include <iostream>
#include <memory>
// 设计一个拷贝成本很高的类
class ExpensiveToCopy {
public:
// 构造函数 - 创建一个大数组
ExpensiveToCopy(const std::string& name, int dataSize)
: m_name(name), m_dataSize(dataSize) {
// 分配大量内存,模拟昂贵的资源
m_data = new int[dataSize];
for (int i = 0; i < dataSize; i++) {
m_data[i] = i; // 初始化数据
}
}
// 拷贝构造函数 - 非常昂贵,需要复制整个大数组
ExpensiveToCopy(const ExpensiveToCopy& other)
: m_name(other.m_name), m_dataSize(other.m_dataSize) {
// 深拷贝,非常耗时
m_data = new int[m_dataSize];
for (int i = 0; i < m_dataSize; i++) {
m_data[i] = other.m_data[i];
}
// 输出提示以便观察拷贝构造函数的调用情况
std::cout << "拷贝构造: " << m_name << std::endl;
}
// 析构函数
~ExpensiveToCopy() {
delete[] m_data;
}
// 禁用赋值运算符以简化例子
ExpensiveToCopy& operator=(const ExpensiveToCopy&) = delete;
private:
std::string m_name;
int* m_data;
int m_dataSize;
};
// 计时辅助函数
template<typename Func>
long long timeIt(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
const int COUNT = 100; // 对象数量,减少一点以便看到输出
const int DATA_SIZE = 100000; // 每个对象中数组的大小
std::cout << "=== 测试push_back ===\n";
long long pushTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 预分配空间避免重新分配的影响
for (int i = 0; i < COUNT; i++) {
// 创建临时对象然后放入vector
// 这个过程会调用拷贝构造函数
objects.push_back(ExpensiveToCopy("对象" + std::to_string(i), DATA_SIZE));
}
});
std::cout << "\n=== 测试emplace_back ===\n";
long long emplaceTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 预分配空间避免重新分配的影响
for (int i = 0; i < COUNT; i++) {
// 直接传递构造函数参数
// 直接在vector内部构造对象,避免了拷贝
objects.emplace_back("对象" + std::to_string(i), DATA_SIZE);
}
});
std::cout << "\npush_back耗时: " << pushTime << " 微秒" << std::endl;
std::cout << "emplace_back耗时: " << emplaceTime << " 微秒" << std::endl;
double percentDiff = (static_cast<double>(pushTime) / emplaceTime - 1.0) * 100.0;
std::cout << "性能差异: push_back比emplace_back慢了 " << percentDiff << "%" << std::endl;
}
在我的电脑上,大约是这样的结果:
=== 测试emplace_back ===
push_back耗时: 66979 微秒
emplace_back耗时: 35858 微秒
性能差异: push_back比emplace_back慢了 86.7896%
这意味着push_back
比emplace_back
慢了约86%!这可不是小数目,尤其是在处理大对象时。
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
"看起来emplace_back完胜啊!为什么还有人用push_back?"
好问题!emplace_back
虽然在大多数情况下更快,但并不是所有场景都适合用它:
- 当你已经有一个现成的对象时,
push_back
可能更直观 - 对于基本类型(int, double等),两者性能差异可以忽略不计
- 对于某些编译器优化情况,比如移动语义,差距可能不明显
来看一个例子,说明什么时候两者其实差不多:
std::vector<int> numbers;
// 对于基本类型,这两个是等价的
numbers.push_back(42);
numbers.emplace_back(42);
// 如果已经有一个现成的对象
std::string name = "张三";
std::vector<std::string> names;
// 这种情况下,如果 string 支持移动构造,两者性能接近
names.push_back(name); // 拷贝name
names.push_back(std::move(name)); // 移动name(推荐)
names.emplace_back(name); // 拷贝name
names.emplace_back(std::move(name)); // 移动name(推荐)
"完美转发是什么鬼?听说emplace_back跟这个有关?"
没错!emplace_back
的强大之处,部分来自于它使用了"完美转发"(Perfect Forwarding)技术。
简单来说,完美转发就是把函数参数"原汁原味"地传递给另一个函数,保持它的所有特性(比如是左值还是右值,是const还是non-const)。
在C++中,这通常通过模板和std::forward
实现:
template <typename... Args>
void emplace_back(Args&&... args) {
// 在容器内部直接构造对象
// 完美转发所有参数
new (memory_location) T(std::forward<Args>(args)...);
}
这样的设计让emplace_back
能够接受任意数量、任意类型的参数,并且完美地转发给对象的构造函数。这就是为什么你可以直接这样写:
people.emplace_back("张三", 25); // 直接传构造函数参数
而不需要先构造一个对象。
"还有其他 emplace 系列函数吗?"
是的!STL容器中有一系列的emplace函数:
vector
、deque
、list
:emplace_back()
list
,forward_list
:emplace_front()
- 所有容器:
emplace()
(在指定位置构造元素) - 关联容器(
map
,set
等):emplace_hint()
(带提示的插入)
它们的共同点是:直接在容器内部构造元素,而不是先构造再拷贝/移动。
实战建议:什么时候用 emplace_back?
1、 复杂对象插入:当你要插入的对象构造成本高、拷贝代价大时
2、 大量数据操作:需要插入大量元素时,性能差异会更明显
3、 直接传参更方便时:比如插入 pair 到 map
// 不那么优雅
std::map<int, std::string> m;
m.insert(std::make_pair(1, "one"));
// 更优雅,也更高效
m.emplace(1, "one");
4、 临时对象场景:当你需要创建临时对象并插入容器时
总结
emplace_back
本质上是通过减少不必要的对象创建和拷贝来提升性能。它利用了 C++ 的完美转发功能,让你可以直接传递构造函数参数,而不需要先创建临时对象。
在处理复杂对象或大量数据时,这种优化尤为明显。当然,对于简单类型或已有对象,两者差异不大。
所以下次当你在写:
myVector.push_back(MyClass(arg1, arg2));
的时候,不妨试试:
myVector.emplace_back(arg1, arg2);
代码更简洁,运行更高效,何乐而不为呢?
记住,在编程世界里,这种看似微小的优化,累积起来就是质的飞跃!
你的容器操作用的是 push_back 还是 emplace_back ?欢迎在评论区分享你的经验!
如果你喜欢这种通俗易懂的 C++ 技术讲解,欢迎关注我的公众号「跟着小康学编程」,这里不仅有性能优化技巧,还有更多 C++11/14/17/20 的现代特性解析,让你的代码既现代又高效!
每天进步一点点,C++技能升级不停歇
看完这篇文章,你是不是对 C++ 容器操作有了新的认识?这只是现代C++高效编程的冰山一角。如果你想掌握更多这样的实用技巧,欢迎关注我的公众号「跟着小康学编程」。
在那里,你会发现:
- 性能调优的秘密武器
- STL容器和算法的高级应用
- C++面试中的那些坑和解法
- Linux C/C++后端技术分享
- 计算机基础原理与网络编程实战
我不喜欢枯燥的理论堆砌,而是用生动例子和实际问题来解释复杂概念。正如你在这篇文章中看到的那样。
如果这篇文章对你有帮助,别忘了点个赞,点个关注,这是支持我继续创作的动力!我们下期再见~
怎么关注我的公众号?
扫码即可关注。
哦对了,我还建了个技术交流群,大家一起聊技术、解答问题。卡壳了?不懂的地方?随时在群里提问!不只是我,群里还有一堆技术大佬随时准备帮你解惑。一起学,才有动力嘛!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。