Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Сабуров С.В. - Язык программирования C и C++ - 2006

.pdf
Скачиваний:
312
Добавлен:
13.08.2013
Размер:
1.42 Mб
Скачать

Тонкости и хитрости в вопросах и ответах

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

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

Если вы не согласны или надеетесь с помощью какого то трюка заставить компилятор, обремененный ограничивающим количество значимых символов компоновщиком, понимать большее количество этих символов, читайте превосходно написанный раздел 3.1.2 X3.159 «Комментариев».

Какая разница между memcpy и memmove?

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

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

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

Почему не определены некоторые подпрограммы из стандартной ANSI! библиотеки, хотя у меня ANSI совместимый компилятор?

Нет ничего необычного в том, что компилятор, воспринимающий ANSI синтаксис, не имеет ANSI совместимых головных файлов или стандартных библиотек.

Почему компилятор «Frobozz Magic C», о котором говорится, что он ANSI!совместимый, не транслирует мою программу? Я знаю, что текст подчиняется стандарту ANSI, потому что он транслируется компилятором gcc

Практически все компиляторы (а gcc — более других) поддерживают некоторые нестандартные расширения. Уверены ли вы, что отвергнутый текст не применяет одно из таких расширений? Опасно экспериментировать с компилятором для

567

Тонкости и хитрости в вопросах и ответах

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

Почему мне не удаются арифметические операции с указателем типа void *?

Потому что компилятору не известен размер объекта, на который указывает void *. Перед арифметическими операциями используйте оператор приведения к типу (char *) или к тому типу, с которым собираетесь работать.

Правильна ли запись a[3]="abc"? Что это значит?

Эта запись верна в ANSI C (и, возможно, в некоторых более ранних компиляторах), хотя полезность такой записи сомнительна. Объявляется массив размера три, инициализируемый тремя буквами 'a', 'b' и 'c' без завершающего стринг символа '\0'. Массив, следовательно, не может использоваться как стринг функциями strcpy, printf %s и т.п.

Что такое #pragma и где это может пригодиться?

Директива #pragma обеспечивает особую, точно определенную «лазейку» для выполнения зависящих от реализации действий: контроль за листингом, упаковку структур, подавление предупреждающих сообщений (вроде комментариев /* NOTREACHED */ старой программы lint) и т.п.

Что означает «#pragma once»? Я нашел эту директиву в одном из головных файлов

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

Вроде бы существует различие между зависимым от реализации, неописанным(unspecified) и неопределенным (undefined) поведением. В чем эта разница?

Если говорить кратко, то при зависимом от реализации поведении необходимо выбрать один вариант и документировать его. При неописанном поведении также выбирается один из вариантов, но в этом случае нет необходимости это документировать. Неопределенное поведение означает, что может произойти все что угодно. Ни в одном из этих случаев Стандарт не выдвигает требований; в первых двух случаях Стандарт иногда

568

Тонкости и хитрости в вопросах и ответах

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

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

Как написать макрос для обмена любых двух значений?

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

Не годится этот прием и в случае, когда оба числа — на самом деле одно и то же число. Из за многих побочных эффектов не годится и «очевидное» суперкомпактное решение для целых чисел a^=b^=a^=b. Когда макрос предназначен для переменных произвольного типа (обычно так и бывает), нельзя использовать временную переменную, поскольку не известен ее тип, а стандартный Си не имеет оператора typeof.

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

У меня есть старая программа, которая пытается конструировать идентификаторы с помощью макроса #define Paste(a, b) a/**/b, но у меня это не работает

То, что комментарий полностью исчезает, и, следовательно, может быть использован для склеивания соседних лексем (в частности, для создания новых идентификаторов), было недокументированной особенностью некоторых ранних реализаций препроцессора, среди которых заметна была реализация Рейзера (Reiser). Стандарт ANSI, как и K&R, утверждает, что комментарии заменяются единичными пробелами. Но поскольку необходимость склеивания лексем стала очевидной, стандарт ANSI ввел для этого специальный оператор ##, который может быть использован так:

#define Paste(a, b) a##b

Как наилучшим образом написать cpp макрос, в котором есть несколько инструкций?

Обычно цель состоит в том, чтобы написать макрос, который не отличался бы по виду от функции. Это значит, что

569

Тонкости и хитрости в вопросах и ответах

завершающая точка с запятой ставится тем, кто вызывает макрос, а в самом теле макроса ее нет.

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

Обычно эта проблема решается с помощью такого определения:

#define Func() do { \

/*

объявления

*/

\

что то1;

\

 

 

что то2;

\

 

 

/*

... */ \

 

 

}

while(0)

/*

(нет завершающей ; ) */

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

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

Можно ли в головной файл с помощью #include включить другой головной файл?

Это вопрос стиля, и здесь возникают большие споры. Многие полагают, что «вложенных с помощью #include файлов» следует избегать: авторитетный Indian Hill Style Guide неодобрительно отзывается о таком стиле; становится труднее найти соответствующее определение; вложенные #include могут привести к сообщениям о многократном объявлении, если головной файл включен дважды; также затрудняется корректировка управляющего файла для утилиты Make. С другой стороны, становится возможным использовать модульный принцип при создании головных файлов (головной файл включает с помощью #include то, что необходимо только ему; в противном случае придется каждый раз использовать

570

Тонкости и хитрости в вопросах и ответах

дополнительный #include, что способно вызвать постоянную головную боль); с помощью утилит, подобных grep (или файла tags) можно легко найти нужные определения вне зависимости от того, где они находятся, наконец, популярный прием:

#ifndef HEADERUSED #define HEADERUSED

...содержимое головного файла...

#endif

делает головной файл «идемпотентным», то есть такой файл можно безболезненно включать несколько раз; средства автоматической поддержки файлов для утилиты Make (без которых все равно не обойтись в случае больших проектов) легко обнаруживают зависимости при наличии вложенных #include.

Работает ли оператор sizeof при использовании средства препроцессора #if?

Нет. Препроцессор работает на ранней стадии компиляции, до того как становятся известны типы переменных. Попробуйте использовать константы, определенные в файле <limits.h>, предусмотренном ANSI, или «сконфигурировать» вместо этого командный файл. (А еще лучше написать программу, которая по самой своей природе нечувствительна к размерам переменных).

Можно ли с помощью #if узнать, как организована память машины — по принципу: младший байт — меньший адрес или наоборот?

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

А уверены ли вы, что нужно точно знать тип организации памяти? Уж лучше написать программу, которая от этого не зависит.

Во время компиляции мне необходимо сложное препроцесссирование, и я никак не могу придумать, как это сделать с помощью cpp

cpp не задуман как универсальный препроцессор. Чем заставлять cpp делать что то ему не свойственное, подумайте о написании небольшого специализированного препроцессора. Легко раздобыть утилиту типа make(1), которая автоматизирует этот процесс.

Если вы пытаетесь препроцессировать что то отличное от Си, воспользуйтесь универсальным препроцессором, (таким как m4).

571

Тонкости и хитрости в вопросах и ответах

Мне попалась программа, в которой, на мой взгляд, слишком много директив препроцессора #ifdef. Как обработать текст, чтобы оставить только один вариант условной компиляции, без использования cpp, а также без раскрытия всех директив #include и #define?

Свободно распространяются программы unifdef, rmifdef и scpp, которые делают в точности то, что вам нужно.

Как получить список предопределенных идентификаторов?

Стандартного способа не существует, хотя необходимость возникает часто. Если руководство по компилятору не содержит этих сведений, то, возможно, самый разумный путь — выделить текстовые строки из исполнимых файлов компилятора или препроцессора с помощью утилиты типа strings(1) системы Unix. Имейте в виду, что многие зависящие от системы предопределенные идентификаторы (например, «unix») нестандартны (поскольку конфликтуют с именами пользователя) и поэтому такие идентификаторы удаляются или меняются.

Как написать cpp макрос с переменным количеством аргументов?

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

#define DEBUG(args) (printf("DEBUG: "), printf args) if(n != 0) DEBUG(("n is %d\n", n));

Очевидный недостаток такого подхода в том, что нужно помнить о дополнительных круглых скобках. Другие решения — использовать различные макросы (DEBUG1, DEBUG2, и т.п.) в зависимости от количества аргументов, или манипулировать запятыми:

#define DEBUG(args) (printf("DEBUG: "), printf(args)) #define _ ,

DEBUG("i = %d" _ i)

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

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

Используйте головной файл <stdarg.h> (или, если необходимо, более старый <varargs.h>).

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

572

Тонкости и хитрости в вопросах и ответах

#include

<stdlib.h>

/*

для malloc, NULL, size_t */

#include

<stdarg.h>

/*

для va_ макросов */

#include

<string.h>

/*

для strcat и т.п. */

char *vstrcat(char *first, ...)

 

{

 

 

 

 

 

 

 

size_t len = 0;

 

 

 

char

*retbuf;

 

 

 

 

va_list

argp;

 

 

 

 

char

*p;

 

 

 

 

 

if(first

== NULL)

 

 

 

 

return

NULL;

 

 

 

len

= strlen(first);

 

 

 

 

va_start(argp,

first);

 

 

while((p = va_arg(argp, char *)) != NULL)

 

 

len +=

strlen(p);

 

 

 

va_end(argp);

 

 

 

 

retbuf = malloc(len + 1);

 

 

/* +1 для \0 */

 

 

 

 

if(retbuf == NULL)

 

 

 

return

NULL;

/*

ошибка */

 

 

(void)strcpy(retbuf,

first);

 

 

va_start(argp,

first);

 

 

while((p = va_arg(argp, char *)) != NULL)

 

 

(void)strcat(retbuf,

p);

 

 

va_end(argp);

 

 

 

 

return

retbuf;

 

 

}

Вызывается функция примерно так:

char *str = vstrcat("Hello, ", "world!", (char *)NULL);

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

Если компилятор разрабатывался до приема стандарта ANSI, перепишите определение функции без прототипа ("char *vstrcat(first) char *first; {") включите <stdio.h> вместо <stdlib.h>, добавьте "extern char *malloc();", и используйте int вместо size_t. Возможно, придется удалить приведение (void) и использовать varargs.h вместо stdarg.

573

Тонкости и хитрости в вопросах и ответах

Помните, что в прототипах функций со списком аргументов переменной длины не указывается тип аргументов. Это значит, что по умолчанию будет происходить «расширение» типов аргументов.

Это также значит, что тип нулевого указателя должен быть явно указан.

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

Используйте vprintf, vfprintf или vsprintf.

Перед вами подпрограмма «error», которая после строки «error:» печатает сообщение об ошибке и символ новой строки.

#include <stdio.h> #include <stdarg.h>

void

error(char *fmt, ...)

{

va_list argp; fprintf(stderr, "error: "); va_start(argp, fmt);

vfprintf(stderr, fmt, argp); va_end(argp);

fprintf(stderr, "\n");

}

Чтобы использовать старый головной файл <varargs.h> вместо <stdarg.h>, измените заголовок функции:

void error(va_alist) va_dcl

{

char *fmt;

измените строку с va_start: va_start(argp);

и добавьте строку

fmt = va_arg(argp, char *);

между вызовами va_start и vfprintf. Заметьте, что после va_dcl нет точки с запятой.

574

Тонкости и хитрости в вопросах и ответах

Как определить, сколько аргументов передано функции?

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

Любая функция с переменным числом аргументов должна быть способна по самим аргументам определить их число. Функции типа printf определяют число аргументов по спецификаторам формата (%d и т.п.) в строке формата (вот почему эти функции так скверно ведут себя при несовпадении списка аргументов и строки формата). Другой общепринятый прием — использовать признак конца списка (часто это числа 0,1, или нулевой указатель, приведенный к нужному типу).

Мне не удается добиться того, чтобы макрос va_arg возвращал аргумент типа указатель!на!функцию

Манипуляции с переписыванием типов, которыми обычно занимается va_arg, кончаются неудачей, если тип аргумента слишком сложен — вроде указателя на функцию. Если, однако, использовать typedef для определения указателя на функцию, то все будет нормально.

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

В общем случае задача неразрешима. В качестве второй функции нужно использовать такую, которая принимает указатель типа va_list, как это делает vfprintf в приведенном выше примере. Если аргументы должны передаваться непосредственно (a не через указатель типа va_list), и вторая функция принимает переменное число аргументов (и нет возможности создать альтернативную функцию, принимающую указатель va_list), то создание переносимой программы невозможно. Проблема может быть решена, если обратиться к языку ассемблера соответствующей машины.

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

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

575

Тонкости и хитрости в вопросах и ответах

таких списков, в нем есть несколько безрассудных идей, которые

можно попробовать...

Переменные какого типа правильнее использовать как булевы? Почему в языке Си нет стандартного типа логических переменных? Что использовать для значений true и false — #define или enum?

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

Выбор между #define и enum — личное дело каждого, и споры о том, что лучше, не особенно интересны.

Используйте любой из вариантов:

#define TRUE 1 #define YES 1 #define FALSE 0 #define NO 0

enum bool {false, true}; enum bool {no, yes};

или последовательно в пределах программы или проекта используйте числа 1 и 0. (Возможно, задание булевых переменных через enum предпочтительнее, если используемый вами отладчик раскрывает содержимое enum переменных).

Некоторые предпочитают такие способы задания:

#define TRUE (1==1) #define FALSE (!TRUE)

или задают «вспомогательный» макрос:

#define Istrue(e) ((e) != 0)

Не видно, что они этим выигрывают.

Разве не опасно задавать значение TRUE как 1, ведь в Си любое не равное нулю значение рассматривается как истинное? А если оператор сравнения или встроенный булев оператор возвратит нечто, отличное от 1?

Истинно (да да!), что любое ненулевое значение рассматривается в Си как значение «ИСТИНА», но это применимо только «на входе», где ожидается булева переменная. Когда булева переменная генерируется встроенным оператором, гарантируется, что она равна 0 или 1.

Следовательно, сравнение

if((a == b) == TRUE)

576

Соседние файлы в предмете Программирование на C++