天天看点

Qt中的隐式数据共享、QSharedDataPointer、QExplicitlySharedDataPointer

一、隐式数据共享

Qt 中的许多 C++ 类使用隐式数据共享来最大化资源使用并最小化复制。当作为参数传递时,隐式共享类既安全又高效,因为只传递指向数据的指针,并且只有在函数写入时才复制数据,即写时复制。

共享类由指向共享数据块的指针组成,该指针包含引用计数和数据。

创建共享对象时,它将引用计数设置为 1。每当新对象引用共享数据时,引用计数就会增加,当对象取消引用共享数据时,引用计数会减少。当引用计数变为零时,共享数据将被删除。

在处理共享对象时,有两种复制对象的方法:深拷贝和浅拷贝。

  • 深拷贝意味着复制一个对象。
  • 浅拷贝是引用拷贝,即只是指向共享数据块的指针。

就内存和 CPU 而言,进行深度复制可能会很昂贵。制作浅拷贝非常快,因为它只涉及设置指针和增加引用计数。

隐式共享对象的对象分配(使用 operator=())是使用浅拷贝实现的。

共享的好处是程序不需要不必要地复制数据,从而减少内存使用和数据复制。对象可以很容易地被赋值,作为函数参数发送,并从函数中返回。

在实现自定义的隐式共享类时,请使用 QSharedData 和 QSharedDataPointer 类。

二、QSharedData

QSharedData 旨在与 QSharedDataPointer 或 QExplicitlySharedDataPointer 一起使用,以实现自定义的隐式共享或显式共享类。QSharedData 提供线程安全的引用计数。

三、QSharedDataPointer 

一、描述

QSharedDataPointer<T> 用来结合 QSharedData 实现隐式共享数据类,。

二、类型成员

1、QSharedDataPointer::Type:共享数据对象的类型。d 指针指向这种类型的对象。

三、成员函数

1、构造函数

QSharedDataPointer(QSharedDataPointer<T> &&o)

移动构造一个 QSharedDataPointer 实例。

QSharedDataPointer(const QSharedDataPointer<T> &o)

将 this 的 d 指针设置为 o 中的 d 指针,并增加共享数据对象的引用计数。

QSharedDataPointer(T *data, QAdoptSharedDataTag)

构造一个 QSharedDataPointer,d 指针设置为data。data的引用计数器不增加。

QSharedDataPointer(T *data)

构造一个 QSharedDataPointer,其 d 指针设置为data并增加data的引用计数。

QSharedDataPointer()

构造一个用 nullptr 作为 d 指针初始化的 QSharedDataPointer。

QSharedDataPointer<T> & operator=(QSharedDataPointer<T> &&other)

将其他移动分配给此 QSharedDataPointer 实例。

QSharedDataPointer<T> & operator=(const QSharedDataPointer<T> &o)

将 this 的 d 指针设置为 o 的 d 指针,并增加共享数据对象的引用计数。this 的旧共享数据对象的引用计数递减。如果旧共享数据对象的引用计数变为0,则删除旧共享数据对象。

2、~QSharedDataPointer()

减少共享数据对象的引用计数。 如果引用计数变为 0,则删除共享数据对象。 然后将其销毁。

3、T * clone()

创建并返回当前数据的深拷贝副本。当引用计数大于 1 时,该函数由 detach() 调用以创建新副本。此函数使用运算符 new 并调用类型 T 的拷贝构造函数。

应该为自定义类型声明此函数的模板特化:

template<> EmployeeData *QSharedDataPointer<EmployeeData>::clone()
    {
        return d->clone();
    }
           

4、const T *constData() 

返回一个指向共享数据对象的常量指针。此函数不调用 detach()。

5、T *data() / T *get()

返回指向共享数据对象的指针。此函数调用 detach()。

6、const T *data()  / const T *get() 

返回指向共享数据对象的指针。此函数不调用 detach()。

7、void detach()

如果共享数据对象的引用计数大于 1,则此函数创建共享数据对象的深度副本,并将 this 的 d 指针设置为副本。

如果需要写时复制,则该函数由 QSharedDataPointer 的非常量成员函数自动调用。不需要自己调用它。

8、void reset(T *ptr = nullptr)

将 this 的 d 指针设置为 ptr,如果 ptr 不是 nullptr,则增加 ptr 的引用计数。旧共享数据对象的引用计数递减,如果引用计数达到0,则删除该对象。

9、T *take()

返回指向共享对象的指针,并将其重置为 nullptr。(即此函数将 this 的 d 指针设置为 nullptr)

返回对象的引用计数不会递减。此函数可以与构造函数一起使用,该构造函数采用 QAdoptSharedDataTag 标记对象来传输共享数据对象,而无需干预原子操作。

四、QExplicitlySharedDataPointer

QExplicitlySharedDataPointer用来实现显示共享数据类。显示共享需要手动调用 detach() 来进行数据的拷贝,除此之外这个类的行为和QSharedDataPointer 一致。

ExplicitlySharedDataPointer 的 d 指针总是指向共享数据对象的内部指针。

五、使用示例

5.1、隐式共享

假设要隐式共享一个 员工(Employee) 类:

在头文件中,定义两个类 Employee 和 EmployeeData。

//员工数据
class EmployeeData : public QSharedData
{
  public:
    EmployeeData() : id(-1)
    { }
    EmployeeData(const EmployeeData &other) : QSharedData(other), id(other.id), name(other.name) 
    { }
    ~EmployeeData()
    { }

    int id;
    QString name;
};

//员工
class Employee
{
  public:
    Employee()
    {
        d = new EmployeeData; 
    }
    Employee(int id, const QString &name) 
    {
        d = new EmployeeData;
        setId(id);
        setName(name);
    }
    Employee(const Employee &other)
          : d (other.d)
    {
    }
    void setId(int id) 
    {
        d->id = id; 
    }
    void setName(const QString &name) 
    {
        d->name = name; 
    }

    int id() const { return d->id; }
    QString name() const { return d->name; }

  private:
    QSharedDataPointer<EmployeeData> d;//D指针指向员工信息
};
           

在 Employee 类中,注意单个数据成员,一个 QSharedDataPointer<EmployeeData> 类型的 d 指针。所有对 EmployeeData 的访问都必须经过 d 指针的 operator->()。对于写访问,operator->() 将自动调用 detach(),如果共享数据对象的引用计数大于 1,它会创建共享数据对象的副本。这确保写入一个 Employee 对象不会影响任何 共享相同 EmployeeData 对象的其他 Employee 对象。

EmployeeData 类继承了 QSharedData,它提供了内部引用计数器。EmployeeData 有一个默认构造函数、一个复制构造函数和一个析构函数。通常,在隐式共享类的数据类中只需要这些的简单实现。

请注意,不需要为 Employee 类实现复制构造函数或赋值运算符,因为 C++ 编译器提供的复制构造函数和赋值运算符将逐个成员进行所需的成员浅拷贝。唯一要复制的成员是 d 指针,它是一个 QSharedDataPointer,其 operator=() 只会增加共享 EmployeeData 对象的引用计数。

#include "employee.h"

int main()
{
    Employee e1(1001, "张三");
    Employee e2 = e1;
    e1.setName("李四");
}
           

 在创建第二个员工 e2 并将 e1 分配给它之后,e1 和 e2 都引用了员工 1001 的 张三。两个 Employee 对象都指向 EmployeeData 的同一个实例,它的引用计数为 2。然后 e1.setName("李四") 被调用以更改员工姓名,但由于引用计数大于 1,因此在更改姓名之前执行写入时复制。现在 e1 和 e2 指向不同的 EmployeeData 对象。它们有不同的名称,但都有 ID 1001。

Qt中的隐式数据共享、QSharedDataPointer、QExplicitlySharedDataPointer

 5.2、显式共享

如果将 Employee 类中的 d 指针声明为 QExplicitlySharedDataPointer<EmployeeData>,则使用显式共享并且不会自动执行写操作时的复制(即,在非常量函数中不调用 detach())。在这种情况下,在 e1.setName("Hans Holbein") 之后,员工的姓名已更改,但 e1 和 e2 仍然引用相同的 EmployeeData 实例,因此只有一个员工 ID 为 1001。

Qt中的隐式数据共享、QSharedDataPointer、QExplicitlySharedDataPointer

e1和e2引用相同的示例。

调用detach()复制:

Qt中的隐式数据共享、QSharedDataPointer、QExplicitlySharedDataPointer

5.3、优化 Qt 容器中的使用性能

如果自定义的隐式共享类类似于上面的 Employee 类并使用 QSharedDataPointer 或 QExplicitlySharedDataPointer 作为唯一成员,则应考虑使用 Q_DECLARE_TYPEINFO() 宏将隐式共享类标记为可移动类型。这可以在使用 Qt 的容器类时提高性能和内存效率。

继续阅读