第16章 容器类

16.1 容器类

Qt库提供了一组通用的基于模板的容器类(container classes)。这些容器类可以用来存储指定类型的项目(items),例如,如果需要一个QString类型的可变大小的数组,那么可以使用QList。与STL(Standard Template Library,C++的标准模板库)中的容器类相比,Qt中的容器类更轻量,更安全,更容易使用。

16.1.1 Qt的容器类简介

  1. Qt提供了一些顺序容器:QList、QStack和QQueue。因为这些容器中的数据都是一个接一个线性存储的,所以称为顺序容器。对于大多数应用程序而言,使用最多的而且最好用的是QList,尽管它是一个数组列表,但是可以快速在其头部和尾部进行添加操作。而QStack和QQueue分别提供了后进先出(LIFO)和先进先出(FIFO)语义。
  2. Qt还提供了一些关联容器:QMap、QMultiMap、QHash、QMultiHash和QSet。因为这些容器存储的是<键,值>对,比如QMap<Key, T>,所以称为关联容器。其中"Multi"容器用来支持一个键多个值的情况。

16.1.1.1 QList

  1. QList是一个模板类,它提供了一个列表。QList实际上是一个T类型项目的指针数组,所以它支持基于索引的访问,而且当项目的数目小于1000时,可以实现在列表中间进行快速的插入操作。QList提供了很多方便的接口函数来操作列表中的项目,例如:
    1)插入操作insert();
    2)替换操作replace();
    3)移除操作removeAt();
    4)移动操作move();
    5)交换操作swapItemAt();
    6)在表尾添加项目append();
    7)在表头添加项目prepend();
    8)移除第一个项目removeFirst();
    9)移除最后一个项目removeLast();
    10)从列表中移除一项并获取这个项目takeAt(),还有相应的takeFirst()和takeLast();
    11)获取一个项目的索引indexOf();
    12)判断是否含有相应的项目contains();
    13)获取一个项目出现的次数count()。
  2. 对于QList,可以使用"<<"操作符来向列表中插入项目,也可以使用"[ ]"操作符通过索引来访问一个项目,其中项目是从0开始编号的。不过,对于只读的访问,另一种方法是使用at()函数,它比"[ ]"操作符要快很多。
cpp 复制代码
QList<QString> list;
list << "aa" << "bb" << "cc"; // 插入项目
if(list[1] == "bb") list[1] = "ab";
list.replace(2,"bc");         // 将"cc"换为"bc"
qDebug() << "the list is: ";  // 输出整个列表
for(int i=0; i<list.size(); ++i){
    qDebug() << list.at(i);   // 现在列表为aa ab bc
}
list.append("dd");            // 在列表尾部添加
list.prepend("mm");           // 在列表头部添加
QString str = list.takeAt(2); // 从列表中删除第3个项目,并获取它
qDebug() << "at(2) item is: " << str;
qDebug() << "the list is: ";
for(int i=0; i<list.size(); ++i)
{
    qDebug() << list.at(i);   // 现在列表为mm aa bc dd
}

16.1.1.2 QMap

  1. QMap类是一个容器类,它提供了一个基于跳跃列表的字典(a skip-list-based dictionary)。QMap<Key,T>是Qt的通用容器类之一,它存储(键,值)对并提供了与键相关的值的快速查找。QMap中提供了很多方便的接口函数,例如:
    1)插入操作insert();
    2)获取值value();
    3)是否包含一个键contains();
    4)删除一个键remove();
    5)删除一个键并获取该键对应的值take();
    6)清空操作clear();
  2. 可以使用"[ ]"操作符插入一个键值对或者获取一个键的值,不过当使用该操作符获取一个不存在的键的值时,会默认向map中插入该键,为了避免这个情况,可以使用value()函数来获取键的值。当使用value()函数时,如果指定的键不存在,那么默认会返回0,可以在使用该函数时提供参数来更改这个默认返回的值。QMap默认是一个键对应一个值的,对于一键多值的情况,更方便的是使用QMap的子类QMultiMap。
cpp 复制代码
QMap<QString,int> map;
map["one"] = 1;          // 向map中插入("one",1)
map["three"] = 3;
map.insert("seven",7);   // 使用insert()函数进行插入
// 获取键的值,使用"[ ]"操作符时,如果map中没有该键,那么会自动插入
int value1 = map["six"];
qDebug() << "value1:" << value1;
qDebug() << "contains 'six' ?" << map.contains("six");
// 使用value()函数获取键的值,这样当键不存在时不会自动插入
int value2 = map.value("five");
qDebug() << "value2:" << value2;
qDebug() << "contains 'five' ?" << map.contains("five");
// 当键不存在时,value()默认返回0,这里可以设定该值,比如这里设置为9
int value3 = map.value("nine",9);
qDebug() << "value3:" << value3;

16.1.1.3 嵌套和赋值

  1. 容器也可以嵌套使用,例如QMap<QString,QList >,这里键的类型是QString,而值的类型是QList,需要注意,在后面的"> >"符号之间要有一个空格,不然编译器会将它当做">>"操作符对待。
  2. 在各种容器中所存储的值的类型可以是任何的可赋值的数据类型,该类型必须提供一个复制构造函数和一个赋值操作运算符,对于一些操作,还需要有一个默认的构造函数。像基本的类型如int和double、指针类型、Qt的数据类型如QString和QDate等,但不包括QObject以及QObject的子类(QWidget、QDialog、QTimer等),不过可以存储这些类的指针,例如QList<QWidget *>。

16.1.2 遍历容器

遍历一个容器可以使用迭代器(iterators)来完成,迭代器提供了一个统一的方法来访问容器中的项目。Qt的容器类提供了两种类型的迭代器。

16.1.2.1 Java风格迭代器

  1. Java风格迭代器在使用时比STL风格迭代器要方便很多,但是在性能上稍微弱于后者。对于每一个容器类,都有两个Java风格迭代器数据类型:一个提供只读访问,一个提供读写访问。
  2. QList示例:
cpp 复制代码
QList<QString> list;
list << "A" << "B" << "C" << "D";
QListIterator<QString> i(list); // 创建列表的只读迭代器,将list作为参数
qDebug() << "the forward is :";
while (i.hasNext())             // 正向遍历列表,结果为A,B,C,D
    qDebug() << i.next(); 
qDebug() << "the backward is :";
while (i.hasPrevious())         // 反向遍历列表,结果为D,C,B,A
     qDebug() << i.previous();

这里先创建了一个QList列表list,然后使用list作为参数创建了一个列表的只读迭代器。这时,迭代器指向列表的第一个项目的前面(这里是指向项目"A"的前面)。然后使用hasNext()函数来检查在该迭代器后面是否还有项目,如果还有项目,那么使用next()来跳过这个项目,next()函数会返回它所跳过的项目。当正向遍历结束后,迭代器会指向列表最后一个项目的后面,这时可以使用hasPrevious()和previous()来进行反向遍历。可以看到,Java风格迭代器是指向项目之间的,而不是直接指向项目。所以,迭代器或者指向容器的最前面,或者指向两个项目之间,或者指向容器的最后面。

  1. QMap示例:
cpp 复制代码
QMap<QString, QString> map;    
map.insert("Paris", "France");    
map.insert("Guatemala City", "Guatemala");    
map.insert("Mexico City", "Mexico");    
map.insert("Moscow", "Russia");    
QMapIterator<QString,QString> i(map);    
while(i.hasNext()) {        
      i.next();        
     qDebug() << i.key() << " : " << i.value();
}   
 if(i.findPrevious("Mexico")) 
         qDebug() << "find 'Mexico'";  // 向前查找键的值 

这里在QMap中存储了一些(首都,国家)键值对,然后删除了包含以"City"字符串结尾的键的项目。对于QMap的遍历,可以先使用next()函数,然后再使用key()和value()来获取键和值的信息。

16.1.2.2 STL风格迭代器

  1. STL风格迭代器兼容Qt和STL的通用算法(generic algorithms),而且在速度上进行了优化。对于每一个容器类,都有两个STL风格迭代器类型:一个提供了只读访问,另一个提供了读写访问。因为只读迭代器比读写迭代器要快很多,所以应尽可能使用只读迭代器。
  2. QList示例
cpp 复制代码
QList<QString> list;
list << "A" << "B" << "C" << "D";
QList<QString>::iterator i;      // 使用读写迭代器
qDebug() << "the forward is :";
for (i = list.begin(); i != list.end(); ++i) {
    *i = (*i).toLower();         // 使用QString的toLower()函数转换为小写
    qDebug() << *i;              // 结果为a,b,c,d
}
qDebug() << "the backward is :";
while (i != list.begin()) {
    --i;
    qDebug() << *i;               // 结果为d,c,b,a
}
QList<QString>::const_iterator j; // 使用只读迭代器
qDebug() << "the forward is :";
for (j = list.constBegin(); j != list.constEnd(); ++j)
    qDebug() << *j;               // 结果为a,b,c,d
  1. STL风格迭代器的API模仿了数组的指针,例如,使用"++"操作符来向后移动迭代器使其指向下一个项目;使用"*"操作符返回迭代器指向的项目等。需要说明的是,不同于Java风格迭代器,STL风格迭代器是直接指向项目的。其中一个容器的begin()函数返回了一个指向该容器中第一个项目的迭代器,end()函数也返回一个迭代器,但是这个迭代器指向该容器的最后一个项目的下一个假想的虚项目,end()标志着一个无效的位置,当列表为空时,begin()函数等价于end()函数。
  2. 在STL风格迭代器中"++"和"--"操作符即可以作为前缀(++i,--i)操作符,也可以作为后缀(i++,i--)操作符。当作为前缀时会先修改迭代器,然后返回修改后的迭代器的一个引用;当作为后缀时,在修改迭代器以前会对其进行复制,然后返回这个复制。如果在表达式中不会对返回值进行处理,那么最好使用前缀操作符(++i,--i),这样会更快一些。对于非const迭代器类型,使用一元操作符"*"获得的返回值可以用在赋值运算符的左侧。STL风格迭代器的常用API如下表所示。
  3. QMap示例:
cpp 复制代码
QMap<QString, int> map;
map.insert("one",1);
map.insert("two",2);
map.insert("three",3);
QMap<QString, int>::const_iterator p;
qDebug() << "the forward is :";
for (p = map.constBegin(); p != map.constEnd(); ++p)
	qDebug() << p.key() << ":" << p.value();// 结果为(one,1),(three,3),(two,2)

这里创建了一个QMap,然后使用STL风格的只读迭代器对其进行了遍历,输出了其中所有项目的键和值。

16.1.2.3 foreach关键字

  1. foreach是Qt向C++语言中添加的一个用来进行容器的顺序遍历的关键字,它使用预处理器来进行实施。 例如:
cpp 复制代码
QList<QString> list;
list.insert(0, "A");
list.insert(1, "B");
list.insert(2, "C");
qDebug() <<"the list is :";
foreach (QString str,list) {    // 从list中获取每一项
    qDebug() << str;            // 结果为A,B,C
}

QMap<QString,int> map;
map.insert("first", 1);
map.insert("second", 2);
map.insert("third", 3);
qDebug() << Qt::endl << "the map is :";
foreach (QString str, map.keys())   // 从map中获取每一个键
    qDebug() << str << " : " << map.value(str);
// 输出键和对应的值,结果为(first,1),(second,2),(third,3)
  1. 注意:从Qt 5.7开始便不再推荐使用该关键字,在新的代码中,更建议使用C++11中基于范围的for循环(range-based for loops)。
cpp 复制代码
const auto list1 = list;
for (const QString &str : list1) 
      qDebug() << "str:" << str;

16.1.3 通用算法

STL风格迭代器可以与STL的标准算法一起使用。

cpp 复制代码
QStringList list;
list << "one" << "two" << "three";
qDebug() << QObject::tr("std::copy算法:");
QList<QString> list0(3);
// 将list中所有项目复制到list0中
std::copy(list.begin(), list.end(), list0.begin());
qDebug() << list0; //结果为one,two,three
qDebug() << Qt::endl << QObject::tr("std::equal算法:");
// 从list开始到结束的所有项目与list0开始及其后面的等数量的项目进行比较,
// 全部相同则返回true
bool ret1 = std::equal(list.begin(), list.end(), list0.begin());
qDebug() << "euqal: " << ret1; //结果为true

16.1.4 QString

QString类提供了一个Unicode(Unicode是一种支持大部分文字系统的国际字符编码标准)字符串。QString存储了一串QChar,而QChar提供了一个16位的Unicode 4.0字符。在后台,QString使用隐式共享(implicit sharing)来减少内存使用和避免不必要的数据拷贝,这也有助于减少存储16位字符的固有开销。

16.1.4.1 隐式共享

  1. 隐式共享(Implicit Sharing)又称为写时复制(copy-on-write)。Qt中很多C++类使用隐式数据共享来尽可能的提高资源使用率和尽可能的减少复制操作。使用隐式共享类作为参数传递是既安全又有效的,因为只有一个指向该数据的指针被传递了,只有当函数向它写入时才会复制该数据。
  2. 共享的好处是程序不需要进行不必要的数据复制,这样可以减少数据的拷贝和使用更少的内存,对象也可以很容易地被分配,或者作为参数被传递,或者从函数被返回。隐式共享在后台进行,在实际编程中不必去关注它。Qt中主要的隐式共享类有:QByteArray、QCursor、QFont、QPixmap、QString、QUrl、QVariant和所有的容器类等等 。
cpp 复制代码
QPixmap p1, p2;
p1.load("image.bmp");
p2 = p1;                        // p1与p2共享数据
QPainter paint;
paint.begin(&p2);               // p2被修改
paint.drawText(0,50, "Hi");
paint.end();

一个共享类由一个指向一个共享数据块的指针和数据组成,在共享数据块中包含了一个引用计数。当一个共享对象被建立时,会设置引用计数为1,例如这里QPixmap类是一个隐式共享类,开始时p1和p2的引用计数都为1。每当有新的对象引用了共享数据时引用计数都会递增,而当有对象不再引用这个共享数据时引用计数就会递减,当引用计数为0时,这个共享数据就会被销毁掉。例如这里执行了"p2 = p1;"语句后,p2便与p1共享同一个数据,这时p1的引用计数为2,而p2的引用计数为0,所以p2以前指向的数据结构将会被销毁掉。

  1. 当处理共享对象时,有两种复制对象的方法:深拷贝(deep copy)和浅拷贝(shallow copy)。
    1)深拷贝意味着复制一个对象,而浅拷贝则是复制一个引用(仅仅是一个指向共享数据块的指针)。一个深拷贝是非常昂贵的,需要消耗很多的内存和CPU资源;而浅拷贝则非常快速,因为它只需要设置一个指针和增加引用计数的值。
    2)当隐式共享类使用"="操作符时就是使用浅拷贝,如上面的"p2 = p1;"语句。但是当一个对象被修改时,就必须进行一次深拷贝,比如上面程序中"paint.begin(&p2);"语句要对p2进行修改,这时就要对数据进行深拷贝,使p2和p1指向不同的数据结构,然后将p1的引用计数设为1,p2的引用计数也设为1。

16.1.4.2 编辑操作

  1. 在QString中提供了多个方便的函数来操作字符串,例如:
    1)append()和prepend()分别实现了在字符串后面和前面添加字符串或者字符;replace()替换指定位置的多个字符;
    2)insert()在指定位置添加字符串或者字符;
    3)remove()在指定位置移除多个字符;
    4)trimmed()除去字符串两端的空白字符,这包括'\t'、'\n'、'\v'、'\f'、'\r'和' ';
    5)simplified()不仅除去字符串两端的空白字符,还将字符串中间的空白字符序列替换为一个空格;
    6)split()可以将一个字符串分割为多个子字符串的列表等等。
  2. 对于一个字符串,也可以使用"[ ]"操作符来获取或者修改其中的一个字符,还可以使用"+"操作符来组合两个字符串。在QString类中一个null字符串和一个空字符串并不是完全一样的。一个null字符串是使用QString的默认构造函数或者在构造函数中传递了0来初始化的字符串;而一个空字符串是指大小为0的字符串。一般null字符串都是空字符串,但一个空字符串不一定是一个null字符串,在实际编程中一般使用isEmpty()来判断一个字符串是否为空。
cpp 复制代码
QString str = "hello";
qDebug() << QObject::tr("字符串大小:") << str.size(); // 大小为5
str[0] = QChar('H');      // 将第一个字符换为'H'
qDebug() << QObject::tr("第一个字符:") << str[0]; // 结果为'H'
str.append(" Qt");        // 向字符串后添加"Qt"
str.replace(1,4,"i");     // 将第1个字符开始的后面4个字符替换为字符串"i"
str.insert(2," my");      // 在第2个字符后插入" my"
qDebug() << QObject::tr("str为:") << str; // 结果为Hi my Qt
str = str + "!!!";        // 将两个字符串组合
qDebug() << QObject::tr("str为:") << str; // 结果为Hi my Qt!!!

str = " hi\r\n Qt!\n  ";
qDebug() << QObject::tr("str为:") << str;
QString str1 = str.trimmed();    // 除去字符串两端的空白字符
qDebug() << QObject::tr("str1为:") << str1;
QString str2 = str.simplified(); // 除去字符串两端和中间多余的空白字符
qDebug() << QObject::tr("str2为:") << str2; //结果为hi Qt!

16.1.4.3 查询操作

  1. 在QString中提供了right()、left()和mid()函数分别来提取一个字符串的最右面,最左面和中间的含有多个字符的子字符串;
  2. 使用indexOf()函数来获取一个字符或者子字符串在该字符串中的位置;
  3. 使用at()函数可以获取一个指定位置的字符,它比"[ ]"操作符要快很多,因为它不会引起深拷贝;
  4. 可以使用contains()函数来判断该字符串是否包含一个指定的字符或者字符串;
  5. 可以使用count()来获得字符串中一个字符或者子字符串出现的次数;
  6. 而使用startsWith()和endsWidth()函数可以用来判断该字符串是否是以一个字符或者字符串开始或者结束的;
  7. 对于两个字符串的比较,可以使用">"和"<="等操作符,也可以使用compare()函数。
cpp 复制代码
qDebug() << Qt::endl << QObject::tr("以下是在字符串中进行查询的操作:") ;
str = "yafeilinux";
qDebug() << QObject::tr("字符串为:") << str;
// 执行下面一行代码后,结果为linux
qDebug() << QObject::tr("包含右侧5个字符的子字符串:") << str.right(5); 
// 执行下面一行代码后,结果为fei
qDebug() << QObject::tr("包含第2个字符以后3个字符的子字符串:") << str.mid(2,3); 
qDebug() << QObject::tr("'fei'的位置:") <<str.indexOf("fei"); //结果为2
qDebug() << QObject::tr("str的第0个字符:") << str.at(0); //结果为y
qDebug() << QObject::tr("str中'i'字符的个数:") << str.count('i'); //结果为2
// 执行下面一行代码后,结果为true
qDebug() << QObject::tr("str是否以"ya"开始?") << str.startsWith("ya"); 

16.1.4.4 转换操作

  1. QString中的toInt()、toDouble()等函数可以很方便的将字符串转换为整型或者double型数据,当转换成功后,它们的第一个bool型参数会为true;
  2. 使用静态函数number()可以将数值转换为字符串,还可以指定要转换为哪种进制;
  3. 使用toLower()和toUpper()函数可以分别返回字符串小写和大写形式的副本。
cpp 复制代码
qDebug() << Qt::endl << QObject::tr("以下是字符串的转换操作:");
str = "100";
qDebug() << QObject::tr("字符串转换为整数:") << str.toInt(); // 结果为100
int num = 45;
qDebug() << QObject::tr("整数转换为字符串:") << QString::number(num);//结果为"45" 
str = "FF";
bool ok;
int hex = str.toInt(&ok,16);   
qDebug() << "ok: "<< ok << QObject::tr("转换为十六进制:") << hex; // 结果为ok:true 255
num = 26;
qDebug() << QObject::tr("使用十六进制将整数转换为字符串:")
               << QString::number(num,16);//结果为1a
str = "123.456";
qDebug() << QObject::tr("字符串转换为浮点型:") << str.toFloat();//结果为123.456
str = "abc";
qDebug() << QObject::tr("转换为大写:") << str.toUpper();// 结果为ABC
str = "ABC";
qDebug() << QObject::tr("转换为小写:") <<str.toLower();// 结果为abc 

16.1.4.5 arg( )函数

cpp 复制代码
int age = 25;
QString name = "yafei";

// name代替%1,age代替%2
str = QString("name is %1, age is %2").arg(name).arg(age);
// 结果为name is yafei,age is 25
qDebug() << QObject::tr("更改后的str为:") << str;

str = "%1 %2";
qDebug() << str.arg("%1f","hello");      // 结果为%1f hello
qDebug() << str.arg("%1f").arg("hello"); // 结果为hellof %2

str = QString("ni%1").arg("hi",5,'*');
qDebug() << QObject::tr("设置字段宽度为5,使用'*'填充:") << str;//结果为ni***hi

qreal value = 123.456;
str = QString("number: %1").arg(value,0,'f',2);
qDebug() << QObject::tr("设置小数点位数为两位:") << str;  //结果为"number:123.45
  1. arg()函数中的参数可以取代字符串中相应的"%1"等标记,在字符串中可以使用的标记在1到99之间,arg()函数会从最小的数字开始对应,比如QString("%5,%2,%7").arg("a").arg("b"),那么"a"会代替"%2","b"会代替"%5",而"%7"会直接显示。
  2. 该函数的另一种重载形式为:arg ( const QString & a, int fieldWidth = 0, const QChar & fillChar = QLatin1Char( ' ' ) ),
    这里可以设定字段宽度,如果第一个参数a的宽度小于fieldWidth的值,那么就可以使用第三个参数设置的字符来进行填充。这里的fieldWidth如果为正值,那么文本是右对齐的,比如前面程序中的结果为"nihi"。而如果为负值,那么文本是左对齐的,例如将前面的程序中的fieldWidth改为-5,那么结果就应该是"nihi"。
  3. arg()另一种重载:arg ( double a, int fieldWidth = 0, char format = 'g', int precision = -1, const QChar & fillChar = QLatin1Char( ' ' ) ),
    它的第一个参数是double类型的,后面的format和precision分别可以指定其类型和精度。

16.1.5 QByteArray和QVariant

16.1.5.1 QByteArray

  1. QByteArray类提供了一个字节数组,它可以用来存储原始字节(包括'\0')和传统的以'\0'结尾的8位字符串。
  2. 使用QByteArray比使用const char * 要方便很多,在后台,它总是保证数据以一个'\0'结尾,而且使用隐式共享来减少内存的使用和避免不必要的数据拷贝。
  3. 除了当需要存储原始二进制数据或者对内存保护要求很高(如在嵌入式Linux上)时,一般都推荐使用QString,因为QString是存储16位的Unicode字符,在应用程序中更容易存储非ASCII和非Latin-1字符,而且QString全部使用的是Qt的API。
  4. QByteArray类拥有和QString类相似的接口函数,除了arg()以外,在QByteArray中都有相似的用法。

16.1.5.2 QVariant

  1. QVariant类像是最常见的Qt的数据类型的一个共用体(union),一个QVariant对象在一个时间只保存一个单一类型的一个单一的值(有些类型可能是多值的,比如字符串列表)。
  2. 可以使用toT()(T代表一种数据类型)函数来将QVariant对象转换为T类型,并且获取它的值。这里toT()函数会复制以前的QVariant对象,然后对其进行转换,所以以前的QVariant对象并不会改变。
  3. QVariant是Qt中一个很重要的类,比如QObject::property()返回的就是QVariant类型的对象。
cpp 复制代码
QVariant v1(15);
qDebug() << v1.toInt();                     // 结果为15

QVariant v2(12.3);
qDebug() << v2.toFloat();                   // 结果为12.3

QVariant v3("nihao");
qDebug() << v3.toString();                  // 结果为"nihao"

QColor color = QColor(Qt::red);
QVariant v4 = color;
qDebug() << v4.type();                      // 结果为QVariant::QColor
qDebug() << v4.value<QColor>();             // 结果为QColor(ARGB 1,1,0,0)

QString str = "hello";
QVariant v5 = str;
qDebug() << v5.canConvert(QMetaType::fromType<int>());  // 方式1,结果为true
qDebug() << v5.canConvert<int>();                       // 方式2,结果为true
qDebug() << v5.toString();                              // 结果为"hello"
qDebug() << v5.convert(QMetaType::fromType<int>());     // 结果为false
qDebug() << v5.toString();                  // 转换失败,v5被清空,结果为"0"
  1. QVariant类的toInt()函数返回int类型的值,toFloat()函数返回float类型的值。
  2. 因为QVariant是Qt Core库的一部分,所以它没有提供对Qt GUI模块中定义的数据类型(例如QColor、QImage、QPixmap等)进行转换的函数,也就是说,这里没有toColor()这样的函数。不过,可以使用QVariant::value()函数或者qvariant_cast()模板函数来完成这样的转换,例如上面程序中对QColor类型的转换。
  3. 对于一个类型是否可以转换为一个特殊的类型,可以使用canConvert()函数来判断,如果可以转换,则该函数返回true。
  4. 也可以使用convert()函数来将一个类型转换为其他不同的类型,如果转换成功则返回true,如果无法进行转换,variant对象将会被清空,并且返回false。
  5. 需要说明,对于同一种转换,canConvert()和convert()函数并不一定返回同样的结果,这通常是因为canConvert()只报告QVariant进行两个类型之间转换的能力。也就是说,如果在提供了合适的数据时,这两个类型间可以进行转换,但是,如果提供的数据不合适,那么转换就会失败,这样convert()的返回值就与canConvert()不同了。例如上面程序中的QString类型的字符串str,当str中只有数字字符时,它可以转换为int类型,比如str = "123",因为它有这个能力,所以canConvert()返回为true。但是,现在str中包含了非数字字符,真正进行转换时会失败,所以convert()返回为false。

16.2 正则表达式

  1. 正则表达式(regular expression),就是在一个文本中匹配子字符串的一种模式(pattern),它可以简写为"regexps",。一个regexp主要应用在以下几个方面:
    1)验证。一个regexp可以测试一个子字符串是否符合一些标准。例如,是一个整数或者不包含任何空格等。
    2)搜索。一个regexp提供了比简单的子字符串匹配更强大的模式匹配。例如,匹配单词mail或者letter,而不匹配单词email或者letterbox。
    3)查找和替换。一个regexp可以使用一个不同的字符串替换一个字符串中所有要替换的子字符串。例如,使用Mail来替换一个字符串中所有的M字符,但是如果M字符后面有ail时不进行替换。
    4)字符串分割。一个regexp可以识别在哪里进行字符串分割。例如,分割制表符隔离的字符串。
  2. 在Qt 5中引入的QRegularExpression类,实现了与Perl兼容的正则表达式,它完全支持Unicode。在QRegularExpression中,一个正则表达式由两部分构成:一个模式字符串和一组模式选项,模式选项用来更改模式字符串的含义。可以在构造函数中直接设置模式字符串:
cpp 复制代码
QRegularExpression re("a pattern");
  1. 也可以使用setPattern()为已有的QRegularExpression对象设置模式字符串:
cpp 复制代码
QRegularExpression re;
re.setPattern("another pattern");
  1. 可以通过pattern()来获取已设置的模式字符串。在QRegularExpression中通过模式选项来改变模式字符串的含义,例如,可以通过设置QRegularExpression::CaseInsensitiveOption使匹配时不区分字母大小写:
cpp 复制代码
QRegularExpression re("Qt rocks", QRegularExpression::CaseInsensitiveOption);

这时除了匹配Qt rocks,还会匹配QT rocks、QT ROCKS、qT rOcKs等字符串。

  1. 也可以通过setPatternOptions()来设置模式选项,通过patternOptions()获取设置的模式选项:
cpp 复制代码
QRegularExpression re("^\\d+$");
re.setPatternOptions(QRegularExpression::MultilineOption);
QRegularExpression::PatternOptions options = re.patternOptions();

模式选项由QRegularExpression::PatternOption枚举类型进行定义,这些选项可以使用按位或操作联合使用。

16.2.1 正则表达式简介

  1. Regexps由表达式(expressions)、量词(quantifiers)和断言(assertions)组成。最简单的一个表达式就是一个字符,例如x和5。而一组字符可以使用方括号括起来,例如[ABC]将会匹配一个A或者一个B或者一个C,这个也可以简写为[A-C],要匹配所有的英文大写字母,就可以使用[A-Z]。
  2. 一个量词指定了必须要匹配的表达式出现的次数。例如,x{1,1}意味着必须匹配且只能匹配一个字符x,而x{1,5}意味着匹配一列字符x,其中至少要包含一个字符x,但是最多包含5个字符x。
  3. 假设要使用一个regexp来匹配0到99之间的整数。因为至少要有一个数字,所以使用表达式 [0-9]{1,1} 开始,它匹配一个单一的数字一次。要匹配0-99,可以想到将表达式最多出现的次数设置为2,即 [0-9]{1,2} 。现在这个regexp已经可以满足假设的需要了,不过,它也会匹配出现在字符串中间的整数。如果想匹配的整数是整个字符串,那么就需要使用断言"^"和"",当 \^ 在regexp中作为第一个字符时,意味着这个regexp必须从字符串的开始进行匹配;当 在regexp中作为最后一个字符时,意味着regexp必须匹配到字符串的结尾。所以,最终的regexp为 [1](#1){1,2}$ 。
  4. 一般可以使用一些特殊的符号来表示一些常见的字符组和量词。例如,[0-9] 可以使用 \d 来替代。而对于只出现一次的量词 {1,1} ,可以使用表达式本身代替,例如x{1,1}等价于x 。所以要匹配0-99,就可以写为 ^\d{1,2} 或者 \^\\d\\d{0,1} 。而 {0,1} 表示字符是可选的,就是只出现一次或者不出现,它可以使用 ?来代替,这样regexp就可以写为 ^\d\d?$ ,它意味着从字符串的开始,匹配一个数字,紧接着是0个或1个数字,再后面就是字符串的结尾。
  5. 现在写一个regexp来匹配单词"mail"或者"letter"其中的一个,但是不要匹配那些包含这些单词的单词,比如"email"和"letterbox"。要匹配"mail",regexp可以写成m{1,1}a{1,1}i{1,1}l{1,1} ,因为 {1,1} 可以省略,所以又可以简写成mail 。下面就可以使用竖线"|"来包含另外一个单词,这里"|"表示"或"的意思。为了避免regexp匹配多余的单词,必须让它从单词的边界进行匹配。首先,将regexp用括号括起来,即 (mail|letter) 。括号将表达式组合在一起,可以在一个更复杂的regexp中作为一个组件来使用,这样也可以方便检测到底是哪一个单词被匹配了。为了强制匹配的开始和结束都在单词的边界上,要将regexp包含在 \b 单词边界断言中,即 \b(mail|letter)\b 。这个 \b 断言在regexp中匹配一个位置,而不是一个字符,一个单词的边界是任何的非单词字符,如一个空格,新行,或者一个字符串的开始或者结束。
  6. 如果想使用一个单词,例如"Mail",替换一个字符串中的字符M,但是当字符M的后面是"ail"的话就不再替换。这样可以使用 (?!E) 断言,例如这里regexp应该写成 M(?!Mail) 。
  7. 如果想统计"Eric"和"Eirik"在字符串中出现的次数,可以使用 \b(Eric|Eirik)\b 或者 \bEi?ri[ck]\b 。这里需要使用单词边界断言'\b'来避免匹配那些包含了这些名字的单词。
cpp 复制代码
QRegularExpression re("^\\d\\d?$");     // 两个字符都必须为数字,第二个可以没有
QString str = "a1";
qDebug() << str.indexOf(re);            // 结果为-1,不是数字开头
str = "5";
qDebug() << str.indexOf(re);            // 结果为0
str = "5b";
qDebug() << str.indexOf(re);            // 结果为-1,第二个字符不是数字

re.setPattern(R"(^\d\d?$)");            // 原始字符串文字无须对反斜杠进行转义
str = "33";
qDebug() << str.indexOf(re);            // 结果为0

re.setPattern("\\b(mail|letter)\\b");   // 匹配mail或者letter单词
str = "emailletter";
qDebug() << str.indexOf(re);            // 结果为-1,mail不是一个单词

re.setPattern("M(?!ail)");              // 匹配字符M,其后面不能跟有ail字符
str = "this is M";
str.replace(re, "Mail");                // 使用"Mail"替换匹配到的字符
qDebug() << "str: " << str;             // 结果为this is Mail
str = "my M,your Ms,his Mail";
qDebug() << str.contains(re);           // 结果为true

16.2.1.1 表达式

  1. 正则表达式中的表达式可以是各种字符和字符组,而一些常用的字符集可以使用一些缩写来表示,如下表所示 。
  2. 对于字符集还有两个特殊的符号"^"和"-":
    1)""在方括号的开始可以表示相反的意思,例如[abc]表示匹配任何字符,但是不匹配'a'或'b'或'c'。
    2)"-"可以表示一个范围的字符,例如[W-Z]表示匹配'W'或者'X'或者'Y'或者'Z'。

16.2.1.2 量词

  1. 默认的,一个表达式将自动量化为{1,1},就是说它应该出现一次。在下表中列出了量词的使用情况,其中E代表一个表达式,一个表达式可以是一个字符,或者一个字符集的缩写,或者在方括号中的一个字符集,或者在括号中的一个表达式。
  2. 使用量词时要注意:
    1)tag+表示匹配一个't'跟着一个'a'然后跟着至少一个'g'。
    2)(tag)+表示匹配"tag"至少一次。
    3)量词一般是贪婪的(greedy),它会尽可能多地去匹配可以匹配的文本,例如,0+匹配它发现的第一个0以及其随后所有连续的0,当应用到字符串20005时,它会匹配其中的3个0。要使量词变得非贪婪(non-greedy,有的地方也称为懒惰lazy),只需在量词后面再添加一个?即可,就会只匹配第一个0。

16.2.1.3 断言

断言在regexp中作出一些有关文本的声明,它们不匹配任何字符。正则表达式中的断言如下表所示,其中E代表一个表达式。

16.2.2 正常匹配和文本捕获

  1. QRegularExpression中提供了match()函数来进行匹配,其第一个参数为const QString &subject,可以通过它来指定要匹配的字符串,该字符串被称为主题字符串(subject string)。match()返回的结果是一个QRegularExpressionMatch对象,可以使用它来检测匹配的结果,例如使用hasMatch()判断是否匹配成功。
  2. 在regexps中使用括号可以使一些元素组合在一起,这样既可以对它们进行量化,也可以捕获它们。例如,使用表达式 mail|letter 来匹配一个字符串,知道有一个单词被匹配了,却不可以知道具体是哪一个,而使用括号就可以捕获被匹配的那个单词,比如使用 (mail|letter) 来匹配字符串"I Sent you some email",这样就可以使用QRegularExpressionMatch类的captured(1)函数来获取捕获的子字符串mail,而(mail|letter)被称为一个捕获组。captured()返回模式字符串中捕获组(模式字符串中每组小括号表示一个捕获组)捕获的子字符串,捕获组从1开始编号,编号为0的是隐式捕获组,它捕获整个正则表达式完全匹配的结果。
  3. 还可以在regexps中使用捕获到的字符串,为了表示捕获到的字符串,使用反向引用 \n,其中n从1开始编号,比如 \1 就表示前面第一个捕获到的字符串。例如,使用\b(\w+)\W+\1\b 在一个字符串中查询重复出现的单词,这意味着先匹配一个单词边界,随后是一个或者多个单词字符,随后是一个或者多个非单词字符,随后是与前面第一个括号中相同的文本,随后是单词边界。
  4. 如果使用括号仅仅是为了组合元素而不是为了捕获文本,那么可以使用非捕获语法,例如 (?:green|blue) 。非捕获括号由"(?:"开始,由")"结束。使用非捕获括号比使用捕获括号更高效,因为regexps引擎只需做较少的工作。
cpp 复制代码
re.setPattern("(mail|letter)(.)");
QRegularExpressionMatch match = re.match("I Sent you some email!");
if (match.hasMatch()) {
    QString matched = match.captured(0);
    QString matched1 = match.captured(1);
    QString matched2 = match.captured(2);
    qDebug() << "matched: " << matched << Qt::endl   // 结果为mail!
             <<"matched1: " << matched1 << Qt::endl  // 结果为mail
             <<"matched2: " << matched2 << Qt::endl; // 结果为!
}
  1. 下面来看一下match()函数的第2个参数offset,用来设置开始匹配的偏移量,例如:
cpp 复制代码
re.setPattern("\\d\\d \\w+");
match = re.match("12 abc 45 def", 1);
qDebug() << match.hasMatch() << match.captured(0); // 结果为:45 def

因为设置了偏移量为1,所以"12 abc"不会被匹配到。

  1. match()函数的第3个参数用来设置匹配类型QRegularExpression::MatchType,默认是正常匹配,还有两个部分匹配类型。
  2. match()函数的第4个参数用来设置匹配选项QRegularExpression::MatchOptions,这些选项可以使用按位或操作联合使用。这里提到了anchoredPattern()函数,使用该函数会返回一个被\A和\z包含的表达式,表明只能在字符串的开头开始匹配,并且只能在字符串的结尾终止匹配,例如anchoredPattern("\d\d \w+")返回\A(?:\d\d \w+)\z。

16.2.3 全局匹配

如果要在主题字符串中查找所有匹配结果,那么使用全局匹配会非常方便。QRegularExpression类中的globalMatch()函数提供了全局匹配,它与match()函数拥有相同的参数,不过其返回值是 QRegularExpressionMatchIterator类对象,这是一个Jave风格迭代器,可以对结果进行遍历。

cpp 复制代码
re.setPattern("(\\w+)");
QRegularExpressionMatchIterator i = re.globalMatch("qt qml quick");
QStringList words;
while (i.hasNext()) {
    QRegularExpressionMatch match = i.next();
    QString word = match.captured(1);
    words << word;
}
qDebug() << "globalMatch: " << words; // 结果为:QList("qt", "qml", "quick")

16.2.4 部分匹配

  1. 当对一个主题字符串进行匹配时,已经到达了主题字符串的末尾,但是只匹配了模式字符串中的一部分,这时可以获得一个部分匹配。要注意,一般部分匹配要比正常匹配效率低很多,因为很多匹配算法的优化将无法使用。
  2. 要使用部分匹配,那么需要在调用match()函数或者globalMatch()函数时指定匹配类型为QRegularExpression::PartialPreferCompleteMatch或者QRegularExpression::PartialPreferFirstMatch。一旦成功进行部分匹配,hasMatch()函数会返回false,但是hasPartialMatch()会返回true,而且无法获取捕获的子字符串,只能通过captured(0)来获取捕获的部分匹配字符串。需要注意,进行部分匹配时也可能导致完全匹配,这时hasMatch()函数会返回true,但是hasPartialMatch()会返回false。
  3. 部分匹配一般应用于两种场景:一个是实时验证用户输入,另一个是增量/多段匹配。

16.2.4.1 实时验证用户输入

  1. 在进行一些指定格式或范围的输入时,希望可以实时跟踪用户输入的内容并进行必要的提示,一般会有3种情况:
    1)输入与正则表达式不可能匹配;
    2)输入与正则表达式完全匹配;
    3)当前输入与正则表达式不是完全匹配,但是如果再输入一些字符则会匹配。
  2. 其实对于验证用户输入,Qt中提供了现成的QValidator验证器类,它就包含了这里提到的3种状态:QValidator::Invalid明显无效、QValidator::Acceptable有效的、QValidator::Intermediate是一个合理的中间值。而QValidator的一个子类QRegularExpressionValidator提供了基于正则表达式的验证器,通过该类就可以方便地实现使用正则表达式实时验证用户输入。
  3. 对于这里提到的第3种情况,需要在输入时进行部分匹配并且进行报告,但后面如果可以完全匹配则报告完全匹配的结果,则可以通过指定匹配类型为QRegularExpression::PartialPreferCompleteMatch来实现。
cpp 复制代码
QRegularExpression rx("^[a-b]\\d{1,3}M$");
QRegularExpressionMatch match = rx.match(arg1, 0, QRegularExpression::PartialPreferCompleteMatch);
bool hasMatch = match.hasMatch();
bool hasPartialMatch = match.hasPartialMatch();
if (hasMatch) {
    ui->label->setText(tr("恭喜,输入的字符完全匹配"));
    ui->label->setStyleSheet("background:green");
    qDebug() << match.captured(0);
} else if(hasPartialMatch) {
    ui->label->setText(tr("输入的字符部分匹配,可以继续输入"));
    ui->label->setStyleSheet("background:yellow");
    qDebug() << match.captured(0);
} else {
    ui->label->setText(tr("输入的字符无效,请重新输入"));
    ui->label->setStyleSheet("background:red");
}

16.2.4.2 增量/多段匹配

这里假设要在一个大文本块里面找到一个子字符串,希望这个大文本块可以分成多个小的文本块给正则表达式引擎,这种情况下,正则表达式引擎需要进行部分匹配并报告,然后可以通过添加新数据进行再次匹配。要实现这样的效果,可以通过指定匹配类型为QRegularExpression::PartialPreferFirstMatch来实现,这时只要发现部分匹配就会报告,不会再尝试其他匹配(即便可以完全匹配)。

cpp 复制代码
re.setPattern("abc|ab");
match = re.match("ab", 0, QRegularExpression::PartialPreferFirstMatch);
qDebug() << match.hasMatch();        // 结果为:false
qDebug() << match.hasPartialMatch(); // 结果为:true
qDebug() << match.captured(0);       // 结果为:ab

16.2.5 通配符匹配

很多命令shell(例如bash和cmd.exe)都支持文件通配符(file globbing),可以使用通配符来识别一组文件。QRegularExpression中提供了wildcardToRegularExpression()函数,可以将通配符模式转换为正则表达式的形式。

cpp 复制代码
QString wildcard = QRegularExpression::wildcardToRegularExpression("*.jpeg");

qDebug() << wildcard;     // 结果为:\\A(?:[^/\\\\]*\\.jpeg)\\z

re.setPattern(wildcard);
match = re.match("foo.jpeg");
qDebug() << match.hasMatch();        // 结果为:true

match = re.match("f_o_o.jpeg");
qDebug() << match.hasMatch();        // 结果为:true

可以看到wildcardToRegularExpression()返回的正则表达式是完全锚定的(即前后分别使用了\A和\z),如果不想返回锚定的正则表达式,可以将其第2个参数指定为QRegularExpression::UnanchoredWildcardConversion。


  1. 0-9 ↩︎
相关推荐
垦利不8 小时前
TS基础篇
开发语言·前端·typescript
人道领域8 小时前
从零实现一个轻量级 RPC 框架:通信协议与动态代理的核心原理
开发语言·网络·qt
fish_xk8 小时前
二叉搜索树
c++
jiushiapwojdap8 小时前
Matlab GUI 界面设计:从入门到实战
开发语言·其他·matlab
lsx2024068 小时前
Go 语言范围(Range)
开发语言
熬夜敲代码的猫8 小时前
C++:让你玩转多态
c++·多态
初心未改HD8 小时前
Go语言同步原语Mutex、WaitGroup、Once深度解析
开发语言·golang
qeen878 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
lynnlovemin8 小时前
C++高精度加减乘除算法详解
开发语言·c++·算法·高精度