4379

Язык программирования Си. Лекции

Конспект

Информатика, кибернетика и программирование

Язык Си создан в начале 70х годов Дэнисом Ритчи в Bell Telephone Laboratories для ОС UNIX. Предшественником Си является язык Би, созданный Кэном Томпсоном, который в свою очередь имеет корни в языке Мартина Ричардсона BCPL. В 1978 г. Брайн Керниган ...

Русский

2012-11-18

580 KB

10 чел.

Язык Си создан в начале 70х годов Дэнисом Ритчи в Bell Telephone Laboratories для ОС UNIX. Предшественником Си является язык Би, созданный Кэном Томпсоном, который в свою очередь имеет корни в языке Мартина Ричардсона BCPL.

В 1978 г. Брайн Керниган и Денис Ритчи написали книгу "Язык программирования Си", которую можно рассматривать как некоторый стандарт языка "K & R". Ее называют белой книгой.

В 1983г. ANSI сформировал технический комитет X3J11 для создания стандарта языка Си. Этот стандарт усовершенствуется до сих пор и поддерживается большинством фирм разработчиков компиляторов.

  1.  Правила записи программы на языке Си

Как указывалось выше, программа перед обработкой компьютером должна быть помещена в файл на диске. Обычно этот файл имеет расширение <.c>.

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

/* Включить описания функций стандартного ввода-вывода */

#include <stdio.h>

/* Включить описания математических функций */

#include <math.h>

/* Другие включаемые файлы */

.

.

.

/* Собственно текст программы */

void main( void ) /* <-- заголовок главной функции */

{

float num; /* <-- описание типов данных */

/* Исполняемые операторы -* */

num = 2.345; /* Присвоим переменной num 

значение 2.345 */

printf( " sin(%f) = %f\n", num, sin(num) ); /* Вывод на экран */

}

Любой файл начинается с директив #include, вставляющих в текст программы так называемые заголовочные файлы, которые содержат описания функций, используемых в этом файле. В нашем примере это описания стандартных функций ввода-вывода <stdio.h> и математических функций <math.h>.

Далее следует заголовок главной функции программы main, операторы описания типов данных и исполняемые операторы.

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

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

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

/* Это комментарий */

// Это тоже комментарии.

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

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

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

  1.  Правила формального описания синтаксиса языка программирования

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

  •  символы в кавычках переносятся в конструкцию языка так, как они записаны. Кавычки при этом отбрасываются. Например, "while" означает, что в конструкции языка присутствует while;
  •  имена, записанные слитно русскими и латинскими буквами, обозначают различные конструкции языка. Например, оператор_цикла;
  •  квадратные скобки охватывают элементы языка, которые могут повторяться 0 или 1 раз. Например, "AB"["C"] означает, что в конструкции языка может присутствовать или AB или ABC;
  •  фигурные скобки охватывают элементы языка, которые могут повторяться 0 или много раз. Например, "AB" { "C" } означает, что в конструкции языка может присутствовать или AB, или ABC, или ABCC и т.д.;
  •  символ | обозначает или, то есть используется для задания альтернативных значений, из списка элементов, разделенных знаком |. Например, "AB"|"C"|"ff" означает, что в конструкции языка может присутствовать или AB или C или ff;
  •  круглые скобки используются для группировки. Например, "A"("B"|"C")"D" означает, что в конструкции языка может присутствовать или ABD или ACD;
  •  многоточие используется для обозначения очевидных пропущенных значений в перечислении;
  •  символ = обозначает - слово есть. Например, буква = "A"|"B"|"C".

В дальнейшем, синтаксис языка Си будет описываться либо с помощью примеров, либо с помощью расширенной формы Бэкуса-Наура. В последнем случае это будет помечаться аббревиатурой БНФ.

  1.  Идентификаторы языка Си

Идентификаторы или имена служат для обозначения различных объектов программ: переменных (ячеек памяти), адресов, функций, файлов и т.д., иначе говоря - данных и действий над данными.

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

 БНФ:

имя = ( буква | "_" ) { буква | цифра | "_" }

буква = |"A"|"B"|...|"Y"|"Z"|"a"|"b"|...|"y"|"z"

цифра = "0"|"1"|...|"9"

При этом прописные и строчные буквы считаются разными.

Длина имени в ANSI стандарте языка Си не ограничена. В Турбо Си имя не может быть длиннее 32 символов. Например: a, a1, _a, a_b.

Выбор имен должен производиться так, чтобы имя как можно точнее соответствовало смыслу объекта или действия, которое оно обозначает. Например: speed_of_body, SpeedOfBody, BodySpeed.

Экономия на длине имен - плохой стиль программирования.

  1.  Понятие о типах данных.

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

  1.  Системы счисления. Представление данных в ЭВМ.

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

Пусть, например, имеем запись числа

an ... a3 a2 a1 a0

тогда его значение можно вычислить по следующей формуле

 an*Mn + ... + a3*M3 + a2*M2 + a1*M1 + a0*M0 ,

где an...a0 - цифры из записи числа.

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

MN - 1 .

В общепринятой десятичной системе счисления для записи чисел используются десять цифр 0,1,2,3,4,5,6,7,8,9. Основание системы счисления - 10. Значение числа определяется, например, так

9721 (10) = 9*103 + 7*102 + 2*101 + 1*100

В вычислительной технике, кроме десятичной, широко используются двоичная, восьмеричная и шестнадцатеричная системы счисления. Все данные внутри ЭВМ представлены в двоичной системе, поскольку в этом случае достаточно всего двух цифр, а электронные схемы, как правило, тоже имеют два различных состояния. Десятичная, восьмеричная и шестнадцатеричная системы используются при выводе информации для пользователя, недостающие цифры шестнадцатеричной системы счисления заменяются буквами A,B,C,D,E,F.

Приведем несколько примеров:

1010 (2) = 1*23 + 0*22 + 1*21 + 0*20 = 10 (10)

2701 (8) = 2*83 + 7*82 + 0*81 + 1*80 = 1473 (10)

F4A (16) = 15*162 + 4*161 + 10*160 = 3914 (10)

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

1473 : 8 = 184 остаток 1

184 : 8 = 23 остаток 0

23 : 8 = 2 остаток 7

2 : 8 = 0 остаток 2

---------------------------

1473 (10) = 2701 (8)

Перевод чисел из двоичной системы счисления в восьмеричную или шестнадцатеричную осуществляется путем разбиения двоичного числа на триады или тетрады и записи вместо них соответствующей восьмеричной или шестнадцатеричной цифры. Например:

0101 1011 1111 1100 (2) = 5BCF (16) = 23548 (10)

5 11=B 15=F 12=C

0 101 101 111 111 100 (2) = 55774 (8) = 23548 (10)

5 5 7 7 4

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

Целые беззнаковые числа хранятся в памяти ЭВМ в виде двоичных чисел, занимающих N двоичных разрядов. Диапазон чисел в этом случае от 0 до 2N-1. Целые числа со знаком, записанные в те же N двоичных разрядов будут иметь диапазон от -2(N-1) до 2(N-1)-1 .

Действительные числа хранятся в памяти ЭВМ в специальном формате с плавающей точкой. При этом часть двоичных разрядов ячейки хранит мантиссу числа со знаком, а другая часть - порядок числа. Диапазон действительных чисел определяется количеством двоичных разрядов, отведенных под порядок, а их точность - количеством разрядов под мантиссу.

Символы представлены в ЭВМ в виде соответствующих целочисленных кодов, хранимых в двоичной форме. Обычно под символ отводится один байт памяти, поэтому количество различных символов равно 28-1=255.

  1.  Основные типы данных языка Си

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

Назначение типа имени осуществляется с помощью описания типа.

 БНФ:

описание_типа =

["const "] имя_типа " " имя ["=" константа]

{ "," имя ["=" константа] } ";"

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

Если в описании имеется знак "=" и константа соответствующего типа, то вместе с объявлением переменной происходит ее инициализация, то есть присваивание начального значения.

В языке Си предопределены несколько имен типов:

 БНФ:

имя_типа = "int" | "short" | "long" | "char" |

"float" | "double"

int - целый длиной 2 байта, диапазон значений -32768 ... +32767;

short - целый короткий, для IBM PC аналогичен int;

long - целый длиной 4 байта, диапазон значений

char - символьный длиной 1 байт, его можно рассматривать как целое -128...+127 (иногда 0...255);

float - тип данных с плавающей точкой, длиной 4 байта, вещественное число с диапазоном значений от  до  и 6-ю значащими цифрами;

double - тип данных с плавающей точкой, длиной 8 байт, вещественное число с диапазоном значений от до  и 14-ю значащими цифрами;

Примеры описаний:

int a, b=4, c; /* описывает целые переменные a,b,c */

/* и инициализирует переменную b */

float speed, line; /* описывает вещественные переменные */

 /* speed, line */

const double pi=3.14159; /* описывается имя pi, которое */

 /* используется как константа */

/* типа double */

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

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

  1.  Правила записи констант различных типов

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

В простейшем случае в Си определены три типа констант: целые, вещественные, символьные. Рассмотрим правила их записи.

 БНФ:

целая_константа =

( десятичная | восьмеричная |

шестнадцатеричная ) ["l"|"L"]

десятичная = цифра {цифра}

восьмеричная = "0" цифра8 {цифра8}

шестнадцатеричная = "0" ( "x" | "X" ) цифра16 {цифра16}

цифра8 = "0"|"1"|...|"7"

цифра16 = "0"|"1"|...|"9"|"A"|"B"|"C"|"D"|"E"|"F"

Обычно целые константы имеют тип int, добавление сзади константы буквы l или L обозначает константу типа long.

Пример правильных целых констант: 17, 012L, 0x35, 1235L

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

БНФ:

вещ_константа = цифра {цифра} "." {цифра}

[ "e" [ "+" | "-" ] цифра {цифра} ]

Пример правильных вещественных констант: 12e-33, 12.5, 128.05e-56, 54e23 .

Отрицательные вещественные константы получаются путем добавления знака "-" перед константой без знака. Значением вещественной константы является число, которое она обозначает.

Символьной константой является любой символ, заключенный в апострофы: 'A', 'c', 'd'. Значение символьной константы - величина целого типа, равная коду символа из таблицы кодов.

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

'\n' - символ перевода строки,

'\r' - символ возврата каретки,

'\t' - символ табуляции,

'\b' - символ возврата на шаг,

'\\' - обратная косая черта,

'\"' - кавычки,

'\0' - символ с кодом 0.

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

Символ может быть представлен и его шестнадцатеричным кодом, перед которым необходимо поставить \x. Например: '\x4C'эквивалентно 'L' и т.д.

  1.  Беззнаковый тип для целых данных

Обычно целый тип охватывает и отрицательные и положительные значения. Например, тип int имеет диапазон значений от -32768 до 32767. Иногда отрицательные значения не нужны. Тогда имеется возможность вдвое увеличить диапазон положительных значений, т. е. от 0 до 65535. Это можно сделать если описать тип как беззнаковый.

 БНФ:

имя_беззнакового_типа =

 "unsigned "("int"|"long"|"short"|"char" )

 Пример:

unsigned int ab, c, d;

  1.  Символьные строки

Последовательность символов, заключенная в двойные кавычки, представляет собой символьную строку. Двойные кавычки в строку не входят. Например, "Это строка символов". Первая кавычка указывает на начало строки, последняя на ее окончание.

Строка символов хранится в памяти ЭВМ как массив символов.

Массив символов представляет собой последовательность символов, расположенных в непрерывной области памяти и объединенных общим именем:

Э

т

о

С

т

р

о

к

а

с

и

м

в

о

л

о

в

\0

В конце строки символов компилятор ставит ноль-символ, т.е. символ, код которого равен 0. Он служит признаком конца строки.

Значение символьной строки - это адрес ее первого символа.

При трансляции программы компилятор разместит все символьные строки в памяти, а в программу вместо них подставит соответствующие адреса (т.е. значения символьных строк!).

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

 БНФ:

описание_массива_символов =

"char " имя "["размер"]" { "," имя "["размер"]" } ";"

где размер - максимально возможное количество символов плюс 1 для размещения нуль символа.

 Пример:

char name[50], fio[81];

Переменная name способна хранить строку из 49 символов, fio из 80. Значениями переменной name и fio являются адреса областей памяти, выделенных для хранения соответствующих строк.

  1.  
    Понятие функции

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

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

 БНФ:

вызов_функции =

имя_ функции "(" [ аргумент { "," аргумент } ] ")"

Пример:

printf( "Это простое число %d", num );

Здесь имя функции "printf", функция имеет два аргумента, первый - строка символов, второй - переменная num.

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

A = sin( x );

Если функция возвращает значение, то ее вызов можно использовать в различных операциях:

 A = sin(x) * cos(y);

n = printf( "%d", k ) + 5;

Функции sin и cos возвращают вычисленные значения синуса и косинуса, а функция printf возвращает количество выведенных на экран байт.

Основная функция, описанная как void main ( void ), не принимает аргументов и не возвращает значения (void - пусто).

  1.  Стандартная функция printf

Функция предназначена для вывода информации на стандартное устройство вывода (stdout), которым обычно является экран дисплея.

 БНФ:

"printf" "(" формат { "," аргумент } ")"

Формат - это адрес строки символов, которая выводится в стандартное устройство вывода.

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

%d - для вывода целых чисел;

%c - для вывода образа символа, соответствующий аргумент должен содержать код символа;

%f - для вывода вещественного числа в виде целой и дробной части;

%e - для вывода вещественного числа в виде мантиссы и порядка;

%g - для вывода вещественного числа в виде %f или %e в зависимости от значения числа;

%u - для вывода беззнакового целого числа в десятичной системе счисления;

%o - для вывода беззнакового целого числа в восмеричной системе счисления;

%x - для вывода беззнакового целого числа в шестнадцатеричной системе счисления;

%s - для вывода на экран символьной строки, соответствующий аргумент должен быть адресом строки (т.е. именем символьного массива или строковой константой).

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

 БНФ:

модификатор = ["-"] {цифра1} [ "." {цифра2} ][l]

Примеры спецификаций преобразований с модификаторами:

%-20.6lf , %6d, %8.4f

Если присутствует "минус", то данные выравниваются по левой колонке поля, отведенного для вывода числа. Ширина этого поля определяется числом, составленным из цифр поля <цифра1>. Для вещественных чисел можно дополнительно задавать число знаков после запятой с помощью числа, составленного из цифр <цифра2>. Буква l в конце модификатора обозначает преобразование для длинных типов данных, т.е. long, unsigned long и double.

Функция printf возвращает число успешно выведенных байт информации.

Пример:

double a=-78.98;

int c=24;

printf("a=%8.4lf,c=%6d",a,c);

На экране:

a=-78.9800,с= 24

  1.  Стандартная функция scanf

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

 БНФ:

"scanf" "(" формат { "," аргумент } ")"

 Пример:

scanf( "%d", &n );

Перед именем аргумента функции подставляется знак &, который делает переменную n доступной для изменения. Строго говоря, операция & означает получение адреса объекта данных, т.е. мы сообщаем функции scanf информацию о том, где находится ячейка, в которую необходимо занести данные. При использовании функции scanf совместно с данными типа int, long, float, double, перед именем переменной всегда должен стоять знак &. При использовании функции для ввода символьной строки знак & не нужен, т.к. имя массива символов и так означает адрес.

Например:

char name[41];

scanf( "%s", name );

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

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

Функция scanf возвращает число успешно прочитанных элементов данных. Это свойство можно использовать для проверки правильности ввода.

Функцию scanf нужно использовать совместно с printf для вывода подсказки.

Например:

printf( "Введите ваше имя ");

scanf("%s", name );

  1.  Операции и выражения

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

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

  1.  Простейшие арифметические операции

Определены для данных арифметических типов, т.е. вещественных (float, double) и целых (int, short, long, char и соответствующих им беззнаковых данных unsigned int, unsigned short, unsigned long, unsigned char).

Действие этих операций - соответствующее арифметическое действие. Результат операции - результат арифметического действия.

Одноместные арифметические операции имеют один операнд. К ним относятся операции "+" и "-", которые меняют знак операнда.

Результатом этих операций является значение операнда с противоположным знаком.

Двуместные арифметические операции имеют два операнда. К ним относятся операции + , - , * , / и % . Каждая из этих операций выполняет соответствующее действие, результат которого и является результатом операции. Операция + вычисляет сумму операндов, операция - вычисляет разность, операция * вычисляет произведение, операция / вычисляет частное от деления вещественных операндов или целую часть частного от деления целых операндов, операция % вычисляет остаток от деления первого операнда на второй.

Арифметические операции в выражениях выполняются слева направо в соответствии с общепринятыми приоритетами. Приоритет можно изменить с помощью скобок.

Примеры выражений с арифметическими операциями:

a + b%c

(a + b)/3*5

  1.  Операция присваивания

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

Результат операции присваивания - значение присвоенной величины.

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

Например:

ab = ( c = d+5 ) * 6

Присваивание имеет самый низкий приоритет из всех операций.

  1.  Оператор-выражение

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

 БНФ:

оператор = выражение ";"

 Пример:

ab = ( c = d + 5 ) * 6; /* это оператор */

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

  1.  Использование в выражениях операндов разных типов

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

Типы данных в порядке возрастания точности:

 char, short, int, long, float, double.

 Пример:

int a, c;

float d, e;

 e = d*(c+e); /* c будет преобразовано в float */

a = 'A' + 20; /* 'A' будет преобразовано в int */

  1.  Операции преобразования типов

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

 int a, b;

float c;

 a = 7; b = 14;

c = a/b; /* результат 0 */

Для получения правильного результата можно поступить так

c = a; c = c/b;

А можно применить операцию преобразования типа:

 БНФ:

преобразование_типа = "("имя_типа ")" выражение

имя_типа - название любого типа, в том числе и заданного программистом.

Для нашего примера применение операции преобразования типа будет выглядеть так:

 c = (float)a / (float)b;

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

  1.  Стандартные математические функции

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

При использовании этих функций в программу необходимо включить файл <math.h>, т.е. необходимо использовать директиву #include <math.h> . При этом будут определены следующие функции:

sin(x) - синус (аргумент в радианах);

cos(x) - косинус (аргумент в радианах);

tan(x) - тангенс (аргумент в радианах);

asin(x) - арксинус (результат в радианах);

acos(x) - арккосинус (результат в радианах);

atan(x) - арктангенс (результат в радианах);

sinh(x) - гиперболический синус;

cosh(x) - гиперболический косинус;

tanh(x) - гиперболический тангенс;

log10(x) - десятичный логарифм;

pow10(x) - возведение числа 10 в степень x;

log(x) - натуральный логарифм;

exp(x) - экспонента;

sqrt(x) - квадратный корень;

pow(x,y) - возведение x в степень y;

fabs(x) - абсолютная величина для double;

abs(x) - абсолютная величина для int.

  1.  Простейшие функции, определяемые программистом

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

 БНФ:

заголовок_функции =

тип имя_функции"("[тип параметр {"," тип параметр}]")"

Например, заголовок функции вычисления котангенса:

double cotan ( double x )

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

Для возврата в вызывающую функцию и для передачи ей вычисленного значения используется оператор return.

 БНФ:

"return " [выражение];

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

double cotan( double x )

 {

double ctg;

ctg = 1.0 / tan(x);

return ctg;

}

или

double cotan( double x )

 {

return 1.0 / tan(x);

}

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

a = b * cotan(c);

При этом значение c будет передано функции в качестве параметра x, затем будет вычислено выражение 1/tan(x) и передано в вызывающую программу в качестве значения функции cotan.

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

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

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

  1.  Дополнительные арифметические операции

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

a = a + 1; эквивалентно a++; или ++a;

Выполнение операций "++" или "--" вызывает увеличение или уменьшение на единицу значения соответствующей переменной.

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

a = 4;

b = ( a++ ) * 5; /* здесь b = 20 */

Если используется префиксная операция, т. е. операция ++ или -- записана перед переменной, то результат операции - значение переменной после увеличения или уменьшения. Иными словами переменная вначале увеличивается или уменьшается, затем используется в выражении. Например:

a = 4;

b = ( ++ a ) * 5; /* здесь b = 25 */

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

  1.  Дополнительные операции присваивания

Очень часто в программах присутствуют операторы вида

 a = a + b;

a = a - b;

a = a * b;

a = a / b;

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

 a += b;

a -= b;

a *= b;

a /= b;

a %= b;

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

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

Использование операций ++, --, +=, , , /=, %= вместо обычных не является обязательным, но их применение считается хорошим стилем программирования на языке Cи.

  1.  Битовые операции

Любые данные, записанные в память ЭВМ, как известно, представляют собой последовательность бит, т.е. последовательность нулей и единиц. Например, любое число типа int будет занимать 2 байта в памяти, т.е 16 бит. Его можно рассматривать двояко: либо как целое число ( так и делается при выполнении операций *,/, +, - , % ), либо как последовательность бит, что возможно при использовании битовых операций.

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

В Си имеются следующие битовые операции:

~ битовое отрицание (одноместная),

& побитовое "и" (двуместная),

^ побитовое "исключающие или" (двуместная),

| побитовое "или" (двуместная).

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

Результаты битовых операций

op1

op2

~op1

op1 & op2

op1 ^ op2

op1 | op2

0

0

1

0

0

0

0

1

1

0

1

1

1

0

0

0

1

1

1

1

0

1

0

1

Рассмотрим несколько примеров.

Первый пример показывает, как с помощью операции | можно установить в единицу выбранные биты операнда:

 /* a = 00001001 = 9 */

char a, b; /* 00011010 = 26 */

a = 9; /* -------- */

b = a | 26 /* b = 31 */ /* b = 00011011 = 31 */

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

 char a, b; /* a = 00101101 = 45 */

a = 45; /* 00001111 */

b = a & 0x0F; /* -------- */

 /* b = 00001101 = 13 */

К битовым операциям относятся операции сдвига << и >> :

a << b сдвиг битов переменной a влево на b позиций,

a >> b сдвиг битов переменной a вправо на b позиций.

Например:

char a, b;

a = 26; /* a = 00011010 = 26 */

 b = a << 2; /* b = 01101000 = 104 */

Сдвиг влево равносилен умножению на 2 в соответствующей степени. Сдвиг вправо - делению на 2 в соответствующей степени.

Все битовые операции выполняются слева направо. В следующей строке приведены битовые операции в порядке уменьшения их приоритета.

~, << >>, &, ^, |

Для двуместных битовых операций определены дополнительные операции присваивания:

a <<= b; эквивалентно a = a << b,

a >>= b; эквивалентно a = a >> b,

a &= b; эквивалентно a = a & b,

a ^= b; эквивалентно a = a ^ b,

a |= b; эквивалентно a = a | b.

  1.  Операции отношения

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

> больше,

< меньше,

>= больше или равно,

<= меньше или равно,

== равно,

!= не равно.

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

Например:

int a,b;

a = 5;

 b = ( a + 5 <= 4 ); /* b = 0 */

Из сказанного выше следует, что в языке Cи отсутствует специальный логический тип данных. Его заменяет целый тип, причем логическому понятию "ложно" соответствует значение 0, а логическому понятию "истина" - любое отличное от 0 целое число.

Операции отношения выполняются слева направо. При нечетком понимании их действия возможно получение, вообще говоря, неверного результата. Например, с точки зрения синтаксиса языка Си выражение a<x<b записано совершенно правильно, но действия, выполняемые в соответствии с ним будут отличаться от принятых в математике: сначала будет вычислено выражение a<x, которое даст результат 0 или 1, а затем этот результат будет сравниваться с b.

Чтобы это выражение соответствовало математическому смыслу, его нужно разбить на две части a < x и x < b и связать его логической операцией && ("и"), т.е. (a < x) && (x < b). Такая запись читается так : если a меньше x и x меньше b, то результат - истина.

  1.  Логические операции

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

! логическое отрицание (одноместная),

&& логическое "и" (двуместная),

|| логическое "или" (двуместная).

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

Результаты логических операций

op1

op2

!op1

op1 && op2

op1 || op2

0

0

не 0 (1)

0

0

0

не 0 (1)

не 0 (1)

0

не 0 (1)

не 0 (1)

0

0

0

не 0 (1)

не 0 (1)

не 0 (1)

0

не 0 (1)

не 0 (1)

Логические операции выполняются слева направо, причем для двуместных операций второй операнд может не вычисляться, если значение первого операнда однозначно определяет значение операции. Это происходит в том случае, если первый операнд для && равен 0 (результат операции тоже 0) и если первый операнд для || не 0 (результат операции тоже не 0).

  1.  Операция определения размера данных

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

 БНФ:

"sizeof(" тип ")"

Результат этой формы операции - размер данного соответствующего типа в байтах, например, sizeof(float) даст в результате 4.

Вторая форма операции выглядит следующим образом:

 БНФ:

"sizeof " имя_данного

Результат этой формы операции - размер памяти, отведенной под соответствующее данное в байтах, например, после описания массива символов char name[40];, операция sizeof name даст в результате 40.

  1.  Приоритеты операций

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

Таблица приоритетов рассмотренных выше операций

Операции одного приоритета

Направление выполнения операции.

! ~ ++ -- (тип) sizeof

* / %

+ -

<< >>

< <= > >=

== !=

&

^

|

&&

||

= *= /= %= += -= <<= >>= &= ^= |=

  1.  Понятие о препроцессоре языка Си

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

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

Так, по директиве #include <stdio.h> в текст программы вставляет содержимое файла stdio.h, находящегося в специальном системном каталоге, а только потом происходит трансляция.

Если в директиве #include имя файла заключено в кавычки, то вставляемый файл вначале ищется по правилам, предусмотренным MSDOS, а только потом в специальном системном каталоге. Например, по директиве препроцессора #include "func.h" будет осуществлена вставка текста из файла func.h, находящегося в текущем каталоге текущего диска.

Директива #define используется для назначения символических имен различным строкам текста. Например, директива:

#define PI 3.14159

осуществляет назначение имени PI строки 3.14159. В дальнейшем, вместо 3.14159 в программе можно использовать имя PI. Вместо него перед трансляцией программы препроцессор подставит соответствующую строку.

В принципе строка может быть сколь угодно сложной, например:

 #define TITLE printf("-------sin(x)-------cos(x)-------");

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

  1.  Операторы языка Си и приемы программирования

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

Признаком конца оператора в простейшем случае является ";". Одиночная ";" является пустым оператором. Как было отмечено выше, точка с запятой поставленная в конце выражения превращает его в оператор.

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

 БНФ:

составной_оператор = "{" оператор { оператор } "}"

 Пример:

{ a = b + c; scanf( "%lf", &t ); }

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

Все операторы языка Си, кроме оператора-выражения начинаются с ключевых слов. Например, описания начинаются с int, double и т.д.

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

  1.  Оператор цикла while

Оператор цикла while предназначен для реализации циклических алгоритмов и имеет следующую форму записи

 БНФ:

"while" "(" выражение ")" оператор

 Пример :

while( a < 18 ) a = a+2;

При выполнении оператора цикла while вначале вычисляется выражение. Если оно не равно 0, то выполняется оператор. Далее снова вычисляется выражение и если оно не равно 0, то снова выполняется оператор. Такие циклические действия продолжаются до тех пор, пока выражение отлично от 0. Отсюда и название цикла while ( пока ). Как только выражение станет равным 0 цикл прекращает выполнятся и управление передается на следующий за циклом оператор.

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

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

Рассмотрим пример программы, которая выводит на экран таблицу функций sin(x) и cos(x):

 #include <math.h>

#include <stdio.h>

void main( void )

{

double x = 0;

while( x < 3.0 )

{

printf( "%6.3lf %9.6lf %9.6lf\n",x, sin(x), cos(x) );

 x += 0.2;

}

}

Если после while( x < 3.0 ) ошибочно поставить точку с запятой, то никаких сообщений об ошибках выдано не будет, но цикл станет бесконечным. Действительно, в этом случае тело цикла будет пустым оператором ";", переменная x не будет меняться, следовательно, результат выражения x < 3.0 всегда будет отличен от нуля.

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

  1.  Условный оператор и условная операция

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

 БНФ:

условный_оператор =

"if" "("выражение")" оператор_1 ["else" оператор_2]

 Пример:

if ( a ) c = b+1; else c = b - 1;

if ( a > b ) k = sin( x ); else k = x + x;

При выполнении условного оператора вначале вычисляется <выражение>. Если его значение не равно нулю, то выполняется <оператор_1>, а <оператор_2> пропускается. Если значение выражения равно нулю, то выполняется <оператор_2>, а <оператор_1> пропускается.

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

 if( b > a )

 

if( c < d ) k= sin(x);

else t = cos(x);

 

else c = tan( x );

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

 if( b < a )

{

if( c < d ) k = sin( x );

else t = cos( x );

}

else c = tan( x );

Особенно это важно если у вложенного оператора if отсутствует else, т.е

 if( b > a )

if( c < d ) k = sin( x );

 else c = tan( x ); /* else относится к

вложенному if */

 if( b > a )

{

if( c < d ) k = sin( x );

 }

else

c = tan( x ); /* else относится к первому if */

Если в качестве <оператора_1> или <оператора_2> используется группа операторов, то ее записывают как составной оператор, заключая ее в операторные скобки "{" и "}".

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

 1) короткие операторы:

if( a > b ) k = sin( x ); else k = x*x;

if( a > b ) k = sin( x );

 else k = x*x;

 2) группы коротких операторов:

 if( a > b ) { ... } else {... }

if( a > b ) { ... }

 else { ... }

или

if( ... )

{ ..... }

else

{ ..... }

 3) длинные группы операторов:

if(... )

{

.

.

.

}

else

{

.

.

.

}

4) множественный выбор:

if ( a == 6 ) { ... }

 else if ( a == 8 ) { ... }

else if ( a == 15 ) { ... }

else if ( a > 20 && a < 28 ) { ... }

else { ... }

В некоторых случаях вместо оператора if удобно использовать условную операцию ?:, которая позволяет сократить запись программ и число используемых переменных.

 БНФ:

условное_операция = выр_0 "?" выр_1 ":" выр_2

Значение условной операции равно <выр_1>, если <выр_0> не равно 0 и <выр_2> в противном случае. Например, следующий оператор

if ( x>a ) f = sin( x-a ); else f = sin(x);

можно заменить условной операцией, и сразу вычислить f:

f = sin( x>a ? x-a : x );

Очевидно, в последнем случае получился более короткий код, поскольку обращений к переменной f и функции sin вдвое меньше.

  1.  Запись алгоритмов с помощью диаграмм Несси - Шнейдермана (структограмм )

Язык Си является достаточно развитым для того, чтобы вообще обойтись без каких-либо особых способов записи алгоритмов.

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

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

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

Рассмотрим базовые логические структуры и их представление в виде структограмм.

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

Проверка условия - управление передается в один из нижних процессов:

Цикл - пока. Процесс повторяется несколько раз пока истинно условие.

  1.  Некоторые приемы программирования

Рассмотрим некоторые типовые приемы программирования и использования рассмотренных выше операторов.

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

программы, которая подсчитывает количество отрицательных значений функции sin( x ) для x меняющегося от xn до xk с шагом h.

Вначале запишем алгоритм с помощью диаграммы Несси-Шнейдермана.


Теперь можно рассмотреть программу на языке Си :

 #include <math.h>

#include <stdio.h>

 /* использование счетчика */

void main( void )

 {

double xn, xk, h, x;

int Count; /* счетчик */

printf( "Введите xn, xk, h \n" );

scanf( "%lf%lf%lf", &xn, &xk, &h );

 x = xn;

Count = 0; /* инициализация счетчика */

while( x <= xk )

{

if( sin(x) < 0 ) Count++; /* выполнение счета */

x += h;

}

printf( "Число отрицательных синусов = %d\n", Count);

}

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

 #include <math.h>

#include <stdio.h>

 /* нахождение суммы отрицательных значений синуса */

 void main( void )

{

double xn, xk, h, x, sum;

printf( "Введите xn, xk, h \n" );

scanf( "%lf%lf%lf", &xn, &xk, &h );

 x = xn;

sum = 0; /* обнуление суммы */

 while( x <= xk )

{

if( sin(x)<0 ) sum += sin(x); /* вычисление суммы */

x += h;

}

printf( "Сумма = %lf\n", sum);

 }

Для повышения эффективности программы можно описать еще одну переменную, например sinus и изменить оператор вычисления суммы:

 if( ( sinus = sin(x) ) < 0 ) sum += sinus;

В этом случае экономится время за счет однократного вычисления синуса.

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

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

if( a > b ) k = sin(x); else k = x * x;

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

 fl = 1; /* флаг */

while ( a > b && fl ) { k = sin(x); fl = 0; }

while ( fl ) { k = x * x; fl = 0; }

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

Следующая программа устанавливает факт наличия отрицательных значений sin(x) для x меняющегося от xn до xk c шагом h :

 #include <math.h>

#include <stdio.h>

 /* использование флагов */

void main( void )

 {

double xn, xk, h, x;

int minus; /* флаг */

printf( "Введите xn, xk, h \n" );

scanf( "%lf%lf%lf", &xn, &xk, &h );

 x = xn;

minus = 0; /* предполагается, что нет не одного

отрицательного значения */

while( x <= xk )

 {

if( sin(x) < 0 ) minus = 1;

x += h;

 }

if( minus ) printf( "Имеются отрицательные значения\n" );

else printf( "Все синусы положительные\n" );

}

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

  1.  Оператор прерывания цикла

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

x = xn;

 minus = 0;

while( x < xk && !minus )

 {

...

}

В языке Си есть специальный оператор для прерывания цикла. Это оператор "break;". Выполнение его в программе немедленно прерывает цикл, в котором он находится и управление передается на следующий за циклом оператор. Используя оператор break, цикл предыдущей программы можно переписать в виде:

 x = xn;

minus = 0;

while( x < xn )

{

if( sin(x) < 0 ) { minus = 1; break; }

 x += h;

}

  1.  Оператор продолжения цикла

Оператор "continue;" вызывает переход к следующей итерации цикла, т.е. к очередной проверке условия. Естественно, все операторы тела цикла, находящиеся между continue и концом тела цикла пропускаются:

while( ... )

{

...

if(...) continue;

... /* операторы пропускаются */

}

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

  1.  Пример организации простейшего меню

Современный стиль программирования предписывает использовать в программах различного рода меню, которые предоставляют пользователю возможность выбора одного из предложенных действий. Следующая программа вычисляет sin(x), cos(x), tan(x) в зависимости от выбора пользователя:

#include <math.h>

#include <stdio.h>

/* Организация простейшего меню */

void main ( void )

{

int loop; /* Флаг, который сигнализирует о конце работы */

int choice; /* Текущий выбор пункта меню */

double fun, x; /* Значения функции и аргумента */

printf( "\nВведите аргумент x=" );

scanf( "%lf", &x );

loop=1;

while ( loop )

{

printf ( "\n Введите номер функции:\n" );

 printf ( "1. sin(x)\n2. cos(x)\n" );

 printf ( "3. tan(x)\n4. Конец работы\n" );

 scanf ( "%d", &choice );

if (choice==1) fun=sin(x);

else if (choice==2) fun=cos(x);

else if (choice==3) fun=tan(x);

else if (choice==4) { loop=0; continue; }

else { printf( "Неверный выбор\n" ); continue; }

 printf( "Значение функции %lf\n", fun );

}

}

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

Для управления выходом из цикла и организации конца работы программы используется флаг loop.

  1.  Множественный выбор. Оператор переключения

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

Для осуществления подобных операций в языке Си существует специальный оператор "switch" -переключатель. Используя этот оператор, вместо вложенных "if" в предыдущей программе можно записать следующую конструкцию:

 switch ( choice )

{

case 1 : fun=sin(x); break;

case 2 : fun=cos(x); break;

case 3 : fun=tan(x); break;

case 4 : loop=0; break;

default: printf( "Неверный выбор\n" ); break;

 }

Здесь ключевое слово "case" (случай) указывает на то, что следующая константа является значением переменной choice, для которого выполняются соответствующие действия.

Оператор "break;" осуществляет выход из оператора "switch", а не из цикла. Именно поэтому для выхода из цикла пришлось использовать специальный флаг loop.

Ключевое слово "default" ( умолчание ) означает, что следом записаны действия, выполняющиеся, если значение choice не совпадет ни с одной из констант, указанных за "case".

Оператор switch в общем виде выглядит так:

 БНФ:

оператор_переключения =

"switch" "(" выражение ")"

"{"

"case" константа ":" { оператор }

"case" константа ":" { оператор }

[ "default" ":" { оператор } ]

"}"

В качестве выражения можно использовать любое выражение, имеющее символьный или целый тип. Константы тоже должны быть символьного или целого типа, например case 4 , case 'A'.

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

 case 2 :

case 4 :

case 8 : f = sin(x); break;

Если группа операторов не завершается оператором break, то будут выполняться все последующие операторы независимо от наличия case с константами до ближайшего break или другого оператора, прерывающего естественную последовательность действий. Иными словами ключевое слово case с константой надо рассматривать как метку для передачи управления. Так, в следующем операторе

switch( ch )

{

case 'A' :

case 'B' : x++;

 case 'C' :

case 'D' : f = sin(x); break;

case 'E' : f = cos(x); break;

}

будет вычислено f=sin(x+1), при ch равном 'A' или 'B'; f=sin(x), при ch равном 'C' или 'D'; f=cos(x), при ch равном 'E'.

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

  1.  Оператор цикла do-while.

Оператор цикла do-while предназначен для реализации циклических алгоритмов и имеет следующую форму записи

 БНФ:

цикл_do-while =

"do" оператор "while" "("выражение")" ";"

Оператор выполняется циклически до тех пор, пока выражение отлично от нуля. В отличие от оператора while, тело оператора do-while выполняется хотя бы один раз до первого вычисления условия.

Работу оператора do-while проиллюстрируем на примере программы, которая определяет корень уравнения x-cos(sin(x))=0 методом итераций, который заключается в циклическом вычислении очередного приближения x_new по предыдущему приближению x_old, согласно выражению x_new=cos(sin(x_old)), вытекающему из исходного уравнения. Процесс итерации заканчивается тогда, когда x_new станет равен x_old. Программа, реализующая этот алгоритм, приведена ниже.

#include <stdio.h>

#include <math.h>

/* Решение уравнения x-cos(sin(x))=0 */

 void main (void)

{

double x_new=0.9, x_old, eps=0.0001;

do

{

x_old = x_new;

x_new = cos(sin(x_old));

} while ( fabs( x_new - x_old ) > eps );

printf ( "x=%lf", x_new );

 }

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

  1.  Перечисления. Работа с клавиатурой IBM PC

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

 БНФ:

перечисление =

"enum" [ имя_перечисления ]

"{"

имя_конст [ "=" конст_выр ]

{ "," имя_конст [ "=" конст_выр ] }

"};"

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

Если константное выражение отсутствует, то имени назначается значение предыдущего выражения, увеличенное на единицу. Если отсутствует выражение, соответствующее первому имени константы, то ему назначается значение 0. Например:

enum DAYS { MON=1, TUE, WED, THU, FRI, SAT, SUN };

enum MONTH { JAN=1, FEB, MAR, APR, MAY, JUN,

JUL, AUG, SEP, OCT, NOV, DEC };

В дальнейшем в программе эти имена можно использовать вместо целых констант. Например, DEC вместо 12, THU вместо 4 и т. д.

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

В библиотеке conio имеется функция, осуществляющая ввод одиночного символа (точнее его кода) без отображения его на экране дисплея. Она имеет следующий прототип:

int getch( void );

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

Некоторым клавишам клавиатуры не соответствует ни один символ из кодовой таблицы. При нажатии на подобные клавиши getch() вначале возвращает нулевое значение. Если при этом обратиться к функции повторно, то она возвратит условный номер клавиши на клавиатуре, так называемый скэн-код. Это свойство используется в функции GetCh() для расширения возможностей getch(). Функция GetCh() будет возвращать коды символов в обычных случаях. При нажатии специальной клавиши GetCh() возвратит скэн-код, увеличенный на 256 (0x100) или на 512 (0x200), в зависимости от того, была ли нажата клавиша Shift или нет. Текст функции GetCh() приводится ниже.

#include <conio.h>

#include <bios.h>

/* Ввод одиночного символа с клавиатуры */

 int GetCh( void )

{

int ch;

if( ( ch = getch() ) == 0 )

ch = getch() | ( bioskey(2) & 3 ? 0x200 : 0x100 );

return ch;

}

Здесь, выражение bioskey(2) & 3 осуществляет проверку нажатия клавиши Shift и отлично от нуля, если последняя нажата.

Прототип функции GetCh() и коды специальных клавиш перечисления KeyboardCodes, получаемых с помощью этой функции, следует поместить в файл, например, keyboard.h и в дальнейшей использовать не числовые значения кодов, а только символические имена. Фрагмент перечисления KeyboardCodes приведен ниже:

enum KeyboardCodes

{

kbF1 = 315, kbF2, kbF3, kbF4, ..., kbF10,

kbShiftF1 = 596, kbShiftF2, kbShiftF3, ..., kbShiftF10,

kbCtrlF1 = 350, kbCtrlF2, kbCtrlF3, ..., kbCtrlF10,

kbAltF1 = 360, kbAltF2, kbAltF3, ..., kbAltF10,

kbAlt1 = 376, kbAlt2, kbAlt3, ..., kbAlt0,

kbAltQ = 272, kbAltW, kbAltE, ..., kbAltP,

kbAltA = 286, kbAltS, kbAltD, ..., kbAltL,

kbAltZ = 300, kbAltX, kbAltC, ..., kbAltM,

kbCtrlA = 1, kbCtrlB, kbCtrlC, ..., kbCtrlZ,

kbUp = 328, kbDown = 336, kbTab = 9,

kbCtrlUp = 397, kbCtrlDown = 401, kbCtrlTab = 404,

kbAltUp = 408, kbAltDown = 416, kbAltTab = 421,

kbShiftUp = 584, kbShiftDown = 592, kbShiftTab = 527,

 kbRight = 333, kbLeft = 331, kbEsc = 27,

kbCtrlRight = 372, kbCtrlLeft = 371,

kbAltRight = 413, kbAltLeft = 411, kbAltEsc = 257,

kbShiftRight = 589, kbShiftLeft = 587,

kbPgUp = 329, kbPgDn = 337, kbIns = 338,

kbCtrlPgUp = 388, kbCtrlPgDn = 374, kbCtrlIns = 513,

kbAltPgUp = 409, kbAltPgDn = 417, kbAltIns = 418,

kbShiftPgUp = 585, kbShiftPgDn = 593, kbShiftIns = 594,

kbHome = 327, kbEnd = 335, kbDel = 339,

kbCtrlHome = 375, kbCtrlEnd = 373, kbCtrlDel = 515,

kbAltHome = 407, kbAltEnd = 415, kbAltDel = 419,

kbShiftHome = 583, kbShiftEnd = 591, kbShiftDel = 595,

kbEnter = 13, kbBackspace = 8,

kbCtrlEnter = 10, kbCtrlBackspace = 127,

kbAltEnter = 284, kbAltBackspace = 270

 };

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

  1.  Пример организации светового меню

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

Экран IBM PC имеет 25 строк и 80 позиций. Нумерация строк и позиций начинается с 1. Первая строка находится вверху, первая позиция слева. Все необходимые функции работы с экраном IBM PC имеются в библиотеке компилятора, их прототипы находятся в файле conio.h. Рассмотрим некоторые из этих функций:

void clrscr( void ); - осуществляет стирание экрана;

void gotoxy( int x, int y ); - перемещает курсор в позицию x строки y;

void cprintf( char *format, ... ); - выполняет то же самое, что и printf, но выводит информацию, используя установленный цвет фона и цвет символа;

void textcolor( int color ); - установка цвета символа с кодом color;

void textbackground( int color ); - установка цвета фона с кодом color;

Последние функции не изменяют цвет уже выведенных символов. Их влияние распространяется на все последующие выводы с помощью функции cprintf.

При установке цвета допускается использовать шестнадцать цветов символа с кодами 0...15, и восемь цветов фона с кодами 0...7. Для удобства работы с цветами в conio.h определены мнемонические имена для цветов:

enum COLORS {

/* цвета для символов и фона */

BLACK /* черный */, BLUE /* синий */,

GREEN /* зеленый */, CYAN /* салатовый */,

RED /* красный */, MAGENTA /* малиновый */,

BROWN /* коричневый */, LIGHTGRAY /* светло-серый */,

/* цвета только для символов */

DARKGRAY /*темно-серый */, LIGHTBLUE /* ярко-синий */,

LIGHTGREEN /*ярко-зеленый*/, LIGHTCYAN /*ярко-салатовый*/,

LIGHTRED /*ярко-красный*/, LIGHTMAGENTA /*ярко-малиновый*/,

YELLOW /* желтый */, WHITE /* белый */ };

Приведенная ниже программа вычисляет функции sin(x), cos(x) и tan(x) в зависимости от выбора пользователя. Выбор осуществляется с помощью светового меню. Движение светового окна организуется путем перерисовки пункта меню другим цветом фона. Основной цикл этой программы управляет изменением переменной choice, в которой хранится текущий выбор пользователя.

#include <stdio.h>

#include <math.h>

#include <conio.h>

#include "keyboard.h"

#define N 4

#define ROW 10

#define COL 35

#define TEXT_C WHITE

#define TEXT_BG BLACK

#define CHOICE_BG LIGHTGRAY

void out_str( int num, int bg_color );

/* Организация светового меню */

void main ( void )

{

 int loop; /* Флаг конца работы */

int choice; /* Текущий выбор пункта меню */

int old_choice; /* Старый выбор пункта меню */

double fun, x; /* Значения функции и аргумента */

 int i;

textbackground( TEXT_BG ); textcolor( TEXT_C );

clrscr(); gotoxy( COL, ROW-1 );

cprintf( "Аргумент x=" ); scanf( "%lf", &x );

 /* Начальный вывод всех пунктов меню */

 i=1; while( i <= N ) { out_str( i, TEXT_BG ); i++; }

loop = 1; choice = 1; old_choice = 2;

while ( loop )

{

out_str( old_choice, TEXT_BG );

out_str( choice, CHOICE_BG );

old_choice = choice;

switch( GetCh() )

{

case kbUp :

if ( choice>1 ) choice--; else choice = N; break;

case kbDown :

if ( choice<N ) choice++; else choice = 1; break;

case kbEnter :

switch ( choice )

{

case 1 : fun=sin(x); break;

case 2 : fun=cos(x); break;

case 3 : fun=tan(x); break;

case 4 : loop=0; continue;

}

textbackground( TEXT_BG ); gotoxy( COL-5, ROW+6 );

 cprintf( "Значение функции %lf\n", fun );

}

}

}


/* Функция вывода строки меню с указанным цветом фона */

void out_str( int num, int bg_color )

{

textbackground( bg_color ); gotoxy( COL, ROW+num );

switch( num )

{

case 1: cprintf( "1. sin(x) " ); break;

case 2: cprintf( "2. cos(x) " ); break;

case 3: cprintf( "3. tan(x) " ); break;

case 4: cprintf( "4. Конец работы" ); break;

 }

}

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

Массивы. Адресная арифметика языка Си

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

  1.  Описание массива

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

int a[100], ab[2*40];

double c[200], speed[100];

char name[20];

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

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

d[0], d[1], d[2], d[3], d[4].

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

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

  1.  Ввод-вывод массива

Язык Си не имеет встроенных средств для ввода-вывода массива целиком, поэтому массив вводят и выводят поэлементно с помощью циклов, как, например, в следующей программе:

 #include <stdio.h>

void main(void)

{

double a[100]; int n, i;

 printf("Введите количество чисел n = ");

 scanf("%d", &n);

if( n>(sizeof a)/sizeof(double) )

 { printf("Слишком много элементов\n"); return; }

 for(i=0; i<n; i++)

{

printf("a[%d] = ", i); scanf("%lf", &a[i]);

 }

/* Операторы, обрабатывающие массив */

}

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

#include <stdio.h>

 void main(void)

{

double a[100], temp; int n, end;

for(end=n=0; n<(sizeof a)/sizeof(double); n++)

{

printf("a[%d] = ", n); scanf("%lf", &temp);

if( temp>=1.0e300 ) { end=1; break; }

a[n] = temp;

}

if( end )

 {

/* Операторы, обрабатывающие массив */

}

else

printf("Переполнение массива\n");

}

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

Следующий фрагмент программы выводит массив строками по 5 элементов. После вывода 120 элементов программа останавливается для просмотра выдачи. Очередные 120 элементов выводятся после нажатия на любую клавишу.

 for (i=0; i<n; i++)

{

printf("%10.3lf ", a[i]);

if( (i+6) % 5 == 0 ) printf("\n");

if( (i+121) % 120 == 0 ) { getch(); clrscr(); }

 }

Здесь стандартная функция clrscr() очищает экран.

  1.  Инициализация массива

Инициализация - присвоение значений вместе с описанием данных. Ранее была рассмотрена инициализация простых переменных, например:

int a = 5;

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

int a[4] = { 15, 21, 1, 304 };

индексы элементов -> 0 1 2 3

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

int c[] = { 1, 15, 18, 11, 20 };

транслятор выделит 10 байт для хранения массива из 5 двухбайтовых целых чисел.

Частный случай инициализации массива - инициализация строк. Массив символов может быть проинициализирован стандартным образом:

 char s[] = { 'A', 'B', 'C', 'D' };

Строка символов дополнительно должна завершаться нуль-символом.

 char s[] = { 'A', 'B', 'C', 'D', '\0' };

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

char s[] = "ABCD";

В этом случае нуль-символ автоматически дописывается в конец строки. Два последних примера инициализации строки совершенно эквивалентны.

  1.  Программа вычисления длины строки символов

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

 #include <stdio.h>

void main (void)

{

int len;

char str[81];

printf("Введите строку: "); scanf("%s", str);

for(len=0; str[len]; len++);

 printf("Длина строки = %d\n", len);

}

В этой программе используется цикл for с пустым оператором тела цикла. Цикл будет выполняться до тех пор, пока в строке не встретится нуль-символ, то есть пока выражение str[len] будет отлично от нуля. После окончания цикла переменная len станет равной количеству символов строки str, исключая нуль-символ.

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

int StrLen (char str[])

 {

int len;

for(len=0; str[len]; len++);

 return len;

}

При наличии функции StrLen два последних оператора предыдущей программы можно заменить одним

printf("Длина строки = %d\n", StrLen(str));

  1.  Двумерные массивы (массивы массивов)

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

int a[4][3];

Анализ подобного описания необходимо проводить в направлении выполнения операций [], то есть слева направо. Таким образом, переменная a является массивом из четырех элементов, что следует из первой части описания a[4]. Каждый элемент a[i] этого массива в свою очередь является массивом из трех элементов типа int, что следует из второй части описания.

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

Массив а

Столбец 0

Столбец 1

Столбец 2

Строка 0

18

21

5

Строка 1

6

7

11

Строка 2

30

52

34

Строка 3

24

4

67

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

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

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

В соответствии с интерпретацией описания двумерного массива (слева-направо) элементы последнего располагаются в памяти ЭВМ по строкам.

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

int a[][3] = {

{ 18, 21, 5 },

{ 6, 7, 11 },

{ 30, 52, 34 },

{ 24, 4, 67 }

};

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

Для инициализации двумерного массива символов можно использовать упрощенный синтаксис инициализации строк:

char s[][17] = {

"Строка 1",

"Длинная строка 2",

"Строка 3"

}

Размер памяти заказанный под каждую строку в этом случае должен быть равным длине самой длинной строки с учетом нуль-символа. При этом, для части строк (строка 1 и строка 3) будет выделено излишнее количество памяти. Таким образом, хранение строк различной длины в двумерном массиве символов недостаточно эффективно с точки зрения использования памяти.

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

 for (i=0; i<n; i++)

for (j=0; j<m; j++)

{

printf("a[%d][%d] = ", i, j);

scanf ("%lf", &a[i][j]);

 }

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

Вывод такого же двумерного массива иллюстрирует следующий фрагмент:

 for (i=0; i<n; i++)

{

for (j=0; j<m; j++) printf ("%9.3lf ", a[i][j]);

 printf("\n");

}

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

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

  1.  Адресная арифметика языка Си

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

Для описания указателя на какой-либо тип данных перед именем переменной ставится *. Например в строке

int *a, *b, c, d;

описываются два адреса и две переменные целого типа. В строке

double *bc;

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

int* a, b;

В этой строке создается ложное впечатление о том, что описаны два указателя на тип int, в то время как на самом деле описан один указатель на int, а именно a, и одна переменная типа int.

Описание переменных заставляет компилятор выделять память для хранения этих переменных. Описание указателя выделяет память лишь для хранения адреса. В этом смысле указатель на целое данное и на тип double будут занимать в ЭВМ одинаковое количество байт памяти, зависящее от модели памяти, на которую настроен компилятор. Например, в 16-ти разрядной Small модели длина указателя равна двум байтам, а в 16-ти разрядной Large модели - четырем.

При описании указателей в качестве имени типа данных можно использовать ключевое слово void, например

void *vd;

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

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

int *a, *b;

double *d;

void *v;

...

a = b; /* Правильно */

v = a; /* Правильно */

v = d; /* Правильно */

b = v; /* Неправильно */

d = a; /* Неправильно */

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

"Suspicious pointer conversion", которое переводится как "Подозрительное преобразование указателей".

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

b = (int *) v;

d = (double *) a;

При этом ответственность за корректность подобных операций целиком ложится на программиста. Действительно, в предыдущем примере a является указателем на ячейку памяти для хранения величины типа int. Обычно это ячейка размером 2 байта. После присваивания указателей с явным преобразования типов, делается возможным обращение к этой ячейке посредством указателя d, как к ячейке с величиной типа double. Размер этого типа обычно 8 байт, да и внутреннее представление данных в корне отличается от типа int. Никакого преобразования самих данных не делается, ведь речь идет только об указателях. Дальнейшая работа с указателем d скорее всего заденет байты, соседние с байтами на которые указывает a. Результат интерпретации этих байт будет тоже неверным.

Для поддержки адресной арифметики в языке Си имеются две специальные операции - операция взятия адреса & и операция получения значения по заданному адресу * (операция разадресации).

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

Операция * может применяться только к указателям и только в том случае когда они типизированы. При необходимости применить эту операцию к указателю типа void следует использовать явное преобразование типов. Результатом операции * является значение того объекта, к адресу которого применялась операция *, тип результата совпадает с типом объекта. Результат операции * можно использовать в любом выражении, где допускается использование объекта соответствующего типа.

Рассмотрим работу вышеописанных операций на следующем примере

int *p, a, b;

double d;

void *pd;

p = &a;

*p = 12;

p = &b;

*p = 20;

/* Здесь a содержит число 12, b - число 20 */

pd = &d;

*( (double *) pd ) = a;

/* Здесь d содержит число 12.0 */

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

Состояние ячеек до первого присваивания

 

P, адрес 1000

a, адрес 2000

b, адрес 4000

мусор

мусор

мусор

Состояние ячеек после присваивания p = &a

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

мусор

мусор

Состояние ячеек после присваивания *p = 12

 

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

12

мусор

Состояние ячеек после присваивания p = &b

 

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

мусор

Состояние ячеек после присваивания *p = 20

 

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

20

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

double *a, b;

b = *a;

*a = 135.7;

В этой последовательности используется указатель, которому предварительно не присвоено никакого значения. В ячейке a находится произвольное значение, возможно оставшееся от работы предыдущей программы. Первая операция присваивания приведет к тому, что переменная b получит значение из ячейки памяти с непредсказуемым адресом. Вторая - к тому, что по непредсказуемому адресу будут записаны 8 байт, являющиеся двоичным представлением числа 135.7. Если эти байты попадут на область данных программы, то программа скорее всего выдаст неправильный результат. Если они попадут на область кода программы или на системную область MS DOS, то в лучшем случае программа аварийно завершится, а в худшем компьютер полностью зависнет.

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

Null pointer assingment

К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться. Для компилятора фирмы Borland легко можно отследить момент некорректного обращения к памяти. Для этого нужно в окно просмотра значений выражений поместить выражение (char *) 4, затем пошагово выполнять программу до тех пор, пока строка-подпись фирмы Borland в окне просмотра не изменится. Если программа имеет большой размер, то более целесообразно выполнять ее от одной точки останова до другой.

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

#include <stdio.h>

#include <math.h>

double * Cube(double x)

{

double cube_val;

cube_val = x*x*x;

return &cube_val;

}

void main(void)

{

double *py;

py = Cube(5);

printf("y1 = %lf\n", *py);

sin(0.7);

printf("y1 = %lf\n", *py);

}

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

  1.  Указатели и одномерные массивы

В языке C понятие массива тесно связано с понятием указателя. Действительно, как было описано выше, имя массива представляет собой адрес области памяти, распределенной под этот массив, или иными словами адрес первого элемента массива. Пусть описаны следующие данные:

int a[100], *pa;

и осуществлено присваивание:

pa = a;

Оно является корректным, поскольку имя a обозначает адрес первого элемента массива a и поэтому имеет тип указателя на int. После этого присваивания

pa[0] или *pa будет обозначать a[0];

pa[1] или *(pa+1) будет обозначать a[1];

pa[2] или *(pa+2) будет обозначать a[2] и т. д. И вообще обозначения вида *(pa+n) и pa[n] являются полностью эквивалентными. Точно также эквивалентны выражения *(a+i) и a[i].

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

  •  массиву при описании выделяется память для хранения всех его элементов, а указателю только для хранения адреса;
  •  адрес массива навсегда закреплен за именем, то есть имя массива является адресной константой и выражение вида a = pa недопустимо.

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

int A[20], *pA = A;

double B[20], *pB = B;

то указатель (pA+3) будет иметь значение на 6 байт больше, чем pA, и будет адресовать элемент A[3] массива A. Указатель (pB+3) будет иметь значение на 24 байта больше, чем pB, и будет адресовать элемент B[3] массива B. С указателями типа void подобные операции выполнены быть не могут, поскольку компилятор не знает размера адресуемого данного.

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

pA = pA + i; эквивалентно pA += i;

pA = pA - i; эквивалентно pA -= i;

pA = pA + 1; эквивалентно pA++; или ++pA;

pA = pA - 1; эквивалентно pA--; или --pA; При этом, работа префиксных и постфиксных операций ++ и -- совпадает с их работой для арифметических данных.

Указатели допускается использовать в операциях сравнения. При этом всегда возможно сравнение указателя с нулем и сравнение двух однотипных указателей. Однако правильность результата последнего сравнения для 16-ти разрядного режима работы IBM PC гарантируется только в том случае, если сравниваемые указатели являются указателями на элементы одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).

Разность двух однотипных указателей представляет собой целочисленную величину равную количеству элементов данных (не байт) между соответствующими адресами памяти. Правильность результата этой операции для 16-ти разрядного режима работы IBM PC тоже гарантируется только в том случае, если указатели имеют значение адресов элементов одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).

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

double A[100], *pA, *pA100;

int i;

/* Заполняем массив A. Работаем с массивом */

for (i=0; i<100; i++) A[i]=0;

/* Заполняем массив A. Работаем с указателями */

for (pA=A, pA100=pA+100; pA<pA100; pA++) *pA=11.9;

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

  1.  Указатели и двумерные массивы

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

int A[4][2], B[2];

int *p, (*pA)[4][2], (*pAstr)[2];

Здесь A представляет собой двумерный массив из четырех строк и двух столбцов, B - одномерный массив из двух элементов. Для каждого из этих массивов будет выделено соответствующее количество памяти, достаточное для хранения всех их элементов.

Указатель p представляет собой указатель на величину int, указатель pA - указатель на двумерный массив из четырех строк и двух столбцов, pAstr - указатель на одномерный массив из двух элементов. Все указатели имеют размер, равный размеру адреса для данных в используемой модели памяти. Память для хранения данных, естественно, не выделяется. Количество элементов данных из описания массивов будет использовано лишь для корректного изменения значения указателя при выполнении над ним допустимых арифметических операций.

Смысл трактовки этих указателей определяется направлением слева-направо для подряд следующих операций [], а также изменением приоритета операции * с помощью круглых скобок. Если не поставить круглых скобок, то следующее определение

int *pa[4][2];

рассматривается как определение двумерного массива из указателей на тип int.

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

p = B;

p = &B[1];

p = &A[0][0];

p = A[2];

Следующее присваивание:

p = A; /* неверно */

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

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

p = (int *) A;

элементы, на которые ссылается указатель, и элементы массива A находятся в следующем соответствии:

p[0] эквивалентно A[0][0]

p[1] эквивалентно A[0][1]

p[2] эквивалентно A[1][0]

p[3] эквивалентно A[1][1]

p[4] эквивалентно A[2][0]

p[5] эквивалентно A[2][1]

p[6] эквивалентно A[3][0]

p[7] эквивалентно A[3][1]

Совершенно корректными являются следующие присваивания

pAstr = A;

после которого использование массива A и указателя pAstr совершенно эквивалентны:

pAstr[i][j] эквивалентно A[i][j]

Присваивание

pAstr = &A[2];

устанавливает следующее соответствие между элементами, на которые ссылается указатель pAstr и элементами массива A:

pAstr[0][0] эквивалентно A[2][0]

pAstr[0][1] эквивалентно A[2][1]

pAstr[1][0] эквивалентно A[3][0]

pAstr[1][1] эквивалентно A[3][1]

Следующие присваивания корректны

pA = &A; /* Указатель на двумерный массив */

pAstr = &B; /* Указатель на одномерный массив */

и устанавливают следующее соответствие элементов:

(*pA)[i][j] эквивалентно A[i][j]

(*pAstr)[i] эквивалентно B[i]

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

char *str[] = {

"Строка 1",

"Строка 2",

"Длинная строка 3"

};

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

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

Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находятся функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами:

double sin(double x);

double cos(double x);

double tan(double x);

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

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

double (*fn)(double x);

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

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

fn = sin; /* Настройка указателя на функцию sin */

a = fn(x); /* Вызов функции sin через указатель */

fn = cos; /* Настройка указателя на функцию cos */

b = fn(x); /* Вызов функции cos через указатель */

Можно описать массив указателей на функцию и проинициализировать его:

double (*fnArray[3])(double x) = { sin, cos, tan };

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

for(i=0; i<3; i++)

printf( "F(x) = %lf\n", fnArray[i](x) );

Можно описать функцию возвращающую значение указателя на функцию:

double (*fnFunc(int i)) (double x)

{

switch(i)

{

case 0 : return sin;

case 1 : return cos;

case 2 : return tan;

 }

}

Описанная функция имеет параметр типа int и возвращает значение указателя на функцию с аргументом типа double, возвращающую значение типа double.

После описания функции fnFunc становится возможным следующий цикл:

for(i=0; i<3; i++)

printf( "F(x) = %lf\n", fnFunc(i)(x) );

  1.  Оператор typedef

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

БНФ:

typedef описание_одного_имени

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

typedef double DArray[100];

...

DArray A, B, C;

Если бы в первом описании отсутствовало бы ключевое слово typedef, то имя DArray представляло бы имя массива из 100 элементов типа double, для которого бы выделялся соответствующий объем памяти. При наличии typedef компилятор будет воспринимать имя DArray как имя нового типа данных, а именно, типа массива из 100 элементов типа double. Очевидно, никакой памяти при этом не выделяется.

Во втором описании используется имя нового типа DArray. Каждое из определяемых имен A, B и C будет считаться массивом из ста элементов типа double, и для каждого из них будет выделен соответствующий объем памяти.

Описания указателей на функции из предыдущего раздела можно существенно упростить, используя оператор typedef:

typedef double (*Fun)(double x); /*Тип указателя*/

Fun fnArray[3] = { sin, cos, tan }; /*Массив функций*/

Fun fnFunc(int i) /* Функция, возвращающая функцию */

{

switch(i)

{

case 0 : return sin;

case 1 : return cos;

case 2 : return tan;

 }

}

Совершенно очевидно, что последние описания значительно понятнее.

  1.  Дополнительные описания указателей для IBM PC

Рассмотрим некоторые особенности режимов работы процессоров, используемых в компьютерах IBM PC. При этом следует учитывать, что процессоры фирмы Intel с типом ниже 80386 обеспечивают 16-ти битный режим работы, а процессоры 80386 и выше - как 16-ти, так и 32-битный режимы.

Типичный режим работы процессора - 16-битный, который обеспечивается собственно системой MS DOS, или DOS-сессией эмулируемой 32-битной системой Windows-95, Windows NT или OS/2.

В этом режиме процессор может использовать и двухбайтовые и четырехбайтовые адреса.

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

Физический адрес памяти компьютера вычисляется процессором по следующей формуле:

Физический_адрес = seg * 16 + offs, где seg - двухбайтовый сегментный адрес, offs - двухбайтовое смещение.

Одно двухбайтовое смещение может адресовать не более, чем 64 килобайта памяти (216), то есть так называемый сегмент. Добавление сегментной части к смещению по вышеприведенной формуле и обеспечивает адресацию 1M памяти. Однако, при такой трактовке адреса различные адреса могут указывать на один и тот же байт памяти. Рассмотрим три адреса 246:330, 256:170 и 266:10. И сегментная часть адреса, и смещение в этих адресах записаны в десятичной системе счисления, через двоеточие. Рассчитаем физические адреса для каждого из этих значений:

246 * 16 + 330 = 4266

256 * 16 + 170 = 4266

266 * 16 + 10 = 4266

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

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

Для обозначения соответствующих адресов, используются специальные ключевые слова: near - обозначает 2-х байтовый (близкий) адрес, far - 4-х байтовый (дальний) адрес.

Все действия над адресами типа far выполняются так, что их сегментная часть не меняется. Это позволяет несколько ускорить операции с указателями, но накладывает ограничение в 64K на массив, адресуемый указателем. Если массив должен быть больше 64K, то следует использовать указатели типа huge, которые автоматически поддерживают нормализацию адреса и, поэтому, могут адресовать массив больший 64K.

Примеры описания подобных указателей:

int near *pi; /* 2-х байтовый указатель */

char far *name; /* 4-х байтовый без нормализации */

double huge *pA; /* 4-х байтовый с нормализацией */

Использовать вышеприведенные описатели указателей можно только при полной уверенности в своих действия. Значительно более просто использовать различные типы адресов, меняя модели памяти. Рассмотрим 16-разрядные модели памяти IBM PC.

В крошечной (Tiny) модели памяти сегментные части всех адресов указывают на один и тот же сегмент, размером не более 64K, в котором располагается и код программы, и данные, и стек. Все адреса двухбайтовые (near).

В маленькой (Small) модели памяти сегментная часть адреса кода указывает на один сегмент, размером не более 64K, сегментная часть адресов данных указывает на другой сегмент, размером не более 64K, в котором располагаются данные и стек. Все адреса двухбайтовые (near).

В средней (Medium) модели памяти адреса кода 4-х байтовые (far), то есть размер кода может достигать 1M. Сегментная часть адресов данных указывает на сегмент, размером не более 64K, в котором располагаются данные и стек. Адреса данных двухбайтовые (near).

В компактной (Compact) модели памяти адреса кода 2-х байтовые (near), то есть размер кода не может превышать 64K. Адреса данных 4-х байтовые (far), то есть размер данных может достигать 1M. Однако, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается значительно меньше, например 4K.

В большой (Large) модели памяти все адреса 4-х байтовые (far), то есть и размер кода, и размер данных может достигать 1M. Однако, как и в предыдущей модели, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается размером 4K.

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

Следует обратить внимание на то, что ни в одной модели памяти (даже в huge) нет указателей на данные типа huge. Поэтому работа с массивами большими 64K требует специального описания указателей.

В 32-битных режимах работы 386 процессоров far-адрес состоит из 2-х байтового селектора сегмента и 4-х байтового смещения в сегменте. При этом размер смещения позволяет адресовать 4-х гигабайтное адресное пространство (232).

Для 32-х разрядных режимов могут существовать все вышеперечисленные модели памяти. Однако на практике чаще всего используется так называемая плоская (flat), безсегментная модель памяти. На самом деле она соответствует модели Small с учетом того, что размер сегмента может достигать 4 гигабайт, а смещение в сегменте имеет размер 4 байта. Можно считать, что во flat модели сегментов нет вообще, размер адреса равен 4-м байтам и соответствует физическому адресу (виртуальному) памяти компьютера.

  1.  Непосредственная работа с экранной памятью

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

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

attr = fon * 16 + sym;

или

attr = fon << 4 | sym;

Таким образом, каждой строке экрана соответствует 160 байт экранной памяти. Экранная память начинается с адреса 0xB8000000L. Буква L в конце адресной константы необходима для того, чтобы компилятор создал константу длиной 4 байта.

Ниже приведен текст функции, выводящей строку символов, на которую указывает указатель str, начиная с колонки col из строки row экрана. При выводе строки используется атрибут attr.

void PutsXY (int col, int row, char *str, int attr)

{

char far *p = (char far *) 0xB8000000L;

 p+= 160*(row-1)+2*(col-1); /* Начало строки */

while (*str)

{

*(p++) = *(str++); /* Вывод символа */

*(p++) = attr; /* Вывод атрибута */

}

}

Единственное замечание, которое следует сделать, заключается в том, что для адресации экранной области памяти используется дальний (far) указатель на символы. Такой указатель необходимо использовать для того, чтобы независимо от того, в какой 16-разрядной модели памяти создается программа (даже в Tiny), была возможной адресация этой памяти.

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

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

void PutAttr (int col, int row, int len, int attr)

{

char far *p = (char far *) 0xB8000000L;

p+= 160*(row-1)+2*(col-1)+1;

while (len--)

 {

*p = attr; /* Вывод атрибута */

p += 2; /* Переход к следующему атрибуту */

}

}

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

  1.  Дополнительные сведения о функциях
    1.  Области видимости и глобальные данные

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

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

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

После выполнения следующего примера:

#include <stdio.h>

 int i=1;

void PrintI(void)

{

printf( "i = %d\n", i );

}

void main(void)

{

int i=10;

printf( "i = %d\n", i );

PrintI();

{

int i=100;

printf( "i = %d\n", i );

PrintI();

}

printf( "i = %d\n", i );

PrintI();

}

будет напечатано

i = 10

i = 1

i = 100

 i = 1

i = 10

i = 1

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

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

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

 #include <stdio.h>

int i;

int Sum(int A[], int n)

 {

int s = 0; /* Сумма одномерного массива */

 for(i=0; i<n; i++) s += A[i];

return s;

}

void main(void)

{

int B[3][2] = { { 1, 1 },

{ 2, 2 },

{ 3, 3 } };

int s = 0; /* Сумма двумерного массива */

for(i=0; i<3; i++) s += Sum(B[i], 2);

printf("s = %d\n", s);

 }

Никаких ошибок компиляции в программе нет. Если рассматривать все ее функции по отдельности, то ошибка тоже не видна. Но программа дает неверный результат: s = 2. Это происходит потому, что цикл головной программы выполняется всего один раз, так как после возврата из функции Sum(), значение глобальной переменной i будет равно 3. Если описать переменные циклов i внутри каждой функции, то программа станет выдавать правильную сумму элементов двумерного массива: s = 12.

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

extern double Speed;

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

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

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

static int Count;

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

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

static double cotan(double x);

функция cotan будет доступна только в том модуле (файле), где она определена, то есть, где находится ее тело.

  1.  Время жизни переменных и классы памяти языка Си

Время жизни переменных программы определяется классом памяти. В языке Си принято различать статические (static), автоматические (auto) и динамические данные.

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

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

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

 #include <stdio.h>

void fun(void)

{

static int first = 1;

if( first )

 {

printf("Это первый вызов функции.\n");

first = 0;

}

else

printf("Это не первый вызов функции.\n");

 }

void main(void)

{

fun(); fun(); fun();

 }

Статическая переменная first функции fun() создается и инициализируется при запуске программы, до выполнения функции main(). Поэтому, при первом вызове функции fun() значение first равно 1, при последующих вызовах fun() переменная first будет сохранять свое значение, измененное функцией fun() на 0. Подобным же образом можно организовать подсчет числа обращений к функции.

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

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

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

Глобальные данные не могут быть автоматическими, потому что нет блока, которому они принадлежат.

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

  1.  Передача аргументов в функцию

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

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

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

#include <stdio.h>

 double Sum(double A[], int nA)

{

double s = 0;

while(nA) s += A[--nA];

return s;

}

void main (void)

{

double B[] = { 1, 2, 3, 4, 5 };

int nB = sizeof(B)/sizeof(B[0]);

printf("Сумма = %lf\n", Sum(B,nB));

printf("nB = %d\n", nB);

 }

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

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

 double Sum(double A[], int nA)

 {

double s = 0, *Aend = A + nA;

while( A < Aend ) s += *(A++);

 return s;

}

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

Вообще говоря, для формальных параметров-массивов описания в виде A[] и *A совершенно идентичны и обозначают локальную копию адреса соответствующего типа. Какое из этих описаний использовать, зависит от смысла параметра. Если это массив, то более наглядно использовать описание вида A[].

  1.  Возврат значений из функций

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

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

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

void FillArray(double A[], int nA, double val)

 {

int i;

for (i=0; i<nA; i++) A[i] = val;

}

void main (void)

{

double B[100];

 FillArray(B, 40, 35.4);

/* ... */

FillArray(&B[60], 20, 15.4);

/* ... */

}

Первый вызов FillArray() заполняет 40 первых элементов массива B значением 35.4, второй вызов заполняет 20 элементов массива B, начиная с элемента B[60], значением 15.4. При возврате из функции массив будет изменен, т. к. занесение значения val происходит непосредственно по нужному адресу.

Эту же функцию можно использовать для заполнения строк двумерного массива:

 void main (void)

{

double a[10][20];

int n = sizeof(a) / sizeof(a[0]);

int m = sizeof(a[0]) / sizeof(a[0][0]);

int i;

/* ... */

for(i=0; i<n; i++ )

FillArray(a[i], m, 14.6);

/* ... */

}

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

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

 void Decart(double *px, double *py, double r, double f)

{

(*px) = r * cos(f);

(*py) = r * sin(f);

 }

При обращении к данной функции для параметров px и py нужно передавать адреса:

 void main(void)

{

double x, y, r=5, f=0.5;

/* ... */

Decart( &x, &y, r, f );

 /* ... */

}

В данном примере при вызове функции создаются локальные копии адресов переменных x и y, а внутри функции происходит обращение к переменным x и y через их адреса (как и в случае массивов), поэтому значения x и y после вызова функции будут изменены.

  1.  Работа с динамической памятью
    1.  Стандартные функции управления динамической памятью

Данные, которые создаются, инициализируются и уничтожаются по требованию программиста называются динамическими. Для управления такими данными используются специальные стандартные функции, прототипы которых описаны в заголовочном файле <malloc.h> (для некоторых компиляторов <alloc.h>).

Для запроса динамической памяти служит функция malloc(), которая имеет следующий прототип:

 void * malloc(size_t size);

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

Параметр size, имеет тип size_t, который описан в файле <malloc.h> с помощью оператора typedef и используется для описания размеров, счетчиков и т.д. Обычно тип size_t соответствует типу unsigned int.

В том случае, когда функция malloc() не может удовлетворить запрос на память, она возвращает значение NULL, то есть значение не существующего указателя. Константа NULL описана в заголовочном файле <malloc.h>. Значение NULL возвращается и в том случае, когда значение параметра size нулевое.

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

После того, как выполнена вся работа с выделенной областью памяти, ее следует освободить с помощью функции free(), имеющей следующий прототип:

void free(void *block);

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

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

Не допускается также освобождать уже освобожденный блок памяти.

Значение параметра block равное NULL не вызывает никаких действий со стороны функции free();

Рассмотрим типичную последовательность действий при работе с динамической памятью:

 double *A; int n;

...

n = 200;

...

A = (double *) malloc( n * sizeof(double) );

 ...

/* Работа с массивом A */

...

free(A);

В рассмотренном фрагменте программы выделяется память для хранения n элементов типа double. В целях совместимости никогда не следует явно задавать размер элемента данных. Нужно пользоваться операцией sizeof(). Возвращаемое функцией malloc() значение преобразуется к типу указателя на double.

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

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

void * calloc(size_t nitems, size_t size);

Функция выделяет непрерывный блок памяти для nitems элементов данных размером size байт каждый и заполняет этот блок нулевыми значениями. В остальном работа ее аналогична работе функции malloc().

Функция realloc() служит для изменения размера ранее выделенного блока памяти:

 void *realloc(void *block, size_t size);

Здесь block - адрес ранее выделенного блока памяти, size - новый размер блока в байтах. Функция возвращает значение нового указателя на блок памяти, которое может и не совпадать со старым.

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

Все рассмотренные функции могут выделять память размером не более одного сегмента, то есть не более 64K в 16-ти разрядных моделях и не более 4G в 32-х разрядных моделях памяти.

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

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

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

 unsigned coreleft(void); /* Маленьких модели */

unsigned long coreleft(void); /* Большие модели */

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

  1.  Пример использования динамической памяти

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

 #include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#include <malloc.h>

#include <limits.h>

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

 { printf("Недостаточно памяти!\n"); exit(1); }

 return p;

}

void * Realloc ( void *block, size_t size )

{

void *p = realloc(block, size);

if( !p ) { printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

void main(void)

{

double *A, temp;

unsigned i, n, maxN, goodIO;

A = (double *) Malloc( maxN = UINT_MAX );

maxN /= sizeof(double);

for(goodIO = n = 0; n < maxN; n++)

{

printf("A[%d] = ", n); scanf("%lf", &temp);

if(temp >= 1e300) { goodIO = 1; break; }

A[n] = temp;

}

if(goodIO)

{

A = (double *) Realloc(A, n * sizeof(double));

 /* Обработка массива. Для примера - печать. */

for(i = 0; i < n; i++)

{

printf("%10.3lf ", A[i]);

if( (i + 6) % 5 == 0 ) printf("\n");

if( (i + 121) % 120 == 0 ) { getch(); clrscr(); }

 }

printf("\n");

}

free(A);

}

Максимальный размер сегмента в байтах всегда равен величине наибольшего беззнакового целого числа, значение которого определяет константа UINT_MAX из заголовочного файла <limits.h>.

В программе используются вспомогательные функции Malloc() и Realloc() для обеспечения контроля выделения памяти. В них функция exit() с прототипом в файле <stdlib.h> используется для прерывания работы программы.

  1.  Особенности работы с двумерными массивами

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

  1.  Пересчет индексов вручную

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

 #include <stdio.h>

#include <stdlib.h>

#include <malloc.h>

#define MAXVAL 1000

void *Malloc ( size_t size );

void RandomMatr ( double *Matr, int n, int l );

void OutMatr ( char *name,

double *Matr, int n, int m );

void main( void )

{

size_t n = 5, m = 6;

 double *A;

/* Выделение памяти под матрицу */

 A = (double *) Malloc( n*m*sizeof(double) );

 /* Заполнение матрицы значениями и распечатка */

 RandomMatr(A, n, m);

OutMatr("A", A, n, m);

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

free(A);

}

void RandomMatr (double *Matr, int n, int m)

{

int i, j;

for(i = 0; i < n; i++)

for(j = 0; j < m; j++)

Matr[i*m+j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double *Matr, int n, int m )

{

int i, j;

printf("\nМатрица %s\n---------------\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < m; j++)

printf("%8.1lf ", Matr[i*m+j]);

printf("\n");

}

}

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

 { printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

Функция rand() с прототипом из <stdlib.h> возвращает псевдослучайное число в диапазоне от 0 до MAXVAL-1.

  1.  Массивы с постоянной длиной строки

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

 #include <stdio.h>

#include <stdlib.h>

#include <malloc.h>

#define MAXVAL 1000

#define STRLEN 6

void *Malloc ( size_t size );

void RandomMatr ( double (*Matr)[STRLEN], int n );

void OutMatr ( char *name,

double (*Matr)[STRLEN], int n );

void main( void )

{

size_t n = 5;

 double (*A)[STRLEN];

/* Выделение памяти под матрицу */

 A = (double (*)[STRLEN])

Malloc( n*sizeof(double[STRLEN]) );

 /* Заполнение матрицы значениями и распечатка */

 RandomMatr(A, n);

OutMatr("A", A, n);

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

free(A);

}

void RandomMatr (double (*Matr)[STRLEN], int n)

{

int i, j;

for(i = 0; i < n; i++)

for(j = 0; j < STRLEN; j++)

Matr[i][j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double (*Matr)[STRLEN], int n )

{

int i, j;

printf("\nМатрица %s\n---------------\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < STRLEN; j++)

printf("%8.1lf ", Matr[i][j]);

printf("\n");

}

}

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

 { printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

В этом примере все обращения к элементам двумерного массива аналогичны случаю массива с постоянными границами. Следует обратить внимание на то, что динамическая память выделяется для одномерного массива из элементов типа double[STRLEN], то есть строк двумерного массива, которые должны иметь фиксированную длину.

  1.  Общий случай двумерного массива

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

#include <stdio.h>

 #include <stdlib.h>

#include <malloc.h>

#define MAXVAL 1000

void *Malloc ( size_t size );

double **MakeMatr ( size_t n, size_t m );

void DelMatr ( double *Matr[] );

void RandomMatr ( double *Matr[], int n, int m );

void OutMatr ( char *name,

double *Matr[], int n, int m );

void main( void )

{

int n = 5, m = 6;

 double **A;

/* Выделение памяти под матрицу */

A = MakeMatr(n, m);

/* Заполнение матрицы значениями и распечатка */

 RandomMatr(A, n, m);

OutMatr("A", A, n, m);

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

DelMatr(A);

}

void RandomMatr (double *Matr[], int n, int m)

{

int i, j;

for(i = 0; i < n; i++)

for(j = 0; j < m; j++)

Matr[i][j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double *Matr[], int n, int m )

{

int i, j;

printf("\nМатрица %s\n---------------\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < m; j++)

printf("%8.1lf ", Matr[i][j]);

printf("\n");

}

}

void *Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

 { printf("Недостаточно памяти!\n"); exit(1); }

 return p;

}

/* Конструктор матрицы */

double **MakeMatr( size_t n, size_t m )

{

double **Matr; size_t i;

Matr = (double**) Malloc( (n + 1) * sizeof(double *) );

for(i = 0; i < n; i++)

Matr[i] = (double *) Malloc( m * sizeof(double) );

Matr[n] = NULL;

return Matr;

}

/* Деструктор матрицы */

void DelMatr( double *Matr[] )

{

size_t i;

for(i = 0; Matr[i]; i++) free(Matr[i]);

 free(Matr);

}

Вначале в функции-конструкторе MakeMatr() выделяется вектор памяти размером n+1 элементов для хранения указателей на double. Затем для каждой из n строк массива выделяется память и адрес ее записывается в ранее выделенный вектор указателей. В последний элемент вектора заносится величина NULL, которая в деструкторе будет сигнализировать о конце вектора. Иначе в деструктор пришлось бы передавать дополнительный параметр n.

Рассмотренная схема выделения памяти не имеет практических ограничений даже для 16-разрядной сегментной модели памяти. Нужно лишь чтобы размер строки и размер вектора указателей не превышал сегмента.

Если массив целиком укладывается в сегмент, то для работы с ним можно предложить следующую схему с меньшими накладными расходами на выделение памяти:

 /* Конструктор матрицы */

double **MakeMatr( size_t n, size_t m )

{

double **Matr; size_t i;

Matr = (double**) Malloc( n * sizeof(double *)

+ n * m * sizeof(double) );

for(i = 0; i < n; i++)

Matr[i] = (double *)(Matr + n) + i * m;

return Matr;

}

/* Деструктор матрицы */

void DelMatr( double *Matr[] )

 {

free(Matr);

}

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

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

 #include <stdio.h>

#include <stdlib.h>

#include <malloc.h>

#define MAXVAL 1000

void *Malloc ( size_t size );

double **MakeMatr ( size_t n, size_t m );

void DelMatr ( double *Matr[] );

size_t GetN ( double *Matr[] );

size_t GetM ( double *Matr[] );

void RandomMatr ( double *Matr[] );

void OutMatr ( char *name, double *Matr[] );

void main( void )

{

int n = 5, m = 6;

 double **A;

/* Выделение памяти под матрицу */

A = MakeMatr(n, m);

/* Заполнение матрицы значениями и распечатка */

 RandomMatr( A );

OutMatr("A", A );

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

DelMatr(A);

}

void RandomMatr ( double *Matr[] )

{

int i, j, n = GetN(Matr), m = GetM(Matr);

for(i = 0; i < n; i++)

for(j = 0; j < m; j++)

Matr[i][j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double *Matr[] )

{

int i, j, n = GetN(Matr), m = GetM(Matr);

printf("\nМатрица %s\n---------------\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < m; j++)

printf("%8.1lf ", Matr[i][j]);

printf("\n");

}

}

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

 { printf("Недостаточно памяти!\n"); exit(1); }

 return p;

}

/* Конструктор матрицы */

double **MakeMatr( size_t n, size_t m )

{

double **Matr; size_t i;

Matr = (double**) Malloc( 2 * sizeof(size_t)

+ n * sizeof(double *) );

(size_t *)Matr += 2;

for(i=0; i<n; i++)

Matr[i] = (double *) Malloc( m * sizeof(double) );

Matr[n] = NULL;

*( (size_t *)Matr - 2 ) = n;

*( (size_t *)Matr - 1 ) = m;

return Matr;

}

size_t GetN( double *Matr[] )

{

return *( (size_t *)Matr - 2 );

}

size_t GetM( double *Matr[] )

{

return *( (size_t *)Matr - 1 );

}

/* Деструктор матрицы */

void DelMatr( double *Matr[] )

{

size_t i, n = GetN(Matr);

for(i = 0; i < n; i++) free(Matr[i]);

 free( (size_t *)Matr - 2 );

}

В конструкторе теперь выделяется память для хранения n указателей на double и для двух величин типа size_t, которые служат для хранения размеров матрицы. Выделять дополнительный элемент для занесения NULL теперь нет необходимости, так как теперь с помощью функций GetN() и GetM() можно получить соответствующие размеры массива. Способ индексации не изменяется и она по-прежнему выполняется в стиле индексации двумерных массивов.

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

  1.  Особенности работы с массивами большого размера

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

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

Если эта схема неприемлема или если массив одномерный, то можно воспользоваться специальным атрибутом указателя huge, который имеется у всех компиляторов, ориентированных на IBM PC.

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

При работе с массивами большого размера, соответствующий указатель должен описываться с ключевым словом huge, даже в модели памяти Huge (указатели по умолчанию - far), например:

double huge *A;

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

Для работы с большими блоками памяти используются специальные функции с префиксом far.

Функция выделения памяти:

void far *farmalloc(unsigned long size);

Выделение памяти с обнулением:

void far *farcalloc(unsigned long nitems,

 unsigned long size);

Изменение размера ранее выделенного блока памяти:

 void far *farrealloc(void far *block,

 unsigned long nbytes);

Освобождение блока памяти:

 void farfree(void far *block);

Получение информации о верхнем свободном блоке памяти:

unsigned long farcoreleft(void);

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

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

 #include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#include <malloc.h>

void far FarMalloc( unsigned long size )

{

void far *p = farmalloc(size);

if( !p )

{ printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

void main(void)

{

double huge *A;

unsigned long i, maxN;

/* Выделение максимального блока памяти */

A = (double huge *) FarMalloc( maxN = farcoreleft() );

maxN /= sizeof(double);

printf("Размер массива: %lu\n", maxN);

getch();

/* Заполняем массив */

for(i = 0; i < maxN; i++) A[i] = i;

 /* Печатаем часть массива.*/

for(i = 0; i < 1000; i++)

 {

printf("%10.3lf ", A[i]);

if( (i + 6) % 5 == 0 ) printf("\n");

if( (i + 121) % 120 == 0 ) { getch(); clrscr(); }

}

printf("\n");

 /* Освобождение памяти */

farfree(A);

}

Если в этой программе поменять атрибут huge на far, то вся адресация будет выполняться по модулю равному размеру сегмента и результат будет неверным.

  1.  Модульное программирование в системе Turbo C

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

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

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

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

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

  1.  Обеспечение корректной стыковки модулей

Основной проблемой модульного программирования является обеспечение корректной стыковки отдельных модулей. Язык СИ имеет все необходимые для этого средства. Однако, ни один компилятор языка СИ не в состоянии отследить ошибки стыковки модулей, если программист не создал всех необходимых для этого условий. Рассмотрим основные правила организации модулей при программировании на языке СИ.

Каждый модуль программы должен описываться двумя файлами. Первый файл должен содержать текст функций модуля, то есть реализацию модуля и обычно имеет расширение *.c. Второй файл содержит описание интерфейса модуля, то есть прототипы функций модуля и описания глобальных переменных модуля с помощью оператора extern. Второй файл обычно имеет расширение *.h и называется заголовочным файлом.

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

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

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

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

  1.  Создание библиотек функций

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

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

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

Для создания библиотек функций используются программы, называемые библиотекарями. В системе программирования фирмы Borland библиотекарь имеет имя tlib и может быть вызван из командной строки следующим образом:

БНФ:

"tlib " имя_библ ["/C"] ["/E"] команды [, файл_огл]

где имя_библиотеки - имя файла с создаваемой или модифицируемой библиотекой;

команды - последовательность команд модифицирующих библиотеку;

файл_огл - имя файла, в который будет помещено оглавление библиотеки;

/C - ключ, при наличии которого библиотекарь различает прописные и строчные буквы в именах внешних функций;

/E - клич, при наличии которого библиотекарь создает расширенный словарь.

Каждая команда модификации библиотеки имеет следующую форму:

БНФ:

("+" | "-" | "*" | "-+" | "-*") имя_модуля

где имя_модуля - имя скомпилированного модуля без расширения.

Символ "+" добавляет новый модуль в библиотеку, символ "-" удаляет модуль из библиотеки, символ "*" извлекает модуль из библиотеки без его удаления, символы "-+" или "+-" заменяют модуль в библиотеке, символы "-*" или "*-" извлекают и удаляют модуль из библиотеки.

Например, следующий вызов библиотекаря

tlib graph +line +draw +point, graph

создаст в текущем каталоге новую библиотеку с именем graph.lib, в которую будут добавлены модули line.obj, draw.obj и point.obj. Кроме того будет создан файл с оглавлением библиотеки, имеющий имя graph.lst.

  1.  Некоторые библиотечные функции языка Си

В этом разделе дается краткая информация о часто используемых функциях языка СИ.

  1.  Функции консольного ввода/вывода (уникальны для TC)

Рассматриваемые ниже функции уникальны для компиляторов фирмы Borland. Их прототипы находятся в файле <conio.h>.

Вертикальная позиция курсора в текущем текстовом окне (начиная с 1)

int wherey(void);

Горизонтальная текущая позиция курсора в текущем текстовом окне (начиная с 1)

int wherex(void);

Позиционирование курсора в текстовом окне

 void gotoxy(int x, int y);

Установка активного текстового окна

void window(int left, int top, int right, int bottom);

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

Стирание текущего текстового окна

void clrscr(void);

Стереть до конца строки в текущем текстовом окне

void clreol(void);

Удалить строку в текущем текстовом окне

void delline(void);

Вставить пустую строку в текстовое окно в позицию курсора

void insline(void);

Строки ниже позиции курсора смещаются вниз, а последняя строка теряется.

Выбрать новый цвет фона символа в текстовом режиме

void textbackground(int newcolor);

Выбрать новый цвет символа в текстовом режиме

void textcolor(int newcolor);

Установить атрибут символа для текстовых функций вывода

void textattr(int newattr);

Скопировать текст с текстового экрана в память

 int gettext(int left, int top, int right, int bottom,

 void *destin);

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

Скопировать текст из памяти на текстовый экран

 int puttext(int left, int top, int right, int bottom,

 void *source);

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

Копирует текст на экране с одной прямоугольной области в другую

 int movetext(int left, int top, int right, int bottom,

 int destleft, int desttop);

Возвращает не ноль в случае успеха.

Получить символ с консоли без эха

int getch(void);

Получить символ с консоли с эхом на экране

int getche(void);

Вывести символ в текстовое окно

int putch(int ch);

Возвратить символ назад в буфер клавиатуры

int ungetch(int ch);

Возвращает код символа ch в случае успеха или EOF при ошибке.

Прочитать строку с консоли

char *cgets(char *str);

Байт str[0] перед вызовом функции должен содержать максимальную длину строки, допустимую для ввода. После возврата байт str[1] содержит число фактически прочитанных символов. Сама строка начинается с байта str[2]. Функция возвращает адрес прочитанной строки &str[2].

Вывести строку в текстовое окно

 int cputs(const char *str);

Возвращает последний выведенный символ.

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

 int cscanf(char *format [, address, ...]);

Возвращает число успешно прочитанных полей данных. При попытке прочитать символ конца файла возвращает значение EOF.

Выводит данные в текстовое окно с преобразованием по формату

 int cprintf(const char *format[, argumet,...]);

Возвращает число выведенных байт информации. В отличие от функции printf() использует установки цвета. Не производит автоматического добавления символа '\r' к '\n'.

  1.  Функции обработки строк.

Определение длины строки

int strlen(char *str);

Символ '\0' в длину строки не входит. Не путать с длиной массива, в котором размещается строка.

Слияние двух строк

char *strcat(char *dest, char *src);

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

Функция возвращает адрес строки dest.

Слияние строки dest с частью строки src

 char *strneat(char *dest, char src, int n);

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

Функция сравнения двух строк в алфавитном порядке

 int strcmp(char *s1, char *s2);

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

Функция сравнения части строк

int strncmp(char *s1, char *s2, int n);

Работает также как strcmp(), но сравнивает только n символов строк.

Функция копирования строки

char *strcpy(char *dest, char src);

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

Функция копирования части строки

char *strncpy(char *dest, char src, int n);

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

Поиск символа в строке

char *strchr(char *str, char c);

Функция осуществляет поиск символа c с начала строки, на которую указывает str, и возвращает адрес найденного символа. Если символ не найден возвращает NULL.

Поиск символа с конца строки

 char *strrchr(char *s, char c);

Функция осуществляет поиск символа c с конца строки, на которую указывает str, и возвращает адрес найденного символа. Если символ не найден возвращает NULL.

Форматный вывод в строку

int sprintf(char *str, char *format, ...);

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

Форматный ввод из строки

int sscanf(char *str, char *format, ...);

Функция работает подобно scanf(), но ввод вместо клавиатуры осуществляет из буфера, на который указывает str. Функция возвращает число успешно прочитанных полей данных.

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

#include <string.h>

#include <stdio.h>

 void main(void)

{

int a[1000]; char buf[81]; int n, goodIO;

for( goodIO = n = 0; n < 1000; n++ )

{

printf("a[%d]=", n); scanf("%s", buf);

if( strcmp(buf, "end") == 0) { goodIO = 1; break; }

sscanf (buf, "%d", &a[n]);

 }

if( goodIO )

{

/* ... обработка */

}

}

  1.  Функции распознавания вида символа

Строго говоря, это не функции, а макроопределения, описанные в заголовочном файле <ctype.h>:

isalnum(c) истина если c буква или цифра;

isalpha(c) истина если c буква;

isdigit(c) истина если c цифра;

iscntrl(c) истина если c символ удаления или обычный уп-

равляющий символ;

isprint(c) истина если c печатный символ;

islower(c) истина если c буква нижнего регистра;

isupper(c) истина если c буква верхнего регистра;

ispunct(c) истина если c знак пунктуации;

isspace(c) истина если c пробел, знак табуляции, возврат

каретки, символ перевода строки, вертикальной

табуляции, перевода страницы;

isxdigit(c) истина если c шестнадцатеричная цифра;

_toupper(c) преобразует c из диапазона [a-z] к символам

[A-Z];

_tolower(c) преобразует c из диапазона [A-Z] к символам

[a-z];

_toascii(c) преобразует c больший, чем 127 к диапазону

0-127 путем очистки всех битов, кроме 7 млад-

ших.

  1.  Функции преобразования данных

Прототипы функций преобразования данных находятся в файле <stdlib.h>.

Преобразование строки символов в целое число:

 int atoi(const char *s);

long atol(const char *s);

Возвращает преобразованное значение входной строки. Если строка не может быть преобразована возвращает ноль.

Преобразование строки символов в вещественное число:

 double atof(const char *s);

Возвращает преобразованное значение входной строки. Если строка не может быть преобразована возвращает ноль.

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

 long strtol(const char *s, char **endptr, int radix);

unsigned long strtoul(const char *s, char **endptr,

 int radix);

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

Преобразование строки символов в вещественное число:

 double strtod(const char *s, char **endptr);

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

Преобразование целого числа в строку символов с указанием системы счисления:

 char *ltoa(long value, char *string, int radix);

char *itoa(int value, char *string, int radix);

char *ultoa(unsigned long value, char *string,

 int radix);

Возвращает указатель на выходную строку.

Преобразование вещественного числа в строку символов:

 char *ectv(double value, int ndig, int *dec, int *sign);

char *fctv(double value, int ndig, int *dec, int *sign);

Функции возвращают указатель на статический буфер памяти с выходной строкой, содержащей только цифры числа. Буфер обновляется при каждом вызове функции. Для функции ectv() ndig является числом цифр в выходной строке, для fctv() - числом цифр в выходной строке после десятичной точки. Параметр dec показывает положение десятичной точки в выходной строке, которая явно не присутствует. Параметр sign принимает отличное от нуля значение для отрицательных чисел.

Преобразование вещественного числа в строку:

 char *gctv(double value, int ndec, char *buf);

Функции возвращают указатель на буфер buf памяти с выходной строкой, содержащей готовое к печати символьное представление числа из ndec цифр в формате F Фортрана, если возможно. В противном случае число будет представлено в формате e функции printf().

  1.  Структуры языка C.

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

Отличие от массивов - элементы структуры разного типа.

Необходимость: часто реальный объект характеризуется величинами разного типа.

Пример: товар на складе

название char name[21];

цена float price;

 количество int number;

Все три переменных неразрывно связаны с каким-то товаром.

  1.  Описание структуры

1 способ

struct { char name[21];

float price;

int number;} goods;

Выделяется 27 байт для переменной goods;

2 способ

struct _GOODS { char name[21];

 float price;

int number;};

Оператор не выделяет память, а устанавливает тип структуры. Для выделения памяти надо выдать оператор:

struct _GOODS goods1, goods2;

Выделяет память для goods1 и goods2, каждой по 27 байт.

Можно смепшать два способа:

struct _GOODS { char name[21];

 float price;

int number;} goods;

Устанавливает тип структуры и выделяет память для goods.

3 способ. Используется оператор описания типа typedef:

typedef double real;

обычное описание

Если при описании имени стоит слово typedef, то описание не вы-

деляет память, а создает новый тип данных - real, который можно

применять также как и любое другое описание типа:

real a, b;

Еще пример:

 typedef char string[40];

новый тип string

 string a, b, c; - описание трех переменных, каждая из

которых является массивом из 40 символов.

В случае структуры имеем:

typedef struct { char name[21];

 float price;

int number;} GOODS;

Описание типа

GOODS goods1, goods2; - выделение памяти для переменных

goods1 и goods2.

  1.  Трактовка имени структуры.

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

goods2 = goods1;

При этом вся область памяти goods1 копируется в область памяти goods2.

  1.  Доступ к элементу структуры.

Для этого используется операция ".".

goods1.name - образовалось составное имя. Тип составного имени такой же как тип соответствующего элемента структуры.

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

 goods2.price = 20*goods1.price;

scanf("%s", goods1.name);

 goods1.name[3];

Из структур можно составить массив:

GOODS ab[50];

Тогда ab - адрес массива;

ab[2] - значение структуры;

ab[2].price - значение элемента структуры.

Структура может входить в другую структуру:

 typedef struct { GOODS goods; int fl;} GF;

GF a - описание;

a.good.name

Никаких ограничений на уровень вложенности структур нет.

  1.  Инициализация структур.

Статические структуры могут быть проинициализированы подобно массивам:

static GOODS a = { "Телепвизор", 14000.0, 20};

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

  1.  Структуры и функции.

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

Пример:

typedef struct { double r, f;} POLAR;

typedef struct { double x, y;} DECART;

DECART ptod(POLAR pcoord)

{

DECART dcoord;

dcoord.x = pcoord.r*cos(pcoord.f);

dcoord.y = pcoord.r*sin(pcoord.f);

return dcoord;

}

void main(void)

{

DECART a; POLAR b = { 15.2, 0.18};

a = ptod(b);

 .

.

.

}

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

Значиительно эффективнее передавать адреса параметров:

 void prot (DECART*dc, POLAR*pc)

{

(*dc).x = (*pc).r*cos((*pc).f);

(*dc).y = (*pc).r*cos((*pc).f);

 }

(*dc) в скобках потому, что "." имеет более высший приоритет. Головная программа при этом выглядит так:

 void main(void)

{

DECART a; POLAR b = { 15.2, 0.18};

 ptod(&a, &b);

.

.

.

}

Запись вида (*dc).x громоздка и плохо понятна. Поэтому разработчики языка C предусмотрели более понятную эквивалентную запись:

(*dc).x эквивалентно dc->x.

Используя ее:

void ptod(DECART *dc, POLAR *pc)

{

dc->x = pc->r*cos(pc->f);

dc->y = pc->r*sin(pc->f);

 }

  1.  Поля бит в структурах.

Для целых чисел допускается использовать область памяти меньше байта:

 struct {

int c1:4; -8<c1<7

int c2:12 -2 <c2<2 -1

 } ab;

ab.c1 будет преобразовано в целый тип, затем будет использоваться. На преобразование тратится время и память. Рекомендуется использовать для беззнаковых типов данных и в крайних случаях.

/* Пример использования структур */

#include <stdio.h>

 #include <string.h>

#include <math.h>

typedef struct {

char name[21];

int number;

float price;

} GOODS;

void in_goods ( GOODS gs[], int *n );

int in_goods1 ( GOODS *g );

void out_goods ( GOODS gs[], int n );

void sort_goods ( GOODS gs[], int n);

void main( void )

{

int n; GOODS goods[100];

in_goods ( goods, &n );

sort_goods ( goods, n );

out_goods ( goods, n );

{ float f=0; sin(f); }

}

void in_goods( GOODS gs[], int *n)

 {

printf("Введите характеристики товаров в виде:\n" \

"наименование количество цена\n" \

"-----окончание ввода \"end\"-------\n");

 *n=0;

while( in_goods1(&gs[*n]) ) (*n)++;

}

int in_goods1( GOODS *g )

{

scanf( "%s", g->name );

if ( strcmp(g->name, "end")==0 ) return 0;

scanf( "%d%f", &g->number, &g->price );

return 1;

}

void out_goods( GOODS gs[], int n )

{

int i;

printf("*----------------------------------*\n");

printf("| Наименование | Кол-во | Цена |\n");

printf("|---------------|--------|--------|\n");

for( i=0; i<n; i++)

printf( "| %20s | %6d | %10.2f |\n",

gs[i].name, gs[i].number, gs[i].price );

printf("*-------------------------------*\n");

}

void sort_goods( GOODS gs[], int n )

{

int i, j, GOODS r;

for (i=0; i<n-1; i++)

for(j=i+1; j<n; j++)

if( gs[i].price )

{ r=gs[j]; gs[j]=gs[i]; gs[i]=r; }

 }

  1.  Объединения.

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

 union {int a; long b;} pr;

Для переменной pr выделяется память, достаточная для хранения самого длинного элемента объединения, т. е. в нашем примере - 4 байта.

Если использовать pr.a, то выделенная память будет использоваться как int, в случае pr.b как long. Однако, участок памяти один и тот же. Поэтому одновременно существовать pr.a и pr.b не могут. Ответственность за некорректное использование памяти лежит на программисте. Объединения используются для экономии памяти:

 union {int bc[100]; double kk[20];} cc;

На все отводится 200 байт.

int bc[100] 200 байт

______________________________

|----------------------------|

double kk[20] (160 байт)

Одновременно работать с массивом cc.bc и cc.kk нельзя.

Объединения нельзя инициализировать.

  1.  Дополнительные сведения о препроцессоре языка C.

Препроцессор обрабатывает текст программы перед компиляцией.

  1.  Включение файлов (рассмотрено ранее):

#include <conio.h> - файл из специального каталога;

#include "d:\\user\\ff.h" - файл ищется по правилам MS DOS.

  1.  Текстовые подстановки (рассматривалось ранее):

#define N 21

#define ABC (a*b*c+\

 d*sin(x))

\ - переход на следующую строку.

  1.  Создание макросов.

#define SQR(x) ((x)*(x))

В результирующую строку подставляется фактическая строка x. Например, если в тексте программы встречается SQR(y), то после макрораскрутки получим ((y)*(y)). Скобки нужны для того, чтобы не получилось недоразумений, например

#define SQR(x) x*x

SQR(y+2); превратится в

y+2*y+2;

Конечно, это не то, что хотелось.

  1.  Отмена ранее созданного имени:

#undef SQR.

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

#if константное выражение

.

. строки программы

.

#else

.

. строки программы

.

#endif

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

Пример:

#define DEBUG 1

.

.

.

#if DEBUG

printf("%d", x);

 #endif

Можно проверить наличие или отсутствие какого-либо имени:

#ifdef имя (если имя определено, то истина)

.

.

.

#else

.

.

.

#endif

#ifndef имя (если имя не определено)

.

.

.

#else

.

.

.

#endif

Это свойство широко используется в библиотечных включаемых файлах, чтобы избежать противоречия. Например, в conio.h имеются следующие строки:

#ifndef COLORS

enum COLORS {BLACK, ...};

#endif

Условное выражение.

Эта конструкция языка C в некоторых случаях позволяет заменить оператор if и сократить запись программы.

БНФ:

условное выр = выр0 "?" выр1 ":" выр2

Значение условного выражения равно выр1, если выр0 не равно 0 и выр2 впротивном случае.

Пример:

a = b>c ? b:c; тоже самое, что

 if (b>c) a=b; else a = c;

С помощью условного выражения можно, например, определить макросы max и min:

 #define max(x,y) ((x)>(y) ? (x):(y))

#define min(x,y) ((x)<(y) ? (x):(y))

При этом макросы max и min будут работать для любых типов арифметических данных.

Например:

a = max(b,c);

  1.  Приоритеты и направления операций.

----------------------------------------------------------

| | |

|--------------------------------------------------|-----|

| () вызов функции | |

| [] выделение элемента массива | |

| . выделение элементов структуры или объеди- | -> |

| нения | |

| -> выделения элементов структуры или объеди- | |

| нения | |

|--------------------------------------------------|-----|

| ! логическое отрицание | |

| ~ побитовое отрицание | |

| - изменение знака | |

| ++ увеличение на единицу | |

| -- уменьшение на единицу | <- |

| & определение адреса | |

| * обращение по адресу | |

| (тип) преобразование типа | |

| sizeof определение размера в байтах | |

|--------------------------------------------------|-----|

| * умножение | |

| / деление | -> |

| % остаток от деления | |

|--------------------------------------------------|-----|

| + сложение | |

| - вычитание | -> |

|--------------------------------------------------|-----|

| << сдвиг влево | |

| >> сдвиг вправо | -> |

|--------------------------------------------------|-----|

| < меньше | |

| <= меньше или равно | |

| > больше | |

| >= больше или равно | |

|--------------------------------------------------|-----|

| == равно | |

| != не равно | -> |

|--------------------------------------------------|-----|

| & побитовое и | -> |

|--------------------------------------------------|-----|

| ^ побитовое исключающее или | -> |

|--------------------------------------------------|-----|

| | побитовое или | -> |

|--------------------------------------------------|-----|

| && логическое и | -> |

|--------------------------------------------------|-----|

| || логическое или | -> |

|--------------------------------------------------|-----|

| ? : условная операция | -> |

|--------------------------------------------------|-----|

| = присваивание | |

| *= /= %= += -= | <- |

| <<= >>= &= ^= |= | |

|--------------------------------------------------|-----|

| , операция запятая | -> |

|__________________________________________________|_____|

Динамические данные.

  1.  Линейные списки.

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

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

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

......... ......... ......... ......... ...........

: : : : : : : : : : : : : :NULL:

......... ......... ......... ......... ...........

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

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

 typedef struct _GOODS{

char name[21];

int number;

float price;

struct _GOODS *next;

 } GOODS;

Очевидно, программа должна иметь переменную - указатель на первый элемент списка. Последний элемент списка отличается тем, что в поле next имеет константу NULL - то есть пустой указатель. Тогда список может быть представлен так:

......... ......... ......... ...........

TOP : : : : : : : : : : :NULL:

......... ......... ......... ...........

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

 GOODS *new_GOODS ( void)

{

GOODS *p;

p = (GOODS* ) malloc( sizeof(GOODS) );

 if (p==NULL) {printf("Недостаточно памяти\n");

 exit(1);}

return p;

}

GOODS *in_goods( void )

 {

GOODS *top, *q;

printf("Введите характеристики товаров в виде:\n" \

"наименование количество цена\n" \

"-------окончание ввода \"end\"-------\n");

 top = NULL;

while (1)

{

q = new_GOODS(); if( !in_goods1(q) ) { free(q);

return top; }

q->next = top; top = q;

 }

}

Обращение к этой программе будет выглядеть так:

top = in_goods();

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

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

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

 void out_goods( GOODS *top)

{

printf("*--------------------------------------*\n");

printf("| Наименование | Кол-во | Цена |\n");

printf("|--------------------|--------|--------|\n");

while( top != NULL )

{

printf( "| %20s | %10.2f |\n",

top->name, top->number, top->price );

top = top->next;

}

printf("*--------------------------------------*\n");

 }

Головная функция должна выглядеть следующим образом:

 void main( void )

{

GOODS *top;

top = in_goods();

out_goods ( top );

 }

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

Рассмотрим вспомогательную задачу: необходимо вставить запись, на которую указывает указатель g в список за записью, на которую указывает указатель p.

 g .........

: :. :

......|..

2 ^ | 1

| V

TOP ......... ......... ......... ......... ..........

: : : : : p : : : : : : : : :NULL:

......... ......... ......... 2 ......... ..........

p

g->next = p->next; 1

 p->next = g; 2

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

 GOODS *in_goods( void )

{

GOODS *top, *q, *p;

 printf("Введите характеристики товаров в виде:\n" \

"наименование количество цена\n" \

"-------окончание ввода \"end\"-------\n");

/* Получение списка из двух элементов */

 top = NULL;

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

q->next = NULL; top = q;

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

if(q->price < top->price ) { q->next = top; top = q; }

else { q->next = NULL; top->next = q; }

 /* Получение списка из остальных элементов */

 while( 1 )

{

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

if(q->price < top->price )

{ q->next = top; top = q; }

else

{

p = top;

while(p->next && (q->price > p->next->price)) p=p->next;

q->next = p->next; p->next = q;

 }

}

}

/* Пример использования списка # 1 */

#include <stdio.h>

 #include <stdlib.h>

#include <string.h>

#include <alloc.h>

#include <math.h>

typedef struct _GOODS{

char name[21];

int number;

float price;

struct _GOODS *next;

} GOODS;

GOODS in_goods ( void );

GOODS new_goods ( void );

int in_goods1 ( GOODS *g );

void out_goods (GOODS *top );

void main( void )

{

GOODS *top;

top = in_goods();

out_goods ( top );

{ float f=0; sin(f); }

}

GOODS *new_GOODS ( void )

{

GOODS *p;

p = (GOODS*) malloc( sizeof(GOODS) );

 if (p==NULL) {printf("Недостаточно памяти\n");

 exit(1);}

return p;

}

GOODS *in_goods( void )

 {

GOODS *top, *q;

printf("Введите характеристики товаров в виде:\n" \

"наименование количество цена\n" \

"-------окончание ввода \"end\"-------\n");

 top = NULL;

while (1)

{

q = new_GOODS(); if( !in_goods1(q) ) { free(q);

return top; }

q->next = top; top = q;

}

}

int in_goods1( GOODS *g )

{

scanf( "%s", q->name );

if ( strcmp(g->name, "end")== ) return 0;

scanf( "%d%f", &g->number, &g->price );

return 1;

}

void out_goods( GOODS *top)

{

printf("*--------------------------------------*\n");

printf("| Наименование | Кол-во | Цена |\n");

printf("|--------------------|--------|--------|\n");

while( top != NULL )

{

printf( "| %20s | %10.2f |\n",

top->name, top->number, top->price );

top = top->next;

}

printf("*--------------------------------------*\n");

 }

/* Пример использования списка с сортировкой */

 #include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <alloc.h>

#include <math.h>

typedef struct _GOODS{

char name[21];

int number;

float price;

struct _GOODS *next;

} GOODS;

GOODS in_goods ( void );

GOODS new_goods ( void );

int in_goods1 ( GOODS *g );

void out_goods (GOODS *top );

void main( void )

{

GOODS *top;

top = in_goods();

out_goods ( top );

{ float f=0; sin(f); }

}

GOODS *new_GOODS ( void )

{

GOODS *p;

p = (GOODS*) malloc( sizeof(GOODS) );

 if (p==NULL) {printf("Недостаточно памяти\n");

 exit(1);}

return p;

}

GOODS *in_goods( void )

 {

GOODS *top, *q, *p;

printf("Введите характеристики товаров в виде:\n" \

"наименование количество цена\n" \

"-------окончание ввода \"end\"-------\n");

/* Получение списка из двух элементов */

 top = NULL;

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

q->next = NULL; top = q;

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

if(q->price < top->price ) { q->next = top; top = q; }

else { q->next = NULL; top->next = q; }

 /* Получение списка из остальных элементов */

 while( 1 )

{

q=new_GOODS(); if(!in_goods1(q)) {free(q); return top;}

if(q->price < top->price )

{ q->next = top; top = q; }

else

{

p = top;

while(p->next && (q->price > p->next->price)) p=p->next;

q->next = p->next; p->next = q;

}

}

}

int in_goods1( GOODS *g )

{

scanf( "%s", q->name );

if ( strcmp(g->name, "end")== ) return 0;

scanf( "%d%f", &g->number, &g->price );

return 1;

}

void out_goods( GOODS *top)

{

printf("*--------------------------------------*\n");

printf("| Наименование | Кол-во | Цена |\n");

printf("|--------------------|--------|--------|\n");

while( top != NULL )

{

printf( "| %20s | %10.2f |\n",

top->name, top->number, top->price );

top = top->next;

}

printf("*--------------------------------------*\n");

 }

  1.  Организация данных в виде стека.

Понятие стека ("магазина"): первый пришел, последний ушел.

LIFO (LAST IN FIRST OUT)

Описание стека как списка:

typedef struct _LIST {

info_t info; /* тип данных для информации */

struct _LIST *next;

} LIST;

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

LIST *head = NULL; /* голова списка */

Действия со стеком определяется несколькими функциями:

  1.  Помещение элемента в стек (в голову списка)

 void add_head (LIST *head, info_t a)

{

LIST *t;

if (t=(LIST*) malloc (sizeof (LIST)))

{

t->info = a; /* 1 */

t->next = (*head); /* 2 */

(*head) = t; /* 3 */

}

}

t .......... 2

: 1 a : :

..........

......... ......... ..........

3 : : : : : : : :NULL:

 ......... ......... ..........

head 3

  1.  Извлечение из стека (из головы списка)

 info_t get_head (LIST *head)

{

LIST *t; info_t a;

if ( *head)

{

a = (*head)->info; /* 1 */

t = (*head); /* 2 */

(*head) = (*head)->next; /* 3 */

free (t);

}

return a;

}

t 2

 .......... ......... ..........

: 1 a : : : : : : :NULL:

.......... ......... ..........

3

head 3

  1.  Организация данных в виде очереди.

Понятие очереди: первый пришел, первый ушел.

FIFO (FIRST IN FIRST OUT).

Описание очереди: такое же, что и стека, но надо хранить и начало и хвост очереди.

.......... ......... ..........

head : : : : : : : :NULL:

.......... ......... ..........

tail

Тогда в вызывающей программе очередь описывается так:

 LIST *head = NULL, *tail = NULL;

Помещение элемента в очередь (в хвост списка):

 1-ый элемент: head

3 ..........

: :NULL:

5 ..........

tail t

 Остальные: ......... ......... .........

: : : : : : : : :

......... ......... .........

head 5 4

tail ...........

5 :1 a:NULL:

...........

t 2

void add_tail (LIST*head, LIST*tail, info_t a)

{

LIST*t;

if (t = (LIST*) malloc (sizeof (LIST)))

{

t->info = a; /* 1 */