记录 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
相关推荐
QQ同步助手15 分钟前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
qq_4335545426 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能34 分钟前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts
ཌ斌赋ད41 分钟前
FFTW基本概念与安装使用
c++
薄荷故人_1 小时前
从零开始的C++之旅——红黑树封装map_set
c++
悲伤小伞2 小时前
C++_数据结构_详解二叉搜索树
c语言·数据结构·c++·笔记·算法
m0_675988233 小时前
Leetcode3218. 切蛋糕的最小总开销 I
c++·算法·leetcode·职场和发展
code04号6 小时前
C++练习:图论的两种遍历方式
开发语言·c++·图论
煤泥做不到的!7 小时前
挑战一个月基本掌握C++(第十一天)进阶文件,异常处理,动态内存
开发语言·c++
F-2H7 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++