11.4. Контейнеры указателей.

Кроме 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 станут его производными.