Класс string
Как мы только что видели, применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому достаточно распространена разработка собственного класса или классов для представления строкового типа– чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.
Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:
- инициализация массивом символов (строкой встроенного типа) или другим объектом типа string. Встроенный тип не обладает второй возможностью;
- копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy();
- доступ к отдельным символам строки для чтения и записи. Во встроенном массиве для этого применяется операция взятия индекса или косвенная адресация;
- сравнение двух строк на равенство. Для встроенного типа используется функция strcmp();
- конкатенация двух строк, получая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat(), однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat();
- вычисление длины строки. Узнать длину строки встроенного типа можно с помощью функции strlen();
- возможность узнать, пуста ли строка. У встроенных строк для этой цели приходится проверять два условия:
char str = 0;
//...
if ( ! str || ! *str )
return;
Класс 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 )
// инициализация сработала
Как скопировать одну строку в другую? С помощью обычной операции присваивания:
st2 = st3; // копируем st3 в st2
Для конкатенации строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки:
string s1( "hello, " );
string s2( "world\n" );
Мы можем получить третью строку, состоящую из конкатенации первых двух, таким образом:
string s3 = s1 + s2;
Если же мы хотим добавить s2 в конец s1, мы должны написать:
s1 += s2;
Операция сложения может конкатенировать объекты класса string не только между собой, но и со строками встроенного типа. Можно переписать пример, приведенный выше, так, чтобы специальные символы и знаки препинания представлялись встроенным типом, а значимые слова – объектами класса string:
const char *pc = ", ";
string s1( "hello" );
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(); // почти правильно
Функция 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
Объясните разницу в поведении следующих операторов цикла:
while ( st++ )
++cnt;
while ( *st++ )
++cnt;
Упражнение 3.14
Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string:
// ***** Реализация с использованием C-строк *****
#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;
}
Что эти программы делают?
Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?
Упражнение 3.15
Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения.