<< Пред.           стр. 11 (из 121)           След. >>

Список литературы по разделу

  int ia[ e1em_size ];
 
  for ( int ix = 0; ix < e1em_size; ++ix )
  ia[ ix ] = ivec[ ix ];
 
  // ...
 }
 Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например:
 void print_vector( vector ivec )
 {
  if ( ivec.empty() )
  return;
 
  for ( int ix=0; ix< ivec.size(); ++ix )
  cout << ivec[ ix ] << ' ';
 }
 Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму:
 vector< int > ivec( 10, -1 );
 Все десять элементов вектора будут равны -1.
 Массив встроенного типа можно явно инициализировать списком:
 int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 };
 Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа:
 // 6 элементов ia копируются в ivec
 vector< int > ivec( ia, ia+6 );
 Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон:
 // копируются 3 элемента: ia[2], ia[3], ia[4]
 vector< int > ivec( &ia[ 2 ], &ia[ 5 ] );
 Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например:
 vector< string > svec;
 
 void init_and_assign()
 {
  // один вектор инициализируется другим
  vector< string > user_names( svec );
  // ...
 
  // один вектор копируется в другой
  svec = user_names;
 }
 Говоря об идиоме STL, мы подразумеваем совсем другой подход к использованию вектора. Вместо того чтобы сразу задать нужный размер, мы определяем пустой вектор:
 vector< string > text;
 Затем добавляем к нему элементы при помощи различных функций. Например, функция push_back()вставляет элемент в конец вектора. Вот фрагмент кода, считывающего последовательность строк из стандартного ввода и добавляющего их в вектор:
 string word;
 while ( cin >> word ) {
  text.push_back( word );
  // ...
 }
 Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора:
 cout << "считаны слова: \n";
 for ( int ix =0; ix < text.size(); ++ix )
  cout << text[ ix ] << ' ';
 cout << endl;
 более типичным в рамках данной идиомы будет использование итераторов:
 cout << "считаны слова: \n";
 for ( vector::iterator it = text.begin();
  it != text.end(); ++it )
  cout << *it << ' ';
 cout << endl;
 Итератор – это класс стандартной библиотеки, фактически являющийся указателем на элемент массива.
 Выражение
 *it;
 разыменовывает итератор и дает сам элемент вектора. Инструкция
 ++it;
 сдвигает указатель на следующий элемент. Не нужно смешивать эти два подхода. Если следовать идиоме STL при определении пустого вектора:
 vector ivec;
 будет ошибкой написать:
 ivec[0] = 1024;
 У нас еще нет ни одного элемента вектора ivec; количество элементов выясняется с помощью функции size().
 Можно допустить и противоположную ошибку. Если мы определили вектор некоторого размера, например:
 vector ia( 10 );
 то вставка элементов увеличивает его размер, добавляя новые элементы к существующим. Хотя это и кажется очевидным, тем не менее, начинающий программист вполне мог бы написать:
 const int size = 7;
 int ia[ size ] = { 0, 1, 1, 2, 3, 5, 8 };
 vector< int > ivec( size );
 for ( int ix = 0; ix < size; ++ix )
  ivec.push_back( ia[ ix ] );
 Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего получился вектор ivec размера 14.
 Следуя идиоме STL, можно не только добавлять, но и удалять элементы вектора. (Все это мы рассмотрим подробно и с примерами в главе 6.)
 Упражнение 3.24
 Имеются ли ошибки в следующих определениях?
 int ia[ 7 ] = { 0, 1, 1, 2, 3, 5, 8 };
 
 (a) vector< vector< int > > ivec;
 (b) vector< int > ivec = { 0, 1, 1, 2, 3, 5, 8 };
 (c) vector< int > ivec( ia, ia+7 );
 (d) vector< string > svec = ivec;
 (e) vector< string > svec( 10, string( "null" ));
 Упражнение 3.25
 Реализуйте следующую функцию:
 bool is_equa1( const int*ia, int ia_size,
  const vector &ivec );
 Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров “хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().
 3.11. Класс complex
 Класс комплексных чисел complex – еще один класс из стандартной библиотеки. Как обычно, для его использования нужно включить заголовочный файл:
 #include
 Комплексное число состоит из двух частей – вещественной и мнимой. Мнимая часть представляет собой квадратный корень из отрицательного числа. Комплексное число принято записывать в виде
 2 + 3i
 где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex:
 // чисто мнимое число: 0 + 7-i
 comp1ex< double > purei( 0, 7 );
 
 // мнимая часть равна 0: 3 + Oi
 comp1ex< float > rea1_num( 3 );
 
 // и вещественная, и мнимая часть равны 0: 0 + 0-i
 comp1ex< long double > zero;
 
 // инициализация одного комплексного числа другим
 comp1ex< double > purei2( purei );
 Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex:
 complex< double > conjugate[ 2 ] = {
  complex< double >( 2, 3 ),
  complex< double >( 2, -3 )
 };
 Вот как определяются указатель и ссылка на комплексное число:
 complex< double > *ptr = &conjugate[0];
 complex< double > &ref = *ptr;
 Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.)
 3.12. Директива typedef
 Директива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например:
 typedef double wages;
 typedef vector vec_int;
 typedef vec_int test_scores;
 typedef bool in_attendance;
 typedef int *Pint;
 Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов:
 // double hourly, weekly;
 wages hourly, weekly;
 
 // vector vecl( 10 );
 vec_int vecl( 10 );
 
 // vector test0( c1ass_size );
 const int c1ass_size = 34;
 test_scores test0( c1ass_size );
 
 // vector< bool > attendance;
 vector< in_attendance > attendance( c1ass_size );
 
 // int *table[ 10 ];
 Pint table [ 10 ];
 Эта директива начинается с ключевого слова typedef, за которым идет спецификатор типа, и заканчивается идентификатором, который становится синонимом для указанного типа.
 Для чего используются имена, определенные с помощью директивы typedef? Применяя мнемонические имена для типов данных, можно сделать программу более легкой для восприятия. Кроме того, принято употреблять такие имена для сложных составных типов, в противном случае воспринимаемых с трудом (см. пример в разделе 3.14), для объявления указателей на функции и функции-члены класса (см. раздел 13.6).
 Ниже приводится пример вопроса, на который почти все дают неверный ответ. Ошибка вызвана непониманием директивы typedef как простой текстовой макроподстановки. Дано определение:
 typedef char *cstring;
 Каков тип переменной cstr в следующем объявлении:
 extern const cstring cstr;
 Ответ, который кажется очевидным:
 const char *cstr
 Однако это неверно. Спецификатор const относится к cstr, поэтому правильный ответ – константный указатель на char:
 char *const cstr;
 3.13. Спецификатор volatile
 Объект объявляется как volatile (неустойчивый, асинхронно изменяемый), если его значение может быть изменено незаметно для компилятора, например переменная, обновляемая значением системных часов. Этот спецификатор сообщает компилятору, что не нужно производить оптимизацию кода для работы с данным объектом.
 Спецификатор volatile используется подобно спецификатору const:
 volatile int disp1ay_register;
 volatile Task *curr_task;
 volatile int ixa[ max_size ];
 volatile Screen bitmap_buf;
 display_register – неустойчивый объект типа int. curr_task – указатель на неустойчивый объект класса Task. ixa – неустойчивый массив целых, причем каждый элемент такого массива считается неустойчивым. bitmap_buf – неустойчивый объект класса Screen, каждый его член данных также считается неустойчивым.
 Единственная цель использования спецификатора volatile – сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.
 3.14. Класс pair
 Класс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:
 #inc1ude
 Например, инструкция
 pair< string, string > author( "James", "Joyce" );
 создает объект author типа pair, состоящий из двух строковых значений.
 Отдельные части пары могут быть получены с помощью членов first и second:
 string firstBook;
 
 if ( Joyce.first == "James" &&
  Joyce.second == "Joyce" )
  firstBook = "Stephen Hero";
 Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef:
 typedef pair< string, string > Authors;
 
 Authors proust( "marcel", "proust" );
 Authors joyce( "James", "Joyce" );
 Authors musil( "robert", "musi1" );
 Вот другой пример употребления пары. Первое значение содержит имя некоторого объекта, второе – указатель на соответствующий этому объекту элемент таблицы.
 class EntrySlot;
 extern EntrySlot* 1ook_up( string );
 
 typedef pair< string, EntrySlot* > SymbolEntry;
 
 SymbolEntry current_entry( "author", 1ook_up( "author" ));
 // ...
 
 if ( EntrySlot *it = 1ook_up( "editor" ))
 {
  current_entry.first = "editor";
  current_entry.second = it;
 }
 (Мы вернемся к рассмотрению класса pair в разговоре о контейнерных типах в главе 6 и об обобщенных алгоритмах в главе 12.)
 3.15. Типы классов
 Механизм классов позволяет создавать новые типы данных; с его помощью введены типы string, vector, complex и pair, рассмотренные выше. В главе 2 мы рассказывали о концепциях и механизмах, поддерживающих объектный и объектно-ориентированный подход, на примере реализации класса Array. Здесь мы, основываясь на объектном подходе, создадим простой класс String, реализация которого поможет понять, в частности, перегрузку операций – мы говорили о ней в разделе 2.3. (Классы подробно рассматриваются в главах 13, 14 и 15. Мы дали краткое описание класса для того, чтобы приводить более интересные примеры. Читатель, только начинающий изучение С++, может пропустить этот раздел и подождать более систематического описания классов в следующих главах.)
 Наш класс String должен поддерживать инициализацию объектом класса String, строковым литералом и встроенным строковым типом, равно как и операцию присваивания ему значений этих типов. Мы используем для этого конструкторы класса и перегруженную операцию присваивания. Доступ к отдельным символам String будет реализован как перегруженная операция взятия индекса. Кроме того, нам понадобятся: функция size() для получения информации о длине строки; операция сравнения объектов типа String и объекта String со строкой встроенного типа; а также операции ввода/вывода нашего объекта. В заключение мы реализуем возможность доступа к внутреннему представлению нашей строки в виде строки встроенного типа.
 Определение класса начинается ключевым словом class, за которым следует идентификатор – имя класса, или типа. В общем случае класс состоит из секций, предваряемых словами public (открытая) и private (закрытая). Открытая секция, как правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string – указатель на char, а также _size типа int. _size будет хранить информацию о длине строки, а _string – динамически выделенный массив символов. Вот как выглядит определение класса:
 #inc1ude
 
 class String;
 istream& operator>>( istream&, String& );
 ostream& operator<<( ostream&, const String& );
 
 class String {
 public:
  // набор конструкторов
  // для автоматической инициализации
  // String strl; // String()
  // String str2( "literal" ); // String( const char* );
  // String str3( str2 ); // String( const String& );
 
  String();
  String( const char* );
  String( const String& );
 
  // деструктор
  ~String();
 
  // операторы присваивания
  // strl = str2
  // str3 = "a string literal"
 
  String& operator=( const String& );
  String& operator=( const char* );
 
  // операторы проверки на равенство
  // strl == str2;
  // str3 == "a string literal";
 
  bool operator==( const String& );
  bool operator==( const char* );
 
  // перегрузка оператора доступа по индексу
  // strl[ 0 ] = str2[ 0 ];
 
  char& operator[]( int );
 
  // доступ к членам класса
  int size() { return _size; }
  char* c_str() { return _string; }
 
 private:
  int _size;
  char *_string;
 }
 Класс String имеет три конструктора. Как было сказано в разделе 2.3, механизм перегрузки позволяет определять несколько реализаций функций с одним именем, если все они различаются количеством и/или типами своих параметров. Первый конструктор
 String();
 является конструктором по умолчанию, потому что не требует явного указания начального значения. Когда мы пишем:
 String str1;
 для str1 вызывается такой конструктор.
 Два оставшихся конструктора имеют по одному параметру. Так, для
 String str2("строка символов");
 вызывается конструктор
 String(const char*);
 а для
 String str3(str2);
 конструктор
 String(const String&);
 Тип вызываемого конструктора определяется типом фактического аргумента. Последний из конструкторов, String(const String&), называется копирующим, так как он инициализирует объект копией другого объекта.
 Если же написать:
 String str4(1024);
 то это вызовет ошибку компиляции, потому что нет ни одного конструктора с параметром типа int.
 Объявление перегруженного оператора имеет следующий формат:
 return_type operator op (parameter_list);
 где operator – ключевое слово, а op – один из предопределенных операторов: +, =, ==, [] и так далее. (Точное определение синтаксиса см. в главе 15.) Вот объявление перегруженного оператора взятия индекса:
 char& operator[] (int);
 Этот оператор имеет единственный параметр типа int и возвращает ссылку на char. Перегруженный оператор сам может быть перегружен, если списки параметров отдельных конкретизаций различаются. Для нашего класса String мы создадим по два различных оператора присваивания и проверки на равенство.
 Для вызова функции-члена применяются операторы доступа к членам – точка (.) или стрелка (->). Пусть мы имеем объявления объектов типа String:
 String object("Danny");
 String *ptr = new String ("Anna");
 String array[2];
 Вот как выглядит вызов функции size() для этих объектов:
 vector sizes( 3 );
 
 // доступ к члену для objects (.);
 // objects имеет размер 5
 sizes[ 0 ] = object.size();
 
 // доступ к члену для pointers (->)
 // ptr имеет размер 4
 sizes[ 1 ] = ptr->size();
 
 // доступ к члену (.)
 // array[0] имеет размер 0
 sizes[ 2 ] = array[0].size();
 Она возвращает соответственно 5, 4 и 0.
 Перегруженные операторы применяются к объекту так же, как обычные:
 String namel( "Yadie" );
 String name2( "Yodie" );
 
 // bool operator==(const String&)
 if ( namel == name2 )
  return;
 else
 // String& operator=( const String& )
  namel = name2;
 Объявление функции-члена должно находиться внутри определения класса, а определение функции может стоять как внутри определения класса, так и вне его. (Обе функции size() и c_str() определяются внутри класса.) Если функция определяется вне класса, то мы должны указать, кроме всего прочего, к какому классу она принадлежит. В этом случае определение функции помещается в исходный файл, допустим, String.C, а определение самого класса – в заголовочный файл (String.h в нашем примере), который должен включаться в исходный:
 // содержимое исходного файла: String.С
 
 // включение определения класса String

<< Пред.           стр. 11 (из 121)           След. >>

Список литературы по разделу