Qt隐式共享与显式共享

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Amnes1a/article/details/69945878
Qt中的很多C++类都使用了隐式数据共享来最大化资源使用和最小化拷贝代价。隐式共享类在作为参数传递时,不仅安全而且高效,因为只是指向数据的指针被传递了,底层的数据只有在函数向它执行写动作时才会发生拷贝,即写时拷贝。

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

当一个隐式共享类的对象被创建时,它会引用计数设为1。无论何时,当一个新的对象引用这个共享数据时,引用计数会增加,当一个对象解引用这个共享数据时,引用计数会减少。当引用计数变为0时,共享数据会被删除。

当处理共享数据时,通常有两种方式拷贝一个对象。我们通常称它们为深拷贝和浅拷贝。其中,深拷贝意味着复制一个对象;浅拷贝只拷贝引用,即只有指向共享数据的指针被复制。但执行一次深拷贝在内存和CPU方面的代价都是昂贵的。执行一次浅拷贝是非常快的,因为这只牵涉到设置一个指针和增加引用计数。隐式共享对象的赋值(使用operator=)被实现为浅拷贝。

共享的好处就在于程序不必执行不必要的拷贝,这就会促使更少的内存使用和更少的数据复制。这样,对象就可以简单的被赋值,作为函数的参数传递,作为函数的返回值。

隐式共享大多发生的背后,编程人员一般不需要关注它们。但是,隐式共享导致Qt的容器类和STL中的容器类有很大的不同。由于隐式共享,当复制一个容器时,它们其实是共享一份数据的。如下代码所示:

QVector<int> a, b;
a.resize(100000); // make a big vector filled with 0.
QVector<int>::iterator i = a.begin();
b = a;
此处,迭代器i的使用要格外小心,因为它指向了共享数据。如果我们指向*i = 4,我们改变的将会是共享的实体,即会影响到两个容器。
其实,在多线程应用程序中,隐式共享也会发生。从Qt4开始,隐式共享类就可以在线程间安全的拷贝,它们完全是可重入的。在很多人看来,考虑到引用计数的行为,隐式共享和多线程应该是不兼容的概念。但其实,Qt使用了原子引用计数来确保共享数据的完整性,避免了引用计数的潜在的错误。但原子引用计数并不能保证线程安全。当在线程间共享一个隐式共享类的实例时,还是应该使用合适的锁。在对于所有的可重入类来说都是必须的,无论共享或不共享。但是,原子引用计数可以保证线程作用于它自己本地的一个隐式共享类的实例是安全的。我们通常建议使用信号和槽在线程间传递数据,因为这不需要任何显式的锁机制。

当然,除了Qt自带的类的隐式共享,我们还可以使用QSharedData和QSharedDataPointer这两个类来实现我们自己的隐式共享。

如果对象将要被改变并且其引用计数大于1,隐式共享会自动的从共享块中分离该对象。(这经常被称为写时复制)

隐式共享类可以控制它自己的内部数据。在它的要修改数据的成员函数中,它会在修改数据之前自动的分离。但是,请注意,对于容器类的迭代器来说比较特殊,参见上面所讲。

下面,我们以一个员工类为例,来实现一个隐式共享类。步骤如下:

定义类Emplyee,该类只有一个唯一的数据成员,类型为QSharedDataPointer<EmployeeData>。
定义类EmployeeData类,其派生自QSharedData。该类中包含的就是原本应该放在Employee类中的那些数据成员。
类定义如下:
#include <QSharedData>
#include <QString>

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;
};
在Employee类中,要注意这个数据成员d。所有对employee数据的访问都必须经过d指针的operator->()来操作。对于写访问,operator->()会自动的调用detach(),来创建一个共享数据对象的拷贝,如果该共享数据对象的引用计数大于1的话。也可以确保向一个Employee对象执行写入操作不会影响到其他的共享同一个EmployeeData对象的Employee对象。
类EmployeeData继承自QSharedData,它提供了幕后的引用计数。
在幕后,无论何时一个Employee对象被拷贝、赋值或作为参数传,QSharedDataPointer会自动增加引用计数;无论何时一个Employee对象被删除或超出作用域,QSharedDataPointer会自动递减引用计数。当引用计数为0时,共享的EmployeeData对象会被自动删除。

void setId(int id) { d->id = id; }

void setName(const QString &name) { d->name = name; }
在Employee类的非const成员函数中,无论何时d指针被解引用,QSharedDataPointer都会自动的调用detach()函数来确保该函数作用于一个数据拷贝上。并且,在一个成员函数中,如果对d指针进行了多次解引用,而导致调用了多次detach(),也只会在第一次调用时创建一份拷贝。
int id() const { return d->id; }

QString name() const { return d->name; }
但在Employee的const成员函数中,对d指针的解引用不会导致detach()的调用。

还有,没必要为Employee类实现拷贝构造函数或赋值运算符,因为C++编译器提供的拷贝构造函数和赋值运算符的逐成员拷贝就足够了。因为,我们唯一需要拷贝的就是d指针,而该指针是一个QSharedDataPointer,它的operator=()仅仅是递增了共享对象EmployeeData的引用计数。

隐式共享 VS 显式共享
上面讲到的隐式共享,对于Employee类来说可能会有问题。考虑一种情况,如下代码所示:
#include "employee.h"

int main()
{
Employee e1(1001, "Tom");
Employee e2 = e1;
e1.setName("Jerry");
}
在创建e2,并将e1赋值给它后,e1和d2都引用了同一个员工,即Tom,1001。这两个Employee对象指向了同一个EmployeeData实例,所以该实例的引用计数为2。紧接着执行e2.setName("Jerry")来改变员工名字,但因为此时的引用计数大于1,所以会在名字发生改变前执行一次写时复制,使e1和e2指向不同的EmployeeData对象,然后对e1执行名字修改动作。从而导致e1和e2有不同的名字,但有相同的ID,1001,这可能不是我们想要的。当然,如果我们确实想创建出第二个完全不同的员工,那么可以再在e1上调用setId(1002),修改其ID。但是,如果我们就是只想改变员工的名字呢?此时,我们就可以考虑使用显式共享来替代隐式共享。
如果我们在Employee类中的声明d指针时使用的是QExplicitySharedDataPointer<EmployeeData>,那么就是使用了显式共享,写时复制操作就不会自动发生了(即 在 非 const成员函数中不会自动调用detach())。这样一来,e1.setName("Jerry")执行之后,员工的名字被改变了,但e1和e2仍然引用同一个EmployeeData实例,故还是只有一个id为1001的员工。

---------------------
作者:求道玉
来源:CSDN
原文:https://blog.csdn.net/Amnes1a/article/details/69945878
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自www.cnblogs.com/findumars/p/10247625.html