|
||
![]() |
||
Контейнерные классы -- это универсальные шаблонные классы, предназначенные для хранения элементов заданного типа в смежных областях памяти. Стандарт C++ уже включает в себя большое количество контейнеров, как часть STL (Standard Template Library -- Стандартная Библиотека Шаблонов).
Qt имеет свой набор шаблонных классов. Таким образом, при создании программ разработчик может использовать как контейнерные классы из библиотеки Qt, так и классы из STL. Если вы уже знакомы с контейнерами из STL, то мы не видим веских причин для того, чтобы насильно заставлять себя переходить на использование контейнеров из Qt.
В этой главе мы рассмотрим наиболее важные контейнеры из STL и Qt. Мы так же поближе рассмотрим классы QString и QVariant, которые имеют много общего с контейнерами и в отдельных случаях могут использоваться как альтернатива контейнерам.
Начальные сведения о классах и функциях STL вы найдете по адресу: http://www.sgi.com/tech/stl/.
Классы векторов, списков и словарей (map) -- это шаблонные классы, параметризуемые типом объектов, которые предполагается хранить в контейнере. Значения, которые хранятся в контейнерах, могут быть базового типа (например int или double), указателями или классами, которые имеют конструктор по-умолчанию (конструктор, у которого нет входных аргументов или все входные аргументы имеют значения по-умолчанию), конструктор копирования и перегруженный оператор присваивания. Среди классов, которые отвечают этим требованиям, можно назвать QDateTime, QRegExp, QString и QVariant. Классы Qt, которые наследуют QObject, не могут быть помещены в контейнеры, поскольку у них нет конструктора копирования и оператора присваивания. Однако, это не является большой проблемой, поскольку сохраняется возможность помещать в контейнеры указатели этих типов.
В этом разделе мы рассмотрим наиболее общие операции над векторами, а в следующих двух разделах расскажем о списках и словарях (map). Большая часть примеров, рассматриваемых в этой главе, будет основана на классе Film, который хранит название фильма и его продолжительность. (Мы отказались от более подходящего для этого случая названия Movie, потому что это имя очень похоже на QMovie -- класс Qt, который предназначен для показа анимированных изображений.)
Ниже приводится определение класса Film:
class Film { public: Film(int id = 0, const QString &title = "", int duration = 0); int id() const { return myId; } void setId(int catalogId) { myId = catalogId; } QString title() const { return myTitle; } void setTitle(const QString &title) { myTitle = title; } int duration() const { return myDuration; } void setDuration(int minutes) { myDuration = minutes; } private: int myId; QString myTitle; int myDuration; }; int operator==(const Film &film1, const Film &film2); int operator<(const Film &film1, const Film &film2);Мы не включили в класс явное определение конструктора копирования и оператора присваивания, потому что они предоставляются C++ автоматически. Если бы наш класс выполнял дополнительное резервирование памяти, под данные-члены, тогда нам пришлось бы включить в него явную реализацию конструктора копирования и оператора присваивания.
В дополнение к классу мы реализовали два оператора сравнения -- "равно" и "меньше". Оператор "равно" используется для поиска элемента в контейнере. Оператор "меньше" -- используется для нужд сортировки. В данной ситуации нет необходимости реализовать четыре других оператора сравнения ("!=", "<=", ">", ">="), поскольку STL никогда ими не пользуется.
Ниже приводится исходный код трех функций:
Film::Film(int id, const QString &title, int duration) { myId = id; myTitle = title; myDuration = duration; } int operator==(const Film &film1, const Film &film2) { return film1.id() == film2.id(); } int operator<(const Film &film1, const Film &film2) { return film1.id() < film2.id(); }При сравнивании экземпляров Film, используются их числовые идентификаторы, а не названия, поскольку к названию фильма не предъявляется требование уникальности.
Рисунок 11.1. Вектор экземпляров класса Film.
В STL, класс вектора носит имя std::vector<T> и определен в заголовке <vector>. Объявить вектор, который будет хранить массив экземпляров класса Film, можно так:
vector<Film> films;Эквивалентное объявление, использующее класс Qt -- QValueVector<T>:
QValueVector<Film> films;Вектор, созданный подобным образом, изначально имеет размер 0. Если заранее известно количество элементов в векторе, можно явно указать начальный размер в определении и с помощью оператора "[ ]" присвоить значения его элементам.
Очень удобно заполнять вектор с помощью функции push_back(). Она добавляет указанный элемент в конец вектора, увеличивая его размер на 1:
films.push_back(Film(4812, "A Hard Day's Night", 85)); films.push_back(Film(5051, "Seven Days to Noon", 94)); films.push_back(Film(1301, "Day of Wrath", 105)); films.push_back(Film(9227, "A Special Day", 110)); films.push_back(Film(1817, "Day for Night", 116));Как правило, Qt предоставляет функции с теми же именами, что и STL, но в некоторых случаях Qt добавляет к классам дополнительные методы, с более интуитивно понятными именами. Например, классы Qt могут добавлять элементы как с помощью метода push_back(), так и с помощью дополнительного метода append().
Еще один способ заполнения вектора состоит в том, чтобы задать при объявлении его начальный размер, а потом выполнить инициализацию отдельных элементов:
vector<Film> films(5); films[0] = Film(4812, "A Hard Day's Night", 85); films[1] = Film(5051, "Seven Days to Noon", 94); films[2] = Film(1301, "Day of Wrath", 105); films[3] = Film(9227, "A Special Day", 110); films[4] = Film(1817, "Day for Night", 116);Элементы вектора, которые не были инициализированы явно, приобретают значения, присвоенные конструктором по-умолчанию. В случае базовых типов языка C++ и указателей, начальные значения элементов вектора не определены, аналогично локальным переменным, размещаемым на стеке.
Векторы допускают обход всех элементов в цикле, с использованием оператора "[ ]":
for (int i = 0; i < (int)films.size(); ++i) cerr << films[i].title().ascii() << endl;В качестве альтернативы -- можно использовать итератор:
vector<Film>::const_iterator it = films.begin(); while (it != films.end()) { cerr << (*it).title().ascii() << endl; ++it; }Каждый контейнерный класс имеет два типа итераторов: iterator и const_iterator. Различие между ними заключается в том, что const_iterator не позволяет модифицировать элементы вектора.
Функция-член контейнера -- begin() возвращает итератор, который ссылается на первый элемент в контейнере (например, films[0]). Функция-член контейнера -- end() возвращает итератор, который ссылается на элемент "следующий за последним" (например, films[5]). Если контейнер пуст, значения, возвращаемые функциями begin() и end(), эквивалентны. Это обстоятельство может использоваться для проверки наличия элементов в контейнере, хотя для этой цели гораздо удобнее использовать функцию empty().
Итераторы обладают интуитивно понятным синтаксисом, который напоминает синтаксис указателей языка C++. Для перемещения к следующему или предыдущему элементу, можно использовать операторы "++" b "--", а унарный "*" -- для получения доступа к элементу контейнера, находящемуся в позиции итератора.
Если необходимо отыскать некоторый элемент в векторе, можно воспользоваться функцией STL -- find():
vector<Film>::iterator it = find(films.begin(), films.end(), Film(4812)); if (it != films.end()) films.erase(it);Она возвращает итератор, указывающий на первый встретившийся элемент вектора, отвечающий критериям поиска (элементы контейнера сравниваются перегруженным operator==() с последним аргументом функции). Определение функции находится в заголовке <algorithm>, где вы найдете множество других шаблонных функций. Qt предоставляет аналоги некоторых из них, правда под другими именами (например, qFind()). Вы можете использовать их, если не желаете пользоваться библиотекой STL.
Сортировка элементов вектора может быть произведена функцией sort():
sort(films.begin(), films.end());Для сравнения элементов вектора она использует оператор "<", если явно не указывается другая функция сравнения. На отсортированных векторах, для поиска некоторого элемента может использоваться функция binary_search(). Она дает результат, аналогичный find() (при условии, что в векторе нет двух фильмов с одинаковыми числовыми идентификаторами), но при этом работает намного быстрее.
int id = 1817; if (binary_search(films.begin(), films.end(), Film(id))) cerr << "Found " << id << endl;В позицию итератора, с помощью функции insert(), может быть вставлен новый элемент или удален существующий, с помощью функции erase():
films.erase(it);Элементы, которые следуют за удаляемым будут перемещены на одну позицию влево (или выше, если хотите) и размер вектора будет уменьшен на 1 элемент.
Список (если быть более точным -- связанный список) -- это структура данных, которая может хранить элементы списка в областях памяти с произвольным размещением. В отличие от векторов, списки не предоставляют такого количества способов произвольного доступа к своим элементам, как векторы, но с другой стороны, функции insert() и erase() исполняются очень быстро.
Большинство алгоритмов работы с векторами не применимы к спискам, в особенности это относится к функциям sort() и binary_search(), по причине того, что списки не обладают возможностью быстрого доступа к произвольному элементу. Сортировка STL-списков выполняется функцией sort().
Рисунок 11.2. Список экземпляров класса Film.
list<Film> films;Эквивалентный класс в Qt -- QValueList<T>;:
QValueList<Film> films;Новый элемент может быть добавлен в список вызовом функции push_back() или insert(). В отличие от векторов, вставка элемента в начало или в середину списка выполняется так же быстро, как и добавление элемента в конец списка.
В STL, списки не имеют оператора "[ ]", поэтому, для выбора нужного элемента приходится использовать итераторы. (Списки Qt поддерживают оператор "[ ]", но на больших списках он может работать очень медленно.) Синтаксис и порядок использования аналогичен векторам, например:
list<Film>::const_iterator it = films.begin(); while (it != films.end()) { cerr << (*it).title().ascii() << endl; ++it; }Списки предоставляют практически тот же набор функций, что и векторы, включая empty(), size(), erase() и clear(). Функция find() так же имеется.
Некоторые функции Qt возвращают QValueList<T>. Если есть необходимость пройти в цикле по списку, то нужно создать копию списка и выполнить проход по копии. Ниже представлен пример корректной работы со списком QValueList<int>, который возвращает QSplitter::sizes():
QValueList<int> list = splitter->sizes(); QValueList<int>::const_iterator it = list.begin(); while (it != list.end()) { do_something(*it); ++it; }Следующий код -- неправильный:
// НЕВЕРНО! QValueList<int>::const_iterator it = splitter->sizes().begin(); while (it != splitter->sizes().end()) { do_something(*it); ++it; }Это происходит потому, что QSplitter::sizes() возвращает результат по значению. Если не сохранить его копию, C++ автоматически удалит его еще до того как начнется итерация. Всегда создавайте копию контейнера, возвращаемого по значению, когда требуется получить для него итератор.
На первый взгляд, операция создания копии может показаться ресурсоемкой, но это не так, благодаря тому, что Qt использует оптимизацию, которая называется implicit sharing (неявное совместное использование данных). Суть оптимизации заключается в том, что фактически, операция копирования может и не производиться, не смотря на то, что программа запросила ее.
Класс QStringList, широко используемый в Qt, это дочерний класс QValueList<QString>. Он расширяет набор методов предка своими, дополнительными функциями, которые делают этот класс очень мощным инструментом. Более подробно мы расскажем о нем в последнем разделе этой главы.
Словари предназначены для хранения произвольного количества элементов, в виде пар "ключ-значение". Причем к "ключам" предъявляется требование уникальности. Словари обладают широкими возможностями доступа к произвольным элементам и незначительными накладными расходами на операцию добавления нового элемента. Если в словарь вставляется новое значение по существующему ключу, то оно затирает старое значение в паре "ключ-значение".
Рисунок 11.3. Словарь экземпляров класса Film.
class Film { public: Film(const QString &title = "", int duration = 0); QString title() const { return myTitle; } void setTitle(const QString &title) { myTitle = title; } int duration() const { return myDuration; } void setDuration(int minutes) { myDuration = minutes; } private: QString myTitle; int myDuration; }; Film::Film(const QString &title, int duration) { myTitle = title; myDuration = duration; }В этой версии отсутствует числовой идентификатор фильма, поскольку теперь он будет использоваться в качестве "ключа" в словаре. Кроме того, здесь отсутствуют операторы сравнения -- словари изначально упорядочивают элементы по ключу, но не по значению.
Класс словаря в STL определен под именем std::map<K, T>, в файле заголовка <map>. Ниже приводится пример объявления словаря, с целыми значениями в качестве ключей и Film -- в качестве значения:
map<int, Film> films;Эквивалент в Qt -- QMap<K, T>:
QMap<int, Film> films;Наиболее естесственный способ заполнения словарей -- присваивать значение по заданному ключу:
films[4812] = Film("A Hard Day's Night", 85); films[5051] = Film("Seven Days to Noon", 94); films[1301] = Film("Day of Wrath", 105); films[9227] = Film("A Special Day", 110); films[1817] = Film("Day for Night", 116);Итератор словаря предоставляет возможность доступа к паре "ключ-значение". Ключ извлекается с помощью (*it).first, а значение -- (*it).second:
map<int, Film>::const_iterator it = films.begin(); while (it != films.end()) { cerr << (*it).first << ": " << (*it).second.title().ascii() << endl; ++it; }Большинство компиляторов допускают запись в виде it->first и it->second, но более переносимый вариант, все таки: (*it).first и (*it).second.
Итераторы словарей в Qt несколько отличаются от итераторов словарей в STL. В Qt ключ можно получить с помощью it.key(), а значение -- it.data():
QMap<int, Film>::const_iterator it = films.begin(); while (it != films.end()) { cerr << it.key() << ": " << it.data().title().ascii() < endl; ++it; }При обходе словаря в цикле, элементы словаря всегда упорядочены по значению ключа.
Для доступа к значениям словаря и их изменения может использоваться оператор "[ ]", однако, при попытке получить значение по несуществующему в словаре ключу, будет создан новый элемент словаря с заданным ключом и пустым значением. Чтобы избежать случайного создания пустых элементов, используйте функцию find(), чтобы получить искомый элемент:
map<int, Film>::const_iterator it = films.find(1817); if (it != films.end()) cerr << "Found " << (*it).second.title().ascii() << endl;Эта функция вернет итератор end(), если ключ отсутствует в словаре.
В примере выше, в качестве ключа использовались целые числа, однако, для этих целей могут использоваться и другие типы. Наиболее популярный -- QString, например:
map<QString, QString> actorToNationality; actorToNationality["Doris Day"] = "American"; actorToNationality["Greta Garbo"] = "Swedish";Если необходимо хранить несколько значений с одинаковыми ключами, используйте multimap<K, T>. Если необходимо хранить одни только ключи, используйте set<K> или multiset<K>. Qt не имеет классов, эквивалентных приведенным.
Класс QMap<K, T> имеет несколько дополнительных функций, особенно удобных при работе с небольшими наборами данных. Функции QMap<K, T>::keys() и QMap<K, T>::values() возвращают списки QValueList ключей и значений словаря.
Кроме STL-подобных контейнеров, Qt предоставляет еще целый ряд контейнерных классов. Они были разработаны в начале 90-х годов прошлого века для Qt 1.0, еще до того, как STL стала частью C++, и потому имеют свой характерный синтаксис. Поскольку эти классы оперируют указателями на объекты, их часто называют контейнерами указателей (pointer-based containers), в противоположность более современным контейнерам значений (value-based containers) Qt и STL. В Qt 4 контейнеры указателей еще останутся, для сохранения совместимости, но их использование не будет приветствоваться.
Контейнеры указателей сохраняют свою актуальность лишь благодаря тому, что в Qt 3 еще имеется ряд немаловажных функций, которые работают с ними. Один пример мы приводили в Главе 3, когда выполняли итерации по виджетам, второй -- в Главе 6, когда выполняли итерации по окнам в MDI-приложении.
Основными контейнерами указателей являются классы QPtrVector<T>, QPtrList<T>, QDict<T>, QAsciiDict<T>, QIntDict<T> и QPtrDict<T>.
Класс QPtrVector<T> предназначен для хранения вектора указателей. Ниже приводится пример создания QPtrVector<Film> с пятью элементами:
QPtrVector<Film> films(5); films.setAutoDelete(true); films.insert(0, new Film(4812, "A Hard Day's Night", 85)); films.insert(1, new Film(5051, "Seven Days to Noon", 94)); films.insert(2, new Film(1301, "Day of Wrath", 105)); films.insert(3, new Film(9227, "A Special Day", 110)); films.insert(4, new Film(1817, "Day for Night", 116));Класс QPtrVector<T> не имеет функции append(), поэтому приходится явно указывать индекс для добавляемых элементов. В этом примере использована первая версия класса Film, которая содержит переменную-член -- числовой идентификатор фильма.
Контейнеры указателей в Qt обладают одним замечательным свойством -- "auto-delete" (автоматическое удаление). Если автоудаление разрешено, Qt становится владельцем всех объектов, вставляемых в контейнер и удаляет их автоматически, когда удаляется контейнер (или при вызове методов remove() и clear()).
Для исключения элемента из вектора, должна вызываться функция remove(), с указанием индекса удаляемого элемента:
films.remove(2);Эта функция не изменяет размер вектора, она просто обнуляет указатель с заданным индексом. Если разрешено автоудаление, то автоматически удаляется объект, на который указывал элемент вектора.
Чтобы обойти все элементы вектора в цикле, можно просто использовать индексы:
for (int i = 0; i < (int)films.count(); ++i) { if (films[i]) cerr << films[i]->title().ascii() << endl; }В данном примере сначала выполняется проверка указателя (указатель не должен быть пустым), а затем выполняются все необходимые действия над указателем.
Класс QPtrList<T> предназначен для хранения списка указателей. Добавление новых элементов в QPtrList<T> производится функциями append(), prepend() и insert():
QPtrList<Film> films; films.setAutoDelete(true); films.append(new Film(4812, "A Hard Day's Night", 85)); films.append(new Film(5051, "Seven Days to Noon", 94));Список указателей имеет "текущий" элемент, значение которого изменяется функциями навигации по списку, такими как first(), next(), prev() и last(). Один из способов выполнения прохода по списку:
Film *film = films.first(); while (film) { cerr << film->title().ascii() << endl; film = films.next(); }Однако списки допускают доступ к элементам по индексу:
for (int i = 0; i < (int)films.count(); ++i) cerr << films.at(i)->title().ascii() << endl;Третий возможный вариант обхода списка, заключается в использовании QPtrListIterator<T>.
Классы QDict<T>, QAsciiDict<T>, QIntDict<T> и QPtrDict<T> являются близкими эквивалентами map<K, T>. Эти классы так же хранят пары "ключ-значение". Ключ в них может быть представлен одним из четырех типов: QString, const char *, int и void *, в зависимости от типа используемого класса. Поскольку все четыре класса предоставляют одинаковую функциональность, мы рассмотрим только один из них -- QIntDict<T>.
Для демонстрации воспользуемся второй версией класса Film, которая использовалась ранее, совместно с классом map<K, T>.
QIntDict<Film> films(101); films.setAutoDelete(true);Конструктору передается число, используемое классом для определения количества памяти, которую нужно выделить под элементы словаря. Для улучшения производительности, это число должно быть простым и немного больше, чем количество элементов, которое предполагается вставить в словарь. Список простых чисел, меньших 10 000, вы найдете по адресу: http://doc.trolltech.com/3.2/primes.html.
Вставка нового элемента выполняется функцией insert(), которой передаются ключ и значение:
films.insert(4812, new Film("A Hard Day's Night", 85)); films.insert(5051, new Film("Seven Days to Noon", 94));Для доступа к элементу словаря можно использовать функцию find() или оператор "[ ]". Для удаления элемента -- функцию remove(). Для изменения значения, ассоциированного с заданным ключом -- replace().
Если функция insert() вызывается несколько раз с одним и тем же ключом, доступ будет иметься только к значению, которое было вставлено последним. При вызове remove(), элементы удаляются в обратном порядке. Чтобы избежать вставки нескольких значений с одим и тем же ключом, используйте replace() вместо insert().
Обход элементов контейнера может быть выполнен с помощью итератора:
QIntDictIterator<Film> it(films); while (it.current()) { cerr << it.currentKey() << ": " << it.current()->title().ascii() << endl; ++it; }Текущий ключ итератора может быть получен вызовом currentKey(), а текущее значение -- функцией current(). Порядок следования элементов в словаре не определен.
Для хранения элементов базовых типов языка C++ (int, double и т.п) и структур, Qt предоставляет специальный, вектор-подобный класс QMemArray<T>. В некоторых приложениях он может использоваться напрямую, однако, чаще используются два производных класса QByteArray (QMemArray<char>) и QPointArray (QMemArray<QPoint>). Мы уже использовали их несколько раз в предыдущих главах.
Ниже приводится пример создания QByteArray:
QByteArray bytes(4); bytes[0] = 'A'; bytes[1] = 'C'; bytes[2] = 'D'; bytes[3] = 'C';При создании экземпляра QMemArray<T>, необходимо либо сразу указать начальный размер будущего массива, либо вызвать функцию resize() после создания. Доступ к элементам массива выполняется с помощью оператора "[ ]":
for (int i = 0; i < (int)bytes.size(); ++i) cerr << bytes[i] << endl;Поиск элемента в массиве осуществляется с помощью функции QMemArray<T>::find():
if (bytes.find( A ) != -1) cerr << "Found" < endl;Иногда программисты забывают об одной особенности класса QMemArray<T> и его производных -- они используют то, что называется explicitly shared (явное совместное использование данных). Это означает, что созданные копии объекта (с помощью конструктора копирования или оператором присваивания) ссылаются на одни и те же данные. Когда данные модифицируются с помощью одного объекта, изменения будут видны в другом. Не следует путать явное совместное использование данных (explicitly shared) с неявным совместным использованием данных (implicitly shared), которое лишено данной проблемы.
Избежать описанной проблемы несложно, для этого достаточно выполнить полное копирование объекта вызовом copy():
duplicate = bytes.copy();Теперь два объекта будут ссылаться на различные наборы данных.
Скорее всего, в Qt 4, предпочтение будет отдано классу QValueVector<T>, а классы QByteArray и QPointArray станут его производными.
Строки используются практически во всех программах ничуть не реже других типов.
Язык C++ предоставляет два типа строк: традиционные строки языка C -- массивы символов, завершающиеся символом '\0' и класс string. Qt предоставляет гораздо более мощный класс QString. Он предназначен для хранения строк с 16-ти битными символами Unicode. Unicode содержит наборы символов ASCII и Latin-1 с их обычными числовыми значениями. Но поскольку каждый символ в QString представлен 16-ю битами, он может содержать тысячи других символов. Дополнительную информацию об Unicode вы найдете в Главе 15.
Конкатенация двух строк QString может выполняться двухместным оператором "+" или оператором "+=". Ниже приводится пример использования обоих операторов:
QString str = "User: "; str += userName + "\n";Кроме того, имеется функция QString::append(), которая идентична по своему действию оператору "+=":
str = "User: "; str.append(userName); str.append("\n");И совершенно иной подход к объединению строк состоит в использовании функции QString::sprintf():
str.sprintf("%s %.1f%%", "perfect competition", 100.0);Она поддерживает тот же набор спецификаторов формата, что и библиотечная функция sprintf(). В примере выше, в строку str будет записана строка "perfect competition 100.0%".
Еще один способ "сборки" строки из других строк и чисел -- использовать arg():
str = QString("%1 %2 (%3s-%4s)") .arg("permissive").arg("society").arg(1950).arg(1970);В этом примере "%1" будет заменено словом "permissive", "%2" -- "society", "%3" -- "1950" и "%4" -- "1970". В результате получится строка "permissive society (1950s-1970s)". Класс имеет несколько перегруженных функций arg() для обработки различных типов данных. Некоторые из них имеют дополнительные параметры, управляющие длиной выходной строки, базой системы счисления и точностью представления чисел с плавющей точкой. В большинстве случаев arg() представляет лучшее решение, чем sprintf(), потому что она более безопасна, полностью поддерживает Unicode и позволяет переводчикам изменять порядок следования параметров "%n".
QString позволяет преобразовывать числа в их строковое представление, с помощью статической функции QString::number():
str = QString::number(59.6);или с помощью QString::setNum():
str.setNum(59.6);Обратное преобразование может быть выполнено функциями toInt(), toLongLong(), toDouble() и т.д., например:
bool ok; double d = str.toDouble(&ok);Эти функции могут принимать необязательный аргумент типа bool, в котором возвращается признак успеха преобразования. Если преобразование не может быть выполнено, они всегда возвращают 0.
Зачастую возникает ситуация, когда необходимо извлечь часть строки. Функция mid() возвращает подстроку заданной длины, начиная с заданной позиции в исходной строке. Например, следующий код выводит строку "pays":
QString str = "polluter pays principle"; cerr << str.mid(9, 4).ascii() << endl;Если опустить второй аргумент (или передать в качестве второго аргумента число -1), функция вернет подстроку, начиная с заданной позиции и до конца исходной строки. Например, следующий код выведет строку "pays principle":
QString str = "polluter pays principle"; cerr << str.mid(9).ascii() << endl;Дополнительно имеются функции left() и right(). Они обе принимают количество символов n и возвращают первые или последние n символов исходной строки, соответственно. Например, следующий код выведет строку "polluter principle":
QString str = "polluter pays principle"; cerr << str.left(8).ascii() << " " << str.right(9).ascii() << endl;Если нужно выполнить проверку -- начинается ли или заканчивается ли строка определенной комбинацией символов, для этих целей существуют функции startsWith() и endsWith():
if (uri.startsWith("http:") && uri.endsWith(".png")) ...Это гораздо быстрее и проще, чем:
if (uri.left(5) == "http:" && uri.right(4) == ".png") ...Оператор сравнения строк "==" чувствителен к регистру символов. Для выполнения регистронезависимого сравнения, можно воспользоваться функциями upper() или lower(), например:
if (fileName.lower() == "readme.txt") ...Для замены одной подстроки в строке другой подстрокой, используйте функцию replace():
QString str = "a sunny day"; str.replace(2, 5, "cloudy");в результате получится строка "a cloudy day". То же самое действие может выполнено с помощью функций remove() и insert():
str.remove(2, 5); str.insert(2, "cloudy");В первой строке удаляется пять символов, начиная со 2-й позиции, в результате получается строка "a day" (с двумя пробелами), затем, во второую позицию вставляется слово "cloudy".
Существуют перегруженные версии функции replace(), которые заменяют все вхождения первого аргумента на второй. Например, чтобы заменить все символы '&' в строке на "&":
str.replace("&", "&");Очень часто возникает необходимость выбросить из начала и конца строки все лишние пробельные символы (такие как: пробелы, символы табуляции, символы перевода строки). Для этой цели существует функция stripWhiteSpace():
QString str = " BOB \t THE \nDOG \n"; cerr << str.stripWhiteSpace().ascii() << endl;Строка str может быть изображена как:
QString str = " BOB \t THE \nDOG \n"; cerr << str.simplifyWhiteSpace().ascii() << endl;Результат работы функции будет выглядеть так:
QString str = "polluter pays principle"; QStringList words = QStringList::split(" ", str);В этом примере, строка "polluter pays principle" разбивается на три подстроки; "polluter", "pays" и "principle". Функция split() может принимать третий необязательный параметр типа bool, который определяет -- должны ли игнорироваться пустые подстроки (по-умолчанию) или нет.
Элементы списка QStringList могут быть объединены в одну строку, с помощью функции join(). В качестве аргумента ей передается строка, которая должна быть вставлена между объединяемыми строками. Например, следующий код демонстрирует, как можно объединить все строки в списке, отсортированном по алфавиту, в единую строку, причем подстроки отделяются друг от друга символом перевода строки:
words.sort(); str = words.join("\n");Еще одна немаловажная операция над строками -- определение длины строки. Для этого предназначена функция length() и, как вариант, isEmpty(), которая возвращает true, если длина строки равна 0.
QString различает пустые строки и несуществующие (NULL) строки. Эти различия корнями уходят в язык программирования C. Чтобы проверить -- существует ли строка, можно вызывать функцию isNull(). Для большинства приложений очень важно знать -- содержит ли строка хотя бы один символ. Функция isEmpty() вернет true, если строка не содержит ни одного символа (пустая или несуществующая строка).
Преобразования, между const char * и QString, в большинстве случаев выполняются автоматически:
str += " (1870)";Этот код добавляет строку типа const char * к строке типа QString.
В некоторых ситуациях возникает необходимость явно выполнять преобразование между const char * и QString. Чтобы преобразовать строку QString в const char *, используйте функцию ascii() или latin1(). Обратное преобразование может быть выполнено за счет операции приведения типа.
Когда вызываются функции ascii() или latin1(), или когда выполняется автоматическое преобразование к типу const char *, возвращаемая строка принадлежит объекту QString. Это означает, что нас не должна беспокоить проблема утечки памяти -- Qt самостоятельно утилизирует память, по мере необходимости. С другой стороны, необходимо проявлять большую осторожность при работе с указателями. Например, если оригинальная версия строки QString будет изменена, то ранее полученный указатель на const char * может оказаться недопустимым. Если же необходимо сохранить предыдущий вариант строки, то для этих целей можно воспользоваться услугами класса QByteArray или QCString. Они хранят полную копию данных.
Класс QString поддерживает implicit sharing (неявное совместное использование данных). Это означает, что на копирование строки уходит времени не больше, чем необходимо для копирования указателя на строку. Собственно копирование производится только тогда, когда выполняется попытка изменить одну из копий. Все это делается автоматически и незаметно для нас.
Вся прелесть неявного совместного использования данных состоит в том, что таким образом оптимизируется скорость выполнения операций и при этом нам не нужно постоянно помнить об этом -- это просто работает!
Qt использует это метод оптимизации и для других классов, включая: QBrush, QFont, QPen, QPixmap, QMap<K, T>, QValueList<T> и QValueVector<T>. Что повышает эффективность передачи экземпляров классов по значению, как в виде аргументов функций, так и в виде возвращаемых значений.
C++ -- это строго типизированный язык, однако, иногда возникает необходимость сохранять данные в более общем виде. Самый простой способ -- использовать строки. Например, строки могут хранить текстовые или числовые данные. Qt предоставляет более простой способ работы с переменными -- класс QVariant.
Класс QVariant может хранить значения многих типов Qt: QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize и QString.. Он так же может хранить контейнеры: QMap<QString, QVariant>, QStringList и QValueList<QVariant>.. Мы уже использовали QVariant, когда разрабатывали приложение Spreadsheet, в Главе 4, для хранения содержимого ячейки.
Одно из обычных применений класса QVariant -- создание словарей (map), в которых в качестве ключа используются строки, а в качестве значений -- экземпляры класса QVariant. Как правило, информация о конфигурации приложения сохраняется и загружается с помощью QSettings, но иногда приложения могут обслуживать настройки напрямую, например, сохраняя их в базе данных. QMap<QString, QVariant> идеально подходит в таких ситуациях:
QMap<QString, QVariant> config; config["Width"] = 890; config["Height"] = 645; config["ForegroundColor"] = black; config["BackgroundColor"] = lightGray; config["SavedDate"] = QDateTime::currentDateTime(); QStringList files; files << "2003-05.dat" << "2003-06.dat" << "2003-07.dat"; config["RecentFiles"] = files;
Принцип Действия Неявного Совместного Использования Данных |
---|
Механизм неявного совместного использования данных работает полностью автоматически и незаметно для нас. Таким образом, когда мы используем классы, поддерживающие этот механизм, мы не должны писать дополнительный код, который поддерживал бы его работу. Но знать принцип его действия вам все таки необходимо, поэтому рассмотрим простенький пример и исследуем -- что же скрыто от наших глаз. QString str1 = "Humpty"; QString str2 = str1;Здесь в переменную str1 записывается строка "Humpty" и затем выполняется присваивание переменной str2. С этого момента обе переменные указывают на одну и ту же структуру данных в памяти (типа QStringData). Вместе с символами строки она хранит счетчик ссылок, который содержит количество объектов, ссылающихся на нее. Поскольку и str1, и str2 ссылаются на одни и те же данные, то счетчик ссылок равен 2. str2[0] = 'D';Когда выполняется изменение содержимого переменной str2, то прежде всего создается полная копия данных, таким образом, теперь str1 и str2 ссылаются на различные структуры и все изменения будут производиться над их собственными копиями данных. Счетчик ссылок переменной str1 ("Humpty") теперь стал равен 1 и счетчик ссылок переменной str2 ("Dumpty") так же стал равен 1. Когда счетчик ссылок равен 1, это означает, что данные используются только одним объектом. str2.truncate(4);Если теперь выполнить модификацию переменной str2, то никакого копирования производиться уже не будет, потому что счетчик ссылок равен 1. Функция truncate() будет оперировать с данными, принадлежащими переменной str2, и счетчик ссылок останется равным 1. str1 = str2;После такого присваивания, счетчик ссылок переменной str1 станет равным 0, это означает, что строка "Humpty" больше не нужна. В этом случае память, ранее занимаемая переменной str1, будет освобождена. Теперь обе переменные будут ссылаться на строку "Dump", а счетчик ссылок станет равным 2. Создание классов, использующих оптимизацию неявного совместного использования данных, выполняется довольно просто. В ежеквартальнике Qt Quarterly, в статье "Data Sharing with Class" ( http://doc.trolltech.com/qq/qq02-data-sharing-with-class.html) описывается -- как это сделать. |
Обход в цикле элементов словаря, который хранит значения в вариантном виде, может оказаться не такой простой задачей, если некоторые из значений являются контейнерами. Чтобы проверить тип вариантного значения вам придется воспользоваться функцией type():
QMap<QString, QVariant>::const_iterator it = config.begin(); while (it != config.end()) { QString str; if (it.data().type() == QVariant::StringList) str = it.data().toStringList().join(", "); else str = it.data().toString(); cerr << it.key().ascii() << ": " << str.ascii() << endl; ++it; }С помощью QVariant можно создавать довольно сложные структуры данных, хранящие значения контейнерного типа:
QMap<QString, QVariant> price; price["Orange"] = 2.10; price["Pear"].asMap()["Standard"] = 1.95; price["Pear"].asMap()["Organic"] = 2.25; price["Pineapple"] = 3.85;В этом примере был создан словарь со строковыми ключами (название продукта) и значениями типа double (цена) или типа QMap. Словарь верхнего уровня содержит три ключа: "Orange", "Pear" и "Pineapple". Значение, связанные с ключом "Pear" -- это словарь с двумя ключами ("Standard" и "Organic").
Возможность создания структур данных, подобных этой, может показаться очень соблазнительной, так как можно структурировать данные по своему усмотрению. Но удобство QVariant -- вещь дорогостоящая. Ради удобочитаемости придется пожертвовать быстродействием.
|