容器

一、Qt容器类之顺序存储容器
容器Containers,有时候也被称为集合collections,指的是能够在内存中存储其他特定类型的对象的对象,这种对象一般是通用的模板类。C++提供了一套完整的解决方案,成为标准模板库Standard Template Library,也就是我们常说的STL。
 
Qt提供了它自己的一套容器类,这就是说,在Qt的应用程序中,我们可以使用标准C++的STL,也可以使用Qt的容器类。Qt容器类的好处在于,它提供了平台无关的行为,以及 隐式数据共享技术。所谓平台无关,即Qt容器类不因编译器的不同而具有不同的实现;所谓“隐式数据共享”,也可以称作“写时复制copy on write”,这种技术允许在容器类中使用传值参数,而不会发生额外的性能损失。Qt容器类提供了类似Java的遍历器语法,同样也提供了类似STL的遍历器语法,以方便用户选择自己习惯的编码方式。最后一点,在一些嵌入式平台,STL往往是不可用的,这时你就只能使用Qt提供的容器类,除非你想自己创建。
 
今天我们要说的是“顺序储存容器”。所谓顺序存储,就是它存储数据的方式是一个接一个的,线性的。
 
第一个顺序存储容器是QVector<T>,即向量。QVector<T>是一个类似数组的容器,它将数据存储在连续内存区域。同C++数组不同之处在于,QVector<T>知道它自己的长度,并且可以改变大小。对于获取随机位置的数据,或者是在末尾处添加数据,QVector<T>的效率都是很高的,但是,在中间位置插入数据或者删除数据,它的效率并不是很高。在内存中QVector<T>的存储类似下图(出自C++ GUI Programming with Qt4, 2nd Edition):
 
同STL的vector<T>类类似,QVector<T>也提供了[]的重载,我们可以使用[]赋值:
 
QVector< double> v(2);
v[0] = 1.1;
v[1] = 1.2;
 
如果实现不知道vector的长度,可以创建一个空参数的vector,然后使用append()函数添加数据:
 
QVector< double> v;
v.append(1.1);
v.append(1.2);
 
在QVector<T>类中,<<也被重载,因此,我们也可以直接使用<<操作符:
 
QVector< double> v;
v << 1.1 << 1.2;
 
注意,如果QVector<T>中的数据没有被显式地赋值,那么,数据项将使用加入类的默认构造函数进行初始化,如果是基本数据类型和指针,则初始化为0.
 
QLinekdList<T>是另外一种顺序存储容器。在数据结构中,这是一个链表,使用指针连接起所有数据。它的内存分布如下(出自C++ GUI Programming with Qt4, 2nd Edition):
正如数据结构中所描述的那样,QLinkedList<T>的优点是数据的插入和删除很快,但是随机位置值的访问会很慢。与QVector<T>不同,QLinkedList<T>并没有提供重载的[]操作符,你只能使用append()函数,或者<<操作符进行数据的添加,或者你也可以使用遍历器,这个我们将在后面内容中详细描述。
 
QList<T>是一个同时拥有QVector<T>和QLinkedList<T>的大多数优点的顺序存储容器类。它像QVector<T>一样支持快速的随机访问,重载了[]操作符,提供了索引访问的方式;它像QLinkedList<T>一样,支持快速的添加、删除操作。除非我们需要进行在很大的集合的中间位置的添加、删除操作,或者是需要所有元素在内存中必须连续存储,否则我们应该一直使用Qlist<T>。
 
QList<T>有几个特殊的情况。一个是QStringList,这是QList<QString>的子类,提供针对QString的很多特殊操作。QStack<T>和QQueue<T>分别实现了数据结构中的堆栈和队列,前者具有push(), pop(), top()函数,后者具有enqueue(), dequeue(), head()函数。具体情况请查阅API文档。
 
另外需要指出的一点是,我们所说的模板类中的占位符T,可以使基本数据类型,比如int,double等,也可以指针类型,可以是类类型。如果是类类型的话,必须提供默认构造函数,拷贝构造函数和赋值操作符。Qt的内置类中的QByteArray,QDateTime,QRegExp,QString和QVariant是满足这些条件的。但是,QObject的子类并不符合这些条件,因为它们通常缺少拷贝构造函数和赋值操作符。不过这并不是一个问题,因为我们可以存储QObject的指针,而不是直接存储值。T也可以是一个容器,例如:
 
QList<QVector< int> > list;
 
注意,在最后两个>之间有一个空格,这是为了防止编译器把它解析成>>操作符。这个空格是必不可少的,切记切记!
 
下面我们来看一个类(出自C++ GUI Programming with Qt4, 2nd Edition):
 
class Movie
{
public:
        Movie( const QString &title = "", int duration = 0);

         void setTitle( const QString &title) { myTitle = title; }
        QString title() const { return myTitle; }
         void setDuration( int duration) { myDuration = duration; }
        QString duration() const { return myDuration; }

private:
        QString myTitle;
         int myDuration;
};
 
我们能不能把这个类放进Qt容器类呢?答案是肯定的。下面我们来对照着前面所说的要求:第一,虽然这个类的构造函数有两个参数,但是这两个参数都有默认值,因此,像Movie()这种写法是允许的,所以,它有默认构造函数;第二,这个类表面上看上去没有拷贝构造函数和赋值操作符,但是C++编译器会为我们提供一个默认的实现,因此这个条件也是满足的。对于这个类而言,默认拷贝构造函数已经足够,无需我们自己定义。所以,我们可以放心的把这个类放进Qt的容器类。
 
二、Qt容器类之遍历器和隐式数据共享
前面说过,Qt容器类提供了两种遍历器:Java风格的和STL风格的。前者比较容易使用,后者则可以用在一些通过算法中,功能比较强大。
 
对于每一个容器类,都有与之相对应的遍历器:只读遍历器和读写遍历器。只读遍历器有QVectorIterator<T>,QLinkedListIterator<T>和QListIterator<T>三种;读写遍历器同样也有三种,只不过名字中具有一个Mutable,即QMutableVectorIterator<T>,QMutableLinkedListIterator<T>和QMutableListIterator<T>。这里我们只讨论QList的遍历器,其余遍历器具有几乎相同的API。
 
Java风格的遍历器的位置如下图所示(出自C++ GUI Programming with Qt4, 2nd Edition):
 
可以看出,Java风格的遍历器,遍历器不指向任何元素,而是指向第一个元素之前、两个元素之间或者是最后一个元素之后的位置。使用Java风格的遍历器进行遍历的典型代码是:
 
QList< double> list;
// ...
QListIterator< double> i(list);
while (i.hasNext()) {
        doSomethingWith(i.next());
}
 
这个遍历器默认指向第一个元素,使用hasNext()和next()函数从前向后遍历。你也可以使用toBack()函数让遍历器指向最后一个元素的后面的位置,然后使用hasPrevious()和previous()函数进行遍历。
 
这是只读遍历器,而读写遍历器则可以在遍历的时候进行增删改的操作,例如:
 
QMutableListIterator< double> i(list);
while (i.hasNext()) {
         if (i.next() < 0.0)
                i.remove();
}
 
当然,读写遍历器也是可以从后向前遍历的,具体API和前面的几乎相同,这里就不再赘述。
 
对应于Java风格的遍历器,每一个顺序容器类C<T>都有两个STL风格的遍历器:C<T>::iterator和C<T>::const_iterator。正如名字所暗示的那样,const_iterator不允许我们对遍历的数据进行修改。begin()函数返回指向第一个元素的STL风格的遍历器,例如list[0],而end()函数则会返回指向 最后一个之后的元素的STL风格的遍历器,例如如果一个list长度为5,则这个遍历器指向list[5]。下图所示STL风格遍历器的合法位置:
 
 
如果容器是空的,begin()和end()是相同的。这也是用于检测容器是否为空的方法之一,不过调用isEmpty()函数会更加方便。
 
STL风格遍历器的语法类似于使用指针对数组的操作。我们可以使用++和--运算符使遍历器移动到下一位置,遍历器的返回值是指向这个元素的指针。例如QVector<T>的iterator返回值是 T * 类型,而const_iterator返回值是 const T * 类型。
 
一个典型的使用STL风格遍历器的代码是:
 
QList< double>::iterator i = list.begin();
while (i != list.end()) {
        *i = qAbs(*i);
        ++i;
}
 
对于某些返回容器的函数而言,如果需要使用STL风格的遍历器,我们需要建立一个返回值的拷贝,然后再使用遍历器进行遍历。如下面的代码所示:
 
QList< int> list = splitter->sizes();
QList< int>::const_iterator i = list.begin();
while (i != list.end()) {
        doSomething(*i);
        ++i;
}
 
而如果你直接使用返回值,就像下面的代码:
 
// WRONG
QList< int>::const_iterator i = splitter->sizes().begin();
while (i != splitter->sizes().end()) {
        doSomething(*i);
        ++i;
}
 
这种写法一般不是你所期望的。因为sizes()函数会返回一个临时对象,当函数返回时,这个临时对象就要被销毁,因此调用临时对象的begin()函数是相当不明智的做法。并且这种写法也会有性能问题,因为Qt每次循环都要重建临时对象。因此请注意, 如果要使用STL风格的遍历器,并且要遍历作为返回值的容器,就要先创建返回值的拷贝,然后进行遍历。
 
在使用Java风格的只读遍历器时,我们不需要这么做,因此系统会自动为我们创建这个拷贝,所以,我们只需很简单的按下面的代码书写:
 
QListIterator< int> i(splitter->sizes());
while (i.hasNext()) {
        doSomething(i.next());
}
 
这里我们提出要建立容器的拷贝,似乎是一项很昂贵的操作。其实并不然。还记得我们上节说过一个隐式数据共享吗?Qt就是使用这个技术,让拷贝一个Qt容器类和拷贝一个指针那么快速。如果我们只进行读操作,数据是不会被复制的,只有当这些需要复制的数据需要进行写操作,这些数据才会被真正的复制,而这一切都是自动进行的,也正因为这个原因,隐式数据共享有时也被称为“写时复制”。隐式数据共享不需要我们做任何额外的操作,它是自动进行的。隐式数据共享让我们有一种可以很方便的进行值返回的编程风格:
 
QVector< double> sineTable()    
{    
                QVector< double> vect(360);    
                 for ( int i = 0; i < 360; ++i)    
                                vect[i] = std::sin(i / (2 * M_PI));    
                 return vect;    
}
// call
QVector< double> v = sineTable();
 
Java中我们经常这么写,这样子也很自然:在函数中创建一个对象,操作完毕后将其返回。但是在C++中,很多人都会说,要避免这么写,因为最后一个return语句会进行临时对象的拷贝工作。如果这个对象很大,这个操作会很昂贵。所以,资深的C++高手们都会有一个STL风格的写法:
 
void sineTable(std::vector< double> &vect)    
{    
                vect.resize(360);    
                 for ( int i = 0; i < 360; ++i)    
                                vect[i] = std::sin(i / (2 * M_PI));    
}
// call
QVector< double> v;
sineTable(v);
 
这种写法通过传入一个引用避免了拷贝工作。但是这种写法就不那么自然了。而隐式数据共享的使用让我们能够放心的按照第一种写法书写,而不必担心性能问题。
 
Qt所有容器类以及其他一些类都使用了隐式数据共享技术,这些类包括QByteArray, QBrush, QFont, QImage, QPixmap和QString。这使得这些类在参数和返回值中使用传值方式相当高效。
 
不过,为了正确使用隐式数据共享,我们需要建立一个良好的编程习惯。这其中之一就是, 对list或者vector使用at()函数而不是[]操作符进行只读访问。原因是[]操作符既可以是左值又可以是右值,这让Qt容器很难判断到底是左值还是右值,而at()函数是不能作为左值的,因此可以进行隐式数据共享。另外一点是,对于begin(),end()以及其他一些非const容器,在数据改变时Qt会进行深复制。为了避免这一点, 要尽可能使用const_iterator, constBegin()和constEnd().
 
最后,Qt提供了一种不使用遍历器进行遍历的方法:foreach循环。这实际上是一个宏,使用代码如下所示:
 
QLinkedList<Movie> list;
Movie movie;
...
foreach (movie, list) {
         if (movie.title() == "Citizen Kane") {
                std::cout << "Found Citizen Kane" << std::endl;
                 break;
        }
}
 
很多语言,特别是动态语言,以及Java 1.5之后,都有foreach的支持。Qt中使用宏实现了foreach循环,有两个参数,第一个是单个的对象,成为遍历对象,相当于指向容器元素类型的一个指针,第二个是一个容器类。它的意思很明确:每次取出容器中的一个元素,赋值给前面的遍历元素进行操作。需要注意的是, 在循环外面定义遍历元素,对于定义中具有逗号的类而言,如QPair<int, double>,是唯一的选择
 
三、Qt容器类之关联存储容器
今天我们来说说Qt容器类中的关联存储容器。所谓关联存储容器,就是容器中存储的一般是二元组,而不是单个的对象。二元组一般表述为<Key-Value>,也就是“键-值对”。
 
首先,我们看看数组的概念。数组可以看成是一种<int-Object>形式的键-值对,它的Key只能是int,而值的类型是Object,也就是任意类型(注意,这里我们只是说数组可以是任意类型,这个Object并不必须是一个对象)。现在我们扩展数组的概念,把Key也做成任意类型的,而不仅仅是int,这样就是一个关联容器了。如果学过数据结构,典型的关联容器就是散列(Hash Map,哈希表)。Qt提供两种关联容器类型:QMap<K, T>和QHash<K, T>。
 
QMap<K, T>是一种键-值对的数据结构,它实际上使用跳表skip-list实现,按照K进行升序的方式进行存储。使用QMap<K, T>的insert()函数可以向QMap<K, T>中插入数据,典型的代码如下:
 
QMap<QString, int> map;
map.insert( "eins", 1);
map.insert( "sieben", 7);
map.insert( "dreiundzwanzig", 23);
 
同样,QMap<K, T>也重载了[]运算符,你可以按照数组的复制方式进行使用:
 
map[ "eins"] = 1;
map[ "sieben"] = 7;
map[ "dreiundzwanzig"] = 23;
 
[]操作符同样也可以像数组一样取值。但是请注意,如果在一个非const的map中,使用[]操作符取一个不存在的Key的值,则这个Key会被自动创建,并将其关联的value赋予一个空值。如果要避免这种情况,请使用QMap<K, T>的value()函数:
 
int val = map.value( "dreiundzwanzig");
 
如果key不存在,基本类型和指针会返回0,对象类型则会调用默认构造函数,返回一个对象,与[]操作符不同的是,value()函数不会创建一个新的键-值对。如果你希望让不存在的键返回一个默认值,可以传给value()函数第二个参数:
 
int seconds = map.value( "delay", 30);
 
这行代码等价于:
 
int seconds = 30;
if (map.contains( "delay"))
        seconds = map.value( "delay");
 
QMap<K, T>中的K和T可以是基本数据类型,如int,double,可以是指针,或者是拥有默认构造函数、拷贝构造函数和赋值运算符的类。并且K必须要重载<运算符,因为QMap<K, T>需要按K升序进行排序。
 
QMap<K, T>提供了keys()和values()函数,可以获得键的集合和值的集合。这两个集合都是使用QList作为返回值的。
 
Map是单值类型的,也就是说,如果一个新的值分配给一个已存在的键,则旧值会被覆盖。如果你需要让一个key可以索引多个值,可以使用QMultiMap<K, T>。这个类允许一个key索引多个value,如:
 
QMultiMap< int, QString> multiMap;
multiMap.insert(1, "one");
multiMap.insert(1, "eins");
multiMap.insert(1, "uno");

QList<QString> vals = multiMap.values(1);
 
QHash<K, T>是使用散列存储的键-值对。它的接口同QMap<K, T>几乎一样,但是它们两个的实现需求不同。QHash<K, T>的查找速度比QMap<K, T>快很多,并且它的存储是不排序的。对于QHash<K, T>而言,K的类型必须重载了==操作符,并且必须被全局函数qHash()所支持,这个函数用于返回key的散列值。Qt已经为int、指针、QChar、QString和QByteArray实现了qHash()函数。
 
QHash<K, T>会自动地为散列分配一个初始大小,并且在插入数据或者删除数据的时候改变散列的大小。我们可以使用reserve()函数扩大散列,使用squeeze()函数将散列缩小到最小大小(这个最小大小实际上是能够存储这些数据的最小空间)。在使用时,我们可以使用reserve()函数将数据项扩大到我们所期望的最大值,然后插入数据,完成之后使用squeeze()函数收缩空间。
 
QHash<K, T>同样也是单值类型的,但是你可以使用insertMulti()函数,或者是使用QMultiHash<K, T>类来为一个键插入多个值。另外,除了QHash<K, T>,Qt也提供了QCache<K, T>来提供缓存,QSet<K>用于仅存储key的情况。这两个类同QHash<K, T>一样具有K的类型限制。
 
遍历关联存储容器的最简单的办法是使用Java风格的遍历器。因为Java风格的遍历器的next()和previous()函数可以返回一个键-值对,而不仅仅是值,例如:
 
QMap<QString, int> map;
...
int sum = 0;
QMapIterator<QString, int> i(map);
while (i.hasNext())
        sum += i.next().value();
 
如果我们并不需要访问键-值对,可以直接忽略next()和previous()函数的返回值,而是调用key()和value()函数即可,如:
 
QMapIterator<QString, int> i(map);
while (i.hasNext()) {
        i.next();
         if (i.value() > largestValue) {
                largestKey = i.key();
                largestValue = i.value();
        }
}
 
Mutable遍历器则可以修改key对应的值:
 
QMutableMapIterator<QString, int> i(map);
while (i.hasNext()) {
        i.next();
         if (i.value() < 0.0)
                i.setValue(-i.value());
}
 
如果是STL风格的遍历器,则可以使用它的key()和value()函数。而对于foreach循环,我们就需要分别对key和value进行循环了:
 
QMultiMap<QString, int> map;
...
foreach (QString key, map.keys()) {
         foreach ( int value, map.values(key)) {
                doSomething(key, value);
        }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值