Правила программирования на Си и Си++ Ален И. Голуб




Название Правила программирования на Си и Си++ Ален И. Голуб
страница 9/19
Дата публикации 19.06.2014
Размер 3 Mb.
Тип Реферат
literature-edu.ru > Авто-ремонт > Реферат
1   ...   5   6   7   8   9   10   11   12   ...   19
Часть

7

Правила, относящиеся к языку Си

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

85. Подавляйте демонов сложности (часть 2)

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

85.1. Устраняйте беспорядок

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

Таблица 2. Как ничего не делать в Си

Плохо

Хорошо

Комментарии

type *end = array;

end += len-1;

type *end =

array+(len-1)

Инициализируйте при объявлении.

while (*p++ != '\0')

while ( *p++)




while (gets(buf) != NULL)

while (gets(buf) )




if ( p != NULL )

if ( p )

!=0 ничего не делает в выражении

if ( p == NULL )

if ( !p )

отношения

if (условие != 0)

if ( условие )




if (условие == 0)

if ( !условие )




if( условие )

return TRUE;

else

return FALSE;

return условие;

(или return условие != 0). Если оно не было верным, то вы не сможете выполнить return TRUE.

return условие?0:1;

return условие?1:0;

return !условие;

return условие!=0;

Используйте соответствующий оператор. Операторы отношения типа ! и != выполняют по определению сравнение с 1 или 0.

++x;

f(x);

--x;

f( x-1 );

Не модифицируйте значение, если вам после этого не нужно его использовать более одного раза.

return ++x;

return x+1;

См. предыдущее правило.

int x;

f( (int)x );

f(x);

Переменная x и так имеет тип int.

(void) printf("все в порядке");

printf("все в порядке");

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

if ( x > y )

else if ( x < y )

else if ( x ==y )

if ( x > y )

else if ( x < y )

else

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

*(p+i)

p[i];

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

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

a + b;

Конечно, если вы хотели записать:

a += b;

то вы, должно быть, попали в беду.

85.2. Избегайте битовых масок; используйте битовые поля

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

struct fred

{

int status;

// ...§

};
#define CONDITION_A 0x01

#define CONDITION_B 0x02

#define CONDITION_C 0x03
#define SET_CONDITION_A(p) ((p)->status |= CONDITION_A)

#define SET_CONDITION_B(p) ((p)->status |= CONDITION_B)

#define SET_CONDITION_C(p) ((p)->status |= CONDITION_C)
#define CLEAR_CONDITION_A(p) ((p)->status &= ~CONDITION_A)

#define CLEAR_CONDITION_B(p) ((p)->status &= ~CONDITION_B)

#define CLEAR_CONDITION_C(p) ((p)->status &= ~CONDITION_C)
#define IN_CONDITION_A(p) ((p)->status & CONDITION_A)

#define IN_CONDITION_B(p) ((p)->status & CONDITION_B)

#define IN_CONDITION_C(p) ((p)->status & CONDITION_C)
#define POSSIBILITIES(x) ((x) & 0x0030)

#define POSSIBILITY_A 0x0000

#define POSSIBILITY_B 0x0010

#define POSSIBILITY_C 0x0020

#define POSSIBILITY_D 0x0030

Это означает необходимость в дополнение к полю из структуры данных сопровождать 17 макросов, которые к тому же будут, вероятно, спрятаны где-то в заголовочном файле, а не в том, где они используются. Ситуация еще более ухудшится, если вы не включите эти макросы и организуете проверки прямо в программе. Что-нибудь типа:

if ( struct.status &= ~CONDITION_A )

// ...

по меньшей мере, с трудом читается. Еще хуже нечто, подобное следующему:

struct.status = POSSIBILITY_A;

if ( POSSIBILITIES(struct.status) == POSSIBILITY_A )

// ...

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

enum { possibility_a, possibility_b, possibility_b, possibility_d };
struct fred

{

unsigned in_condition_a : 1;

unsigned in_condition_b : 1;

unsigned in_condition_c : 1;
unsigned possibilities : 2;

};

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

struct fred flintstone;

flintstone.in_condition_a = 1;

if ( flintstone.in_condition_a )

// ...
flintstone.possibilities = possibility_b;
if ( flintstone.possibilities == possibility_a )

// ...

Единственным очевидным исключением из этого правила является взаимодействие с архитектурами со страничной организацией памяти; битовые поля не гарантируют какого-то упорядочивания в типе int, из которого выделяются биты.

85.3. Не используйте флагов завершения

Флаг завершения типа "готов" едва ли нужен в Си или Си++. Его использование просто добавляет одну лишнюю переменную в процедуру. Не делайте так:

BOOL готов = FALSE;

while ( !готов )

{

if ( некоторое_условие() )

готов = 1;

}

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

while ( 1 )

{

if ( некоторое_условие() )

break;

}

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

Единственным исключением из этого правила является выход из вложенных циклов в Си++, где оператор goto может привести к пропуску программой вызова конструктора или деструктора. Эта проблема была рассмотрена в правиле 54.

85.4. Рассчитывайте, что ваш читатель знает Си

Не делайте чего-то подобного этому:

#define SHIFT_LEFT(x, bits) ((x) << (bits))

Программисты на Си знают, что << означает "сдвиг влево". Аналогично, не делайте таких вещей:

x++; // инкрементировать x

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

85.5. Не делайте вид, что Си поддерживает булевый тип (#define TRUE)

Нижеследующее может скорее вызвать проблемы, чем нет:

#define TRUE 1

#define FALSE 0

Любая отличная от нуля величина в Си означает истину, поэтому в следующем фрагменте f() может вернуть совершенно законное значение "истина", которое не совпало с 1, и проверка даст отрицательный результат:

if( f() == TRUE ) // Вызов не выполняется, если f() возвращает

// значение "истина", отличное от 1.

// ...

Следующий вариант надежен, но довольно неудобен. Я не думаю, что можно рекомендовать что-либо из подобной практики:

#define FALSE 0

if( f() != FALSE )

// ...

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

Часто необходимость в явном сравнении на истину или ложь можно устранить при помощи переименования:

if( я_сонливый(p) )

значительно лучше, чем:

if( я_сонливый(p) != FALSE )

Так как определения TRUE и FALSE спрятаны в макросах, то хороший сопровождающий программист не может делать каких-либо предположений об их действительных значениях. Например, FALSE может быть -1, а TRUE — 0. И следственно, если функция возвращает в явном виде TRUE или FALSE, то наш прилежный сопровождающий программист должен будет потратить несколько дней, чтобы убедиться, что при проверке возвращаемого значения для каждого вызова используется явная проверка на равенство TRUE или FALSE (сравните для примера с простым логическим отрицанием ! перед вызовом). Следующий фрагмент:

if( я_сердитый() )

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

И напоследок — имейте в виду, что следующий вариант не будет работать:

#define FALSE 0

#define TRUE !FALSE

Операция !, подобно всем операторам отношений, преобразует операнд в 1, если он имеет значение "истина" (отличен от нуля), и 0, если наоборот. Предыдущий вариант идентичен следующему:

#define FALSE 0

#define TRUE 1

Вот более надежный, но нелепый вариант:

#define IS_TRUE(x) ((x) == 0)

#define IS_FALSE(x) ((x) != 0)

86. Для битового поля размером 1 бит должен быть определен тип unsigned

После того, как ANSI Си позволил назначать битовому полю знаковый тип, мне доводилось видеть код, подобный:

struct fred

{

int i : 1;

}

a_fred;

Возможными значениями являются 0 и -1. Оператор типа:

#define TRUE 1

// ...

if( a_fred.i == TRUE )

// ...

не будет работать, потому что поле a_fred.i может иметь значение 0 или -1, но оно никогда не будет равняться 1. Следовательно, оператор if никогда не выполняется.

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

Это правило подтверждено стандартом ANSI Си, но многие программисты, похоже, не подозревают о том способе, которым язык должен работать. ANSI Си говорит, что указатель может переходить на ячейку, следующую после окончания массива, но он не может иметь величину меньше, чем базовый адрес массива. Нарушение этого правила может прервать программу, которую пытаются выполнить, например, в сегментной модели памяти процессоров 80x86. Следующий код не будет работать:

int array[ SIZE ];

int *p = array + SIZE; // Здесь все в порядке; вы можете

// двигаться дальше.

while ( --p >= array ) // Это не работает - возможен

// бесконечный цикл.

//...

Проблема состоит в том, что при сегментной архитектуре есть возможность того, что массив попадет на начало сегмента и получит исполнительный адрес 0x0000. (В архитектуре 8086 это будет смещением — частью адреса любого байта, состоящего из адреса сегмента и смещения). Если p установлен на начало массива (0x0000), то операция --p вызывает его перемещение на адрес 0xfffe (если у типа int размер 2 байта), который считается большим, чем p. Другими словами, предыдущий цикл никогда не закончится. Исправьте эту ситуацию следующим образом:

while ( --p >= array )

{

// ...

if ( p == array )

break;

}

Вы можете выйти из положения так:

int *p = array + (SIZE - 1);

do

{

// ...
} while ( p-- > array );

но позаботьтесь, чтобы p был внутри массива перед началом цикла. (Указатель должен быть инициализирован значением p+(SIZE-1), а не p+SIZE).

88. Используйте указатели вместо индексов массива

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

struct thing

{

int field;

int another_field;

int another_field;

};
thing array[ nrows ][ ncols ];

int row, col;
for ( row = 0; row < nrows ; ++nrows )

for ( col = 0; col < ncols; ++cols )

array[row][col].field = 0;

Выражение array[row][col] требует двух умножений и одного сложения во время выполнения. Вот что происходит на самом деле:

array + (row * size_of_one_row) + (col * size_of_a_thing)

Каждая структура имеет размер 12 байтов, и 12 не является степенью 2, поэтому вместо умножения нельзя использовать более эффективный сдвиг.

Вы можете сделать то же самое посредством указателей следующим образом:

thing *p = (thing *)array;

int n_cells = nrows * ncols;

while ( --n_cells >= 0 )

(p++)->field = 0;

При этом здесь вообще нет умножения во время выполнения. Оператор инкрементирования p++ просто прибавляет 12 к p.

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

Аналогично, если внутренняя часть цикла в принципе неэффективна — скажем, например, мы сделали следующее:

for ( row = 0; row < nrows ; ++nrows )

for ( col = 0; col < ncols ; ++cols )

f( array[row][col] );

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

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

89. Избегайте goto, за исключением…

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

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

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

while ( 1 )

{

while ( условие )

{

// ...

while ( другое_условие )

{

метка1:

// ...

goto метка2;

}

// ...

}
if ( третье_условие )

{

// ...

if ( другое_условие )

goto метка1;

else

{

метка2:

// ...

}

}

}

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

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

switch( некоторое_условие )

{

case A: if ( некоторое_другое_условие )

// ...

else

{

case b: // ...

}

}

Оператор goto полезен в некоторых случаях. Вот два из них:

  • Множество переходов goto к единственной метке, стоящей перед оператором return, лучше, чем множество операторов return. Такую процедуру легче отлаживать, так как для перехвата выхода из нее вы можете установить единственную точку прерывания. Имейте в виду, что метка должна предшествовать оператору; она не может стоять перед закрывающей фигурной скобкой. При необходимости пользуйтесь следующим приемом:

    // ...

    exit:

    return ;

    }

  • Переходы goto вниз по программе, обеспечивающие выход из системы вложенных циклов, лучше, чем флаг завершения типа "готов", который должен проверяться в каждом операторе управления циклом. Если каждый из операторов while в следующем примере выполнить по 100 раз, то флаг "готов" нужно проверить 1000000 раз, хотя он установлен всего лишь на случай ошибки

    int готов = 0;

    int условие1, условие2, условие3;

    // ...

    while ( !готов && условие1 )

    {

    while ( !готов && условие2 )

    {

    while ( !готов && условие3 )

    {

    if ( нечто_ужасное )

    готов = 1;

    }

    }

    }

    Исключите миллионы ненужных проверок при помощи goto следующим образом:

    while ( условие1 )

    {

    while ( условие2 )

    {

    while ( условие3 )

    {

    if ( нечто_ужасное )

    goto выход;

    }

    }

    }

    выход:

    // ...

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

1   ...   5   6   7   8   9   10   11   12   ...   19

Похожие:

Правила программирования на Си и Си++ Ален И. Голуб icon Основы информатики и вычислительной техники системы программирования
Рассматриваются основные понятия языков программирования. Излагаются процедурный и объектный подходы в программировании. Более подробно...
Правила программирования на Си и Си++ Ален И. Голуб icon Рабочая программа по курсу «основы Программирования на языке ассемблер»
Программа предназначена для обучения основам программирования на языке низкого уровня Ассемблере учащихся средних школ, учреждений...
Правила программирования на Си и Си++ Ален И. Голуб icon Конспект лекций доцента и. А. Волковой по курсу «системы программирования»
Система программирования – комплекс программных инструментов и библиотек, который поддерживает создание и существование программного...
Правила программирования на Си и Си++ Ален И. Голуб icon Практикум на ЭВМ технология программирования в среде С++
Трунов К. В., Рыков В. И. Методы и технологии С++. Технология программирования в среде С++. /Издание Башкирского ун-та. Уфа 2007....
Правила программирования на Си и Си++ Ален И. Голуб icon Гигиенические требования к условиям обучения в общеобразовательных...
Настоящие Санитарно-эпидемиологические правила(далее — Санитарные правила) направлены на предотвращение неблагоприятного воздействия...
Правила программирования на Си и Си++ Ален И. Голуб icon Среда программирования Visual C++ 0 Общий вид окна
Совокупность средств и правил для представления алгоритма в виде пригодном для выполнения вычислительной машиной называется языком...
Правила программирования на Си и Си++ Ален И. Голуб icon Обоснование выбора средств и методов разработки
На данный момент существует огромное множество языков программирования с помощью которых можно написать данную дипломную работу....
Правила программирования на Си и Си++ Ален И. Голуб icon Учебно-методический комплекс по дисциплине «Технологии программирования»
Техническое задание по разработке дизайнерского проекта приложения «умк – учебно-методический комплекс по дисциплине «Технологии...
Правила программирования на Си и Си++ Ален И. Голуб icon Создание диалектов языков программирования с использованием грамматических аспектов
Такие языки называют диалектами. Диалекты языков программирования встречаются довольно часто: практически каждая субд использует...
Правила программирования на Си и Си++ Ален И. Голуб icon Календарно тематическое планирование по литературному чтению 2 класс
...
Литература


При копировании материала укажите ссылку © 2015
контакты
literature-edu.ru
Поиск на сайте

Главная страница  Литература  Доклады  Рефераты  Курсовая работа  Лекции