Вызов функции
Вызов функции влечет выполнение содержащихся в теле функции операторов.
Функции должны быть объявлены перед их вызовом. В Стандарт ANSI языка Си языка Си объявления функции (называемом также объявлением прототипа функции) используется для контроля типов. Компилятор выполнит проверки того, что количество и типы параметров, переданные функции, правильны.
Если объявление типа функции опущено, компилятор будет использовать правило умолчания K&R и установит для нее тип int. Для функций стандарта K&R компилятор не выполняет контроля типов.
Пример 1
#include <stdio.h>
int main()
{
  void intro (void);     /* объявление прототипа функции*/

  intro();              /*вызов функции*/
...

}
void intro (void)        /*определение прототипа функции*/
{
  printf("Работает вызываемая фукция intro()");

}
В данном примере при вызове функции intro() функция main() является вызывающей функцией. При вызове функции printf() вызывающей функцией является intro(). Ключевое слово void в объявлении прототипа функции intro() показывает, что функция intro() не имеет параметров и не возвращает значение.


Передача адреса в функцию
Если в функцию передается адрес переменной, то функция может изменять значение этой переменной. Если функция предполагает, что значением ее параметра будет адрес, этот параметр должен быть объявлен в функции как указатель.
Для передачи функции адреса переменной используется операция получения адреса &:
&операнд
Операция получения адреса — это унарная операция, результат которой — адрес объекта, определяемого операндом. Приоритет операции определяется по таблице приоритетов операций языка Си. В качестве операнда может использоваться именующее выражение.
Для объявления формального параметра функции как адреса используется операция косвенного доступа *.
Пример 1
#include <stdio.h>
int room1 = 19, room2 = 24;
int main ()
{
  int num1 = 10, num2 = 5;
  void swap (int *, int *);

  printf ("%d %d\n", num1, num2);
  swap(&num1, &num2);
  printf ("%d %d\n", num1, num2);
  printf ("%d %d\n", room1, room2);
  swap(&room1, &room2);
  printf ("%d %d\n", room1, room2);
}
void swap (int *one, int *two)
{
  int temp;
  temp =*one;
  *one = *two;
  *two = temp;
}
вывод:
5
5
10
При вызове функции swap(&room1, &room2) фактическими параметрами будут значения адресов переменных room1 и room2, которые будут ставиться в соответствие формальным параметрам one и two. Использование операции косвенного доступа *one и *two в функции swap()делает доступными сами переменные room1 и room2.
Указатели на функцию
Указатель может быть использован для ссылки на функцию, точно также как и для ссылки на переменную.
Пример 2
int main ()
{
int (*funptr)(int, int);     /*объявляется указатель на функцию*/
int max(int, int);           /*объявляется функция,  на которую будет ссылаться указатель*/
...
funptr = max;                /*инициализация указателя*/
c=(*funptr)(a,b);            /* c=maxfun(a,b);*/
...
int maxfun(inti, int j)
{
return (i>j)?i:j;
}
Пример 3
Пусть функции, выполняющие различные запросы пользователя, определены в другом месте.
int main()
{
int append(void), find(void), list(void);
int remove(void), replace(void), substitute(void);

static int (*command[])(void) = {
     append,
     find,
     list,
     remove,
     replace,
     substitute};
int i, (*fp)(void);
char string[80];
...
scanf("%s", string);
...
i = is_keyword(string);
if (i==-1)
   exit(0);
fp=comand[i];      /*инициализирует указатель на функцию*/
(*fp)();           /*вызов функции(*command[i])()*/
...
}
Чтобы использовать имя функции в выражении, функция должна быть явно объявлена. В функции main() объявляются имена функций. Затем инициализируется массив указателей command[] адресами функций. Объявляется указатель на функцию fp. Читается запрос пользователя и передается функции is_keyword(), возвращающей индекс соответствующего элемента массива адресов функций. Этот индекс используется для инициализации указателя fp, определяющего требуемую функцию..


Передача в функцию адреса массива
Язык Си не допускает копирования всего массива для передачи его в функцию. Тем не менее, функции можно передавать элемент массива или начальный адрес массива.
Адрес массива — адрес первого элемента массива.
Для того, чтобы функция имела доступ к массиву, ей достаточно передать адрес массива.
Пример 1
/* Копирует массив с использованием функции */
int main( )
{
void stringcopy (char [], char []);
char current[30], target[30];
. . .
stringcopy(current, target);
. . .
}
/* Копирует str1 в str2 */
void stringcopy(char str1[], char str2[])
/* str1 = 600 str2 = 500 */
{
int i;
for ( i = 0; str2[i]!= '\0'; i++)
str1[i] = str2[2]; /* адрес [смещение] */
str1[i] = '\0';
}
Выражение типа str2[i] интерпретируется так, что означает адрес[смещение].
Если в вызове функции в качестве фактического параметра используется имя массива без скобок, то значением этого параметра будет начальный адрес массива. Если в примере начальные адреса массивов current и target равны соответственно 600 и 500, то в момент вызова функции stringcopy() значения формальных параметров str1 и str2 будут равны соответственно 600 и 500. Зная эти адреса, функция stringcopy() может обращаться к элементам массива, используя операцию доступа к элементу массива []. Выражение current[3], так и str2[3] будут интерпретированы как 500[3]. Доступ выполняется к значению, запомненному по адресу:

В примере 2 показаны два способа объявления параметров функции при передаче адреса массива:
Пример 2
int main()
{
int str1[20], str2[20];
void array_cpy(char [], char []);
void ptr_cpy(char *, char *);
...
array_cpy(str1, str2);
/*ptr_cpy(str1, str2);*/
...
}
void array_cpy(char one[], char two[])
{
int i;
for (i = 0; two[i] !='\0'; i++)
one[i] = two [i];
one[i] = '\0';
}
В функции array_cpy() формальные параметры объявляются с использованием операции доступа к элементу массива []. Эта же операция применяется и в теле функции.
void ptr_cpy(char *one, char *two)
{
for ( ; *two !='\0'; one++, two++)
*one = *two ;
*one = '\0';
}
В функции ptr_cpy() формальные параметра объявляется как указатели. В теле функции используется операция косвенного доступа *. Такая совместимость объявлений и операций настоятельно рекомендуется по стилевым причинам, но она не является необходимой. Тела двух функций можно без ущерба поменять местами. Важно помнить, что доступ к данным, хранящимся по адресу, обеспечивают обе операции * и []. Операцию * можно применять даже к имени массива. В действительности, выражение array_name[i]преобразуется компилятором в выражение *(array_name + i).


Адресная арифметика
Адресная арифметика языка Си включает в себя следующие арифметические операции над указателями.
  • Указателю можно присвоить значение указателя того же типа или адрес объекта того же типа.
  • Целое число можно сложить с указателем или вычесть из него.
  • Указатель можно сравнивать с другим указателем.
  • Указатель можно вычесть из другого указателя.
  • Указателю можно присвоить значение нулевого указателя — константы NULL.
  • Указатель можно сравнивать с нулевым указателем.
Другие арифметические операции над указателями недопустимы.
Константа NULL объявляется в файле <stdio.h>. Нулевой указатель часто используется для организации сложных конфигураций данных, например, связанных списков и деревьев. Другой пример использования нулевого указателя: результат выполнения возвращающих указатель функций, сообщающий о неудаче.
Чтение или запись по адресу 0 в языке Си не разрешены и могут быть причиной аварийного завершения программы.
При использовании адресной арифметики важно использовать указатели с правильным типом. Если указатель будет использован для хранения адресов целых типа int, он должен быть объявлен как указатель на int. Если указатель будет содержать адреса объектов типа double, он должен быть объявлен как указатель на double, и т.д.
В языке Си последовательный доступ к данным через указатели осуществляется очень просто путем использования возможностей, предоставляемых адресной арифметикой. Когда целое число прибавляется к указателю (или вычитается из него), оно предварительно умножается на размер объекта (в байтах), на который ссылается указатель.
Пример 1
int a, *p_int;
...
char ch, *p_char;
p_int = &a;
p_char = &ch;
p_int++;
p_char++;
Пусть переменная a имеет адрес 500, а переменная ch — 520. Тогда после инициализации значением указателя p_int будет 500, а p_char соответственно 520. Операция инкрементации применительно к указателям увеличит p_int на 4, а p_char на 1.
Адресную арифметику особенно удобно использовать при обработке массивов.
Типичные приемы работы с указателями
В примерах приводятся три (из множества) возможных варианта функции strlen(), которая возвращает количество символов в строке вплоть до признака конца строки '\0' , но не включая его.
Пример 2
int strlen (register const char *s)
{
register char *p;
p = s;
while (*p != '\0')
p++;
return p-s;
}
Этот вариант функции наиболее нагляден.
Пример 3
int strlen (register const char *s)
{
register char *p;
p = s;
while(*p)
   p++;
return p-s;
}
Данный вариант равносилен предыдущему, т.к. выражение *p будет истинно, пока его значение не нулевой символ (не символ '\0').
Пример 4
int strlen (register const char *s)
{
register char *p;
p = s;
while (*p++)
;
return (p-s)-1;
}
В данном варианте используется выражение, применяемое особенно часто: *p++. Так как унарные операции правоассоциативные (связываются с операндами справа налево), это выражение интерпретируется как *(p++). Это значит, что будет увеличен указатель p, но не значение выражения *p. Поскольку операция инкрементации ++ употребляется в постфиксной форме (после операнда), указатель p не увеличивается, пока его значение не использовано. Общее действие состоит в использовании ссылки *p и последующем увеличении указателя p. В операторе возврата из (p-s) вычитается 1; это делается потому, что после инкрементации p указывает на следующий за нулевым символом байт.


Алфавит языка программирования Си
Алфавит языка программирования — множество символов, допустимых при построении конструкций программы.
Алфавит языка Си включает:
  • строчные, прописные буквы латинского алфавита (a...z, A...Z) и символ подчеркивания _ (строчные и прописные буквы различаются)
  • цифры (0...9)
  • специальные знаки: . , ; + - * / = < > % & ! ( ) { } ^ | ? : [ ] ^ ~' " # \
  • служебные слова: char, short, int, long, float, double, enum, struct, union, signed, unsigned, void, auto, extern, register, static, break, case, continue, default, do, else, for, goto, if, return, switch, while, sizeof, typedef.
Служебные слова, которые также называют ключевыми словами, — это словосочетания, считающиеся единым целым и зарезервированные для определенных целей. Их нельзя использовать в программе в другом качестве.
Из символов алфавита в соответствии с правилами синтаксиса строят различные конструкции. Простейшей из них является конструкция идентификатор.
Идентификатор — это последовательность букв латинского алфавита (включая символ подчеркивания) и цифр, которая обязательно начинается с буквы. Идентификаторы используются как имена переменных, функций и типов данных. Не рекомендуется начинать идентификатор с символа подчеркивания, это избавит вас от опасности перепутать его с метками, используемыми некоторыми ассемблерами. Идентификатор не должен совпадать ни с одним ключевым словом языка Си.
Пример 1
NAME1
name1
Total_5
My_name_family
Идентификатор может быть произвольной длины, но в некоторых ЭВМ не все символы учитываются компилятором и загрузчиком.
Описательные (наглядные) имена переменных могут значительно улучшить понятность вашей программы и облегчить процесс отладки и модификации программы. Короткие имена переменных не ускорят выполнение программы.


Аргументы функции
Фактический параметр — выражение, которое указывается в круглых скобках в вызове функции. Часто фактические параметры функции называют аргументами.
В вызове функции должно быть указано правильное количество, порядок и типы параметров.
Компилятор стандарта ANSI языка Си проверяет согласованность параметров вызова функции ее аргументам в следующих случаях:
  • если функция объявлена в программе перед ее вызовом;
  • если функция определена в исходном файле перед вызовом.
Компилятор ANSI Си проверяет правильность вызова функции: правильно ли указано количество параметров и их типы. Если необходимо, компилятор выполнит автоматическое преобразование параметров вызова функции к соответствующему типу. Если преобразование не имеет смысла, во время компиляции выдается сообщение об ошибке.
Все аргументы функции “вызываются по значению”. В функцию передается значение аргумента или фактического параметра.
Пример 1
#include <stdio.h>
int main()
{
void print_growth (float, float);          /*объявление прототипа функции */
float  amount = 250.,                      /*начальный вклад */
       interest = .075;                    /* процент 7.5% */

print_growth (amount, interest);           /*вызов функции */
printf ("main: amount is %.2f \n", amount);
}

void print_growth (float val, float rate)  /*определение прототипа функции */
 /* val = 250., rate=.075 */
{
   val = (1+rate)* val;
   printf ("Value after 1 year: %.2f\n", val);
 }
Результат выполнения программы:
Value after 1 year: 268.75
main: amount is 250.00
При вызове в функцию print_growth() передаются значения фактических параметров: в данном случае значения переменных amount и interest. Эти значения присваиваются новым переменным (формальным параметрам) val и rate, имеющим свои собственные адреса в памяти. Значение переменных amount и interest не изменяются вызовом функции print_growth().


Арифметические операции
Арифметические операцииязык Си — операции, реализующие арифметические действия.
  • Аддитивные операции
    • сложение (+)
    • вычитание (-)
  • Мультипликативные операции
    • умножение (*)
    • деление (/)
    • получение остатка от деления (%)
  • Изменение знака(унарный минус) (-)
Пример 1
x+y
num-20
200*val
10/3
10.0/3.0
clock%60
-(prev+current)
Приоритеты арифметических операций определяются таблицей приоритетов операций языка Си.
При делении целых дробная часть результата отбрасывается (10/3 равно 3). Стандарт ANSI языка Си требует, чтобы округление производилось в сторону нуля. Если один или оба операнда – числа с плавающей точкой, то округления не происходит. Результат — вещественное число (10.0/3.0 равно 3.3333…).
Операция получение остатка от деления (%) выполняет деление и выдает в качестве результата остаток от деления.
10%3 (произносится "10 по модулю 3") равно 1. Операнды не могут быть вещественными числами.
Операцию изменения знака иногда называют унарным минусом, так как она имеет только один операнд.


Двоичная арифметика
Двоичная арифметика включает в себя арифметические действия над двоичными числами.
Правила выполнения арифметических действий над двоичными числами задаются таблицами двоичного сложения, вычитания и умножения:
Таблица 1
Слагаемое 1 | Слагаемое 2 | Сумма
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0 (единица переноса в старший разряд)

Таблица 2
Уменьшаемое | Вычитаемое | Разность
0 | 0 | 0
0 | 1 | 1(заем единицы из старшего разряда)
1 | 0 | 1
1 | 1 | 0

Таблица 3
Сомножитель 1 | Сомножитель 2 | Произведение
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

Правила арифметики во всех позиционных системах счисления аналогичны. Поэтому сложение двух чисел в двоичной системе можно выполнять столбиком, начиная с младшего разряда, подобно тому, как это делается в десятичной системе. В каждом разряде в соответствии с таблицей двоичного сложения производится сложение двух цифр слагаемых или этих двух цифр и 1, если имеется перенос из соседнего младшего разряда. В результате получается цифра соответствующего разряда суммы и, возможно, также 1 переноса в старший разряд.
Рис. 1. Сложение
Вычитание чисел в двоичной системе выполняется подобно вычитанию в десятичной системе. При вычитании в данном разряде при необходимости занимается 1 из следующего старшего разряда. Эта занимаемая 1 равна двум 1 данного разряда. Занимание производится каждый раз, когда цифра в разряде вычитаемого больше цифры в том же разряде уменьшаемого.
Рис. 2. Вычитание
Умножение двоичных многоразрядных чисел производится путем образования частичных произведений и последующего их суммирования. В соответствии с таблицей двоичного умножения каждое частичное произведение равно 0, если в соответствующем разряде множителя стоит 0, или равно множимому, сдвинутому на соответствующее число разрядов влево, если в разряде множителя стоит 1. Таким образом, операция умножения многоразрядных двоичных чисел сводится к операциям сдвига и сложения. Положение запятой определяется так же, как при умножении десятичных чисел.
Рис. 3. Умножение
Деление чисел в двоичной системе производится аналогично делению десятичных чисел. Достаточно рассмотреть деление двух целых двоичных чисел, так как делимое и делитель всегда могут быть приведены к такому виду путем переноса запятой в делителе и делимом на одинаковое число разрядов и дописывания 0 в недостающие справа разряды.
Рис. 4. Деление
Благодаря простоте правил двоичного сложения, вычитания и умножения применение в ЭВМ двоичной системы счисления позволяет упростить схемы устройств, выполняющих арифметические операции.


Атрибут const
Для указания того, что значение переменной не будет изменено в программе, может быть использован атрибут const. В этом случае переменная размещается в памяти, доступной только для чтения.
Использование атрибута const в прототипе определения функции указывает, что параметр не будет изменяться функцией.
Если вы попробуете изменить значение переменной const, компилятор выдаст сообщение об ошибке.
Пример 1
int main()
{
void stringcopy(char [], const char []);
. . .
}
void stringcopy(char str1[], const char str2[])
{
. . .
}
float area_circle(double radius)
{
 const double pi = 3.1415;
. . .
}


Автоматический класс памяти
Автоматическая переменная — переменная, которая объявляется внутри блока.
Областью действия автоматической переменной является блок, в котором она объявлена. Автоматическая переменная создается динамически (автоматически) при входе в блок и прекращает существование при выходе из него.
Область действия и время жизни параметра функции такие же, как и у автоматической переменной.
В программе может быть несколько совпадающих идентификаторов. Тем не менее, две переменные, объявленные в одном и том же блоке, не могут иметь одинаковых имен. Если переменная во внутреннем блоке имеет то же имя, что и объявленная во внешнем блоке переменная, операторы внутри блока ссылаются на переменную, объявленную во внутреннем блоке.
Пример 1
int main()
{
void print_cube(int);
int x;                         /* известно только в main() */

x = getchar();
if ( x == 'c')
   {
    float x;                   /* известно в этом блоке */
    x = .75;                   /* ссылается на float x */
   }
print_cube(100);
}
void print_cube(int x)         /* известно только в print_cube()*/
{
    printf ("%d\n", x*x*x);
}


Программирование на языке Си
Глава 1. Введение
1.1. Стадии создания программного продукта
Тест: Анализ
1.2. Выбор системы счисления для представления числовой информации
1.3. Перевод числовой информации из одной ПСС в другую
Тест: Перевод чисел из десятичной ПСС в двоичную
Тест: Перевод чисел из двоичной ПСС в десятичную
Тест: Перевод чисел из двоичной ПСС в восьмеричную
Тест: Перевод чисел из восьмеричной ПСС в двоичную
Тест: Перевод чисел из двоичной ПСС в шестнадцатеричную
Тест: Перевод чисел из шестнадцатеричной ПСС в двоичную
1.4. Двоичная арифметика
Тест: Двоичная арифметика
1.5. Формы представления числовой информации
Тест: Прямой, обратный, дополнительный коды
1.6. Технология программирования
1.7. Синтаксис и семантика языка программирования
Глава 2. Основные понятия языка программирования Си
2.1. Характерные черты языка программирования Си
2.2. Алфавит языка программирования Си
Тест: Идентификаторы
2.3. Минимальная программа на языке программирования Си
2.4. Выражения
2.5. Операторы
Тест: Операторы 1
Тест: Операторы 2
2.6. Комментарии
2.7. Общая структура программы на языке Си
Глава 3. Основные типы данных и операции языка Си
3.1. Основные типы данных
3.2. Константы
3.3. Атрибут const
3.4. Приоритеты операций
3.5. Арифметические операции
3.6. Операция присваивания
3.7. Операции инкрементации и декрементации
Тест: Операции инкрементации и декрементации
3.8. Операция приведения
3.9. Преобразование данных
3.10. Операции отношений
Тест: Операции отношений
3.11. Логические операции
Тест: Логические операции
3.12. Условная операция
Тест: Условная операция 1
Тест: Условная операция 2
Тест: Приоритеты операций
3.13. Переименование типа
Глава 4. Функции
4.1. Функции
4.2. Определение функции
4.3. Объявление функций
4.4. Вызов функции
4.5. Аргументы функции
4.6. Назначение стека при вызове функции
4.7. Оператор return
4.8. Рекурсивные функции
Тест: Вызовы и аргументы функции
Тест: Определение прототипа функции
Тест: Объявление прототипа функции
Тест: Рекурсивные функции 1
Тест: Рекурсивные функции 2
Глава 5. Классы памяти
5.1. Обзор классов памяти
5.2. Автоматический класс памяти
5.3. Внешний класс памяти
5.4. Статический класс памяти
5.5. Регистровый класс памяти
Тест: Классы памяти
Глава 6. Управляющие операторы
6.1. Управляющий оператор if
6.2. Оператор цикла while
Тест: Оператор цикла while 1
Тест: Оператор цикла while 2
6.3. Оператор цикла do-while
6.4. Оператор цикла for
6.5. Операция продолжения ("запятая")
6.6. Оператор goto
6.7. Оператор switch
6.8. Операторы break и continue
Глава 7. Массивы
7.1. Массивы
7.2. Доступ к элементам массива
7.3. Массивы символов
7.4. Обработка массивов
7.5. Инициализация массива
7.6. Передача в функцию адреса массива
7.7. Передача в функцию многомерного массива
Глава 8. 8. Указатели
8.1. Указатели
8.2. Использование указателей
8.3. Адресная арифметика
8.4. Указатель на void
8.5. Массивы указателей
8.6. Двукратные указатели
8.7. Передача адреса в функцию
Тест: Указатели 1
Тест: Указатели 2
Глава 9. Структуры и объединения
9.1. Структуры
9.2. Доступ к элементам структуры
9.3. Инициализация структуры
9.4. Массивы структур
9.5. Указатели на структуры
9.6. Структура как параметр функции
9.7. Самоссылающиеся структуры
Тест: Структуры 1
Тест: Структуры 2
Тест: Указатели на структуры
9.8. Объединения
Глава 10. Препроцессор
10.1. Шаги компиляции Си
10.2. Препроцессор языка Си
10.3. Поименованные константы
10.4. Макросы
10.5. Организация файлов программы
10.6. Условная компиляция
Глава 11. Доступ к аргументам командной строки
11.1. Доступ к аргументам командной строки
Глава 12. Побитовые операции
12.1. Обзор побитовых операций
Глава 13. Библиотеки
13.1. Библиотеки в ОС UNIX
Раздел 13.2. Библиотека ввода-вывода
13.2.1. Обзор средств ввода-вывода
13.2.2. Структура FILE
13.2.3. Буферизованый ввод-вывод
13.2.4. Функция fopen()
13.2.5. Функция fclose()
13.2.6. Функции чтения-записи
13.2.7. Функции getchar() и putchar()
13.2.8. Функция ungetc()
13.2.9. Функции обработки строк
13.2.10. Обзор функций форматированного ввода-вывода
13.2.11. Функция printf()
Тест: Функция printf()
13.2.12. Функция scanf()
Тест: Функция scanf()
13.2.13. Функции gets() и puts()
13.2.14. Вытеснение буфера
13.2.15. Произвольный доступ
13.2.16. Функции обработки ошибок
Приложение. Некоторые команды ОС Linux
Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си
Лабораторная работа 2. Целые типы данных в языке программирования Си
Лабораторная работа 3. Арифметические операции языка программирования Си
Лабораторная работа 4. Управляющие операторы if и while
Лабораторная работа 5. Управляющий оператор выбора switch языка программирования Си
Лабораторная работа 6. Массивы в языке программирования Си. Оператор цикла for.


Обзор побитовых операций
Побитовые операции — это операции, работающие с отдельными битами операндов. Операнд должен быть целочисленным, т.е. иметь тип long long, long, unsigned, int, shor или char.
Приоритеты побитовых операций приведены в таблице приоритетов операций языка Си.
Поразрядное дополнение (~) – это унарная операция, результатом которой является значение, полученное поразрядным дополнением операнда, т.е. каждый бит со значением 0 заменяется на 1 и наоборот.
Поразрядное умножение И (&) – значением выражения, использующего эту операцию, является поразрядное логические умножение двух операндов.
Исключающее ИЛИ (^) — значением выражения, использующего эту операцию, является поразрядное исключающее ИЛИ ее операндов.
Поразрядное сложение ИЛИ (|) — значением выражения, использующего эту операцию, является поразрядное логические сложение двух операндов.
Знаки этих побитовых операций являются односимвольными. Не путайте их с операциями логического умножения && (И) и логического сложения || (ИЛИ). Значением выражений, использующих логические операции может быть только истина или ложь. Значением побитовых операций обычно является новое битовое значение.
Сдвиг влево (<<) — значением выражения, использующего эту операцию, является битовое представление левого операнда, сдвинутого влево на число битов, определяемое значением правого операнда.
Сдвиг вправо (>>) — значением выражения, использующего эту операцию, является битовое представление левого операнда, сдвинутого вправо на число битов, определяемое значением правого операнда.
Пример 1
int num1, num2;
num1 = 5;  /* 00101 */
num2 = 11; /* 01011 */
Рис. 1. Поразрядное И
Рис. 2. Поразрядное ИЛИ
Рис. 3. Исключающее ИЛИ
Значением выражения, содержащего операцию поразрядного дополнения, является логическое отрицание значения операнда. Значения битов инвертируются: единицы заменяются нулями, нули – единицами.
Пример 2
num1 = 5;    /* 00101 */
num1             00101
~num1            11010
Операции сдвига << и >>
Целочисленное_выражение << Целочисленное_выражение
Целочисленное_выражение >> Целочисленное_выражение
При сдвиге значения операнда влево биты справа заполняются нулями.
При сдвиге вправо возможны следующие ситуации:
  • при сдвиге положительных и беззнаковых значений биты слева заполняются нулями;
  • при сдвиге отрицательных значений результат совпадает с результатом арифметического или логического сдвига в зависимости от аппаратного обеспечения ЭВМ: логический сдвиг вправо заполняет освобождающиеся слева биты нулями; арифметический сдвиг размножает знаковый разряд, т.е. самый левый бит.
Пример 3
Предположим, что программа выполняется на 8-разрядном компьютере. Первый (старший) разряд является знаковым.
x = x<<2; /* сдвиг значения переменной x влево на 2 бита */
исходное x = 00011000
Результат операции x<<2 есть 01100000.
y >>= 3; /* сдвиг значения переменной y вправо на 3 бита */
исходное y =11011001
Результат операции y>>3 есть 00011011 для логического сдвига и 11111011 для арифметического сдвига.


Операторы break и continue
Управляющий оператор break осуществляет немедленный выход из циклов while, do-while и for. Одиночный оператор break не может использоваться для выхода из более чем одного уровня вложения циклов. Оператор break может использоваться и для выхода из оператора switch.
Управляющий оператор continue осуществляет прекращение выполнения текущей итерации цикла, вызывая немедленное выполнение следующей итерации. Оператор continue используется только внутри циклов. Управление передается на управляющую часть (заголовок) цикла, с пропуском оставшихся операторов тела цикла.
Особенности выполнения операторов break и continue представлены на следующих рисунках.
Рис. 1. Цикл while
Рис. 2. Цикл for
Рис. 3. Цикл do-while


Буферизованый ввод-вывод
Буферизация - способ организации ввода-вывода в программе, позволяющий минимизировать число обращений к устройству.
При чтении из дискового файла, блок данных копируется с диска в пользовательский буфер. Многими операционными системами используется также промежуточный "системный буфер". Этот блок данных, называемый также физическим блоком или блоком ввода-вывода, имеет объем, заданный поименованной константой BUFSIZ, определенной в файле заголовков stdio.h. Операции чтения из файла фактически читают данные из этого буфера. При исчерпании буфера вновь выполняется чтение с диска, и буфер пополняется. Обращение к диску лишь при необходимости обеспечивает существенную экономию времени. Программисты могут читать из файла множеством способов (посимвольно, построчно и т.д.) и не задумываться о минимизации количества обращений к диску. Стандартный пакет ввода-вывода гарантирует решение этой проблемы.
В области данных выполняющейся программы содержится множество структур FILE - по одной для каждого файла. Первые три структуры зарезервированы для потоков stdin, stdout и stderr. Следующая структура обычно используется для первого файла, открытого функцией fopen(). Одно из полей структуры FILE указывает на связанный с файлом пользовательский буфер. Еще одно поле указывает позицию буфера, с которой будет работать следующая операция чтения или записи. Остальные поля содержат информацию о том, как открывался файл (r, w, a, r+,w+,a+), был достигнут ли конец файла и т.д.
Рис. 1. Буферизованный ввод-вывод


Характерные черты языка программирования Си
Язык программирования Си был разработан Деннисом Ритчи в начале 1970-х годов для ускорения разработки операционной системы UNIX (ОС UNIX). В результате на языке Си написана большая часть ОС UNIX, компилятор Си и практически все прикладные программы для ОС UNIX. Хотя язык Си был задуман как язык системного программирования, это не узко специализированный язык со множеством ограничений. Большой набор операций и типов данных, современное оформление и высокая степень машинонезависимости сделали его привлекательным языком программирования общего назначения.
Первое издание книги Kernigan, Ritchie. The C Programming Language; Prentice Hall, 1978 описывает так называемый стандарт K&R языка Си. Американский Национальный Институт Стандартов (ANSI) разработал формальный стандарт ANSI языка Си. Этот стандарт значительно улучшил переносимость программ на языке Си.
Язык Си — компилируемый язык, существенно более быстрый, чем интерпретируемые языки. Он сочетает в себе свойства языков высоко и низкого уровней. Наряду с управляющими операторами и структурами данных, характерными для языков высокого уровня, он содержит много операций, типичных для ассемблеров или машинных языков (в частности — побитовые операции). Кроме того, язык Си обеспечивает косвенную адресацию с использованием указателей для быстрого и эффективного доступа к данным. Сходство языка Си с ассемблером делает относительно простым создание компиляторов Си для разных типов процессоров. Благодаря переносимости и сравнительно малым потерям в скорости выполнения, язык Си в настоящее время используется во многих областях, в которых традиционным было применение ассемблеров.
Программа на Си в основном состоит из набора функций. Понятие функции в языке Си сочетает свойства подпрограмм и процедур — в Си нет различий между ними. С помощью функций достигается модульность программы. Это упрощает отладку и позволяет использовать фрагменты кода в различных программах, избегая дублирования этих фрагментов.
Си — весьма гибкий язык, который может быть использован для программирования решения самых различных задач. Круг этих задач простирается от задач управления аппаратурой ЭВМ, требующих работы с физической памятью и устройствами ввода-вывода, до деловых и финансовых задач, манипулирующих сложными структурами данных.


Приложение. Некоторые команды ОС Linux
Общий вид команд:
имя_команды [параметр1] [параметр2]...
Параметры отделяются один от другого и от имени команды пробелами. В квадратные скобки заключаются необязательные параметры. В качестве параметров могут использоваться имена каталогов, имена файлов, модификаторы, определяющие режимы работы команды и т.д. У большинства команд есть опции (ключи), которые модифицируют их выполнение. Модификатор может состоять из одной буквы, определяющей один ключ, или из последовательности букв, неразделенных пробелами, для определения нескольких ключей. Модификатору должен предшествовать знак — (минус).
  • man [номер тома] тема Справочная команда, которая выводит на стандартный вывод (обычно это экран монитора) информацию по заданной вторым параметром темe из тома руководства по ОС Linux, определенного первым параметром.
  • login [имя_пользователя] Команда осуществляет вход пользователя в ОС Linux.
  • logout Команда осуществляет окончание сеанса работы пользователя в ОС Linux.
  • shutdown Команда осуществляет окончание работы ОС Linux.
  • date Команда выводит на стандартный вывод текущие значения времени и даты.
Некоторые команды работы с файлами
В ОС Linux используется иерархическая файловая структура.
  • pwd По этой команде выдается полное имя текущего каталога.
  • cd имя_каталога Эта команда изменяет текущий каталог. После ее выполнения текущим будет каталог с именем, заданным параметром. Этот каталог располагается на следующем более низком уровне. Использование команды cd без параметров обеспечивает переход в домашний каталог.
  • cd .. Такой вариант команды cd обеспечивает подъем на уровень вверх по иерархии каталоговой файловой системы.
  • ls Команда просмотра содержимого текущего каталога.
  • cp имя_файла_1 имя_файла_2 Команда выполняет копирование существующего файла с именем имя_файла_1 в новый файл с именем имя_файла_2.
  • mcopy -t a:имя_файла каталог Команда выполняет копирование существующего на устройстве a файла с именем имя_файла в каталог.
  • mv имя_файла_1 имя_файла_2 С помощью этой команды осуществляется переименовывание файла. Старое имя файла — имя_файла_1, новое имя файла — имя_файла_2.
  • rm имя_файла... Команда выполняет удаление файла с именем имя_файла.
  • mkdir имя_каталога Команда создает в текущем каталоге новый каталог, принадлежащий более низкому уровню.
  • rmdir имя_каталога Команда удаляет заданный каталог.
  • cat [-n] имя_файла Команда конкатенации файлов. В таком виде команда cat выводит заданный файл на стандартный вывод. Если в команде задан ключ -n , то в начале строк выводятся их десятичные номера.
  • more [+число] [имя_файла ] Команда просмотра. По этой команде осуществляется построчный вывод заданного файла в интерактивном режиме на стандартный вывод. Первый параметр (число) задает номер строки файла, с которой начинается вывод.
    • <space> — просмотр вперед на один экран
    • B — просмотр назад на один экран
    • q — выход из команды просмотра
В ОС Linux кроме понятия "стандартный вывод " существует понятие "стандартный ввод". По умолчанию "стандартный вывод" — это экран монитора, а "стандартный ввод " — это клавиатура. Можно с помощью специальных команд "перенаправить" стандартный ввод-вывод:
  • > имя_файла — стандартный вывод направляется в файл с именем имя_файла
  • >> имя_файла — стандартный вывод добавляется в файл с именем имя_файла
  • < имя_файла — стандартный ввод перенаправляется из файла с именем имя_файла
Пример 1
Использование команд перенаправления ввода-вывода.
cat f1.c f2.c > f.c
cat f3.c f4.c >> f.c
Такое использование команды cat позволяет объединить файлы. Первая команда объединит файлы f1.c и f2.c,результат запишет в файл f.c. Вторая команда объединит файлы f3.c и f4.c и результат добавит к старому содержимому файла f.c.
Компиляция программы
Для запуска работы компилятора используется следующая команда:
cc [опции] имя_файла.c
Команда cc выполняет как компиляцию исходных текстов на языках программирования Си и ассемблера, так и обеспечивает редактирование связей, создавая файл образа программы (выполнимый файл). Команда cc выполняется в несколько фаз:
  1. обработка препроцессором
  2. обработка компилятором
  3. обработка оптимизатором (только при опции -O)
  4. обработка ассемблером
  5. обработка компоновщиком (редактором связей)
Некоторые опции команды cc:
  • -E (выполнение только препроцессорной обработки, направление результата в стандартный вывод )
  • -S (остановка перед ассемблированием и сохранение файла под именем имя_файла.s)
  • -c (запрещение компоновки и сохранение объектных файлов под именем имя_файла.o)
  • -o <имя_файла> (создание выполнимого файла под именем имя_файла. Если эта опция не задана, то выполнимый файл будет иметь имя a.out)
Пример 2
Использование команды cc:
cc go.c
После выполнения этой команды при безошибочной компиляции будет получен выполнимый файл под именем a.out.
cc -c main.c
Обработка компилятором прекратится перед компоновкой, при этом будет создан объектный файл под именем main.o.
cc go.c -o go
При выполнении безошибочной компиляции будет создан выполнимый файл под именем go.


Комментарии
Комментарии языка Си — это множество символов, заключенное в "комментарные" скобки /* */.
Использование комментариев в программе очень важно. Комментарии могут значительно повысить удобочитаемость программы. Удобство отладки и модификации программы сильно зависит от наличия ясных комментариев. Рекомендуется включать комментарии в начало каждого файла с текстом программы на языке Си и в начало каждой функции. Все важные и неясные фрагменты программы также должны быть прокомментированы.
Комментарии начинаются с символов /* и заканчиваются символами */. Они не могут быть вложенными.
/*Это /* типичная */ ошибка */
Компилятор интерпретирует фрагмент:
/*Это /* типичная */
как законченный комментарий, после чего будет выдано сообщение об ошибке в оставшемся фрагменте
ошибка */
Комментарии могут также начинаться с символов // (как в языке программирования Си++). В этом случае в качестве комментария рассматриваются все символы, начиная с символов // вплоть до конца строки.


Константы
Константа — определенная в программе на языке Си неизменяемая величина.
Целочисленные константы
Целочисленные константы в программе на языке Си могут быть представлены в десятичной, восьмеричной или шестнадцатеричной системах счисления (СС).
Таблица 1
Десятичная константа | Восьмеричная константа | Шестнадцатеричная константа
10 | 012 | 0xa
2001 | 03721 | 0x7d1
55 | 067 | 0x37

Десятичная константа — целочисленная константа, представленая в десятичной СС.
Восьмеричная константа — целочисленная константа, представленая в восьмеричной СС. Восьмеричная константа должна начинаться с символа 0 (нуль).
Шестнадцатеричная константа– целочисленная константа, представленая в шестнадцатеричной СС. Шестнадцатеричная константа должна начинаться с символов 0x или 0X.
Длинные целые константы отмечаются символами L или l на конце. Например, константа 52l (52L) занимает в памяти столько места, сколько отводится для длинного целого типа 52. Букву L лучше набирать на верхнем регистре, чтобы не перепутать с цифрой 1.
Для обозначения беззнаковых целых констант используется обозначение U или u.
Например, 177536U (177536u).
Использование беззнаковых значений особенно полезно при вычислении машинных адресов, такие вычисления должны выполняться с использованием беззнаковой арифметики.
Пример 1
Таблица 2
Десятичные | 55 | 100 | 255
Восьмеричные | 067 | 0144 | 0377
Шестнадцатеричные | 0х37 | 0х67 | 0Xff
Длинные целые | 55l | 100L | 31l
Беззнаковые целые | 55u | 100u | 31U

Константы с плавающей точкой
Константы с плавающей точкой — вещественные константы. Они могут быть представлены следующим образом:
  • целая часть и (или) дробная часть (1.35 )
  • десятичная точка и (или) экспоненциальное представление (13.5e-1 или 0.135E1)
  • целое в экспоненциальном представлении (1.57e6 )
По умолчанию константы с плавающей точкой имеют двойную точность (тип double). Для указания обычной точности надо использовать F или f (тип float) . Для указания расширенной точности следует использовать L или l ( тип long double).
Константы с плавающей точкой в языке Си представляются только в десятичной системе, но не в восьмеричной и не в шестнадцатеричной.
Пример 2
Таблица 3
С двойной точностью | 5.32 | .75 | 5. | 25e-3 | .240E100
С обычной точностью | 5.32f | .75F | 5.f | 25e-3F | .240E100F
С расширенной точностью | 5.32l | .75L | 5.l | 25e-3L | .240E100l

Символьные константы
Символьная константа — символ, заключенный в одиночные кавычки (апострофы). Для обычных печатных символов в апострофах указывается сам символ, (например, 'a' 'B '+' '5'). Все символьные константы имеют соответствующее им числовое значение согласно используемой таблице кодировки символов (например, ASCII, КОИ-8) и занимают один байт памяти. Кроме символьных констант, имеющих отображение, в таблице кодирования присутствуют и специальные управляющие символы.
Некоторые специальные символы:
  • '\n' — новая строка
  • '\t' — горизонтальная табуляция
  • '\b' - забой
  • '\a' — звуковой сигнал
  • '\"' - двойные кавычки
  • '\0' - нулевой байт
  • '\r' - возврат каретки
  • '\l' — прогон страницы
  • '\\' — обратная косая черта
  • '\'' — одиночная кавычка
  • '\v' — символ вертикальной табуляции
В качестве символьных констант могут использоваться восьмеричные и шестнадцатеричные значения, состоящие не более чем из трех знаков. Например, '\007'(восьмеричное) или '\X07' (шестнадцатеричное) число, которое является ASCII-кодом звукового сигнала.
Строковые константы
Строковая константа– это заключенная в двойные кавычки последовательность, состоящая из нуля или более символов. При размещении строковых констант в памяти компилятор Си помещает в конце каждой строки символ NULL('\0') в качестве признака конца строки. Строковые константы размещаются в области данных. Разделенные символами пробелов строковые константы автоматически объединяются.
Например, две строковые константы "Смежные строки" "объединяются" будут соединены в одну строковую константу "Смежные строки объединяются". А в конце этой стоки компилятор языка Си поместит признак конца строки '\0'.
Значением выражения типа строковая константа является адрес, начиная с которой хранится эта константа.


Объявление функций
Оператор объявления функции (объявление прототипа функции) имеет следующий вид:
возвращаемый_тип имя_функции (список объявлений типов параметров);
В стандарте ANSI языка Си типы возвращаемого значения функции и типы каждого из аргументов указываются в определении функции.
Пример 1
double area_circle(double rad)   /* это определение функции*/
{
...
}
Любая вызывающая функция должна быть проинформирована о типе возвращаемого значения и о типах каждого из аргументов путем использования оператора объявления функции (объявление прототипа функции). Если объявление прототипа функции задано, то компилятор проверяет правильность использования функции. В противном случае подобные ошибка дадут о себе знать только при выполнении программы. Если определение функции предшествует вызову, то объявление прототипа функции не требуется.
Объявление может быть сделано перед вызывающей функцией или внутри ее.
Пример 2
int main()
{
double area_circle(double); /* это объявление функции*/
...
}
Объявление функции area_circle() известно только той функции, в которой оно сделано, то есть функции main().
Пример 3
double area_circle(double); /* и это объявление функции*/
int main ()
{
...
}
Объявление известно всем функциям, расположенным в исходном файле после него. Если при определении функции она будет объявлена с другим типом возвращаемого значения (например, float), будет выдано сообщение об ошибке, состоящей в повторном объявлении функции.
В объявлении прототипа функции могут указываться необязательные (фиктивные) имена.
Для функций, не возвращающих никакого значения, используется тип данных (ключевое слово) void. Тогда при неумышленном использовании оператора:
X = f();
во время компиляции будет выдано сообщение, указывающее на ошибку в операции присваивания (правая часть операции присваивания имеет тип void).
Пример 4
#include <stdio.h>
int main( )
{
  double area_circle(double);   /* это объявление функции*/
  double rad = 50.5, area;

  area=area_circle(rad);
  printf("Area is %f\n",area);
}
/*возвращает площадь круга, заданного радиусом*/
double area_circle(double radius)   /* это определение функции*/
  {
  return 3.14159*radius*radius;
  }


Определение функции
Определение функции может иметь одну из двух форм:
возвращаемый_тип имя_функции (список объявлений формальных параметров)
{
   объявления   /* тело функции */
   опреаторы
}

возвращаемый_тип имя_функции (список формальных параметорв)
объявление формальных параметров
{
   объявления     /* тело функции */
   опреаторы
}
Рекомендуется первая форма, соответствующая Стандарт ANSI языка Си. Вторая форма использует стандарт K&R языка Си и обеспечивает совместимость с существующими программами на языке Си.
Тело функции – это составной оператор.
Имя функции является идентификатором. Подобно другим идентификаторам имя может содержать буквы, цифры, символ подчеркивания, но не может начинаться с цифры. Имя функции считается внешним идентификатором; оно может использоваться внутри любой другой функции (или внутри определяемой функции). Количество значащих символов в имени зависит от системы. Рекомендуется использовать в именах строчные буквы.
В функцию может передаваться информация извне через аргументы, которые называются формальными параметрами. Кроме того, функция может возвращать значение в вызывающую ее функцию, которое называется возвращаемым значением. Тип возвращаемого значения определяет ключевое слово, стоящее перед именем функции в ее определении.
Важным различием между определением функции в стандарте ANSI (называемым также определением прототипа функции) и определением функции в стандарте K&R является то, что использовании формы ANSI требует, чтобы при вызове функции ее аргументы были преобразованы к типу, ожидаемому функцией. Для того, чтобы эти соглашения выполнялись, вызову функции должно предшествовать определение прототипа функции или объявление прототипа функции. Если тип фактического параметра не может быть преобразован к требуемому типу аргумента, компилятор выдает сообщение об ошибке.
При использовании определения или объявления функции в форме K&R Си выполняются предполагаемые по умолчанию преобразования типа.
Пример 1
/*возвращает площадь круга, заданного радиусом*/
double area_circle(double radius)
{
return 3.14159*radius*radius;
}


Оператор цикла do-while
Оператор цикла do-while — управляющий оператор языка Си, реализующий выполнение цикла в алгоритме.
Синтаксис оператора цикла do-while определяется следующим образом:
do
    оператор
 while (выражение);
Рис. 1. Блок-схема цикла do-while
Цикл do-while похож на цикл while за исключением того, что значение выражения проверяется после выполнения оператора. Поэтому тело цикла выполняется как минимум один раз. Если значение выражения истинно, тело цикла выполняется повторно, выражение вновь вычисляется и т.д.., до тех пор пока значение выражения не станет ложным. Оператор может быть как простым, так и составным.
Пример 1
/*Печатать меню, пока не введен символ 'q'*/
. . .
do {
     printf ("\t Основное меню \n");
     printf ("d - Ввод данных \n");
     printf ("r - Вывод отчета\n");
     printf ("q - Выход\n");
     scanf ("%c", &choice);
     if (choice == 'd')
        getdata();
     else
        if (choice == 'r')
           report();
        else
           if (choice != 'q')
              printf("Неверный ввод. Попробуйте еще раз.\n");
     while (getchar() != '\n')
       ; /* Удаление оставшихся символов */
    }
while (choice != 'q');


Доступ к элементам массива
Операция доступа к элементу массива — операция "квадратные скобки", которая используется следующим образом:
имя_массива[индекс]
Эта операция относится к первичным и находится в самой высокой строке таблицы приоритетов операций языка Си.
Имя массива — идентификатор, которому ставится в соответствие начальный адрес массива (адрес первого элемента).
Для доступа к элементу массива (его изменения, печати, присваивания значения другой переменной и т.д.) надо использовать операцию "квадратные скобки".
В скобках должно стоять целочисленное выражение, значение которого должно находиться в диапазоне от 0 до (размер массива – 1).
Пример 1
char id[8];
id[0] ='j';
Элементу номер 0 массива id присваивается код символа 'j'.
Результат операции доступа к элементу массива — значение, хранящееся по адресу необходимого элемента массива. Этот адрес определяется следующим образом:

Смещение - это разница между адресом данного элемента массива и начальным адресом массива.
Для одномерного массива смещение определяется по формуле:

Пример 2
char id[8];
id[3] = id[0];
Байт из элемента id[0] копируется в элемент id[3].
Адрес элемента id[3]:

Пример 3
float price[3];
price[num] = price[num-1]*2;
Если значение num равно 2, то price[2] = price[1]*2;
Адрес элемента price[2]:
начальный адрес массива price + смещение = начальный адрес массива price + 2 [image] 4
Для массива произвольной размерности смещение определяется по формуле:

Пример 4
int table[4][3]; /*table[длина 2-го измерения][длина 1-го измерения]*/
printf ("%d\n", table[2][1]); /*table[индекс2][индекс1]*/
Печатает целое число, хранящееся во 2-й строке 1-го столбца массива table .
Адрес элемента table[2][1]:
начальный адрес массива table + смещение =
начальный адрес массива table + (индекс2 [image] (длина 1-го измерения ) + индекс1) [image] длина элемента в байтах =
начальный адрес массива table + (2 [image] 3 + 1) [image] 4
Пример 5
/* Запоминает рост вклада по годам в массиве и печатает его */
#include <stdio.h>
int main()
{
const float investment = 6000, interest = .085;
float  rate;
double value[5];
int    year;

rate = 1 + interest;                    /* 108.5% от предыдущей суммы */
value[0] = investment;
year = 1;
while (year < 5)
  {
  value[year] = value[year-1] * rate;
  year += 1;                           /* year = year+1 */
  }
printf("начальный вклад: %.2f\n", value[0]);
year = 1;
while (year < 5)
  {
  printf("год %.d\n", year);
  printf("%.2f\n", value[year]);
  year += 1;                           /* year = year+1 */
 }
}


Доступ к аргументам командной строки
В операционной среде, обеспечивающей поддержку языка Си, имеется возможность передать аргументы или параметры запускаемой программе с помощью командной строки.
Пример 1
Это командная строка (символ $ не входит в командную строку, это "приглашение" операционной системы).
$ a.out file1 file2
Аргументы командной строки — это множество слов, из которых состоит командная строка.
В функцию main() при передаче управления всегда передаются два аргумента. В программе они могут игнорироваться и, следовательно, не обрабатываться. Чтобы получить доступ к аргументам командной строки, в определении функции main() в круглых скобках после идентификатора main должны быть объявлены два формальных параметра.
argc (сокращение от argument count)- первый параметр функции main(), имеет смысл счетчика аргументов. Это количество слов, на которые разбивается символами промежутков командная строка.
argv (сокращение от argument vector- вектор аргументов) — второй параметр функции main(). Во время выполнения программы на языке Си слова из командой строки размещены в виде символьных строк в программном адресном пространстве. В этой же памяти запоминается и завершаемый нулем (NULL) массив адресов (указателей) этих строк.
Пример 2
#include <stdio.h>
int main (int argc, char *argv[])
{
  int i;
  printf ("number of words: %d\n", argc);
  for (i=0; i<argc; i++)
      printf ("%s\n", argv [i]);
}
В прим. 2 argv объявляется как массив указателей на символьные значения. argv[0] – это адрес, по которому хранится первое слово ( в прим. 1 это a.out); argv[1] — адрес второго слова и т.д.
Рис. 1. Пример командной строки
Использование двукратного указателя для доступа к командной строке
Второй параметр функции main() argv — это двукратный указатель.
Пример 3
/* печатает аргументы командной строки, используя argv как двукратный указатель*/
#include <stdio.h>
int main(int argc, char **argv)
  {
  for(; *argv != NULL; argv++)
    printf("%s\n", * argv);
  }


Доступ к элементам структуры
Операция доступа к элементу структуры — операция "точка"(.), результат которой — значение элемента структуры.
имя_структуры.имя_элемента_структуры
Эта операция используется для доступа к элементу структуры с тем, чтобы присвоить ему значение, напечатать его, использовать его значение в арифметической операции и т.д. Эта операция является первичной и находится в самой верхней строке таблицы приоритетов операций языка Си.
Пример 1
#include <stdio.h>
#include <stdlib.h>
struct emp {
            char name[21];
            char id[8];
            double salary;      /*это шаблон, память не выделяется */
            };
int main()
{
  struct emp prgmr;   /*это объявление переменной - выделяется память */
  int num;
  ...
  gets(prgmr.name);
  gets(prgmr.id);
  gets(buf);
  prgmr.salary = atof(buf);
  printf("Name: %s\n", prgmr.name);
  printf("Id: %s\n", prgmr.id);
  printf("salary: %.2f\n", prgmr.salary);

  ...
  prgmr.salary *= 1.15;

  ...
}


Внешний класс памяти
Внешняя переменная — переменная, которая определяется извне функции. Она доступна всем функциям, определенным далее в файле. В отличии от автоматической переменной, внешний идентификатор по умолчанию имеет нулевое значение. Элементы внешнего массива инициализируются нулевыми значениями.
Преимущество внешних переменных в том, что, в отличие от автоматических переменных, к ним можно обратиться из более чем одной функции. При использовании внешних переменных программа работает быстрее, чем при передаче в функцию длинного списка параметров. В дополнение к этому, внешние переменные обеспечивают способ разделения данных между функциями, не вызывающими друг друга.
Тем не менее, не все переменные могут быть сделаны внешними. Для улучшения структуры программы и повышения ее читаемости лучше сделать переменную автоматической, если она используется только внутри функции или блока. Кроме того, поскольку внешние переменные доступны всем функциям, требуется предпринимать усилия, чтобы избежать конфликтующих изменений значений этих переменных.
Подобно другим идентификаторам, имена внешних переменных могут содержать буквы, цифры и символ подчеркивания, но не могут начинаться с цифры. Количество значащих символов зависит от системы.
Примечание 1
Некоторые компиляторы не делают различий между строчными и прописными буквами во внешних идентификаторах. Рекомендуется использовать строчные буквы.
Пример 1
int status;
char systname[20];
void fillbuf(void), emptybuf(void);
int main()
{
  fillbuf();
  ...
  emptybuf();
  ...
}
char buf[1024];
void fillbuf(void)
{
  ...
}
void emptybuf(void)
{
 ...
}
Множественные исходные файлы
Программы на языке Си часто располагаются в нескольких исходных файлах. Одним из примеров является операционная система UNIX, исходные файлы которой функционально структурированы. Существуют файлы, содержащие программы драйверов устройств, управления процессами, файловой системой и т.д. Использование нескольких исходных файлов ускоряет редактирование, компиляцию и отладку. Оно облегчает структуризацию программы и организацию коллективной работы.
Для внешних переменных существует отличие между определением и объявлением.
Внешнее определение переменной (например, int status;) выделяет область памяти под переменную .
Внешнее объявление переменной (например, external int status;) памяти не выделяет, оно информирует компилятор о том, что переменная определена где-то еще. Это предотвращает выдачу сообщения об ошибке "неопределенный идентификатор" при использование переменной в файле, в котором она не определена.
Для функции объявление класса external не требуется, так как имя функция всегда определено как внешний идентификатор.
Пример 2
/* Файл one.c */

int status;
char sysname[20];
void fillbuf(void);

main ()
{
    fillbuf();
    . . .
    if (...)
       emptybuf();
}

/* Файл two.c */

external int status;
external char sysname[20];
char buf[1024];
void emptybuf(void);
void fillbuf(void)
{
    . . .
}
void emptybuf(void)
{
  fillbuf();
}
Для создания выполнимого файла программы надо выполнить совместную компиляцию этих двух файлов, используя, например, следующую команду операционной системы UNIX:
$ cc one.c two.c


Оператор цикла for
Оператор цикла for — управляющий оператор языка Си, реализующий выполнение цикла в алгоритме.
Синтаксис оператора цикла for определяется следующим образом:
for (выражение1; выражение2; выражение3)
    оператор
Рис. 1. Блок-схема цикла for
Цикл for используется для описания определенных действий для инициализации переменных цикла, циклического повторения тела цикла и изменения значений его переменных.
Выражение2 цикла for всегда используется в качестве условия его окончания. Выражение1 часто применяется для инициализации переменных, а выражение3 для их изменения. Выражение1 вычисляется один раз при входе в цикл; выражение2 используется многократно как условие окончания; выражение3 вычисляется многократно после выполнения операторов тела цикла. Оператор for эквивалентен следующей последовательности операторов:
выражение1;
while (выражение2)
{
   оператор
   выражение3;
}
Преимущество синтаксиса оператора цикла for по сравнению с синтаксисом оператора цикла while заключается в том, что управляющие выражения цикла собраны вместе, что повышает удобочитаемость программы.
Пример 1
i = 0;
for (num = 100 ; num > 0; num -= 10)
   tens[i++] = num;
Любое из трех или все три выражения в операторе for могут отсутствовать, однако разделяющие их символы ; опускать нельзя. Если опущено выражение2, то оно считается по умолчанию истинным.
Циклы for могут быть вложенными. Их вложение часто используется для обработки многомерных массивов. Более того, любые из циклов языка Си могут быть вложены друг в друга, например цикл while внутри цикла for, цикл for внутри цикла do-while и т.д.
Пример 2
/* Сумма столбцов 0, 1, 2 и 3 размещается в столбце 4 */
cols =5;
for (r = 0; r<rows; r++)        /* для каждой строки */
    for (c = 0; c<cols-1; c++)  /* для каждого столбца */
        table[r][cols - 1] += table[r][c];
Пример 3
/*печатает 25 элементов массива */
for (i = 0; i<25; i++)
     printf ("%d ", ray[i]);
Пример 4
/* суммирует элементы массива */
sum = 0;
for (i = 0; i<max; )
    sum += quantity[i++];
Пример 5
/* бесконечный цикл */
for (;;)
   doinit();


Формы представления числовой информации
Число 0,028 можно записать так: 28 [image] 10-3, или 0,03 (с округлением), или 2,8 [image] 10-2 и т.д. Разнообразие форм записи одного числа может послужить причиной затруднений для работы компьютерa. Во избежание этого нужно или создать специальные алгоритмы распознавания числа, или указывать каждый раз форму его записи. Второй путь проще.
Существует две формы записи чисел: естественная и нормальная. При естественной форме число записывается в естественном натуральном виде, например, 12560 – целое число, 0,00345 – правильная дробь, 4,475 – неправильная дробь. При нормальной форме запись одного числа может принимать разный вид в зависимости от ограничений, накладываемых на ее форму. Например, число 12560 может быть записано так: 12560 = 1,256 [image] 104 = 0,1256 [image] 105 = 125600 [image] 10-1 и т.д.
Автоматное представление числа – представление числа в разрядной сетке компьютера.
Представление целых чисел в компьютере
Естественная форма представления числа в компьютере характеризуется тем, что положение его разрядов в автоматном представлении остается всегда постоянным независимо от величины самого числа. Эта форма записи чисел также называется представлением чисел с фиксированной точкой.
Так как числа бывают положительные и отрицательные, то формат (разрядная сетка) автоматного представления числа разбивается на знаковую часть и поле числа. В знаковую часть записывается 0 для положительного числа и 1 для отрицательного.
Если компьютер работает только с целыми числами, то в разрядной сетке один разряд отводится под знак числа, а последующие разряды образуют поле числа.
Диапазон представления чисел в этом случае находится в пределах от [image] до [image], где [image] — количество разрядов беззнаковой части.
Если в результате операции появится число, по абсолютному значению превышающее максимально допустимое число, то возникает переполнение разрядной сетки компьютера, что нарушает нормальное функционирование программы.
Обычно целые положительные числа представляются в компьютере в прямом коде, а отрицательные — в дополнительном коде.
Прямой код двоичного числа
Прямой код двоичного числа [image] — такое автоматное представление числа [image], старший разряд которого является знаковым. Знак числа [image] для положительных чисел, и [image] для отрицательных чисел.
Пример 1
Пусть размер разрядной сетки равен 8
Рис. 1. Прямой код
В прямом коде в разрядную сетку компьютера можно записать следующее максимальное по абсолютному значению число:

где [image] — количество разрядов разрядной сетки компьютера.
Диапазон изменения машинного представления целых чисел для прямого кода лежит в пределах

Обратный код двоичного числа
Обратный код отрицательного двоичного числа [image] — такое автоматное представление этого числа [image], для которого [image], если [image] и [image], если [image]. То есть обратный код числа является инверсным изображением самого числа, в котором все разряды исходного числа принимают инверсное (обратное) значение, т.е. все нули заменяются на единицы и наоборот.
Пример 2
Дополнительный код двоичного числа
Дополнительный код отрицательного двоичного числа [image] – такое автоматное представление этого числа [image], для которого [image], если [image] и [image], если [image], за исключением последнего значащего разряда, для которого [image], если [image].
Пример 3
Дополнительный код является математическим дополнением основанию q ПСС:

где [image] — абсолютное значение числа [image], [image] — количество разрядов в представлении числа.
Пример 4
Дополнительный код двоичного числа можно получить из обратного кода, прибавив к младшему разряду единицу.
Пример 5
В компьютере операцию вычитания заменяют операцией сложения в соответствии со следующей формулой:

Таким образом, для выполнения вычитания двух целых чисел выполняется сложение прямого кода уменьшаемого с дополнительным кодом вычитаемого, при этом единица переноса из старшего разряда игнорируется.
Пример 6
заменяется на [image] (дополнительный код до 100 - 11) = 124
Ответ : 24 (1 в старшем разряде игнорируется).
Представление чисел в форме с плавающей точкой
В нормальной форме число представляется следующим образом:

где [image] — мантисса числа [image]; [image] — порядок числа [image].
Такое представление чисел неоднозначно; для определенности обычно вводят некоторые ограничения. Наиболее распространено и удобно для представления в компьютере ограничение вида
(1)

где [image] — основание системы счисления.
Нормализованная форма представления чисел — форма представления чисел, для которых справедливо условие (1).
Так как положение разрядов для изображения мантиссы в автоматном представлении числа не постоянно, такую форму представления чисел называют формой представления числа с плавающей точкой (ПТ). Формат автоматного представления числа с ПТ должен содержать знаковые части и поля мантиссы и порядка. Выделяются специальные разряды для изображения знака числа (мантиссы) и знака порядка. Кодирование знаков такое же, как для формы с фиксированной точкой. Этот формат является машинно зависимым.
Рис. 2. Пример формата с плавающей точкой
Пример 7
Пусть в данную разрядную сетку надо записать числа А1 = -10110,1111 и А2 = 0,0001100010111.
Эти числа надо записать в нормальной форме:
А1 = -0.101101111 [image] 10101
A2=0.1100010111 [image] 10-11
Мантисса числа А1(со знаком) — [m1] = 1 101101111
Порядок числа А1(со знаком) — [p1]=0 0101
Мантисса числа А2(со знаком) — [m2] = 0 110001011
Порядок числа А2(со знаком) — [p2]=1 0011
[A1]=1 101101111 0 0101
[A2]=0 110001011 1 0011
Погрешности представления числовой информации
Представление числовой информации в ЭВМ, как правило, влечет за собой появление погрешностей (ошибок), величина которых зависит от формы машинного представления числа и от длины разрядной сетки ЭВМ.
Абсолютная погрешность представления числа — разность между истинным значением входной величины [image] и ее значением, полученным из машинного представления [image]:

Относительная погрешность представления числа определяется следующим образом:

Входные величины независимо от количества значащих цифр могут содержать грубые ошибки, возникающие из-за опечаток, ошибочных отсчетов показаний каких-либо приборов, некорректной постановки задачи или отсутствия более полной и точной информации. Например, часто принимают [image]. Однако эта величина может быть получена с более высокой точностью. Если принять, что точное значение [image], то абсолютная погрешность равна [image].
Часто некоторая величина в одной ПСС имеет конечное значение, а в другой ПСС становится бесконечной величиной. Например, дробь [image] имеет конечное десятичное представление, но, будучи переведена в двоичную ПСС, становится бесконечной дробью.
Следовательно, при переводе чисел из одной ПСС в другую неизбежно возникают погрешности, оценить которые нетрудно, если известны их истинные значения.
Максимальная погрешность перевода десятичной информации в двоичную не будет превышать единицы младшего разряда разрядной сетки ЭВМ. Минимальная погрешность будет равна 0.


Выражения
Выражение — это конструкция языка Си, имеющая значение. Этой конструкцией может быть идентификатор, константа, выражение в скобках или осмысленным образом связанные знаками операций выражения. Скобки используются для группировки выражений с целью задания желаемого порядка вычислений.
Пример 1
Выражения-идентификаторы:
    • val
    • x
    • total_sum
Пример 2
Выражения-константы:
    • 5
    • 127.6
    • 'B'
    • "Main menu"
Пример 3
Выражения, содержащие операции:
    • x + y
    • current < max
    • val - num / 10
Пример 4
Выражения в скобках:
    • (x + y)
    • (current < max)


Функции gets() и puts()
Функция gets() — стандартная функция языка Си для ввода строки.
Объявление прототипа функции gets():
char *gets(char *s)
Функция puts() — стандартная функция языка Си для вывода строки.
Объявление прототипа функции puts():
int puts(const char *s)
Эти объявления находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция gets() вводит входную строку из потока stdin. При этом символ новой строки отбрасывается, а введенные символы дополняются в конце нулевым символом (символом '\0'). Необходимо позаботиться, чтобы принимающий массив символов был достаточно большим для размещения наибольшей возможной строки. Функция возвращает NULL по концу файла, если символов для чтения нет.
В отличии от функции scanf(), использующей спецификатор %s, функция gets() читает полную строку, включая символы пробела и табуляцию.
Функция puts()выводит заканчивающуюся нулевым символом строку символов в поток stdout. При этом вместо нулевого символа выводится символ новой строки.
Функции fgets()и fputs()подобны функциям gets() и puts()(Функции чтения-записи).
Пример 1
#include <stdio.h>
#define LINELEN 256
int main()
  {
  char name[LINELEN+1];
  printf("Введите полное имя:");
  if (gets(name) == NULL)
    printf("Ошибка\n");
  else
    puts(name);
}
Вывод программы:
Введите полное имя:Иванов Петр Васильевич
Иванов Петр Васильевич


Функции обработки ошибок
Объявления прототипов функций обработки ошибок находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
Каждая структура FILE содержит информацию о том, был ли достигнут конец соответствующего файла и о том, произошла ли ошибка ввода-вывода (например, попытка писать в файл, который был открыт только для чтения).
Функция feof() - стандартная функция ввода-вывода, которая проверяет достижение конца файла.
Объявление прототипа функции feof():
int feof(FILE *stream)
Функция feof() возвращает отличное от нуля значение, если в процессе чтения или записи был достигнут конец файла, иначе она возвращает 0.
Пример 1
while (1)
   {
   n=fread(buf, 1, BUFSIZ, rp);
   fwrite(buf, 1, n, wp);
   if (feof(rp))
     break;
   }
Попытка записи в файл может быть безуспешной, если размер файла достиг максимума или исчерпалось свободное дисковое пространство.
Функция ferror() - стандартная функция ввода-вывода, которая выполняет проверку полей структуры FILE.
Функция clearerr() - стандартная функция ввода-вывода, которая выполняет очистку полей структуры FILE.
Объявления прототипов функциий ferror() и clearerr():
int ferror(FILE *stream)
void clearerr(FILE *stream)
Функция ferror() возвращает отличное от нуля значение, если перед этим произошла ошибка ввода-вывода, иначе - 0.
Функция clearerr() сбрасывает признаки ошибки и конца файла в 0.
Пример 2
/*пытается MAX раз выполнить чтение с устройства*/
success = 0;
clearerr(rp);
for(i=1; i<MAX; i++)
   {
   fread(buf, BUFSIZ, 1, rp);
   if(!ferror(rp))
     {
     success = 1;
     break;
     }
   clearerr(rp);
   }
if (success)
   printf("Чтение выполнено\n");
else
   printf("После максимального числа попыток чтение не выполнено\n");


Функция fclose()
Функция fclose() - стандартная функция ввода-вывода языка Си, с помощью которой осуществляется закрытие файла.
int fclose (FILE *stream)
Функция fclose() закрывает файл, на который указывает ее параметр. При закрытии файла его буфер записывается (вытесняется) на диск, если вывод реально выполнялся. Функция fclose() возвращает EOF в случае ошибки и нуль в противном случае.
Хотя открытый файл автоматически закрывается при завершении программы, даже если функция fclose() не вызвана, рекомендуется закрывать файл явно. Во-первых, при этом освобождается место в памяти, которое может быть использовано для открытия нового файла. Во-вторых, буфер, если он есть, записывается за приемлемое время. Одновременно в программе может быть открыто не более 20 файлов.
Объявление прототипа функции fclose() находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Пример 1
Эта программа копирует файл, заданный вторым аргументом командной строки в файл, заданный третьим аргументом командной строки.
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *rp, *wp;

if(arg != 3)
  {
   printf ("Имя второго файла не определено\n");
   exit (1);
  }
if((rp=fopen(argv[1],"r"))==NULL)
  {
   printf ("Не открыт файл %s\n",argv[1]);
   exit (2);
  }
if((wp=fopen(argv[2],"w"))==NULL)
  {
  printf ("Не открыт файл %s\n",argv[2]);
  fclose(rp);
  exit (2);
}
/*здесь должны быть функции чтения-записи*/

fclose(rp);
fclose(wp);
exit (0);
}


Вытеснение буфера
Функция fflush() — стандартная функция языка Си для вытеснения содержимого пользовательского буфера в файл или в системный буфер, не дожидаясь заполнения пользовательского буфера.
Объявление прототипа функции fflush():
int getchar(FILE *stream)
Это объявление находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция fflush() обычно используется для файлов, открытых для записи, возвращает значение EOF в случае возникшей при записи ошибки или нуль в противном случае.
Эту функцию можно использовать для сброса ввода (остатка входного файла).
Пример 1
fflush(stdin); /*не запрашивает уже выполненный ввод*/
printf("ВВедите данные: ");
gets(buf);


Функция fopen()
Функция fopen() — функция открытия файла.
FILE *fopen(const char *имя_файла, const char *тип)
Первым аргументом функции fopen() является адрес строки, содержащей путевое имя файла. Второй аргумент — это адрес строки, определяющей тип работы с открываемым файлом.
Заданный именем файл открывается в соответствии с указанным типом. Тип может принимать следующие значения:
  • "r" — текстовый файл открывается для чтения (read);
  • "w"- текстовый файл создается для записи; старое содержимое, если оно было, выбрасывается (write);
  • "a" — текстовый файл открывается или создается для записи в конец файла (add);
  • "r+" — текстовый файл открывается для исправления, т.е. для чтения и записи;
  • "w+"- текстовый файл создается для исправления, старое содержимое, если оно было, выбрасывается;
  • "a+" — текстовый файл открывается или создается для исправления уже существующей информации и добавления новой в в конец файла.
Функция fopen() возвращает указатель на структуру FILE, который затем присваивается переменной. Этот указатель называют внутренним. После открытия файла к нему обращаются с использованием указателя на этот файл, имя файла больше не потребуется.
Для обозначения двоичных файлов к типу может добавляться символ b (например, "rb", "wb", "r+b"). Однако, ОС UNIX не делает никаких различий между двоичными и текстовыми файлами, следовательно, символ b игнорируется.
Объявление прототипа функции fopen() находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Пример 1
#include <stdio.h>
int main()
{
  FILE *fp;
  fp = fopen("logfile", "w");
  if (fp == NULL)
     printf("Open failed\n");
  ...
}
Для обеспечения уверенности в успешном открытии файла необходимо проверять код, возвращаемый функцией fopen().
В таблице 1 приводятся результаты успешного открытия файла.
Таблица 1
 | чтение "r" | запись "w" | добавление "a"
файл существует | - | старое содержимое отбрасывается | -
файл не существует | ошибка | файл создается | файл создается

Если файл существует и открывается для чтения или дополнения, то он не изменяется при открытии. При открытии существующего файла для записи, он усекается. Если файл, открываемый для записи, не существует, то он будет создан. Однако, открытие несуществующего файла для чтения повлечет ошибку.
Открытие файла с типом "r+", "w+" или "a+" называют открытием для обновления, что означает, что все три типа допускают как чтение, так и запись. При таком открытии выполняются те же действия c существующими и несуществующими файлами, что и при открытии с соответствующими типами "r", "w" и "a". При использовании типов "a" и "a+" запись будет выполняться в конец файла.


Обзор функций форматированного ввода-вывода
В языке программирования Си нет встроенных операторов ввода-вывода. Операции ввода-вывода могут осуществляться с помощью функций из стандартной библиотеки ввода-вывода. К таким функциям относятся и функции форматированного ввода-вывода, которые осуществляют ввод или вывод данных и их преобразования в нужное представление. Эти функции представлены в табл. 1.
Таблица 1
Устройство ввода-вывода | функция ввода | функция вывода
стандартное устройство ввода-вывода | printf() | scanf()
файл | fprintf() | fscanf()
строка | sprintf() | sscanf()

Функция printf() обеспечивает форматированного вывод: программист может задать ширину колонок, выравнивание по левому или правому краю,вывод чисел в десятичной или восьмеричной записи и т.д.
Функция scanf() осуществляет ввод в определенном формате, например, три целых числа, разделенных табуляциями.
Функция printf() выводит информацию на стандартное устройство вывода, обычно являющееся монитором терминала. Используя средства системы ОС UNIX, стандартное устройство вывода можно переназначить на вывод в файл, в конвейер (pipe) или в файл устройства.
Функции fprintf()и sprintf()аналогичны функции printf().Функции sprintf() осуществляет вывод в строку, завершая вывод символом '\0' (признак конца строки). Строка должна быть достаточно большой, чтобы вмещать результат вывода.
Функция scanf() читает информацию со стандартного устройства ввода, который обычно является клавиатурой терминала. Используя средства системы ОС UNIX, стандартное устройство ввода можно переназначить на ввод из файла, из конвейера или из файла устройства.


Произвольный доступ
Если не выполнялись специальные действия, операции чтения и записи обращаются к последовательным байтам в файле. Позиция в файле для следующей операции чтения или записи хранится во внутреннем указателе — поле в структуре FILE, автоматически устанавливаемом и изменяемом стандартными функциями ввода-вывода. Однако можно изменить место следующего чтения или записи, используя функцию fseek(), модифицирующую внутренний указатель.
Объявление прототипа функции fseek():
int fseek(FILE *stream, long offset, int ptrname)
Это объявление находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция fseek() — стандартная функция языка Си, устанавливает позицию в потоке stream для следующей операции чтения-записи на расстоянии offset байтов от позиции ptrname, принимающей следующие значения:
  • 0 — начало
  • 1 — текущая позиция
  • 2 — конец
Функция fseek() возвращает ненулевое значение для недопустимых установок, 0 в противном случае.
Пример 1
fseek(fp, 0L, 0); /*установка в начало*/
fseek(fp, 0L, 2); /*в конец файла*/
fseek(fp, -sizeof(struct emp), 1); /*возврат на одну структуру*/
Для реализации произвольного доступа к файлу бывает полезна функция ftell().
Объявление прототипа функции ftell():
long ftell(FILE *stream)
Функция ftell() — стандартная функция языка Си, которая возвращает смещение текущей позиции внутреннего указателя относительно начала файла (например, 0, если он указывает на начало файла). Для компилятора языка Си в ОС UNIX это смещение измеряется в байтах, для других операционных систем это может быть не так.
Пример 2
/*Печатает хранящуюся информацию о запрошенном объекте*/
#include <stdio.h>
struct part_info {
                 char desc[20];
                 int qty;
                 int cost;
                 }part;
int main()
{
  FILE *rptr;
  long x;

  if ((rptr=fopen("p_data","r")) == NULL)
    {
    fprintf(stderr,"Файл данных не может быть открыт\n");
    exit(1);
    }
  printf("Введите порядковый номер записи:");
  scanf("%ld", &x);
  fseek(rptr, ((x-1)*sizeof(part)), 0);
  fread (&part, sizeof(part), 1, rptr);
  printf("%s\t%d\t%d\n", part.desc, part.qty, part.cost);
}
Произвольный доступ при обновлении файла
Файл может использоваться как для чтения так и для записи, если он открыт для "обновления" ("r+", "w+", "a+"). В этом случае используется лишь один буфер, который должен вытесняться на диск между выполнением операций чтения и записи для обеспечения требуемой взаимной последовательности считываемых и записываемых данных. Для этого необходимо или явно вызывать функцию fflush(), или использовать функцию fseek(), которая, кроме установки позиции в файле для следующего чтения или записи, вызывает функцию fflush() для вытеснения буфера.
Пример 3
#include <stdio.h>
...
if ((rptr=fopen("p_data","r+")) == (FILE *)NULL)
  {
   fprintf(stderr,"Файл данных не может быть открыт\n");
   exit(1);
  }
/*изменяет все поля cost в учетном файле, составленном из структур*/
while (fread(&part, sizeof(part), 1, fp) != 0)
  {
   part.cost *= 1.07;
   fseek(fp, -sizeof(part), 1);
   /*на одну запись назад*/
   fwrite(&part, sizeof(part), 1, fp);
   fseek(fp, 0L, 1);
/* вытесняет буфер перед следующим чтением */
}
...


Функции getchar() и putchar()
Функция getchar() — стандартная функция языка Си для ввода символа.
Объявление прототипа функции getchar():
int getchar(void)
Функция putchar() — стандартная функция языка Си для вывода символа.
Объявление прототипа функции putchar():
int putchar(int c)
Эти объявления находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция getchar() читает из стандартного устройства ввода один символ и возвращает его код в соответствии с таблицей кодирования. Если при попытке чтения были обнаружены конец файла или ошибка, функция getchar() возвращает значение EOF. При вводе с терминала getchar() возвращает EOF, если пользователь нажал в начале строки комбинацию клавиш <CTRL>, <d>.
Программа, работающая в среде ОС UNIX и использующая функцию getchar() для ввода, не получит вводимые с клавиатуры символы до тех пор, пока пользователь не нажал клавишу <RETURN>. Это позволяет пользователю корректировать набираемые символы (например, с помощью забоя) прежде, чем программа их получит. Однако, некоторые программы, например, редактор “vi”, работают с терминалом в режиме, при котором вводимые символы немедленно становятся доступными программе.
Функция putchar() выводит один символ в стандартный вывод (по умолчанию — на экран терминала), возвращает при успешном завершении код выводимого символа иначе EOF. Обычно возвращаемое значение не обрабатывается.
Пример 1
#include <stdio.h>
/* копирование ввода на вывод */
int main()
{
  int c;

  while ((c=getchar())!=EOF)
     putchar(c);
}


Функция printf()
Функция printf() — функция форматированного вывода языка Си, осуществляющая вывод в стандартный поток stdout в соответствии с форматом.
int printf(формат[,аргументы]...)
Функция printf() возвращает количество выведенных символов или, в случае ошибки, отрицательное число.
Формат — это строка, заключенная в двойные кавычки, которая может содержать литеральные символы, копируемые в поток вывода, и спецификации преобразования.
Спецификация преобразования — последовательность символов, начинающаяся с символа % и заканчивающаяся символом-спецификатором.
Объявление прототипа функции printf() находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Спецификаторы преобразования должны соответствовать типу указанных аргументов. При несовпадении не будет выдано сообщений об ошибках ни при компиляции, ни при выполнении программы, но при этом вывод программы может содержать "мусор".
Таблица 1
Символ-спецификатор | Тип аргумента; вид печати
c | int; единичный символ после преобразования в unsigned char
d, i | int; знаковое целое число в десятичной записи
o | int; знаковое целое число в восьмеричной записи
x, X | int; знаковое целое число в шестнадцатеричной (a-f или A-F) записи
u | int; беззнаковое целое число в десятичной записи
e, E | double; десятичная форма записи вида [-]m.dddddde [image] xx или [-]m.ddddddE [image] xx, где количество d определяется точностью. По умолчанию точность равна 6, нулевая точность подавляет печать десятичной точки
f | double; десятичная форма записи вида [-]mmm.ddd, где количество d определяется точностью. По умолчанию точность равна 6, нулевая точность подавляет печать десятичной точки
g, G | double; используется %e или %E, если порядок меньше -4 или больше или равен точности, в противном случае используется %f. Завершающие нули и десятичная точка не печатаются
s | char; печатает символы, расположенные до признака конца строки, или в количестве, заданном точностью
p | void *;указатель
n | int *; число символов, напечатанных к данному моменту данным вызовом функции, записывается в аргумент (никакие другие аргументы не преобразуются)
% | Аргумент не преобразуется; печатается символ %

Пример 1
#include <stdio.h>
int main()
{
   char c = 'j';
   int val = 59;
   float total = 7500.5;
   long double var = 63.78;
   char initials[4];
   initials[0] = 'I';
   initials[1] = 'M';
   initials[2] = 'R';
   initials[3] = '\0';
   printf("%c\n", c);
   printf("%d\n", val);
   printf("%o\n", val);
   printf("%x\n", val);
   printf("%e\n", total);
   printf("%e\n", total);
   printf("%Lf\n", var);
   printf("%s\n", initials);
}
вывод:
j
59
73
3b
7.500500e+03
7500.500000
63.780000
IMR
Дополнительное форматирование
Функция printf() предоставляет множество возможностей по форматированию вывода.
В спецификации преобразования между символом % и символом-спецификатором могут быть расположены следующие элементы (в указанном порядке):
  • Флаги (в любом порядке), модифицирующие спецификацию:
    • Символ минус (-): выравнивание по левому краю. Задание ширины поля по умолчанию устанавливает выравнивание по правому краю. Для выравнивания по левому краю надо указать символ минус (-) перед шириной поля, например, %-15d, %-15s.
    • Символ плюс (+): печать числа со знаком. По умолчанию перед отрицательными числами выводится минус (-). Для того, чтобы перед положительными числами выводился плюс (+), надо использовать %+d, %+f и т.д.
    • Символ пробел: если число положительное или нуль, то при печати числу будет предшествовать пробел.
    • Символ нуль (0): указывает, что числа должны дополняться слева нулями до всей ширины поля.
    • Символ #. Для печати ведущих нулей восьмеричных чисел используется спецификатор %#o, для вывода 0x перед шестнадцатеричными числами используется спецификатор %#x. При использовании %#f и %#e десятичная точка будет выводиться, даже если в числе нет дробной части.
  • Число, определяющее минимальную ширину поля. Минимальная ширина поля используется для задания столбцов. Например, %15d напечатает по меньшей мере 15 символов. Число будет выравнено по правому краю и дополнено слева до 15 символов пробелами. При применении этого формата на последующих строчках будет напечатан выравненный по правому краю столбец шириной 15 символов. Число печатается, даже если его длина превышает 15 символов,так как 15 — это минимальная ширина поля.
  • Символ точка (.), отделяющий ширину поля от величины, устанавливающей точность.
  • Число (точность) использующееся для ограничения количества выводимых символов. При использовании вместе с %f (например, %.2f), она ограничивает количество цифр после десятичной точки (в данном примере до 2). %15.2f выводит вещественное число с двумя знаками после десятичной точки в поле с минимальной шириной 15 символов, включая десятичную точку. Использование точности с %s (например, %.15s) определяет максимальное количество выводимых символов (в данном примере 15). Точное количество выводимых символов можно контролировать, задавая и минимальную ширину поля и точность (максимальное количество символов), например, 15.15.
  • Модификаторы h, l или L. Символ h указывает на то, что соответствующий аргумент должен печататься как short или unsigned short; символ l сообщает, что аргумент имеет тип long или unsigned long; символ L сообщает, что аргумент имеет тип long double.
Пример 2
Пусть для вывода некоторого набора данных используется следующий вариант функции printf():
printf("%d %d %s %f\n", mod, gt, it, cst);
Вывод будет выглядеть следующим образом:
2901 6 Cerebral Calculator 75.489998
30 7229 Blue Ribbon Cable 26.000000
31650 100 Glow Worm Glare Screen 89.989998
2 677 Personal Mainframe 9000.000000
Пример 3
Пусть для вывода этого же набора данных используется вариант функции printf()с возможностями дополнительного форматирования:
printf("%-6d %4d %-24s %7.2f\n", mod, gt, it, cst);
В этом случае вывод будет выглядеть следующим образом:
2901     6 Cerebral Calculator        75.48
30    7229 Blue Ribbon Cable          26.00
31650  100 Glow Worm Glare Screen     89.98
2      677 Personal Mainframe       9000.00


Функции чтения-записи
Стандартная библиотека ввода-вывода содержит множество функций для чтения и записи. Файл может читаться посимвольно, построчно, с форматными преобразованиями или поблочно.
Когда аргументом функции является указатель на FILE, этот указатель в описании функции называют потоком.
Объявления прототипов функций чтения-записи находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
EOF — признак конца файла определен в файле заголовков stdio.h. Значение EOF зависит от компилятора (типичным значением для ОС UNIX является –1). Значение EOF может быть введено с клавиатуры путем нажатия клавиш <CTRL>, <D> в начале строки.
Описания функций чтения-записи
int fgetc(FILE *stream)
Функция возвращает следующий символ из потока в формате int или EOF, если исчерпан файл или обнаружена ошибка.
int fputc(int c, FILE *stream)
Функция пишет символ, переведенный в формат unsigned char в поток, возвращает переданный символ или EOF в случае ошибки.
int ungetc(int c, FILE *stream)
Функция отправляет символ, переведенный в формат unsigned char, обратно в поток. При следующем чтении из потока он будет получен снова. Для каждого потока вернуть можно не более одного символа. Нельзя возвращать EOF. Функция возвращает переданный символ или EOF в случае ошибки.
char *fgets(char *s, int n, FILE *stream)
Функция читает не более n-1 символов в массив s, прекращая чтение, если встретился символ новой строки, который включается в массив, кроме того, записывается символ '\0'. Функция возвращает адрес массива s или NULL, если исчерпан файл или обнаружена ошибка.
int fputs(char *s, FILE *stream)
Функция пишет строку s, которая может не иметь символ '\n', в поток, возвращает неотрицательное целое или EOF в случае ошибки.
int fscanf( FILE *stream, const char *format, ... /*аргументы*/)
Функция читает данные из потока под управлением формата и преобразованные величины присваивает аргументам, каждый из которых должен быть указателем. Завершает работу, если исчерпался формат. Возвращает EOF по исчерпании файла или перед любым преобразованием, если возникла ошибка. В остальных случаях функция возвращает количество преобразованных и введенных элементов. Правила построения форматной строки такие же, как для функции scanf().
int fprintf( FILE *stream, const char *format, ... /*аргументы*/)
Функция преобразует и пишет вывод в поток под управлением формата. Возвращаемое значение — число записанных символов или, в случае ошибки, отрицательное значение. Правила построения форматной строки такие же, как для функции printf().
int fread (void *ptr, size_t size, size_t nitems, FILE *stream)
Функция читает из потока в массив ptr не более nitems объектов размера size. Функция возвращает количество прочитанных объектов, которое может быть меньше заявленного. Для определения состояния после чтения следует использовать функции обработки ошибок feof() и ferroe().
int fwrite (const void *ptr, size_t size, size_t nitems, FILE *stream)
Функция пишет из массива ptr в поток nitems объектов размера size. Функция возвращает количество записанных объектов, которое в случае ошибки меньше nitems.
В следующих примерах рассмотрены различные варианты копирования файла. Предполагается, что файлы предварительно открыты с помощью функции fopen() и после использования будут закрыты функцией fclose().
Пример 1
Копирование файла посимвольно
while((c=fgetc(rp)) != EOF)
   fputc(c, wp);
Пример 2
Копирование файла построчно
char buf[256];
while((fgets(buf, 256, rp)) != NULL)
   fputs(buf, wp);
Пример 3
Копирование файла в соответствии с указанным форматом
char name[40];
int id;
while(fscanf(rp,"%s %d", name, &id) != EOF)
   printf (wp, "%s %d\n", name, id);
Пример 4
Копирование файла поструктурно
struct info{
            char name[50];
            int id;
           } part;
while (fread(&part, sizeof(part), 1, rp) != 0)
   fwrite (&part, sizeof(part),1, wp);
В данных примерах rp — указатель FILE, используемый для доступа к файлу, открытому для чтения, wr — для записи. Эти указатели инициируются при открытии файлов, содержат информацию об открытии файла и не должны изменяться программистом. Эти указатели совместно используются функциями стандартной библиотеки ввода-вывода так, чтобы гарантировать, что следующие друг за другом вызовы функций чтения или записи обеспечивают последовательный доступ. Поэтому данные указатели иногда называют внутренними указателями.


Функция scanf()
Функция scanf() — функция форматированного ввода языка Си, осуществляющая ввод со стандартного потока stdin в соответствии с форматом. При необходимости функция преобразует вводимые символы в числа в соответствии с форматом и сохраняет данные по указанным адресам. Функция прекращает ввод после первой же ошибки, оставляя ошибочный символ непрочитанным. Функция scanf() возвращает количество прочитанных и преобразованных полей или признак конца файла (EOF) при достижении конца файла.
Функция scanf() — это функция из стандартной библиотеки ввода-вывода.
Объявление прототипа функции scanf() находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Синтаксис функции scanf() аналогичен синтаксису функции printf().
int scanf()(формат[,список указателей]...)
Форматная строка обычно содержит спецификации преобразования, которые используются для управления вводом. В форматную строку могут входить следующие элементы:
  • пробелы и табуляции, которые игнорируются;
  • обычные символы (кроме символа %), которые ожидаются в потоке ввода среди символов, отличных от символов-разделителей;
  • спецификации преобразования
Для функции scanf() спецификация преобразования может состоять из следующих символов:
  • символ %
  • символ *(необязательный), подавляющий присваивание;
  • число(необязательное), специфицирующее максимальную ширину поля;
  • символ h или l, или L(необязательный), указывающий размер присваемого значения;
  • символ-спецификатор
Допустимые значения символа-спецификатора приведены в таблице 1.
Таблица 1
Символ-спецификатор | Данные на вводе; тип аргумента
d | Десятичное целое; int *
i | Целое; int *. Целое может быть восьмеричным (с нулем слева) или шестнадцатеричным (с 0x или 0X слева)
o | Восьмеричное целое (с нулем слева или без него); int *.
u | Беззнаковое целое десятичное число; int *.
x | Шестнадцатеричное целое (с 0x или 0X слева или без них); int *.
c | Символ; char *. Символы ввода размещаются в указанном массиве символов в количестве, заданном шириной поля, по умолчанию это количество равно 1. Символ '\0' (признак конца строки) не добавляется. Символы-разделители здесь рассматриваются как обычные символы и поступают в аргумент.
s | Строка символов, отличных от символов-разделителей (записывается без двойных кавычек); char *, указывающий на массив размера достаточного, чтобы вместить строку и добавленный к ней символ '\0'.
e, f, g | Число с плавающей точкой; float *. Формат ввода для float состоит из необязательного знака, строки цифр, возможно с десятичной точкой, и необязательного порядка, состоящего из E или e и целого, возможно со знаком
p | Значение указателя в виде, в котором функция printf() его напечатает; void *.
n | Записывает в аргумент число символов, прочитанных к данному моменту данным вызовом функции; int *. Никакого чтения ввода не происходит. Счетчик числа введенных элементов не увеличивается.
[...] | Выбирает из ввода самую длинную ненулевую строку, состоящую из символов, заданных в квадратных скобках; char *. В конце строки добавляется символ '\0'. Спецификатор вида []...] включает символ ] в заданное множество символов.
[^...] | Выбирает из ввода самую длинную ненулевую строку, состоящую из символов, не входящих в заданное в квадратных скобках множество; char *. Спецификатор вида [^]...] включает символ ] в заданное множество символов.
% | Обычный символ %; присваивание не делается.

Функция scanf() использует адрес аргумента для размещения преобразованных данных. Распространенной ошибкой при использовании функции scanf() является отсутствии операции получения адреса (&). В этом случае функция scanf() интерпретирует значение аргумента как адрес и либо записывает данные в непредсказуемое место памяти, либо пытается сделать это в защищенную от записи область, что влечет аварийное завершение работы программы.
Значение, возвращаемое функцией scanf(), может использоваться для проверки правильности ввода данных.
Пример 1
#include <stdio.h>
int main()
{
  int number;

  printf("Введите число");
  while(scanf("%d",&number) != 1)
     printf("Ошибка. Попытайтесть еще раз ");
  printf("Спасибо. Вы ввели число %d\n", number);
...
}
Экран монотора:
Введите число  tyuh
Ошибка. Попытайтесть еще раз Ошибка. Попытайтесть еще раз Ошибка. Попытайтесть еще раз
В примере 1 в программе сделана неудачная попытка обеспечить ввод того, что нужно программисту. При неудачном вводе числа "ошибочный" символ (в данном случае t) остается во входном потоке. При повторных вызовах функции scanf() ввод вновь начинается с этого символа и попытка ввода оказывается безуспешной. Необходимо убрать ненужные символы из входного потока. В примере 2 показано, как можно очистить входную строку, используя функцию getchar().
Пример 2
#include <stdio.h>
int main()
{
  int number;

  printf("Введите число");
  while(scanf("%d",&number) != 1)
    {
     while(getchar() != '\n')
        ;           /* очищает строку*/
     printf("Ошибка. Попытайтесть еще раз ");
    }
  printf("Спасибо. Вы ввели число %d\n", number);
...
}
Экран монотора:
Введите  число  tyuh
Ошибка. Попытайтесть еще раз 34
Спасибо. Вы ввели число 34


Функции обработки строк
<string.h> - файле заголовков, в котором объявляются функции обработки строк, а так же типов, поименованных констант и макросов.
Программы, использующие эти функции, должны включать директиву препроцессора include:
#include <sting.h>
В системах ОС UNIX эти функции обычно содержатся в стандартной библиотеке Си /lib/libc.a
Один или более параметров этих функций имеют тип указателя на символ (char *). Это означает, что в качестве значения параметра ожидается адрес символа, например, начальный адрес символьного массива.


Функция ungetc()
Функция ungetc() — стандартная функция языка Си для работы с символом.
Объявление прототипа функции ungetc():
int ungetc(int c, FILE *stream)
Это объявление находится в файле заголовков stdio.h, поэтому программы, использующие эту функцию, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция ungetc() возвращает один символ во входной поток при условии, что выполнялось хотя бы одно чтение. Следующая операция чтения прочитает этот символ.
Часто эта функция реализуется как макрокоманда, определенная в файле заголовков stdio.h. Функция ungetc() используется для вставки последнего прочитанного символа обратно во входной поток так, что следующая операция чтения прочтет его снова. Это может потребоваться в случае, когда один символ читается много раз, или когда значение прочитанного символа не принадлежит требуемому диапазону (например, ожидается число, а было прочитано 'z').
Вставка символа гарантируется лишь тогда, когда из входного потока было прочитано хотя бы что-нибудь, и когда поток буферизован (открыт функцией fopen()). Если функция ungetc() не может вставить символ, она возвращает EOF.
Пример 1
Функция выбрасывает из входного потока символы промежутков.
#include <stdio.h>
void skip_whites(FILE *fp)
{
int c;
while ((c=fgetc(fp)) == ' ' || c=='\t' || c== '\n')
   ;
ungetc(c,fp); /*возвращает во входной поток отличный от промежутков символ*/
}


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


Управляющий оператор if
Простая форма оператора if
Оператор if — управляющий оператор языка Си, реализующий ветвление алгоритма.
if (выражение)
    оператор
Блок-схема простой формы оператора if представлена на рис. 1.
Рис. 1. Блок-схема простой формы оператора if
Если выражение в скобках истинно, то оператор выполняется, в противном случае оператор пропускается. Оператор может быть простым или составным (блоком). Скобки вокруг выражения обязательны.
Пример 1
if  (x > largest)
   largest = x;
Пример 2
if (val < min || val > max)
  {
   printf ("Значение %d выходит за пределы.\n", val);
   printf ("Пределы %d - %d.\n",min, max);
  }
Пример 3
if (num < 0)
   num = - num;
Полная форма оператора if
if (выражение)
    оператор1
else
    оператор2
Блок-схема полной формы оператора if-else представлена на рис. 2.
Рис. 2. Блок-схема полной формы оператора if
Если выражение в скобках истинно, то оператор1 выполняется, а оператор2 пропускается. Если выражение ложно, пропускается оператор1, а выполняется оператор2. Оператор1 и оператор2 могут быть простыми или блоками. Скобки вокруг выражения, следующего за ключевым словом if, обязательны.
Пример 4
if (quantity < 1000)
  printf ("Количество недостаточное \n");
else
    printf ("Количество достаточное \n");
Пример 5
if ( x != 0)
    y = y/x;
else
   {
     printf ("Ошибка");
     printf ("Деление на нуль\n");
    y = 0;
   }
Обычными для многих программ являются серии вложенных операторов if-else. Такая серия может быть записана "ступеньками".
Пример 6
if (hour >= 8 && hour < 17)
    rate = rate * 1.02;
else
    if (hour >= 17 && hour < 23)
       rate = rate * 1.01;
Пример 7
if (age < 13)
     printf ("ребенок");
else
 if (age < 21)
          printf ("юноша");
   else
     printf ("мужчина");
При использовании вложенных операторов if-else может возникнуть проблема связывания else. else связывается с ближайшим if.
Пример 8
if (c > '')
   if ( c >= '0'&& c <= '9')
       digits += 1;
   else
       count += 1;
else связывается со вторым if. Оператор, следующий за первым ключевым словом if, является оператором if-else. Переменная count является счетчиком нецифровых символов.
Пример 9
if (c > ' ')
{
if ( c >= '0' && c <= '9')
digits += 1;
}
else
count += 1;
else связывается с первым if. Оператор, следующий за первым ключевым словом if, является блоком. Переменная count является счетчиком управляющих символов.


Обзор средств ввода-вывода
В программах на языке Си ввод-вывод выполняется с использованием либо стандартной библиотеки ввода-вывода, либо системных вызовов. Системные вызовы в различных операционных системах отличаются и поэтому непереносимы. В отличие от них функции стандартной библиотеки ввода-вывода полностью переносимы.
Для уменьшения количества обращений к устройствам (например, к диску) стандартный ввод-вывод выполняется с буферизацией.
Буфер ввода-вывода — это временная область в основной памяти, которая содержит считываемые или записываемые данные.
Данные пересылаются на устройства (с устройств) большими порциями, размер которых определяется поименованной константой BUFSIZ.
Поток (stream) — обобщенный термин для обозначения источника или адресата данных, будь то файл или некоторое физическое устройство.
Шаги файлового ввода-вывода
Для файлового ввода-вывода посредством стандартных библиотечных функций ввода-вывода необходимо выполнить следующие шаги:
  1. Подключить файл заголовков, используя следующую директиву препроцессора #include <stdio.h>.
  2. Объявить указатель на FILE для каждого файла.
  3. Открыть файл, используя функцию fopen().
  4. Использовать функции чтения-записи стандартного ввода-вывода.
  5. Закрыть файл, используя функцию fclose().
В таблице 1 приводятся некоторые поименованные константы из файла stdio.h. Значения указанных констант зависят от реализации.
Таблица 1
Константа | Смысл константы
EOF | признак конца файла
NULL | 0
BUFSIZ | размер буфера ввода-вывода
FILE | структура, определенная с помощью typedef, содержащая информацию об открытом файле
stdin | указатель на FILE, открытый для стандартного ввода
stdout | указатель на FILE, открытый для стандартного вывода
stderr | указатель на FILE, открытый для стандартного вывода сообщений об ошибках


Организация файлов программы
Файлы заголовков
Директива препроцессора include влечет вставку в текущую позицию текста программы на языке Си копии указанного файла.
#include  <file.h>
#include "file.h"
Подобные файлы, называемые файлами заголовков или файлами вставок, обычно содержат директивы define для поименованных констант и макросов, объявления функций, внешних переменных и т.п., необходимые для нескольких исходных файлов. Преимущество подобного выделения в отдельный файл в том, что такие определения локализуются в общем месте. Несколько программ могут включать этот файл и быть уверенными в использовании одинаковой информации. Если необходимо изменение, его можно сделать в одном месте.
По соглашению, имена файлов-вставок имеют расширение .h , что означает файл заголовков (header), названных так потому, что директивы include обычно указываются в начале исходных файлов языка Си. Если имя файла заключено в угловые скобки < и >, препроцессор ищет файл в "стандартном месте". В операционных системах UNIX таким стандартным местом является каталог /usr/include. Если имя файла заключено в двойные кавычки ", используется стандартное соглашение операционной системы для доступа к файлу. В операционных системах UNIX может использоваться полное или относительное путевое имя.
Пример 1
#include "proj.h"
#include "/local/include/x33.h"
#include "../include/defs.h"
Файлы вставки могут быть вложены; файл заголовков может содержать директивы include.
Образец файла заголовков
#define TABLTSIZE 1000
#define SYSNAMELEN 20
#define MASK 010
#define CLEARLINE() while (getchar () != "\n');
#define MAX(A,B) (A>B ? A : B)
extern int status;
extern char sysname[];
extern double table[];
extern void print_err(int);
extern double std_dev(double);
typedef char BYTE;
В файле заголовков не должно быть ни определений функций (программ этих функций), ни определений внешних переменных, по которым выделяется память (например, int x). Иначе, если в нескольких файлах используется один и тот же файл заголовков, возникнут неприятности. При связывании этих файлов редактор связей может сообщить о "многократном определении идентификаторов".
В файле заголовков могут содержаться и другие директивы препроцессора и языка Си, например, операторы typedef, шаблоны структур и объединений (struct, union) и операторы enum.
Организация файлов программы
На рис. 1 приводится типичный способ организации файлов, составляющих программу. Файл заголовков projX.h содержит директивы препроцессора define, объявления внешних переменных и оператор переопределения типа typedef, используемые в файлах с расширением .c.
Файл defs.c определяет только внешние переменные, используемые несколькими файлами. Функции не должны включаться в этот файл.
Файл, содержащий функцию main(), часто называют main.c для простоты идентификации.
Функции, определенные в файлах calc.c и bufct1.c взаимосвязаны и являются составными частями программы .
Рис. 1. Пример организации файлов программы


Операции инкрементации и декрементации
Операции инкрементации и декрементации являются унарными операциями, то есть операциями, имеющими один операнд.
операнд++
++операнд
Операция инкрементации ++ добавляет к операнду единицу.
операнд--
--операнд
Операция декрементации -- вычитает из операнда единицу.
Операндом может быть именующее выражение, например, имя переменной.
Приоритет этих операций определяется таблицей приоритетов языка Си.
Следующие три строки увеличивают переменную x на 1:
x = x + 1;
++x;
x++;
Операции инкрементации и декрементации имеют префиксную (++x, --x ) и постфиксную (x++ , x--) форму записи. Они удобны при использовании в более сложных, чем приведенный пример, выражениях. При использовании префиксной формы записи операнд увеличивается или уменьшается сразу же.
Пример 1
x = 3;
y = ++x;
Переменная х сразу же увеличивается до 4 и это значение присваивается переменной y.
При использовании постфиксной форме записи операнд увеличивается или уменьшается после того, как он используется.
Пример 2
x = 3;
y = x++;
Переменной y присваивается значение 3, а затем переменная х увеличивается до 4.
Эти операции используются для сокращения текста программы и облегчения набора программы с клавиатуры, и для большинства компьютеров повышают эффективность работы программы. Они часто используются для увеличения или уменьшения индекса массива при переборе его элементов.
Но при работе с операциями инкрементации и декрементации надо быть осторожными: их операнд, являющийся именующим выражением, не следует использовать более одного раза в одном и том же выражении.
Пример 3
/*Пример плохого стиля:*/
array[num] = ++num;
Различные компиляторы могут интерпретировать это выражение по-разному и нельзя точно сказать, какой результат будет получен. Возьмите себе за правило: переменную, увеличиваемую или уменьшаемую в данном выражении, используйте в нем только один раз.
Пример 4
/*Более ясный стиль:*/
array[num] = num + 1;
++num;
Пример 5
i = 0;
while (i < size)
   {
    table[i] = 0;
    i = i+1;
   }
Пример 6
/*Эквивалентный примеру 5*/
i = 0;
while (i < size)
    table[i++] = 0;


Инициализация массива
Инициализация массива — это присваивание элементам массива начальных значений.
Внешние, статические внешние, статические внутренние и автоматические переменные составных типов данных, таких как массивы, могут быть проинициализированы при их объявлении. Любая автоматическая переменная или ее часть (например, элемент массива), которые не были инициализированы, остаются неопределенными. Любая внешняя или статическая переменная или часть ее (например, элемент массива), будучи не проинициализированными, устанавливаются в 0 или NULL (для указателей).
Пример 1
char alpha[10];
char beta[10] = {'a, 'b', 'c'};
char gamma[10] = "This is gamma";
char delta[5][10] ={"line 1",
                    "line 2",
                    "line 3"};
int num1[10];
int num2[10] = {2, 4, 6, 8, 10};
int num3[5][10] ={{0, 1, 2, 3, 4},
                   {2, 4, 6, 8, 10},
                   {3, 6, 8, 10, 12}};
int main ()
{
static char local[] = "local string";
char errmsg[] = "name", "too", "long";
int lengths[10] = {23, 56, 83};
...
}
Размеры строк gamma и local равны количеству символов в строке, увеличенному на 1 (для символа NULL — признака конца строки).
Автоматические переменные инициализируются при каждом входе в блок. Внешние и статические переменные инициализируются один раз, перед началом выполнения программы.


Использование указателей
Указатель должен быть объявлен соответствующим образом:
тип *идентификатор;
Пример 1
int num1 = 3, num2 = 6, *p;
В прим. 1 идентификатор p объявляется как указатель на целое. Это означает, что, как только он будет проинициализирован, его значением будет адрес целого. Целые и указатели на целое могут быть объявлены в одном операторе. Все указатели должны быть проинициализированы.
Операция косвенного доступа
*операнд
Операция косвенного доступа (*) является унарной операцией , результат которой — значение объекта, на который указывает операнд. Тип результата определяется типом указателя. Приоритет этой операции определяется по таблице приоритетов операций языка Си. В качестве операнда должно использоваться именующее выражение.
Пример 2
На рис. 1 приведена программа, для которой показано содержание переменных , располагаемых на стеке.
Пусть адреса переменных num1, num2 и указателя p соответственно равны 500, 504 и 510.
Рис. 1. Использование указателей
Вывод программы:
3
20
20
6
В прим. 2 сначала указателю p присваивается адрес переменной num1, после чего он "указывает на" num1. После этого доступ к значению переменной num1 может быть выполнен либо непосредственно через идентификатор (num1), либо косвенно через указатель(*p).


Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си
Цель работы
Знакомство с простейшими командами операционной системы Linux (ОС UNIX), разработка простой программы на языке Си, ее компиляция и запуск на выполнение в ОС Linux.
Теоретическая часть
Предполагается , что данные лабораторные работы будут выполняться в рамках операционной система Linux (ОС Linux).
ОС Linux (разновидность ОС UNIX) является мощной операционной системой, обладающей богатым набором команд для организации вычислительного процесса.
В данном курсе используется лишь незначительная часть команд ОС Linux, которые обычно применяют начинающие программисты. Эти команды описаны в документе Приложение. Некоторые команды ОС Linux. Полную информацию по всем средствам ОС Linux можно получить посредством справочной команды man.
Язык Си является функциональным языком, то есть основным понятием языка является понятие функции.
Изучите:
  • Минимальная программа на языке программирования Си
  • Общая структура программы на языке Си
  • Обзор средств ввода-вывода
  • Обзор функций форматированного ввода-вывода
  • Функция printf()
Практическая часть
Задание 1
В своей книге "Язык программирования Си" Деннис Ритчи (создатель языка Си) и Брайтел Керниган предлагают, чтобы первой написанной на языке Си программой была программа, печатающая "Hello, world!". Попробуйте написать такую программу. Она будет состоять из одной функции main(), которая будет вызывать стандартную функцию printf().
С помощью какого-либо текстового редактора создайте файл, содержащий исходный текст вашей программы на языке Си. Все имена исходных файлов с программами на языке Си должны иметь суффикс .с. Назовите ваш файл prog.с.
В таком виде разработанную вами программу понимаете вы, то есть программист, но не понимает компьютер. Компьютер понимает программу, представленную машинными командами, в которых вся информация закодирована определенным образом двоичными разрядами. Чтобы получить из исходного текста программы на языке Си выполнимую программу, состоящую из машинных команд, необходимо выполнить некоторую работу. Эту работу делает компилятор языка Си.
Изучите :
  • Шаги компиляции Си
Для того, чтобы получить из исходного файла на языке Си с именем prog.c выполнимую программу, необходимо выполнить компиляцию, используя следующую команду ОС Linux:
cc prog.с
Команда cc запускает работу компилятора, который читает исходный текст программы из файла prog.c, проверяет его на наличие синтаксических и семантических ошибок и обрабатывает.
Результатом работы компилятора при отсутствии ошибок на всех этапах компиляции является файл, имя которого "по умолчанию" a.out (выполнимая программа). При этом старое содержимое файла a.out будет уничтожено.
Для выполнения созданного файла надо выполнить команду:
./a.out
В ОС Linux процесс компиляции может быть остановлен после любого шага путем использования соответствующей опции (ключа) команды сс.
Изучите:
  • команду cc: Приложение. Некоторые команды ОС Linux
Выполните следующие команды:
  • Для изменения имени выполнимого файла используйте следующую команду: mv a.out newname
  • Для создания выполнимого файла с индивидуальным именем, например, newname, выполните следующую команду: cc prog.с -o newname
  • Выполните следующую команду:./newname Эта команда запускает выполнимый файл с именем newname, находящийся в текущем каталоге
Задание 2
Изучите уже готовую простую программу, которая вводит число, являющееся радиусом сферы, вычисляет объем этой сферы и выводит результат на экран монитора. Ваша задача понять назначение каждой строки программы.
  1. /*Программа для вычисления
  2. объема шара*/
  3. #include <stdio.h>
  4. int main()
  5. {
  6. float radius;
  7. void title(void), report(float);
  8. //
  9. title(); /* вызов функции */
  10. printf("Введите радиус:");
  11. scanf("%f", &radius);
  12. report(radius);
  13. }
  14. //
  15. void title(void)
  16. {
  17. printf("Эта программы вычисляет");
  18. printf(" объем сферы.\n");
  19. }
  20. //
  21. //
  22. void report(float r)
  23. {
  24. float volume, pi = 3.14159;
  25. volume = 4.0/3.0 *pi*r*r*r;
  26. printf("Объем сферы = %.2f\n", volume);
  27. }
Примечание 1
В данной программе строки пронумерованы только для того, чтобы на них ссылаться ниже по тексту. В программе на языке Си строки не нумеруются.
Примечание 2
В данной программе не осуществляется проверка корректного ввода данных и использования функции scanf().
Вывод программы:
Эта программы вычисляет объем сферы.
Введите радиус: 57.5
Объем сферы = 796327.63
Эта программа запрашивает у пользователя радиус, вычисляет объем сферы указанного радиуса и печатает результат. В программе используются три функции: main(), title(), report(). Выполнение программы всегда начинается с функции main().
Строки 1, 2: комментарий. Комментарии предназначены для читателя и компилятором игнорируются. Комментарии могут располагаться на той же строке, что и операция, которую они объясняют. Длинный комментарий может располагаться на отдельной строке или занимать несколько строк.
Строка 3: директива препроцессора, которая подключает заголовочный файл stdio.h. Этот файл является частью пакета, имеющегося в любом компиляторе Си и содержащего информацию о вводе-выводе (например, средства взаимодействия программы с терминалом). Для того чтобы получить компактный объектный код программы, подобные файлы включаются тогда, когда они необходимы.
Строка 4: main() — имя функции, круглые скобки указывают на то, что идентификатор main — это имя функции, а не имя переменной. Обычно в круглых скобках содержится информация, передаваемая этой функции. В нашем конкретном примере передаваемая информация отсутствует (точнее, не используется) и, следовательно, в скобках ничего не содержится.
Строка 5: открывающаяся фигурная скобка отмечает начало последовательности операторов, обозначающих тело или определение функции. Конец определения отмечается закрывающейся фигурной скобкой (строка 13). Фигурные скобки применяются также для того, чтобы объединить несколько операторов программы в сегмент или блок операторов.
Строка 6: объявляется локальная (автоматическая) переменная, имеющая имя или идентификатор radius.
Строка 7: объявления функций void title(void) и void report(float). Обе функции не возвращают значений, на что указывает ключевое слово void, стоящее перед именами функций. Функция title(void) — функция без параметров, на что указывает ключевое слово void, стоящее в круглых скобках после имени функции. Функция report(float) — ожидает в качестве аргумента число с плавающей точкой, на что указывает ключевое слово float, стоящее в круглых скобках после имени функции.
Строки 8, 14, 20, 21: пустая строка, а точнее пустой комментарий.
Строка 9: выполняется вызов функции title(), управление временно покидает функцию main(). Выполняются операторы функции title(), после чего управление возвращается в функцию main(). Строка 9 содержит комментарий, который поясняет действие.
Строки 10, 17, 18, 26: выполняется вызов библиотечной функции printf(), доступной для общего пользования. Эта функция выводит сообщение на стандартное устройство вывода (обычно на экран монитора) в соответствии с форматной строкой.
Строка 11: выполняется вызов библиотечной функции scanf(), доступной для общего пользования. Она вводит данные со стандартного устройства ввода (обычно это клавиатура). Здесь введенное пользователем число с плавающей точкой запоминается в переменную radius. Для этого используется операция & (обращение по адресу).
Строка 12: выполняется вызов функции report(). В качества фактического параметра функции передается введенное пользователем значение радиуса.
Строка 13: когда функция main() выходит на закрывающую блок фигурную скобку, выполнение программы завершается.
Строка 15: определение функции title().
Строки 16, 19: открывающая и закрывающая фигурные скобки, которые ограничивают блок операторов, составляющих тело функции title(). При выходе на закрывающую фигурную скобку происходит возврат в вызывающую функцию, в данном случае в функцию main().
Строка 22: определение функции report(). Эта функция имеет один параметр, являющийся числом с плавающей точкой. В скобках объявлены формальные параметры функции.
Строки 23, 27: открывающая и закрывающая фигурные скобки, которые ограничивают блок операторов, составляющих тело функции report(). При выходе на закрывающую фигурную скобку происходит возврат в вызывающую функцию, в данном случае в функцию main().
Строка 24: объявление локальных (автоматических) переменных volume и pi, доступных только внутри функции report().
Строка 25: вычисляется объем сферы по формуле, при этом используются арифметические операции языка Си и операция присваивания. Результат присваивается переменной volume.
Строка 26:выполняется вызов библиотечной функции printf(), которая печатает значение переменной volume с двумя знаками после десятичной точки.
Порядок выполнения работы
Примечание 3
Данный порядок определен для лаборатории САПР кафедры РК6 МГТУ им. Н.Э.Баумана
  1. Загрузить операционную систему Linux, определив пароль следующим образом (пароль набирается "вслепую", то есть не отображается на экране): password: demos
  2. После того, как будет загружена ОС Linux, начать сеанс работы пользователя, введя имя и пароль пользователя:
    • login: api
    • password: plus-plus
  3. В результате будет загружена оболочка Shell ОС Linux. Находясь в оболочке Shell, перейти в рабочий каталог, выполнив команду:
    • cd work
  4. В оболочке Shell запустить оконную систему X-Window, выполнив команду: startx
  5. Находясь в оконной системе X-Window (голубой экран), открыть три окна: командное окно (Shell), окно редактора (Edit), инструментальное окно (Xkbd):
    • Нажав на голубом фоне правую кнопку мыши, выбрать из появившегося меню режим Shell, в котором, в свою очередь, выбрать терминал с русской кодировкой (КОИ-8).
    • Нажав на голубом фоне правую кнопку мыши, выбрать из появившегося меню режим Edit, в котором, в свою очередь, выбрать редактор с русской кодировкой (КОИ-8).
    • Нажав на голубом фоне правую кнопку мыши, выбрать из появившегося меню режим Tools, в котором, в свою очередь, выбрать Xkbd. С помощью этого инструмента можно выбрать русский (KOИ-8) или английский (ASCII) язык при редактировании текста программы.
  6. В окне редактора Edit набрать текст программы задания 1, сохранив его в файле с именем prog.c.
  7. Выполнить компиляцию программы, используя команду:
    • cc prog.с
  8. Исправить все синтаксические и семантические ошибки, при этом записывая в отчете саму ошибку, ее причину и способ устранения.
  9. После того, как компиляция выполнится безошибочно, выполнить программу, используя следующую команду:
    • ./a.out
  10. Повторить пункты 6-9 для второго задания, сохраняя программу в файле lab1.с.
  11. Выполнить поэтапное получение выполнимого файла, используя различные ключи команды сс (-E, -S, -c).
  12. Посмотреть результаты каждого шага компиляции с помощью команд more или cat.
  13. Выполнить команду сс с ключом -о.
  14. Посмотреть содержание текущего каталога.
  15. Скопировать содержимое файла lab1.с в файл lab1_new.с.
  16. Переименовать файл lab1_new.с в lab1_old.с.
  17. Вывести на экран содержимое файла lab1_old.с с пронумерованными строками.
  18. Посмотреть, в каком каталоге вы находитесь в данный момент.
  19. Объяснить полученные результаты.
  20. Закончить работу в ОС Linux:
  • закрыть все окна, выбрав режим exit;
  • завершить работу оконной системы X-Window, выбрав режим exit;
  • закончить сеанс работы, используя команду logout;
  • завершить работу ОС Linux, используя команду shutdown.
Контрольные вопросы
  1. Из чего состоит простейшая программа на языке программирования Си?
  2. Какова общая структура программы на языке Си?
  3. Что такое функция?
  4. Что такое идентификатор?
  5. Для чего нужны комментарии в программе? Как их использовать?
  6. Какой вид должны иметь имена исходных файлов программ на языке Си?
  7. Какая команда ОС Linux используется для компиляции исходного текста программы на языке Си?
  8. Какие ключи имеет эта команда? Для чего они используются?
  9. Какое имя будет присвоено выполнимому файлу после компиляции?
  10. С помощью какой команды можно вывести на экран монитора содержимое файла с пронумерованными строками?
  11. Как получить выполнимый файл с индивидуальным именем?
  12. Как скопировать файл?
  13. Как удалить файл?
  14. Как создать новый каталог?
  15. Как сделать этот каталог текущим?
  16. Как посмотреть содержание каталога?
  17. Как посмотреть документацию по определенной команде?
Содержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Текст программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы
  • Выводы
Дополнительные задания
Примечание 4
В заданиях 1-3 не используйте оператры циклов и условные операторы.
  1. Разработайте алгоритм и напишите программу, которая последовательно читает из стандартного потока ввода пять символов и выводит их в стандартный вывод (используйте стандартные функции getchar() putchar()). Откомпилирйте программу и проверьте ее, вводя пять или более символов в строке. затем попробуйте выполнить программу, вводя по одному символу в строке. Объясните результаты.
  2. Разработайте алгоритм решения следующей задачи, а затем напишите программу. Пусть расход бензина определен следующим образом: автомобиль "Быстрая лань" — 10 литров на 100 км, автомобиль "Ломовая лошадь" — 20 литров на 100 км, автомобиль "Ну-ка, догони" — 15 литров на 100 км. Если бензин стоит в среднем 20 рублей за литр, и некто проезжает в год 25000 км, определите расходы на бензин для каждого автомобиля. Сколько рублей будет сэкономлено за шесть лет, если ездить на "Быстрой лани" по сравнению с марками "Ломовая лошадь" и "Ну-ка, догони".
  3. Разработайте алгоритм решения следующей задачи, а затем напишите программу. Некоторый банк выплачивает 7% годовых. Какова будет сумма вклада через пять лет после вложения в банк 2000 рублей. Какова будет сумма вклада, если в течении пяти лет ежегодно вносить в банк по 2000 рублей.
  4. Если вы имеете навыки программирования на языке Си, то измените программу так, чтобы она делала проверку корректного ввода данных и использования функции scanf(). Для этого изучите:
    • Функция scanf()
    • Оператор цикла while


Лабораторная работа 2. Целые типы данных в языке программирования Си
Цель работы
Изучение целых типов данных, реализованных в языке программирования Си. Определение размера памяти, выделяемой под каждый тип данных, диапазона изменения целых чисел, знакомство с ситуацией переполнения разрядной сетки компьютера.
Теоретическая часть
Каждая область памяти компьютера представляет собой набор двоичных цифр (битов или разрядов), то есть нулей и единиц, которые интерпретируются компьютером в соответствии с командами выполняемой программы.
Программы используют память для хранения чисел (арифметических значений), закодированных символов (букв алфавита, цифр, знаков пунктуации и т.д.) и другой информации. Полученный после компиляции программы файл содержит двоичное представление как команд, так и данных. Перед выполнением программы вся эта информация помещается в оперативную память компьютера.
Необходимо понимать разницу между числами и их представлением. Число — это абстрактное выражение количества.
Пример 1
  • количество элементов в паре — 2
  • количество квадратов на шахматной доске — 64
  • число дней в году — 365.25
Представление числа — последовательность символов, используемая для обозначения числа в соответствии с некоторыми соглашениями. В практической жизни для представления чисел обычно используется десятичная позиционная система счисления. Но десятичное представление неудобно для представления данных в компьютере.
Изучите:
  • Выбор системы счисления для представления числовой информации
  • Формы представления числовой информации
  • Перевод числовой информации из одной ПСС в другую
  • Основные типы данных
  • Арифметические операции
  • Константы
  • Поименованные константы
  • Функция printf()
Операция sizeof
В языке Си имеется встроенная операция sizeof(), которая позволяет определить размер объекта. Объект задается в скобках. Результат операции — размер объекта в байтах. Эта операция является унарной и имеет такой же приоритет, как и прочие унарные операции.
Практическая часть
Изучите программу:
/*Лабораторная работа 2*/
#include <stdio.h>
#include <values.h>
#define MAX1 (127) /*определение поименованной константы*/
int main()
{
  void test(void);   /*объявление прототипа функции test*/
  void size(void);   /*объявление прототипа функции size*/

  size();            /*вызоф функции size*/
  test();            /*вызоф функции test*/
}

void size(void)/*определение прототипа функции size*/
/*выводит на стандартный вывод размеры данных целого типа (в байтах)*/
  {
   printf("Данные типа int занимают %d байта.\n", sizeof(int));
   printf("Данные типа short занимают %d байта.\n", sizeof(short));
   printf("Данные типа char занимают %d байт.\n", sizeof(char));
   printf("Данные типа long занимают %d байта.\n", sizeof(long));
  }

void test(void)/*определение прототипа функции test*/
/*К максимально возможному целому числу прибавляется 1,
 возникает переполнение разрядной сетки*/
  {
  char a = MAX1, a1, a2; /* объявления переменных целого типа*/
  short b, b1, b2;
  int с, c1, c2;
  long d, d1, d2;

  b = MAXSHORT;
  c = MAXINT;
  d = MAXLONG;
  a1 = a + 1;
  a2 = a1 + 1;
  b1 = b + 1;
  b2 = b1 + 1;
  c1 = c + 1;
  c2 = c1 + 1;
  d1 = d + 1;
  d2 = d1 + 1;
  printf("%d %d %d\n", a, a1, a2);
  printf("%с %с %с\n", a, a1, a2);
  printf("%x %x %x\n", a, a1, a2);
  printf("%hd %hd %hd\n", b, b1, b2);
  printf("%hx %hx %hx\n", b, b1, b2);
  printf("%d %d %d\n", c, c1, c2);
  printf("%x %x %x\n", c, c1, c2);
  printf("%ld %ld %ld\n", d, d1, d2);
  printf("%lx %lx %lx\n", d, d1, d2);
}
Примечание 1
Использование специкатора %d и %x в функции printf() по отношению к переменной типа char не совсем корректно, так как на экран монитора будет выводится значение переменной, расширенной до размера int.
  • Войдите в ОС Linux так, как вы это делели в документе Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си.
  1. Наберите текст данной программы в текстовом редакторе, сохранив под именем lab2.c.
  2. Получите выполнимый файл, используя команду OC Linux:
    • cc lab2.c -o lab2
  3. Выполните программу, проанализируйте результаты.
  4. Выясните, как обрабатывается переполнение разрядной сетки.
  5. Повторите команды OC Linux, описанные в документе Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си.
Контрольные вопросы
  1. Что такое позиционные системы счисления?
  2. Почему работа компьютера основана на двоичной системе счисления?
  3. Как перевести двоичное число в восьмеричное и шестнадцатеричное представление?
  4. Что такое прямой, обратный и дополнительный коды целого числа?
  5. Каков формат представления целых чисел в компьютере?
  6. Какие форматы целых чисел существуют в языке Си?
  7. Как определить размер максимального целого, представленного в определенном формате?
  8. Что такое переполнение разрядной сетки компьютера?
Cодержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Текст программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы
  • Выводы


Лабораторная работа 3. Арифметические операции языка программирования Си
Цель работы
Изучение арифметических операций языка программирования Си, операции присваивания, инкрементации и декрементации.
Теоретическая часть
Язык программирования Си предоставляет богатый набор операций, некоторые из которых являются арифметическими.
Изучите:
  • Приоритеты операций
  • Арифметические операции
  • Операция присваивания
  • Операции инкрементации и декрементации
  • Операторы
  • Преобразование данных
Практическая часть
Изучите программу:
/*Лабораторная работа 3*/
/*Операции, преобразование типов данных*/

#include <stdio.h>
int main()
{
/*Это объявления функций*/
  void test_add(void);
  void test_devide(void);
  void test_convert(void);
  void test_increment(void);

/*Это вызовы функций*/
  test_add();
  test_devide();
  test_convert();
  test_increment();
}

/* Далее идут определения функций*/
void test_add(void)
{
/*выполнение операции сложения*/
  printf("5 + 3 = %d\n", 5 + 3);
  printf("5. + 3 = %d\n", 5. + 3);
  printf("5. + 3 = %f\n", 5. + 3);
  printf("(int)5.+3 = %d\n",(int)5.+3);
}

void test_devide (void)
{
/*выполнение операции деления*/
  printf("5/3 = %d\n", 5/3);
  printf("5/-3 = %d\n", 5/-3);
  printf("5./3 = %d\n", 5./3);
  printf("5./3 = %f\n", 5./3);
  printf("(int)5./3 = %d\n",(int)5./3);
}

void test_convert (void)
{
  int x, y;

/*преобразование типов данных*/
  x =(2 + 3) * 6;
  printf("x =%d\n", x);
  x = (12 + 6)/2 * 3;
  printf("x =%d\n", x);
  y = x = (3 + 2) / 4;
  printf("x =%d y=%d\n", x, y);
  y = 3 + 2 * (x = 7/2);
  printf("x =%d y=%d\n", x, y);
  x = (int)(3.8 + 3.3);
  printf("x =%d\n", x);
  x = (int)3.8 + (int)3.3;
  printf("x =%d\n", x);
}

void test_increment(void)
{
  int a, b;
  float x, y;
  a = b =1;
  x = y = 1.;
  printf("a=%c (%d)\|n ", a, a);
  printf("a++ =%d \n", a++);
  printf("a=%d a++ =%d \n", a, a++);
  printf("b=%c (%d) \n ", b, b);
  printf("++b=%d \n ", ++b);
  printf("b=%d ++b =%d \n", b, ++b);
  printf("x=%f x++ =%f \n", x, x++);
  printf("y=%f ++y =%f \n", y, ++y);
}
  • Войдите в ОС Linux так, как вы это делали в лабораторной работе 1: Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си.
  1. Наберите текст данной программы в текстовом редакторе, сохранив под именем lab3.c.
  2. Получите выполнимый файл, используя команду OC Linux:
    • cc lab3.c -o lab3
  3. Выполните программу, проанализируйте и объясните результаты.
  4. Выясните, как влияет спецификатор функции printf() на представление данных при выводе. Объясните каждую строчку в выводе программы.
  5. Повторите команды OC Linux, описанные в лабораторной работе 1: Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си.
Контрольные вопросы
  1. Чем отличаются в языке Си выражение и оператор?
  2. Что такое оператор? Какие в языке Си существуют операторы?
  3. Как выполняются арифметические операции? Что такое приоритет и ассоциативность?
  4. Как выполняются операции деления и деления по модулю?
  5. Как выполняется операция присваивания?
  6. Что такое преобразование типа? Когда оно выполняется?
Содержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Текст программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы (пояснения для каждой строки вывода программы)
  • Выводы по работе


Лабораторная работа 4. Управляющие операторы if и while
Цель работы
Изучение операций отношений, логических операций, составных операций присваивания, управляющего оператора if, оператора цикла while, а также стандартных функций ввода-вывода getchar() и putchar().
Теоретическая часть
Изучите:
  • Операции отношений
  • Логические операции
  • Составные операции присваивания
  • Управляющий оператор if
  • Оператор цикла while
  • Функции getchar() и putchar()
Практическая часть
  1. Разработайте программу, которая вводит со стандартного потока ввода множество символов, ограниченное вводом признака конца файла (EOF). Определите и выведите на экран число введенных символов и строк, а также выведите на экран монитора эти символы и строки. Используйте в программе только управляющий оператор if. Выполните эту программу, вводя различные комбинации символов, в том числе символов табуляции, пробела, <Enter>, <CTRL>+<d>. В отчете зафиксируйте результаты и объясните их.
  2. Измените программу, заменив там, где это разумно, управляющий оператор if на оператор цикла while. Повторите эксперимент, объясните результаты.
  3. Внесите изменения в программу так, чтобы она считала количество введенных слов. Слово — это множество символов, не содержащее символы-разделители (пробел, табуляция, переход на новую строку).
  4. Повторите команды OC Linux, описанные в лабораторной работе 1: Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си
Контрольные вопросы
  1. Что такое операции отношений?
  2. Какое значение в языке программирования Си принимается за ложь, а какое за истину?
  3. Какие логические операции есть в языке программирования Си?
  4. Как работает управляющий оператор if?
  5. Как работает управляющий оператор цикла while?
  6. Для чего и как используется функция getchar()?
  7. Для чего и как используется функция putchar()?
Содержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Текст программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы
  • Выводы по работе


Лабораторная работа 5. Управляющий оператор выбора switch языка программирования Си
Цель работы
Изучение управляющего оператора switch языка программирования Си , сравнение оператора switch с оператором if.
Теоретическая часть
Изучите:
Оператор switch
Практическая часть
Разработайте два варианта следующей программы (1-ый вариант — с использованием только управляющего оператора if, 2-ой вариант — с использованием управляющего оператора switch):
  • Задать (ввести) целое положительное число в промежутке от 1 до 100. Предусмотреть обработку ошибок при вводе. В соответствии с этим числом сформировать запись следующего вида: "Ваш возраст 1 год", "Ваш возраст 22 года", "Ваш возраст 15 лет" и т.д.
Пример 1
Возможный пример структуры функции main():
#include <stdio.h>
int main()
{ . . .
  input_number(...);
  /* ввод и проверка числа */
  form_string (...);
  /* формирование записи */
  output_string (...);
  /* вывод записи */
  . . .
}
  • Выполните отладку первого варианта, сохранив исходный текст программы в файле lab5_if.c. Выполнимый файл должен иметь имя lab5_if.
  • Выполните отладку второго варианта, сохранив исходный текст программы в файле lab5_sw.c. Выполнимый файл должен иметь имя lab5_sw.
  • Сравните два варианта программы, дайте объяснения.
  • Повторите команды OC Linux, описанные в документе Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си
Контрольные вопросы
  1. Для чего используется оператор switch?
  2. Что можно использовать в качестве метки в операторе switch?
  3. Какую роль выполняет оператор break?
  4. Что такое "проваливание" выполнения оператора switch?
  5. Для чего используется метка default?
  6. В каком случае целесообразно использовать оператор switch, а в каком оператор if?
Содержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Тексты двух вариантов программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы
  • Выводы по работе


Лабораторная работа 6. Массивы в языке программирования Си. Оператор цикла for.
Цель работы
Изучение организации массивов в языке программирования Си, способов обработки массивов, цикла for, операции продолжения ("запятая").
Теоретическая часть
Изучите:
  • Оператор цикла for
  • Операция продолжения ("запятая")
  • Массивы
  • Доступ к элементам массива
  • Массивы символов
  • Обработка массивов
Стандартные функции для получения "случайного" числа
Объявление функции rand():
int rand(void)
Функция rand() возвращает "псевдослучайное" число в диапазоне от 0 до числа, определенного поименованной константой RAND_MAX (RAND_MAX не меньше 32767).
Объявление функции srand():
void srand(unsigned int seed)
Функция srand() использует параметр seed в качестве "семени" для новой последовательности "псевдослучайных" чисел. Изначально параметр seed равен 1. Эту функцию достаточно использовать в программе один раз, но до первого вызова функции rand().
Объявления этих функций находятся в заголовочном файле stdlib.h.
В качестве параметра функции srand() удобно использовать значение, возвращаемое функцией time().
Объявление функции time():
time_t time(time_t *tp)
Функция time() возвращает текущее календарное время (т.е. время, прошедшее после определенной даты, обычно после 0 ч 00 мин с GSM 1-го января 1970 г.) или -1, если время неизвестно. Если tp не равно NULL, то возвращаемое значение записывается и по адресу *tp. Объявление этой функции находится в заголовочном файле time.h.
Пример 1
#include <stdlib.h>
#include <time.h>
...
srand (time(NULL));
/* устанавливает семя для новой
последовательности псевдослучайных чисел*/
i = rand()%5;
/* результат выражения - случайное число от 0 до 4*/
Практическая часть
  • Спроектировать и разработать следующую программу: задана итоговая таблица чемпионата по футболу, в котором участвовало 8 команд. Элементы таблицы, принадлежащие главной диагонали, равны нулю, а все остальные элементы равны 0, 1 или 3 (т.е. числу очков, набранных в игре: 0 — поражение, 1 — ничья, 3 — победа). Определить и вывести на экран количество очков, набранных каждой командой, максимальное число очков, все команды, набравшие максимальное число очков. Для заполнения таблицы используйте стандартные функции для получения "случайного" числа.
Пример 2
Возможный вариант функции main() программы:
/* Лабораторная работа 6*/
/* Чемпионат по футболу */
#include <stdio.h>
. . .
main()
{
  int tab[8][8];
. . .
  diag(tab);     /* обнуление элементов главной диагонали таблицы */
  up(tab);       /* заполнение элементов, лежащих выше главной диагонали,
                    значения элементов определяются с помощью функции rand() */
  down(tab);     /* заполнение элементов, лежащих ниже главной диагонали */
  print_tab(tab);/* печать массива */
  max_tab(tab);  /* поиск команды, набравшей максимальное число очков.
                    Эта функция, в свою очередь, вызывает другие функции,
                    которые определяют сумму по строкам таблицы,
                    максимальное значение из этих сумм,
                    печатают максимальное число очков и номера
                    команд, набравших это число очков */
}
  • Выполните отладку программы, сохранив исходный текст программы в файле lab6.c. Выполнимый файл должен иметь имя lab6.
  • Повторите команды OC Linux, описанные в документе Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си.
Контрольные вопросы
  1. Чем отличается цикл for от цикла while?
  2. Что такое массив?
  3. Как в языке программирования Си определяется строка символов?
  4. Как осуществляется доступ к элементам массива?
  5. Как в языке программирования Си обр
  6. Как осуществляется доступ к элементам массива?
  7. Как в языке программирования Си обрабатывается выход за границы массива?
  8. Как можно передавать в функцию массив?
Содержание отчета
  • Название работы
  • Цель работы
  • Ответы на контрольные вопросы
  • Текст программы
  • Результаты отладки (ошибки, их причины и способы устранения)
  • Результаты работы программы
  • Выводы по работе


Библиотеки в ОС UNIX
Библиотека - это набор функций, которые могут быть использованы во многих программах. Компиляторы языка Си оснащены одной или более библиотеками. Кроме того, программист сам может создавать собственные библиотеки. Обычно библиотека - это файл, содержащий объектный код различных функций, представленный в специальном формате.
Ниже приводится список библиотечных функций, которые обычно поставляются вместе с компилятором Си:
  • Ввод-вывод
  • Обработка строк
  • Обработка символов
  • Распределение памяти
  • Функции общего назначения
  • Математические функции
Все эти функции поставляются с компилятором Си ОС UNIX. Стандарт ANSI языка Си языка Си специфицирует, что эти и другие функции должны быть в составе компилятора Си. Рекомендуемый список основан на составе библиотек, поставляемых вместе с компилятором Си ОС UNIX.
Рис. 1. Библиотеки в ОС UNIX
На рис.1 представлен сокращенный список библиотек и сопровождающих файлов, поставляемых вместе с компилятором Си ОС UNIX. Хотя места хранения этих файлов и их форматы зависят от версии системы UNIX, общая организация библиотеки такова же, как и в среде других операционных систем.
В ОС UNIX стандартные библиотеки находятся в каталоге in/usr/ccs/lib. Особый интерес представляет файл /usr/ccs/lib/libc.so, называемый "Стандартная библиотека Си". Он содержит объектный код стандартных функций ввода-вывода, функций обработки строк и символов, функций распределения памяти и функций общего назначения. Указанный файл используется редактором связей для автоматического поиска идентификатора, неопределенного в используемой программе. Например, если программа использует функцию printf(), редактор связей выполняет поиск определения printf() в каталоге /usr/ccs/lib/libc.so (разделяемой объектной библиотеке). Определения printf() отображается в виртуальное адресное пространство программы во время выполнения. Это называется динамическим связыванием. Оно позволяет связывать разделяемые объекты с исполнимым кодом во время выполнения программ. Это позволяет обеспечить совместное использование несколькими программами размещенных в памяти библиотечных функций.
В руководстве по некоторым функциям указано, что в использующие их программы должен быть вставлен файл заголовков. Например, файл stdio.h - для функций ввода-вывода, string.h - для функций обработки строк. В ОС UNIX эти файлы находятся в каталоге /usr/include.
В число других библиотек, поставляемых вместе с компилятором Си ОС UNIX, входят математическая библиотека /usr/ccs/lib/libm.a и библиотека функций работы с экраном /usr/ccs/lib/libcurses.a. Библиотека функций работы с экраном - набор не зависящих от типа монитора функций для управления экраном, например, очистки экрана, позиционирования курсора, подчеркивания, выделения цветом и т.д. Если используются функции из библиотеки, отличной от стандартная библиотека Си, необходимо сообщить об этом редактору связей путем указания в командной строке флага -l, который должен следовать после имени исходного файла.
Пример 1
$cc -Xa prog.c -lm используются математические функции
$cc -Xa prog.c -lcurses используются функции работы с экраном


Логические операции
В языке Си существуют следующие логические операции: логическое умножение И — коньюнкция (&&), логическое сложение ИЛИ — дизьюнкция(||), логическое отрицание НЕ (!).
Приоритеты логических операций определяются таблицей приоритетов операций языка Си.
Значением логического выражения является 0 (ложь) или 1 (истина). Значения логических операций определяются в соответствии с таблицами истинности этих операций.
Таблица 1
операнд 1 | операнд 2 | коньюнкция
ложь | ложь | ложь
ложь | истина | ложь
истина | ложь | ложь
истина | истина | истина

Таблица 2
операнд 1 | операнд 2 | дизьюнкция
ложь | ложь | ложь
ложь | истина | истина
истина | ложь | истина
истина | истина | истина

Таблица 3
операнд | отрицание
ложь | истина
истина | ложь

Пример 1
Логическое И
(x >= 1) && (x <= 500)
Пример 2
Логическое ИЛИ
(input == 'q') || (input == 'Q')
Пример 3
Логическое отрицание
!(x>y && y>z)
Для эффективности в языке Си вычисляется логическое выражение слева направо и прекращает вычисление, когда истинность значения определена.
Пример 4
(x<y) && (y<z)
Это выражение истинно, если истинно как выражение (x<y), так и выражение (y<z). Если (x<y) ложно, то все выражение должно быть ложно и выражение (y<z) игнорируется.
Пример 5
(x<y) || (y<z)
Это выражение истинно, если истинно либо выражение (x<y), либо выражение (y<z), или если истинны оба выражения. Если (x<y) истинно, все выражение истинно, так что в этом случае не вычисляется истинность выражения (y<z).
Унарная операция ! (НЕ) обращает логическое значение. Если (x< y) ложно, то !(x< y) истинно.


Макросы
Макрос — это короткая процедура, у которой могут быть аргументы.
Макрос определяется с помощью директивы препроцессора #define.
Определение макроса похоже на определение поименованной константы.
#define идентификатор(аргумент[,аргумент]...) строка-шаблон
В скобках непосредственно после идентификатора могут быть указаны имена аргументов. Между идентификатором и символом ( не должно быть символов промежутка. При расширении макроса препроцессор осуществляет вставку строк. Препроцессор ничего не знает о синтаксисе языка Си или логике программы. Поэтому такие подстановки могут быть источниками синтаксических ошибок.
Пример 1
#define SQUARE(X) ((X) * (X))
#define MAX(A,B) ((A) > (B) ? (A) : (B))
int main()
{
  int int1, int2;
  int1 = SQUARE(3);
  int2 = SQUARE(int1 + 1);
  printf("max = %d\n", MAX(int1,int2));
}
Результат препроцессирования:
int main()
{
  int int1, int2;
  int1 = ((3) * (3));
  int2 = ((int1 + 1) * (int1 + 1));
  printf ("max = %d\n", ((int1) > (int2) ? (int1) : (int2)));
}
Вывод программы:
max = 100
Если в прим. 1 в определении SQUARE не будут использованы скобки, то переменной int2 будет присвоено другое значение:
int2 = int1 + 1 * int1 + 1;
Для просмотра результата препроцессирования надо использовать команду компиляции cc с опцией -P.
Макрос может использовать другие ранее определенные макросы. Многие препроцессоры Си, включая препроцессор системы ОС UNIX, не допускают использования рекурсивных макросов.
Иногда удобно определять макрос так, чтобы он был блоком операторов.
Одно из преимуществ блока — возможность определить локальные переменные. Если только макрос содержит более одного оператора, эти операторы должны быть включены в блок, иначе возможны неприятности.
Пример 2
/* Эти операторы следует заключить в блок */
#define DOIT(A, B, C) A = D * 2; \
                      B = C * 2;
void f(void)
{
   int i, j, k;
   if (i < j)
       DOIT(i, j, k) /* Здесь после обработки препроцессором будет синтаксическая ошибка */
   else
      printf("Something \n");
Хотя этот фрагмент программы и выглядит корректно, однако после расширения препроцессором компилятор обнаружит ошибку. Синтаксис оператора if-else требует единственного оператора между if и else. В такой ситуации использование блока в макросе устранит проблему.
Стандарт ANSI языка Си запрещает макроподстановки в строковых литералах.
Операция #
Операция #- операция для создания строки из аргумента макроса.
Пример 3
Определение макроса:
#define string(x) #x
Использование макроса:
string(hello)
Результат подстановки:
"hello"
Пример 4
#define SWAP(A,B) {int temp; \
                    temp = A;\
                    A = B;\
                    B =temp; \
                    }
#define PRINT(A,B) \
            printf(#A": %d, "#B": %d\n", A, B)
int main( )
{
  int num1 = 30, num2 = 90;
  PRINT(num1, num2);
  if (num2 > num1)
     SWAP(num1, num2);
  PRINT(num1, num2);
}
Вывод программы:
num1: 30, num2: 90
num1: 30, num2: 90
Операция ##
Операция ## — операция для объединения лексем в определении макросов одну лексему .
Пример 5
#define WheatBread 0
#define RyeBread 1
#define WhiteBread 2
#define PumpernickelBread 3
#define Bread(x) x ## Bread
int main ()
{
    printf("Value of WheatBread is %d\n", Bread(Wheat));
    printf("Value of RyeBread is %d\n", Bread(Rye));
    printf("Value of WhiteBread is %d\n", Bread(White));
}
Вывод:
Value of WheatBread is 0
Value of RyeBread is 1
Value of WhiteBread is 2
Вызов макроса Bread(Rye) расширяется сначала в Rye##Bread, потом – в одно слово RyeBread, заменяемое затем константой 1.
Сравнение функций и макросов
Большим преимуществом макросов является то, что они быстрее функций. Потери времени при вызове функции включают в себя время на сохранение аргументов и регистров на стеке и восстановление их при возврате. При вызове макроса этого не происходит.
Однако, если макрос используется много раз, исполняемая программа будет иметь больший размер, чем при использовании функций.
Преимуществом функций является возможность возвратить результат при помощи оператора return. В макросе оператор return вызовет немедленный выход из функции, в которой вызван макрос.
Кроме того, функции могут быть рекурсивными. Большая же часть препроцессоров Си (в том числе и в операционной системе UNIX) запрещают рекурсивные макросы.
К тому же макросы сложнее для отладки, чем функции. Отлаживая макрос, надо смотреть на результат работы препроцессора. Для этого в операционной системе UNIX можно воспользоваться следующими командами:
$cc -Xa -P proc.c
$cat proc.i


Передача в функцию многомерного массива
Многомерный массив — массив, имеющий более одного измерения. Число измерений массива в языке Си не ограничено.
В функцию можно передавать лишь адрес массива, а не копию всего массива. Тем не менее, в функции для доступа к элементам массива можно использовать индексы.
Пример 1
/* Программа для чтения текстового файла и выдачи на печать лишь тех слов,
 которые начинаются с прописной буквы */
#include <stdio.h>
int main()
{
  void print_caps(int, const char [][20]);
  int amount_read = 0;
  char input[1000][20];         /* Это массив из 1000 массивов из 20 символов */
  while (scanf("%s", input[amount_read]) == 1)
     amount_read++;               /* input[amount_read] - один из массивов из 20 символов */
  print_caps(amount_read, input);
}
/* print_caps печатает лишь те слова, которые начинаются с прописной буквы */
void print_caps(int n_words, const char words[][20])
/* words - Это массив из произвольного числа массивов из 20 символов */
{
   int i=0;
   while (i < n_words)
      {
      if (words[i][0] <= 'Z' && words[i][0] >= 'A')
                                   /* words[i][0] - 0-ой символ i-го массива из 20 символов */
         printf("%s\n", words[i]); /* words[i] - Один из массивов из 20 символов*/
      i++;
      }
 }
Значением операции доступа к элементу массива words[i][j] будет адрес, вычисленный по следующим формулам:

Лабораторная работа 6. Массивы в языке программирования Си. Оператор цикла for.


Массивы указателей
Массив указателей - это массив адресов.
Массив указателей объявляется также, как и прочие массивы:
тип *идентификатор[целочисленное выражение]
Пример 1
char buffer[30001];
/* Текстовый буфер, заканчивающийся \0*/
char *line_num[2001];
/*Максимальное число строк - 2000*/

...

void number_lines(void)
  {
  int i;
  char *p;
  line_num[0]=buffer;
  for(p=buffer, i=1; *p != '\0'; p++)
     if(*p == '\n')
        if(*(p+1) != '\0')
           line_num[i++] = p+1;
  line_num[i] = NULL;
  }
Предположим, что массив buffer уже заполнен текстом. Функция number_lines() занесет начальные адреса каждой из находящихся в буфере строк в массив символьных указателей line_num, то есть line_num[0] будет указывать на первую строку, line_num[1] будет указывать на вторую строку и т.д. Список указателей на строки завершается нулем.
Инициализация массива указателей при объявлении
Пример 2
/*пример сканера для идентификации команд */
#include <stdio.h>
#include <string.h>
int is_keyword(char *);
char *keyword[] = {
                   "append",
                   "find",
                   "list",
                   "remove",
                   "replace",
                   "substitute",
                   (char *)NULL
                   };
/*возвращает индекс команды, иначе -1*/
int is_keyword(char *)
  {
  int i;
  for (i=0; keyword[i] != NULL; i++)
     if(strcmp(str, keyword[i]) == 0)
        return i;
  return -1;
  }
Строковые константы располагаются в области данных. Их начальные адреса будут использоваться при инициалицации массива указателей. Массив указателей использует меньше памяти, чем требовалось бы для двумерного массива символов. При использовании двумерного массива символов пришлось бы найти самое длинное ключевое слово и указать его длину, увеличенную на 1 за счет символа - признака конца строки, в качестве второй размерности массива. Например, имеется 30 ключевых слов и самое длинное содержит 7 символов. Тогда двумерный массив должен быть объявлен следующим образом:
char keyword[30][8];
В итоге появилось бы много неиспользуемых байтов, так как многие ключевые слова содержат менее 7 символов .


Массивы символов
Символьная строка — это последовательность символов из некоторого набора, например из таблицы кодирования ASCII. В языке Си для хранения символьных строк используются символьные массивы. Например, для того, чтобы зарезервировать место для однокилобайтного буфера, можно воспользоваться следующим объявлением:
char buf [1024];
Строки в языке Си должны заканчиваться признаком конца строки — “нулевым” символом '\0', т.е. байтом, все биты которого равны 0. Библиотечные функции, которые работают со строками, “ищут” нулевой символ, так как это единственный способ определить конец строки. Таким образом, при объявлении символьного массива, в котором будет храниться строка, необходимо предусмотреть дополнительный байт для признака конца строки.
Пример 1
/* запрашивает у пользователя имя и выводит его*/
#include <stdio.h>
int main()
 { int   i;
   char first [11];    /* длина имени  до 10 символов */

   printf ("Пожалуйста, введите имя \n");
   i = 0;
   while (i<10 && (first[i] = getchar ()) != '\n')
     i += 1;
   first[i] = '\0';
   printf (" Введено имя: %s\n", first);
 }


Массивы структур
Массив структур — это массив, каждый элемент которого является структурой. В памяти элементы массива структур размещаются последовательно.
Рис. 1. Массив структур
Массивы структур широко используются для структурной организации данных в прикладных программах и системном программном обеспечении.
Пример 1
/* Печать общей суммы зарплаты служащих */
#include <stdio.h>
#define NUM_EMPS 100
void fillarray(struct emp *, int);
struct emp {
            char name[21];
            char id[8];
            double salary;          /*это шаблон, память не выделяется */
           };
int main()
{
  struct emp staff[NUM_EMPS];       /*выделяется память под массив структур*/
  int num;
  double sal_tot = 0;
   fillarray(staff , NUM_EMPS);    /*заполняет массив структур*/
   for (i=0; i < NUM_EMPS; i++)
   sal_tot += staff[i].salary;
   printf ("total of salaries: %.2f\n", sal_tot);
}


Массивы
Массив – это упорядоченный набор объектов, тип которых совпадает, например, набор целых чисел или символов. В массиве можно хранить множество однотипных объектов без необходимости введения отдельного имени переменной для каждого из них. Массив может быть одномерным или многомерным. Количество измерений и длина каждого из измерений определяет общую длину массива, которая ограничена лишь адресным пространством конкретного компьютера.
Массивы являются производными типами данных, создаваемыми из существующих типов данных.
Массив, как и любой другой объект в языке Си, должен быть объявлен перед тем, как он будет использован:
тип идентификатор[длина измерения 1][длина измерения 2]...[длина измерения n]
В объявлении массива в квадратных скобках после идентификатора (имени массива) указывается длина каждого измерения. Количество измерений массива определяется по числу квадратных скобок в его объявлении.
Индекс массива определяет используемый элемент массива и указывается в квадратных скобках после имени массива. Индекс массива — это целочисленное выражение, значение которого может быть в диапазоне от 0 до значения, равного длине измерения, уменьшенной на 1.
Пример 1
сhar id[8];
Это объявление отводит 8 байт для 8-элементного массива символов.
Рис. 1. Одномерный массив
Пример 2
float price[3];
Это объявление отводит место для 3 вещественных чисел.
Рис. 2. Одномерный массив
Пример 3
int table[3][3];
Это объявление двумерного массива целых чисел.
Этот массив можно рассматривать как набор из трех 3-элементных массивов целых чисел.
Рис. 3. Двумерный массив
Аналогично, трехмерный массив val[5][4][3] можно рассматривать как пять массивов размерности 4 на 3.
Обычно строкам и столбцам придается определенный смысл.
Элементы массива хранятся в памяти последовательно. Если первый элемент символьного массива id хранится по адресу 5000, то второй будет храниться по адресу 5001, третий – по адресу 5002 и т.д.
Двумерный массив располагается в памяти построчно: сначала нулевая строка, потом 1 и т.д. Можно сказать, что “последний индекс меняется быстрее других”. Элементы table[3][3] хранятся в следующем порядке:
[0][0], [0][1], [0][2], [1][0], [1][1], [1][2], [2][0], [2][1], [2][2]


Минимальная программа на языке программирования Си
Минимальная программа на языке Си должна включать функцию с именем main(). При начале выполнения программы управление из операционной системы всегда передается функции main().
Примеры минимальной программы, допустимые с точки зрения синтаксиса, но ничего не делающие:
int main ( )
{
}
или
int main ( ){}
или
int main
(
 )
{
}
Язык Си использует свободный, а не позиционный формат расположения программы, что делает несущественным позиции размещения элементов программы. При чтении текста программы на Си компилятор распознает лексемы, такие как имена переменных, знаки операций и разделители. Как правило, символы промежутков (пробелы, знаки табуляции и символы перевода строки) игнорируются.
Имя файла, содержащего программу на языке Си должно оканчиваться расширением .с.


Поименованные константы
Поименованные константы определяются с помощью директивы препроцессора define:
#define идентификатор строка-шаблон
Поименованные константы используются для того, чтобы облегчить чтение и изменение программ.
До и после идентификатора в директиве define должен быть один или более пробелов. Строка-шаблон начинается с первого отличного от пробела символа и заканчивается символом перевода строки. Строку-шаблон можно продолжить более чем на одну строку, начиная новые строки символом \.
После определения поименованной константы препроцессор заменяет в исходном тексте программы идентификатор значением строки-шаблона. Внутри комментариев и строковых констант такая замена не производится.
В отличии от операторов языка Си, директивы препроцессора Си не заканчиваются символом ;.
Пример 1
#define TRUE     1
#define FALSE    0
#define MAXITEMS 500
int main()
{
   int i, found, val[MAXITEMS];
   found = FALSE ;
   while (found == FALSE)
     {
     ...
     if (выражение)
        found = TRUE;
     }
   ...
   for (i = 0; i < MAXITEMS; i++)
   /*  прочитать значение val [i] */
   ...
}
Использование в прим. 1 поименованных констант TRUE и FALSE делает текст программы более понятным. Константа MAXITEMS упрощает сопровождение программы. К примеру, если потребуется изменить количество предметов, придется исправить только одну директиву препроцессора и перекомпилировать программу.
При обработке препроцессором поименованные константы TRUE, FALSE и MAXITEMS заменяются, соответственно, на значения 0, 1 и 500 . При этом директивы препроцессора удаляются из исходного текста программы и не попадают на следующий этап компиляции.


Оператор goto
Управляющий оператор goto вызывает переход на выполнение оператора, перед которым указана соответствующая метка.
goto метка;
Метка — это идентификатор , завершаемый двоеточием.
Метка должна предшествовать оператору, в вырожденном случае — пустому оператору. Можно переходить только на метки, указанные внутри текущей функции, нельзя перейти на метку, указанную в другой функции.
Оператор goto следует использовать только для выхода из глубоко вложенных циклов. При большом числе операторов goto программа становится неструктурированной и трудной для отладки.
Пример 1
main ()
{
. . .
/* не рекомендуется */
start: оператор
оператор
if (выр)
goto start;
оператор
оператор
...
}
прим. 1 демонстрирует плохой стиль программирования. Такое использование оператора goto делает программу плохо структурированной, а, следовательно, плохо отлаживаемой, ненадежной и неэффективной.
Пример 2
main ()
{
...
/* рекомендуется */
while (выр)
  {
   while (выр)
     {
      оператор
      while (выр)
        {
         while (выр)
           {
            оператор
            if (выр)
               goto end;
            оператор
            }
          оператор
          }
      }
   }
end:;
}
прим. 2 демонстрирует ситуацию, когда использование оператора goto оправдано.


Операция приведения
Операция приведение — это операция, используемая для временного преобразования значения операнда, который является выражением, в данные другого типа. Операция приведения является унарной операцией:
(тип)операнд
Тип задается ключевым словом, определяющим тип данных.
Подобно другим операциям языка Си операция приведение указана в таблице приоритетов операций.
Пример 1
int main ( )
{
  float func(float);
  int x;

  func ((float)x);
  func (x);
. . .
}
float func(float)
{
  float z;
. . .
  return z;
}
При вызове функции func() ее фактический параметр (переменная целого типа x) явно приводится к вещественному типу float, так как функция func() требует вещественного значения аргумента. Хорошим стилем программирования является передача функции параметров ожидаемого ею типа. Если при вызове функции не выполняется явное приведение типа фактического параметра, как во втором вызове функции func(), компилятор выполнит его сам некоторым способом (ему известно, что функция func() ожидает параметр типа float из прототипа объявления функции). Использование приведения делает текст программы более понятным, поскольку пожелания программиста указаны явно.
Другое использование операции приведения показано в прим. 2.
Пример 2
int x=15, y=4;
float result;
result = (float)x / (float)y;
Значение переменной result будет иметь дробную часть. Если бы операция приведения не использовалась, то дробная часть результата была бы потеряна.


Операция продолжения ("запятая")
Операция продолжения ("запятая") — бинарная операция языка Си, значением которой является значение правого операнда.
Операция "запятая" используется следующим образом:
выражение_А, выражение_В
Сначала вычисляется выражение_А, затем выражение_В.
Значением всего выражения является значение выражение_В.
Приоритетэтой операции определен в таблице приоритетов языка Си.
В общем случае эта операция используется тогда, когда синтаксис языка требует одного выражения, а программисту хочется выполнить два или более действия путем задания нескольких выражений. Операция "запятая" чаще всего используется для включения дополнительных выражений в заголовок цикла for. Вспомним синтаксис цикла for:
for (выражение1; выражение2; выражение3)
    оператор
В прим. 1 приводится корректный цикл for, в котором инициализируются и увеличиваются две переменные.
Пример 1
for (i = 0, j = 10; i<max; i++, j++)
    list[i] = name[j];
Здесь выражением1 является следующее выражение : i = 0, j = 10, а выражением3 является выражение i++, j++.
Выражение типа (выражение1, выражение2, выражение3, выражение4) корректно. Поскольку операция "запятая" левоассоциативна, т.е. связывает свои операнды слева направо, значением всего выражения будет значение выражение4.
Пример 2
x = 0;
while (printf("x is %d\n",x), x<5000)
    tryit (x++);
В языке программирования Си любое выражение имеет значение, например значение выражения 5+2 равно 7, значением выражения х = 200 является 200, значением выражения x < y есть 1 (истина) или 0 (ложь). В прим. 2 в цикле while значение выражения в круглых скобках есть значение второго выражения x < 5000, которое будет либо 0 (ложью) либо 1 (истиной). Функция printf() вычисляется каждый раз при вычислении выражения, определяющего условие продолжения цикла, однако не влияет на истинность значения проверки.


Операции отношений
Операции отношений — операции языка Си, которые используются для сравнения двух числовых значений.
Приоритет данных операций определяется таблицей приоритетов языка Си.
Значением операции отношения (например, x < max) может быть либо истина, либо ложь. Язык Си присваивает значение 0 ложному отношению и 1 — истинному отношению. На самом деле, любое ненулевое выражение в Си считается истинным, а нулевое — ложным.
Таблица 1
знак операции | смысл операции | пример
< | меньше | x < max
<= | меньше или равно | x <= max
> | больше | x > max
>= | больше или равно | x >= max
== | равно | x == max
!= | не равно | x != max

При использовании операций, обозначаемых двумя символами (например, <=, >=, == или !=), нельзя вставлять пробел между символами.
Будьте осторожны с операцией "равно" ==, это не то же, что операция присваивания =.


Операторы
Оператор — это конструкция языка Си, которая вызывает выполнение действия.
В языке программирования Си существуют следующие операторы :
  • простые операторы
  • блоки операторов (блоки, составные операторы)
  • управляющие операторы
Простой оператор или оператор-выражение — это выражение, заканчивающееся символом ; (точка с запятой).
Специальным случаем простого оператора является пустой оператор, который ничего не делает. Пустой оператор состоит из единственного символа ; (точка с запятой). Он используется в тех случаях, когда синтаксис языка требует наличия оператора там, где не требуется никаких действий.
Блок операторов (блок, составной оператор) — это последовательность операторов, заключенная в фигурные скобки {}.
Блок используется для объединения нескольких операторов в один оператор. После закрывающей фигурной скобки точка с запятой может не указываться. После открывающей фигурной скобки можно объявлять переменные. Такие переменные, называемые автоматическими, известны только внутри этого блока, после выхода из блока они больше неизвестны.
"Тело" функции также представляет собой блок операторов.
Управляющие операторы — операторы языка Си, реализующие ветвления алгоритма, циклы и передачу управления.
Все управляющие операторы начинаются с ключевого слова, такого как: if, while, for и т.д.


Обработка массивов
Выход за границу массива
Выход за границу массива — обращение к элементу массива, индексы которого выходят за указанные в объявлении массива пределы.
Программист сам должен заботиться о том, чтобы индексы элемента массива лежали в пределах, указанных при объявлении массива.
Для увеличении скорости работы программы не выполняется проверка, лежит ли индекс массива в указанных при объявлении пределах. Такая проверка должна производится во время работы программы каждый раз при обращении к массиву с использованием этого индекса. Если бы Си проверял допустимость индекса массива, то программа работала бы намного медленнее.
Но помимо увеличения скорости работы программы, отсутствие проверки индексов имеет и нежелательные последствия — индекс может выйти за границы массива. В этой ситуации возможно одно из двух:
  • Что-то будет прочитано или записано в память за границами массива.
  • Получившийся адрес будет лежать за пределами доступной области памяти, например, при попытке записи в блок памяти, доступной только для чтения. При этом выполнение программы аварийно прекратится.
Программист должен обеспечивать изменение индекса массива в диапазоне от 0 до (размер_массива-1).
Пример 1
/*Пример выхода за границу массива: */
int ray[20], i;
i = 0;
while (i <= 20)
   {
    ray [i] = i*10;
    i += 1;
   }
Копирование массивов
Массивы должны копироваться поэлементно.
Нельзя копировать массивы так, как это показано в прим. 2. В левой части операции присваивания нельзя использовать только имя массива.
Имя массива, используемое в программе без скобок, воспринимается как адрес, начиная с которого хранится массив. Этот адрес нельзя изменить, он не является именующым выражением.
Пример 2
int prev[20], current[20];
prev = current; /*Неправильно: так копировать  массив нельзя */
Пример 3
int prev[20], current[20];
i = 0;
while (i<20) /*Правильно: копировать массив надо так */
{
prev[i] = current [i];
i += 1;
}


Указатели
Идея использования адресов в программах очень стара. Самые первые программисты имели хорошее представление об адресном пространстве программы. Написанные ими программы на машинном языке или языке ассемблера выполняли непосредственные и эффективные пересылки данных, расположенных по одному адресу, в другую адресуемую область.
При появлении языков более высокого уровня программисту был предоставлен высший уровень абстракции. Удобные средства, такие как имена переменных, индексирование массивов, функции и подпрограммы, освободили программиста от необходимости думать на низком уровне машинных адресов.
Язык Си предлагает лучшее из обоих подходов. Язык Си дает возможность манипулировать адресами посредством переменных типа указатель. Эффективность и гибкость, предоставляемая указателями, в сочетании с высокой мобильностью языка Си -- вот две причины частого использования языка Си вместо машинного языка или языка ассемблера там, где наиболее важна скорость выполнения программы.
Указатель — это переменная, содержащая адрес.
Указатель должен быть объявлен также, как объявляются обычные переменные, но перед идентификатором должен стоять символ *.
Пример 1
int          *p_i;   /* указатель на целое типа int*/
float        *p_f;   /*указатель на вещественное типа float */
struct  str  *p_s;   /*указатель на структуру  str*/
Для корректного использования адресной арифметики важно объявлять указатель на тот тип данных, с которым он будет использоваться в программе.
Для чего используются указатели?
  • Для эффективного доступа к данным.
  • Для разработки гибких программ.
  • Для изменения переменных, передаваемых в функцию.
  • Для работы с динамически распределяемой памятью.
  • Для доступа по аппаратно-фиксированным адресам в системных программах.


Преобразование данных
Преобразование при присваивании
Если в операции присваивания оба операнда операции присваивания имеют один и тот же тип данных, то никакого преобразования данных не требуется. Однако, выражение слева (именующее выражение) не обязано иметь тот же тип, что и выражение справа. В этом случае делается попытка преобразования правого выражения к типу левого объекта.
Возможны следующие варианты использования операции присваивания, которые потребуют преобразования данных:
  1. Объект типа float или double = выражение типа char или int
  2. Объект типа char или int = выражение типа float или double
  3. Объект меньшего размера = выражение большего размера.
  4. Объект большего размера = выражение меньшего размера
В первом варианте присваивание обычно не влечет никакой потери данных. Значение выражение типа char или int будет помещено в область памяти, отведенную для float или double. При этом объект, представленный в формате целых чисел, преобразуется в формат с плавающей точкой для вещественных чисел.
Пример 1
int_var = 27;
float_var = int_var; /* float_var теперь равно 27.0*/
В некоторых ситуациях этот вид присваивания может привести к потере данных. Например, на некотором компьютере формат целых чисел типа int может содержать 10 цифр, а формат с плавающей точкой типа float – 7 значащих цифр. Присваивание переменной типа float 10-значащего целого приведет к потере точности. Для большинства компьютеров эта проблема преодолевается путем присваивания целых переменным типа double.
Во втором варианте присваивание вызывает отбрасывание дробной части. Если результат не помещается в формат типа char или int, часть значения теряется.
В третьем варианте в присваиваниях вида
  • объект типа char = выражение типа int
  • объект типа float = выражение типа double
данные могут быть потеряны.
В дальнейшем термин "младшие байты" и "старшие байты" относятся к двоичному представлению значения, независимо от того, в каком порядке эти байты хранятся в памяти различных процессов.
В четвертом варианте присваивания вида
  • объект типа int = выражение типа char
  • объект типа long = выражение типа short
преобразование выполняется следующим образом.
Значение переносится в младший байт(ы). Обработка старшего байта(ов) зависит от знакового бита меньшего объекта и от используемого процессора.
Пример 2
Если у значения правого выражения знаковый бит равен 0, старшие байты заполняются нулями .
int_var = char_var
Рис. 1.
Пример 3
Если у значения правого выражения знаковый бит равен 1 и правый операнд имеет знаковый тип, старшие байты заполняются единицами.
int_var = char_var
Рис. 2.
Пример 4
Если у значения правого выражения знаковый бит равен 1 и правый операнд имеет беззнаковый тип, старшие байты заполняются нулями.
int_var = char_var
Рис. 3.
Автоматические преобразования
Язык Си допускает выражения с операндами различных типов. Например, можно делить переменную типа float на константу типа int, прибавлять константу типа int к переменной типа char и т.д. Если выражение имеет операнды различных типов, низкий тип всегда преобразуется к более высокому. Один тип ниже другого, если он занимает меньше памяти.
Рис. 4. Преобразования при вычислении выражения
На рис. 4 показано как будет выполняться преобразование при вычислении значения выражения. Преобразования по горизонтали выполняются автоматически. Например, объект типа char в выражении всегда временно преобразуется в тип int. Преобразования из младшего в старший тип (снизу вверх) выполняются в случае необходимости. Например, если выражение содержит операнды типов int и double, то операнд типа int будет временно преобразован в тип double.
В стандарте ANSI языка Си арифметика с операндами типа float может выполняться с одинарной точностью. Обычно же операнд типа float автоматически преобразуется в тип double.
Для устранения неоднозначности преобразования можно использовать операцию приведения типа (type).
Пример 5
y = (int)7.2 / 3;
Лабораторная работа 3. Арифметические операции языка программирования Си


Препроцессор языка Си
Препроцессор языка Си – это составная часть компилятора, реализующая первую стадию компиляции.
Препроцессор - это мощный инструмент, часто используемый для повышения удобочитаемости, надежности и переносимости программ.
Обработка препроцессором выполняется на первом этапе компиляции исходного текста программы.
Препроцессор читает исходный текст, отыскивая и обрабатывая директивы препроцессора, которые представляют собой строки текста, начинающиеся с символа #.
Пример 1
#include <stdio.h>
#define SIXE 100
Для переносимости программы следует указывать символ # в позиции 1, а непосредственно после него – управляющее слово (include, define , и т.п.), поскольку некоторые препроцессоры языка K&R Си не допускают, чтобы символы промежутков предшествовали символу # или следовали непосредственно после него.
Препроцессор позволяет определять константы и макросы, включать файлы, предоставляет возможности условной компиляции.


Приоритеты операций
Приоритет операции определяет порядок вычисления операций в выражении.
Ассоциативность определяет порядок вычисления операций, имеющих равный приоритет.
Приоритет операции определяется ее положением в таблице приоритетов. Самыми приоритетными являются первичные операции в верхней строке, затем идут унарные операции и т.д.
Унарная операция - операция, имеющая один операнд.
Если одна операция приоритетнее другой, она теснее связывает свои операнды, образуя неявные скобки.
Таблица 1
Вид операции | Знак операции | Ассоциативность
первичные | ( ) [ ] -> . | слева направо
унарные | ! ~ - + ++ -- (type) * & sizeof | справа налево
мультипликативные | * / % | слева направо
аддитивные | + - | слева направо
сдвиг | << >> | слева направо
отношение | < <= > >= | слева направо
равенство | == != | слева направо
побитовая | & | слева направо
побитовая | ^ | слева направо
побитовая | | | слева направо
логическая | && | слева направо
логическая | || | слева направо
условная | ?: | справа налево
присваивание | = op= | справа налево
запятая | , | слева направо

Пример 1
X = 5 + 3 * 2
Данное выражение включает три операции: =, + и *. Наиболее приоритетной является операция умножения *, откуда вытекает следующее отношение:
X = 5 + (3 * 2)
Операция сложения + приоритетнее, чем операция присваивания =, что приводит к следующему соотношению:
X = (5 + (3 * 2))
И окончательно получается выражение:
(X = (5 + (3 * 2)))
Последнее выражение показывает, как компилятор Си будет интерпретировать исходное выражение. Переменной Х будет присвоено значение 11.
Скобки можно использовать для повышения удобочитаемости или для изменения стандартной группировки операндов.
Пример 2
X = (5 + 3) * 2
Это выражение даст результат, отличный от предыдущего примера. В данном случае переменной Х присваивается значение 16.
Что произойдет, если в выражении две операции имеют одинаковый приоритет?
Х = 12 / 2 * 3
Будет ли это выражение интерпретироваться так, что даст результат 18
Х = (12 / 2) * 3
или так, что даст результат 2?
Х = 12 / (2 * 3)
Ответ содержится в третьем столбце таблицы, названном ассоциативность. "Слева направо" означает, что самая левая операция в выражении применяется первой. Предыдущее выражение следует интерпретировать как Х = (12 / 2) * 3.


Операция присваивания
Именующие выражение = Выражение
Операция присваивания помещает значение выражения (своего правого операнда) в область памяти, определяемую левым операндом, который должен быть именующим выражением.
Именующее выражение — выражение, определяющее объект, которому может быть присвоено какое-либо значение, например, переменную или элемент массива.
Пример 1
count = 1   /* верно */
(a - b) = 6 /* неверно */
Пример 2
int  main ()
{
   unsigntd int        quantity;
   float               total, price = 150.00;

   quantity = 4U;
   total = quantity * price;
   . . .
}
Как видно из прим. 2, начальное значение переменной может быть присвоено как при объявлении, так и операцией присваивания.
Составные операции присваивания
Составные операции присваивания вида ор= работают следующим образом:
выражение1 ор= выражение2
эквивалентно выражению
выражение1 = выражение1 ор выражение2
Знак ор может быть знаком одной из перечисленных ниже арифметических операций или одной из бинарных побитовых операций.
Таблица 1
Составная операция присваивания | Эквивалентное выражение
x += 50 | x = x + 50
x -= 50 | x = x - 50
x *= 50 | x = x * 50
x /= 50 | x = x / 50
x %= 50 | x = x % 50
x *= a + b | x = x * (a + b)

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


Двукратные указатели
Двукратный указатель - это переменная, которая содержит адрес указателя, то есть это указатель на указатель.
Двукратные указатели часто используются в системных программах. Трехкратные указатели встречаются редко. Язык Си допускает так же использование многократных указателей (например, ****p). Однако, лучше избегать этого.
Объявление двукратного указателя:
тип **идентификатор;
Пример 1

/*пример сканера для идентификации команд */
#include <stdio.h>
#include <string.h>
int is_keyword(char *);
char *keyword[] = {
                   "append",
                   "find",
                   "list",
                   "remove",
                   "replace",
                   "substitute",
                   (char *)NULL
                   };
/*возвращает индекс команды, иначе -1*/
int is_keyword(char *)
  {
  char **p;
  for (p=keyword; *p != NULL; p++)
     if(strcmp(str, *p) == 0)
        return p-keyword;
  return -1;
  }
Массив keyword - это массив указателей на символ, который инициализируется адресами строковых констант. Такие указатели обычно не изменяются во время выполнения программы. Двукратный указатель p используется для перебора элементов массива.
p содержит адрес одного из элементов массива keyword, *p - это адрес строки, записанный в этом элементе массива, а **p (в примере не используется) указывает на первый символ в этой строке.


Указатель на void
Тип "указатель на неопределенный тип" (указатель на void) — обобщенный указательный тип. Значение указателя любого типа может быть преобразовано и присвоено указателю на тип void и наоборот без изменения его значения.
Пример 1
int main()
{
  int a_function(void *);
  char name[50] = "William";
  int num = 95;

  a_function(name);
  /* передан указатель на char */
  a_function(&num);
  /* передан указатель на int */
}

int a_function(void *vptr)
{
  ...
}
В общем случае, отличном от рассмотренного примера, результат преобразования значений и присваивания указателей различных типов непредсказуем.


Рекурсивные функции
Функция в языке программирования Си может быть рекурсивной, т.е. может вызывать сама себя. Каждый раз, когда функция вызывает себя, на стек помещается новый кадр. Функция должна вызывать себя условно, например, внутри оператора if, иначе бесконечная рекурсия переполнит стек.
Пример 1
void printd (int n)
{
  int i;

  if (n < 0)
    {
     putchar ('-');
     n = -n;
    }
  if ( (i = n/10) != 0)
     printd(i);
  putchar(n % 10 + '0');
}
Рис. 1. Рекурсивный поток управления через вызовы функции printd()
В прим. 1 в функцию printd() передано значение 543. Эффект этой функции состоит в том, что printd() рекурсивно разбивает значение на составляющие его цифры и затем печатает эти цифры.


Регистровый класс памяти
Регистровая переменная — автоматическая переменная, располагаемая, по возможности, на машинном регистре.
Для объявления регистровой переменной используется ключевое слово register. В этом случае переменная запоминается на машинном регистре, а не на стеке. Количество регистров, доступных для такого использования, является машинно-зависимым. Если потребность в регистровой памяти превышает число доступных аппаратных регистров, избыточные регистровые переменные трактуются как автоматические. В этом случае ключевое слово register попросту игнорируется.
Регистровые переменные рекомендуется использовать для повышения быстродействия программы. Лучшими кандидатами на регистровые переменные являются переменные, к которым осуществляется частый доступ, такие как счетчики циклов или индексов массивов.
Автоматические переменные и формальные параметры функции могут быть объявлены с классом памяти register. Внешние и статические переменные не могут быть регистровыми. Допустимые типы данных также зависят от системы, обычно это следующие типы данных: char, int и указатели. Массив не может запоминаться в регистре. Регистровые переменные имеют такие же область действия и время жизни, что и автоматические переменные. Операция & (операция получения адреса) не может быть применима к регистровой переменной.
Пример 1
f (register int count, register int num)
  {
   register int i;
   . . .
  }


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

где [image] — запись числа [image] системе счисления [image]; [image] — символы системы, образующие базу:

По этому принципу построены непозиционные системы счисления .
Непозиционная система счисления — система, для которой значение символа не зависит от его положения в числе.
К непозиционным системам счисления относятся, например, система с одним символом (палочкой), римская система.
Позиционная система счисления (ПСС) — система, для которой значение цифры определяется ее положением в числе.
Любая позиционная система счисления характеризуется основанием (базисом) [image]. Для естественной позиционной системы [image] — целое положительное число.
Основание позиционной системы счисления — количество знаков или символов, используемых для изображения числа в данной системе.
В позиционной системе счисления число можно представить в виде следующего ряда:

где [image] — произвольное число, записанное в ПСС с основанием [image]; [image] +1 и [image] — количество целых и дробных разрядов.
На практике используют сокращенную запись числа:

В повседневной жизни наиболее известна десятичная система счисления, в которой для записи используются цифры 0, 1, ... , 9.
Правильный выбор ПСС для ЭВМ — важный практический вопрос, так как от его решения зависят такие технические характеристики ЭВМ, как скорость вычислений, объем памяти, сложность алгоритмов арифметических операций. При выборе ПСС для ЭВМ необходимо учитывать следующее:
  • основание ПСС определяет количество устойчивых состояний, которые должен иметь функциональный элемент, выбранный для изображения числа;
  • длина числа существенно зависит от основания ПСС;
  • ПСС должна обеспечивать простые алгоритмы выполнения арифметических и логических операций.
С учетом этих требований привычная десятичная СС не является наилучшей. Подавляющее большинство компонентов электронных схем, применяемых для построения ЭВМ, имеют два устойчивых состояния. С этой точки зрения для ЭВМ наиболее подходит двоичная СС, в которой для записи чисел используются цифры 0 и 1
Пример 1

Также в ЭВМ часто используют восьмеричную и шестнадцатеричную СС.
В шестнадцатеричной СС используются следующие символы: .
Пример 2

В восьмеричной СС числа изображают с помощью цифр [image].
Пример 3

В таблице 1 приведены эквиваленты десятичных чисел в других СС:
Таблица 1
Десятичная цифра | q=2 | q=8 | q=16
0 | 0000 | 0 | 0
1 | 0001 | 1 | 1
2 | 0010 | 2 | 2
3 | 0011 | 3 | 3
4 | 0100 | 4 | 4
5 | 0101 | 5 | 5
6 | 0110 | 6 | 6
7 | 0111 | 7 | 7
8 | 1000 | 10 | 8
9 | 1001 | 11 | 9
10 | 1010 | 12 | A
11 | 1011 | 13 | B
12 | 1100 | 14 | C
13 | 1101 | 15 | D
14 | 1110 | 16 | E
15 | 1111 | 17 | F

Длина числа (ДЧ) – количество позиций (или разрядов) в записи числа. В техническом аспекте ДЧ интерпретируется как длина разрядной сетки (ДРС). Для разных ПСС характерна разная ДЧ для записи одного и того же числа. Чем меньше основание ПСС, тем больше ДЧ.
Длина разрядной сетки определяет максимальное по абсолютному значению число, которое может быть представлено в заданной разрядной сетке. Пусть [image] — длина разрядной сетки. Тогда

Диапазон представления чисел (ДП) в заданной ПСС -- интервал числовой оси, заключенный между максимальным и минимальным числами, представленными длиной разрядной сетки:

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


Оператор return
Функция может передавать одно значение обратно в вызывающую функцию.
Тип возвращаемого значения определяется в определении прототипа функции и объявляется в объявлении прототипа функции.
Оператора возврата return — оператор, с помощью которого функция возвращает значение в вызывающую функцию.
Синтаксис оператора возврата return:
return выражение;
При выходе на оператор return выполняется возврат из функции так, что управление вновь передается в вызывающую функцию. В случае если оператор return имеет выражение, значением вызова функции является значение этого выражения. Если оператор return не имеет выражения , значение вызова функции не определено.
Функция может иметь более одного оператора return.
Пример 1
int func1(void)
{
...
if (n==0 || n==1)
   return 1;
else
   return 0;
}
Пример 2
void func2(void)
{
...
return ;
}
В прим. 1 функция func1() возвращает значение целого типа. В прим. 2 функция func2() не возвращает никакого значения.
Функция main() может использовать оператор return для возврата некоторого целого значения в родительский процесс.
Пример 3
#include <stdio.h>
int main ()
{
   int area_rect (int, int);
   int len = 50, width = 4, area;

   area = area_rect (len, width);
   printf ("Area is %d \n", area);
   return 0;
}

int area_rect (int l, int w)
{
/*площадь прямоуголника*/
   return l*w;
}
Функции, возвращающие указатели
Функция может возвращать указатель. При этом она должна быть определена и объявлена соответствующим образом. Например, если функция возвращает указатель на целое, ее определение обычно имеет вид:
int *f(void)
{
. . .
}
Кроме того, функция должна быть объявлена в некоторой вызывающей функции или выше всех таких функций по тексту программы.
Пример 4
main ()
{
int *f(void);
...
}
В прим. 4 объявление известно внутри функции main().
Пример 5
int *f(void);
main ()
{
...
}
В прим. 5, подобно внешней переменной, объявление известно всем функциям, расположенным ниже его в исходном тексте программы. Объявление может предваряться ключевым словом extern.
Пример 6
#include "local.h"
main ()
{
...
}
Прим. 6 напоминает, что объявления функций часто находятся в файлах заголовков, область действия этих объявлений такая же, как в прим. 5.
Пример 7
/* main () - передает адрес массива, содержащего город и улицу,
в функцию getstreet(), которая возвращает указатель на улицу*/
#include <stdio.h>
int main ()
{
   char *getstreet (char *);
   static char info[] = "Москва, Арбат";

   printf ("Улица %s\n", getstreet(info));
}
/* Предполагается, что p указывает на строку вида "Город, Улица" */
char *getstreet(char *p)
{
  while ( *p != ',')
    p++;
  return p + 2;
}
Вывод:
Улица Арбат


Стадии создания программного продукта
При разработке программного продукта можно выделить следующие стадии:
    1. стадия предпроектных исследований и технического задания — определение требований к программному продукту и осуществление формальной постановки задачи;
    2. стадия технического предложения (анализ) — определение методов решения задачи;
    3. стадия эскизного проектирования — разработка структуры программного продукта, выбор структур для хранения данных, построение и оценка алгоритмов подпрограмм и определение особенностей взаимодействия программы с вычислительной средой (другими программами, операционной системой и техническими средствами);
    4. стадия технического проектирования — составление программы на выбранном языке программирования, ее тестирование и отладка;
    5. стадия рабочего проектирования — оформление документации;
    6. стадия испытаний — всестороннее тестирование программы;
    7. стадия внедрения в эксплуатацию.
Стадия предпроектных исследований и технического задания
Иногда эту стадию называют стадией постановки задачи. Постановка задачи должна содержать достаточно информации для того, чтобы позволить программисту или аналитику однозначно определить, что будет делать создаваемая программа. Детали того, как она будет это делать, должны определиться позже, при написании алгоритма.
Постановка решаемой на ЭВМ задачи должна включать список ее входных данных, список требуемых результатов и любые инструкции (правила), которых нужно следовать при решении задачи. В постановке также должно быть указано, должна ли задача решаться для конкретного случая (конкретных данных) или должна обобщаться для переменных входных данных. Если возможно, решение должно обобщаться так, чтобы его можно было использовать для решения класса задач. Например, программа, написанная для вычисления текущей зарплаты, должна быть написана так, чтобы она могла бы периодически использоваться и в будущем без изменений.
В результате согласования между заказчиком и исполнителем всех перечисленных вопросов составляют техническое задание (ТЗ) в соответствии с ГОСТ 19.201-78, которое служит основанием для дальнейшей работы.
Стадия технического предложения
На данной стадии выполняется анализ задачи – это определение и детализация логического порядка действий, которые нужно выполнить над данными, чтобы получить требуемое решение. На этом этапе процесса решения задачи следует в общих чертах описать, что необходимо сделать. Детали того, как это должно быть сделано, будут уточняться на следующем шаге.
Пример 1
Определить взлетную готовность самолета:
Найти суммарный вес всех грузов и суммы их вращательных моментов относительно носа, с учетом веса топлива, тары и экипажа. Использовать программный модуль XYZ для определения потребности в топливе, обратиться к руководству по самолету для нахождения допустимого веса и расположения центра массы.
Сравнить вычисленные результаты с допустимыми ограничениями и напечатать сообщение "Готов к взлету" или "Не готов к взлету"
Часто формальная постановка задачи однозначно определяет метод ее решения. Если задача может быть решена несколькими методами, выбирается один из них с учетом сложности реализации, точности результатов и т.д.
Стадия эскизного проектирования
При использовании процедурного подхода сложные задачи в процессе анализа разбиваются на подзадачи, для которых может строиться своя модель и выбираться свой метод решения. При этом результаты одной подзадачи могут использоваться как исходные данные в другой.
Целесообразно проверить правильность выбранных моделей и методов, выполнив их вручную для некоторых значений исходных данных.
При определении типов исходных данных необходимо также продумать, для каких сочетаний этих данных результат не существует или не может быть получен данным методом, что также надо учитывать при разработке программы.
Одновременно с написанием алгоритма, необходимо точно определить тип и структуру обработанных данных. В одних случаях данными могут быть несколько обычных чисел, в других организация данных будет более сложной.
При определении структуры данных с каждым объектом данных должно быть связано осмысленное имя или идентификатор. При разработке программы идентификаторы будут связаны с расположением данных в памяти.
На данной стадии разрабатываются и оцениваются алгоритмы подпрограмм
Алгоритмом называют формально описанную последовательность действий, которые необходимо выполнить для получения требуемого результата.
Разработка алгоритма состоит в пошаговом описании предлагаемого решения задачи. Каждый шаг должен быть описан в виде кратких и точных операторов с использованием структурированного языка или псевдокода.
Каждой процедуре должно быть дано содержательное имя или идентификатор, для того, чтобы ее можно было вызвать по имени как модуль из другой процедуры:
Пример 2
find_area (Найти площадь),
calc_balance (Вычислить_баланс)
prepare_fit (Подготовить_декларацию)
В операторах разработчик алгоритма должен использовать идентификаторы предварительно определенных данных. Данные, определенные вне модуля ( при постановке задачи или ее анализе), рассматриваются как внешние данные и должны быть включены в разработанное определение структуры данных. Внешние данные могут совместно использоваться несколькими процедурами. Данные, принадлежащие одному модулю, являются внутренними или временными и могут быть определены внутри самого модуля. Использование внутренних (или локальных) данных повышает модульность программы.
Алгоритм должен быть детализирован до такой степени, когда каждый оператор выполняет простую операцию или вызов другой процедуры. Этого можно достичь путем использования повелительных и условных операторов (предложений). Разработка алгоритма завершается, когда каждый из операторов может быть записан непосредственно на языке программирования.
Сравните два алгоритма.
Пример 3
  • Алгоритм
  • Quad_roots_1 (квадр_корни_1)
    • Ввести три коэффициента квадратного уравнения
    • Вывести корни
Пример 4
  • Алгоритм
  • Quad_roots_ 2 (квадр_корни_2)
  • Замечание – для случая комплексных корней алгоритм не применим.
    • Ввести три коэффициента квадратного уравнения a, b, c.
    • Вычислить

    • Вычислить

    • Напечатать значения корень1, корень2
Какой из алгоритмов лучше?
На изображение схем алгоритмов существуют ГОСТ 19.701-90, согласно которому каждой группе действий ставится в соответствие блок особой формы.
Стадия технического проектирования
Разработанные алгоритмы реализуют, составляя по ним текст программы с использованием конкретного языка программирования. Язык может быть определен в техническом задании, а может выбираться исходя из особенностей конкретной разработки.
Возможно будет выбран язык программирования Си. Программа должна быть занесена в исходный (текстовый) файл на вашем компьютере с использованием стандартного редактора.
Отладка программы - процесс локализации и исправления ошибок получил название. При отладке программы часто используют специальные программы – отладчики, которые позволяют выполнить любой фрагмент программы в пошаговом режиме и проверить содержимое интересующих нас переменных.
Тестирование программы состоит в выполнении следующих действий, предшествующих передаче программы ее предполагаемым пользователям:
Выполнять до тех пор пока исполнение программы дает неправильные результаты:
  • Делать:
    • При необходимости перекодировать алгоритм.
    • Внести исправления в текст программы.
    • Компилировать файл исходной программы.
  • Сделано
  • Создать исполнимый файл.
Стадия рабочего проектирования
Если программу предполагается использовать и сопровождать в течении какого-либо срока, она должна быть документирована. Несмотря на то, что здесь документирование рассматривается как отдельный этап в процессе разработки решения задачи, оно должно выполняться на протяжении всего этого процесса.
Документация должна включать:
  • описание постановки задачи,
  • описание анализа задачи,
  • описание определения структуры данных,
  • описание алгоритма,
  • текст программы с комментариями,
  • тесты с входными и выходными данными,
  • записи о процедурах внедрения,
  • руководство пользователя,
  • записи о всех модификациях.
Стадия испытаний
Тестирование программы выполняется постоянно на протяжении всей работы над программой. Завершающее тестирование должно быть всесторонним. При этом должны быть учтены все возможные варианты входных данных, а также все ошибочные ситуации.
Стадия внедрения в эксплуатацию
После завершения разработки программы ее использование должно контролироваться с целью наблюдения за правильностью ее работы и установления обратной связи с конечным пользователем, который может внести предложения по повышению эффективности и простоты использования программы.


Назначение стека при вызове функции
Стек — это область памяти, используемая выполняемой программой для временного запоминания значений. При вызове функции все значения фактических параметров функции помещаются на стек. Затем запоминаются значения существенных регистров, таких как, например, указатель текущей команды. Значения регистров восстанавливаются при завершении управления вызванной функцией. При передаче управления вызванной функции значения фактических параметров связываются с соответствующими областями памяти на стеке, выделенными под формальные параметры. Затем определяется расположение на стеке для локальных (автоматических) переменных. Этот набор параметров, регистров и автоматических переменных для вызова функции называется кадром (frame). Если вызванная функция выполняет вызов другой функции, на стек помещается еще один кадр. Только один кадр является активным в любой момент времени. Активная часть стека "растет" при вызове функции и "сокращается" при выходе из функции. Стековое пространство все время повторно используется, и это является причиной того, почему автоматические переменные имеют до инициализации неопределенные значения. Они зависят от того, какие значения запоминались в соответствующем месте памяти до данного вызова функции.
Пример 1
int main()
{
   int x = 10, y = 100;
   f(5, 10);
. . .
}
double f (int x, int sum)
{
   double total;
   g(20.5);
. . .
}
float g(double val)
{
int count;
. . .
}
Показанная на рис. 1 диаграмма показывает гипотетическую систему построения стека. Компиляторы Си управляют стеком различными способами. Например, стек может расти от больших адресов к меньшим или наоборот; количество запоминаемых регистров зависит от системы; стековое пространство в некоторых системах может выделяться динамически по мере требований и т.д.
Рис. 1. Диаграмма построения стека


Шаги компиляции Си
Компиляция программы - процесс преобразования исходного текста программы в исполнимый модуль, состоящий из машинных команд.
Компилятор языка Си - составная часть операционной системы, например, ОС UNIX, которая выполняет компиляцию программы, написанной на языке программирования Си.
На рис.1 представлены шаги, через которые проходит программа на Си при ее компиляции.
Рис. 1. Шаги компиляции Си
На первом шаге программа "препроцессируется". Специальные директивы обрабатываются препроцессором. Результат этой обработки - расширенный исходный текст программы на языке Си.
На следующем шаге программа действительно компилируется и переводится на низкоуровневый язык ассемблера.
Затем ассемблер транслирует ассемблерную программу в объектный код. Хотя объектный код - это машинный код, он еще невыполним.
Исполняемый код строит программа - компоновщик, называемая в ОС UNIX редактором связей. Одна из задач редактора связей состоит в согласовании неразрешенных ссылок.
Неразрешенная ссылка - ссылка на объект, который в данной части программы неопределен. Редактор связей связывает объявления внешних переменных с соответствующими внешними определениями. Другим типом неразрешенной ссылки является вызов функции, не определенной в программе, например, вызов функции printf(). Редактор связей ищет объектный код функции в одной или более библиотеках и включает этот код в исполнимый модуль. Результатом работы редактора связей при отсутствии ошибок является выполнимая программа.
В системе ОС UNIX процесс компиляции может быть остановлен после любого шага путем использования соответствующего флага команды cc, который определяет режим работы команды. Создается файл с тем же именем, что и исходный файл, и окончанием .i, .s или .o, указывающем на тип файла. По умолчанию имя исполнимой программы - a.out.
Пример 1
cc -Xa -P prog.c
При выполнении этой команды компиляция останавливается после работы препроцессора. Создается файл с окончанием .i. Этот режим используется при отладке для контроля подстановок препроцессора.
Пример 2
cc -Xa -S prog.c
При выполнении этой команды компиляция останавливается перед ассемблированием. Создается файл с окончанием .s, содержащую ассемблерную программу.
Пример 3
cc -Xa -c prog.c
При выполнении этой команды компиляция останавливается перед редактированием связей. Создается файл с окончанием .o. Этот режим используется при компиляции файлов, множество которых не является завершенной программой, например, файлов, не содержащих функцию main().
Любой из этих типов файлов может быть использован как аргумент команды cc системы ОС UNIX. При этом определяется, через какие шаги компиляции прошел файл, так что шаги не повторяются. В примере 4 приводятся правильные команды cc, создающие исполняемые файлы, если не было ошибок.
Пример 4
cc -Xa prog.i
cc -Xa file1.i file2.c file3.o


Статический класс памяти
Статическая переменная — переменная, значение которой сохраняется после выхода из блока до повторного входа в него. При объявлении статической переменной используется ключевое слово static. Статической может быть объявлена как внешняя, так и внутренняя переменная.
Статические переменные во многом похожи на внешние. Они инициализируются нулем и существуют на протяжении жизни программы (в отличии от автоматических переменных, существующих лишь во время жизни блока).
Внешняя статическая переменная объявляется вне функции, но известна лишь в том файле, в котором она объявлена (даже при использовании ключевого слова external). Если внешняя переменная используется лишь в том файле, в котором она объявлена, ее следует сделать статической. Это повышает удобочитаемость и улучшает структуру программы. Кроме того, это позволяет избежать конфликтов с другими файлами, в которых могут содержаться те же имена внешних переменных, используемых в других целях.
Внутренние статические переменные объявляются внутри функции. В отличии от автоматических переменных, они существуют на протяжении жизни всей программы.
Статические переменные инициализируются лишь однажды, перед выполнением программы, и сохраняют свои значения от одного вызова функции к другому.
Пример 1
static void errmsg (void);
static int count;
int adjust_total (int amount)
{
   static int total = 1024;
   int x = 0;

   total -= amount;
   ...
}
/* статическая функция является приватной в ее исходном файле */
static void errmsg (void)
{
...
}


Структура FILE
При включении в программу файла stdio.h в системе ОС UNIX выделяется память для массива структур FILE. Стандартные библиотечные функции используют структуру FILE для хранения информации об открытом фале.
Структура FILE — это структура, в которой хранится информация о том, как открыт файл (например, для чтения), и где позиция следующего чтения или записи. Структура FILE инициализируется вызовом функции fopen(). Она используется операционной системой и доступна различным функциям для чтения-записи. Программисту не нужно обращаться прямо к полям этой структуры и даже знать, что это за поля. Программист должен лишь объявить указатель на FILE перед открытием файла. Этот указатель получает значение во время открытия файла вызовом функции fopen() и должен впоследствии передаваться как параметр другим функциям, работающим с данным файлом. Программисту никогда нельзя изменять значение этого указателя. После закрытия файла соответствующая ему переменная файлового указателя на FILE может использоваться повторно.
Пример 1
/*Образец определения типа  FILE*/
typedef struct
{
   int           _cnt;   /*количество оставшихся в буфере символов*/
   unsigned char *_ptr;  /*следующая позиция в буфере*/
   unsigned char *_base; /*размещение буфера*/
   unsigned char _flag;  /*признак ошибки чтения-записи или конца файла*/
   unsigned char _file;  /*дескриптор файла ситемы UNIX*/
} FILE;


Структуры
Структура — это набор элементов, которые могут иметь различные типы.
Структуру можно представить себе как запись, состоящую из нескольких полей или элементов. Структуры обеспечивают удобный способ структурной организации связанных по смыслу переменных.
Агрегатным типом данных называется тип, конструируемый из элементов независимых (возможно различных) типов. Структуры являются одновременно агрегатным и производным типом данных.
Задание шаблонов и объявление структур
Новый тип данных для структуры создается путем описания шаблона структуры.
Шаблон структуры представляет собой описывающий элементы структуры список объявлений переменных.
struct [имя_шаблона] {список_элементов};
Область действия шаблона структуры определяется также, как и для переменной. Если шаблон встречается внутри блока, то он известен только в пределах этого блока. Однако, обычно шаблон структуры задается в начале файла или в заголовочном файле. Таким способом шаблон можно сделать доступным всем определенным в файле функциям. Как только шаблон описан, его имя может быть использовано в любом месте файла для объявления соответствующих переменных.
Пример 1
struct emp {
            char name[21];
            char id[8];
            double salary;     /*это шаблон, память не выделяется */
            };
int main ()
{
struct emp prgmr;              /*это объявление: выделяется память под переменную prgmr*/
int num;
...
}
int f(void)
{
struct emp supervisor;         /*это объявление: выделяется память под переменную supervisor*/
...
}
Элементы структуры хранятся в памяти в том порядке, в каком они появляются в списке элементов. При этом в структуру могут быть включены неиспользуемые байты, необходимые для соблюдения требований процессора к выравниванию данных. Так числовые типы данных обычно выравниваются на границу слова. Рисунок показывает гипотетическую ситуацию, так как фактический размер структуры машинно-зависим.
Рис. 1. Элементы структуры
Ниже рассматриваются альтернативные способы объявления структур.
Одновременное задание имени структуры и объявление структурных переменных
По желанию программиста, переменные типа структуры могут быть объявлены сразу после описания шаблона. Если шаблон и объявление переменной задаются вне функции, то переменная является внешней, в противном случае область ее видимости ограничивается блоком.
Пример 2
struct emp {
            char   name[21];
            char   id[8];
            double salary;
           } prgmr, employee[100], *p;
В прим. 2 объявляется структура prgmr, массив структур employee и указатель на структуру p.
Пропуск имени структуры
Другой способ состоит в том, чтобы опустить имя шаблона структуры. В этом случае все переменные данного типа должны быть перечислены после шаблона.
Пример 3
struct {
        char   name[21];
        char   id[8];
        double salary;
       } prgmr, employee[100], *p;
Использование объявления typedef
Третий способ состоит в том, чтобы использовать оператор typedef. Это избавит программиста от необходимости писать как ключевое слово struct, так и имя структуры при объявлении структурных переменных. Использование оператора typedef для задания однословного имени нового типа может также способствовать повышению удобочитаемости программы.
Пример 4
typedef struct emp {
                    char   name[21];
                    char   id[8];
                    double salary;
                   } EMPLOYEE;
EMPLOYEE prgmr, employee[100], *p;


Структура как параметр функции
Структура может быть передана функции как параметр. При этом на стеке в кадре вызываемой функции размещаются значения всех элементов структуры, а затем они ставятся в соответствие формальному параметру.
Функция может возвращать структурное значение.
В языке Си реализована операция копирования структур — присваивание значений элементов одной структуры элементам другой структуры того же типа при помощи операции присваивания. Первые компиляторы Си не обеспечивали этой возможности.
В прим. 1 показано, как в функцию передается структура и как функция возвращает структурное значение.
Пример 1
#include <stdio.h>
#include "emp.h"        /* содержит шаблон структуры emp */
int main()
{
   struct emp prgmr, raise(struct emp, double);
   ...
   printf("Old salary: %.2f\n", prgmr.salary);
   prgmr = raise(prgmr, .12);
   printf("New salary: %.2f\n", prgmr.salary);
}
struct emp raise(struct emp person, double increase)
{
  person.salary *= (1+ increase);
  return person;
}
Данная программа выполнялась бы более эффективно, если в функцию raise() передавался указатель на структуру. В этом случае при передаче управления в функцию передается лишь значение указателя, что делает доступными в функции элементы структуры. Этот вариант программы показан в прим. 2.
Пример 2
#include <stdio.h>
#include "emp.h" /* содержит шаблон структуры emp */
int main()
{
   struct emp prgmr;
   void raise(struct emp *, double);
   ...
   printf("Old salary: %.2f\n", prgmr.salary);
   raise(&prgmr, .12);
   printf("New salary: %.2f\n", prgmr.salary);
}
void raise(struct emp *person, double increase)
{
person->salary *= (1+ increase);
}


Инициализация структуры
Инициализация структуры заключается в присваивании начальных значений элементам структуры. Структуры могут быть проинициализированы при их объявлении.
Инициализирующая запись - это заключенный в фигурные скобки список, элементы которого разделяются запятыми и являются константами. Любые неинициализированные элементы внешних или статических структур по умолчанию равны 0. Значения неинициализированных элементов автоматических структур неопределены.
Пример 1
struct course {
              char name[30];
              int  number;
              char nickname[30];
              } title = {"C for new programmers",
                          1633,
                         "TRR YYY"};
struct mailinfo {
                char name[25];
                char mailadr[30];
                } proj_member [] = {
                    {"Ivanov Oleg", "rz3bb!imr"},
                    {"Klimov Alex", "rz3bb!ha"},
                    {"Petrova Elena", "rz3bb!wij"},
                    { "", ""}
                };
int f (void)
{
static struct mailinfo admin =
              {"Administrator", "root"};
...
}
В качестве признака окончания массива часто используется "нулевая" запись, поэтому зачастую используют циклы подобные прим. 2. Цикл будет выполняться до тех пор, пока первый символ поля name структуры, на которую указывает указатель p, не равен '\0'.
Пример 2
for (p = proj_member; *p->name != '\0'; p++)
   printf ("%s %s \n", p->name, p->mail_addr);


Указатели на структуры
Указатель на структуру объявляется точно так же, как и указатель на данные простых типов: используется операция * и указывается тип данных. Тип данных структуры указывается заданием ключевого слова struct и имени шаблона этой структуры.
Пример 1
Объявление указателей:
int        *ip;
struct emp *sp;
Пример 2
#include <stdio.h>
#define NUM_EMPS 100
void fillarray(struct emp *, int);
struct emp {                              /*это шаблон, память не выделяется */
            char   name[21];
            char   id[8];
            double salary;
            };
int main()
{
struct emp staff[NUM_EMPS], *sp;         /* выделяется память под массив структур*/
double     sal_tot = 0;

fillarray(staff , NUM_EMPS);             /*заполняет массив структур данными */
for (sp = staff; sp != &staff[NUM_EMPS]; sp++)
      sal_tot += sp->salary;
printf("total of salaries: %.2f\n", sal_tot);
printf("total of salaries: %.2f\n", sal_tot);
}
Рис. 1. Указатель на массив структур
В прим. 2 указатель sp после инициализации указывает на начало массива структур staff, т.е. содержит адрес первой структуры массива. Поскольку sp – указатель на структуру, то результат операции *sp является структурой, в данном случае staff[0]. Чтобы обратиться к полю salary структуры staff[0], можно использовать следующие выражения:
  • staff[0].salary
  • (*sp).salary (круглые скобки требуются в этом контексте, чтобы учесть приоритет операций).
Операция доступа к элементу структуры через указатель
Ввиду того, что указатели на структуры используются очень часто , в языке Си есть специальная операция доступа к элементу структуры через указатель (->), позволяющая сослаться на элемент структуры, заданный указателем на нее:
имя_указателя->имя_элемента
Эта операция является первичной и находится в самой верхней строке таблицы приоритетов операций языка Си.
После того, как указатель sp инициализирован и указывает на элемент массива структур staff[0], следующие выражения эквиваленты:
  • staff[0].salary /*имя_структуры.имя_элемента*/
  • sp->salary /*имя_указателя->имя_элемента*/


Общая структура программы на языке Си
Программа на языке Си — это набор функций и объявлений данных, содержащийся в одном или более файлах. Одна из этих функций - функция main().
Функция — это именованный фрагмент программы. Данные могут передаваться в функцию и функция может возвращать значение.
Программа на языке Си состоит из функций, произвольным образом упорядоченных в файле. Фактически функции могут быть размещены и в разных файлах. Концепция функции в языке Си покрывает все типы подпрограмм в других языках программирования: функции и процедуры.
Функции позволяют писать модульные программы, в которых каждая функция выполняет собственную задачу. Функции обычно невелики (страница или меньше, могут быть и однострочные).
Функции позволяют избежать дублирования кода в одной программе. Кроме того, несколько программ могут совместно использовать код функции.
Программы легче читаются, так как детали “скрыты” внутри функций.
Программы легче пишутся, поскольку функции позволяют разбить большие задачи на задачи меньшего размера, более простые для решения.
Выполнение программы начинается с выполнения функции main(). Обычно функция main() располагается в начале файла. Порядок расположения функций в файле не имеет значения. Часто они располагаются в алфавитном порядке для удобства поиска при модификации программы.
Пример 1
Структура программы на языке Си:

объявления
int main(...)/*определение прототипа функции*/
 {
  объявления

  операторы
 }

объявления
int f(...) /*определение прототипа функции*/
 {
  объявления

  операторы
 }

void g(...)/*определение прототипа функции*/
 {
   объявления

   операторы
 }
Примечание 1
Определения прототипов функций не могут быть вложенными.
Лабораторная работа 1. Первое знакомство с операционной системой UNIX и языком программирования Си


Самоссылающиеся структуры
Структура не может включать элементы того же типа, что и сама структура (рекурсивное определение). Однако, несколько элементов структуры могут быть указателями на такие же структуры. Подобные самоссылающиеся структуры обычно используются для создания динамических структур данных таких, как, например, связанные списки или деревья.
Пример 1
/* Связанный список */
struct info {int         num;
             float       sum;
             struct info *next
             };
Рис. 1. Связанный список
Пример 2
/* Дерево */
struct node {int         key;
             char        description [50];
             struct node *left, *right;
            };
Рис. 2. Дерево


Оператор switch
Оператор switch — управляющий оператор языка Си, реализующий ветвление алгоритма.
Часто в программах необходимо произвести выбор одного из нескольких вариантов дальнейших действий. Это можно сделать, используя конструкцию, состоящую из цепочки операторов if-else, но во многих случаях оказывается более удобным использование оператора switch.
Синтаксис оператора switch определен следующим образом:
switch (выражение)
     {
      case константа1: операторы
      case константа2: операторы
       . . .
      default:         операторы
      }
Выражение в скобках должно быть целого типа (можно использовать символьные константы, так как их тип целый). Выражение вычисляется и сравнивается с различными константами, записанными после меток case. Допускается использование констант целого или символьного типа или константное выражение указанных типов (например, 5, 'a', 2048/512). Переменные или вызовы функций использовать нельзя. Если подходящая константа найдена, вычисления продолжаются с оператора, следующего за меткой case, соответствующей этой константе. Затем выполняются все последующие операторы вплоть до появления оператора break или завершающей оператор switch скобки }. Метки case могут располагаться в любом порядке, но значения соответствующих констант должны быть различными.
switch, case, default и break — ключевые слова. Оператор break вызывает немедленный выход из оператора switch. Распространенная ошибка состоит в пропуске оператора break, когда необходим выход из оператора switch.
Метка default может отсутствовать. Если же она есть и среди указанных констант не найдено подходящей, управление передается операторам, следующим за меткой default. Если default отсутствует и среди указанных констант не найдено подходящей, оператор switch пропускается.
Рис. 1. Сравнение операторов switch и if
При использование оператора switch константы четко выделены, в противоположность операторам if-else, где они разбросаны по тексту. Разница в удобочитаемости возрастает с увеличением числа анализируемых случаев.
Если значение в операторе switch совпадает с одной из констант, вычисления продолжаются после соответствующей метки. Последующие операторы выполняются до тех пор, пока не встретится оператор break, не зависимо от того, предшествуют этим операторам какие-либо метки case. Говорят, что выполнение "проваливается", если не встретился оператор break. Пример 1 демонстрирует особенность оператора switch.
Пример 1
input = getchar();
switch (input)
{
case 'a':
case 'A': add_record();
          break;
case 'd':
case 'D': find_record();
          delete_record();
          break;
case 'u':
case 'U': find_record();
          query_change();
          change_record();
          break;
default:  printf("Illegal choice\n");
          break;
}
Когда требуется использовать оператор switch, а когда конструкцию if-else? Нельзя применять оператор switch, когда выбор вариантов основан на вычислении значения переменной или выражения вещественного типа. Удобного способа применить оператор switch в случае, когда возможные значения переменной попадают в некоторый диапазон, также не существует. Проще написать, например, так:
if (integer < 1000 && integer > 2)
При замене этой строки оператором switch придется ввести в программу метки для всех целых чисел от 3 до 999. Тем не менее, если есть возможность использования оператора switch, ею надо воспользоваться, так как программа будет выполняться более эффективно.
Лабораторная работа 5. Управляющий оператор выбора switch языка программирования Си


Синтаксис и семантика языка программирования
Термином язык в широком смысле слова обозначают любое средство общения, любую систему символов или знаков для обмена информацией.
Алгоритмический язык — набор символов и система правил образования и истолкования конструкций из этих символов для задания алгоритмов.
Алгоритмический язык для записи программ и данных называют языком программирования.
Любой язык, в том числе язык программирования, подчиняется ряду правил. Их принято разделять на правила, определяющие синтаксис языка, и правила, определяющие семантику.
Синтаксис языка — совокупность правил, определяющих допустимые конструкции (слова, предложения) языка, его форму.
Семантика языка — совокупность правил, определяющих смысл синтаксически корректных конструкций языка, его содержание.
Языки программирования относятся к формальным языкам, для которых, в отличии от естественных языков однозначно определены синтаксис и семантика. Описание синтаксиса языка включает определение алфавита и правил построения различных конструкций языка из символов алфавита и более простых конструкций. Для этого обычно используют форму Бэкуса-Наура (БНФ) или синтаксические диаграммы.
Семантику языка программирования закладывают в его компилятор. Таким образом, синтаксически корректная программа, написанная на языке программирования, после преобразования ее в последовательность машинных команд обеспечит выполнение ЭВМ требуемых операций.


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


Анализ
Постановка задачи: даны достоинства пяти выданных из колоды карт. Карты должны быть упорядочены по их достоинству (туз - старший). Разместить их в упорядоченном массиве и напечатать их отсортированные значения.
Выполните анализ задачи. Завершите разработку определения структуры данных и алгоритма, называемого sort_hand (сорт_расклад) и решающего данную задачу.
Ответ
Анализ: Упорядоченности можно добиться путем выбора первой карты, ее последовательного сравнения с картами от второй до пятой по порядку и взаимного перекладывания карт, при необходимости. Затем берется вторая карта и сравнивается с картами от третьей до пятой, и т.д.
Структура данных:
  • Массив из 5 элементов целого типа mas_card. Элементы массива могут принимать значения в следующем диапазоне: 1 - шестерка, 2 - семерка, 3 - восьмерка, 4 - девятка, 5 - валет, 6 - дама, 7 - король, 8 - туз.
  • Переменные целого типа i и j - счетчики циклов.
  • Вспомогательная переменная целого типа a .

Для решения задачи надо выполнить следующие действия:
  • заполнить массив, например, случайным образом, и напечатать его ;
  • отсортировать массив в порядке убывания;
  • напечатать отсортированный массив
Эти действия будет реализовывать алгоритм sort_hand:
sort_hand
   Выполнить
       input
       sorty
       output
   Выполнено
Более детально разрабатываются каждая из составных частей алгоритма sort_hand:
input
   Цикл i от 1 до 5
     Выполнить
     Определить случайным образом i-ый элемент массива mas_card
     Вывести этот элемент
     Выполнено

sorty
   Цикл i от 1 до 4
     Выполнить
     Цикл j от i+1 до 5
        Выполнить
        Если mas_card(i)<mas_card(j)

           Выполнить
           a=mas_card(i)
           mas_card(i)=mas_card(j)
           mas_card(j)=a
           Выполнено
        j=j+1
        Выполнено
      i=i+1
      Выполнено
output
   Цикл i от 1 до 5
     Выполнить

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


Двоичная арифметика
Выполните следующие действия двоичной арифметики:
  1. 100111001.11 + 110110.01
  2. 11100111. + 1111.1
  3. 10100011.001 — 101.01
  4. 11100111. — 1111.1
  5. 1100.01 [image] 101.011
  6. 11100111. 10.1
  7. 110010.001 / 101.1 (с точностью до 2 разрядов)
  8. 11100111. / 1111.1 (с точностью до 2 разрядов)
Ответ
  1. 101110000.
  2. 11110110.1
  3. 10011101.111
  4. 11010111.1
  5. 1000001.11011
  6. 1001000001.1
  7. 1001.00
  8. 1110.11
При неправильном ответе изучите:
Двоичная арифметика


Классы памяти
Что печатает эта программа (выполните теоретически)?
/* Файл stat1.c */
int k=1;
int main()
{
   int kinc(void), kdbl(void), kmod(int), kset(void);
   int k, j;
   r = kset();
   j = 1;
   while(j <= 3) {
     printf("\n k = %d, j = %d\n", k, j);
     printf("KINC k = %d\n", kinc());
     printf("KDBL k = %d\n", kdbl());
     printf("KMOD k = %d\n", kmod(k+j));
     j++;
   }
}
/* Файл stat2.c */
static int k=10;
int kinc(void)
{
   return k += 1;
}
int kdbl(void)
{
   return k *= 2;
}
int kmod(int k)
{
   static int j = 5;
   return k = j += k;
/* Файл stat3.c */
extern int k;
int kset(void)
{
   return k;
}
Ответ
k = 1, j = 1
KINC k = 11
KDBL k = 22
KMOD k = 7

k = 1, j = 2
KINC k = 23
KDBL k = 46
KMOD k = 10

k = 1, j = 3
KINC k = 47
KDBL k = 94
KMOD k = 14
При неправильном ответе изучите :
Обзор классов памяти


Прямой, обратный, дополнительный коды
Пусть длина разрядной сетки — 8
Представьте прямой, обратный и дополнительный коды следующих целых десятичных чисел:
  1. 5
  2. -5
  3. 43
  4. -43
  5. 115
  6. -115
Ответ
Таблица 1
Число | Прямой код | Обратный код | Дополнительный код
5 | 0000 0101 | не существует | не существует
-5 | 1000 0101 | 1111 1010 | 1111 1011
43 | 0010 1011 | не существует | не существует
-43 | 1010 1011 | 11010100 | 11010101
115 | 0111 0011 | не существует | не существует
-115 | 1111 0011 | 1000 1100 | 1000 1101

При неправильном ответе изучите:
Формы представления числовой информации


Вызовы и аргументы функции
Пусть задано следующее определение функции:
int func(int one, int two, double sum)
  {
  double num;
  ...
  }
  1. Сколько аргументов ожидает эта функция?
  2. Каковы типы one, two, sum?
  3. Напишите вызов функции func(), передающий ей правильные параметры.
  4. Каков тип возвращаемого значения?
  5. В чем разница между переменными sum и num?
  6. Что общего между переменными sum и num?
Ответ
  1. три
  2. int, int, double
  3. int x; x = func(4, 7, (double)15);
  4. int
  5. sum - это формальный параметр функции, num - это автоматическая переменная.
  6. Обе переменные располагаются на стеке и определены в течение времени выполнения функции.
При неправильном ответе изучите:
Определение функции


Определение прототипа функции
Допишите операторы определения прототипа функции, соответствующие вызовам этих функций в функции main():
int main()
  {
  int line[100];
  int s;

  s = adjust(line[0]);
  sum(line);
  }
... adjust(...)
{
...
}
...sum(...)
{
...
}
Ответ
int  adjust(int a)
  {
   ...
   return a*a;
  }
void sum(int b)
  {
  ...
  }
При неправильном ответе изучите:
Определение функции


Объявление прототипа функции
Для примера, приведенного в тесте Определение прототипа функции написать объявления прототипов функций.
Ответ
int main ()
{
int adjust(int);
void sum(int);
...
}
При неправильном ответе изучите:
Объявление функций


Идентификаторы
Какие идентификаторы допустимы в языке Си?
  1. x
  2. 6x
  3. PrevAmt
  4. employee name
  5. num_parts
  6. возраст5
  7. part5
  8. $pay
Ответ
1, 3, 5, 7
При неправильном ответе изучите:
Алфавит языка программирования Си


Операции инкрементации и декрементации
Определите значения переменных x и y после использования операций инкрементации и декрементации:
  1. x = 3; y = ++x;
  2. x = 3; y = x++;
  3. x = 3; y = --x;
  4. x = 3; y = x--;
Ответ
  1. x=4, y=4
  2. x=4, y=3
  3. x=2, y=2
  4. x=2, y=3
При неправильном ответе изучите:
Операции инкрементации и декрементации


Логические операции
Вычислите значения следующих выражений, предполагая, что h=0, x=1, y=5, z=9, m=0. Заполните таблицу.
Таблица 1
Выражение | Истина или ложь
(x<y) && (y<x) |
(x<y) || (y<x) |
(x>y) || (y>x) |
!(x<y && y<x) |
(y=0) && (x<z) |
(m<z) && (z<=h) |
(z==9) && (x ==m)

Ответ
Таблица 2
Выражение | Истина или ложь
(x<y) && (y<x) | ложь
(x<y) || (y<x) | истина
(x>y) || (y>x) | истина
!(x<y && y<x) | истина
(y=0) && (x<z) | ложь
(m<z) && (z<=h) | ложь
(z==9) && (x ==m) | ложь

При неправильном ответе изучите:
Логические операции


Операторы 2
Каким математическим выражениям соответствуют следующие операторы языка Си?
  1. z = a/b-2;
  2. a = (b-c)/f*g;
  3. a = 5-b-c/f/g;
  4. x=(a*a-b*b)/2*a*b;
  5. x=(a*a-b*b)/2*a-b;
  6. x=(a*a-b*b)/2/a/b;
Ответ
  1. z = [image] — 2
  2. a = [image] g
  3. a = 5-b-
  4. x = [image] a [image] b
  5. x = [image] a-b
  6. x =
При неправильном ответе изучите:
  • Операторы
  • Приоритеты операций
  • Арифметические операции


Операторы 1
Запишите следующие отношения в виде операторов языка Си:
  1. z =
  3. x =
Ответ
  1. z=a/b;
  2. a=(b-c)/(f*g); или a=(b-c)/f/g;
  3. x=(a*a-b*b)/(2*a*b); или x=(a*a-b*b)/2/a/b;
При неправильном ответе изучите:
  • Операторы
  • Приоритеты операций
  • Арифметические операции


Операции отношений
Вычислите значения следующих выражений, предполагая, что w=0, x=1, y=5. Заполните таблицу.
Таблица 1
Выражение | Числовое значение | Истина или ложь
1>2 |  |
x>1 |  |
y>x |  |
x>=y |  |
y>=5 |  |
x!=3 |  |
x==y |  |
w==x-1 |  |
x=y |  |
x=0 |  |
y |  |
'a'-040

Ответ
Таблица 2
Выражение | Числовое значение | Истина или ложь
1>2 | 0 | ложь
x>1 | 0 | ложь
y>x | 1 | истина
x>=y | 0 | ложь
y>=5 | 1 | истина
x!=3 | 1 | истина
x==y | 0 | ложь
w==x-1 | 1 | истина
x=y | 5 | истина
x=0 | 0 | ложь
y | 5 | истина
'a'-040 | 0101 | истина


Указатели 1
Использование операций получения адреса и косвенного доступа.
Каковы выходные данные следующей программы?
#include <stdio.h>
int main()
{
  int count = 10, x;
  int *ip;
  ip = &count;
  x=*ip;
  printf("count = %d, x = %d\n",count ,x);
}
Ответ
count = 10, x = 10
При неправильном ответе изучите:
Использование указателей
Передача адреса в функцию


Указатели 2
Использование операций получения адреса и косвенного доступа.
Каковы выходные данные следующей программы?
#include <stdio.h>
int main()
{
  int i1, i2;
  int *p, *q;

  i1 = 5;
  p = &i1;
  i2 = *p/2 + 10;
  q = p;
  printf("i1 = %d, i2 = %d\n", i1, i2);
  printf("*p = %d, *q = %d\n", *p, *q);
}
Ответ
i1 = 5, i2 = 12
*p = 5, *q = 5
При неправильном ответе изучите:
Использование указателей
Передача адреса в функцию


Указатели на структуры
Замените в программе все подчеркивания на правильные операторы, используя указатели на структуры.
#include <stdio.h>
struct emp{
          char   name[21];
          char   id[8];
          double salary;
};
int main()
{
struct emp prgmr;
___raise(struct emp ____, double);
printf("Old salary: %.0f\n", prgmr.salary);
raise(___prgmr, .12);
printf("New salary: %.0f\n", prgmr.salary);
}
___raise(struct emp ____person, double increase)
{
person___salary *= (1+increase);
}

Ответ
#include <stdio.h>
struct emp{
          char   name[21];
          char   id[8];
          double salary;
};
int main()
{
struct emp prgmr;
void raise(struct emp *, double);
printf("Old salary: %.0f\n", prgmr.salary);
raise(&prgmr, .12);
printf("New salary: %.0f\n", prgmr.salary);
}
void raise(struct emp *person, double increase)
{
person->salary *= (1+increase);
}
При неправильном ответе изучите:
Указатели на структуры


Приоритеты операций
Какие значения будут присваиваться переменным? Определите порядок вычисления операций в соответствии с приоритетом.
  1. x = 15+3*2-4
  2. x = (15+3)*2-4
  3. x = 15+3*2-4/2
  4. x=24/3*2/4
  5. x=24/(3*2)/4
  6. x=(24/3*2)/4
Ответ
  1. 17
  2. 32
  3. 19
  4. 4
  5. 1
  6. 4
При неправильном ответе изучите:
Приоритеты операций


Функция printf()
Дано:
#include <stdio.h>
int main()
{
   int val;
   float sum;
   char name[36];
...
}
Напишите вызов функции printf(), которая выполняет следующие действия:
  1. Выводит значение переменной val, строку; содержащуюся в массиве name и значение выражения sum/val.
  2. Выводит значение переменной val в поле, минимальной ширины 8 символов с выравниванием по левому краю; значение переменной sum в поле, минимальной ширины 10 символов с выравниванием по правому краю и с 3 знаками после десятичной точки.
Ответ
  1. printf("%d %s %f", val, name, sum/val);
  2. printf("%-8d %10.3f", val, sum);
При неправильном ответе изучите:
Функция printf()


Рекурсивные функции 1
Напишите рекурсивную функцию, возвращающую факториал положительного числа x:

Ответ
int fact(int x)
{
/* Можно проверять, что число не превосходит*/
/* максимально возможного значения,*/
/*фактариал которого помещается в int*/
  if (x==1)
    return  1;
  else
    return(x*fact(x-1));
}
Примечание 1
Другой вариант функции смотрите в тесте Условная операция 1
При неправильном ответе изучите:
Рекурсивные функции


Рекурсивные функции 2
Напишите рекурсивную функцию, возвращающую значение n-ого числа из последовательности Фибоначчи:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Ответ
int fib(int n)
{
   if(n==1 || n==2)
     return 1;
   else
     return (fib(n-1)+fib(n-2));
}
Примечание 1
Другой вариант функции смотрите в тесте Условная операция 2
При неправильном ответе изучите:
Рекурсивные функции


Функция scanf()
Дано:
#include <stdio.h>
int main()
{
   int x, sum;
   double diameter;
   char name[81];
   ...
}
Используя функцию scanf(), выполните следующие действия:
  1. Прочитайте целое число в десятичной форме в переменную sum.
  2. Прочитайте в переменную diameter вещественное число в форме с плавающей точкой.
  3. Пусть входной поток содержит строку "Ivanov Oleg". Что будет содержать массив name, если использовать одно из двух выражений:
    • scanf("%s",&name[0]);
    • scanf("%s",name);
Ответ
  1. scanf("%d",&sum);
  2. scanf("%e",&diameter);
  3. Массив name будет содержать:
  • Ivanov
  • Ivanov
При неправильном ответе изучите:
Функция scanf()


Структуры 2
Используя программу, текст которой приведен на рис. 1, заполните строки, помеченные буквами E, F, G и H.
Рис. 1. Текст программы
Ответ
#include <stdio.h>
struct t_info {
               float price;
               int   sold;
};
int main()
{
  struct t_info ticket;
  int    num;

  t_info.price = 500.;               /* E */
  t_info.sold = 0;                   /* F */
  t_info.sold += 25;                 /* G */
  printf("%.2f\n",t_info.sold * t_info.price);      /* H */
}
При неправильном ответе изучите:
Структуры


Структуры 1
Используя программу, текст которой приведен на рис. 1, определите, какому описанию (от A до D) соответствуют следующие определения:
  1. элементы или поля структуры t_info
  2. объявление и выделение памяти для одной структуры
  3. имя типа, позволяющее далее объявлять переменные этого типа
  4. шаблон структуры, описывающий поля структуры t_info, не выделяет память
Рис. 1. Текст программы
Ответ
  1. B
  2. D
  3. A
  4. C
При неправильном ответе изучите:
Структуры


Перевод чисел из двоичной ПСС в десятичную
Перевести заданное двоичное число в десятичную ПСС
  1. 01010101
  2. 10101010
  3. 00001111
  4. 01110111
  5. 01111000
  6. 11111111
  7. 00001000
  8. 00010000
  9. 00010001
  10. 00010010
  11. 00010011
  12. 00010100
Ответ
  1. 85
  2. 170
  3. 15
  4. 119
  5. 120
  6. 255
  7. 8
  8. 16
  9. 17
  10. 18
  11. 19
  12. 20
При неправильном ответе изучите:
Перевод числовой информации из одной ПСС в другую


Перевод чисел из двоичной ПСС в восьмеричную
    Перевести заданное двоичное число в восьмеричную ПСС
    1. 01010101
    2. 10101010
    3. 00001111
    4. 01110111
    5. 01111000
    6. 11111111
    7. 00001000
    8. 00010000
    9. 00010001
    10. 00010010
    11. 00010011
    12. 00010100
    Ответ
    1. 125
    2. 252
    3. 17
    4. 167
    5. 170
    6. 377
    7. 10
    8. 20
    9. 21
    10. 22
    11. 23
    12. 24
    При неправильном ответе изучите:
    Перевод числовой информации из одной ПСС в другую


Перевод чисел из восьмеричной ПСС в двоичную
Перевести заданное восьмеричное число в двоичную ПСС
  1. 125
  2. 252
  3. 17
  4. 167
  5. 170
  6. 377
  7. 10
  8. 20
  9. 21
  10. 22
  11. 23
  12. 24
Ответ
  1. 01010101
  2. 10101010
  3. 00001111
  4. 01110111
  5. 01111000
  6. 11111111
  7. 00001000
  8. 00010000
  9. 00010001
  10. 00010010
  11. 00010011
  12. 00010100
При неправильном ответе изучите:
Перевод числовой информации из одной ПСС в другую


Перевод чисел из двоичной ПСС в шестнадцатеричную
Перевести заданное двоичное число в шестнадцатеричную ПСС
  1. 01010101
  2. 10101010
  3. 00001111
  4. 01110111
  5. 01111000
  6. 11111111
  7. 00001000
  8. 00010000
  9. 00010001
  10. 00010010
  11. 00010011
  12. 00010100
Ответ
  1. 55
  2. AA
  3. F
  4. 77
  5. 78
  6. FF
  7. 8
  8. 10
  9. 11
  10. 12
  11. 13
  12. 14
При неправильном ответе изучите:
Перевод числовой информации из одной ПСС в другую


Перевод чисел из десятичной ПСС в двоичную
Перевести заданное десятичное число в двоичную ПСС
  1. 85
  2. 170
  3. 15
  4. 119
  5. 120
  6. 255
  7. 8
  8. 16
  9. 17
  10. 18
  11. 19
  12. 20
Ответ
  1. 01010101
  2. 10101010
  3. 00001111
  4. 01110111
  5. 01111000
  6. 11111111
  7. 00001000
  8. 00010000
  9. 00010001
  10. 00010010
  11. 00010011
  12. 00010100
При неправильном ответе изучите:
Перевод числовой информации из одной ПСС в другую


Перевод чисел из шестнадцатеричной ПСС в двоичную
Перевести заданное шестнадцатеричное число в двоичную ПСС
  1. 55
  2. AA
  3. F
  4. 77
  5. 78
  6. FF
  7. 8
  8. 10
  9. 11
  10. 12
  11. 13
  12. 14
Ответ
  1. 01010101
  2. 10101010
  3. 00001111
  4. 01110111
  5. 01111000
  6. 11111111
  7. 00001000
  8. 00010000
  9. 00010001
  10. 00010010
  11. 00010011
  12. 00010100
При неправильном ответе изучите:
Перевод числовой информации из одной ПСС в другую


Условная операция 1
Напишите рекурсивную функцию (смотрите тест Рекурсивные функции 1), возвращающую факториал положительного числа x:

Используйте условную операцию.
Ответ
int fact(int x)
  {
  return (x==1)?1:(x*fact(x-1));
  }
При неправильном ответе изучите:
  • Рекурсивные функции
  • Условная операция


Условная операция 2
Напишите рекурсивную функцию (смотрите тест Рекурсивные функции 2), возвращающую значение n-ого числа из последовательности Фибоначчи:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Используйте условную операцию.
Ответ
int fib(int n)
  {
  return (n==1 || n==2)?1:(fib(n-1)+fib(n-2));
  }
При неправильном ответе изучите:
  • Рекурсивные функции
  • Условная операция


Оператор цикла while 2
Найдите ошибку в цикле while:
/* подсчет введенных символов кроме символов "новая строка"*/
input = getchar();
while (input = '\n') {
   input = getchar();
   c = c+1;
   }
Ответ
В выражении в скобках цикла while используется не операция сравнения, а операция присваивания.
При неправильном ответе изучите:
Оператор цикла while.


Оператор цикла while 1
Надо выполнить следующую последовательность действий:
  • присвоить переменной a нулевое значение
  • повторить 10 раз:
    • переменную a увеличить на 1;
    • переменную b увеличить на значение переменной а.
Найдите ошибку в циклах while, реализующих эти действия:
Вариант 1.
a=0;
while (a<10)
   a = a+1;
   b = b+a;
Вариант 2.
a=0;
while (a<10);{
   a = a+1;
   b = b+a;
   }
Ответ
В первом варианте телом цикла является только оператор a = a+1;.
Во втором варианте телом цикла является пустой оператор.
При неправильном ответе изучите:
Оператор цикла while.


Перевод числовой информации из одной ПСС в другую
В процессе преобразования информации в ЭВМ возникает необходимость перевода чисел из одной позиционной системы счисления (ПСС) в другую.
Для реализации машинных алгоритмов перевода применяют приведенные ниже способы.
Правило перевода целых чисел
Целое число в системах счисления с основаниями [image] и [image] можно представить следующим образом:
(1)

В общем виде задачу перевода из ПСС с основанием [image] в ПСС с основанием [image] можно представить как задачу определения коэффициентов [image] нового ряда , изображающего число в ПСС с основанием [image].
Целое число в ПСС с основанием [image] записывается в виде:

Преобразовав это выражение по схеме Горнера, получим:
(2)

Правую часть выражения (1) разделим на величину основания [image]. В результате определим первый остаток [image] и целую часть [image].
Разделив целую часть на [image], найдем второй остаток [image]. Повторив процесс деления [image] раз, получим последнее целое частное [image], которое, по условию, меньше основания ПСС [image] и является старшей цифрой числа, представленного в ПСС с основанием [image].
Пример 1
Перевести десятичное число [image] =98 в двоичную ПСС ( [image] =2).

Правило перевода правильных дробей
Правильную дробь в системах счисления с основаниями [image] и [image] можно представить следующим образом:

Преобразовав это выражение по схеме Горнера, получим:
(3)

Если правую часть выражения (2) умножить на [image], то получится новая неправильная дробь, в целой части которой будет число [image]. Умножив затем оставшуюся дробную часть на величину основания [image], получим дробь, в целой части которой будет [image], и т.д. Повторяя процесс умножения [image] раз, найдем все [image] цифр в новой ПСС. При этом все действия должны выполняться по правилам [image] –арифметики, и, следовательно, в целой части получающихся дробей будут проявляться эквиваленты цифр новой системы счисления, записанные в исходной ПСС.
Пример 2
Перевести десятичную дробь А=0.625 в двоичную ПСС (q2=2)
0.625 [image] 2 = 1.25 ( [image] =1)
0.25 [image] 2 = 0.5 ( [image] =0)
0.5 [image] 2 = 1.0 ( [image] =1)
Ответ: А2=0.101
При переводе правильных дробей из одной ПСС в другую можно получить дробь в виде бесконечного или расходящегося ряда. Процесс перевода можно закончить, если появится дробная часть, имеющая во всех разрядах нули, или будет достигнута заданная точность перевода. Точность перевода — это требуемое количество разрядов в представлении результата. При переводе дроби необходимо указать количество разрядов числа в новой ПСС. При этом возникает погрешность перевода чисел, которую надо оценить.
Для перевода неправильных дробей из одной ПСС в другую необходим раздельный перевод целой и дробной частей по правилам, описанным выше. Полученные результаты записываются в виде новой дроби в ПСС с основанием [image].
Существуют и другие методы перевода из одной ПСС в другую.


Переименование типа
Оператор typedef — это оператор языка Си, по которому задается новое имя (синоним) существующему типу данных.
Синтаксис оператора typedef похож на объявления переменной:
typedef существующий_тип новое_имя
Имена подчиняются тем же правилам, что и прочие идентификаторы.
Хотя объявления typedef не являются директивами препроцессора языка Си, они часто используются в файлах заголовков.
Оператор typedef может использоваться также для объявления производных типов, таких как, например, матрица 20 на 40 целых значений.
Использование заглавных букв в именах типов, определенных оператором typedef, — это соглашение, а не правило.
Пример 1
typedef char BYTE;
typedef unsigned short USHORT;
typedef int MATRIX[20][40];
typedef int WORD;
int main()
{
BYTE input;
WORD buf [512];
MATRIX prev, current;
. . .
}


Основные типы данных
В языке Си все переменные должны быть объявлены явно.
Объявлении переменной — это оператор языка Си, который выглядит следующим образом:
тип идентификатор;
Тип задается соответствующим ключевым словом, например, int или char.
Тип данных определяет формат представления данных.
Идентификатор — в данном случае это имя переменной. В одном операторе объявления могут быть объявлено несколько переменных одного типа путем перечисления идентификаторов, отделенных друг от друга запятыми.
На рис. 1 приводятся основные типы данных языка Си.
Рис. 1. Основные типы данных языка Си
Размер данных определенного типа, а также диапазон значений, которые они могут принимать, зависят от реализации и не зафиксированы в языке Си. Это позволяет компилятору языка Си генерировать программы, выполняемые на конкретном процессоре с максимальной эффективностью. В файлах <limits.h> и <float.h> содержится информация о фактических размерах и диапазонах значений для различных компиляторов и различных процессоров.
Данные целого типа могут быть определены как со знаком, так и без знака. Положительные целые числа хранятся в памяти в прямом коде, отрицательные — в дополнительном коде.
Размер данных типа char равен одному байту данной машины.
Размер данных типа short меньше или равен размеру int.
Данные типа int обычно имеют размер машинного слова.
Данные типа long , соответственно, имеют размер больший или равный размеру int.
В общем, для [image] –битовой машины значение максимального целого числа со знаком (тип int ) равно [image], а наибольшее беззнаковое целое (тип unsigned int) равно [image].
Вещественные данные хранятся в формате с плавающей точкой. Размер данных типа long double больше или равен размеру double.
Размещение в памяти данных целого типа в языке Си
Пример 1
signed char a = 5, b = -5;
Данные типа char занимают один байт – 8 разрядов:
+5 0 0000101 (прямой код)
-5 1 1111011 (дополнительный код)
unsigned signed char a = 5;
5 00000101
signed int a = 5, b = -5;
Данные типа int занимают одно машинное слово – 4 байта – 32 разряда:
+5 0 0000000000000000000000000000101 (прямой код)
-5 1 1111111111111111111111111111011 (дополнительный код)
В прим. 1 показано, как хранятся данные целого типа в памяти гипотетической машины с 8-разрядным байтом и 32-разрядным словом. Реальный внутренний формат целых типов зависит от процессора. Например, разные процессоры могут располагать байты целого числа в разном порядке.
Лабораторная работа 2. Целые типы данных в языке программирования Си


Объединения
Объединение — объект, который в каждый момент времени содержит один из нескольких элементов различных типов.
Все компоненты объявления структур, такие как шаблоны, имена типа, имена элементов и т.д. применимы и при объявлении объединений. Единственное отличие состоит в том, что при объявлении объединения вместо ключевого слова struct используется union.
Примечание 1
Объединение не может иметь битовые поля.
Операции доступа к элементу структуры (операция . и операция ->) могут применяться и к объединениям. Допустимы массивы объединений и указатели на объединения. Объединения могут передаваться функции как параметры и возвращаться функцией.
Пример 1
struct s_tag{
            char c;
            int i;
            doubl d;
            } s_item;
Рис. 1. Структура
Пример 2
union u_tag{
           char c;
           int i;
           doubl d;
           } u_item;
Рис. 2. Объединение
В отличие от структуры, объединение может в любой момент времени содержать только один из своих элементов. Объединение позволяет использовать одну область памяти для хранения различных видов данных в разные моменты времени. Фактически, объединение — это структура, в которой все поля начинаются со смещением 0, таким образом, поля накладываются друг на друга.
Пример 3
printf("размер структуры = %d\n", sizeof(s_item));
printf("размер объединения = %d\n", sizeof(u_item));
Вывод:
размер структуры = 16
размер объединения = 8
Объединения часто включаются в структуры, один из элементов которых является ключом, указывающим тип хранимого в памяти элемента объединения.
Пример 4
/* содержит информацию о работающих служащих и пенсионерах */
struct mail {
            char id;      /* a - active (), r - retired()*/
            union{
                 struct{char name[30];
                        char dept[10];
                        char location[3];
                        } active;
                 struct{char name[30];
                        char street[20];
                        char city_state[3];
                        char zip[5];
                        } retired;
                 } info;
             } preson;
Структура типа struct mail используется для хранения почтового адреса работающего служащего или пенсионера. При заполнении структуры этого типа информацией в нее заносится порция данных, соответствующая элементам active или retired объединения info. Поле id устанавливается равным 'a' или 'r' для указания фактически записанного в объединении элемента. При применении объединения используется меньше памяти, чем в случае применения структуры, которая имела бы идентичные поля, но некоторые из них не использовались бы.

Рис. 3.  Объединение как элемент структуры


Условная компиляция
Условная компиляция — это средство препроцессора, которое в зависимости от условия включает в программу операторы языка Си или препроцессора.
Условная компиляция позволяет строить гибкие программы на языке Си. Это средство особенно часто используется при разработке программ, которые должны переноситься на различные процессоры, в различные операционные системы и в прочие операционные среды.
Условная компиляция реализуется с помощью следующих директив препроцессора:
  • #if
  • #else
  • #elif (аналогично else if)
  • #endif
  • #ifdef
  • #ifndef
В основном, условная компиляция выполняется следующим образом:
если это-и-это истинно
     вставить указанные здесь операторы в исполнимую программу
Можно задать и ветвь иначе.
Директивы #if используются для вставки строк в файл, в случае, если истинно некоторое константное выражение.
Директивы #ifdef используются для вставки строк, если определена указанная поименованная константа.
Для вставки строк при условии, что константа не имеет определения, вместо директивы #ifdef можно использовать директиву #ifndef.
Большинство препроцессоров Си включает предопределенную константу, имя которой указывает на используемый компьютер, со значением равным 1 (истина). Это надо проверять в документации используемого препроцессора.
Пример 1
#include "local.h"
#if vax||u3b
#define MAGIC 330
#elif u3b5
#define MAGIC 430
#elif u3b2
#define MAGIC 530
#else
#define MAGIC 500
#endif
Чтобы изменить значение используемой поименованной константы, следует применять директиву #undef перед изменяющей значение директивой #define (этого требуют многие препроцессоры). Не является ошибкой применение #undef к еще неопределенному идентификатору, либо переопределение макроса или поименованной константы той же самой строкой-шаблоном.
Пример 2
#ifdef LIMIT      /* #if определена константа LIMIT*/
#undef LIMIT
#endif
#define LIMIT 1000
Пример 3
f()
{
...
#ifdef DEBUG
   printf ("x=%d\n", x);
   printf ("y=%d\n", y);
#endif
...
}
Для запуска программы в режиме отладки надо включить в файл определение поименованной константы:
#define DEBUG
Тогда любые операторы, находящиеся между #ifdef DEBUG и #endif будут включены в исполнимый код. Возможно любое количество пар #ifdef и #endif. В системах UNIX константа DEBUG может быть определена не в программе, а в вызывающей ее командной строке:
$cc -Xa -DDEBUG prog.c


Условная операция
Условная операция - операция языка Си, которая по действию аналогична оператору if-else.
Синтаксис условной операции:
выражение1 ? выражение2 : выражение3
Если выражение1 истинно, значением всего выражения будет значение выражения2, иначе — выражения3.
Вместо оператора if-else иногда может быть использована условная операция ?:. Она выполняет проверку и выбор между двумя значениями внутри одного выражения. В отличие от конструкции if-else, являющейся оператором, эта операция может использоваться там, где синтаксис требует указывать выражение. Поэтому использование условного оператора ?: зачастую является более кратким и эффективным.
Подобно другим операциям языка Си, условная операция ?: указана в таблице приоритетов операций языка Си. Это единственная операция, составленная из двух символов, разделенных между собой выражениями (в отличии от &&, ||, >=, <= и т.д.). Это единственная операция, требующая три операнда.
Пример 1
/* Функция вычисления абсолютной величины*/
int abs(int x)
{
   if ( x >= 0)
       return x;
   else
       return -x;
}
Пример 2
/* Альтернативный вариант*/
int abs(int x)
{
   return (x >= 0) ? x : -x;
}
Пример 3
largest = x > max ? x : max;


Обзор классов памяти
Класс памяти - характеристика переменной, которая определяет способ ее хранения .
Язык Си реализует следующие классы памяти:
  • автоматический
  • регистровый
  • внешний
  • статический
Характеристики классов памяти:
  • область действия — часть программы, в которой определено имя;
  • время жизни — промежуток времени, в течении которого содержимое переменной остается правильным;
  • размещение связанных с идентификатором значений в памяти.
табл. 1 обобщает характеристики классов памяти языка Си. Правильное использование классов памяти улучшает структуру, модульность, повышает удобочитаемость и скорость работы программы.
Таблица 1
Класс | Область действия | Время жизни | Место хранения | Инициализация массивов, структур | Значение по умолчанию
Автоматический | Блок | Исполнение блока | Стек | Да | Не определено
Регистровый * | Блок | Исполнение блока | Машинный регистр | Нет | Не определено
Внешний ** | Объявление до конца файла | Постоянно | Область данных | Да | 0
Статический внешний*** | Объявление до конца файла | Постоянно | Область данных | Да | 0
Статический внутренний | Блок | Постоянно | Область данных | Да | 0

Примечание 1
* — повышение быстродействия
** — доступность из других файлов
*** — недоступность из других файлов
Рис. 1. Размещение данных различных классов памяти
На рис. 1 показано, как типичный компилятор Си размещает команды и данные компилируемой программы на языке Си. Детали зависят от системы. Например, в некоторых системах стек растет от больших адресов к меньшим, в других справедливо обратное. Команды могут размещаться после данных или наоборот. Стек и области данных могут динамически расширяться посредством системных вызовов.
Раздел команд содержит машинный код, определяющий логику программы. На некоторых машинах этот раздел защищен от записи, так как программы не могут сами себя изменять.
Объектам, размещенным в области данных, назначается определенное расположение в этой области, остающееся неизменным во время выполнения этой программы (при использовании виртуальных адресов они имеют один и тот же относительный адрес). В этой области размещаются внешние и статические переменные. Если они не инициализируются в программе, то они устанавливаются в нуль перед выполнением программы и "остаются живыми" в течении времени выполнения программы. Строковые константы, например, заключенные в кавычки строки в вызове функции printf(), также запоминаются в этой области.
Стек является динамической областью данных. Память в стеке многократно используется повторно при вызовах функций и выходе из них. На стеке размещаются автоматические переменные и параметры функции.


Оператор цикла while
Оператор цикла while — управляющий оператор языка Си, реализующий выполнение цикла в алгоритме.
Синтаксис оператора цикла while определяется следующим образом:
while (выражение)
   оператор1
Рис. 1. Цикл while
На рис. 1 представлена блок-схема цикла while. Вычисляется выражение; если оно истинное (ненулевое), выполняется оператор. Выражение вычисляется снова и, если оно остается истинным, снова выполняется оператор, и т.д. Если выражение ложное, происходит выход из цикла. Как правило, значение выражения изменяется в результате выполнения некоторых действий в цикле. Оператор в цикле while выполняется 0 или более раз. Оператор может быть простым или составным (блоком).
Пример 1
count = 1;
while (count < 100)
    {
     оператор
     оператор
     count += 1;
    }
Пример 2
while ((c = getchar()) != '\n')
   {
    оператор
    оператор
    оператор
   }
Пример 3
/* Эта программа считает и строки */
/* и символы во входном потоке */
#include <stdio.h>
int main ()
{
  int lines = 0, chars = 0, c;
 /* читать до конца EOF */
  while ((c = getchar()) != EOF)
      {
       chars += 1;
       if (c == '\n')
           lines += 1;
       }
    printf ("%d lines \n", lines);
    printf ("%d characters \n", chars);
  }
Лабораторная работа 4. Управляющие операторы if и while


Обзор побитовых операций
Побитовые операции — это операции, работающие с отдельными битами операндов. Операнд должен быть целочисленным, т.е. иметь тип long long, long, unsigned, int, shor или char.
Приоритеты побитовых операций приведены в таблице приоритетов операций языка Си.
Поразрядное дополнение (~) – это унарная операция, результатом которой является значение, полученное поразрядным дополнением операнда, т.е. каждый бит со значением 0 заменяется на 1 и наоборот.
Поразрядное умножение И (&) – значением выражения, использующего эту операцию, является поразрядное логические умножение двух операндов.
Исключающее ИЛИ (^) — значением выражения, использующего эту операцию, является поразрядное исключающее ИЛИ ее операндов.
Поразрядное сложение ИЛИ (|) — значением выражения, использующего эту операцию, является поразрядное логические сложение двух операндов.
Знаки этих побитовых операций являются односимвольными. Не путайте их с операциями логического умножения && (И) и логического сложения|| (ИЛИ). Значением выражений, использующих логические операции может быть только истина или ложь. Значением побитовых операций обычно является новое битовое значение.
Сдвиг влево (<<) — значением выражения, использующего эту операцию, является битовое представление левого операнда, сдвинутого влево на число битов, определяемое значением правого операнда.
Сдвиг вправо (>>) — значением выражения, использующего эту операцию, является битовое представление левого операнда, сдвинутого вправо на число битов, определяемое значением правого операнда.
Пример 1
int num1, num2;
num1 = 5;  /* 00101 */
num2 = 11; /* 01011 */
Рис. 1. Поразрядное И
Рис. 2. Поразрядное ИЛИ
Рис. 3. Исключающее ИЛИ
Значением выражения, содержащего операцию поразрядного дополнения, является логическое отрицание значения операнда. Значения битов инвертируются: единицы заменяются нулями, нули – единицами.
Пример 2
num1 = 5;    /* 00101 */
num1             00101
~num1            11010
Операции сдвига << и >>
Целочисленное_выражение << Целочисленное_выражение
Целочисленное_выражение >> Целочисленное_выражение
При сдвиге значения операнда влево биты справа заполняются нулями.
При сдвиге вправо возможны следующие ситуации:
  • при сдвиге положительных и беззнаковых значений биты слева заполняются нулями;
  • при сдвиге отрицательных значений результат совпадает с результатом арифметического или логического сдвига в зависимости от аппаратного обеспечения ЭВМ: логический сдвиг вправо заполняет освобождающиеся слева биты нулями; арифметический сдвиг размножает знаковый разряд, т.е. самый левый бит.
Пример 3
Предположим, что программа выполняется на 8-разрядном компьютере. Первый (старший) разряд является знаковым.
x = x<<2; /* сдвиг значения переменной x влево на 2 бита */
исходное x = 00011000
Результат операции x<<2 есть 01100000.
y >>= 3; /* сдвиг значения переменной y вправо на 3 бита */
исходное y =11011001
Результат операции y>>3 есть 00011011 для логического сдвига и 11111011 для арифметического сдвига.


Функции gets() и puts()
Функция gets() — стандартная функция языка Си для ввода строки.
Объявление прототипа функцииgets():
char *gets(char *s)
Функция puts() — стандартная функция языка Си для вывода строки.
Объявление прототипа функции puts():
int puts(const char *s)
Эти объявления находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция gets() вводит входную строку из потока stdin. При этом символ новой строки отбрасывается, а введенные символы дополняются в конце нулевым символом (символом '\0'). Необходимо позаботиться, чтобы принимающий массив символов был достаточно большим для размещения наибольшей возможной строки. Функция возвращает NULL по концу файла, если символов для чтения нет.
В отличии от функции scanf(), использующей спецификатор %s, функция gets() читает полную строку, включая символы пробела и табуляцию.
Функция puts()выводит заканчивающуюся нулевым символом строку символов в поток stdout. При этом вместо нулевого символа выводится символ новой строки.
Функции fgets()и fputs()подобны функциям gets() и puts()(Функции чтения-записи).
Пример 1
#include <stdio.h>
#define LINELEN 256
int main()
  {
  char name[LINELEN+1];
  printf("Введите полное имя:");
  if (gets(name) == NULL)
    printf("Ошибка\n");
  else
    puts(name);
}
Вывод программы:
Введите полное имя:Иванов Петр Васильевич
Иванов Петр Васильевич


Функции getchar() и putchar()
Функция getchar() — стандартная функция языка Си для ввода символа.
Объявление прототипа функцииgetchar():
int getchar(void)
Функция putchar() — стандартная функция языка Си для вывода символа.
Объявление прототипа функции putchar():
int putchar(int c)
Эти объявления находятся в файле заголовков stdio.h, поэтому программы, использующие эти функции, должны включать следующую директиву препроцессора:
#include <stdio.h>
Функция getchar() читает из стандартного устройства ввода один символ и возвращает его код в соответствии с таблицей кодирования. Если при попытке чтения были обнаружены конец файла или ошибка, функция getchar() возвращает значение EOF. При вводе с терминала getchar() возвращает EOF, если пользователь нажал в начале строки комбинацию клавиш <CTRL>, <d>.
Программа, работающая в среде ОС UNIX и использующая функцию getchar() для ввода, не получит вводимые с клавиатуры символы до тех пор, пока пользователь не нажал клавишу <RETURN>. Это позволяет пользователю корректировать набираемые символы (например, с помощью забоя) прежде, чем программа их получит. Однако, некоторые программы, например, редактор “vi”, работают с терминалом в режиме, при котором вводимые символы немедленно становятся доступными программе.
Функция putchar() выводит один символ в стандартный вывод (по умолчанию — на экран терминала), возвращает при успешном завершении код выводимого символа иначе EOF. Обычно возвращаемое значение не обрабатывается.
Пример 1
#include <stdio.h>
/* копирование ввода на вывод */
int main()
{
  int c;

  while ((c=getchar())!=EOF)
     putchar(c);
}


Указатели на структуры
Указатель на структуру объявляется точно так же, как и указатель на данные простых типов: используется операция * и указывается тип данных. Тип данных структуры указывается заданием ключевого слова struct и имени шаблона этой структуры.
Пример 1
Объявление указателей:
int        *ip;
struct emp *sp;
Пример 2
#include <stdio.h>
#define NUM_EMPS 100
void fillarray(struct emp *, int);
struct emp {                              /*это шаблон, память не выделяется */
            char   name[21];
            char   id[8];
            double salary;
            };
int main()
{
struct emp staff[NUM_EMPS], *sp;         /* выделяется память под массив структур*/
double     sal_tot = 0;

fillarray(staff , NUM_EMPS);             /*заполняет массив структур данными */
for (sp = staff; sp != &staff[NUM_EMPS]; sp++)
      sal_tot += sp->salary;
printf("total of salaries: %.2f\n", sal_tot);
printf("total of salaries: %.2f\n", sal_tot);
}
Рис. 1. Указатель на массив структур
В прим. 2 указатель sp после инициализации указывает на начало массива структур staff, т.е. содержит адрес первой структуры массива. Поскольку sp – указатель на структуру, то результат операции *sp является структурой, в данном случае staff[0]. Чтобы обратиться к полю salary структуры staff[0], можно использовать следующие выражения:
  • staff[0].salary
  • (*sp).salary (круглые скобки требуются в этом контексте, чтобы учесть приоритет операций).
Операция доступа к элементу структуры через указатель
Ввиду того, что указатели на структуры используются очень часто , в языке Си есть специальная операция доступа к элементу структуры через указатель(->), позволяющая сослаться на элемент структуры, заданный указателем на нее:
имя_указателя->имя_элемента
Эта операция является первичной и находится в самой верхней строке таблицы приоритетов операций языка Си.
После того, как указатель sp инициализирован и указывает на элемент массива структур staff[0], следующие выражения эквиваленты:
  • staff[0].salary /*имя_структуры.имя_элемента*/
  • sp->salary /*имя_указателя->имя_элемента*/


Операторы 2
Каким математическим выражениям соответствуют следующие операторыязыка Си?
  1. z = a/b-2;
  2. a = (b-c)/f*g;
  3. a = 5-b-c/f/g;
  4. x=(a*a-b*b)/2*a*b;
  5. x=(a*a-b*b)/2*a-b;
  6. x=(a*a-b*b)/2/a/b;
Ответ
  1. z = [image] — 2
  2. a = [image] g
  3. a = 5-b-
  4. x = [image] a [image] b
  5. x = [image] a-b
  6. x =
При неправильном ответе изучите:
  • Операторы
  • Приоритеты операций
  • Арифметические операции


Учебный курс
Программирование на языке Си

Рассматриваются вопросы представления данных в компьютере, основные типы данных, структура программы на языке программирования Си, основные операции и операторы, классы памяти, некоторые стандартные функции языка Си.
Содержание базового курса и лабораторных работ соответствует курсу лекций "Информатика", которые автор читает в МГТУ им. Н.Э.Баумана. Предполагается, что лабораторные работы будут проводится в ОС Linux. Начальные сведения по операционной системе Linux приводятся в Приложении.
Учебный курс разработан для студентов вузов, обучающихся по специальностям, связанным с информатикой. Он также может быть полезен всем изучающим программирование самостоятельно.
Разработчик курса: ст. преподаватель Федорук Е.В.

Содержание