Программирование >>  Обобщенные обратные вызовы 

1 [ 2 ] 3 4 5 ... 84


наличии ошибок в используемой вами реализации стандартной библиотеки и стремитесь избежать больших проблем.

= 1;

= 2;

Обе эти строки -- грубейшие, но трудно обнаруживаемые ошибки, поскольку такая программа вполне может работать (в зависимости от используемой конкретной реализации библиотеки).

Имеется существенная разница между функциями size/resize и capacity/reserve, т.е. между размером и емкостью вектора.

size говорит нам, сколько элементов содержится в контейнере в настоящее время, а resize изменяет содержимое контейнера таким образом, чтобы он содержал указанное количество элементов в контейнере путем добавления или удаления их из конца контейнера. Обе эти функции присутствуют в контейнерах list, vector и deque и отсутствуют в остальных.

capaci Су возвращает количество мест для элементов в контейнере, т.е. указывает, сколько элементов можно разместить в контейнере перед тем, как добавление очередного элемента вызовет выделение нового блока памяти. Функция reserve при необходимости увеличивает (но никогда не уменьшает) размер внугрсннего буфера , чтобы он был способен вместить как минимум указанное количество элементов. Обе функции предусмотрены только у контейнера vector.

В нашем случае мы использовали вызов v. reserve(2) и, таким образом, гарантировали, что V. capaci tyC) >= 2, но мы не добавляли элементы в вектор v, так что вектор V остается пуст! На данный момент все, что можно сказать о векторе - это то, что в нем есть место как минимум для двух элементов.

> Рекомендация

Помните о разнице между size/resize и capacity/reserve.

Мы можем безопасно использовать оператор operator[] (или функцию at) только для изменения элементов, которые реально содержатся в контейнере, т.е. реально учтены в size. Вы можете удивиться, почему оператор operator[] не может быть достаточно интеллектуальным, чтобы добавить элемент в контейнер, если он еще не в контейнере. Но если бы operator[] позволял делать такие вещи, мы бы могли создавать вектор с дырами ! Рассмотрим, например, следующий фрагмент.

vector<int> v; V.reserveC 100 );

vL99] =42; Ошибка, но, допустим, такое возможно... ... что тогда можно сказать о значениях v[0..98]?

Увы, поскольку не предусмотрено, чтобы оператор operator[] выполнял проверку диапазона, в большинстве реализаций выражение v[0] будет просто возвращать ссылку на еще неиспользуемую память во внутреннем буфере вектора, а именно на то место в памяти, где в конечном итоге будет находиться первый элемент вектора. Следовательно, скорее всего, инструкция v[0] = 1; будет нормально работать , т.е., например, при выводе cout v[0] вы, вероятно, увидите на экране I, как и ожидалось (и совершенно необоснованно!).

Но описанный сценарий - не более чем типичный вариант того, что может случиться. На самом деле все зависит от реализации стандартной библиотеки. Стандарт ничего не говорит о том, что должно происходить при записи элемента v[0] в пусто.м векторе v, поскольку программист легко может узнать о том, что вектор пуст, чтобы не пытаться выполнять такую запись. В К01ще концов, если ему очень надо, он может обеспечить выполнение соответствующей проверки, воспользовавшись вызовом v.atCO)...



Само собой разумеется, присваивания v[0] = 1; v[l] =2; будут вполне корректны и осмысленны, если заменить вызов v.reserve(2) вызовом v.resize(2). Можно также получить корректный код, заменив присваивания вызовами v.push back(l); v.push back(2);, которые обеспечивают безопасный способ размещения элементов в конце контейнера.

for(vector<int>::iterator i = v.beginC); i<v.end(); i++){ cout *i end!;

Во-первых, заметим, что этот цикл ничего не выводит, поскольку вектор все еще пуст. Это может удивить автора рассматриваемого кода, если, конечно, он не сообразит, что по сути он ничего не внес в контейнер, а всего лишь поиграл (так и хочется сказать - с огнем) с зарезервированным местом в памяти, которое официально вектором не использовано.

То есть формально в цикле нет ошибки, что, однако, не исключает необходимости привести ряд стилистических замечаний.

1. Старайтесь по возможности использовать const-вариант итератора. Если итератор не используется для модификации содержимого вектора, следует использовать const... i terator.

2. Итераторы следует сравнивать при помощи оператора сравнения ! = , но не при помощи <. Конечно, так как vector<int>: : i terator - итератор произвольного доступа (само собой разумеется, не обязательно типа i nt*!), нет никаких проблем при сравнении <v.end(), использованном в примере. Однако такое сравнение при помощи < работает только с итераторами произвольного доступа, в то время как сравнение с использованием оператора != работает со всеми типами итераторов. Поэтому мы должны везде использовать именно такое сравнение, а сравнение < оставить только для тех случаев, где это действительно необходимо. (Заметим, что сравнение != существенно облегчит переход к использованию другого типа контейнера в будущем, если это вдруг потребуется. Например, итераторы std: : 1 i st не поддерживают сравнение с использованием оператора <, так как являются би-направленными итераторами.)

3. Лучше использовать префиксную форму операторов -- и ++ вместо постфиксной. Возьмите за привычку писать в циклах по умолчанию ++i вместо 1++, если только вам действительно не требуется старое значение i. Например, постфиксная форма оператора естественна при использовании кода наподобие v[i++] для обращения к i-му элементу и одновременно увеличения счетчика цикла.

4. Избегайте излишних вычислений. В нашем случае значение, возвращаемое при вызове v.endO, не изменяется в процессе работы цикла, так что вместо вычисления его заново при каждой итерации лучше вычислить его один раз перед началом цикла.

Примечание. Если ваша реализация vector<i nt> : : i terator представляет собой простой указатель int*, а функция end() встраиваемая, то при определенном уровне оптимизации накладные расходы будут сведены практически к пулю, так как интеллектуальный компилятор будет способен обнаружить, что значение, возвращаемое end О, не изменяется в процессе работы цикла. Современные компиляторы вполне способны справиться с такой задачей. Однако если vec-tor<int>: : iterator не является int- (например, в большинстве отладочных реализаций это тип класса), функции не являются встраиваемыми и/или компилятор не способен выполнить необходимую оптимизацию, вьтесепие вычислений из цикла может существенно повысить производительность кода.

5. Предпочтительнее использовать \п вместо end!. Использование endl заставляет выполнить сброс внутренних буферов потока. Если поток буферизован, а сброс буферов



всякий раз по окончании вывода строки не требуется, лучше использовать endl один раз, в конце цикла; зто также повысит производительность вашей профаммы.

Все это были комментарии низкого уровня ; однако есть замечание и на более высоком уровне.

6. Вместо разработки собственных циклов, используйте там, где это ясно и просто, стандартные библиотечные алгоритмы сору и for each. Больше полагайтесь на свой вкус. Я говорю так потому, что это как раз тот случай, когда определенную роль и фа ют вкус и эстетизм. В простых случаях сору и for each могут улучшить читаемость и понятность кода по сравнению с циклами, разработанными вручную. Тем не менее, часто код с использованием for each может оказаться менее понятным и удобочитаемым, так как тело цикла придется разбивать на отдельные функторы. Иногда такое разбиение идет коду только на пользу, а иногда совсем наоборот.

Вот почему вкус играет здесь такую важную роль. Я бы в рассматриваемом примере заменил цикл чем-то наподобие

copyCv.begi п(),v.end(),ostream iterator<i nt>(cout, \n ));

Кроме тою, при использовании того же алгоритма сору вы не сможете ошибиться в применении !=. ++, end() и endl, поскольку вам просто не придется ничего этого делать самостоятельно. (Конечно, при этом предполагается, что вы не намерены сбрасывать буферы потока при выводе каждого целого числа. Если эго для вас критично, вам действительно придется писать свой цикл вместо использования стандартного алгоритма std: :сору.) Повторное использование, в случае корректного применения, не только делает код более удобочитаемым и понятным, но и повышает его качество, позволяя избежать различного рода ловушек при написании собственного кода.

Вы можете пойти дальше и написать свой собственный алгоритм для копирования контейнера, т.е. алгоритм, который работает со всем контейнером в целом, а не с какой-то его частью, определяемой диапазоном итераторов. Такой подход автоматически заставит использовать const iterator. Рассмотрим следующий пример.

tempiate<class Container, class Outputlterator> outputlterator copy(const Container* c,

outputiterator result)! return std::copy( с.begin(), c.endC), result );

Здесь мы просто написали обертку для применения std: :сору ко всему контейнеру целиком, и так как контейнер передается как const&, итераторы автоматически будут константными (const iterator).

> Рекомендация

Будьте максимально корректны при использовании модификатора const. В частности, используйте const i terator, если вы не модифицируете содержимое контейнера.

Сравнивайте итераторы при помощи оператора !=, а не <

Лучше использовать префиксную форму операторов - - и ++ вместо постфиксной, если только вам не требуется старое значение переменной.

Лучше использовать существующие алгоритмы, в частности, стандартные алгоритмы (такие как for each), а не разрабатывать собственные циклы.

Далее нам встречается код cout v[0];



1 [ 2 ] 3 4 5 ... 84

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика