Программирование — страница 47 из 57

Язык программирования С

“С — это язык программирования

со строгим контролем типов и слабой проверкой”.

Деннис Ритчи (Dennis Ritchie)


Данная глава представляет собой краткий обзор языка программирования С и его стандартной библиотеки с точки зрения человека, знающего язык С++. В ней перечислены свойства языка С++, которых нет в языке C, и приведены примеры того, как программистам на языке С обойтись без них. Рассмотрены различия между языками C и C++, а также вопросы их одновременного использования. Приведены примеры ввода-вывода, список операций, управление памятью, а также иллюстрации операций над строками. 

27.1. Языки С и С++: братья

 Язык программирования С был изобретен и реализован Деннисом Ритчи (Dennis Ritchie) из компании Bell Labs. Он изложен в книге The C Programming Language Брайана Кернигана (Brian Kernighan) и Денниса Ритчи (Dennis Ritchie) (в разговорной речи известной как “K&R”), которая, вероятно, является самым лучшим введением в язык С и одним из лучших учебников по программированию (см. раздел 22.2.5). Текст исходного определения языка С++ был редакцией определения языка С, написанного в 1980 году Деннисом Ритчи. После этого момента оба языка стали развиваться самостоятельно. Как и язык C++, язык C в настоящее время определен стандартом ISO.

Мы рассматриваем язык С в основном как подмножество языка С++. Следовательно, с точки зрения языка С++ проблемы описания языка С сводятся к двум вопросам.

• Описать те моменты, в которых язык С не является подмножеством языка C++.

• Описать те свойства языка С++, которых нет в языке C, и те возможности и приемы, с помощью которых этот недостаток можно компенсировать.


 Исторически современный язык С++ и современный язык С являются “братьями”. Они оба являются наследниками “классического С”, диалекта языка С, описанного в первом издании книги Кернигана и Ритчи The C Programming Language, в который были добавлены присваивание структур и перечислений.



В настоящее время практически повсеместно используется версия C89 (описанная во втором издании книги K&R[12]). Именно эту версию мы излагаем в данном разделе. Помимо этой версии, кое-где все еще по-прежнему используется классический С, и есть несколько примеров использования версии C99, но это не должно стать проблемой для читателей, если они знают языки C++ и C89.

Языки С и С++ являются детищами Исследовательского центра компьютерных наук компании Bell Labs (Computer Science Research Center of Bell Labs), МюррейХилл, штат Нью-Джерси (Murray Hill, New Jersey) (кстати, мой офис находился рядом с офисом Денниса Ритчи и Брайана Кернигана).



 Оба языка в настоящее время определены и контролируются комитетами по стандартизации ISO. Для каждого языка разработано множество реализаций. Часто эти реализации поддерживают оба языка, причем желаемый язык устанавливается путем указания расширения исходного файла. По сравнению с другими языками, оба языка, С и С++, распространены на гораздо большем количестве платформ.

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

• Ядра операционных систем.

• Драйверы устройств.

• Встроенные системы.

• Компиляторы.

• Системы связи.


Между эквивалентными программами, написанными на языках С и С++, нет никакой разницы в производительности.

Как и язык C++, язык C очень широко используется. Взятые вместе, они образуют крупнейшее сообщество по разработке программного обеспечения на Земле.

27.1.1. Совместимость языков С и С++

 Часто приходится встречать название “C/C++.” Однако такого языка нет. Употребление такого названия обычно является признаком невежества. Мы используем такое название только в контексте вопросов совместимости и когда говорим о крупном сообществе программистов, использующих оба этих языка.

 Язык С++ в основном, но не полностью, является надмножеством языка С. За несколькими очень редкими исключениями конструкции, общие для языков С и С++, имеют одинаковый смысл (семантику). Язык С++ был разработан так, чтобы он был “как можно ближе к языку С++, но не ближе, чем следует”. Он преследовал несколько целей.

• Простота перехода.

• Совместимость.


Многие свойства, оказавшиеся несовместимыми с языком С, объясняются тем, что в языке С++ существует более строгая проверка типов.

Примером программы, являющейся допустимой на языке С, но не на языке С++, является программа, в которой ключевые слова из языка С++ используются в качестве идентификаторов (раздел 27.3.2).


int class(int new, int bool); /* C, но не C++ */


Примеры, в которых семантика конструкции, допустимой в обоих языках, отличается в них, найти труднее, но все же они существуют.


int s = sizeof('a'); /* sizeof(int), обычно 4 в языке C и 1 в языке C++ */


Строковый литерал, такой как

'a'
, в языке С имеет тип
int
и
char
— в языке C++. Однако для переменной
ch
типа
char
в обоих языках выполняется условие
sizeof(ch)==1.

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

printf()
(раздел 27.6), но за исключением этой функции (а также некоторых попыток пошутить) эта глава имеет довольно сухое и формальное содержание. Ее цель проста: дать читателям возможность читать и писать программы на языке С, если возникнет такая необходимость. Она содержит также предупреждения об опасностях, которые очевидны для опытных программистов, работающих на языке С, но, как правило, неожиданных для программистов, работающих на языке С++. Мы надеемся, что вы научитесь избегать этих опасностей с минимальными потерями.

Большинство программистов, работающих на языке С++, рано или поздно так или иначе сталкиваются с программами, написанными на языке С. Аналогично, программисты, создающие программы на языке С, часто вынуждены работать с программами, написанными на языке С++. Большинство из того, что мы описываем в этой главе, уже знакомо программистам, работающим на языке С, но некоторые из этих сведений могут быть отнесены к уровню экспертов. Причина проста: не все имеют одинаковое представление об уровне экспертов, поэтому мы описываем то, что часто встречается в реальных программах. Рассуждения о вопросах совместимости может быть дешевым способом добиться незаслуженной репутации “эксперта по языку С”. Однако следует помнить: реальный опыт достигается благодаря практическому использованию языка (в данном случае языка С), а не изучению эзотерических правил языка (как это излагается в разделах, посвященных совместимости).


Библиография

ISO/IEC 9899:1999. Programming Languages — C. В этой книге описан язык C99; большинство компиляторов реализует язык C89 (часто с некоторыми расширениями).

ISO/IEC 14882:2003-27-01 (2-е издание). Programming Languages — C++. Эта книга написана с точки зрения программиста, идентична версии 1997 года.

Kernighan, Brian W., and Dennis M. Ritchie. The C Programming Language. Addison-Wesley, 1988. ISBN 0131103628.

Stroustrup, Bjarne. “Learning Standard C++ as a New Language”. C/C++ Users Journal,May 1999.

Stroustrup, Bjarne. “C and C++: Siblings”; “C and C++: A Case for Compatibility”; and “C and C++: Case Studies in Compatibility”. The C/C++ Users Journal, July, Aug., and Sept. 2002.


Статьи Страуструпа легко найти на его домашней странице.

27.1.2. Свойства языка С++, которых нет в языке С

С точки зрения языка C++ в языке C (т.е. в версии C89) нет многих свойств.

• Классы и функции-члены.

• В языке С используются структуры и глобальные функции.

• Производные классы и виртуальные функции

• В языке С используются структуры, глобальные функции и указатели на функции (раздел 27.2.3).

• Шаблоны и подставляемые функции

• В языке С используются макросы (раздел 27.8).

• Исключения

• В языке С используются коды ошибок, ошибочные возвращаемые значения и т.п.

• Перегрузка функций

• В языке С каждой функции дается отдельное имя.

• Операторы

new/delete

• В языке С используются функции

malloc()/free()
и отдельный код для инициализации и удаления.

• Ссылки

• В языке С используются указатели.

• Ключевое слово

const
в константных выражениях

• В языке С используются макросы.

• Объявления в инструкциях

for
и объявления как инструкции

• В языке С все объявления должны быть расположены в начале блока, а для каждого набора определений начинается новый блок.

• Тип

bool

• В языке С используется тип

int
.

• Операторы

static_cast
,
reinterpret_cast
и
const_cast

• В языке С используются приведения вида

(int)a
, а не
static(a)
.

• // комментарии

• В языке С используются комментарии

/* ... */


 На языке С написано много полезных программ, поэтому этот список должен напоминать нам о том, что ни одно свойство языка не является абсолютно необходимым. Большинство языковых возможностей — и даже большинство свойств языка С — разработано только для удобства программистов. В конце концов, при достаточном запасе времени, мастерстве и терпении любую программу можно написать на ассемблере. Обратите внимание на то, что благодаря близости моделей языков С и С++ к реальным компьютерам они позволяют имитировать многие стили программирования.

Остальная часть этой главы посвящена объяснению того, как писать полезные программы без помощи этих свойств. Наши основные советы по использованию языка С++ сводятся к следующему.

• Имитируйте стили программирования, для которых разработаны свойства языка С++, чтобы поддерживать возможности, предусмотренные языком C.

• Когда пишете программу на языке C, считайте его подмножеством языка C++.

• Используйте предупреждения компилятора для проверки аргументов функций.

• Контролируйте стиль программирования на соответствие стандартам, когда пишете большие программы (см. раздел 27.2.2).


Многие детали, касающиеся несовместимости языков С и С++, устарели и носят скорее технический характер. Однако, для того чтобы читать и писать на языке С, вы не обязаны помнить об этом.

• Компилятор сам напомнит вам, если вы станете использовать средства языка С, которых нет в языке C.

• Если вы следуете правилам, перечисленным выше, то вряд ли столкнетесь с чем-либо таким, что в языке С имеет другой смысл по сравнению с языком С++.


В отсутствие всех возможностей языка С++ некоторые средства в языке С приобретают особое значение.

• Массивы и указатели.

• Макросы.

• Оператор

typedef
.

• Оператор

sizeof
.

• Операторы приведения типов.


В этой главе будет приведено несколько примеров использования таких средств.

 Я ввел в язык С++ комментарии

//
, унаследованные от его предшественника, языка BCPL, когда мне надоело печатать комментарии вида
/* ... */
. Комментарии
//
приняты в большинстве диалектов языка, включая версию C99, поэтому их можно использовать совершенно безопасно. В наших примерах мы будем использовать комментарии вида
/* ... */
исключительно для того, чтобы показать, что мы пишем программу на языке C. В языке C99 реализованы некоторые возможности языка C++ (а также некоторые возможности, несовместимые с языком С++), но мы будем придерживаться версии C89, поскольку она используется более широко.

27.1.3. Стандартная библиотека языка С

 Естественно, возможности библиотек языка С++, зависящие от классов и шаблонов, в языке С недоступны. Перечислим некоторые из них.

• Класс

vector
.

• Класс

map
.

• Класс

set
.

• Класс

string
.

• Алгоритмы библиотеки STL: например,

sort()
,
find()
и
copy()
.

• Потоки ввода-вывода

iostream
.

• Класс

regex
.


Из-за этого библиотеки языка С часто основаны на массивах, указателях и функциях. К основной части стандартной библиотеки языка С относятся следующие заголовочные файлы.

. Общие утилиты (например,
malloc()
и
free()
; см. раздел 27.4).

. Стандартный механизм ввода-вывода; см. раздел 27.6.

. Манипуляции со строками и памятью в стиле языка C; см. раздел 27.5.

. Стандартные математические функции для операций над числами с плавающей точкой; см. раздел 24.8.

. Коды ошибок математических функций из заголовочного файла
; см. раздел 24.8.

. Размеры целочисленных типов; см. раздел 24.2.

. Функции даты и времени; см. раздел 26.6.1.

. Условия для отладки (debug assertions); см. раздел 27.9.

. Классификация символов; см. раздел 11.6.

. Булевы макросы.


Полное описание стандартной библиотеки языка С можно найти в соответствующем учебнике, например в книге K&R. Все эти библиотеки (и заголовочные файлы) также доступны и в языке С++.

27.2. Функции

В языке C есть несколько особенностей при работе с функциями.

• Может существовать только одна функция с заданным именем.

• Проверка типов аргументов функции является необязательной.

• Ссылок нет (а значит, нет и механизма передачи аргументов по ссылке).

• Нет функций-членов.

• Нет подставляемых функций (за исключением версии C99).

• Существует альтернативный синтаксис объявления функций.


Помимо этого, все остальное мало отличается от языка С++. Изучим указанные отличия по отдельности.

27.2.1. Отсутствие перегрузки имен функций

Рассмотрим следующий пример:


void print(int);         /* печать целого числа */

void print(const char*); /* печать строки */ /* ошибка! */


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


void print_int(int);            /* печать целого числа int */

void print_string(const char*); /* печать строки */


Иногда это свойство называют преимуществом: теперь вы не сможете случайно использовать неправильную функцию для вывода целого числа! Очевидно, что нас такой аргумент убедить не сможет, а отсутствие перегруженных функций усложняет реализацию идей обобщенного программирования, поскольку они основаны на семантически похожих функциях, имеющих одинаковые имена. 

27.2.2. Проверка типов аргументов функций

Рассмотрим следующий пример:


int main()

{

  f(2);

}


 Компилятор языка С допускает такой код: вы не обязаны объявлять функции до их использования (хотя можете и должны). Определение функции

f()
может находиться где-то в другом месте. Кроме того, функция
f()
может находиться в другом модуле компиляции, в противном случае редактор связей сообщит об ошибке.

К сожалению, это определение в другом исходном файле может выглядеть следующим образом:


/* other_file.c: */

int f(char* p)

{

  int r = 0;

  while (*p++) r++;

  return r;

}


Редактор связей не сообщит об этой ошибке. Вместо этого вы получите ошибку на этапе выполнения программы или случайный результат.

 Как решить эту проблему? На практике программисты придерживаются согласованного использования заголовочных файлов. Если все функции, которые вы вызываете или определяете, объявлены в заголовке, поставленном в соответствующее место программы с помощью директивы

#include
, будет включен механизм проверки типов. Однако в больших программах на это трудно рассчитывать. Вследствие этого в большинстве компиляторов языка С существуют опции, предусматривающие выдачу предупреждений о вызовах необъявленных функций: воспользуйтесь ими. Кроме того, с первых дней существования языка C появились программы, с помощью которых можно выявлять все возможные проблемы, связанные непротиворечивостью типов. Обычно они называются lint. Используйте их для любой нетривиальной программы на языке С. Вы обнаружите, что программы lint подталкивают вас использовать язык С как подмножество языка С++. Одно из наблюдений, приведших к разработке языка С++, состояло в том, что компилятор мог легко проверять многое (но не все), что могли проверять программы
lint
.

Вы можете попросить включить проверку аргументов функций в языке С. Для этого достаточно объявить функцию с заданными типами аргументов (точно так же, как в языке С++). Такое объявление называется прототипом функции (function prototype). Тем не менее следует избегать объявлений, не задающих аргументы; они не являются прототипами функций и не включают механизм проверки типов.


int g(double); /* прототип — как в языке С ++ */

int h();       /* не прототип — типы аргументов не указаны */


void my_fct()

{

  g();       /* ошибка: пропущен аргумент */

  g("asdf"); /* ошибка: неправильный тип аргумента */

  g(2);      /* OK: 2 преобразуется в 2.0 */

  g(2,3);    /* ошибка: один аргумент лишний */


  h();       /* Компилятор допускает! Результат непредсказуем */

  h("asdf"); /* Компилятор допускает! Результат непредсказуем */

  h(2);      /* Компилятор допускает! Результат непредсказуем */

  h(2,3);    /* Компилятор допускает! Результат непредсказуем */

}


 В объявлении функции

h()
не указан тип аргумента. Это не означает, что функция
h()
не получает ни одного аргумента; это значит: принимает любой набор аргументов и надеется, что это набор при вызове окажется правильным. И снова отметим, что хороший компилятор предупредит об этой проблеме, а программа
lint
перехватит ее.



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

char
и
short
преобразуются в переменные типа
int
, а переменные типа
float
— в переменные типа
double
. Если вы хотите знать, скажем, что произойдет с переменной типа
long
, загляните в хороший учебник по языку С. Наша рекомендация проста: не вызывайте функций, не имеющих прототипов.

Обратите внимание на то, что, хотя компилятор допускает передачу аргументов неправильного типа, например параметр типа

char*
вместо параметра типа int, использование таких аргументов приводит к ошибкам. Как сказал Деннис Ритчи: “С — это язык программирования со строгим контролем типов и слабой проверкой”.

27.2.3. Определения функций

Можете определять функции точно так же, как в языке С++. Эти определения являются прототипами функций.


double square(double d)

{

  return d*d;

}


void ff()

{

  double x = square(2);       /* OK: переводим 2 в 2.0 и вызываем */

  double y = square();        /* пропущен аргумент */

  double y = square("Hello"); /* ошибка: неправильный тип

                                 аргументов */

 double y = square(2,3);      /* ошибка: слишком много аргументов */

}


Определение функции без аргументов не является прототипом функции.


void f() { /* что-то делает */ }


void g()

{

  f(2);    /* OK в языке C; ошибка в языке C++ */

}


Код


void f();  /* не указан тип аргумента */


означающий, что функция

f()
может принять любое количество аргументов любого типа, выглядит действительно странно. В ответ на это я изобрел новое обозначение, в котором понятие “ничего” указывалось явным образом с помощью ключевого слова
void
(void — слово из четырех букв, означающее “ничего”).


void f(void); /* не принимает никаких аргументов */


 Впрочем, вскоре я об этом пожалел, потому что эта конструкция выглядит странно и при последовательной проверке типов аргументов является излишней. Что еще хуже, Деннис Ритчи (автор языка C) и Дуг Мак-Илрой (Doug McIlroy) (законодатель мод в Исследовательском центре компьютерных наук в компании Bell Labs (Bell Labs Computer Science Research Center; см. раздел 22.2.5) назвали это решение “отвратительным”. К сожалению, оно стало очень популярным среди программистов, работающих на языке С. Тем не менее не используйте его в программах на языке С++, в которых оно выглядит не только уродливо, но и является совершенно излишним.

 В языке C есть альтернативное определение функции в стиле языка Algol-60, в котором типы параметров (не обязательно) указываются отдельно от их имен.


int old_style(p,b,x) char* p; char b;

{

  /* ... */

}


 Это определение “в старом стиле” предвосхищает конструкции языка С++ и не является прототипом. По умолчанию аргумент без объявленного типа считается аргументов типа

int
. Итак, параметр
x
является аргументом функции
old_style()
, имеющим тип
int
. Мы можем вызвать функцию
old_style()
следующим образом:


old_style();               /* OK: пропущены все аргументы */

old_style("hello",'a',17); /* OK: все аргументы имеют правильный тип */

old_style(12,13,14);       /* OK: 12 — неправильный тип */

                           /* но old_style() может не использовать p */


Компилятор должен пропустить эти вызовы (но мы надеемся, что он предупредит о первом и третьем аргументах).

Мы рекомендуем придерживаться следующих правил проверки типов аргументов функций.

• Последовательно используйте прототипы функций (используйте заголовочные файлы).

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

• Используйте (какую-нибудь) программу

lint
.


В результате вы получите код, который одновременно будет кодом на языке C++. 

27.2.4. Вызов функций, написанных на языке С, из программы на языке С++, и наоборот

Вы можете установить связи между файлами, скомпилированными с помощью компилятора языка С, и файлами, скомпилированными с помощью компилятора языка С++, только если компиляторы предусматривают такую возможность. Например, можете связать объектные файлы, сгенерированные из кода на языке С и С++, используя компиляторы GNU C и GCC. Можете также связать объектные файлы, сгенерированные из кода на языке С и С++, используя компиляторы Microsoft C и C++ (MSC++). Это обычная и полезная практика, позволяющая использовать больше библиотек, чем это возможно при использовании только одного из этих языков.

 В языке C++ предусмотрена более строгая проверка типов, чем в языке C. В частности, компилятор и редактор связей для языка C++ проверяют, согласованно ли определены и используются функции

f(int)
и
f(double)
, даже если они определены в разных исходных файлах. Редактор связей для языка C не проводит такой проверки. Для того чтобы вызвать функцию, определенную в языке C, в программе, написанной на языке C++, и наоборот, необходимо сообщить компилятору о том, что вы собираетесь сделать.


// вызов функции на языке C из кода на языке C++:

extern "C" double sqrt(double); // связь с функцией языка C


void my_c_plus_plus_fct()

{

  double sr = sqrt(2);

}


По существу, выражение

extern "C"
сообщает компилятору о том, что вы используете соглашения, принятые компилятором языка С. Помимо этого, с точки зрения языка С++ в этой программе все нормально. Фактически стандартная функция
sqrt(double)
из языка C++ обычно входит и в стандартную библиотеку языка С. Для того чтобы вызвать функцию из библиотеки языка С в программе, написанной на языке С++, больше ничего не требуется. Язык C++ просто адаптирован к соглашениям, принятым в редакторе связей языка C.

Мы можем также использовать выражение

extern "C"
, чтобы вызвать функцию языка С++ из программы, написанной на языке С.


// вызов функции на языке C++ из кода на языке C:

extern "C" int call_f(S* p, int i)

{

  return p–>f(i);

}


Теперь в программе на языке C можно косвенно вызвать функцию-член

f()
.


/* вызов функции на языке C++ из функции на языке C: */

int call_f(S* p, int i);

struct S* make_S(int,const char*);


void my_c_fct(int i)

{

  /* ... */

  struct S* p = make_S(x, "foo");

  int x = call_f(p,i);

  /* ... */

}


Для того чтобы эта конструкция работала, больше о языке С++ упоминать не обязательно.

Выгоды такого взаимодействия очевидны: код можно писать на смеси языков C и C++. В частности, программы на языке C++ могут использовать библиотеки, написанные на языке C, а программы на языке C могут использовать библиотеки, написанные на языке С++. Более того, большинство языков (особенно Fortran) имеют интерфейс вызова функций, написанных на языке С, и допускают вызов своих функций в программах, написанных на языке С.

В приведенных выше примерах мы предполагали, что программы, написанные на языках C и C++, совместно используют объект, на который ссылается указатель

p
. Это условие выполняется для большинства объектов. В частности, допустим, что у нас есть следующий класс:


// В языке C++:

class complex {

  double re, im;

public:

  // все обычные операции

};


Тогда можете не передавать указатель на объект в программу, написанную на языке С, и наоборот. Можете даже получить доступ к членам

re
и
im
в программе, написанной на языке C, с помощью объявления


/* В языке C: */

struct complex {

  double re, im;

  /* никаких операций */

};


 Правила компоновки в любом языке могут быть сложными, а правила компоновки модулей, написанных на нескольких языках, иногда даже трудно описать. Тем не менее функции, написанные на языках C и C++, могут обмениваться объектами встроенных типов и классами (структурами) без виртуальных функций. Если класс содержит виртуальные функции, можете просто передать указатели на его объекты и предоставить работу с ними коду, написанному на языке C++. Примером этого правила является функция

call_f()
: функция
f()
может быть
virtual
. Следовательно, этот пример иллюстрирует вызов виртуальной функции из программы, написанной на языке C.

Кроме встроенных типов, простейшим и наиболее безопасным способом совместного использования типов является конструкция

struct
, определенная в общем заголовочном файле языков C и C++. Однако эта стратегия серьезно ограничивает возможности использования языка С++, поэтому мы ее не рекомендуем. 

27.2.5. Указатели на функции

Что можно сделать на языке С, если мы хотим использовать объектно-ориентированную технологию (см. разделы 14.2–14.4)? По существу, нам нужна какая-то альтернатива виртуальным функциям. Большинству людей в голову в первую очередь приходит мысль использовать структуру с “полем типа” (“type field”), описывающим, какой вид фигуры представляет данный объект. Рассмотрим пример.


struct Shape1 {

  enum Kind { circle, rectangle } kind;

  /* ... */

};


void draw(struct Shape1* p)

{

  switch (p–>kind) {

  case circle:

    /* рисуем окружность */

    break;

  case rectangle:

    /* рисуем прямоугольник */

    break;

  }

}


int f(struct Shape1* pp)

{

  draw(pp);

  /* ... */

}


Этот прием срабатывает. Однако есть две загвоздки.

• Для каждой псевдовиртуальной функции (такой как функция

draw()
) мы должны написать новую инструкцию
switch
.

• Каждый раз, когда мы добавляем новую фигуру, мы должны модифицировать каждую псевдовиртуальную функцию (такую как функция

draw()
), добавляя новый раздел case в инструкцию
switch
.


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


typedef void (*Pfct0)(struct Shape2*);

typedef void (*Pfct1int)(struct Shape2*,int);


struct Shape2 {

  Pfct0 draw;

  Pfct1int rotate;

  /* ... */

};


void draw(struct Shape2* p)

{

  (p–>draw)(p);

}


void rotate(struct Shape2* p, int d)

{

  (p–>rotate)(p,d);

}


Структуру

Shape2
можно использовать точно так же, как структуру
Shape1
.


int f(struct Shape2* pp)

{

  draw(pp);

  /* ... */

}


Проделав небольшую дополнительную работу, мы можем добиться, чтобы объекту было не обязательно хранить указатель на каждую псевдовиртуальную функцию. Вместо этого можем хранить указатель на массив указателей на функции (это очень похоже на то, как реализованы виртуальные функции в языке С++). Основная проблема при использовании таких схем в реальных программах заключается в том, чтобы правильно инициализировать все эти указатели на функции.

27.3. Второстепенные языковые различия

В этом разделе приводятся примеры незначительных различий между языками С и С++, которые могут вызвать у читателей затруднения, если они впервые о них слышат. Некоторые из них оказывают серьезное влияние на программирование, поскольку их надо явным образом учитывать.

27.3.1. Дескриптор пространства имен struct

 В языке C имена структур (в нем нет ключевого слова

class
, а есть только слово
struct
) находятся в отдельном от остальных идентификаторов пространстве имен. Следовательно, имени каждой структуры (называемому дескриптором структуры (structure tag)) должно предшествовать ключевое слово
struct
. Рассмотрим пример.


struct pair { int x,y; };

pair p1;        /* ошибка: идентификатора pair не в области

                /* видимости */

struct pair p2; /* OK */

int pair = 7;   /* OK: дескриптора структуры pair нет в области

                /* видимости */

struct pair p3; /* OK: дескриптор структуры pair не маскируется

                /* типом int*/

pair = 8;       /* OK: идентификатор pair ссылается на число типа

                /* int */


Довольно интересно, что, применив обходной маневр, этот прием можно заставить работать и в языке С++. Присваивание переменным (и функциям) тех же имен, что и структурам, — весьма распространенный трюк, используемый в программах на языке С, хотя мы его не рекомендуем.

 Если вы не хотите писать ключевое слово

struct
перед именем каждой структуры, используйте оператор
typedef
(см. раздел 20.5). Широко распространена следующая идиома:


typedef struct { int x,y; } pair;

pair p1 = { 1, 2 };


В общем, оператор

typedef
используется чаще и является более полезным в программах на языке С, в которых у программиста нет возможности определять новые типы и связанные с ними операции.

 В языке C имена вложенных структур находятся в том же самом пространстве имен, что и имя структуры, в которую они вложены. Рассмотрим пример.


struct S {

  struct T { /* ... */ };

  / * ... */

};


struct T x; /* OK в языке C (но не в C++) */


В программе на языке C++ этот фрагмент следовало бы написать так:


S::T x; // OK в языке C++ (но не в C)


При малейшей возможности не используйте вложенные структуры в программах на языке C: их правила разрешения области видимости отличаются от наивных (и вполне разумных) предположений большинства людей. 

27.3.2. Ключевые слова

Многие ключевые слова в языке C++ не являются ключевыми словами в языке С (поскольку язык С не обеспечивает соответствующие функциональные возможности) и поэтому могут использоваться как идентификаторы в программах на языке C.



 Не используйте эти имена как идентификаторы в программах на языке C, иначе ваш код станет несовместимым с языком C++. Если вы используете одно из этих имен в заголовочном файле, то не сможете использовать его в программе на языке C++.

Некоторые ключевые слова в языке C++ являются макросами в языке C.



В языке C они определены в заголовочных файлах

и
(
bool
,
true
,
false
). Не пользуйтесь тем, что они являются макросами в языке C.

27.3.3. Определения

Язык C++ допускает определения в большем количестве мест программы по сравнению с языком C. Рассмотрим пример.


for (int i = 0; i

                                         // недопустимое в языке C

while (struct S* p = next(q)) { // определение указателя p,

                                // недопустимое в языке C

  /* ... */

}


void f(int i)

{

  if (i< 0 || max<=i) error("Ошибка диапазона");

  int a[max]; // ошибка: объявление после инструкции

              // в языке С не разрешено

  /* ... */

}


Язык C (C89) не допускает объявлений в разделе инициализации счетчика цикла

for
, в условиях и после инструкций в блоке. Мы должны переписать предыдущий фрагмент как-то так:


int i;

for (i = 0; i


struct S* p;

while (p = next(q)) {

  /* ... */

}


void f(int i)

{

  if (i< 0 || max<=i) error("Ошибка диапазона");

  {

    int a[max];

    /* ... */

  }

}


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


int x;

int x; /* определяет или объявляет одну целочисленную переменную

          с именем x в программе на языке C; ошибка в языке C++ */


 В языке С++ сущность должна быть определена только один раз. Ситуация становится интереснее, если эти две переменные типа

int
с одинаковыми именами находятся в разных модулях компиляции.


/* в файле x.c: */

int x;


/* в файле y.c: */

int x;


Ни компилятор языка С, ни компилятор языка С++ не найдет никаких ошибок в файлах

x.c
или
y.c
. Но если файлы
x.c
и
y.c
скомпилировать как файлы на языке С++, то редактор связей выдаст сообщение об ошибке, связанной с двойным определением. Если же файлы
x.c
и
y.c
скомпилировать на языке C, то редактор связей не выдаст сообщений об ошибке и (в полном соответствии с правилами языка C) будет считать, что речь идет об одной и той же переменной
x
, совместно используемой в файлах
x.c
и
y.c
. Если хотите, чтобы в программе всеми модулями совместно использовалась одна глобальная переменная
x
, то сделайте это явно, как показано ниже.


/* в файле x.c: */

int x = 0; /* определение */


/* в файле y.c: */

extern int x; /* объявление, но не определение */


Впрочем, лучше используйте заголовочный файл.


/* в файле x.h: */

extern int x;  /* объявление, но не определение */


/* в файле x.c: */

#include "x.h"

int x = 0;     /* определение */


/* в файле y.c: */

#include "x.h"

/* объявление переменной x находится в заголовочном файле */


А еще лучше: избегайте глобальных переменных.

27.3.4. Приведение типов в стиле языка С

В языке C (и в языке C++) можете явно привести переменную

v
к типу
T
, используя минимальные обозначения.


(T)v


 Это так называемое “приведение в стиле языка С”, или “приведение в старом стиле”. Его любят люди, не умеющие набирать тексты (за лаконичность) и ленивые (потому что они не обязаны знать, что нужно для того, чтобы из переменной

v
получилась переменная типа
T
). С другой стороны, этот стиль яростно отвергают программисты, занимающиеся сопровождением программ, поскольку такие преобразования остаются практически незаметными и никак не привлекают к себе внимания. Приведения в языке С++ (приведения в новом стиле (new-style casts), или приведения в шаблонном стиле (template-style casts); см. раздел А.5.7) осуществляют явное преобразование типов, которое легко заметить. В языке С у вас нет выбора.


int* p = (int*)7;   /* интерпретирует битовую комбинацию:

                       reinterpret_cast(7) */

int x = (int)7.5;   /* усекает переменную типа: static_cast(7.5) */

typedef struct S1 { /* ... */ } S1;

typedef struct S2 { /* ... */ } S2;

S1 a;

const S2 b;         /* в языке С допускаются неинициализированные

                    /* константы */

S1* p = (S2*)&a;    /* интерпретирует битовую комбинацию:

                       reinterpret_cast(&a) */

S2* q = (S2*)&b;    /* отбрасывает спецификатор const:

                       const_cast(&b) */

S1* r = (S1*)&b;    /* удаляет спецификатор const и изменяет тип;

                       похоже на ошибку */


Мы не рекомендуем использовать макросы даже в программах на языке C (раздел 27.8), но, возможно, описанные выше идеи можно было бы выразить следующим образом:


#define REINTERPRET_CAST(T,v) ((T)(v))

#define CONST_CAST(T,v) ((T)(v))


S1* p = REINTERPRET_CAST (S1*,&a);

S2* q = CONST_CAST(S2*,&b);


Это не обеспечит проверку типов при выполнении операторов

reinterpret_cast
и
const_cast
, но сделает эти ужасные операции заметными и привлечет внимание программиста. 

27.3.5. Преобразование указателей типа void*

В языке указатель типа

void*
можно использовать как в правой части оператора присваивания, так и для инициализации указателей любого типа; в языке C++ это невозможно. Рассмотрим пример.


void* alloc(size_t x); /* выделяет x байтов */


void f (int n)

{

  int* p = alloc(n*sizeof(int)); /* OK в языке C;

                                    ошибка в языке C++ */

  /* ... */

}


Здесь указатель типа

void*
возвращается как результат функции
alloc()
и неявно преобразовывается в указатель типа
int*
. В языке C++ мы могли бы переписать эту строку следующим образом:


int* p = (int*)alloc(n*sizeof(int)); /* OK и в языке C,

                                        и в языке C++ */


Мы использовали приведение в стиле языка C (раздел 27.3.4), чтобы оно оказалось допустимым как в программах на языке C, так и в программах на языке C++.

 Почему неявное преобразование

void*
в
T*
является недопустимым в языке С++? Потому, что такие преобразования могут быть небезопасными.


void f()

{

  char i = 0;

  char j = 0;

  char* p = &i;

  void* q = p;

  int* pp = q; /* небезопасно; разрешено в языке C,

                  ошибка в языке C++ */

  *pp = –1;    /* перезаписываем память, начиная с адреса &i */


В данном случае мы даже не уверены, какой фрагмент памяти будет перезаписан: переменная

j
или часть памяти, на которую ссылается указатель
p
? А может быть, память, использованная для управлении вызовом функции
f()
(стек функции
f
)? Какие бы данные ни были перезаписаны, вызов функции
f()
приведет к печальным последствиям.

Обратите внимание на то, что (обратное) преобразование указателя типа

T*
в указатель типа
void*
является совершенно безопасным, — вы не сможете придумать ужасные примеры, подобные предыдущему, — и они допускаются как в языке C, так и в языке C++.

К сожалению, неявное преобразование

void*
в
T*
широко распространено в языке C и, вероятно, является основной проблемой совместимости языков С и С++ в реальных программах (см. раздел 27.4).

27.3.6. Перечисление

В языке C можно присваивать целое число перечислению без приведения

int
в
enum
. Рассмотрим пример.


enum color { red, blue, green };

int x = green;      /* OK в языках C и C++ */

enum color col = 7; /* OK в языке C; ошибка в языке C++ */


Одним из следствий этого факта является то, что в программах на языке С мы можем применять операции инкрементации (

++
) и декрементации (
––
) к переменным, являющимся перечислениями. Это может быть удобным, но одновременно небезопасным.


enum color x = blue;

++x; /* переменная x становится равной значению green;

        ошибка в языке C++ */

++x; /* переменная x становится равной 3; ошибка в языке C++ */


Выход за пределы перечисления может входить в наши планы, а может быть неожиданным.

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

enum
.


color c2 = blue;     /* ошибка в языке C: переменная color не находится

                        в пределах области видимости; OK в языке C++ */

enum color c3 = red; /* OK */

27.3.7. Пространства имен

В языке С нет пространств имен (в том смысле, как это принято в языке С++). Так что же можно сделать, чтобы избежать коллизий имен в больших программах, написанных на языке С? Как правило, для этого используются префиксы и суффиксы. Рассмотрим пример.


/* в bs.h: */

typedef struct bs_string { /* ... */ } bs_string; /* строка

                                                     Бьярне */

typedef int bs_bool; /* булев тип Бьярне */


/* in pete.h: */

typedef char* pete_string; /* строка Пита */

typedef char pete_bool;    /* булев тип Пита */


Этот прием настолько широко используется, что использовать одно- и двухбуквенные префиксы обычно уже недостаточно.

27.4. Свободная память

 В языке С нет операторов

new
и
delete
, работающих с объектами. Для использования свободной памяти в нем используются функции, работающие с памятью. Наиболее важные функции определены в стандартном заголовочном файле общих утилит
.


void* malloc(size_t sz); /* выделить sz байтов */

void free(void* p);      /* освободить область памяти, на которую

                            ссылается указатель p */

void* calloc(size_t n, size_t sz); /* выделить n*sz байтов,

                                      инициализировав их нулями */

void* realloc(void* p, size_t sz); /* вновь выделить sz байтов

                                      в памяти, на которую ссылается

                                      указатель p*/


Тип

typedef size_t
— это тип без знака, также определенный в заголовочном файле
.

 Почему функция

malloc()
возвращает указатель
void*
? Потому что она не имеет информации о том, объект какого типа вы хотите разместить в памяти. Инициализация — это ваша проблема. Рассмотрим пример.


struct Pair {

  const char* p;

  int val;

};


struct Pair p2 = {"apple",78};

struct Pair* pp = (struct Pair*) malloc(sizeof(Pair)); /* выделить
 память */

pp–>p = "pear"; /* инициализировать */

pp–>val = 42;


Теперь мы не можем написать инструкцию


*pp = {"pear", 42}; /* ошибка: не C и не C++98 */


ни в программе на языке C, ни в программе на языке C++. Однако в языке С++ мы могли бы определить конструктор для структуры

Pair
и написать инструкцию
Pair* pp = new Pair("pear", 42)
;

В языке C (но не в языке C++; см. раздел 27.3.4) перед вызовом функции malloc() можно не указывать приведение типа, но мы не рекомендуем это делать.


int* p = malloc(sizeof(int)*n); /* избегайте этого */


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

перед использованием функции
malloc()
. Однако при этом исчезает и визуальный маркер, свидетельствующий о том, что размер памяти подсчитан неправильно.


p = malloc(sizeof(char)*m); /* вероятно, ошибка — нет места для m целых */


 Не используйте функции

malloc()/free()
в программах, написанных на языке C++; операторы
new/delete
не требуют приведения типа, выполняют инициализацию (вызывая конструкторы) и очищают память (вызывая деструкторы), сообщают об ошибках, связанных с распределением памяти (с помощью исключений), и просто работают быстрее. Не удаляйте объект, размещенный в памяти с помощью функции
malloc()
, выполняя оператор
delete
, и не удаляйте объект, созданный с помощью оператора new, вызывая функцию
free()
. Рассмотрим пример.


int* p = new int[200];

// ...

free(p); // ошибка


X* q = (X*)malloc(n*sizeof(X));

// ...

delete q; // error


Этот код может оказаться вполне работоспособным, но он не является переносимым. Более того, для объектов, имеющих конструкторы и деструкторы, смешение стилей языков C и C++ при управлении свободной памятью может привести к катастрофе. Для расширения буферов обычно используется функция

realloc()
.


int max = 1000;

int count = 0;

int c;

char* p = (char*)malloc(max);

while ((c=getchar())!=EOF) { /* чтение: игнорируются символы

                                в конце файла */

  if (count==max–1) {        /* необходимо расширить буфер */

    max += max;              /* удвоить размер буфера */

    p = (char*)realloc(p,max);

    if (p==0) quit();

  }

  p[count++] = c;

}


Объяснения операторов ввода в языке С приведены в разделах 27.6.2 и Б.10.2.

 Функция

realloc()
может выделить память на прежнем участке, а может и перенести его содержимое во вновь выделенную область памяти. Даже не думайте применять функцию
realloc()
к области памяти, выделенной с помощью оператора
new
.

Используя стандартную библиотеку языка C++, этот код можно переписать примерно так:


vector buf;

char c;

while (cin.get(c)) buf.push_back(c);


Более подробное обсуждение стратегий ввода и распределения памяти можно найти в статье “Learning Standard C++ as a New Language” (см. список библиографических ссылок в конце раздела 27.1).

27.5. Строки в стиле языка С

Строка в языке C (в литературе, посвященной языку С++, ее часто называют С-строкой (C-string), или строкой в стиле языка С (C-style)) — это массив символов, завершающийся нулем. Рассмотрим пример.


char* p = "asdf";

char s[ ] = "asdf";



В языке C нет функций-членов, невозможно перегружать функции и нельзя определить оператор (такой как

==
) для структур. Вследствие этого для манипулирования строками в стиле языка С необходим набор специальных функций (не членов класса). В стандартных библиотеках языков C и C++ такие функции определены в заголовочном файле
.


size_t strlen(const char* s); /* определяет количество символов */

char* strcat(char* s1, const char* s2);     /* копирует s2 в конец s1 */

int strcmp(const char* s1, const char* s2); /* лексикографическое 
сравнение */

char* strcpy(char* s1,const char* s2);           /* копирует s2 в s1 */

char* strchr(const char *s, int c);              /* копирует c в s */

char* strstr(const char *s1, const char *s2);    /* находит s2 в s1 */

char* strncpy(char*, const char*, size_t n);     /* сравнивает n 
символов */

char* strncat(char*, const char, size_t n);      /* strcat с n 
символами */

int strncmp(const char*, const char*, size_t n); /* strcmp с n 
символами */


Это не полный список функций для работы со строками, но он содержит самые полезные и широко используемые функции. Кратко проиллюстрируем их применение.

 Мы можем сравнивать строки. Оператор проверки равенства (

==
) сравнивает значения указателей; стандартная библиотечная функция
strcmp()
сравнивает значения C-строк.


const char* s1 = "asdf";

const char* s2 = "asdf";

if (s1==s2) { /* ссылаются ли указатели s1 и s2 на один и тот же

                 массив? */

              /* (обычно это нежелательно) */

}

if (strcmp(s1,s2)==0) { /* хранят ли строки s1 и s2 одни и те же

                           символы? */

}


Функция

strcmp()
может дать три разных ответа. При заданных выше значениях
s1
и
s2
функция
strcmp(s1,s2)
вернет нуль, что означает полное совпадение. Если строка
s1
предшествует строке
s2
в соответствии с лексикографическим порядком, то она вернет отрицательное число, и если строка
s1
следует за строкой
s2
в лексикографическом порядке, то она вернет положительное число. Термин лексикографический (lexicographical) означает “как в словаре.” Рассмотрим пример.


strcmp("dog","dog")==0

strcmp("ape","dodo")<0 /* "ape" предшествует "dodo" в словаре */

strcmp("pig","cow")>0 /* "pig" следует после "cow" в словаре */


Результат сравнения указателей

s1==s2
не обязательно равен 0 (
false
). Механизм реализации языка может использовать для хранения всех строковых литералов одну и ту же область памяти, поэтому можем получить ответ 1 (
true
). Обычно функция
strcmp()
хорошо справляется со сравнением С-строк.

Длину С-строки можно найти с помощью функции

strlen()
.


int lgt = strlen(s1);


Обратите внимание на то, что функция

strlen()
подсчитывает символы, не учитывая завершающий нуль. В данном случае
strlen(s1)==4
, а строка "
asdf
" занимает в памяти пять байтов. Эта небольшая разница является источником многих ошибок при подсчетах.

Мы можем копировать одну С-строку (включая завершающий нуль) в другую.


strcpy(s1,s2); /* копируем символы из s2 в s1 */


Программист должен сам гарантировать, что целевая строка (массив) имеет достаточный размер, чтобы в ней поместились символы исходной строки.

Функции

strncpy()
,
strncat()
и
strncmp()
являются версиями функций
strcpy()
,
strcat()
и
strcmp()
, учитывающими не больше
n
символов, где параметр
n
задается как третий аргумент. Обратите внимание на то, что если в исходной строке больше n символов, то функция
strncpy()
не будет копировать завершающий нуль, поэтому результат копирования не будет корректной С-строкой. Функции
strchr()
и
strstr()
находят свой второй аргумент в строке, являющейся их первым аргументом, и возвращают указатель на первый символ совпадения. Как и функция
find()
, они выполняют поиск символа в строке слева направо. Удивительно, как много можно сделать с этими простыми функциями и как легко при этом допустить незаметные ошибки. Рассмотрим простую задачу: конкатенировать имя пользователя с его адресом, поместив между ними символ @. С помощью класса
std::string
это можно сделать так:


string s = id + '@' + addr;


С помощью стандартных функций для работы с С-строками этот код можно написать следующим образом:


char* cat(const char* id, const char* addr)

{

  int sz = strlen(id)+strlen(addr)+2;

  char* res = (char*) malloc(sz);

  strcpy(res,id);

  res[strlen(id)+1] = '@';

  strcpy(res+strlen(id)+2,addr);

  res[sz–1]=0;

  return res;

}


Правильный ли ответ мы получили? Кто вызовет функцию

free()
для строки, которую вернула функция
cat()
?


ПОПРОБУЙТЕ

Протестируйте функцию

cat()
. Почему в первой инструкции мы добавляем число 2? Мы сделали глупую ошибку в функции
cat()
, найдите и устраните ее. Мы “забыли” прокомментировать код. Добавьте соответствующие комментарии, предполагая, что читатель знает стандартные функции для работы с С-строками.

27.5.1. Строки в стиле языка С и ключевое слово const

Рассмотрим следующий пример:


char* p = "asdf";

p[2] = 'x';


 В языке С так писать можно, а в языке С++ — нет. В языке C++ строковый литерал является константой, т.е. неизменяемой величиной, поэтому оператор p

[2]='x'
(который пытается превратить исходную строку в строку "asxf") является недопустимым. К сожалению, некоторые компиляторы пропускают присваивание указателю
p
, что приводит к проблемам. Если вам повезет, то произойдет ошибка на этапе выполнения программы, но рассчитывать на это не стоит. Вместо этого следует писать так:


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
, поэтому мы используем стандартный механизм ввода-вывода языка С, определенный в заголовочном файле
. Эквивалентами потоков ввода и вывода
cin
и
cout
из языка С++ в языке С являются потоки
stdin
и
stdout
. Стандартные средства ввода-вывода языка С и потоки
iostream
могут одновременно использоваться в одной и той же программе (для одних и тех же потоков ввода-вывода), но мы не рекомендуем это делать. Если вам необходимо совместно использовать эти механизмы, хорошенько разберитесь в них (обратите особое внимание на функцию
ios_base::sync_with_stdio()
), используя хороший учебник. См. также раздел Б.10.

27.6.1. Вывод

Наиболее популярной и полезной функцией библиотеки

stdio
является функция
printf()
. Основным предназначением функции
printf()
является вывод С-строки.


#include

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 ch)

{

  printf("double %g string %s int %d char %c\n", d, s, i, ch);

}


где символы

%g
означают: “Напечатать число с плавающей точкой, используя универсальный формат”, символы
%s
означают: “Напечатать С-строку”, символы
%d
означают: “Напечатать целое число, используя десятичные цифры,” а символы
%c
означают: “Напечатать символ”. Каждый такой спецификатор формата связан со следующим, до поры до времени не используемым аргументом, так что спецификатор
%g
выводит на экран значение переменной
d
;
%s
— значение переменной
s
,
%d
— значение переменной
i
, а
%c
— значение переменной
ch
. Полный список форматов функции
printf()
приведен в разделе Б.10.2.

 К сожалению, функция

printf()
не является безопасной с точки зрения типов. Рассмотрим пример.


char a[] = { 'a', 'b' };      /* нет завершающего нуля */

void f2(char* s, int i)

{

  printf("goof %s\n", i);     /* неперехваченная ошибка */

  printf("goof %d: %s\n", i); /* неперехваченная ошибка */

  printf("goof %s\n", a);     /* неперехваченная ошибка */}


Интересен эффект последнего вызова функции printf(): она выводит на экран каждый байт участка памяти, следующего за элементом a[1], пока не встретится нуль. Такой вывод может состоять из довольно большого количества символов.

Недостаток проверки типов является одной из причин, по которым мы предпочитаем потоки

iostream
, несмотря на то, что стандартный механизм ввода-вывода, описанный в библиотеке
stdio
языков C и C++, работает одинаково. Другой причиной является то, что функции из библиотеки
stdio
не допускают расширения: мы не можем расширить функцию
printf()
так, чтобы она выводила на экран значения переменных вашего собственного типа. Для этого можно использовать потоки
iostream
. Например, нет никакого способа, который позволил бы вам определить свой собственный спецификатор формата
%Y
для вывода структуры
struct Y
.

Существует полезная версия функции

printf()
, принимающая в качестве первого аргумента дескриптор файла.


int fprintf(FILE* stream, const char* format, ...);


Рассмотрим пример.


fprintf(stdout,"Hello, World!\n"); // идентично

                                   // printf("Hello,World!\n");

FILE* ff = fopen("My_file","w");   // открывает файл My_file

                                   // для записи

fprintf(ff,"Hello, World!\n");     // запись "Hello,World!\n"

                                   // в файл My_file


Дескрипторы файлов описаны в разделе 27.6.3.

27.6.2. Ввод

Ниже перечислены наиболее популярные функции из библиотеки

stdio
.


int scanf(const char* format, ...); /* форматный ввод из потока stdin */

int getchar(void);      /* ввод символа из потока stdin */

int getc(FILE* stream); /* ввод символа из потока stream*/

char* gets(char* s);    /* ввод символов из потока stdin */


Простейший способ считывания строки символов — использовать функцию

gets()
. Рассмотрим пример.


char a[12];

gets(a); /* ввод данных в массив символов a вплоть до символа '\n' */


 Никогда не делайте этого! Считайте, что функция

gets()
отравлена. Вместе со своей ближайшей “родственницей” — функцией
scanf("%s")
— функция
gets()
является мишенью для примерно четверти успешных хакерских атак. Она порождает много проблем, связанных с безопасностью. Как в тривиальном примере, приведенном выше, вы можете знать, что до следующей новой строки будет введено не более 11 символов? Вы не можете этого знать. Следовательно, функция
gets()
почти наверное приведет к повреждению памяти (байтов, находящихся за буфером), а повреждение памяти является основным инструментом для хакерских атак. Не считайте, что можете угадать максимальный размер буфера, достаточный на все случаи жизни. Возможно, что “субъект” на другом конце потока ввода — это программа, не соответствующая вашим критериям разумности.

Функция

scanf()
считывает данные с помощью формата точно так же, как и функция
printf()
. Как и функция
printf()
, она может быть очень удобной.


void f()

{

  int i;

  char c;

  double d;

  char* s = (char*)malloc(100);

  /* считываем данные в переменные, передаваемые как указатели: */

  scanf("%i %c %g %s", &i, &c, &d, s);

  /* спецификатор %s пропускает первый пробел и прекращает

     действие на следующем пробеле */

}


 Как и функция

printf()
, функция
scanf()
не является безопасной с точки зрения типов. Форматные символы и аргументы (все указатели) должны точно соответствовать друг другу, иначе во время выполнения программы будут происходить странные вещи. Обратите также внимание на то, что считывание данных в строку
s
с помощью спецификатора
%s
может привести к переполнению. Никогда не используйте вызовы
gets()
или
scanf("%s")
!

 Итак, как же безопасно ввести символы? Мы можем использовать вид формата %s, устанавливающий предел количества считываемых символов. Рассмотрим пример.


char buf[20];

scanf("%19s",buf);


Нам требуется участок памяти, заканчивающийся нулем (содержание которого вводится функцией

scanf()
), поэтому 19 — это максимальное количество символов, которое можно считать в массив
buf
. Однако этот способ не отвечает на вопрос, что делать, если некто введет больше 19 символов. Лишние символы останутся в потоке ввода и будут обнаружены при следующей попытке ввода.

Проблема с функцией

scanf()
означает, что часто благоразумно и легче использовать функцию
getchar()
. Типичный ввод символов с помощью функции
getchar()
выглядит следующим образом:


while((x=getchar())!=EOF) {

  /* ... */

}


Макрос

EOF
, описанный в библиотеке
stdio
, означает “конец файла”; см. также раздел 27.4.

Альтернативы функций

scanf("%s")
и
gets()
в стандартной библиотеке языка C++ от этих проблем не страдают.


string s;

cin >> s; // считываем слово

getline(cin,s); // считываем строку

27.6.3. Файлы

В языке C (и C++) файлы можно открыть с помощью функции

fopen()
, а закрыть — с помощью функции
fclose()
. Эти функции, вместе с представлением дескриптора файлов
FILE
и макросом
EOF
(конец файла), описаны в заголовочном файле
.


FILE *fopen(const char* filename, const char* mode);

int fclose(FILE *stream);


По существу, мы используем файлы примерно так:


void f(const char* fn, const char* fn2)

{

  FILE* fi = fopen(fn, "r");  /* открываем файл fn для чтения */

  FILE* fo = fopen(fn2, "w"); /* открываем файл fn для записи */

  if (fi == 0) error("невозможно открыть файл для ввода");

  if (fo == 0) error("невозможно открыть файл для вывода");

  /* чтение из файла с помощью функций ввода из библиотеки stdio,

     например, getc() */

  /* запись в файл с помощью функций вывода из библиотеки stdio,

     например, fprintf() */

  fclose(fo);

  fclose(fi);

}


Учтите: в языке С нет исключений, потому вы не можете узнать, что при обнаружении ошибок файлы были закрыты.

27.7. Константы и макросы

В языке С константы не являются статическими.


const int max = 30;

const int x; /* неинициализированная константа: OK в C

                (ошибка в C++) */


void f(int v)

{

  int a1[max]; /* ошибка: граница массива не является константой

                  (OK в языке C++) */

               /* (слово max не допускается в константном

                  выражении!) */

  int a2[x];   /* ошибка: граница массива не является константой */


  switch (v) {

  case 1:

    /* ... */

    break;

  case max:    /* ошибка: метка раздела case не является

                  константой
 (OK в языке C++) */

    /* ... */

    break;

  }

}


По техническим причинам в языке С (но не в языке C++) неявно допускается, чтобы константы появлялись из других модулей компиляции.


/* файл x.c: */

const int x;     /* инициализирована в другом месте */


/* файл xx.c: */

const int x = 7; /* настоящее определение */


В языке С++ в разных файлах могут существовать два разных объекта с одним и тем же именем

x
. Вместо использования ключевого слова
const
для представления символьных констант программисты на языке С обычно используют макросы. Рассмотрим пример.


#define MAX 30

void f(int v)

{

  int a1[MAX]; /* OK */

  switch (v) {

  case 1:

    /* ... */

    break;

  case MAX:    /* OK */

    /* ... */

    break;

  }

}


 Имя макроса

MAX
заменяется символами
30
, представляющими собой значение этого макроса; иначе говоря, количество элементов массива
a1
равно
30
, а меткой второго раздела case является число
30
. По общепринятому соглашению имя макроса
MAX
состоит только из прописных букв. Это позволяет минимизировать ошибки, вызываемые макросами.

27.8. Макросы

 Берегитесь макросов: в языке С нет по-настоящему эффективных способов избежать макросов, но их использование имеет серьезные побочные эффекты, поскольку они не подчиняются обычным правилам разрешения области видимости и типов, принятым в языках С и С++. Макросы — это вид текстуальной подстановки. См. также раздел А.17.2.

 Как защититься от потенциальных проблем, связанных с макросами, не отказываясь от них навсегда (и не прибегая к альтернативам, предусмотренным в языке С++?

• Присваивайте всем макросам имена, состоящие только из прописных букв:

ALL_CAPS
.

• Не присваивайте имена, состоящие только из прописных букв, объектам, которые не являются макросами.

• Никогда не давайте макросам короткие или “изящные” имена, такие как

max
или
min
.

• Надейтесь, что остальные программисты следуют этим простым и общеизвестным правилам.


В основном макросы применяются в следующих случаях:

• определение “констант”;

• определение конструкций, напоминающих функции;

• улучшение синтаксиса;

• управление условной компиляцией.


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

Мы считаем, что макросы используются слишком часто, но в программах на языке С у них нет разумных и полноценных альтернатив. Их даже трудно избежать в программах на языке С++ (особенно, если вам необходимо написать программу, которая должна подходить для очень старых компиляторов или выполняться на платформах с необычными ограничениями).

Мы приносим извинения читателям, считающим, что приемы, которые будут описаны ниже, являются “грязными трюками”, и полагают, что о них лучше не говорить в приличном обществе. Однако мы думаем, что программирование должно учитывать реалии и что эти (очень простые) примеры использования и неправильного использования макросов сэкономят часы страданий для новичков. Незнание макросов не приносит счастья. 

27.8.1. Макросы, похожие на функции

Рассмотрим типичный макрос, напоминающий функцию.


#define MAX(x, y) ((x)>=(y)?(x):(y))


Мы используем прописные буквы в имени

MAX
, чтобы отличить его от многих функций с именем
max
(в разных программах). Очевидно, что этот макрос сильно отличается от функции: у него нет типов аргументов, нет тела, нет инструкции
return
и так далее, и вообще, зачем здесь так много скобок? Проанализируем следующий код:


int aa = MAX(1,2);

double dd = MAX(aa++,2);

char cc = MAX(dd,aa)+2;


Он разворачивается в такой фрагмент программы:


int aa = ((1)>=( 2)?(1):(2));

double dd = ((aa++)>=(2)?( aa++):(2));

char cc = ((dd)>=(aa)?(dd):(aa))+2;


Если бы всех этих скобок не было, то последняя строка выглядела бы следующим образом.


char cc = dd>=aa?dd:aa+2;


Иначе говоря, переменная

cc
могла бы легко получить другое значение, которого вы не ожидали, исходя из определения макроса. Определяя макрос, не забывайте заключить в скобки каждый аргумент, входящий в выражение.

 С другой стороны, не всегда скобки могут спасти нас от второго варианта развертывания. Параметру макроса x было присвоено значение

aa++
, а поскольку переменная x в макросе
MAX
используется дважды, переменная a может инкрементироваться также дважды. Не передавайте макросу аргументы, имеющие побочные эффекты.

Какой-то “гений” определил макрос следующим образом и поместил его в широко используемый заголовочный файл. К сожалению, он также назвал его

max
, а не
MAX
, поэтому когда в стандартном заголовке языка C++ объявляется функция


template inline T max(T a, T b) { return a


имя

max
разворачивается с аргументами
T a
и
T b
, и компилятор видит строку


template inline T ((T a)>=(T b)?(T a):(T b))

  { return a


Сообщения об ошибке, выдаваемые компилятором, интересны, но не слишком информативны. В случае опасности можете отменить определение макроса.


#undef max


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

Не все параметры макросов используются как выражения. Рассмотрим следующий пример:


#define ALLOC(T,n) ((T*)malloc(sizeof(T)*n))


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

sizeof
.


double* p = malloc(sizeof(int)*10); /* похоже на ошибку */


К сожалению, написать макрос, который позволял бы выявить исчерпание памяти, — нетривиальная задача. Это можно было бы сделать, если бы мы в каком-то месте программы соответствующим образом определили переменную

error_var
и функцию
error()
.


#define ALLOC(T,n) (error_var = (T*)malloc(sizeof(T)*n), \

                   (error_var==0)\

                   ?(error("Отказ выделения памяти"),0)\

                   :error_var)


Строки, завершающиеся символом

\
, не содержат опечаток; это просто способ разбить определение макроса на несколько строк. Когда мы пишем программы на языке C++, то предпочитаем использовать оператор
new
.

27.8.2. Синтаксис макросов

Можно определить макрос, который приводит текст исходного кода в приятный для вас вид. Рассмотрим пример.


#define forever for(;;)

#define CASE break; case

#define begin {

#define end }


 Мы резко протестуем против этого. Многие люди пытались делать такие вещи. Они (и люди, которым пришлось поддерживать такие программы) пришли к следующим выводам.

• Многие люди не разделяют ваших взглядов на то, что считать лучшим синтаксисом.

• Улучшенный синтаксис является нестандартным и неожиданным; остальные люди будут сбиты с толку.

• Использование улучшенного синтаксиса может вызвать непонятные ошибки компиляции.

• Текст программы, который вы видите перед собой, не совпадает с текстом, который видит компилятор, и компилятор сообщает об ошибках, используя свой словарный запас, а не ваш.


Не пишите синтаксические макросы, для того чтобы улучшить внешний вид вашего кода. Вы и ваши лучшие друзья могут считать его превосходным, но опыт показывает, что вы окажетесь в крошечном меньшинстве среди более крупного сообщества программистов, поэтому кому-то придется переписать ваш код (если он сможет просуществовать до этого момента). 

27.8.3. Условная компиляция

Представьте себе, что у вас есть два варианта заголовочного файла, например, один — для операционной системы Linux, а другой — для операционной системы Windows. Как выбрать правильный вариант в вашей программе? Вот как выглядит общепринятое решение этой задачи:


#ifdef WINDOWS

  #include "my_windows_header.h"

#else

  #include "my_linux_header.h"

#endif


Теперь, если кто-нибудь уже определил

WINDOWS
до того, как компилятор увидел этот код, произойдет следующее:


#include "my_windows_header.h"


В противном случае будет включен другой заголовочный файл.


#include "my_linux_header.h"


Директива

#ifdef WINDOWS
не интересуется, что собой представляет макрос
WINDOWS
; она просто проверяет, был ли он определен раньше.

В большинстве крупных систем (включая все версии операционных систем) существуют макросы, поэтому вы можете их проверить. Например, можете проверить, как компилируется ваша программа: как программа на языке C++ или программа на языке C.


#ifdef __cplusplus

  // в языке C++

#else

 /* в языке C */

#endif


Аналогичная конструкция, которую часто называют стражем включения (include guard), обычно используется для предотвращения повторного включения заголовочного файла.


/* my_windows_header.h: */

#ifndef MY_WINDOWS_HEADER

#define MY_WINDOWS_HEADER

  /* информация о заголовочном файле */

#endif


Директива

#ifndef
проверяет, не было ли нечто определено раньше; например,
#ifndef
противоположна директиве
#ifdef
. С логической точки зрения эти макросы, использующиеся для контроля исходного файла, сильно отличаются от макросов, использованных для модификации исходного кода. Просто они используют одинаковый базовый механизм для выполнения своих функций. 

27.9. Пример: интрузивные контейнеры

Контейнеры из стандартной библиотеки языка С++, такие как

vector
и
map
, являются неинтрузивными; иначе говоря, они не требуют информации о типах данных, использованных как их элементы. Это позволяет обобщить их для практически всех типов (как встроенных, так и пользовательских), поскольку эти типы допускают операцию копирования. Существует и другая разновидность контейнеров — интрузивные контейнеры (intrusive container), популярные в языках C и C++. Для того чтобы проиллюстрировать использование структур, указателей и свободной памяти, будем использовать неинтрузивный список.

Определим двухсвязный список с девятью операциями.


void init(struct List* lst); /* инициализирует lst пустым */

struct List* create();       /* создает новый пустой список

                                в свободной памяти */

void clear(struct List* lst);   /* удаляет все элементы списка lst */

void destroy(struct List* lst); /* удаляет все элементы списка lst,

                                   а затем удаляет сам lst */

void push_back(struct List* lst, struct Link* p); /* добавляет

                                  элемент p в конец списка lst */

void push_front(struct List*, struct Link* p); /* добавляет элемент p

                                  в начало списка lst */


/* вставляет элемент q перед элементом p in lst: */

void insert(struct List* lst, struct Link* p, struct Link* q);

struct Link* erase(struct List* lst, struct Link* p); /* удаляет

                                         элемент p из списка lst */


/* возвращает элемент, находящийся за n до или через n узлов

   после узла p:*/

struct Link* advance(struct Link* p, int n);


Мы хотим определить эти операции так, чтобы их пользователям было достаточно использовать только указатели

List*
и
Link*
. Это значит, что реализации этих функций можно кардинально изменять, не влияя на работу их пользователей. Очевидно, что выбор имен был сделан под влиянием библиотеки STL. Структуры
List
и
Link
можно определить очевидным и тривиальным образом.


struct List {

  struct Link* first;

  struct Link* last;

};


struct Link { /* узел двухсвязного списка */

  struct Link* pre;

  struct Link* suc;

};


Приведем графическое представление контейнера

List
:



В наши намерения на входит демонстрация изощренных методов или алгоритмов, поэтому ни один из них на рисунке не показан. Тем не менее обратите внимание на то, что мы не упоминаем о данных, которые хранятся в узлах (элементах списков). Оглядываясь на функции-члены этой структуры, мы видим, что сделали нечто подобное, определяя пару абстрактных классов

Link
и
List
. Данные для хранения в узлах будут предоставлены позднее. Указатели
Link*
и
List*
иногда называют непрозрачными типами (opaque types); иначе говоря, передавая указатели
Link*
и
List*
своим функциям, мы получаем возможность манипулировать элементами контейнера
List
, ничего не зная о внутреннем устройстве структур
Link
и
List
.

Для реализации функций структуры

List
сначала включаем некоторые стандартные библиотечные заголовки.


#include

#include

#include


В языке C нет пространств имен, поэтому можно не беспокоиться о декларациях или директивах

using
. С другой стороны, мы должны были бы побеспокоиться о слишком коротких и слишком популярных именах (
Link
,
insert
,
init
и т.д.), поэтому такой набор функций нельзя использовать в реальных программах.

Инициализация тривиальна, но обратите внимание на использование функции

assert()
.


void init(struct List* lst) /* инициализируем *lst

                               пустым списком */

{

  assert(lst);

  lst–>first = lst–>last = 0;

}


Мы решили не связываться с обработкой ошибок, связанных с некорректными указателями на списки, во время выполнения программы. Используя макрос

assert()
, мы просто получим сообщение о системной ошибке (во время выполнения программы), если указатель на список окажется нулевым. Эта системная ошибка просто выдаст нам имя файла и номер строки, если будет нарушено условие, указанное как аргумент макроса
assert()
;
assert()
— это макрос, определенный в заголовочном файле
, а проверка доступна только в режиме отладки. В отсутствие исключений нелегко понять, что делать с некорректными указателями.

Функция

create()
просто создает список
List
свободной памяти. Она напоминает комбинацию конструктора (функция
init()
выполняет инициализацию) и оператора
new
(функция
malloc()
выделяет память).


struct List* create() /* создает пустой список */

{

  struct List* lst =

         (struct List*)malloc(sizeof(struct List));

  init(lst);

  return lst;

}


Функция

clear()
предполагает, что все узлы уже созданы и расположены в свободной памяти, и удаляет их оттуда с помощью функции
free()
.


void clear(struct List* lst) /* удаляет все элементы списка lst */

{

  assert(lst);

  {

  struct Link* curr = lst–>first;

  while(curr) {

    struct Link* next = curr–>suc;

    free(curr);

    curr = next;

  }

  lst–>first = lst–>last = 0;

  }

}


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

suc
класса
Link
. Мы не можем получить безопасный доступ к члену объекта после его удаления с помощью функции
free()
, поэтому ввели переменную
next
, с помощью которой храним информацию о своей позиции в контейнере
List
, одновременно удаляя объекты класса
Link
с помощью функции
free()
.

Если не все объекты структуры

Link
находятся в свободной памяти, лучше не вызывать функцию
clear()
, иначе она вызовет разрушение памяти.

Функция

destroy()
, по существу, противоположна функции
create()
, т.е. она представляет собой сочетание деструктора и оператора
delete
.


void destroy(struct List* lst) /* удаляет все элементы списка lst;

                                  затем удаляет сам список lst */

{

  assert(lst);

  clear(lst);

  free(lst);

}


Обратите внимание на то, что перед вызовом функции очистки памяти (деструктора) мы не делаем никаких предположений об элементах, представленных в виде узлов списка. Эта схема не является полноценной имитацией методов языка С++ — она для этого не предназначена.

Функция

push_back()
— добавление узла
Link
в конец списка — вполне очевидна.


void push_back(struct List* lst, struct Link* p) /* добавляет элемент p

                                                    в конец списка lst */

{

  assert(lst);

  {

    struct Link* last = lst–>last;

    if (last) {

      last–>suc = p;  /* добавляет узел p после узла last */

      p–>pre = last;

    }

    else {

      lst–>first = p; /* p — первый элемент */

      p–>pre = 0;

    }

    lst–>last = p;    /* p — новый последний элемент */

    p–>suc = 0;

  }

}


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

p
равен нулю. Передайте нуль вместо указателя на узел, и ваша программа даст сбой. Этот код нельзя назвать совершенно неправильным, но он не соответствует промышленным стандартам. Его цель — проиллюстрировать общепринятые и полезные методы (а также обычные недостатки и ошибки).

Функцию

erase()
можно было бы написать следующим образом:


struct Link* erase(struct List* lst, struct Link* p)

/* 
 удаляет узел p из списка lst;

    возвращает указатель на узел, расположенный после узла p

*/

{

  assert(lst);

  if (p==0) return 0; /* OK для вызова erase(0) */

  if (p == lst–>first) {

    if (p–>suc) {

      lst–>first = p–>suc; /* последователь становится
 первым */

      p–>suc–>pre = 0;

      return p–>suc;

    }

    else {

      lst–>first = lst–>last = 0; /* список становится
 пустым */

      return 0;

    }

  }

  else if (p == lst–>last) {

    if (p–>pre) {

      lst–>last = p–>pre;   /* предшественник становится 
последним */

      p–>pre–>suc = 0;

    }

    else {

      lst–>first = lst–>last = 0; /* список становится
 пустым */

      return 0;

    }

  }

  else {

    p–>suc–>pre = p–>pre;

    p–>pre–>suc = p–>suc;

    return p–>suc;

  }

}


Остальные функции читатели могут написать в качестве упражнения, поскольку для нашего (очень простого) теста они не нужны. Однако теперь мы должны разрешить основную загадку этого проекта: где находятся данные в элементах списка? Как реализовать простой список имен, представленных в виде С-строк. Рассмотрим следующий пример:


struct Name {

  struct Link lnk; /* структура Link нужна для выполнения ее
 операций */

  char* p;         /* строка имен */

};


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

Link
? Но поскольку мы знаем, что структура
List
хранит узлы
Link
в свободной памяти, то написали функцию, создающую объекты структуры
Name
в свободной памяти.


struct Name* make_name(char* n)

{

  struct Name* p = (struct Name*)malloc(sizeof(struct Name));

  p–>p = n;

  return p;

}


Эту ситуацию можно проиллюстрировать следующим образом:



Попробуем использовать эти структуры.


int main()

{

  int count = 0;

  struct List names; /* создает список */

  struct List* curr;

  init(&names);


  /* создаем несколько объектов Names и добавляем их в список: */

  push_back(&names,(struct Link*)make_name("Norah"));

  push_back(&names,(struct Link*)make_name("Annemarie"));

  push_back(&names,(struct Link*)make_name("Kris"));

  /* удаляем второе имя (с индексом 1): */

  erase(&names,advance(names.first,1));

  curr = names.first; /* выписываем все имена */

  for (; curr!=0; curr=curr–>suc) {

    count++;

    printf("element %d: %s\n", count, ((struct Name*)curr)–>p);

  }

}


Итак, мы смошенничали. Мы использовали приведение типа, чтобы работать с указателем типа

Name*
как с указателем типа
Link*
. Благодаря этому пользователь знает о библиотечной структуре
Link
. Тем не менее библиотека не знает о прикладном типе
Name
. Это допустимо? Да, допустимо: в языке C (и C++) можно интерпретировать указатель на структуру как указатель на ее первый элемент, и наоборот.

Очевидно, что этот пример можно также скомпилировать с помощью компилятора языка С++.


ПОПРОБУЙТЕ

Программисты, работающие на языке C++, разговаривая с программистами, работающими на языке C, рефреном повторяют: “Все, что делаешь ты, я могу сделать лучше!” Итак, перепишите пример интрузивного контейнера

List
на языке C++, продемонстрировав, что это можно сделать короче и проще без замедления программы или увеличения объектов.


Задание

1. Напишите программу “Hello World!” на языке C, скомпилируйте ее и выполните.

2. Определите две переменные, хранящие строки “Hello” и “World!” соответственно; конкатенируйте их с пробелом между ними и выведите в виде строки

Hello World!
.

3. Определите функцию на языке C, получающую параметр

p
типа
char*
и параметр
x
типа
int
, и выведите на печать их значения в следующем формате:
p is "foo" and x is 7
. Вызовите эту функцию для нескольких пар аргументов.


Контрольные вопросы

В следующих вопросах предполагается выполнение стандарта ISO C89.

1. Является ли язык C++ подмножеством языка C?

2. Кто изобрел язык C?

3. Назовите высокоавторитетный учебник по языку С.

4. В какой организации были изобретены языки C и C++?

5. Почему язык С++ (почти) совместим с языком C?

6. Почему язык C++ только почти совместим с языком C?

7. Перечислите десять особенностей языка C++, отсутствующих в языке C.

8. Какой организации “принадлежат” языки C и C++?

9. Перечислите шесть компонентов стандартной библиотеки языка C++, которые не используются в языке C.

10. Какие компоненты стандартной библиотеки языка C можно использовать в языке C++?

11. Как обеспечить проверку типов аргументов функций в языке C?

12. Какие свойства языка C++, связанные с функциями, отсутствуют в языке C? Назовите по крайней мере три из них. Приведите примеры.

13. Как вызвать функцию, написанную на языке C, в программе, написанной на языке C++?

14. Как вызвать функцию, написанную на языке C++, в программе, написанной на языке C?

15. Какие типы совместимы в языках C и C++? Приведите примеры.

16. Что такое дескриптор структуры?

17. Перечислите двадцать ключевых слов языка C++, которые не являются ключевыми словами языка C.

18. Является ли инструкция

int x
; определением в языке C++? А в языке C?

19. В чем заключается приведение в стиле языка С и чем оно опасно?

20. Что собой представляет тип

void*
и чем он отличается в языках C и C++?

21. Чем отличаются перечисления в языках C и C++?

22. Что надо сделать в программе на языке C, чтобы избежать проблем, связанных с совпадением широко распространенных имен?

23. Назовите три наиболее широко используемые функции для работы со свободной памятью в языке C.

24. Как выглядит определение в стиле языка С?

25. Чем отличаются оператор

==
и функция
strcmp()
для С-строк?

26. Как скопировать С-строки?

27. Как определить длину С-строки?

28. Как скопировать большой массив целых чисел типа

int
?

29. Назовите преимущества и недостатки функции

printf()
.

30. Почему никогда не следует использовать функцию

gets()
? Что следует использовать вместо нее?

31. Как открыть файл для чтения в программе на языке C?

32. В чем заключается разница между константами (

const
) в языке C и C++?

33. Почему мы не любим макросы?

34. Как обычно используются макросы?

35. Что такое “страж включения”?


Термины


Упражнения

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

1. Реализуйте варианты функций

strlen()
,
strcmp()
и
strcpy()
.

2. Завершите пример с интрузивным контейнером

List
из раздела 27.9 и протестируйте каждую его функцию.

3. Усовершенствуйте пример с интрузивным контейнером List из раздела 27.9 по своему усмотрению. Предусмотрите перехват и обработку как можно большего количества ошибок. При этом можно изменять детали определений структур, использовать макросы и т.д.

4. Если вы еще на переписали пример с интрузивным контейнером

List
из раздела 27.9 на языке C++, сделайте это и протестируйте каждую функцию.

5. Сравните результаты упр. 3 и 4.

6. Измените представление структур

Link
и
List
из раздела 27.9 без изменения интерфейса пользователя, обеспеченного функциями. Разместите узлы в массивах и предусмотрите члены
first
,
last
,
pre
, и
suc
типа
int
(индексы массива).

7. Назовите преимущества и недостатки интрузивных контейнеров по сравнению с неинтрузивными контейнерами из стандартной библиотеки языка С++. Составьте списки аргументов за и против этих контейнеров.

8. Какой лексикографический порядок принят на вашем компьютере? Выведите на печать каждый символ вашей клавиатуры и ее целочисленный код; затем выведите на печать символы в порядке, определенном их целочисленными кодами.

9. Используя только средства языка C, включая его стандартную библиотеку, прочитайте последовательность слов из потока

stdin
и выведите ее в поток
stdout
в лексикографическом порядке. Подсказка: функция сортировки в языке C называется
qsort()
; найдите ее описание. В качестве альтернативы вставляйте слова в упорядоченный список по мере его считывания. В стандартной библиотеке языка C списка нет.

10. Составьте список свойств языка C, заимствованных у языков C++ или C with Classes (раздел 27.1).

11. Составьте список свойств языка C, не заимствованных у языка C++.

12. Реализуйте (либо с помощью С-строк, либо с помощью типа

int
) таблицу поиска с операциями
find(struct table*, const char*)
,
insert(struct table*, const char*, int)
и
remove(struct table*, const char*)
. Эту таблицу можно представить в виде массива пар структур или пар массивов (
const char*[]
и
int*
); выбирайте сами. Выберите типы возвращаемых значений для ваших функций. Документируйте ваши проектные решения.

13. Напишите программу на языке С, которая является эквивалентом инструкций

string s
;
cin>>s
;. Иначе говоря, определите операцию ввода, которая считывала бы в массив символов, завершающийся нулем, произвольно длинную последовательность символов, разделенных пробелами.

14. Напишите функцию, получающую на вход массив целых чисел типа

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

15. Сымитируйте одиночное наследование в языке C. Пусть каждый базовый класс содержит указатель на массив указателей на функции (для моделирования виртуальных функций как самостоятельных функций, получающих указатель на объект базового класса в качестве своего первого аргумента); см. раздел 27.2.3. Реализуйте вывод производного класса, сделав базовый класс типом первого члена производного класса. Для каждого класса соответствующим образом инициализируйте массив виртуальных функций. Для проверки реализуйте вариант старого примера с классом

Shape
с базовой и производной функциями
draw()
, которые просто выводили имя своего класса. Используйте только средства и библиотеку, существующие в стандарте языка С.

16. Для запутывания реализации предыдущего примера (за счет упрощения обозначений) используйте макросы.


Послесловие

Мы уже упоминали выше, что не все вопросы совместимости решены наилучшим образом. Тем не менее существует много программ на языке С (миллиарды строк), написанных кем-то, где-то и когда-то. Если вам придется читать и писать такие программы, эта глава подготовит вас к этому. Лично мы предпочитаем язык C++ и в этой главе частично объяснили почему. Пожалуйста, не недооценивайте пример интрузивного списка

List
— интрузивные списки
List
и непрозрачные типы являются важной и мощной технологией (как в языке C, так и в языке C++).

Часть V