记录 Java 转 C++ 开发时遇到的那些“坑”(二)

1. 将 string 内容拷贝到 char 数组时的注意事项

假设有一天你得到了一个 char 数组,你想把它保存在本地缓存里,于是你想到可以对它进行base64编码,等需要的时候再从缓存文件里读出来,对其进行 base64 解码后,再转成 char 数组。

demo 代码如下:

c++ 复制代码
char key[8] = {1, 2, 3, 4, 5, 6, 7, 8};

string encodeStr = base64_encode(key, sizeof(key));
cout << "encode      =" << encodeStr << endl;//打印下base64编码后的key
string decodeStr = base64_decode(encodeStr);
char tmpKey[8] = {0};
strcpy(tmpKey, decodeStr.c_str());

cout << "encode again=" << base64_encode(tmpKey, sizeof(tmpKey)) << endl;//打印下base64编码后的tmpKey

输出结果:

ini 复制代码
encode      =AQIDBAUGBwg=
encode again=AQIDBAUGBwg=

两次编码的结果一致,没问题。

现在改下原始的 char 数组,将第四个元素改成0:

c++ 复制代码
char key[8] = {1, 2, 3, 0, 5, 6, 7, 8};

string encodeStr = base64_encode(key, sizeof(key));
cout << "encode      =" << encodeStr << endl;//打印下base64编码后的key
string decodeStr = base64_decode(encodeStr);
char tmpKey[8] = {0};
strcpy(tmpKey, decodeStr.c_str());

cout << "encode again=" << base64_encode(tmpKey, sizeof(tmpKey)) << endl;//打印下base64编码后的tmpKey

执行结果:

ini 复制代码
encode      =AQIDAAUGBwg=
encode again=AQIDAAAAAAA=

为什么只是改了一个字符,两次编码的结果反而不相等了呢?

聪明的你应该已经知道原因了,如果一个 string 中间有\0, 那么 c_str() 得到的值其实会做截断。根本原因是 C 语言的字符串实际上是以\0结尾的字符数组。

道理都懂,但是开发的时候稍不注意,这个特性就会在关键的时候背刺你。在我当时参与的业务场景里,这个 char 数组是由另一个 sdk 生成的,大部分情况下生的 char 数组里是没有\0的。一旦出问题,很难排查。

解决办法:改用 std:copy 或者 memcpy :

c++ 复制代码
char key[8] = {1, 2, 3, 0, 5, 6, 7, 8};

string encodeStr = base64_encode(key, sizeof(key));
cout << "encode      =" << encodeStr << endl;
string decodeStr = base64_decode(encodeStr);
char tmpKey[8] = {0};
std::copy(decodeStr.begin(), decodeStr.end(), tmpKey);

cout << "encode again=" << base64_encode(tmpKey, sizeof(tmpKey)) << endl;

2. mutex 是不可重入的

c 复制代码
mutex mutex_;

void demo_mutex1(){
    std::lock_guard<std::mutex> lock1(mutex_);
    std::cout << "Hello, World!" << std::endl;
    std::lock_guard<std::mutex> lock2(mutex_);
    std::cout << "Hello, World!" << std::endl;
}

上述代码会卡住,只打印1次。因为mutex是不可重入的。所以使用时要尤其注意,以防出现死锁。

解决办法1:用代码块。这样 lock_guard 就会在代码作用域结束时,释放所持有的锁。

c 复制代码
void demo_mutex2(){
    std::cout << "Hello, World!" << std::endl;
    {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "Hello, World!" << std::endl;
    }
    {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "Hello, World!" << std::endl;
    }

}

解决办法2:使用可重入锁 recursive_mutex:

c 复制代码
void demo_mutex3(){
    std::lock_guard<std::recursive_mutex> lock1(recursive_mutex_);
    std::cout << "Hello, World!" << std::endl;
    std::lock_guard<std::recursive_mutex> lock2(recursive_mutex_);
    std::cout << "Hello, World!" << std::endl;
}

3. lambda 函数里尝试打印函数名时的一个注意事项

日常开发中一个常见做法是,利用 __FUNCTION__ 获取当前所在的函数名,然后在日志里打印。不过如果日志代码是在 lambda 函数里,__FUNCTION__ 就会失效。 例如:

c 复制代码
void demo_callback() {
    std::cout << "[" << __FUNCTION__ << "]" << "begin " << std::endl;
    getFileMd5("./1.zip", [](const string filePath, const string md5) {
        std::cout << "[" << __FUNCTION__ << "]" << "callback begin"<< std::endl;
        std::cout << "[" << __FUNCTION__ << "]" << "filePath= " << filePath << ", md5=" << md5 << std::endl;
    });
}

打印结果会是:

ini 复制代码
[demo_callback]begin 
[operator()]callback begin
[operator()]filePath= ./1.zip, md5=0a762bb0109079550889a443c7b67769

看来lambda函数里的__FUNCTION__只能拿到"operator()",无法获取外层的真正函数名。

于是我打算在 lambda 函数外把__FUNCTION__暂时存起来,然后再传递给lambda函数内部。

上图中的"Capture 'funcName' by reference"可是CLion给的提示哦,应该不会错吧。点击之后,代码里会自动添加一句&funcName

于是就有了下面的代码:计算文件的 md5,通过回调的方式返回。

c 复制代码
void getFileMd5(const string filePath, CallbackFunction callback) {
    string funcName = __FUNCTION__;
    std::thread th([filePath, callback, &funcName]() {
        cout << "[" << funcName << "]" << "thread begin"<< endl;

        MD5 md5 = MD5();
        auto md5Chars = md5.digestFile(filePath.c_str());
        string md5Str = string(md5Chars);
        cout << "[" << funcName << "]" << "md5Str=" << md5Str << endl;
        callback(filePath, md5Str);
    });
    th.detach();
}


void demo_callback() {
    std::cout << "[" << __FUNCTION__ << "]" << "begin " << std::endl;
    string funcName = __FUNCTION__;
    getFileMd5("./1.zip", [&funcName](const string filePath, const string md5) {
        std::cout << "[" << funcName << "]" << "callback begin" << std::endl;
        std::cout << "[" << funcName << "]" << "filePath= " << filePath << ", md5=" << md5 << std::endl;
    });
}

 

运行结果:

ini 复制代码
[demo_callback]begin 
[]thread begin
[]md5Str=40178e0d50fa8f67f154b4f7c740cf4c
[]callback begin
[]filePath= ./1.zip, md5=40178e0d50fa8f67f154b4f7c740cf4c

奇怪,为什么打印出来的 funcName 会是空字符串呢?

最后我发现,罪魁祸首就是这个 CLion 自动生成的&funcName,解决办法是去掉&,即从引用传递改成值传递。

原因是,在 demo_callback 和 getFileMd5 这两个函数里,lambda 函数的生命周期都比变量 funcName 的生命周期要长。当在lambda 函数里尝试访问 funcName 时,funcName 引用指向的对象其实已经被回收了,这样一来就会出现"引用悬空",可能导致难以预期的结果,甚至可能出现乱码。

纠正后的代码:

c 复制代码
void getFileMd5(const string filePath, CallbackFunction callback) {
    string funcName = __FUNCTION__;
    std::thread th([filePath, callback, funcName]() {
        cout << "[" << funcName << "]" << "thread begin"<< endl;
        MD5 md5 = MD5();
        auto md5Chars = md5.digestFile(filePath.c_str());
        string md5Str = string(md5Chars);
        cout << "[" << funcName << "]" << "md5Str=" << md5Str << endl;
        callback(filePath, md5Str);
    });
    th.detach();
}


void demo_callback() {
    std::cout << "[" << __FUNCTION__ << "]" << "begin " << std::endl;
    string funcName = __FUNCTION__;
    getFileMd5("./1.zip", [funcName](const string filePath, const string md5) {
        std::cout << "[" << funcName << "]" << "callback begin" << std::endl;
        std::cout << "[" << funcName << "]" << "filePath= " << filePath << ", md5=" << md5 << std::endl;
    });
}

输出结果:

ini 复制代码
[demo_callback]begin 
[getFileMd5]thread begin
[getFileMd5]md5Str=d41d8cd98f00b204e9800998ecf8427e
[demo_callback]callback begin
[demo_callback]filePath= ./1.zip, md5=d41d8cd98f00b204e9800998ecf8427e
相关推荐
曙曙学编程10 分钟前
stm32——GPIO
c语言·c++·stm32·单片机·嵌入式硬件
△曉風殘月〆30 分钟前
Visual Studio中的常用调试功能(下)
c++·ide·visual studio·调试
武当豆豆38 分钟前
C++编程学习(第25天)
开发语言·c++·学习
minji...4 小时前
C++ string类(STL简介 , string类 , 访问修改字符)
开发语言·c++
Forward♞4 小时前
Qt——文件操作
开发语言·c++·qt
十五年专注C++开发5 小时前
CMake进阶: CMake Modules---简化CMake配置的利器
linux·c++·windows·cmake·自动化构建
winds~6 小时前
【git】 撤销revert一次commit中的某几个文件
linux·c++
carver w6 小时前
MFC,C++,海康SDK,回调,轮询
开发语言·c++·mfc
m0_552200827 小时前
《UE5_C++多人TPS完整教程》学习笔记43 ——《P44 奔跑混合空间(Running Blending Space)》
c++·游戏·ue5
八个程序员7 小时前
c++计算器(简陋版)
c++·游戏