Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп
Шрифт:
Интервал:
Закладка:
const char* p = "asdf"; // теперь вы не сможете записать символ
// в строку "asdf" с помощью указателя p
Эта рекомендация относится как к языку C, так и к языку C++.
Функция strchr() из языка C порождает аналогичную, но более трудноуловимую проблему. Рассмотрим пример.
char* strchr(const char* s,int c); /* найти c в константной строке s
(не C++) */
const char aa[] = "asdf"; /* aa — массив констант */
char* q = strchr(aa,'d'); /* находит символ 'd' */
*q = 'x'; /* изменяет символ 'd' в строке aa на 'x' */
Опять-таки, этот код является недопустимым ни в языке С, ни в языке С++, но компиляторы языка C не могут найти ошибку. Иногда это явление называют трансмутацией (transmutation): функция превращает константы в не константы, нарушая разумные предположения о коде.
В языке C++ эта проблема решается с помощью немного измененного объявления стандартной библиотечной функции strchr().
char const* strchr(const char* s, int c); // найти символ c
// в константной строке s
char* strchr(char* s, int c); // найти символ c в строке s
Аналогично объявляется функция strstr().
27.5.2. Операции над байтами
В далеком средневековье (в начале 1980-х годов), еще до изобретения указателя void*, программисты, работавшие на языках C (и C++), для манипуляции байтами использовали строки. В настоящее время основные стандартные библиотечные функции для работы с памятью имеют параметры типа void* и возвращают указатели типа void*, чтобы предупредить пользователей о непосредственной работе с памятью без контроля типов.
/* копирует n байтов из строки s2 в строку s1 (как функция strcpy): */
void* memcpy(void* s1, const void* s2, size_t n);
/* копирует n байтов из строки s2 в строку s1
(диапазон [s1:s1+n] может перекрываться с диапазоном [s2:s2+n]): */
void* memmove(void* s1, const void* s2, size_t n);
/* сравнивает n байтов из строки s2 в строку s1
(как функция strcmp): */
int memcmp(const void* s1, const void* s2, size_t n);
/* находит символ c (преобразованный в тип unsigned char)
среди первых n байтов строки s: */
void* memchr(const void* s, int c, size_t n);
/* копирует символ c (преобразованный в тип unsigned char)
в каждый из n байтов строки, на который ссылается указатель s: */
void* memset(void* s, int c, size_t n);
Не используйте эти функции в программах на языке C++. В частности, функция memset() обычно влияет на гарантии, выданные конструкторами.
27.5.3. Пример: функция strcpy()
Определение функции strcpy() представляет собой печально известный пример лаконичного стиля, который допускает язык C (и C++) .
char* strcpy(char* p, const char* q)
{
while (*p++ = *q++);
return p;
}
Объяснение, почему этот код на самом деле копирует С-строку q в С-строку p, мы оставляем читателям в качестве упражнения.
ПОПРОБУЙТЕ
Является ли корректной реализация функции strcpy()? Объясните почему.
Если вы не можете аргументировать свой ответ, то не вправе считать себя программистом, работающим на языке C (однако вы можете быть компетентным в других языках программирования). Каждый язык имеет свои собственные идиомы, это относится и к языку C.
27.5.4. Вопросы стиля
Мы потихоньку втягиваемся в длинные и часто яростно оспариваемые вопросы стиля, которые, впрочем, часто не имеют большого значения. Мы объявляем указатель следующим образом:
char* p; // p — указатель на переменную типа char
Мы не принимаем стиль, продемонстрированный ниже.
char *p; /* p — нечто, что можно разыменовать, чтобы получить символ */
Пробел совершенно игнорируется компилятором, но для программиста он имеет значение. Наш стиль (общепринятый среди программистов на языке С++) подчеркивает тип объявляемой переменной, в то время как альтернативный стиль (общепринятый среди программистов на языке С) делает упор на использовании переменной. Мы не рекомендуем объявлять несколько переменных в одной строке.
char c, *p, a[177], *f(); /* разрешено, но может ввести в заблуждение */
Такие объявления часто можно встретить в старых программах. Вместо этого объявления следует размещать в нескольких строках, используя свободное место для комментариев и инициализации.
char c = 'a'; /* символ завершения ввода для функции f() */
char* p = 0; /* последний символ, считанный функцией f() */
char a[177]; /* буфер ввода */
char* f(); /* считывает данные в буфер a;
возвращает указатель на первый считанный символ */
Кроме того, выбирайте осмысленные имена.
27.6. Ввод-вывод: заголовок stdio
В языке С нет потоков ввода-вывода iostream, поэтому мы используем стандартный механизм ввода-вывода языка С, определенный в заголовочном файле <stdio.h>. Эквивалентами потоков ввода и вывода cin и cout из языка С++ в языке С являются потоки stdin и stdout. Стандартные средства ввода-вывода языка С и потоки iostream могут одновременно использоваться в одной и той же программе (для одних и тех же потоков ввода-вывода), но мы не рекомендуем это делать. Если вам необходимо совместно использовать эти механизмы, хорошенько разберитесь в них (обратите особое внимание на функцию ios_base::sync_with_stdio()), используя хороший учебник. См. также раздел Б.10.
27.6.1. Вывод
Наиболее популярной и полезной функцией библиотеки stdio является функция printf(). Основным предназначением функции printf() является вывод С-строки.
#include<stdio.h>
void f(const char* p)
{
printf("Hello, World!n");
printf(p);
}
Это не очень интересно. Намного интереснее то, что функция printf() может получать любое количество аргументов и начальную управляющую строку, которая определяет, как вывести дополнительные аргументы. Объявление функции printf() в языке C выглядит следующим образом:
int printf(const char* format, ...);
Многоточие (...) означает “и, возможно, остальные аргументы”. Мы можем вызвать функцию printf() так:
void f1(double d, char* s, int i, char