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

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

 float fp, *fp2;
 Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:
 string *ps;
 string* ps;
 Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую:
 //внимание: ps2 не указатель на строку!
 string* ps, ps2;
 Можно предположить, что и ps, и ps2 являются указателями, хотя указатель – только первый из них.
 Если значение указателя равно 0, значит, он не содержит никакого адреса объекта.
 Пусть задана переменная типа int:
 int ival = 1024;
 Ниже приводятся примеры определения и использования указателей на int pi и pi2:
 //pi инициализирован нулевым адресом
 int *pi = 0;
 
 // pi2 инициализирован адресом ival
 int *pi2 = &ival;
 
 // правильно: pi и pi2 содержат адрес ival
 pi = pi2;
 
 // pi2 содержит нулевой адрес
 pi2 = 0;
 Указателю не может быть присвоена величина, не являющаяся адресом:
 // ошибка: pi не может принимать значение int
 pi = ival
 Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные:
 double dval;
 double *ps = &dval;
 то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:
 // ошибки компиляции
 // недопустимое присваивание типов данных: int* <== double*
 pi = pd
 pi = &dval;
 Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них.
 Конечно, бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес с каким-то другим). Для разрешения таких ситуаций введен специальный указатель void, который может указывать на любой тип данных, и следующие выражения будут правильны:
 // правильно: void* может содержать
 // адреса любого типа
 void *pv = pi;
 pv = pd;
 Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)
 Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:
 int ival = 1024;, ival2 = 2048;
 int *pi = &ival;
 мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:
 // косвенное присваивание переменной ival значения ival2
 *pi = ival2;
 
 // косвенное использование переменной ival как rvalue и lvalue
 *pi = abs(*pi); // ival = abs(ival);
 *pi = *pi + 1; // ival = ival + 1;
 Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем результат типа int*
 int *pi = &ival;
 Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды.
 int **ppi = π
 int *pi2 = *ppi;
 
 cout << "Значение ival\n"
  << "явное значение: " << ival << "\n"
  << "косвенная адресация: " << *pi << "\n"
  << "дважды косвенная адресация: " << **ppi << "\n"
  << endl;
 Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия:
 int i, j, k;
 int *pi = &i;
 
 // i = i + 2
 *pi = *pi + 2;
 
 // увеличение адреса, содержащегося в pi, на 2
 pi = pi + 2;
 К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы.
 Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора:
 int ia[10];
 int *iter = &ia[0];
 int *iter_end = &ia[10];
 
 while (iter != iter_end) {
  do_something_with_value (*iter);
  ++iter;
 }
 Упражнение 3.8
 Даны определения переменных:
 int ival = 1024, ival2 = 2048;
 int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;
 Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?
 (a) ival = *pi3; (e) pi1 = *pi3;
 (b) *pi2 = *pi3; (f) ival = *pi1;
 (c) ival = pi2; (g) pi1 = ival;
 (d) pi2 = *pi1; (h) pi3 = &pi2;
 Упражнение 3.9
 Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код
 pi = &ival;
 pi = pi + 1024;
 почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?
 Упражнение 3.10
 Данная программа содержит ошибку, связанную с неправильным использованием указателей:
 int foobar(int *pi) {
  *pi = 1024;
  return *pi;
 }
 
 int main() {
  int *pi2 = 0;
  int ival = foobar(pi2);
  return 0;
 }
 В чем состоит ошибка? Как можно ее исправить?
 Упражнение 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
 Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал, например
 const char *st = "Цена бутылки вина\n";
 компилятор помещает все символы строки в массив и затем присваивает st адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?
 Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например:
 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
 const char *st = "Цена бутылки вина\n";
 
 int main() {
  int len = 0;
  while ( st++ ) ++len;
 
  cout << len << ": " << st;
  return 0;
 }
 В этой версии указатель st не разыменовывается. Следовательно, на равенство 0 проверяется не символ, на который указывает st, а сам указатель. Поскольку изначально этот указатель имел ненулевое значение (адрес строки), то он никогда не станет равным нулю, и цикл будет выполняться бесконечно.
 Во второй версии программы эта погрешность устранена. Программа успешно заканчивается, однако полученный результат неправилен. Где мы не правы на этот раз?
 #include
 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. Этот оператор не вписывается в логику программы, и код теперь трудно понять. Исправления такого рода часто называют заплатками – нечто, призванное заткнуть дыру в существующей программе. Гораздо лучшим решением было бы пересмотреть логику. Одним из вариантов в нашем случае может быть определение второго указателя, инициализированного значением 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;
 Класс string стандартной библиотеки С++ реализует все перечисленные операции (и гораздо больше, как мы увидим в главе 6). В данном разделе мы научимся пользоваться основными операциями этого класса.
 Для того чтобы использовать объекты класса string, необходимо включить соответствующий заголовочный файл:
 #include
 Вот пример строки из предыдущего раздела, представленной объектом типа string и инициализированной строкой символов:
 #include
 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
 #include
 
 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 ];

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

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