x ) printf(“ Заданное число больше\n”);
} while ( s-x );
printf(“ Вы угадали число \n”);
printf(“ Затратили на угадывание %d попыток\n”,n);
}
В отличие от оператора for, в теле цикла операторов while и do-while
(если оно не пустой оператор) обязательны действия, направленные на
34
изменения значения условия. Если таких действий нет, то циклы while
и do-while могут стать бесконечными. В вышеуказанном примере это
действия, связанные с изменением значения переменной x.
3.4. Операторы break и continue
Оператор break имеет два применения. Первое – окончание case в
операторе switch. Второе – немедленное окончание цикла, не связанное
с проверкой обычного условия окончания цикла. Когда оператор break
встречается внутри оператора цикла (является частью тела цикла), то
происходит немедленный выход из цикла и переход к выполнению оператора, следующего за оператором цикла:
# include < stdio.h >
/* Пример 16 */
main()
{
int i;
for ( i=0; i<1000; i++)
{
printf(“%d - %d\n”,i,i*i*i);
if ( i*i*i > 10000 ) break
}
}
В данном примере выход из цикла произойдет, когда значение куба
переменной i превысит величину 10000.
Еще один полезный оператор – continue. Если оператор continue встретился в теле цикла, то он передает управление на начало следующей
итерации цикла. В циклах while и do-while на проверку условия, в цикле for – на изменение параметра цикла. Этот оператор необходим, когда
надо закончить текущую итерацию цикла и не выполнять оставшиеся
операторы, входящие в тело цикла
# include < stdio.h >
/* Пример 17 */
main()
{
int i;
for ( i=7; i<1000; i++)
{
35
if ( i%7 != 0 ) continue;
printf(“%d”,i);
}
}
При выполнении данной программы производится печать целых чисел, кратных 7 и не превышающих величину 1000. Если переменная i
не кратна 7, осуществляется переход на следующую итерацию с новым
значением переменной i.
3.5. Оператор безусловного перехода
Оператор безусловного перехода имеет форму:
goto метка;
При написании программы можно вполне обходиться без этого оператора, но тем не менее он иногда бывает полезен. Метка – это идентификатор, за которым следует двоеточие. Меткой необходимо претворять оператор, на который надо сделать безусловный переход. Помеченный оператор должен находиться в той же функции, что и оператор
goto. Наиболее полезен оператор goto для выхода из вложенных циклов,
хотя в языке С имеются другие средства для решения этой задачи.
36
4. МАССИВЫ И УКАЗАТЕЛИ
4.1. Понятие массива, объявление массива
На основе базовых (встроенных) типов данных язык С позволяет
строить другие типы данных и структуры данных. Массив – одна из
наиболее простых и часто используемых структур данных. Под массивом понимают набор данных одного и того же типа, собранных под
одним именем. Каждый элемент массива определяется именем массива
и порядковым номером элемента, который называется индексом. Индекс в языке С всегда целое число.
Основная форма объявления массива размерности N такова:
тип <имя массива>[размер1][размер2]…[размерN]
Чаще всего используются одномерные массивы:
тип <имя массива>[размер];
Тип – базовый тип элементов массива, размер – количество элементов одномерного массива. При описании двумерного массива его объявление имеет следующий вид:
тип <имя массива>[размер1][размер2];
В этом описании можно трактовать объявление двумерного массива
как объявление массива массивов, т. е. массив размера [размер2], элементами которого являются одномерные массивы <имя массива>[размер1].
Размер массива в языке С может задаваться константой или константным выражением. Нельзя задать массив переменного размера. Для этого существует отдельный механизм ( динамическое выделение памяти).
Сначала обсудим более подробно одномерные массивы. В языке С
индекс всегда начинается с нуля. Когда речь идет о первом элементе
массива, то имеем в виду элемент с индексом 0. Если объявляется массив int a[100]; , то это значит, что массив содержит 100 элементов от
a[0] до a[99]. Для одномерного массива легко определить, сколько байт
в памяти будет занимать этот массив:
37
Количество байт=<размер базового типа>*<количество элементов> .
В языке С под массив всегда выделяется непрерывное место в памяти. В языке С не проверяется выход значения индекса за пределы массива. Поэтому, при написании и отладке программ с использованием
массивов, этот момент необходимо учитывать.
4.2. Массивы символов, строки. Функции работы со строками
Массивы типа char – символьные массивы – занимают особое место.
Во многих языках программирования есть специальный тип данных –
строка символов (string). В языке С такого типа нет, а работа со строками реализована путем использования одномерных массивов типа char.
В языке С символьная строка – это одномерный массив типа char, заканчивающийся нулевым байтом. Для нулевого байта определена специальная символьная константа – ‘\0’. Это необходимо учитывать при
описании соответствующего символьного массива. Так, если строка
должна содержать N символов, то в описании массива необходимо указать N+1 элемент.
Например, описание int str[12], предполагает, что строка содержит
11 символов, а последний байт зарезервирован под нулевой байт.
Хотя в языке С нет специального типа строки, язык допускает строковые константы. Строковая константа – это список литер, заключенных в двойные кавычки. Например,
“Borland C++”, “Строковая константа”.
В конец строковой константы не надо ставить символ ‘\0’. Это сделает сам компилятор, и строка “Borland C++” в памяти будет выглядеть
так:
B
o
r
l
a
n
d
C
+
+
\0
Есть два простых способа ввести строку с клавиатуры. Первый способ – воспользоваться функцией scanf() со спецификатором ввода %s.
Надо помнить , что функция scanf() вводит символы до первого пробельного символа. Второй способ – воспользоваться функцией gets(),
объявленной в файле stdio.h. Функция gets() позволяет вводить строки,
содержащие пробелы. Ввод заканчивается нажатием клавиши Enter. Обе
функции автоматически ставят в конце строки нулевой байт. В качестве параметра в этих функциях используется имя массива.
38
Вывод строк производится с помощью функций printf() и puts(). Обе
функции выводят содержимое массива до первого нулевого байта. Функция puts() добавляет в конце выводимой строки символ новой строки.
В функции printf() переход на новую строку надо предусматривать в
строке формата самим.
Рассмотрим пример:
# include < stdio.h >
/* Пример 18 */
/* Ввод строки с клавиатуры и вывод ее на экран */
main()
{
char str[80];
printf(“Введите строку длиной не более 80 символов”);
gets(str);
printf(“ Вы ввели строку %s\n”,str);
printf(“Введите еще одну строку ”);
scanf(“%s”,str);
printf(“ Вы ввели строку ”);
puts(str);
}
Для работы со строками существует специальная библиотека,
описание которой находится в файле string.h. Рассмотрим их подробнее.
Вызов функции strcpy() имеет вид – strcpy(s1,s2). Эта функция используется для копирования содержимого строки s2 в строку s1. Массив s1 должен быть достаточно большим, чтобы в него поместилась s2.
Если места мало, то компилятор не выдаст соответствующего сообщения об ошибке, также это не приведет к прерыванию выполняемой программы, но может привести к порче других данных, что отразится на
результате.
Вызов функции strcat() имеет вид – strcat(s1,s2). Эта функция присоединяет строку s2 к строке s1 и помещает ее в массив, где находится
строка s1, при этом строка s2 не изменяется. Нулевой байт, который
завершал строку s1, будет заменен первым символом строки s2. И в
функции strcpy(), и в функции strcat() полученная строка автоматически завершается нулевым байтом. Рассмотрим пример использования этих
функций:
39
# include < stdio.h >
# include < string.h >
/* Пример 19 */
main()
{
char s1[20], s2[20];
strcpy(s1,” Hello, “);
strcpy(s2,” World !”);
puts(s1); puts(s2);
strcat(s1,s2);
puts(s1); puts(s2);
}
Вызов функции strcmp() имеет вид – strcmp(s1,s2). Эта функция сравнивает строки s1 и s2 и возвращает значение ноль, если строки равны,
т. е. содержат одно и то же число одинаковых символов. Под сравнением строк понимается сравнение в лексикографическом смысле. Если s1
лексикографически больше s2, то функция strcmp() возвращает положительное значение, если меньше – отрицательное.
Вызов функции strlen() возвращает длину строки s, при этом завершающий нулевой байт не учитывается. Вызов strlen(“hello”) вернет значение 5. Рассмотрим пример программы, в которой определяется длина
строки, вводимой с клавиатуры:
# include < stdio.h >
# include < string.h >
/* Пример 20 */
main()
{
char s[80];
printf(“ Введите строку: ”);
gets(s);
printf(“строка \n%s\n имеет длину %d символов\n”,s,strlen(s));
}
40
4.3. Двумерные массивы
Язык С допускает многомерные массивы, простейшими из которых
являются двумерные массивы. Можно сказать, что двумерный массив –
это массив одномерных массивов.
Двумерный массив int a[3][4] можно представить в виде табл. 4.1.
Таблица 4.1
Двумерный массив int a[3][4]
a[0][0]
a[1][0]
a[2][0]
a[0][1]
a[1][1]
a[2][1]
a[0][2]
a[1][2]
a[2][2]
a[0][3]
a[1][3]
a[2][3]
Здесь первый индекс – номер строки, второй номер столбца. Количество байт памяти, необходимое для хранения двумерного массива в
памяти определяется как
<размер типа данных>*<число строк>*<число столбцов>.
В памяти компьютера двумерный массив располагается по строкам.
Память для массивов, которые определены как глобальные, отводится в
процессе компиляции и сохраняется на все время выполнения программы.
Часто двумерные массивы используются для работы с числовыми и
с символьными таблицами(массивы строк). Рассмотрим пример:
# include < stdio.h >
# include < string.h >
/* Пример 21 */
main()
{
char text[5][20];
strcpy(text[0],”Turbo Basic”);
strcpy(text[1],”Turbo Pascal”);
strcpy(text[2],”Borland C++”);
strcpy(text[3],”Turbo Prolog”);
strcpy(text[4],”Turbo Fortran”);
}
В данной прграмме заполняется массив text[][], причем, в функции
strcpy при заполнении массива, используется только первый индекс.
Заполнение text[][] иллюстрируется табл. 4.2.
41
Таблица 4.2
Расположение массива text[][] в памяти компьютера
T
T
B
T
T
u
u
o
u
u
r
r
r
r
r
b
b
l
b
b
o
o
a
o
o
B a s i c \0
P a s c a l \0
n d
C + + \0
P r o l o g \0
F o r t r a n \0
4.4. Инициализация массивов
При программировании бывает важным уметь инициализировать
массивы, т. е. присваивать элементам массива некоторые начальные
значения. Самый простой способ инициализации – указать список инициализаторов в фигурных скобках при объявлении массива, например:
float ff[5]={1.4, 2.5, 3.6, 12.8, 0.9};
int z[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
Многомерные массивы можно инициализировать, рассматривая их
как массивы массивов:
char str z[3][4]={ {1,2,3,4},{5,6,7,8},{9,10,11,12}};
Количество инициализаторов может быть меньше, чем количество
элементов массива. В этом случае оставшиеся элементы массива считаются неопределенными.
Символьные массивы могут инициализироваться как обычный массив:
char str[12]={‘a’,’b’,’c’,’d’};
а могут – как строка символов:
char str[12]=“abcd”;
Отличие состоит в том, что во втором случае будет добавлен автоматически нулевой байт.
Допускается объявление и инициализация массива без явного указания его размера:
char str[]=”abcd”;
В этом случае компилятор сам определит необходимое количество
элементов массива, включая нулевой байт. Таким образом можно объявлять массивы любого типа:
int a[]={10, 20, 30, 40, 50};
42
Но при объявлении многомерных массивов с неизвестным количеством элементов, можно не указывать размер только в самых левых квадратных скобках:
int a[][3]={1, 2, 3
5, 6, 7
8, 9, 10};
Рассмотрим пример использования массивов на примере задачи сортировки одномерного целочисленного массива по убыванию значений:
# include < stdio.h >
/* Пример 22 */
main()
{
int arr[10]={9, 12, 43, 2, 4, 78, 15, 34, 11, 27};
int i,j,tmp;
printf(“ Неотсортированный массив: ”);
for ( i=0; i<10; i++) printf(“%d “, arr[i]);
printf(“\n”);
for ( i=0; i<8; i++)
for ( j=i+1; j<9; j++)
if( arr[i] < arr[j] )
{
tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
printf(“ Отсортированный массив: ”);
for ( i=0; i<10; i++) printf(“%d “, arr[i]);
printf(“\n”);
}
4.5. Указатели, объявление указателей,
операции над указателями
Понимание и правильное использование указателей является основой для создания профессиональных программ на языке С.
Указатель – это переменная, которая предназначена для хранения и
использования в программе адреса некоторого объекта. Здесь имеется в
43
виду адрес в памяти компьютера. Адрес представляет собой простое
целое число, но его нельзя трактовать как переменную или константу
целого типа. Если переменная по смыслу является указателем, то она
должна быть соответствующим образом объявлена. Форма объявления
указателя следующая:
Тип *<имя переменной>;
В этом объявлении тип – некоторый допустимый для языка С тип
данных, на который указывает указатель. Знак * – означает, что следующая за ним переменная является указателем. Например:
char *ch;
int *temp, i, *j;
float *pf, f;
Здесь объявлены указатели ch,temp,j,pf, переменная i типа int и переменная f типа float.
С указателями связаны две специальные операции & и *. Обе эти
операции являются унарными, т. е. имеют один операнд, перед которым они ставятся. Операция & соответствует по смыслу операции взятия (определения) адреса. Операция * является по смыслу операцией
взятия (определения) значения по указанному адресу. Данные операции нельзя спутать с соответсвующими им по написанию бинарными
операциям – поразрядным AND и операцией умножения, так как они
являются унарными, что всегда видно из контекста программы.
При объявлении указателя очень важным является базовый тип, который сообщает компилятору сколько байт памяти занимает переменная, на которую указывает данный указатель. Простейшие действия с
указателями проиллюстрируем на примере следующей программы:
# include < stdio.h >
/* Пример 23 */
main
{
float x=12.3, y;
float *p;
p=&x;
y=*p;
printf(“ x = %f y = %f”,x,y);
44
*p++; /* Увеличиваем на 1 значение, взятое по указателю p */
printf(“ x = %f y = %f”,x,y);
y=1+*p*y; /* Добавляем 1 к произведению значения взятого по
указателю p на y */
printf(“ x = %f y = %f”,x,y);
}
К указателям можно применять операцию присваивания, если они
являются указателями одного типа. Например:
# include < stdio.h >
/* Пример 24 */
main
{
int x=12;
int p, g;
p=&x;
g=p;
printf(“%p”,p);
printf(“%p”,g);
printf(“%d %d”,x,*g);
}
В этом примере приведена спецификация формата %p функции
printf(), которая используется для вывода адреса памяти в шестнадцатеричной форме.
Нельзя создать переменную типа void, но можно создать указатель
на такой тип. Указателю на void можно присвоить указатель любого
другого типа. При обратном присваивании необходимо использовать
явное преобразование указателя на void. Например, рассмотрим следующий фрагмент:
void *pv;
float f, *pf;
pf=&f;
pv=pf;
pf=(float*)pv;
В языке С допустимо присвоить указателю любой адрес памяти. Однако если объявлен указатель на целое (int p), а по адресу, который
присвоен данному указателю, находится переменная типа float (float x),
45
то при компиляции программы будет выдано сообщение об ошибке в
строке p=&x . Эту ошибку можно исправить, преобразовав указатель
на int к типу указателя на float явным преобразованием типа: p=(int*)&x;
, но при этом теряется информация о том, на какой тип указывал исходный указатель.
Над указателями можно производить арифметические операции: сложение и вычитание. Арифметические действия над указателями имеют
свои особенности. Рассмотрим программу:
# include < stdio.h >
/* Пример 25 */
main
{
int *p;
int x=12;
p=&x;
printf(“%p %p”,p,++p);
}
При выполнении этой программы увидим, что при операции ++p,
значение указателя p увеличиться на 2, а не на 1. Это правильно, так
как следующее значение указателя указывает на адрес следующего целого, а не на следующий адрес (целое занимает 2 байта). Таким образом, при каждой операции ++p значение указателя будет увеличиваться
на количество байт, занимаемой переменной базового типа указателя.
К указателям можно прибавлять или вычитать некоторое целое. Пусть
указатель p имеет значение 4000 и указывает на целое. Тогда в результате выполнения оператора p=p+5; значение указателя станет равным
4010. Общая формула для вычисления значения указателя после выполнения операции p=p+m; будет иметь вид:
=
+m*<количество байт базового типа указателя>
Аналогичны правила вычитания целых констант от значения указателя. Кроме того, можно вычитать один указатель из другого. Так, если
p1 и p2 – указатели на элементы одного и того же массива, то операция
p1-p2 дает такой же результат, как вычитание индексов соответствующих элементов массива.
Другие арифметические операции над указателями запрещены, т. е.
нельзя складывать два указателя, умножать друг на друга и делить и т. д.
46
Указатели можно сравнивать. Применимы все 6 операций сравнения. Сравнение p
# include < ctype.h >
/* Пример 26 */
main()
{
char str[]=”String From Letters in Different Registers”;
int i;
printf(“Строка Будет Напечатана Заглавными Буквами”);
while ( str[i] )
printf(“%c”, toupper(str[i++]));
}
# include < stdio.h >
# include < ctype.h >
main()
{
char str[]=”String From Letters in Different Registers”;
47
char *p;
p=str;
printf(“Строка будет напечатана строчными буквами”);
while ( str[i] )
printf(“%c”, tolower(*p++));
}
Если в этих примерах заменить строку на английском языке на строку, набранную русскими буквами, то никакого преобразования букв в
строчные, или наоборот, в прописные не произойдет. Это связано с тем,
что стандартные функции toupper() и tolower() анализируют значения аргумента и возвращают то же самое значение, если он не является соответственно строчной или прописной буквой латинского алфавита. Если же
аргумент является строчной буквой латинского алфавита, то значением
функции toupper() будет соответствующая прописная буква (точнее, код
этой буквы). Функция tolower() изменяет код только прописных букв латинского алфавита. Прототипы этих функций находятся в файле ctype.h.
4.7. Массивы указателей
Указатели, как и переменные любого другого типа, могут объединяться в массивы. Объявление массива указателей на 12 чисел имеет
вид int *x[11]; . Каждому из элементов массива можно присвоить адрес;
например, пятому элементу этого массива присвоим адрес целой и ранее объявленной переменной y: x[4]=&y;. Если затем необходимо найти
значение переменной y, то это можно сделать, выполнив операцию *x[4].
Рассмотрим пример использования массива указателей:
# include < stdio.h >
# include < string.h >
# include < stlib.h >
# include < conio.h >
/* Пример 27 */
main()
{
char *ext[]={“exe”,”com”,”dat”,”c”,”pas”,”cpp”};
char ch, sl[80];
for( ; ; )
{ do
48
{ printf(“ Файлы с расширением:\n”);
printf(“1. exe\n”);
printf(“2. com\n”);
printf(“3. dat\n”);
printf(“4. c\n”);
printf(“5. pas\n”);
printf(“6. cpp\n”);
printf(“7. Exit\n”);
printf(“ Ваш выбор: \n”);
ch=getche();
printf(“\n”);
}
while ((ch<’1’)||(ch>’7’));
if ( ch == 7 ) break;
strcpy(sl,”dir *.”);
strcat(sl,ext[ch-49];
system(sl);
}
}
Данная программа формирует командную строку с учетом пожеланий пользователя и затем выполняет ее, используя библиотечную функцию system(). Данная функция выполняет указанную в командной строке команду (dir – вывод справочника файлов). Расширение для имен
выводимых файлов задается пользователем. Константа 49 условно состоит из двух слагаемых – 48 и 1. 48- это символьный код нуля. Поэтому, если пользователь программы хочет вывести файлы с расширением
pas , он указывает символьную пятерку, которой соответствует код – 53.
Разность 53-49 определяет индекс пятого элемента в массиве ext (первый элемент массива в С имеет нулевой индекс).
Часто массив указателей используется, если надо иметь ссылки на
стандартный набор строк. Например, для хранения сообщений о возможных ошибках удобно сделать так:
char *errors[]={“Cannot open file”,
“Cannot close file”,
“Allocation error”,
“System error” };
49
При таком объявлении строчные константы будут занесены в раздел
констант в памяти, массив указателей будет состоять из четырех элементов, под которые будет выделена память, и эти элементы будут инициированы адресами, указывающими на начало строчных констант.
Строчная константа в языке С ассоциируется с адресом начала строки в памяти, тип строки получает char*(указатель на тип char). Поэтому возможно и активно используется следующее присваивание:
char *p;
p=”Hello, World !”;
В языке С возможна ситуация, когда указатель указывает на указатель. В этом случае описание имеет следующий вид:
int **point;
Здесь point имеет тип указатель на указатель int. Соответственно,
чтобы получить целочисленное значение переменной, на которую указывает point, надо в выражении использовать **point. Рассмотрим пример:
# include < stdio.h >
/* Пример 28 */
main()
{
int i;
int *pi;
int **ppi;
i=12;
pi=&i;
ppi=π
printf(“i = %d pi = %p ppi = %p **ppi = %d\n”,i,pi,ppi,**ppi);
}
После того, как указатель был объявлен, но до того, как ему было
присвоено значение, указатель содержит неопределенное значение. Попытка использовать такое значение может вызвать ошибку при выполнении программы, и даже, нарушить работу операционной системы.
Принято считать, что указатель с неопределенным значением должен
иметь значение null, но это не делает такой указатель “безопасным”. С
другой стороны нулевой указатель иногда удобно использовать как признак окончания некоторого массива указателей или списка, поэтому он
часто фигурирует в операциях сравнения.
50
5. ФУНКЦИИ
5.1. Объявление функций. Оператор return
Функция – это некоторая логически законченная совокупность операторов языка, которая выполняет определенную конкретную задачу, и
может быть вызвана для своего выполнения необходимое количество
раз. Основная форма описания функции имеет вид:
Тип < имя функции > ( список параметров )
{
тело функции
}
Тип определяет тип значения, которое возвращает функция с помощью оператора return. Если тип не указан, то по умолчанию предполагается, что функция возвращает целое значение ( типа int ). Список
параметров состоит из перечня типов и имен параметров, разделенных
запятыми. Функция может не иметь параметров, но круглые скобки в ее
описании необходимы в любом случае. В списке параметров для каждого параметра должен быть указан тип. Оператор return имеет два варианта использования. Во-первых, этот оператор вызывает немедленный
выход из текущей функции и возврат в вызывающую программу. Вовторых, этот оператор может использоваться для возврата значения функции. В теле функции может быть несколько операторов return, но может быть ни одного. В этом случае возврат в вызывающую программу
происходит после выполнения последнего оператора в теле функции.
Приведем пример функции, реализующей возведение числа a в натуральную степень b :
/* Пример 29 */
float step(float a, int b)
{
float i;
51
if ( a < 0 ) return (-1);
for ( i=1; i<=b; i++) a*=a;
return a;
}
Эта функция возвращает значение –1, если основание отрицательное, и a в степени b , если основание неотрицательное. Другой пример
– функция для нахождения наибольшего из двух целых чисел:
/* Пример 30 */
int max( int a, int b)
{
int m;
if ( a > b ) m=a;
else m=b;
return m;
}
Можно написать эту функцию, не используя дополнительную переменную:
/* Пример 31 */
int max( int a, int b)
{
if ( a > b ) return a;
else return b;
}
Еще короче:
int max( int a, int b)
{
if ( a > b ) return a;
return b;
}
В случае, когда оператор return отсутствует в теле функции, или за
ним нет значения, то возвращаемое значение не определено. Если функция должна возвращать значение в соответствии со своим описанием,
но не делает этого, компилятор выдаст предупреждение. Все функции,
возвращающие значения, могут использоваться в выражениях языка С,
но они не могут использоваться в левой части оператора присваива52
ния, за исключением тех случаев, когда возвращаемым значением является указатель.
Использование функций, возвращающих указатели, имеет некоторые особенности, так как их значениями являются адреса памяти данных определенного типа. Рассмотрим пример функции, возвращающей
указатель на тип char. Эта функция находит в строке первый пробел и
возвращает его адрес:
/* Пример 32 */
char* find( char* string )
{
int i=0;
while (( string[i] != ‘ ‘) && (string[i] != ‘\0’)) i++;
if ( string[i] == ‘ ‘ ) return &string[i];
else return NULL
}
Когда функция не должна возвращать никакого значения, она имеет
тип void. Например, для вывода на экран горизонтальной строчки, состоящей из заданного символа, начиная с текущего положения курсора,
можно использовать следующую функцию:
/* Пример 33 */
void gorisont_line( char ch )
{
int i;
for ( i=0; i<80; i++ ) printf(“%c”,ch);
}
Если для функции, которая не возвращает значение, не объявить тип
void, то она по умолчанию будет иметь тип int, что вызовет предупреждающее сообщение компилятора. Объявление типа возвращаемого значения функции, даже если в этом нет необходимости, является хорошим неформальным правилом при программировании.
5.2. Прототипы функций
Особенностью стандарта ANSI языка С является то, что для создания
правильного машинного кода функции ему необходимо сообщить до ее
первого вызова тип возвращаемого результата, а также количество и
53
типы аргументов. Для этого в языке С используется понятие прототипа
функции. Прототип функции задается следующим образом:
Тип < имя функции> ( список параметров );
Использование прототипа функцией является ее объявлением. Чаще
всего прототип функции полностью совпадает с заголовком в описании функции, хотя это и не всегда так. При объявлении функции компилятору важно знать имя функции, количество и тип параметров и
тип возвращаемого значения. При этом имена формальных параметров
не играют никакого значения и игнорируются компилятором. Поэтому
прототип функции может выглядить как:
/* Пример 34 */
int func( int a, float b, char* c);
или так
int func( int , float , char* );
Рассмотрим пример:
# include < stdio.h >
float sqr( float a );
main()
{
float b=7.8;
printf(“ Квадрат числа %f равен %f”,b,sqr(b));
}
float sqr(float a)
{
return a*a
}
Если функция не имеет аргументов, то при объявлении прототипа
такой функции следует вместо аргументов писать ключевое слово void.
Использование данного ключевого слова необходимо и при объявлении
функции main(). Такое объявление может иметь вид void main(void) или
main(void).
5.3. Область действия и область видимости
Область действия переменной – это правила, которые устанавливают, какие данные доступны из данного места программы.
В языке С каждая функция – это отдельный блок программы. Попасть в тело функции нельзя иначе, как через вызов данной функции.
54
В частности, нельзя оператором локального перехода goto перейти в
середину другой функции.
С точки зрения области действия переменных различают три типа
переменных: глобальные, локальные и формальные параметры. Правила области действия определяют, где каждая из них может применяться.
Локальные переменные – это переменные, объявленные внутри блока, в частности внутри функции. Язык С поддерживает простое правило: переменная может быть объявлена внутри любого блока программы. Локальная переменная доступна внутри блока, в котором она объявлена. Вспомним, что блок открывается фигурной скобкой и закрывается фигурной скобкой. Область действия локальной переменной – блок.
Локальная переменная существует пока выполняется блок, в котором эта переменная объявлена. При выходе из блока эта переменная (и
ее значение) теряется
# include < stdio.h >
void ff( void );
/* Пример 35 */
main ( void )
{
int i=1;
ff();
printf(“ В функции main значение i равно %c\n”,i);
}
void ff(void)
{
int i=12;
printf(“ В функции ff значение i равно %c\n”,i);
}
Пример показывает, что при вызове функции значение переменной
i, объявленной в main(), не изменилось.
Формальные параметры – это переменные, объявленные при описании функции как ее аргументы. Функции могут иметь некоторое количество параметров, которые используются при вызове функций для передачи значений в тело функции. Формальные параметры могут использоваться в теле функции так же, как локальные переменные, которыми
они по сути дела и являются. Область действия формальных параметров – блок, являющийся телом функции.
55
Глобальные переменные – это переменные, объявленные вне какойлибо функции. В отличие от локальных переменных глобальные переменные могут быть использованы в любом месте программы, но перед
их первым использованием они должны быть объявлены. Область действия глобальной переменной – вся программа.
Использование глобальных переменных имеет свои недостатки:
– они занимают память в течение всего времени работы программы;
– использование глобальных переменных делает функции менее общими и затрудняет их использование в других программах;
– использование внешних переменных делает возможным появление ошибок из-за побочных явлений. Эти ошибки, как правило, трудно
отыскать.
5.4. Классы памяти
В языке С имеется инструмент, позволяющий управлять использованием памяти и , тем самым, создавать гибкие программы. Этот инструмент – классы памяти. Каждая объявленная переменная принадлежит к
одному из четырех классов памяти, которые описываются следующими
ключевыми словами:
auto – автоматическая;
extern – внешняя;
static – статическая;
register – регистровая.
Тип памяти указывается модификатором – ключевым словом, стоящим перед указанием типа переменной. Например,
static int sum; register int plus.
Если ключевое слово перед спецификацией типа локальной переменной при ее объявлении нет, то по умолчанию она принадлежит классу
auto. Поэтому практически это слово не используется.
Автоматические переменные (auto) имеют локальную область действия. Они известны только внутри блока, в котором определены. Другие функции могут использовать то же имя, но это должны быть переменные, относящиеся к разным блокам. Автоматическая переменная
создается (под нее отводится память) при входе в блок функции. При
выходе из блока автоматическая переменная пропадает, а область памя56
ти, в которой находилась эта переменная, считается свободной и может
использоваться для других целей.
Автоматические переменные хранятся в оперативной памяти, точнее в той области памяти, которая отводится под стек. Регистровые
(registr) переменные хранятся в регистрах процессора, доступ к которым значительно быстрее, чем к автоматическим переменным. В остальном регистровые переменные аналогичны автоматическим переменным. Регистровая память процессора невелика, и если доступных регистров нет, то переменная становится простой автоматической переменной. Описание регистровой переменной имеет вид: registr int q;
Внешняя переменная (extern) относится к глобальным переменным.
Она может быть объявлена как вне , так и внутри тела функции:
void ff(void)
{
……………
extern int x; /* объявление переменной внутри функции*/
……………
}
Появление ключевого слова extern связано с модульностью языка С,
т. е. возможностью составлять многофайловую программу, где каждый
файл компилируется отдельно. Когда мы в одном из файлов опишем
вне тела функции глобальную переменную, например так – float global; , то
для нее выделится место в памяти в разделе глобальных переменных и
констант. Если мы используем эту глобальную переменную в другом
файле, то при раздельной компиляции компилятор не будет знать, что
это за переменная. Использование объявления extern float global; не приводит к выделению памяти, а сообщает компилятору, что такая переменная будет описана в другом файле. И тогда при компоновке программы, состоящей из нескольких файлов, компоновщик будет искать
описание этой переменной и связывать ее с использованием в других
файлах. Объявление внешней переменной может быть как вне функции, так и внутри функции. Если это же имя без ключевого слова extern
будет объявлено внутри функции, то под этим именем будет создана
уже другая автоматическая переменная. Можно к объявлению этой переменной добавить ключевое слово auto, чтобы показать, что вы не
ошиблись, а намеренно продублировали имя. Объявлений переменной
как внешней может быть несколько, в том числе и в одной функции
57
или в одном файле. Описание же переменной должно быть только одно.
Следующие примеры демонстрируют разные способы описания внешних переменных:
/* Пример 36 */
int var; /* описана внешняя переменная var */
main(void)
{
extern int var; /* объявлена та же внешняя переменная */
…………
}
func1()
{
extern int var1; /* объявлена внешняя переменная var1 */
………… /* переменная var также внешняя*/
}
func2() /* переменная var внешняя */
{
/* переменная var1 невидима для этой функции */
…………
}
int var1; /* описание внешней переменной */
func3() /* для этой функции var1 - внешняя */
{
int var; /* здесь var – локальная и не связана*/
………… /* с соответствующей глобальной */
}
func4() /* здесь var является внешней и глобальной */
{
auto int var1; /* var1- локальная и автоматическая */
…………
}
При описании статических переменных перед указанием типа ставится ключевое слово static. Область действия локальной статической переменной является вся программа. Место в памяти под локальные статические переменные выделяется в начале работы программы в разделе глобальных и статических переменных. Однако область видимости локальных статических переменных такая же, как и
58
у автоматических. Значение статических переменных сохраняется от
одного вызова функции до другого. Локальные статические переменные инициализируются нулем, если не указан другой инициализатор. При этом описание с инициализацией static int count=12; вызывает однократную инициализацию переменной count при выделении для нее места. При последующих вызовах функции, в которой
описана эта переменная, инициализации не происходит. Это позволяет использовать такую переменную для организации счетчика количества вызовов функции.
# include < stdio.h >
void trystat(void);
/* Пример 37 */
main(void)
{
int i;
for ( i=1; i<=3; i++)
{
printf(“ вызов - %d\n”,i);
trystat();
printf(“ вызов - %d\n”,i);
trystat();
}
}
void trystat(void)
{
int aut_l=1;
static int stat_l=1;
printf(“aut_l = %d stat_l = %d\n”,aut_l++,stat_l++);
}
Можно описать также глобальную статическую переменную, т. е.
описать переменную типа static вне любой функции. Отличие внешней
переменной от внешней статической переменной состоит в том, что
внешняя переменная может использоваться функциями в любом файле,
а внешняя статическая переменная может использоваться только функциями того файла, где она описана, причем только после ее определения. Все глобальные переменные, как статические так и нестатические,
инициализируются нулем, если в программе не предусмотрено другой
59
инициализации. В табл. 5.1 приведены область действия и продолжительность существования переменных разных классов памяти:
Таблица 5.1
Область действия и продолжительность
существования переменных разных классов памяти
Ключевое
слово
Время
существования
Область
действия
Автоматический
Регистровый
Статический локальный
Статический глобальный
auto
registr
static
static
Временно
Временно
Постоянно
Постоянно
Блок
Блок
Блок
Ф ай л
Внешний
extern
Постоянно
Программа
Класс памяти
В программе может быть описано несколько переменных с одним и
тем же именем, но в разных блоках. В нижеприведенном примере объявлена одна глобальная и три локальных переменных с одним и тем же
именем var:
# include < stdio.h >
void ff( void );
void fl( void );
/* Пример 38 */
int var=5;
main( void )
{
int var=10;
printf(“ var = %d\n”,var); /* var = 10 */
{
int var=100;
printf(“ var = %d\n”,var); /* var = 100 */
}
printf(“ var = %d\n”,++var); /* var = 11 */
ff();
printf(“ var = %d\n”,++var); /* var = 12*/
fl();
printf(“ var = %d\n”,++var); /* var = 13 */
fl();
60
printf(“ var = %d\n”,++var); /* var = 14 */
}
void ff( void )
{
int var=55;
printf(“ var = %d\n”,var); /* var = 55 */
}
void fl( void )
{
printf(“ var = %d\n”,var); /* var = 5 */
}
5.5. Параметры и аргументы функций
В языке С все аргументы функции передаются по значению. При
вызове функции в стеке выделяется место для формальных параметров
функции, и в это место заносится значение фактического параметра, т.
е. значение параметра при вызове функции. Далее функция использует
это значение, при этом она может изменить значение параметра. При
выходе из функции измененные значения параметров теряются. Таким
образом, в языке С вызванная функция не может изменить значения
переменных, указанных в качестве фактических параметров при обращении к ней. Например, функция swap(), которая должна менять значение параметров местами, не будет этого делать:
/* Пример 39 */
void swap(int a, int b)
{
int tmp=a;
a=b;
b=tmp;
}
Но тем не менее функцию можно приспособить для изменения аргументов. Для этого необходимо в качестве параметра передавать адрес
переменной, которую надо изменять, т. е. передавать указатель на переменную. Такой прием в языке С называется передачей параметра по
ссылке. Вызванная функция должна описывать аргумент как ссылку и
обращаться к фактической переменной косвенно, через эту ссылку. Если
61
в качестве аргумента берется имя массива, то передаваемое функции
значение фактически есть адрес первого элемента массива.
Теперь функцию swap() можно описать следующим образом:
/* Пример 40 */
void swap(int *a, int *b)
{
int tmp=*a;
*a = *b;
*b=tmp;
}
Для иллюстрации использования этих двух способов передачи параметров приводим текст следующей программы:
# include < stdio.h >
/* Пример 41 */
void swap( int a, int b);
void swap1( int *a, int *b);
void main( void )
{
int x=10, y=20;
printf(“ Сначала x = %d y = %d\n”,x,y);
swap(x,y);
printf(“ Теперь x = %d y = %d\n”,x,y);
printf(“ Ничего не изменилось \n”);
swap1(&x,&y);
printf(“ Теперь x = %d y = %d\n”,x,y);
printf(“ Значения поменялись \n“);
}
void swap(int a, int b)
{
int tmp=a;
a=b;
b=tmp;
}
void swap1(int *a, int *b)
{
int tmp=*a;
62
*a = *b;
*b=tmp;
}
Результатом работы этой программы будет следующее: сначала x=10
y=20, потом x=20 y=10. Значения переменных x и y изменились, так как
были переданы по ссылке. Еще одна особенность состоит в том, что
при вызове функции swap() можно задать конкретные значения, например swap(10,20). Вызвать функцию swap1() в виде swap1(&10,&20)
нельзя.
Если в качестве аргумента функции используется массив, есть лишь
один способ – передача параметра по ссылке. Сделать это можно тремя
способами:
func(int ar[10]);
func(int ar[]);
func(int *ar);
Все три способа дадут один и тот же результат.
Рассмотрим программу, где используется функция сортировки массива по возрастанию значений:
# include < stdio.h >
/* Пример 42 */
void sort(int arr[], int n);
void main( void )
{
int mass[10]={1,-6,21,3,-7,4,-12,9,5,17};
int i, size=10;
printf(“ до сортировки: \n“);
for ( i=0; i<10; i++)
printf(“%d”,mass[i]);
printf(“\n”);
sort(mass,size);
printf(“ после сортировки: \n“);
for ( i=0; i<10; i++)
printf(“%d”,mass[i]);
}
void sort(int arr[], int n);
{
63
int i,j,tmp;
for ( i=0; i
# include < math.h >
/* Пример 44 */
double f(double x);
double f1(double x);
double f2(double x);
double ff(double (*pf)(double x), double x); /* указатель на функцию в качестве аргумента*/
void main(void)
{
double z=2.3, y;
double (*ptrf)(double x); /* указатель на функцию */
ptrf=f; /* инициализация указателя */
y=(*ptrf)(z); /* вызов функции через указатель – 1-й способ*/
printf(“z=%f y=%f\n”,z,y);
y=ptrf(z); /* вызов функции через указатель – 2-й способ*/
printf(“z=%f y=%f\n”,z,y);
ptrf=sin; /* использование стандартной функции*/
y=ptrf(z);
printf(“z=%f sin(z)=%f\n”,z,y);
y=(*ptrf)(4.6) /* можно таким образом */
printf(“z=%f y=%f\n”,z,y);
y=ff(f1,z); /* вызов ff с параметром f1*/
printf(“z=%f y=%f\n”,z,y);
65
y=ff(cos,z); /* другой вызов с использованием стандартной
функции*/
printf(“z=%f y=%f\n”,z,y);
y=ff(ptrf,z); /* то же через указатель на функцию*/
printf(“z=%f y=%f\n”,z,y);
}
double f(double x)
{
puts(“ In f()”);
return x;
}
double f1(double x)
{
puts(“ In f1()”);
return x*x;
}
double f2(double x)
{
puts(“ In f2()”);
return x*x*x;
}
double ff(double (*pf)(double x), double x)
/* Функция с указателем на функцию в качестве параметра*/
{
puts(“ In ff() %f\n”,pf(x));
return pf(x);
}
При использовании указателей на функции можно образовывать массивы указателей. Такая структура данных называется jump table.
Если определены объявления
int f0(void);
int f1(void);
int f2(void);
int f3(void);
int(*jtable[](void)=(f0,f1,f2,f4);
Соответствующую функцию можно вызвать
val = (*jtable[i])(); или val = jtable[i]();
66
6. ТИПЫ ДАННЫХ, ОПРЕДЕЛЯЕМЫЕ ПОЛЬЗОВАТЕЛЕМ
6.1. Структура
Пользователь при написании программы на языке С может создать
пять типов данных:
– структуры (structure),
– объединения (union),
– перечисляемый тип (enumeration),
– поля битов (bit fields),
– с помощью оператора typedef создать новое имя (псевдоним) для
уже существующего типа.
Структура объединяет несколько переменных, возможно разного типа.
Переменные, которые объединены структурой, называются членами,
элементами или полями структуры. Пример определения структуры:
/* Пример 45 */
struct student{
char name[30];
int kurs;
char group[3];
int stip;
};
Объявление структуры является оператором, и поэтому после такого объявления должна состоять точка с запятой. При этом надо понимать, что пока
никакая переменная не объявлена, так как выделения памяти под переменную не произошло. Здесь под именем student задан вид структуры, иначе
говоря, ее шаблон, и определен новый тип struct student. Для того чтобы
объявить конкретные переменные типа struct student, можно записать
struct student stud1, stud2;
Теперь объявлены две переменные – stud1 и stud2. Компилятор автоматически выделит под них место в памяти компьютора. Под каждую
67
из переменных типа структуры выделяется непрерывный участок памяти. Задание шаблона структуры и объявление переменных может производится и в одном операторе
/* Пример 46 */
struct student{
char name[30];
int kurs;
char group[3];
int stip;
} stud1, stud2;
Здесь одновременно задается структура с именем student и объявляются переменные stud1 и stud2.
Доступ к конкретному элементу(полю) структуры осуществляется с
помощью операции ‘точка’(dot). Например:
strcpy(stud1.name, “Петров И.И”);
Если необходимо содержимое третьего поля переменной stud2 структуры student напечатать, то надо записать:
printf(“%s”,stud2.group);
Структуры, как и переменные других типов, могут объединяться в массивы
структур. Чтобы объявить массив структур, надо сначала задать шаблон структуры( используем имеющийся шаблон student), а затем объявить массив:
Struct student stud1kurs[200];
Этот оператор создаст в памяти 200 переменных типа структуры с
шаблоном student и именами stud1kurs[0], stud1kurs[1] и т.д.
Для доступа к полю kurs 35-го элемента массива используем
stud1kurs[34].kurs
Если объявлены две переменные типа структуры с одним шаблоном,
можно сделать присваивание
stud1 = stud2;
При этом произойдет побитовое копирование каждого поля одной
переменной в соответствующее поле другой переменной. В то же время нельзя использовать операцию присваивания переменных типа структуры, шаблоны которых описаны под разными именами, пусть даже
совсем идентично. Тем не менее в этом случае присваивание возможно
для отдельных полей, имеющих один и тот же тип данных.
68
Переменная типа структуры может быть глобальной, локальной переменной и формальным параметром. Можно, естественно, использовать структуру или ее поле как любую другую переменную в качестве
параметра функции.
Например:
func1(right.a); или func2(&left.b);
Заметим, что & ставится перед именем структуры, а не перед именем поля. Можно в качестве формального параметра передать по значению всю структуру
#include
/* Пример 47 */
struct stru{
int x;
char y;
};
void ff( struct stru param);
main(void)
{
struct stru arg;
arg.x=3;
arg.y=’a’;
ff(arg);
}
void ff(struct stru param)
{
printf(“%d %c\n”,param.x,param.y);
}
Можно также создать указатель на структуру и передавать аргумент
типа структуры по ссылке. Объявить указатель на структуру можно следующим образом:
struct stru *adr_pointer;
Здесь adr_pointer – переменная указатель на структуру struct stru.
Если структура передается по значению, то все поля структуры заносятся в стек. Если структура простая и содержит мало элементов, то это не так
страшно. Если же структура в качестве своего поля содержит массив, то
69
стек может переполниться. При передаче по ссылке в стек заносится только адрес структуры. При этом копирования структуры не происходит, а
также появляется возможность изменять содержимое полей структуры
/* Пример 48 */
struct complex{
float x;
float y;
} c1,c2;
struct complex *a;
a = &c1;
указателю а присваивается адрес переменной с1. Получить значение
поля х переменной с1 можно так:
(*a).x;
Использование указателей на структуру встречается достаточно часто, например, при программировании задач по созданию и обработке
динамических связанных структур данных( очереди, стеки, деревья).
Поэтому, кроме способа получить значение поля структуры, используя
(*a).x можно использовать другой. В языке С вводится специальная операция -> ( стрелка, arrow).
Операция стрелка употребляется вместо операции точка, когда мы
хотим использовать значение поля структуры с применением переменной указателя. Вместо (*a).x можно использовать a-> x. Этот способ
чаще всего и применяется.
Завершая разговор о структурах необходимо сказать, что в качестве
полей структуры можно использовать массивы, структуры и массивы
структур.
Пусть объявления переменных имеют вид
Struct addr{
Char city[34];
Char street[76];
Int house;
};
struct fulladdr{
struct addr addres;
int room;
char name[48];
} a,b;
70
Здесь addr – шаблон структуры, определенный перед объявлением
структуры fulladdr и объявлением переменной а типа структуры fulladdr.
Для присвоения значения полю house структуры addres переменной а
используем
a.adress.house = 234;
6.2. Доступ к отдельным битам
В отличие от многих других языков программирования язык С обеспечивает доступ к одному или нескольким битам в байте или слове. В
конкретных задачах часто бывает необходимо, чтобы некоторая переменная принимала только два значения. Для этого достаточно использовать только один бит памяти. Такая переменная, по своему содержательному смыслу, является некоторым признаком или флагом.
Один из методов, встроенных в язык С и позволяющий иметь доступ
к биту, – это поля битов (bit-fields). В действительности поля битов –
это специальный тип членов структуры, в котором определено, из скольких бит состоит каждое поле. Основная форма объявления такой структуры следующая:
struct имя_структуры {
тип имя1: длина в битах;
тип имя2: длина в битах;
тип имя3: длина в битах;
...
тип имяN: длина в битах;
};
В этом объявлении структуры тип может быть одним из следующих:
int, unsigned, signed.
Имя1 может быть пропущено, тогда соответствующее количество бит
не используется (пропускается ). Длина структуры в целом всегда кратна восьми. Так, если указать
Struct onebit{
Unsigned one_bit: 1;
} obj;
Здесь для переменной obj будет выделено 8 бит, но использоваться
будет только первый. В структуре также могут быть смешаны обычные
переменные и поля битов.
71
6.3. Объединения
В языке С определен еще один тип для размещения в памяти нескольких переменных разного типа – объединение( union ). Объявляется
объединение также как и структура, например
/* Пример 49 */
union u{
int i;
char ch;
long int l
};
Это объявление задает шаблон объединения. Можно объявить переменные
union u alfa, beta;
Можно было объявить переменные одновременно с заданием шаблона. В отличие от структуры для переменной типа union места в памяти
выделяется ровно столько, сколько нужно полю объединения, имеющему наибольший размер в байтах. В приведенном выше примере под
переменную alfa будет выделено 4 байта памяти. Действительно, поле i
требует 2 байта, поле ch – 1 байт, и поле l – 4 байта. Остальные переменные будут располагаться в том же месте памяти. Синтаксис использования полей объединения такой же, как и для структуры:
u.ch = ‘N’;
Для объединения также разрешена операция ->, если идет обращение к объединению с помощью указателя.
Программа, приведенная ниже, выдает на экран двоичный код символа, вводимого с клавиатуры:
#include < stdio.h >
#include < conio >
/* Пример 50 */
struct byte{
int b1: 1;
int b2: 1;
int b3: 1;
int b4: 1;
int b5: 1;
72
int b6: 1;
int b7: 1;
int b8: 1;
};
union bits{
char ch;
struct byte b;
} u;
void decode ( union bits b );
main ( void )
{
do {
b.ch:=getche();
printf(“;”);
decode(u);
} while ( u.ch != ‘q’);
}
void decode ( union bits b )
{
if ( b.byte.b8 ) printf(“1”);
else printf(“0”);
if ( b.byte.b7 ) printf(“1”);
else printf(“0”);
if ( b.byte.b6 ) printf(“1”);
else printf(“0”);
if ( b.byte.b5 ) printf(“1”);
else printf(“0”);
if ( b.byte.b4 ) printf(“1”);
else printf(“0”);
if ( b.byte.b3 ) printf(“1”);
else printf(“0”);
if ( b.byte.b2 ) printf(“1”);
else printf(“0”);
if ( b.byte.b1 ) printf(“1”);
else printf(“0”);
printf(“\n”);
}
73
6.4. Перечислимый тип
Перечислимый тип ( enumeration ) – это множество поименованных
целых констант. Перечислимый тип определяет все допустимые значения которые могут иметь переменные этого типа. Основная форма объявления такого типа следующая:
enum имя_типа {список_названий} список переменных;
Список переменных может быть пустым. Пример определения перечислимого типа и переменной данного типа
enum season { win, spr, sum, aut };
enum season s;
Ключом к пониманию сущности перечислимого типа является то,
что каждое из имен win, spr, sum, aut представляют собой целую величину. Если эти величины не определены по другому, то по умолчанию
они соответсвенно равны нулю, единице, двум и трем. Оператор
printf(“%d %d”,win,aut); выдаст на экран числа 0 и 3. Во время объявления типа можно одному или нескольким символам присвоить другие
значения, например
enum value { one=1,two,three,ten=10,thousand=1000,next};
Если теперь напечатать значения
printf(“%d %d %d %d %d\n”,one,two,ten,thousand,next);
то на экране появятся числа 1 2 10 1000 1001, т.е. каждый следующий символ увеличивается на единицу по сравнению с предыдущим,
если нет другого присваивания.
С переменными перечислимого типа можно производить следующие
операции:
– присвоить переменную типа enum другой переменной того же типа;
– провести сравнение с целью выяснения равенства или неравенства;
– арифметические операции с константами типа enum (i=win+aut).
Нельзя использовать арифметические операции и операции ++ и –
для переменных типа enum.
Основная причина использования перечислимого типа – улучшение
читаемости программ.
74
6.5. Переименование типов
Язык С позволяет дать новое название уже существующим типам
данных. Для этого используется ключевое слово typedef. При этом создается новый тип данных. Например:
typedef char SYMBOL;
typedef unsigned UNSIGN;
typedef float real;
Достаточно часто используется оператор typedef с применением структур:
/* Пример 51 */
typedef struct st_tag{
char name[40];
int kurs;
char group[5];
int stip;
} STUDENT;
Теперь для определения переменной можно использовать
struct st_tag avar;
А можно использовать
STUDENT avar;
75
7. ВВОД/ВЫВОД И РАБОТА С ФАЙЛАМИ
7.1. Организация ввода-вывода
В языке С отсутствуют специальные операторы ввода-вывода. Все действия,
связанные с вводом-выводом, выполняются с помощью функций библиотеки
С. Программист может также создать свои собственные функции ввода-вывода
на основе библиотечных. При вводе-выводе все данные рассматриваются как
поток байтов, связанный либо с файлом на диске, либо с нефайловым физическим устройством (клавиатура, экран монитора, принтер и т. п.). Функции ввода-вывода позволяют выделять из потока и обрабатывать данные различных
форматов и размеров, обеспечивая при этом буферизированный форматированный или неформатированный ввод или вывод.
Для использования функций ввода-вывода необходимо директивой #include
включить файл stdio.h, содержащий объявления функций ввода-вывода, а также определение констант, типов и структур, используемых этими функциями.
Открытие потока осуществляется функцией fopen. При успешном открытии
эта функция возвращает указатель структуры типа FILE, которая содержит информацию, необходимую для работы с потоком. При ошибке открытия возвращается значение NULL, которое определено как константа в stdio.h. Указатель
потока используется в дальнейшем во всех функциях вашей программы, работающих с данным потоком. Количество одновременно открытых потоков ограничевается установками операционной системы.
Параметрами функции fopen являются строка, указывающая путь к файлу и
его имя, и строка, определяющая тип доступа к потоку. Литерал типа доступа
может иметь значения: “r” – для чтения, “w” – для записи, “a” - для записи в
конце потока, “r+” – для чтения и записи, “w+” – пустой поток для чтения и
записи, “a+” – для чтения и записи в конце потока. Закрытие потока осуществляется функцией fclose. Указатель потока можно позиционировать на любое
место в потоке. Для получения текущей позиции в потоке используются функции ftell и fgetpos, для изменения позиции указателя – fseek и fsetpos. В stdio.h
определены стандартные указатели потоков: stdin – стандартный ввод(клавиатура), stdout – стандартный вывод(дисплей), stdprn – стандартная печать, stderr
76
– стандартный вывод сообщений об ошибках. Эти указатели можно использовать во всех функциях , требующих указатель файла, без предварительного
открытия потока с помощью функции fopen. Кроме того, функции ввода-вывода на терминал (getc, putc и т.п.) не требуют указатель потока, так как используют stdin и stdout. Существуют также функции, реализующие ввод-вывод в текущее окно экрана монитора путем прямой записи в видеопамять ( getch, getche,
putch, cprintf и т.д.). Эти функции требуют подключения директивой #include
файла conio.h.
7.2. Классификация функций чтения и записи
Классификация функций чтения и записи представлена в табл. 7.1.
Таблица 7.1
Классификация функций чтения и записи
Чтение
Объект операции
Последовательность
байт
Отдельный символ
Форматированные
данные
Количество
элементов
Введенный
символ
Введенное
число
Строка
Количество
введенных
полей
fread
fgetchar
getchar
Данное типа int
Строка
Возвращаемое значение
Из потока Из любого Из строки При успешном
stdin
потока
С
завершении
fgetc getc
getw
gets
fgets
scanf
sscanf
Запись
Объект операции
Отдельный символ
Строка
Форматированные
данные
fputc putc
putw
puts
fputs
printf
fprintf
EOF
EOF
NULL
EOF
Возвращаемое значение
Из потока Из любого Из строки При успешном
stdin
потока
С
завершении
Последовательность fputchar
байт
putchar
При
ошибке
sprintf
Выведенный
символ
Выведенное
число
Последний
символ
Количество
выведенных
байт
При
ошибке
EOF
EOF
EOF
EOF
77
Рассмотрим пример программы, которая выводит содержимое файла
autoexec.bat в стандартный поток вывода, а также выводит на экран
монитора последнии 15 байт этого файла.
#include
#include
/* Пример 52 */
main(void)
{
FILE *f; int a; long int n;
if((f =fopen(“c:\\autoexec.bat”,”r”))==NULL) /*открываем поток f */
{ prinf(“ Ошибка при открытии файла\n”); exit(1); }
while((a=fgetc(f)) != EOF /* пока не конец файла читаем в память*/
fputc(a,stdout); /* и выводим в стандартный поток вывода*/
n=-15;
fseek(f,n,SEEK_END); /* позиционирование за 15 байт до конца*/
while((a=fgetc(f)) != EOF) /* пока не конец файла читаем в память*/
putchar(a); /* и выводим на экран монитора*/
fclose(f); /* закрываем поток*/
getch();
}
7.3. Функции библиотеки ввода-вывода
Библиотека функций ввода-вывода, которые можно использовать при
программировании на языке С, весьма разнообразна, что определяет
необходимость привести ее полностью.
7.4. Функции для работы с файлами
fopen – открывает поток, связанный с файлом filename и типом доступа type.
FILE *fopen(char *filename, char *type);
fclose – закрывает поток stream.
int fclose(FILE *stream);
fcloseall – закрывает все открытые потоки.
int fcloseall( void );
remove – удаляет файл с именем filename.
int remove( char *filename );
rename – переименовывает файл oldname в файл newname.
78
int rename(char *oldname, char *newname);
ftell – возвращает положение указателя текущей позиции файла, связанного с потоком stream. Значение возвращается в виде смещения в
байтах относительно начала файла. Значение, возвращаемое функцией
ftell, в дальнейшем можно использовать при вызове функции fseek. ftell
возвращает положение указателя текущей позиции при успешном завершении.При ошибке возвращается значение – 1L.
long int ftell( FILE *stream);
fseek – устанавливает адресный указатель файла, соответствующий
потоку stream, в новую позицию, которая расположена по смещению
offset относительно места в файле, определенного параметром
fromtwhere. Параметр fromtwhere может иметь одно из трех значений 0,
1 или 2, которые представлены тремя символическими константами,
определенными в файле stdio.h, следующим образом:
SEEK_SET(0) – начало файла, SEEK_CUR(1) – позиция текущего
указателя файла, SEEK_END(2) – конец файла(EOF); Функция fseek
возвращает значение 0, если указатель файла успешно перенесен, и ненулевое значение в случае неудачного завершения.
int fseek(FILE *stream, long int offset, int fromwhere);
fgetpos – сохраняет позицию указателя файла, связанного с потоком
stream, в месте, указываемом параметром pos. При успешном завершении fgetpos возвращает 0.
int fgetpos(FILE *stream, fpos_t *pos);
Здесь и далее fpos_t - предварительно объявленный тип typedef long
fpos_t.
fsetpos - устанавливает указатель текущей позиции файла, связанного с потоком stream в новую позицию, которая определяется значением, получаемым предшествующим вызовом функции fgetpos. При успешном завершении fsetpos возвращает 0.
Int fsetpos( FILE *stream, const fpos_t *pos);
7.5. Функции неформатированного ввода-вывода
fgetc – получает символ из потока stream.
int fgetc(FILE *stream);
fgetchar – получает символ из потока stdin.
int fgetchar( void );
79
fgets – получает строку s длиной не более n символов из потока
stream.
char *fgets(char *s, int n, FILE *stream);
fputc – выводит символ с в поток stream.
int fput(int c, FILE *stream);
fputchar- выводит символ c в поток stdout.
int fputchar(int c);
fputs – выводит строку символов string в поток stream.
int fputs(char *string, FILE *stream);
gets – получает строку символов s из потока stdin.
char *gets(char *s);
getc – выводит из потока stream символ этого потока.
int getc(FILE *stream);
getchar – выводит символ из потока stdin.
int getchar( void );
putc – выводит символ c в поток stream.
int putc(int c, FILE *stream);
putchar – выводит символ с в поток stdout.
int putchar(int c);
puts – выводит строку s в поток stdout.
int puts(const char *s);
putw – помещает в поток stream целое значение w.
int putw(int w, FILE *stream);
getw – вводит из потока stream целое число.
int getw(FILE *stream);
7.6. Функции блочного ввода-вывода
fread – считывает n элементов данных длиной size из потока stream
по адресу ptr.
size_t fread(void *ptr, size_t size, size_t n, FILE *stream );
Здесь и далее size_t – предварительно объявленный в библиотеке
тип typedef usigned size_t.
fwrite – записывает n элементов данных длиной size из ptr и поток
stream.
Size_t fwrite( void *ptr, size_t size, size_t n, FILE *stream);
80
7.7. Функции форматированного ввода-вывода
printf – производит форматированный вывод в stdout.
int printf(const char *format [,argument,…]);
scanf – выполняет форматированный вывод из потока stdin.
int scanf(const char *format [,adress,…]);
fprintf – посылает форматированный вывод в поток stream.
int fprintf(FILE *stream, const char *format [,argument,…]);
fscanf – выполняет форматированный ввод из потока stream.
int fscanf(FILE *stream, const char *format [,adress,…]);
sprintf – производит форматированный вывод в сстроку buffer.
int sprintf(char *buffer, const char *format [,argument,…]);
sscanf – выполняет форматированный ввод из строки buffer.
Int sscanf(const char *buffer, const char *format [,adress,…]);
7.8. Функции ввода-вывода на экран(conio.h)
cprintf – осуществляет форматированный вывод на экран.
int cprintf(const char *format [,argument,…]);
Таблица 7.2
Файлы и их назначение
Заголовочный файл
assert.h
ctype.h
errno.h
float.h
limits.h
locale.h
match.h
setjmp.h
signal.h
stdarg.h
stddef.h
stdio.h
stdlib.h
string.h
time.h
Назначение
Диагностика программ
Преобразование и проверка символов
Проверка ошибок
Работа с числами с плавающей точкой
Определение размеров целочисленных типов
Поддержка интернациональной среды
Математическая библиотека
Возможность нелокальных переходов
Обработка сигналов
Поддержка функций с неопределенным числом параметров
Разное
Библиотека стандартного ввода-вывода
Функции общего назначения
Функции работы со строками символов
Функции работы с датами и временем
81
getch – читает один символ с консоли без вывода его на экран.
int getch(void);
getche – cчитывает один символ с консоли и отображает его в текущем текстовом окне экрана.
int getche( void );
putch – выводит символ на экран.
int putch(int c);
Каждая библиотечная функция , определенная стандартом языка С,
имеет прототип в соответствующем заголовочном файле. В соответствие со стандартом ANSI языка С должно быть 15 следующих заголовочных файлов (табл. 7.2).
На самом деле каждый из компиляторов содержит , как правило больше заголовочных файлов, например – библиотеку функций работы с
графическим экраном graphics.h и библиотеку функций для работы с
текстовым экраном conio.h.
82
Библиографический список
1. Трой Д. А. Программирование на языке Си для персонального компьютера IBM PC/ Пер.с англ. В. А. Кузьмина; Под ред. И. В. Емелина.
М.: Радио и связь, 1991. 429 с.
2. Уинер Р. Язык Турбо Си/ Пер. с англ. М. П. Матенина; Под ред.
В. В. Мартынюка. М.: Мир 1991. 384 с.
3. Уэйт М. и др. Язык Си: Руководство для начинающих / М.Уэйт,
С. Прата, Д. Мартин; Под ред. Э. А. Трахтенгерца. М.: Мир, 1988.
512 с.
4. Березин Б. И., Березин С. Б. Начальный курс С и С++. М.: ДиалогМИФИ, 1999. 288 с.
83
Оглавление
Предисловие .............................................................................................
1. Основные понятия языка С ................................................................
1.1. Алфавит, идентификаторы, ключевые слова, комментарии ..
1.2. Примеры простых программ ......................................................
1.3. Определение некоторых понятий ..............................................
2. Переменные, константы, операции и выражения ...........................
2.1. Базовые типы данных и объявление переменных ...................
2.2. Основная форма объявления переменных ................................
2.3. Константы ....................................................................................
2.4. Символьные переменные и строки ..........................................
2.5. Инициализация переменных .....................................................
2.6. Операции .....................................................................................
2.7. Выражения ..................................................................................
3. Операторы .............................................................................................
3.1. Условный оператор .....................................................................
3.2. Оператор множественного выбора ............................................
3.3. Операторы циклов .......................................................................
3.4. Операторы break и continue ........................................................
3.5. Оператор безусловного перехода ...............................................
4. Массивы и указатели ...........................................................................
4.1. Понятие массива, объявление массива .....................................
4.2. Массивы символов, строки. Функции работы со строками ..
4.3. Двумерные массивы ....................................................................
4.4. Инициализация массивов ...........................................................
4.5. Указатели, объявление указателей, операции
над указателями ...........................................................................
4.6. Связь указателей и массивов .....................................................
4.7. Массивы указателей ....................................................................
5. Функции ................................................................................................
5.1. Объявление функций. Оператор return ....................................
5.2. Прототипы функций ...................................................................
5.3. Область действия и область видимости ...................................
5.4. Классы памяти .............................................................................
5.5. Параметры и аргументы функций .............................................
5.6. Рекурсивные функции ................................................................
5.7. Указатель на функцию ................................................................
84
3
4
4
5
8
9
9
11
13
16
18
18
25
27
27
29
32
35
36
37
37
38
41
42
43
47
48
51
51
53
54
56
61
64
65
6. Типы данных, определяемые пользователем ...................................
6.1. Структура ....................................................................................
6.2. Доступ к отдельным битам .......................................................
6.3. Объединения ...............................................................................
6.4. Перечислимый тип .....................................................................
6.5. Переименование типов ..............................................................
7. Ввод/вывод и работа с файлами .........................................................
7.1. Организация ввода-вывода .........................................................
7.2. Классификация функций чтения и записи .............................
7.3. Функции библиотеки ввода-вывода ..........................................
7.4. Функции для работы с файлами ................................................
7.5. Функции неформатированного ввода-вывода .........................
7.6. Функции блочного ввода-вывода ..............................................
7.7. Функции форматированного ввода-вывода .............................
7.8. Функции ввода-вывода на экран(conio.h) ................................
Библиографический список ....................................................................
67
67
71
72
74
75
76
76
77
78
78
79
80
81
81
83
85
Учебное издание
Кучин Николай Валентинович
Павлова Марина Михайловна
ОСНОВЫ ПРОГРАММИРОВАНИЯ
НА ЯЗЫКЕ СИ
Учебное пособие
Редактор А. В. Семенчук
Компьютерная верстка А. Н. Колешко
Лицензия ЛР №020341 от 07.05.97. Сдано в набор 11.09.01. Подписано к печати 04.10.01.
Формат 60×84 1/16. Бумага тип. №3. Печать офсетная. Усл. печ. л. 4,65. Усл. кр.-отт. 4,77.
Уч. -изд. л. 5,0. Тираж 100 экз. Заказ №
Редакционно-издательский отдел
Лаборатория компьютерно-издательских технологий
Отдел оперативной полиграфии
СПбГУАП
190000, Санкт-Петербург, ул. Б. Морская, 67