本文用以记录工作过程中 C++ 踩到的一些坑。

# 1、成员函数指针与普通函数指针不同

成员函数需要一个额外的参数(指向类实例的指针 this ),因此不能直接将成员函数传递给普通的函数指针参数。

1
2
3
4
5
6
7
class OtherClass {
public:
typedef function<int(int, int)> callbacktype;
void registerCallBack(callbacktype cb) {
cout << "test: " << cb(2, 4) << endl;
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func1();
int func2(int a, int b);
private:
OtherClass* _otherclass;
};

void MyClass::func1() {
cout << "in func1" << endl;
//wanna set func2 as a callback function into OtherClass
}

int MyClass::func2(int a, int b) {
cout << "in func2" << endl;
return a + b;
}

1
2
3
4
5
6
7
void MyClass::func1() {
cout << "in func1" << endl;
function<int(int, int)> cb = [](int a, int b) {
return a + b;
};
_otherclass->registerCallBack(cb);
}

1
2
3
void MyClass::func1() {
_otherclass->registerCallBack(func2); //error:invalid use of non-static member function
}

1
2
3
4
void MyClass::func1() {
auto cbf = std::bind(&MyClass::func2, this, std::placeholders::_1, std::placeholders::_2);
_otherclass->registerCallBack(cbf);
}

这里解释一下 bind 的作用:

这行代码是使用 C++ 中的 std::bind 函数创建了一个函数对象(function object),并将其分配给了名为 cbf 的变量。这个函数对象的作用是将 MyClass 类中的成员函数 func2 绑定到当前实例( this 指针),并且允许传递两个参数。

在 C++ 中,类的成员函数通常需要一个指向类实例的指针,通常称为 this 指针,以访问该实例的成员变量和其他成员函数。当您在成员函数内部使用 this 关键字时,它指向当前对象的地址。因此,绑定到当前实例意味着创建一个函数对象,该函数对象将包含指向当前对象的 this 指针,以便在后续调用该函数对象时,能够访问当前对象的成员函数和成员变量。

std::bind 函数的第一个参数 &MyClass::func2 是要绑定的成员函数的指针,而第二个参数 this 是指向当前对象的指针。这样,创建的函数对象 cbf 就可以在以后的调用中像普通函数一样使用,而它内部会使用正确的 this 指针来访问当前对象的成员函数 func2 和其他成员。

这种绑定技术非常有用,因为它允许您将成员函数作为回调传递给其他函数或对象,并且在后续调用时,不需要手动传递 this 指针。这可以简化代码并提高代码的可重用性。

1
2
3
4
5
int main() {
MyClass mc;
mc.func1();
return 0;
}

以下是 chatgpt 给出的示例:

成员函数指针和普通函数指针的差异在于成员函数需要额外的参数来指定类的实例,通常是指向类对象的指针,即 this 指针。

在 C++ 中,成员函数指针的类型由类的名称、成员函数名称和函数签名(参数类型和返回类型)决定。例如,对于类 MyClass 的成员函数 void func(int x, int y) ,其成员函数指针的类型为:

1
void (MyClass::*)(int, int)

由于成员函数指针需要额外的参数来传递类的实例,它不能直接传递给普通的函数指针参数。考虑以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public:
void func(int x, int y) {
// ...
}
};

void callFunction(void (*funcPtr)(int, int)) {
// 调用函数指针
funcPtr(10, 20);
}

int main() {
MyClass obj;
callFunction(obj.func); // 错误!成员函数指针不能直接传递给普通函数指针参数
return 0;
}

上面的示例中,我们有一个类 MyClass 和一个普通函数 callFunction ,该函数接受一个普通的函数指针作为参数,并调用它。然后我们在 main 函数中尝试将类的成员函数指针 obj.func 传递给 callFunction ,但这是错误的。因为 obj.func 是成员函数指针,它需要额外的参数 this 来指定类实例,而普通函数指针不支持传递额外的参数。

(其实这里还可以再进一步拓展:为什么明明在传递参数时 “已经指定了具体某个对象”—— 毕竟我们传递的是 obj.func ,为什么还得再多此一举显式地指定?)

为了解决这个问题,C++ 提供了 std::function ,它可以用来包装成员函数指针,并且可以传递额外的参数。修改上面的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <functional>

class MyClass {
public:
void func(int x, int y) {
std::cout << "Result: " << x + y << std::endl;
}
};

void callFunction(std::function<void(MyClass*, int, int)> funcPtr, MyClass* obj, int x, int y) {
// 调用函数指针
funcPtr(obj, x, y);
}

int main() {
MyClass obj;
callFunction(&MyClass::func, &obj, 10, 20); // 正确:使用 std::function 传递成员函数指针和类实例
return 0;
}

在上面的示例中,我们使用 std::function 来包装成员函数指针,并将类实例 obj 作为额外参数传递给了函数指针。这样我们就成功地将成员函数指针传递给了普通函数,并通过 funcPtr 调用了成员函数。注意在 callFunction 中,我们传递了额外的参数 obj、x 和 y 给成员函数指针 funcPtr。(看得出来这种方式很麻烦,不如 bind 来得简洁)

# 2、线程函数未返回导致回收时被阻塞

代码的大致逻辑为:
在主线程中创建某个类 A,调用该类的 start 函数,start 函数会用 pthread_create 创建一个子线程,子线程又会再创建一个子线程。然后主线程阻塞在 getchar 函数,等待按键按下,阻塞结束后调用类 A 的 stop 函数,在该函数中回收前面创建的两个子线程。而在子线程中,循环执行某些内容,直到类 A 的某个 flag 在 stop 中被置零,才跳出 while 循环。(所以我们通过 this 指针传递类 A 的某个 flag)

下面是从项目代码中提取并简化的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include "myClass.h"

int main() {
myClass mc;
mc.start();

getchar();

mc.stop();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef __MYCLASS_H
#define __MYCLASS_H
#include <iostream>
#include <pthread.h>

class myClass {
public:
myClass(){}
void start();
void stop();
private:
bool _isStart = false;
pthread_t _firstThreadHandler;
pthread_t _secondThreadHandler;
static void* firstThread(void* arg);
static void* secondThread(void* arg);
};

#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "myClass.h"
#include <unistd.h>

void myClass::start()
{
std::cout << "myClass start!" << std::endl;
_isStart = true;

pthread_create(&_firstThreadHandler, NULL, &firstThread, (void *)this);
}

void myClass::stop()
{
std::cout << "myClass stop!" << std::endl;
_isStart = false;

pthread_join(_firstThreadHandler, nullptr);
_firstThreadHandler = 0;
pthread_join(_secondThreadHandler, nullptr);
_secondThreadHandler = 0;
}

void* myClass::firstThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
pthread_create(&this_ptr->_secondThreadHandler, NULL, &secondThread, (void *)arg);
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "first" << std::endl;
} return nullptr;
}

void* myClass::secondThread(void* arg)
{
myClass* this_ptr = (myClass*) arg;
while(this_ptr->_isStart) {
usleep(10000);
std::cout << "second" << std::endl;
} return nullptr;
}

上述代码是已经纠正后的正确版本。

而在项目代码中,在修正前遇到的问题是,firstThread 能够正常 join 回收,跳出 while 循环,而 secondThread 却无法正常回收,无法跳出 while 循环。出错的原因是 secondThread 漏了返回,即漏了 return nullptr; 这一句。

但,神奇的是,从现象看就是一直在打印 second ,看似没有跳出 while 循环,但其实已经跳出了,只不过没有 return,那为什么还会一直打印呢?搞不懂。

而且,简化后的示例代码即使漏写 return 语句,也不会出现上述问题。玄学。

# 3、指针作为类内成员变量的初始化问题

对于类内的指针,请务必确保其正确初始化。
要么在定义时直接赋值为 nullptr,要么在类的构造函数中赋值,否则容易出现以下问题:

1
2
3
4
5
6
7
8
{
if(myptr == nullptr) { // 如果我们企图通过指针是否为空来判断是否进行新对象的创建
myptr = new myClass;
} // 那么如果 myptr 没有被初始化赋值,那么其值未定义,是野指针,此时不会走入这个创建逻辑

myptr->callsomefunc(); // 如果此时我们企图利用这个虽然非空但是并不指向对象的野指针调用函数
// 那么我们就会遇到段错误 -- 这里我们没有成功创建对象,于是这里空指针企图调用函数,crash
}

在 C++ 中,未初始化的指针的值是未定义的,它可能是任意值,包括零。这是因为在创建对象时,编译器并不保证为每个成员变量初始化一个特定的值,因此它们的初始状态是不确定的。

即使你没有显式地初始化指针成员变量,它的值也可能每次都是零,这可能是由于编译器或运行时库的一些默认行为。这些默认行为可能会导致未初始化的指针被赋予特定的值,但这并不是标准规定的。

当你修改代码并重新编译后,编译器可能生成了不同的机器代码,或者编译器的优化策略发生了变化,这可能会导致未初始化的指针的值发生变化。在这种情况下,由于未初始化的指针的值本身是未定义的,它可以在不同的编译器、编译选项或运行时环境下表现出不同的行为。

总之,依赖未初始化指针的具体值是一种不好的编程实践,因为它可能导致不可预测的行为。最好的做法是始终显式初始化指针,以确保它具有可预测的初始状态。

# 4、线程函数中访问已销毁的结构体对象中的指针问题

大致场景抽象如下:

我们定义了一个结构体,在结构体中存储了两个指针:

1
2
3
4
struct ThreadParams {
rgbDriver* rgbImpl;
tofDriver* tofImpl;
};

之所以定义这么一个结构体,是因为我们希望同时传递两个指针到一个线程中:

1
2
3
4
5
6
7
8
9
10
ThreadParams threadParams;
threadParams.rgbImpl = _rgb_impl;
threadParams.tofImpl = _tof_impl;

//其中,_rgb_impl 和 _tof_impl 是两个指针

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

然后,我们在线程中去获取这两个指针:

1
2
3
4
5
6
7
8
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

// 其他业务逻辑...
// ...
}

现在问题来了,即使我们在外面传递的指针是空指针,如:

1
2
3
4
5
6
7
8
ThreadParams threadParams;
threadParams.rgbImpl = nullptr;
threadParams.tofImpl = nullptr;

pthread_create(&_mipiTriggerThreadHandle,
NULL,
&mipiTriggerFunc,
static_cast<void*>(pthreadParams));

我们在线程函数 mipiTriggerFunc 中仍然可能得到非空的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* MIPI::mipiTriggerFunc(void* arg) {
ThreadParams* params = static_cast<ThreadParams*>(arg);
rgbDriver* rgbImpl = params->rgbImpl;
tofDriver* tofImpl = params->tofImpl;

if(rgbImpl) {
printf("这一行还是有可能会被打印");
}
if(tofImpl) {
printf("这一行还是有可能会被打印");
}

// 其他业务逻辑...
// ...
}

实际情况就是这两个指针非空,于是造成后续的逻辑出错、混乱,甚至出现段错误,因为后续逻辑试图访问一个野指针的成员变量。

(又或者,即使我们这两个指针都是非空的可用的指针,当我们传递进去之后,在线程函数中得到的仍然会是野指针,同样会导致出错)

之所以造成这个现象,是因为我们传递的是一个临时结构体变量,在线程函数执行过程中,这个临时结构体变量就(可能)被销毁了,导致在线程函数中访问到的是野指针。

所以要解决这个问题也很简单,动态分配内存,使得该结构体对象不会被自动销毁。

1
2
3
4
5
6
ThreadParams* pthreadParams = new ThreadParams;
pthreadParams->rgbImpl = _rgb_impl;
pthreadParams->tofImpl = _tof_impl;

// 记得在线程函数不再使用这两个指针时释放结构体的内存
delete pthreadParams;

更新于

请我喝杯咖啡吧~

Rick 微信支付

微信支付

Rick 支付宝

支付宝