头图

大家好,我是小康。

你有没有过这样的经历?写了一大堆代码,明明逻辑没问题,程序跑得却像蜗牛一样慢。特别是当你在处理大量数据,往容器里疯狂塞东西的时候。

如果你经常和 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 传进去,在容器内部构造对象

这个差别看似小,实际上在性能上却能带来很大的提升,尤其是当:

  1. 对象构造成本高(比如有很多成员变量)
  2. 拷贝成本高(比如内部有动态分配的内存)
  3. 你需要插入大量对象时

"来点实际的例子!"

好的,我们来看一个真正能展示差异的例子。我们创建一个拷贝成本真正很高的类,这样才能看出 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_backemplace_back慢了约86%!这可不是小数目,尤其是在处理大对象时。

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

"看起来emplace_back完胜啊!为什么还有人用push_back?"

好问题!emplace_back虽然在大多数情况下更快,但并不是所有场景都适合用它:

  1. 当你已经有一个现成的对象时push_back可能更直观
  2. 对于基本类型(int, double等),两者性能差异可以忽略不计
  3. 对于某些编译器优化情况,比如移动语义,差距可能不明显

来看一个例子,说明什么时候两者其实差不多:

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函数:

  • vectordequelist: 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++后端技术分享
  • 计算机基础原理与网络编程实战

我不喜欢枯燥的理论堆砌,而是用生动例子和实际问题来解释复杂概念。正如你在这篇文章中看到的那样。

如果这篇文章对你有帮助,别忘了点个,点个关注,这是支持我继续创作的动力!我们下期再见~

怎么关注我的公众号?

扫码即可关注

哦对了,我还建了个技术交流群,大家一起聊技术、解答问题。卡壳了?不懂的地方?随时在群里提问!不只是我,群里还有一堆技术大佬随时准备帮你解惑。一起学,才有动力嘛!


小康
33 声望4 粉丝

一枚分享编程技术和 AI 相关的程序员 ~