Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
222 |
class ilist_item { public:
ilist_item( int value, ilist_-item *item_to_link_to = 0 );
// ...
};
inline ilist_item::
ilist_item( int value, ilist_item *item ) : _value( value )
{
if ( item ) _next = 0;
else {
_next = item->_next; item->_next = this;
}
Операция insert() в общем случае работает с двумя параметрами – значением и адресом элемента, после которого производится вставка. Наш первый вариант
inline void ilist::
insert( ilist_item *ptr, int value )
{
new ilist_item( value, ptr ); ++_size;
реализации имеет два недочета. Сможете ли вы их найти?
}
Одна из проблем заключается в том, что указатель не проверяется на нулевое значение. Мы обязаны распознать и обработать такую ситуацию, иначе это приведет к краху программы во время исполнения. Как реагировать на нулевой указатель? Можно аварийно закончить выполнение, вызвав стандартную функцию abort(), объявленную в
#include <cstdlib> // ...
if ( ! ptr )
заголовочном файле cstdlib: abort();
Кроме того, можно использовать макрос assert(). Это также приведет к аварийному
#include <cassert>
// ...
завершению, но с выводом диагностического сообщения: assert( ptr != 0 );
Третья возможность – возбудить исключение:
С++ для начинающих |
223 |
if ( ! ptr )
throw "Panic: ilist::insert(): ptr == O";
В общем случае желательно избегать аварийного завершения программы: в такой ситуации мы заставляем пользователя беспомощно сидеть и ждать, пока служба поддержки обнаружит и исправит ошибку.
Если мы не можем продолжать выполнение там, где обнаружена ошибка, лучшим решением будет возбуждение исключения: оно передает управление вызвавшей программе в надежде, что та сумеет выйти из положения.
Мы же поступим совсем другим способом: рассмотрим передачу нулевого указателя как
if ( ! ptr )
запрос на вставку элемента перед первым в списке: insert_front( value );
Второй изъян в нашей версии можно назвать философским. Мы реализовали size() и _size как пробный вариант, который может впоследствии измениться. Если мы преобразуем функции size() таким образом, что она будет просто пересчитывать элементы списка, член _size перестанет быть нужным. Написав:
++_size;
мы тесно связали реализацию insert() с текущей конструкцией алгоритма пересчета элементов списка. Если мы изменим алгоритм, нам придется переписывать эту функцию, как и insert_front(), insert_end() и все операции удаления из списка. Вместо того чтобы распространять детали текущей реализации на разные функции класса, лучше
inline void ilist::bump_up_size() { ++_size; }
инкапсулировать их в паре:
inline void ilist::bump_down_size() { --_size; }
Поскольку мы объявили эти функции встроенными, эффективность не пострадала. Вот
inline void ilist::
insert( ilist_item *ptr, int value ) if ( !ptr )
insert_front( value ); else {
bump_up_size();
new ilist_item( value, ptr );
}
окончательный вариант insert():
}
С++ для начинающих |
224 |
Реализация функций insert_front() и insert_end() достаточно очевидна. В каждой
inline void ilist::
insert_front( int value )
{
ilist_item *ptr = new ilist_item( value );
if ( !_at_front )
_at_front = _at_end = ptr; else {
ptr->next( _at_front ); _at_front = ptr;
}
bump_up_size();
}
inl-ine void ilist::
insert_end( int value )
{
if ( !_at_end )
_at_end = _at_front = new ilist_item( value ); else _at_end = new ilist_item( value, _at_end );
bump_up_s-ize();
из них мы должны предусмотреть случай, когда список пуст.
}
find() ищет значение в списке. Если элемент с указанным значением найден,
ilist_item* ilist::
find( int value )
{
ilist_item *ptr = _at_front; while ( ptr )
{
if ( ptr->value() == value ) break;
ptr = ptr->next();
}
return ptr;
возвращается его адрес, иначе find() возвращает 0. Реализация find()выглядит так:
}
ilist_item *ptr = mylist.find( 8 );
Функцию find() можно использовать следующим образом: mylist.insert( ptr, some_value );
или в более компактной записи:
С++ для начинающих |
225 |
mylist.insert( mylist.find( 8 ), some_value );
Перед тем как тестировать операции вставки элементов, нам нужно написать функцию display(), которая поможет нам при отладке. Алгоритм display() достаточно прост: печатаем все элементы, с первого до последнего. Можете ли вы сказать, где в данной
// не работает правильно!
for ( ilist_item *iter = _at_front; // |
начнем с первого |
|
iter != _at_end; |
// |
пока не последний |
++iter ) |
// |
возьмем следующий |
cout << iter->value() << ' ';
// теперь напечатаем последний
реализации ошибка?
cout << iter->value();
Список – это не массив, его элементы не занимают непрерывную область памяти.
Инкремент итератора
++iter;
вовсе не сдвигает его на следующий элемент списка. Вместо этого он указывает на место в памяти, непосредственно следующее за данным элементом, а там может быть все что угодно. Для изменения значения итератора нужно воспользоваться членом _next объекта ilist_item:
iter = iter->_next;
Мы инкапсулировали доступ к членам ilist_item набором встраиваемых функций.
class ilist_item { public:
ilist_item( int value, ilist_item *item_to_link_to = 0 ); int value() { return _value; }
iilst_item* next() { return _next; }
void next( ilist_item *link ) { _next = link; } void value( int new_value ) { _value = new_value; }
private:
int _value; ilist_item *_next;
Определение класса ilist_item теперь выглядит так:
};
Вот определение функции display(), использующее последнюю реализацию класса ilist_item:
С++ для начинающих |
226 |
#include <iostream>
class ilist { public:
void display( ostream &os = cout ); // ...
};
void ilist:: |
|
display( ostream &os ) |
|
{ |
"; |
os << "\n( " << _size << " )( |
|
ilist_item *ptr = _at_front; |
|
while ( ptr ) { |
|
os << ptr->value() << " "; |
|
ptr = ptr->next(); |
|
} |
|
os << ")\n"; |
|
} |
|
Тестовую программу для нашего класса ilist в его текущей реализации можно представить таким образом:
С++ для начинающих |
227 |
#include <iostream> #include "ilist.h"
int main()
{
ilist mylist;
for ( int ix = 0; ix < 10; ++ix ) { mylist.insert_front( ix ); mylist.insert_end( ix );
}
cout <<
"Ok: после insert_front() и insert_end()\n"; mylist.display();
ilist_item *it = mylist.find( 8 ); cout << "\n"
<<"Ищем значение 8: нашли?"
<<( it ? " да!\n" : " нет!\n" );
mylist.insert( it, 1024 ); cout << "\n" <<
"Вставка элемента 1024 после 8\n";
mylist.display();
int elem_cnt = mylist.remove( 8 ); cout << "\n"
<<"Удалено " << elem_cnt
<<" элемент(ов) со значением 8\n";
mylist.display();
cout << "\n" << "Удален первый элемент\n";
mylist.remove_front(); mylist.display();
cout << "\n" << "Удалены все элементы\n"; mylist.remove_all(); mylist.display();
}
Результат работы программы:
Ok: после insert_front() и insert_end()
(20)( 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9 )
Ищем значение 8: нашли? да!
Вставка элемента 1024 после 8 ( 21 )( 9 8 1024 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9 )
Удалено 2 элемент(ов) со значением 8 ( 19 )( 9 1024 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 9 )
Удален первый элемент
( 18 )( 1024 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 9 )
Удалены все элементы
( 0 )( )
С++ для начинающих |
228 |
Помимо вставки элементов, необходима возможность их удаления. Мы реализуем три
void remove_front(); void remove_all ();
таких операции:
int remove( int value );
inline void i1ist:: remove_front()
{
if ( _at_front ) {
ilist_item *ptr = _at_front; _at_front = _at_front->next();
bump_down_size() ; delete ptr;
}
Вот как выглядит реализация remove_front():
}
remove_all() вызывает remove_front() до тех пор, пока все элементы не будут
void ilist:: remove_all()
{
while ( _at_front ) remove_front();
_size = 0;
_at_front = _at_end = 0;
удалены:
}
Общая функция remove() также использует remove_front() для обработки специального случая, когда удаляемый элемент (элементы) находится в начале списка. Для удаления из середины списка используется итерация. У элемента, предшествующего удаляемому, необходимо модифицировать указатель _next. Вот реализация функции:
С++ для начинающих |
229 |
int ilist:: remove( int value )
{
ilist_item *plist = _at_front; int elem_cnt = 0;
while ( plist && plist->value() == value )
{
plist = plist->next(); remove_front(); ++elem_cnt;
}
if ( ! plist ) return elem_cnt;
ilist_item *prev = plist; plist = plist->next();
while ( plist ) {
if ( plist->value() == value ) { prev->next( plist->next() ); delete plist;
++elem_cnt; bump_down_size(); plist = prev->next(); if ( ! plist ) {
_at_end = prev; return elem_cnt;
}
}
else {
prev = plist;
plist = plist->next();
}
return elem_cnt;
}
Следующая программа проверяет работу операций в четырех случаях: когда удаляемые элементы расположены в конце списка, удаляются все элементы, таких элементов нет или они находятся и в начале, и в конце списка.
|
#include "ilist.h" |
|
С |
int main() |
230 |
++ для начинающих |
||
|
{ |
|
|
ilist mylist; |
\n" |
|
cout << "\n----------------------------------------------- |
|
|
<< "тест #1: - элементы в конце\n" |
\n"; |
|
<< "----------------------------------------------- |
|
|
mylist.insert_front( 1 ); mylist.insert_front( 1 ); |
|
|
mylist.insert_front( 1 ); |
|
|
my1ist.insert_front( 2 ); mylist.insert_front( 3 ); |
|
|
my1ist.insert_front( 4 ); |
|
|
mylist.display(); |
|
|
int elem_cnt = mylist.remove( 1 ); |
|
|
cout << "\n" << "Удалено " << elem_cnt |
|
|
<< " элемент(ов) со значением 1\n"; |
|
|
mylist.display(); |
|
|
mylist.remove_all(); |
|
|
cout << "\n----------------------------------------------- |
\n" |
|
<< "тест #2: - элементы в начале\n" |
\n"; |
|
<< "----------------------------------------------- |
|
|
mylist.insert_front( 1 ); mylist.insert_front( 1 ); |
|
|
mylist.insert_front( 1 ); |
|
|
mylist.display(); |
|
|
elem_cnt = mylist.remove( 1 ); |
|
|
cout << "\n" << "Удалено " << elem_cnt |
|
|
<< " элемент(ов) со значением 1\n"; |
|
|
mylist.display(); |
|
|
mylist.remove_all () ; |
|
|
cout << "\n----------------------------------------------- |
\n" |
|
<< "тест #3: - элементов нет в списке\n" |
\n"; |
|
<< "----------------------------------------------- |
|
|
mylist.insert_front( 0 ); mylist.insert_front( 2 ); |
|
|
mylist.insert_front( 4 ); |
|
|
mylist.display(); |
|
|
elem_cnt = mylist.remove( 1 ); |
|
|
cout << "\n" << "Удалено " << elem_cnt |
|
|
<< " элемент(ов) со значением 1\n"; |
|
|
mylist.display(); |
|
|
mylist.remove_all () ; |
|
|
cout << "\n----------------------------------------------- |
\n" |
|
<< "тест #4: - элементы в конце и в начале\n" |
\n"; |
|
<< "----------------------------------------------- |
|
|
my1ist.insert_front( 1 ); mylist.insert_front( 1 ); |
|
|
my1ist.insert_front( 1 ); |
|
|
my1ist.insert_front( 0 ); mylist.insert_front( 2 ); |
|
|
my1ist.insert_front( 4 ); |
|
|
mylist.insert_front( 1 ); my1ist.insert_front( 1 ); |
|
|
mylist.insert_front( 1 ); |
|
|
mylist.display() ; |
|
|
elem_cnt = mylist.remove( 1 ); |
|
|
cout << "\n" << "Удалено " << elem_cnt |
|
|
<< " элемент(ов) со значением 1\n"; |
|
С++ для начинающих |
231 |
}
Результат работы программы:
-----------------------------------------------
тест #1: - элементы в конце
-----------------------------------------------
( 6 )( 4 3 2 1 1 1 )
Удалено 3 элемент(ов) со значением 1 ( 3 )( 4 3 2 )
-----------------------------------------------
тест #2: - элементы в начале
-----------------------------------------------
( 3 )( 1 1 1 )
Удалено 3 элемент(ов) со значением 1 ( 0 )( )
-----------------------------------------------
тест #3: - элементов нет в списке
-----------------------------------------------
( 3 )( 4 2 0 )
Удалено 0 элемент(ов) со значением 1 ( 3 )( 4 2 0 )
-----------------------------------------------
тест #4: - элементы в конце и в начале
-----------------------------------------------
(9 )( 1 1 1 4 2 0 1 1 1 )
Удалено 6 элемент(ов) со значением 1
( 3 )( 4 2 0 )
Последние две операции, которые мы хотим реализовать, – конкатенация двух списков (добавление одного списка в конец другого) и инверсия (изменение порядка элементов на противоположный). Первый вариант concat() содержит ошибку. Сможете ли вы ее
void ilist::concat( const ilist &i1 ) { if ( ! _at_end )
_at_front = i1._at_front;
else _at_end->next( i1._at_front ); _at_end = i1._at_end;
найти?
}
Проблема состоит в том, что теперь два объекта ilist содержат последовательность одних и тех же элементов. Изменение одного из списков, например вызов операций insert() и remove(), отражается на другом, приводя его в рассогласованное состояние. Простейший способ обойти эту проблему – скопировать каждый элемент второго списка. Сделаем это при помощи функции insert_end():