Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

123

#include <comp1ex>

Комплексное число состоит из двух частей вещественной и мнимой. Мнимая часть представляет собой квадратный корень из отрицательного числа. Комплексное число

принято записывать в виде

2 + 3i

где 2 действительная часть, а 3i мнимая. Вот примеры определений объектов типа

//чисто мнимое число: 0 + 7-i comp1ex< double > purei( 0, 7 );

//мнимая часть равна 0: 3 + Oi comp1ex< float > rea1_num( 3 );

//и вещественная, и мнимая часть равны 0: 0 + 0-i comp1ex< long double > zero;

//инициализация одного комплексного числа другим

complex:

comp1ex< double > purei2( purei );

Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также

complex< double > conjugate[ 2 ] = { complex< double >( 2, 3 ), complex< double >( 2, -3 )

определить массив элементов типа complex:

};

complex< double > *ptr = &conjugate[0];

Вот как определяются указатель и ссылка на комплексное число: complex< double > &ref = *ptr;

Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.)

3.12. Директива typedef

Директива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например:

С++ для начинающих

124

 

typedef double

wages;

 

 

typedef vector<int>

vec_int;

 

typedef vec_int

test_scores;

 

typedef bool

in_attendance;

 

typedef int

*Pint;

 

 

 

 

Имена, определенные с помощью директивы typedef, можно использовать точно так же,

//double hourly, weekly; wages hourly, weekly;

//vector<int> vecl( 10 ); vec_int vecl( 10 );

//vector<int> 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;

С++ для начинающих

125

3.13. Спецификатор volatile

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

volatile int disp1ay_register; volatile Task *curr_task; volatile int ixa[ max_size ];

Спецификатор volatile используется подобно спецификатору const: volatile Screen bitmap_buf;

display_register неустойчивый объект типа int. curr_task указатель на неустойчивый объект класса Task. ixa неустойчивый массив целых, причем каждый элемент такого массива считается неустойчивым. bitmap_buf неустойчивый объект класса Screen, каждый его член данных также считается неустойчивым.

Единственная цель использования спецификатора volatile сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.

3.14. Класс pair

Класс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:

#inc1ude <uti1ity>

Например, инструкция

pair< string, string > author( "James", "Joyce" );

создает объект author типа pair, состоящий из двух строковых значений.

string firstBook;

if ( Joyce.first == "James" && Joyce.second == "Joyce" )

Отдельные части пары могут быть получены с помощью членов first и second: firstBook = "Stephen Hero";

Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef:

С++ для начинающих

126

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 (закрытая). Открытая секция, как

С++ для начинающих

127

правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string указатель на char, а также _size типа int. _size будет хранить информацию о длине строки, а _string динамически выделенный массив символов. Вот как выглядит

#inc1ude <iostream>

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;

определение класса:

}

С++ для начинающих

128

Класс 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. Перегруженный оператор сам может быть перегружен, если списки параметров

С++ для начинающих

129

отдельных конкретизаций различаются. Для нашего класса String мы создадим по два различных оператора присваивания и проверки на равенство.

Для вызова функции-члена применяются операторы доступа к членам точка (.) или

String object("Danny");

String *ptr = new String ("Anna");

стрелка (->). Пусть мы имеем объявления объектов типа String:

String array[2];

vector<int> sizes( 3 );

//доступ к члену для objects (.);

//objects имеет размер 5

sizes[ 0 ] = object.size();

//доступ к члену для pointers (->)

//ptr имеет размер 4

sizes[ 1 ] = ptr->size();

//доступ к члену (.)

//array[0] имеет размер 0

Вот как выглядит вызов функции size() для этих объектов: 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 в нашем примере), который должен включаться в исходный:

С++ для начинающих

130

//содержимое исходного файла: String.С

//включение определения класса String #inc1ude "String.h"

//включение определения функции strcmp() #inc1ude <cstring>

bool

// тип возвращаемого значения

String::

// класс, которому принадлежит функция

operator==

// имя функции: оператор равенства

(const String &rhs) // список параметров

{

if ( _size != rhs._size ) return false;

return strcmp( _strinq, rhs._string ) ? false : true;

}

Напомним, что strcmp() функция стандартной библиотеки С. Она сравнивает две строки встроенного типа, возвращая 0 в случае равенства строк и ненулевое значение в случае неравенства. Условный оператор (?:) проверяет значение, стоящее перед знаком вопроса. Если оно истинно, возвращается значение выражения, стоящего слева от двоеточия, в противном случае стоящего справа. В нашем примере значение выражения равно false, если strcmp() вернула ненулевое значение, и true если нулевое. (Условный оператор рассматривается в разделе 4.7.)

Операция сравнения довольно часто используется, реализующая ее функция получилась небольшой, поэтому полезно объявить эту функцию встроенной (inline). Компилятор подставляет текст функции вместо ее вызова, поэтому время на такой вызов не затрачивается. (Встроенные функции рассматриваются в разделе 7.6.) Функция-член, определенная внутри класса, является встроенной по умолчанию. Если же она определена

inline bool

String::operator==(const String &rhs)

{

// то же самое

вне класса, чтобы объявить ее встроенной, нужно употребить ключевое слово inline:

}

Определение встроенной функции должно находиться в заголовочном файле, содержащем определение класса. Переопределив оператор == как встроенный, мы должны переместить сам текст функции из файла String.C в файл String.h.

Ниже приводится реализация операции сравнения объекта String со строкой

inline bool String::operator==(const char *s)

{

return strcmp( _string, s ) ? false : true;

встроенного типа:

}

С++ для начинающих

131

Имя конструктора совпадает с именем класса. Считается, что он не возвращает значение, поэтому не нужно задавать возвращаемое значение ни в его определении, ни в его теле. Конструкторов может быть несколько. Как и любая другая функция, они могут быть

#include <cstring>

// default constructor inline String::String()

{

_size = 0; _string = 0;

}

inline String::String( const char *str )

{

if ( ! str ) {

_size = 0; _string = 0;

}

else {

_size = str1en( str );

_string = new char[ _size + 1 ]; strcpy( _string, str );

}

// copy constructor

inline String::String( const String &rhs )

{

size = rhs._size;

if ( ! rhs._string ) _string = 0;

else {

_string = new char[ _size + 1 ]; strcpy( _string, rhs._string );

}

объявлены встроенными.

}

Поскольку мы динамически выделяли память с помощью оператора new, необходимо освободить ее вызовом delete, когда объект String нам больше не нужен. Для этой цели служит еще одна специальная функция-член деструктор, автоматически вызываемый для объекта в тот момент, когда этот объект перестает существовать. (См. главу 7 о времени жизни объекта.) Имя деструктора образовано из символа тильды (~) и имени класса. Вот определение деструктора класса String. Именно в нем мы вызываем операцию delete, чтобы освободить память, выделенную в конструкторе:

inline String: :~String() { delete [] _string; }

В обоих перегруженных операторах присваивания используется специальное ключевое слово this.

String namel( "orville" ), name2( "wilbur" );

Когда мы пишем:

namel = "Orville Wright";

С++ для начинающих

132

this является указателем, адресующим объект name1 внутри тела функции операции присваивания.

ptr->size();

this всегда указывает на объект класса, через который происходит вызов функции. Если obj[ 1024 ];

то внутри size() значением this будет адрес, хранящийся в ptr. Внутри операции взятия индекса this содержит адрес obj. Разыменовывая this (использованием *this),

inline String&

String::operator=( const char *s )

{

if ( ! s ) { _size = 0;

delete [] _string; _string = 0;

}

else {

_size = str1en( s ); delete [] _string;

_string = new char[ _size + 1 ]; strcpy( _string, s );

}

мы получаем сам объект. (Указатель this детально описан в разделе 13.4.)

return *this;

}

При реализации операции присваивания довольно часто допускают одну ошибку: забывают проверить, не является ли копируемый объект тем же самым, в который происходит копирование. Мы выполним эту проверку, используя все тот же указатель

inline String&

String::operator=( const String &rhs )

{

//в выражении

//namel = *pointer_to_string

//this представляет собой name1,

//rhs - *pointer_to_string.

this:

if ( this != &rhs ) {

Вот полный текст операции присваивания объекту String объекта того же типа:

С++ для начинающих

133

inline String&

String::operator=( const String &rhs )

{

if ( this != &rhs ) { delete [] _string; _size = rhs._size;

if ( ! rhs._string ) _string = 0;

else {

_string = new char[ _size + 1 ]; strcpy( _string, rhs._string );

}

}

return *this;

}

Операция взятия индекса практически совпадает с ее реализацией для массива Array,

#include <cassert>

inline char& String::operator[] ( int elem )

{

assert( elem >= 0 && elem < _size ); return _string[ elem ];

который мы создали в разделе 2.3:

}

Операторы ввода и вывода реализуются как отдельные функции, а не члены класса. (О причинах этого мы поговорим в разделе 15.2. В разделах 20.4 и 20.5 рассказывается о перегрузке операторов ввода и вывода библиотеки iostream.) Наш оператор ввода может прочесть не более 4095 символов. setw() предопределенный манипулятор, он читает из входного потока заданное число символов минус 1, гарантируя тем самым, что мы не переполним наш внутренний буфер inBuf. (В главе 20 манипулятор setw() рассматривается детально.) Для использования манипуляторов нужно включить

#include <iomanip>

inline istream&

operator>>( istream &io, String &s )

{

// искусственное ограничение: 4096 символов const int 1imit_string_size = 4096;

char inBuf[ limit_string_size ];

//setw() входит в библиотеку iostream

//он ограничивает размер читаемого блока до 1imit_string_size-l

io >> setw( 1imit_string_size ) >> inBuf;

s = mBuf; // String::operator=( const char* ); return io;

соответствующий заголовочный файл:

}

С++ для начинающих

134

Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при

inline ostream&

operator<<( ostream& os, const String &s )

{

return os << s.c_str();

реализации операции вывода:

}

Ниже приводится пример программы, использующей класс String. Эта программа берет слова из входного потока и подсчитывает их общее число, а также количество слов "the" и "it" и регистрирует встретившиеся гласные.

С++ для начинающих

135

#include <iostream> #inc1ude "String.h" int main() {

int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0;

// Слова "The" и "It"

// будем проверять с помощью operator==( const char* ) String but, the( "the" ), it( "it" );

// operator>>( ostream&, String& ) while ( cin >> buf ) {

++wdCnt;

// operator<<( ostream&, const String& ) cout << buf << ' ';

if ( wdCnt % 12 == 0 ) cout << endl;

// String::operator==( const String& ) and // String::operator==( const char* );

if ( buf == the | | buf == "The" ) ++theCnt;

else

if ( buf == it || buf == "It" ) ++itCnt;

// invokes String::s-ize()

for ( int ix =0; ix < buf.sizeO; ++ix )

{

// invokes String:: operator [] (int) switch( buf[ ix ] )

{

case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case '0': ++oCnt; break; case 'u': case 'U': ++uCnt; break; default: ++notVowe1; break;

}

}

}

// operator<<( ostream&, const String& ) cout << "\n\n"

<<"Слов: " << wdCnt << "\n\n"

<<"the/The: " << theCnt << '\n'

<<"it/It: " << itCnt << "\n\n"

<<"согласных: " < <notVowel << "\n\n"

<<"a: " << aCnt << '\n'

<<"e: " << eCnt << '\n'

<<"i: " << ICnt << '\n'

<<"o: " << oCnt << '\n'

<<"u: " << uCnt << endl;

}

Протестируем программу: предложим ей абзац из детского рассказа, написанного одним из авторов этой книги (мы еще встретимся с этим рассказом в главе 6). Вот результат работы программы:

С++ для начинающих

136

Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, 1ike a fiery bird in flight. A beautiful fiery bird, he tells her, magical but

untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks,

"I mean, Daddy, is there?"

Слов: 65 the/The: 2 it/It: 1

согласных: 190 a: 22

e: 30 i: 24 о: 10 u: 7

Упражнение 3.26

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

Упражнение 3.27

Модифицируйте тестовую программу так, чтобы она подсчитывала и согласные b, d, f, s, t.

Упражнение 3.28

Напишите функцию-член, подсчитывающую количество вхождений символа в строку

class String {

public:

// ...

int count( char ch ) const;

// ...

String, используя следующее объявление:

};

Упражнение 3.29

Реализуйте оператор конкатенации строк (+) так, чтобы он конкатенировал две строки и

class String {

public:

// ...

String operator+( const String &rhs ) const;

// ...

возвращал результат в новом объекте String. Вот объявление функции:

};