循环中向vector添加元素时触发segfault的原因排查
问题描述
编写的C++代码用于将标准输入解析为存储在vector<Chain>中的对象,程序在执行到chain->jobs.push_back(job);时触发段错误。排查发现,当第二次从map中获取chain指针时会出现该问题。
完整代码
#include <iostream> #include <string> #include <regex> #include <vector> #include <map> using namespace std; struct Job { Job(){}; Job(const Job& job) : job_id_(job.job_id_), runtime_sec_(job.runtime_sec_), next_job_id_(job.next_job_id_) {}; Job(int job_id, int runtime_sec, int next_job_id) : job_id_(job_id), runtime_sec_(runtime_sec), next_job_id_(next_job_id) {}; Job& operator=(const Job& job){ job_id_ = job.job_id_; runtime_sec_ = job.runtime_sec_; next_job_id_ = job.next_job_id_; return *this; } ~Job(){}; int job_id_; int runtime_sec_; int next_job_id_; }; struct Chain { std::vector<Job> jobs; }; void parse_input(std::vector<Chain>& chains) { std::string line; std::getline(std::cin, line); std::map<int, Chain*> chain_map; Job job; std::regex job_regex("([0-9]+),([0-9]+),([0-9]+)"); while (getline(std::cin, line)) { std::cout << line << '\n' << std::flush; std::smatch matches; if (std::regex_match(line, matches, job_regex)) { int job_id = std::stoi(matches[1].str()); auto res = chain_map.find(job_id); Chain* chain; if (res == chain_map.end()) { chains.emplace_back(); chain = &chains.back(); } else { chain = res->second; } Job job(std::stoi(matches[1].str()), std::stoi(matches[2].str()), std::stoi(matches[3].str())); chain->jobs.push_back(job); chain_map[job.next_job_id_] = chain; } else { std::cout << "Malformed input\n"; } std::cout << chains.size() << '\n'; } } int main() { std::vector<Chain> chains; parse_input(chains); return 0; }
测试用标准输入
job_id,runtime,next_job_id 1,30,23 2,23,3 3,20,0 23,50,4 4,43,0
栈追踪信息
Program received signal SIGSEGV, Segmentation fault. 0x000055555555e524 in std::construct_at<Job, Job const&> (__location=0x1e8e5f176c2a300c) at /usr/include/c++/11/bits/stl_construct.h:97 97 { return ::new((void*)__location) _Tp(std::forward<_Args>(__args)...); } (gdb) bt #0 0x000055555555e524 in std::construct_at<Job, Job const&> ( __location=0x1e8e5f176c2a300c) at /usr/include/c++/11/bits/stl_construct.h:97 #1 0x000055555555e569 in std::allocator_traits<std::allocator<Job> >::construct<Job, Job const&> (__a=..., __p=0x1e8e5f176c2a300c) at /usr/include/c++/11/bits/alloc_traits.h:518 #2 0x000055555555cff8 in std::vector<Job, std::allocator<Job> >::push_back ( this=0x5555555bd2a0, __x=...) at /usr/include/c++/11/bits/stl_vector.h:1192 #3 0x000055555555ad09 in parse_input ( chains=std::vector of length 2, capacity 2 = {...}) at main.cpp:58 #4 0x000055555555aff8 in main () at main.cpp:85
问题原因
核心问题在于std::vector的内存管理机制:当调用chains.emplace_back()时,如果vector当前容量不足以容纳新元素,会重新分配更大的内存块,将原有元素复制/移动到新内存后释放旧内存。这会导致之前存储在chain_map中的Chain*指针变成悬空指针——指向已被释放的内存区域。后续通过这些悬空指针访问Chain对象的jobs vector时,就会触发段错误。
以测试输入为例:
- 处理第一行
1,30,23时,chains扩容(初始容量为0,添加第一个元素后容量变为1),chain_map[23]存储第一个Chain对象的地址; - 处理第二行
2,23,3时,chains再次扩容(容量从1变为2),原有第一个Chain对象被移动到新内存,而chain_map[23]仍保留旧地址; - 处理第四行
23,50,4时,从chain_map取出旧地址作为chain指针,调用chain->jobs.push_back()时访问失效内存,触发段错误。
解决方案
方案1:使用智能指针存储Chain对象
将vector<Chain>改为vector<unique_ptr<Chain>>,这样即使vector扩容,存储的是指针副本,Chain对象的实际内存地址不会改变,避免悬空指针问题。修改后的核心代码如下:
struct Chain { std::vector<Job> jobs; }; void parse_input(std::vector<std::unique_ptr<Chain>>& chains) { std::string line; std::getline(std::cin, line); std::map<int, Chain*> chain_map; std::regex job_regex("([0-9]+),([0-9]+),([0-9]+)"); while (getline(std::cin, line)) { std::cout << line << '\n' << std::flush; std::smatch matches; if (std::regex_match(line, matches, job_regex)) { int job_id = std::stoi(matches[1].str()); auto res = chain_map.find(job_id); Chain* chain; if (res == chain_map.end()) { chains.emplace_back(std::make_unique<Chain>()); chain = chains.back().get(); } else { chain = res->second; } Job job(std::stoi(matches[1].str()), std::stoi(matches[2].str()), std::stoi(matches[3].str())); chain->jobs.push_back(job); chain_map[job.next_job_id_] = chain; } else { std::cout << "Malformed input\n"; } std::cout << chains.size() << '\n'; } } int main() { std::vector<std::unique_ptr<Chain>> chains; parse_input(chains); return 0; }
方案2:提前预留足够容量
如果能预估chains需要存储的Chain对象数量,可调用chains.reserve(n)提前分配足够内存,避免后续扩容。比如测试输入最多需要3个Chain对象,可在parse_input开头添加chains.reserve(5);(预留余量),这样emplace_back不会触发扩容,指针始终有效。但该方法灵活性差,无法适配未知数量的输入。
方案3:调整逻辑,先构建映射再填充Job
先遍历所有输入,收集所有需要创建的Chain的ID,一次性创建所有Chain对象并建立映射,再再次遍历输入填充Job。该方法需额外存储输入行,逻辑稍复杂,但也能避免指针失效问题。
验证
使用方案1修改后的代码运行测试输入,不会再触发段错误,可正确将所有Job归类到对应的Chain中。
内容的提问来源于stack exchange,提问作者postelrich

