Главная страница
Навигация по странице:

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

  • 3.4.2. Класс string

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница10 из 93
    1   ...   6   7   8   9   10   11   12   13   ...   93
    90
    int *iter = &ia[0]; int *iter_end = &ia[10]; while (iter != iter_end) { do_something_with_value (*iter);
    ++iter;
    }
    Упражнение 3.8
    Даны определения переменных: int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;
    Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?
    (d) pi2 = *pi1; (h) pi3 = &pi2;
    Упражнение 3.9
    Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код pi = pi + 1024; почти наверняка приведет к тому, что pi будет указывать на случайную область памяти.
    Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?
    Упражнение 3.10
    Данная программа содержит ошибку, связанную с неправильным использованием указателей:
    }
    В чем состоит ошибка? Как можно ее исправить? int ival = 1024, ival2 = 2048;
    (a) ival = *pi3; (e) pi1 = *pi3;
    (b) *pi2 = *pi3; (f) ival = *pi1;
    (c) ival = pi2; (g) pi1 = ival; pi = &ival; int foobar(int *pi) {
    *pi = 1024; return *pi;
    } int main() { int *pi2 = 0; int ival = foobar(pi2); return 0;

    С++ для начинающих
    91
    Упражнение 3.11
    Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована?
    Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?
    3.4.
    Строковые типы
    В С++ поддерживаются два типа строк – встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)
    3.4.1.
    Встроенный строковый тип
    Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С.
    Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций для манипулирования строками. Например: char* strcpy( char*, const char* );
    Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл:
    #include
    Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал, например const char *st = "
    Цена бутылки вина\n"; компилятор помещает все символы строки в массив и затем присваивает st адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?
    Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например:
    // возвращает длину строки int strlen( const char* );
    // сравнивает две строки int strcmp( const char*, const char* );
    // копирует одну строку в другую

    С++ для начинающих
    92
    while (*st++ ) { ... } st разыменовывается, и получившееся значение проверяется на истинность. Любое отличное от нуля значение считается истинным, и, следовательно, цикл заканчивается, когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1 к указателю st и таким образом сдвигает его к следующему символу.
    Вот как может выглядеть реализация функции, возвращающей длину строки. Отметим, что, поскольку указатель может содержать нулевое значение (ни на что не указывать), перед операцией разыменования его следует проверять:
    }
    Строка встроенного типа может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (тогда у нас вообще нет никакой строки) или указывает на массив, состоящий из одного нулевого символа (то есть на строку, не содержащую ни одного значимого символа).
    // pc1 не адресует никакого массива символов char *pc1 = 0;
    // pc2 адресует нулевой символ const char *pc2 = "";
    Для начинающего программиста использование строк встроенного типа чревато ошибками из-за слишком низкого уровня реализации и невозможности обойтись без адресной арифметики. Ниже мы покажем некоторые типичные погрешности, допускаемые новичками. Задача проста: вычислить длину строки. Первая версия неверна.
    Исправьте ее. const char *st = "
    Цена бутылки вина\n"; int main() { int len = 0; while ( st++ ) ++len; cout << len << ": " << st; return 0;
    }
    В этой версии указатель st не разыменовывается. Следовательно, на равенство 0 проверяется не символ, на который указывает st, а сам указатель. Поскольку изначально этот указатель имел ненулевое значение (адрес строки), то он никогда не станет равным нулю, и цикл будет выполняться бесконечно. int string_length( const char *st )
    { int cnt = 0; if ( st ) while ( *st++ )
    ++cnt; return cnt;
    #include

    С++ для начинающих
    93
    Во второй версии программы эта погрешность устранена. Программа успешно заканчивается, однако полученный результат неправилен. Где мы не правы на этот раз? const char *st = "
    Цена бутылки вина\n"; int main()
    { int len = 0; while ( *st++ ) ++len; cout << len << ": " << st << endl; return 0;
    }
    Ошибка состоит в том, что после завершения цикла указатель st адресует не исходный символьный литерал, а символ, расположенный в памяти после завершающего нуля этого литерала. В этом месте может находиться что угодно, и выводом программы будет случайная последовательность символов.
    Можно попробовать исправить эту ошибку: cout << len << ": " << st;
    Теперь наша программа выдает что-то осмысленное, но не до конца. Ответ выглядит так:
    18: ена бутылки вина
    Мы забыли учесть, что заключительный нулевой символ не был включен в подсчитанную длину. st должен быть смещен на длину строки плюс 1. Вот, наконец, правильный оператор: st = st – len - 1; а вот и и правильный результат:
    18:
    Цена бутылки вина
    Однако нельзя сказать, что наша программа выглядит элегантно. Оператор st = st – len - 1; добавлен для того, чтобы исправить ошибку, допущенную на раннем этапе проектирования программы, – непосредственное увеличение указателя st. Этот оператор не вписывается в логику программы, и код теперь трудно понять. Исправления такого рода часто называют заплатками – нечто, призванное заткнуть дыру в существующей программе. Гораздо лучшим решением было бы пересмотреть логику. Одним из
    #include st = st – len;

    С++ для начинающих
    94
    вариантов в нашем случае может быть определение второго указателя, инициализированного значением st: const char *p = st;
    Теперь p можно использовать в цикле вычисления длины, оставив значение st неизменным: while ( *p++ )
    3.4.2.
    Класс string
    Как мы только что видели, применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому достаточно распространена разработка собственного класса или классов для представления строкового типа – чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.
    Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:

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

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

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

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

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

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

    возможность узнать, пуста ли строка. У встроенных строк для этой цели приходится проверять два условия: return; char str = 0;
    //... if ( ! str || ! *str )

    С++ для начинающих
    95
    Класс string стандартной библиотеки С++ реализует все перечисленные операции (и гораздо больше, как мы увидим в главе 6). В данном разделе мы научимся пользоваться основными операциями этого класса.
    Для того чтобы использовать объекты класса string, необходимо включить соответствующий заголовочный файл:
    #include
    Вот пример строки из предыдущего раздела, представленной объектом типа string и инициализированной строкой символов: string st( "
    Цена бутылки вина\n" );
    Длину строки возвращает функция-член size() (длина не включает завершающий нулевой символ).
    Вторая форма определения строки задает пустую строку: string st2; // пустая строка
    Как мы узнаем, пуста ли строка? Конечно, можно сравнить ее длину с 0: if ( ! st.size() )
    // правильно: пустая
    Однако есть и специальный метод empty(), возвращающий true для пустой строки и false для непустой: if ( st.empty() )
    // правильно: пустая
    Третья форма конструктора инициализирует объект типа string другим объектом того же типа: string st3( st );
    Строка st3 инициализируется строкой st. Как мы можем убедиться, что эти строки совпадают? Воспользуемся оператором сравнения (==): if ( st == st3 )
    // инициализация сработала
    #include cout << "
    Длина "
    << st
    << ": " << st.size()
    << " символов, включая символ новой строки\n";

    С++ для начинающих
    96
    Как скопировать одну строку в другую? С помощью обычной операции присваивания: 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 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(); // почти правильно string s1( "hello" );

    С++ для начинающих
    97
    Функция c_str() возвращает указатель на символьный массив, содержащий строку объекта string в том виде, в каком она находилась бы во встроенном строковом типе.
    Приведенный выше пример инициализации указателя char *str все еще не совсем корректен. c_str() возвращает указатель на константный массив, чтобы предотвратить возможность непосредственной модификации содержимого объекта через этот указатель, имеющий тип const char *
    (В следующем разделе мы расскажем о ключевом слове const). Правильный вариант инициализации выглядит так: const char *str = s1.c_str(); // правильно
    К отдельным символам объекта типа string, как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все точки символами подчеркивания: str[ ix ] = '_';
    Вот и все, что мы хотели сказать о классе string прямо сейчас. На самом деле, этот класс обладает еще многими интересными свойствами и возможностями. Скажем, предыдущий пример реализуется также вызовом одной-единственной функции replace(): replace( str.begin(), str.end(), '.', '_' ); replace()
    – один из обобщенных алгоритмов, с которыми мы познакомились в разделе
    2.8 и которые будут детально разобраны в главе 12. Эта функция пробегает диапазон от begin()
    до end(), которые возвращают указатели на начало и конец строки, и заменяет элементы, равные третьему своему параметру, на четвертый.
    Упражнение 3.12
    Найдите ошибки в приведенных ниже операторах:
    (h) pc = st; (l) *pc = ival;
    Упражнение 3.13
    Объясните разницу в поведении следующих операторов цикла: string str( "fa.disney.com" ); int size = str.size(); for ( int ix = 0; ix < size; ++ix ) if ( str[ ix ] == '.' )
    (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;

    С++ для начинающих
    98
    ++cnt; while ( *st++ )
    ++cnt;
    Упражнение 3.14
    Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string:
    #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 ]; strcpy( pc2, pc ); if ( strcmp( pc2, pc ))
    ++errors; delete [] pc2;
    } cout << "C- строки: "
    << errors << " ошибок.\n";
    }
    #include
    #include 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; while ( st++ )
    // *****
    Реализация с использованием C-строк *****
    // *****
    Реализация с использованием класса string *****

    С++ для начинающих
    99
    }
    Что эти программы делают?
    Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?
    Упражнение 3.15
    Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения.
    3.5.
    Спецификатор const
    Возьмем следующий пример кода:
    ... ;
    С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной...
    Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в
    80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?
    Обе эти проблемы решаются одновременно: нужно создать объект со значением 512.
    Присвоив ему осмысленное имя, например bufSize, мы сделаем программу гораздо более понятной: ясно, с чем именно сравнивается переменная цикла. index < bufSize
    В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано. for ( int index = 0; index < bufSize; ++index )
    // ...
    Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания
    (=) вместо сравнения (==):
    // случайное изменение значения bufSize for ( int index = 0; index < 512; ++index ) int bufSize = 512; // размер буфера ввода
    // ...

    С++ для начинающих
    100
    if ( bufSize = 1 )
    // ...
    В результате выполнения этого кода значение bufSize станет равным 1, что может привести к совершенно непредсказуемому поведению программы. Ошибки такого рода обычно очень тяжело обнаружить, поскольку они попросту не видны.
    Использование спецификатора const решает данную проблему. Объявив объект как const int bufSize = 512; // размер буфера ввода мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции. if ( bufSize = 0 ) ...
    Раз константе нельзя присвоить значение, она должна быть инициализирована в месте своего определения. Определение константы без ее инициализации также вызывает ошибку компиляции: const double pi; // ошибка: неинициализированная константа
    Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес константы некоторому указателю? double *ptr = &minWage;
    Должен ли компилятор разрешить подобное присваивание? Поскольку minWage – константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать:
    *ptr += 1.40; // изменение объекта minWage!
    Как правило, компилятор не в состоянии уберечь от использования указателей и не сможет сигнализировать об ошибке в случае подобного их употребления. Для этого требуется слишком глубокий анализ логики программы. Поэтому компилятор просто запрещает присваивание адресов констант обычным указателям.
    Что же, мы лишены возможности использовать указатели на константы? Нет. Для этого существуют указатели, объявленные со спецификатором const: const double *cptr;
    // ошибка: попытка присваивания значения константе const double minWage = 9.60;
    // правильно? ошибка?

    С++ для начинающих
    1   ...   6   7   8   9   10   11   12   13   ...   93


    написать администратору сайта