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

Программирование на языке Си для решения физических задач. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
738 Кб
Скачать

В литературе по программированию часто можно встретить вид функции main(), следующий утверждению о том, что в основной программе должны содержаться исключительно вызовы функций:

void main ( )

{

load ( ) ; run ( ) ; save ( ) ;

}

Некоторые программисты считают, что такой подход не помогает читаемости кода. В самом деле, дойдя до вызова функции load(), программист из названия понимает, что эта функция загружает какие-то данные. Но больше ее название ни о чем не говорит. Для того чтобы понять, что эта функция делает, необходимо просмотреть ее исходный код, который, возможно, еще и содержится в другом файле. То же самое касается и других функций. Таким образом, все равно нужно будет прочитать исходный код всех этих функций, как если бы они были объявлены прямо внутри main(). Поэтому такой вариант:

void main ( )

{

/

Load

data /

. . .

 

 

/

Run

c a l c u l a t i o n s /

. . .

 

 

/ save r e s u l t s /

. . .

}

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

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

11

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

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

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

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

12

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

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

1.3.Синтаксис

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

1.3.1Типы данных

Стандартные типы данных следующие: void (пустой); int (целый); char (символьный); wchar (расширенный символьный); bool (логический); float (вещественный); double (вещественный с двойной точностью).

Существуют четыре спецификатора типа, уточняющих внутреннее представление и диапазон значений стандартных типов:

13

short (короткий); long (длинный); signed (знаковый); unsigned (беззнаковый).

Точные диапазоны значений, размер соответствующих переменных зависят от используемой платформы. Эти сведения можно посмотреть в заголовочных файлах <limits.h> для целых типов и <float.h> — для вещественных. Обычно тип int имеет диапазон от -32 767 до 32 767; размер переменной 4 б, т.е. четыре байта (32 бит). Самое большое целое число может быть представлено типом long long int, дающим размер переменной 8 б и диапазон от минус до плюс 9 223 372 036 854 775 807. Тип float служит для представления вещественных чисел одинарной точности. Переменная этого типа занимает 32 бита (4 б, как и размер int), из которых 1 бит приходится на знак, 8 бит на показатель и 24 бита на мантиссу. Точность составляет 6 знаков после запятой. Максимальное число типа float примерно равно 3.4E+38. Число типа double занимает 8 б, имеет 15 знаков точности и позволяет представлять числа до 1.7E+308. Самым точным является long double, имеющий 19 знаков точности и максимальное число 1.1E+4932. Впрочем, этот тип может быть не реализован, поэтому его использование не рекомендуется1. Нужно также помнить, что у всех этих типов существует минимальное представимое число. Попытка оперировать с меньшими числами приводит к неопределенным результатам.

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

1.3.2Операции

Операция присваивания: a = b

Арифметические операции: + - / * %(остаток) « » (сдвиг)

1 В MS Visual Studio long double соответствует просто типу double.

14

Сокращенные формы:

a += b

a = a + b

a -= b

a = a - b

a /= b

a = a/b

a *= b

a = a*b

a++

a = a + 1

a--

a = a - 1

1.3.3 Ветвление

Полная форма оператора if:

i f

( cond ) {

 

. . .

}

else {

}

. . .

 

Без «если»

i f ( cond ) {

. . .

}

Логические операторы: && И ; || ИЛИ; == равно; ! НЕ Тернарная альтернатива:

max = ( b > a ) ? b : a ;

Оператор switch

swith ( expr ) {

case value1 : . . . ; break ; case value2 : . . . ; break ;

. . .

default : . . . ;

}

1.3.4Циклы

Циклы while:

while ( cond ) {

. . .

}

15

do {

.. .

}while ( cond ) ;

Цикл for:

for

( cond1 ; cond2 ;

o p e r a t o r )

//Example :

 

 

for

( int i

= 1 ; i <

1 0 ; i ++)

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

1.3.5Переменные

Общий вид оператора описания переменных: [класс памяти] [const] тип имя [инициализатор]; Примеры описания:

short int a = 1 ;

const

char C = ’C ’ ;

char

s ,

s f

= ’ f ’ ;

char

t

[ 5 4

] ;

double

c =

0 . 2 2 , x [ 3 ] , sum ;

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

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

16

Определены следующие классы памяти:

1)extern — означает, что переменная определяется в другом месте программы (в другом файле или дальше по тексту);

2)static — статическая переменная; время жизни — постоянное; инициализируется один раз при первом выполнении оператора, содержащего определение переменной; в зависимости от расположения оператора описания, статические переменные могут быть глобальными и локальными; локальные статические переменные видны только в том модуле, в котором они описаны;

3)register — аналогично auto, но память выделяется по возможности в регистрах процессора; если такой возможности у компилятора нет, переменные обрабатываются как auto.

Вот пример объявления идентификаторов:

extern int bar ;

extern int g ( int , int ) ;

double f ( int , double ) ; // e x t e r n can be omitted // f o r f u n c t i o n d e c l a r a t i o n s

(Имя функции имеет атрибут extern по умолчанию.) Определения этих же идентификаторов могут иметь вид:

int

bar ;

 

 

int

g ( int lhs ,

int rhs )

{return l h s rhs ; }

double f ( int i ,

double

d ) {return i+d ; }

Идентификатор может быть объявлен сколько угодно раз, но определен только один раз (на всю программу!).

Приведенные правила важно понимать для разбиения программы на отдельные файлы, см. 1.7.

1.3.6Функции

Определение функции имеет вид:

return_type

f_name ( type_1 val_1 , type_2 val_2 )

{

 

return_type ans ;

. . .

ans ;

return

}

 

17

void proc ( type_1 val_1 , type_2 val_2 , type_n val_n )

{

. . .

h = f_name ( a , b , c ) ;

. . .

}

Если функция ничего не возвращает, считается, что она возвращает пустой тип void. В этом случае использовать оператор return в теле нельзя.

Любопытно, что функция main является единственной функцией, в которой необязательно использовать слово return, даже если она объявлена как int main(). Считается, что по умолчанию она возвращает 0.

1.3.7Структуры

Структуры служат для объединения нескольких переменных одного типа в одну сущность. Определение2 структуры:

struct foo {

 

type1

var1 ;

type2

var2 ;

type3

var3 ;

} ;

 

Обратите внимание на ’;’ после определения структуры. Экземпляр структуры определяется так:

struct foo A;

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

typedef struct foo foo_st ;

Тогда экземпляр можно определить так: foo_st A; . Доступ к полям экземпляра производится с помощью ’.’:

A. var1 = 2 ; A. var2 = s ;

2 Далее в тексте слова «определение» и «объявление» используются свободно. Когда важно их формальное различие, они выделены курсивом.

18

Если переменная B — указатель на экземпляр структуры, то для доступа к значением полей используется сокращенная форма B->var1 вместо (*B).var1 .

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

struct foo {

 

type1

var1 ;

type2

var2 ;

type3

var3 ;

} A, B;

 

При использовании структур в С необходимо помнить о выравнивании выделяемой памяти. Технически наименьший размер данных, который можно адресовать — это слово, 4 б (32 бита). Поэтому при сохранении структуры в памяти компилятору приходится оставлять дополнительные пустые ячейки с тем, чтобы начало каждой переменной приходилось на адрес, кратный 4 б. Например, в следующей структуре

struct f 1 { char s1 ; int n ; char s2 ;

}

после поля s1 придется добавить 3 б пустых. В противном случае поле n начиналось бы на 2 ячейке и, так как тип int имеет размер 4 б, заканчивалось бы на 5 ячейке, делая доступ к этой переменной невозможным, потому что граница слова, 4 ячейка, оказывается внутри ячеек, занимаемых переменной n. Также придется добавить 3 б после переменной s2. Хотя в структуре больше полей нет, в эти ячейки все равно нельзя писать следующие переменные программы. В результате размер структуры f1 составляет 12 б, вместо ожидаемых 6.

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

19

struct f 2 { int n ;

char s1 ; char s2 ;

}

то компилятор дополнит только 2 б после последнего поля, чтобы структура заканчивалось на адресе, кратным 4 б. Тогда полный размер структуры составит 8 б.

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

1.4.Указатели и массивы

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

Основным применением указателей является доступ к «динамически» выделяемым переменным, расположенным в особой области памяти, называемой «кучей» (heap, хип). Локальные же переменные, в том числе параметры функций, во время рабо-

20

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]