引言
- [1. d指针](#1. d指针)
- [2. invokemethod()](#2. invokemethod())
随着AI发展,越来越强大的功能出现,我们只是搬运工。然而,我也想不断地积累自己的经验,提升自己的能力,做一个高级地AI使用者,这就需要一些原始积累,比如基本知识点。这将是一个长久之事。
1. d指针
d指针是实现指针的一种方式,用于隐藏类的实现细节,减少编译依赖,提高编译速度,增强二进制兼容性。
核心思想
d-pointer 的核心思想是:在类的公有接口中只暴露一个指向私有实现类的指针,该私有实现类包含了所有真正的成员变量和私有方法。这样,当实现细节变化时,只要接口不变,使用该类的代码就不需要重新编译。
通常将实现类命名为 Private,将其指针命名为 d_ptr 或 d,因此得名 d-pointer。
使用场景
减少编译依赖:避免头文件中包含大量其他头文件。
增强 ABI 兼容性:可以在不破坏 ABI 的情况下修改实现类的大小和内容。
隐藏实现细节:不暴露私有成员和方法,提高封装性。
使用示例
常规的类实现如下:
cpp
// widget.h
#include <string>
#include <vector>
class Widget {
public:
Widget();
void doSomething();
private:
std::string name;
std::vector<int> data;
int counter;
};
任何包含 widget.h 的源文件都需要知道 std::string 和 std::vector 的具体定义,导致编译缓慢且依赖性强。
使用d指针重构之后实现如下:
widget.h
cpp
// widget.h
#include <memory> // 只需要智能指针
class Widget {
public:
Widget();
~Widget(); // 需要定义,因为 unique_ptr 需要完整类型
void doSomething();
private:
class Private; // 前置声明
std::unique_ptr<Private> d_ptr; // d-pointer
};
widget.cpp
cpp
// widget.cpp
#include "widget.h"
#include <string>
#include <vector>
class Widget::Private {
public:
std::string name;
std::vector<int> data;
int counter = 0;
};
Widget::Widget() : d_ptr(std::make_unique<Private>()) {}
Widget::~Widget() = default; // 必须在 .cpp 中定义,让 unique_ptr 知道如何销毁 Private
void Widget::doSomething() {
auto& d = *d_ptr;
d.name = "hello";
d.data.push_back(42);
++d.counter;
}
支持const方法和子类:
通常定义两个方法来提供const 访问。
cpp
class Widget {
private:
Private* d_func() { return d_ptr.get(); }
const Private* d_func() const { return d_ptr.get(); }
};
#define D_PTR() auto* d = d_func()
#define C_PTR() const auto* d = d_func()
缺点:
外的内存分配:Private 对象在堆上分配(除非使用自定义分配器)。
访问成员需要一层间接:d->member,略微影响性能。
代码稍显繁琐:需要维护两个类。
完整可运行示例
myclass.h
cpp
// myclass.h
#pragma once
#include <memory>
class MyClass {
public:
MyClass();
~MyClass();
void setValue(int x);
int getValue() const;
private:
struct Private;
std::unique_ptr<Private> d;
};
myclass.cpp
cpp
// myclass.cpp
#include "myclass.h"
struct MyClass::Private {
int value = 0;
};
MyClass::MyClass() : d(std::make_unique<Private>()) {}
MyClass::~MyClass() = default;
void MyClass::setValue(int x) {
d->value = x;
}
int MyClass::getValue() const {
return d->value;
}
// main.cpp
#include "myclass.h"
#include <iostream>
int main() {
MyClass obj;
obj.setValue(42);
std::cout << obj.getValue() << std::endl; // 输出 42
return 0;
}
总结
d-pointer(Pimpl)是 C++ 中一种实用的设计模式,用于隔离实现、减少编译依赖、保证 ABI 兼容。通过将私有成员放在一个内部类中,并通过指针持有,实现了接口与实现的分离。在大型项目或库开发中非常有用。
2. invokemethod()
作用
在指定的线程中同步或异步的调用一个对象的槽函数,可调用对象或成员函数,常用于跨线程执行函数或更新UI。
函数原型
static bool invokeMethod(QObject *obj, const char *member,
Qt::ConnectionType type = Qt::AutoConnection,
QGenericReturnArgument ret = QGenericReturnArgument(),
QGenericArgument val0 = QGenericArgument(),
... );
参数:
obj:目标对象,调用线程发生在盖对象所在的线程中;
member:要调用的方法,通常是槽函数;
type:连接方式,分为直接连接,自动连接,队列连接,阻塞队列连接;
ret:返回参数;
val0:传入参数。
使用示例
1.在一个线程中更新另一个线程的界面
invokemethod将一个线程的更新任务放在目标线程所在的时间循环中执行,避免直接访问线程导致崩溃。
cpp
// 在工作线程中更新主线程的标签文本
QMetaObject::invokeMethod(this, [this]() {
ui->label->setText("Done");
}, Qt::QueuedConnection);
2.延迟操作
通过指定队列连接,将一个线程的操作放在目标线程所在的事件循环中,等空闲时执行。
3.动态调用
通过字符串形式的方法名在运行时调用,无需编译时绑定。
使用示例
- 调用无参函数
cpp
// 假设 worker 对象位于另一个线程
QMetaObject::invokeMethod(worker, "doWork", Qt::QueuedConnection);
- 有参调用
cpp
// 调用槽函数 void setValue(int, QString)
int num = 100;
QString str = "hello";
QMetaObject::invokeMethod(receiver, "setValue", Qt::QueuedConnection,
Q_ARG(int, num), Q_ARG(QString, str));
- 使用lambada
cpp
// 不需要注册到元对象系统的函数也能调用
QMetaObject::invokeMethod(this, [this]() {
// 在目标线程执行的代码
updateUI();
}, Qt::QueuedConnection);
- 带返回值,同步阻塞
cpp
QString result;
QMetaObject::invokeMethod(worker, "compute", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QString, result));
使用场景
在工作线程中更新ui线程中的进度条,文本,图标等;
定时器或者异步任务完成后回调;
动态插件系统:通过字符串方式调用未知对象的方法。
注意事项
1.调用的方法必须是槽函数,Q_INVOKABLE 声明的函数,不推荐直接调用信号;
2.自定义的参数类型,必须使用qRegisterMetaType<>()进行元对象的注册;
3.目标对象没有事件循环,则队列连接不会执行;
4.lambda要求目标对象所在的线程有事件循环。