C++嵌入Python循环调用脚本时Py_Finalize()导致崩溃的原因及相关问题咨询
咱们来一步步拆解你遇到的问题,先从最核心的崩溃原因说起:
核心问题:Py_Finalize()为啥会让第二次循环的result变成nullptr?
Py_Finalize()的作用是彻底销毁整个Python解释器的所有运行状态——包括已加载的模块、所有对象实例、内存管理结构,相当于把Python运行时完全关掉了。
你第一次循环调用完Py_Finalize()后,整个Python环境已经被清掉了。第二次循环再调用Py_Initialize()重启解释器时,问题出在你用的pafy和youtube_dl这类扩展模块上:很多C扩展模块在Py_Finalize()之后无法被正确重新初始化,导致第二次执行PyImport_Import(py_module_name)时模块导入失败。
你代码里只检查了py_module是否为nullptr,但没做后续的错误处理——如果模块导入失败,后面获取的function就是nullptr,调用PyObject_CallObject自然会返回nullptr,这就是result变成空指针的原因。
另外要澄清一点:你在函数里声明的result是局部变量,第二次循环时是全新创建的,不是之前的result被影响了,而是当前这次函数调用的流程在模块导入/函数获取环节就失败了,导致result被赋值为nullptr。
子问题1:循环里只调用Py_Initialize()不调用Py_Finalize()会有问题吗?
首先,Python官方推荐的最佳实践是:在整个程序生命周期内只初始化一次解释器——也就是把Py_Initialize()放在main函数开头,Py_Finalize()放在程序结束前,而不是每次循环都初始化/销毁。
如果每次循环都初始化却不销毁,会带来两个主要问题:
- 内存泄漏:每次初始化都会创建新的解释器状态、模块对象等资源,这些资源不会被自动释放,程序跑的时间越长,内存占用越高。
- 状态污染:模块里的全局变量会保留上次运行的状态,可能导致后续调用出现莫名其妙的行为(比如pafy缓存的旧数据影响新请求)。
所以把初始化和销毁移到循环外面是更稳妥的做法。
子问题2:为啥有些只声明一次的对象引用计数大于1?能用Py_SET_REFCNT()吗?
对象引用计数大于1是很正常的,常见原因有这些:
- Python解释器内部会缓存一些常用对象(比如短字符串、小整数),这些对象会被解释器本身持有引用,所以你创建的对象会多一个内部引用。
- 导入的模块会被Python的
sys.modules注册表持有引用,函数对象也会被模块对象持有引用,所以你拿到的py_module和function的引用计数自然会大于1。
至于Py_SET_REFCNT()——**绝对不要手动调用这个函数!**它是Python底层用于初始化对象引用计数的,手动修改会彻底破坏引用计数的正确性,要么导致内存泄漏,要么导致野指针崩溃。正确的做法是始终用Py_XDECREF()/Py_DECREF()来释放你自己持有的引用,解释器会自动处理内部的引用。
额外疑问:PyUnicode_AsWideCharString()的内存释放方式对吗?
你用delete [] play_mrl_wchar是错误的!因为PyUnicode_AsWideCharString()返回的内存是用Python自己的内存分配器分配的,不是C++的new,正确的释放方式是调用PyMem_Free(play_mrl_wchar),否则会导致内存分配器不匹配,引发未定义行为(比如崩溃、内存泄漏)。
修复后的代码示例
我给你调整了代码,解决了上述所有问题:
修改后的C++代码
#include <vector> #include <string> #include <iostream> #include <Python.h> std::string get_play_mrl(const std::string& mrl) { PyObject* py_module_name = PyUnicode_FromString("get_str"); PyObject* py_module = PyImport_Import(py_module_name); if (!py_module) { PyErr_Print(); // 打印具体错误信息,方便调试 std::cout << "Error importing module.\n"; Py_XDECREF(py_module_name); return "Error"; } PyObject* function = PyObject_GetAttrString(py_module, "get_play_url"); if (!function || !PyCallable_Check(function)) { PyErr_Print(); std::cout << "Error getting callable function.\n"; Py_XDECREF(function); Py_XDECREF(py_module); Py_XDECREF(py_module_name); return "Error"; } const char* mrl_c = mrl.c_str(); PyObject* args = PyTuple_Pack(1, PyUnicode_FromString(mrl_c)); PyObject* result = PyObject_CallObject(function, args); if (!result) { PyErr_Print(); std::cout << "Error calling function.\n"; Py_XDECREF(args); Py_XDECREF(function); Py_XDECREF(py_module); Py_XDECREF(py_module_name); return "Error"; } Py_ssize_t size = 0; wchar_t* play_mrl_wchar = PyUnicode_AsWideCharString(result, &size); std::wstring ws(play_mrl_wchar, size); std::string play_mrl_str(ws.begin(), ws.end()); PyMem_Free(play_mrl_wchar); // 用Python的内存释放函数 // 释放所有PyObject引用 Py_XDECREF(py_module_name); Py_XDECREF(py_module); Py_XDECREF(function); Py_XDECREF(args); Py_XDECREF(result); return play_mrl_str; } int main() { // 只初始化一次Python解释器 if (!Py_Initialize()) { std::cout << "Failed to initialize Python interpreter.\n"; return 1; } std::vector<std::string> url_vec = {/* 你的YouTube链接 */}; for (const auto& a : url_vec) { std::string mrl = get_play_mrl(a); // 对mrl执行相关操作... } // 程序结束时销毁解释器 Py_Finalize(); return 0; }
关键修改点
- 将
Py_Initialize()和Py_Finalize()移到main函数首尾,只执行一次,避免重复初始化/销毁带来的问题。 - 增加了完整的错误检查(比如验证function是否可调用、result是否为空),并添加
PyErr_Print()打印具体错误,方便调试。 - 修复了
PyUnicode_AsWideCharString()返回内存的释放方式,改用Python官方的PyMem_Free()。 - 确保在任何错误路径下都能正确释放已创建的PyObject引用,避免内存泄漏。
内容的提问来源于stack exchange,提问作者trofchik

