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

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

91

Упражнение 3.11

Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?

3.4. Строковые типы

В С++ поддерживаются два типа строк встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)

3.4.1. Встроенный строковый тип

Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций

//возвращает длину строки int strlen( const char* );

//сравнивает две строки

int strcmp( const char*, const char* );

// копирует одну строку в другую

для манипулирования строками. Например: char* strcpy( char*, const char* );

Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл:

#include <cstring>

Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал,

например

const char *st = "Цена бутылки вина\n";

компилятор помещает все символы строки в массив и затем присваивает st адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?

Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например:

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

92

while (*st++ ) { ... }

st разыменовывается, и получившееся значение проверяется на истинность. Любое отличное от нуля значение считается истинным, и, следовательно, цикл заканчивается, когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1 к указателю st и таким образом сдвигает его к следующему символу.

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

int string_length( const char *st )

{

int cnt = 0; if ( st )

while ( *st++ ) ++cnt;

return cnt;

перед операцией разыменования его следует проверять:

}

Строка встроенного типа может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (тогда у нас вообще нет никакой строки) или указывает на массив, состоящий из одного нулевого символа (то есть на строку, не содержащую ни одного значимого символа).

//pc1 не адресует никакого массива символов char *pc1 = 0;

//pc2 адресует нулевой символ

const char *pc2 = "";

Для начинающего программиста использование строк встроенного типа чревато ошибками из-за слишком низкого уровня реализации и невозможности обойтись без адресной арифметики. Ниже мы покажем некоторые типичные погрешности, допускаемые новичками. Задача проста: вычислить длину строки. Первая версия неверна.

#include <iostream>

Исправьте ее.

const char *st = "Цена бутылки вина\n";

int main() { int len = 0;

while ( st++ ) ++len;

cout << len << ": " << st; return 0;

}

В этой версии указатель st не разыменовывается. Следовательно, на равенство 0 проверяется не символ, на который указывает st, а сам указатель. Поскольку изначально этот указатель имел ненулевое значение (адрес строки), то он никогда не станет равным нулю, и цикл будет выполняться бесконечно.

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

93

Во второй версии программы

эта погрешность устранена. Программа успешно

#include <iostream>

заканчивается, однако полученный результат неправилен. Где мы не правы на этот раз? const char *st = "Цена бутылки вина\n";

int main()

{

int len = 0;

while ( *st++ ) ++len;

cout << len << ": " << st << endl; return 0;

}

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

st = st – len;

Можно попробовать исправить эту ошибку: cout << len << ": " << st;

Теперь наша программа выдает что-то осмысленное, но не до конца. Ответ выглядит так:

18: ена бутылки вина

Мы забыли учесть, что заключительный нулевой символ не был включен в подсчитанную длину. st должен быть смещен на длину строки плюс 1. Вот, наконец, правильный оператор:

st = st – len - 1;

авот и и правильный результат:

18:Цена бутылки вина

Однако нельзя сказать, что наша программа выглядит элегантно. Оператор

st = st – len - 1;

добавлен для того, чтобы исправить ошибку, допущенную на раннем этапе проектирования программы, – непосредственное увеличение указателя st. Этот оператор не вписывается в логику программы, и код теперь трудно понять. Исправления такого рода часто называют заплатками нечто, призванное заткнуть дыру в существующей программе. Гораздо лучшим решением было бы пересмотреть логику. Одним из

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

94

вариантов в нашем случае может быть определение второго указателя, инициализированного значением st:

const char *p = st;

Теперь p можно использовать в цикле вычисления длины, оставив значение st неизменным:

while ( *p++ )

3.4.2. Класс string

Как мы только что видели, применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому

достаточно распространена разработка собственного класса или классов для представления строкового типа чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.

Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:

инициализация массивом символов (строкой встроенного типа) или другим объектом типа string. Встроенный тип не обладает второй возможностью;

копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy();

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

сравнение двух строк на равенство. Для встроенного типа используется функция strcmp();

конкатенация двух строк, получая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat(), однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat();

вычисление длины строки. Узнать длину строки встроенного типа можно с помощью функции strlen();

возможность узнать, пуста ли строка. У встроенных строк для этой цели

char str = 0; //...

if ( ! str || ! *str )

приходится проверять два условия: return;

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

95

Класс string стандартной библиотеки С++ реализует все перечисленные операции (и гораздо больше, как мы увидим в главе 6). В данном разделе мы научимся пользоваться основными операциями этого класса.

Для того чтобы использовать объекты класса string, необходимо включить соответствующий заголовочный файл:

#include <string>

Вот пример строки из предыдущего раздела, представленной объектом типа string и

#include <string>

инициализированной строкой символов: string st( "Цена бутылки вина\n" );

Длину строки возвращает функция-член size() (длина не включает завершающий

cout << "Длина "

<<st

<<": " << st.size()

<<" символов, включая символ новой строки\n";

нулевой символ).

Вторая форма определения строки задает пустую строку:

string st2; // пустая строка

Как мы узнаем, пуста ли строка? Конечно, можно сравнить ее длину с 0:

if ( ! st.size() )

// правильно: пустая

Однако есть и специальный метод empty(), возвращающий true для пустой строки и false для непустой:

if ( st.empty() )

// правильно: пустая

Третья форма конструктора инициализирует объект типа string другим объектом того же типа:

string st3( st );

Строка st3 инициализируется строкой st. Как мы можем убедиться, что эти строки совпадают? Воспользуемся оператором сравнения (==):

if ( st == st3 )

// инициализация сработала

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

96

Как скопировать одну строку в другую? С помощью обычной операции присваивания:

st2 = st3; // копируем st3 в st2

Для конкатенации строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки:

string s1( "hello, " ); string s2( "world\n" );

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

string s3 = s1 + s2;

Если же мы хотим добавить s2 в конец s1, мы должны написать:

s1 += s2;

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

string s1( "hello" );

const char *pc = ", "; string s2( "world" );

string s3 = s1 + pc + s2 + "\n";

Подобные выражения работают потому, что компилятор знает, как автоматически преобразовывать объекты встроенного типа в объекты класса string. Возможно и простое присваивание встроенной строки объекту string:

string s1;

const char *pc = "a character array";

s1 = pc; // правильно

Обратное преобразование, однако, не работает. Попытка выполнить следующую инициализацию строки встроенного типа вызовет ошибку компиляции:

char *str = s1; // ошибка компиляции

Чтобы осуществить такое преобразование, необходимо явно вызвать функцию-член с несколько странным названием c_str():

char *str = s1.c_str(); // почти правильно

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

97

Функция c_str() возвращает указатель на символьный массив, содержащий строку объекта string в том виде, в каком она находилась бы во встроенном строковом типе.

Приведенный выше пример инициализации указателя char *str все еще не совсем корректен. c_str() возвращает указатель на константный массив, чтобы предотвратить возможность непосредственной модификации содержимого объекта через этот указатель,

имеющий тип

const char *

(В следующем разделе мы расскажем о ключевом слове const). Правильный вариант инициализации выглядит так:

const char *str = s1.c_str(); // правильно

К отдельным символам объекта типа string, как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все

string str( "fa.disney.com" );

int size = str.size();

for ( int ix = 0; ix < size; ++ix ) if ( str[ ix ] == '.' )

точки символами подчеркивания: str[ ix ] = '_';

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

replace( str.begin(), str.end(), '.', '_' );

replace() один из обобщенных алгоритмов, с которыми мы познакомились в разделе 2.8 и которые будут детально разобраны в главе 12. Эта функция пробегает диапазон от begin() до end(), которые возвращают указатели на начало и конец строки, и заменяет элементы, равные третьему своему параметру, на четвертый.

Упражнение 3.12

(a)char ch = "The long and winding road";

(b)int ival = &ch;

(c)char *pc = &ival;

(d)string st( &ch );

(e)pc = 0; (i) pc = '0';

(f)

st

=

pc;

(j)

st

=

&ival;

(g)

ch

=

pc[0];

(k)

ch

=

*pc;

Найдите ошибки в приведенных ниже операторах:

(h) pc = st;

(l) *pc = ival;

Упражнение 3.13 Объясните разницу в поведении следующих операторов цикла:

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

98

while ( st++ )

++cnt;

while ( *st++ ) ++cnt;

Упражнение 3.14

Даны две семантически эквивалентные программы. Первая использует встроенный

// ***** Реализация с использованием C-строк *****

строковый тип, вторая класс string:

#include <iostream> #include <cstring>

int main()

{

int errors = 0;

const char *pc = "a very long literal string";

for ( int ix = 0; ix < 1000000; ++ix )

{

int len = strlen( pc );

char *pc2 = new char[ len + 1 ]; strcpy( pc2, pc );

if ( strcmp( pc2, pc )) ++errors;

delete [] pc2;

}

cout << "C-строки: "

<< errors << " ошибок.\n";

}

// ***** Реализация с использованием класса string *****

#include <iostream> #include <string>

int main()

{

int errors = 0;

string str( "a very long literal string" );

for ( int ix = 0; ix < 1000000; ++ix )

{

int len = str.size(); string str2 = str; if ( str != str2 )

}

cout << "класс string: "

<< errors << " ошибок.\n;