Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

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

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;

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

99

}

Что эти программы делают?

Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?

Упражнение 3.15

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

3.5. Спецификатор const

for ( int index = 0; index < 512; ++index )

Возьмем следующий пример кода:

... ;

С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной...

Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в 80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?

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

index < bufSize

В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления

int bufSize = 512; // размер буфера ввода

// ...

всего одного объекта! Теперь значение 512 локализовано.

for ( int index = 0; index < bufSize; ++index )

// ...

Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок использование операции присваивания (=) вместо сравнения (==):

// случайное изменение значения bufSize

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

100

if ( bufSize = 1 )

//...

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

Использование спецификатора const решает данную проблему. Объявив объект как

const int bufSize = 512; // размер буфера ввода

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

// ошибка: попытка присваивания значения константе

компиляции.

if ( bufSize = 0 ) ...

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

const double pi; // ошибка: неинициализированная константа

Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес

const double minWage = 9.60;

// правильно? ошибка?

константы некоторому указателю? double *ptr = &minWage;

Должен ли компилятор разрешить подобное присваивание? Поскольку minWage константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать:

*ptr += 1.40; // изменение объекта minWage!

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

Что же, мы лишены возможности использовать указатели на константы? Нет. Для этого существуют указатели, объявленные со спецификатором const:

const double *cptr;

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

101

где cptr указатель на объект типа const double. Тонкость заключается в том, что сам

 

 

const double *pc = 0;

 

 

 

 

const double minWage = 9.60;

 

 

// правильно: не можем изменять minWage с помощью pc

 

 

pc = &minWage;

 

 

double dval = 3.14;

 

 

// правильно: не можем изменять minWage с помощью pc

 

 

// хотя dval и не константа

 

 

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

 

 

dval = 3.14159; //правильно

 

указатель не константа, а значит, мы можем изменять его значение. Например:

 

 

*pc = 3.14159; // ошибка

 

 

 

 

 

 

Адрес константного объекта присваивается только указателю на константу. Вместе с тем,

 

такому указателю может быть присвоен и адрес обычной переменной:

 

 

pc = &dval;

 

 

 

 

 

 

Константный указатель не позволяет изменять адресуемый им объект с помощью

 

косвенной адресации. Хотя dval в примере выше и не является константой, компилятор

 

не допустит изменения переменной dval через pc. (Опять-таки потому, что он не в

 

состоянии определить, адрес какого объекта может содержать указатель в произвольный

 

момент выполнения программы.)

 

В реальных программах указатели на константы чаще всего употребляются как

 

формальные параметры функций. Их использование дает гарантию, что объект,

 

переданный в функцию в качестве фактического аргумента, не будет изменен этой

 

 

// В реальных программах указатели на константы чаще всего

 

 

 

 

// употребляются как формальные параметры функций

 

функцией. Например:

 

 

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

 

 

 

 

 

 

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

 

функциях.)

 

Существуют и константные указатели. (Обратите внимание на разницу между

 

константным указателем и указателем на константу!). Константный указатель может

 

адресовать как константу, так и переменную. Например:

 

 

int errNumb = 0;

 

 

 

 

int *const currErr = &errNumb;

 

 

 

 

Здесь curErr константный указатель на неконстантный объект. Это значит, что мы не

 

можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию.

 

Вот как мог бы быть использован указатель curErr: