记录 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
相关推荐
敲键盘的老乡2 分钟前
堆优化版本的Prim
数据结构·c++·算法·图论·最小生成树
码农多耕地呗5 分钟前
trie树-acwing
数据结构·c++·算法
daily_23331 小时前
数据结构——小小二叉树第三幕(链式结构的小拓展,二叉树的创建,深入理解二叉树的遍历)超详细!!!
数据结构·c++·算法
laimaxgg1 小时前
C++特殊类设计(不能被拷贝的类、只能在堆上创建对象的类、不能被继承的类、单例模式)
c++·单例模式
SUN_Gyq1 小时前
什么是 C++ 中的模板特化和偏特化? 如何进行模板特化和偏特化?
开发语言·c++·算法
愿天垂怜1 小时前
【C++】C++11引入的新特性(1)
java·c语言·数据结构·c++·算法·rust·哈希算法
大帅哥_2 小时前
访问限定符
c语言·c++
小林熬夜学编程2 小时前
【Linux系统编程】第五十弹---构建高效单例模式线程池、详解线程安全与可重入性、解析死锁与避免策略,以及STL与智能指针的线程安全性探究
linux·运维·服务器·c语言·c++·安全·单例模式
凯子坚持 c2 小时前
C++之二叉搜索树:高效与美的极致平衡
开发语言·c++
埋头编程~2 小时前
【C++】踏上C++学习之旅(十):深入“类和对象“世界,掌握编程黄金法则(五)(最终篇,内含初始化列表、静态成员、友元以及内部类等等)
java·c++·学习