Моделирование многомерных массивов.
#define N 4 /* число строк */
#define M 5 /* число столбцов */
#define A(i, j) x[M *(i-1) + (j –1))
#include <stdio.h>
void main()
{
/* определение одномерного массива */
double x[N*M];
int i, j, k;
for (k = 0; k < N*M; k++)
x[k] = k;
for (i = 1; i<N; i++) /* перебор строк */
{
printf(“\n Строка %d: “, i);
/* перебор элементов строки */
for (j = 1; j <=M; j++)
printf(“ %6.1f”, A(i, j));
}
}
Для доступа к элементам массива x[] используются макровызовы вида A(i, j), причем, i изменяется от 1 до N, а переменная j изменяется во внутреннем цикле от 1 до M. Переменная i соответствует номеру строки матрицы, а переменная j играет роль второго индекса, т.е. указывает номер столбца. При таком подходе программист оперирует с достаточно естественными обозначениями A(i, j) элементов матрицы, причем нумерация столбцов и строк начинается с 1, как и предполагается в матричном исчислении.
Глава 8. Указатели, массивы, строки
В языке Си, кроме базовых типов, разрешено вводить и использовать производные типы, каждый из которых получен на основе более простых типов. Стандарт языка определяет три способа получения производных типов:
§ массив элементов заданного типа;
§ указатель на объект заданного типа;
§ функция, возвращающая значение заданного типа.
8.1. Указатели на объекты
Адреса и указатели.Каждая переменная в программе – это объект, имеющий имя и значение. По имени можно обратиться к переменной и получить (а затем, например, напечатать) ее значение. В операторе присваивания выполняется обратное действие – имени переменной из левой части оператора присваивания ставится в соответствие значение выражения его правой части. С точки зрения машинной реализации имя переменной соответствует адресу того участка памяти, который для нее выделен, а значение переменной – содержимому этого участка памяти. Соотношение между именем и адресом условно представлено на рисунке.
Рис. 25. Соотношение между именем и адресом
На рисунке имя переменной явно не связано с адресом, однако, например, в операторе присваивания E = C + B; имя переменной E адресует некоторый участок памяти, а выражение C + B определяет значение, которое должно быть помещено в этот участок памяти. В операторе присваивания адрес переменной из левой части оператора обычно не интересует программиста и недоступен. Чтобы получить адрес в явном виде, в языке Си применяют унарную операцию &. Выражение &E позволяет получить адрес участка памяти, выделенного на машинной уровне для переменной Е.
Операция & применима только к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, константам, битовым полям структур, регистровым переменным или внешним объектам (файлам), с которыми может взаимодействовать программа.
Таблица 7. Разные типы данных в памяти ЭВМ
Машинный адрес | 1A2B | 1A2C | 1A2D | 1A2E | 1A2F | 1A30 | 1A31 | 1A32 |
байт | байт | байт | байт | байт | байт | байт | байт | |
Значение в памяти | ‘G’ | 2.015e-6 | ||||||
Имя | ch | date | summa |
Таблица хорошо иллюстрирует связь между именами, адресами и значениям переменных. Предполагалось , что в программе использована, например, такая последовательность определений (с инициализацией):
char ch = ‘G’;
int date = 1937;
float summa = 2.015e-6;
В соответствии с приведенной таблицей переменные размещены в памяти, начиная с байта, имеющего шестнадцатеричный адрес 1A2B. Целые переменные занимают по 2 байта, вещественные с плавающей точкой требуют участков памяти по 4 байта, символьные переменные занимают по одному байту. При таких требованиях к памяти в данном примере &ch = 1A2B (адрес переменной ch); &date = 1A2C; &summa = 1A2E. Адреса имеют целочисленные беззнаковые значения, и их можно обрабатывать как целочисленные величины.
Имея возможность с помощью операции & определять адрес переменной или другого объекта программы, нужно уметь это сохранять, преобразовывать и передавать. Для этих целей в языке Си введены переменные типа «указатель», которые для краткости будем называть просто указателями, если это не приводит к неоднозначности или путанице. Указатель в языке Си можно определить как переменную, значением которой служит адрес объекта как переменную, значением которой служит адрес объекта конкретного типа. Кроме того, значением указателя может быть заведомо не равное никакому адресу значение, принимаемое за нулевой адрес. Для его обозначения в ряде заголовочных файлов, например в файле stdio.h, определена специальная константа NULL.
Как и всякие переменные, указатели нужно определять и описывать, для чего используется, во-первых, разделитель ‘*’. В описании и определении переменных типа «указатель» необходимо сообщать, на объект какого типа ссылается описываемый указатель. Поэтому, кроме разделителя ‘*’, в определения и описания указателей входят спецификации типов, задающие типы объектов, на которые ссылаются указатели.
Примеры определения указателей:
char *z;
/* z – указатель на объект символьного типа */
int *k, *i
/* k, i – указатели на объекты целого типа */
float *f;
/* указатель на объект вещественного типа */
Итак, в определениях и описаниях указателей применяется разделитель ‘*’, который является в этом случае знаком унарной операции косвенной адресации, иначе называемой операцией разыменования или операцией раскрытия ссылки или обращения по адресу. Операндом операции разыменования всегда является указатель. Результат этой операции – тот объект, который адресует указатель-операнд. Таким образом, *z обозначает объект типа char(символьная переменная), на который указывает z; *k – объект типа int (целая переменная), на который указывает k, и т.д. Обозначения *z, *f, *i имеют права переменных соответствующих типов. Оператор z = ‘ ‘; засылает символ «пробел» в тот участок памяти, адрес которого определяет указатель z. Оператор k = i = 0 ; заносит целые нулевые значения в те участки памяти, адреса которых заданы указателями k, i. Обратите внимание, что указатель может ссылаться на объекты того типа, который присутствует в определении указателя. Исключением являются указатели, в определении которых использован тип void - отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования, т.е. операцию ‘*’.
Операции над указателями.В языке Си допустимы следующие (основные) операции над указателями: присваивания, получение значения того объекта, на который ссылается указатель; получение адреса самого указателя; унарные операции изменения значения указателя; аддитивные операции и операции сравнений.
Примеры:
i=&data;
k=i;
z=NULL;
Унарные операции. Операции «++», «--» значения переменных типа указатель меняются по-разному в зависимости от типа данных, с которыми связаны эти переменные. Если указатель связан с типом char, то при выполнении операций его числовое значение изменяется на столько байт, сколько соответствует типу char. Таким образом, при изменении указателя на единицу указатель «переходит к началу» следующего (или предыдущего) поля той длины, которая определяется типом.
Аддитивные операции.Две переменные типа указатель нельзя суммировать, однако к указателю можно прибавить целую величину. При этом вычисляемое значение зависит не только от значения прибавляемой целой величины, но и от типа объекта, с которым связан указатель.
В отличие от операции сложения операция вычитания применима не только к указателю и целой величине, но и к двум указателям на объекты одного типа. С ее помощью можно находить разность (со знаком) двух указателей (одного типа) и тем самым определять «расстояние» между размещением в памяти двух объектов. При этом «расстояние» вычисляется в единицах, кратных «длине» отдельного элемента данных того типа, к которому отнесен указатель.
Например, после выполнения операторов
intx[5], *i, *k, j;
i=&x[0];
k=&x[4];
j=k-i;
j принимает значение 4, а не 8, как можно было бы предположить исходя из того, что каждый элемент массива x[ ] занимает два байта.
В данном примере разность указателей присвоена переменной типа int. Однако, тип разности указателей определяется по-разному, в зависимости от особенностей компилятора. Чтобы сделать язык Си независимым от реализаций, в заголовочном файле stddef.h определено имя (название) ptrdiff_t, с помощью которого обозначается тип разности указателей в конкретной реализации.
Пример:
#include <stdio.h>
#include <stddef.h>
void main()
{
int x[5];
int *i, *k;
ptrdiff_t j;
i=&x[0];
k=&x[4];
j=k-i;
printf(“\nj=%d”, (int)j);
}
Результат:
j=4
Арифметические операции и указатели.Унарные адресные операции & * имеют более высокий приоритет, чем арифметические операции. Унарные операции * и ++ или – имеют одинаковый приоритет и при размещении рядом выполнятся справа налево.
Указатели и отношения.К указателям применяются операции сравнения. Таким образом, указатели можно использовать в отношениях. Но сравнивать указатели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса.
8.2. Указатели и массивы
Указатели и доступ к элементам массивов.По определению, указатель – это либо объект со значением «адрес объекта» или «адрес функции», либо выражение, позволяющее получить адрес объекта или функции.
Пример:
intx, y;
int *p=&x;
p=&y;
Здесь p = указатель-объект, а &x, &y - указатель-выражение, т.е. адреса-константы. Различие между адресом (т.е. указателем-выражением) и указателем-объектом заключается в возможности изменять значения указателей-объектов. Именно поэтому указатели-выражения называют указателями-константами или адресами, а для указателя объекта используют название указатель-переменная или просто указатель.
В соответствии с синтаксисом языка Си имя массива без индексов является указателем-константой, т.е. адресом его первого элемента (с нулевым индексом).
Массивы динамической памяти.В соответствии со стандартом языка массив представляет собой совокупность элементов, каждый из которых имеет одни и те же атрибуты (характеристики). Все элементы размещаются в смежных участках памяти подряд, начиная с адреса, соответствующего началу массива, т.е. значению &имя_массива[0].
Формирование массивов с переменными размерам можно организовать с помощью указателей и средств для динамического выделения памяти. Функция malloc() заголовочного файла stdlib.h динамически выделяет память в соответствии со значениями параметров и возвращает адрес начала выделенного участка памяти. Для универсальности тип возвращаемого значения этой функции есть void *. Этот указатель (указатель такого типа) можно преобразовать к указателю любого типа с помощью операции явного приведения типа (тип *).
Таблица 8. Функции для выделения и освобождения памяти
Функции | Прототип и краткое описание |
malloc | void * malloc(unsigned s); возвращает указатель на начало области (блока) динамической памяти длиной в s байт. При неудачном завершении возвращает значение NULL. |
calloc | void * calloc(unsigned n, unsigned m); Возвращает указатель на начало области (блока) обнуленной динамической памяти, выделенной для размещения n элементов по m байт каждый. При неудачном завершении возвращает значение NULL. |
realloc | void * realloc( void * bl, unsigned ns); Изменяет размер блока ранее выделенной динамической памяти до размера ns байт; bl – адрес начала изменяемого блока. Если bl равен NULL (память не выделялась), то функция выполняется как malloc. |
free | void * free (void * bl); Освобождает ранее выделенный участок (блок) динамической памяти, адрес первого байта которого равен значению bl. |
Пример:
#include <stdio.h>
#include <stdlib.h>
void main()
{
/* работа с динамическими массивами */
/* вычислить y = pi * summa(a[i] / i); */
float *a, y, pi = 3.1416;
int i, n;
printf("\nn="); /* n - число элементов */
scanf("%d", &n);
a = (float *) malloc(n * sizeof(float));
for (i = 0; i < n; i++) /* цикл ввода чисел */
{
printf("a[%d] = ", i);
scanf("%f", &a[i]);
}
/* цикл вычисления суммы */
y = 0;
for (i = 0; i < n; i++) /* цикл вычисления суммы */
{
y = y + a[i] / (i + 1);
}
y = pi * y;
printf("\ny=%12.5f", y);
free (a); /* освобождение памяти */
}
В программе int n –количество вводимых чисел типа float, а – указатель на начало области, выделяемой для размещения n вводимых чисел. Указатель принимает значение адреса области, выделяемой для n штук значений типа float.
Определение количества элементов массива в программе.Используется выражение
sizeof(имя_массива) / sizeof (имя_массива[0])
Массивы указателей и моделирование многомерных массивов.Очевидного способа прямого определения многомерных массивов с несколькими переменными размерами в языке Си не существует. Решение, как и для случая одномерных массивов динамической памяти, находится в области системных средств (библиотечных функций) для динамического управления памятью. Прежде чем рассматривать их возможности для многомерных массивов, необходимо ввести массивы указателей.
Массив указателей фиксированных размеров вводится одним из следующих определений:
тип *имя_массива [размер];
тип *имя_массива [ ] = инициализатор;
тип *имя_массива [размер] = инициализатор;
где
тип может быть как одним из базовых типов, так и производным типом;
имя_массива – свободно выбираемый идентификатор;
инициализатор – список в фигурных скобках значений типа тип *.
Примеры:
intdata [6]; /* обычный массив */
int *pd [6];/ массив указателей */
int *pi[ ] = { &data[0], &data[4], &data[2] };
Здесь каждый элемент массивов pd и pi является указателем на объекты типа int. Значением каждого элемента pd[j] и pi[k] может быть адрес объекта типа int. Все 6 элементов массива pd указателей типа int * не инициализированы. В массиве pi три элемента, которые инициализированы адресами конкретных элементов массива data.
Пример: динамический двумерный массив (суммирование элементов).
#include <stdio.h>
#include <stdlib.h>
void main()
{
/* работа с двумерными динамическими массивами */
/* вычислить y = summa(a[i] [j]); */
float *a, y;
int i, j, n, m, number;
printf("\nn="); /* n - число строк */
scanf("%d", &n);
printf("\nm="); /* m - число столбцов */
scanf("%d", &m);
number = 0;
a = (float *) calloc(n * m, sizeof(float *)) ;
for (i = 0; i < n; i++) /* цикл по i ввода чисел по строкам*/
for (j = 0; j < m; j++) /* цикл по j ввода чисел по столбцам */
{
number = i * m + j;
printf("a[%d,%d] = ", i, j);
scanf("%f", &a[number]);
}
/* цикл вычисления суммы */
y = 0;
for (i = 0; i < n; i++) /* цикл по строкам */
for (j = 0; j < m; j++) /* цикл по столбцам */
{
number = i * m + j;
y = y + a[number];
}
printf("\ny=%12.5f", y);
free (a); /* освобождение памяти */
}
Пример:
умножение матрицы на матрицу (динамические массивы).
#include <stdio.h>
#include <stdlib.h>
void main()
{
/* работа с двумерными динамическими массивами */
/* умножение матрицы а[n, m] на матрицу b [n1, m1]*/
float *a, *b, *c;
int i, j, n, m, n1, m1, k, number, number1, number2;
printf("\nn="); /* n - число строк */
scanf("%d", &n);
printf("\nm="); /* m - число столбцов */
scanf("%d", &m);
printf("\nn1="); /* n - число строк */
scanf("%d", &n1);
printf("\nm1="); /* m - число столбцов */
scanf("%d", &m1);
a = (float *) calloc(n * m, sizeof(float *));
b = (float *) calloc(n1 * m1, sizeof(float *));
c = (float *) calloc(n * n, sizeof(float *));
/* ввод матрицы а */
for (i = 0; i < n; i++) /* цикл по i ввода чисел по строкам */
for (j = 0; j < m; j++) /* цикл по j ввода чисел по столбцам */
{
number = i * m + j;
printf("a[%d,%d] = ", i, j);
scanf("%f", &a[number]);
}
/* ввод матрицы b */
for (i = 0; i < n1; i++) /* цикл по i ввода чисел по строкам */
for (j = 0; j < m1; j++) /* цикл по j ввода чисел по столбцам */
{
number = i * m1 + j;
printf("b[%d,%d] = ", i, j);
scanf("%f", &b[number]);
}
/* цикл вычисления произведения */
for (i = 0; i < n; i++) /* цикл по строкам */
for (k = 0; k < n; k++) /* цикл по столбцам */
for (j = 0; j < m; j++) /* цикл по сумме */
{
number = i * m + j;
number1 = j * n + k;
number2 = i * n + k;
c[number2] = c[number2] + a[number] * b[number1];
}
/* вывод матрицы а */
for (i = 0; i < n; i++) /* цикл по i вывода чисел по строкам */
for (j = 0; j < n; j++) /* цикл по j вывода чисел по столбцам */
{
number = i * n + j;
printf("c[%d,%d] = ", i, j);
printf("%f", c[number]);
}
free (a); /* освобождение памяти */
free (b);
free (c);
}
8.3. Символьная информация и строки
Для символьных строк и символьных констант в языке Си не введено отдельного типа.
Описание символьных переменных имеет вид:
charсписок_имен_переменных;
Например: char a,b;
Программа:ввести предложение, слова в котором разделены пробелами и в конце которого стоит точка. Удалить повторяющиеся пробелы между отдельными словами (оставить по одному пробелу), вывести отредактированное предложение на экран.
#include <stdio.h>
/* ввести предложение, слова в котором разделены пробелами
и в конце которого стоит точка.
Удалить повторяющиеся пробелы между отдельными словами
(оставить по одному пробелу), вывести отредактированное предложение на экран */
void main()
{
char z, s; /* текущий вводимый символ */
printf("\n Напишите предложение с точкой в конце ");
for (z=s=' '; z != '.'; s=z)
{
/* s - предыдущий символ */
scanf("%c", &z);
if (z == ' ' && s == ' ') continue;
printf("%c", z);
} /* конец цикла */
} /* конец программы */
В программе две символьные переменные: z – для чтения очередного символа и s – для хранения предыдущего. В заголовке цикла переменные z и s получают значение «пробел». Очередной символ вводится как значение переменной z, и пара z, s анализируется. Если хотя бы один из символов значений пары отличен от пробела, то значение z печатается. В заголовке цикла z сравнивается с символом «точка» и при несовпадении запоминается как значение s. Далее цикл повторяется до появления на входе точки, причем появление двух пробелов (z и s) приводит к пропуску оператора печати.
Помимо scanf() и printf() для ввода и вывода символов в библиотеке предусмотрены специальные функции обмена.
getchar()- функция без параметров. Позволяет читать из входного потока (обычно клавиатура) по одному символу за обращение.
putchar(X)– выводит символьное значение Х в стандартный выходной поток (обычно экран дисплея).
Пример:подсчет числа отличных от пробелов символов.
#include <stdio.h>
/* подсчет числа отличных от пробелов символов */
void main()
{
char z; /* вводимый символ */
int k; /* количество значащих символов */
printf("\n Напишите предложение с точкой в конце ");
for (k = 0; (z = getchar()) != '.';)
if (z != ' ') k++;
printf("\n Количество символов=%d", k);
} /* конец программы */
Внутренние коды и упорядоченность символов.В языке принято соглашение, что везде, где синтаксис позволяет использовать целые числа, можно использовать и символы, т.е. данные типа char, которые при этом представляются числовыми значениями своих внутренних кодов. Такое соглашение позволяет сравнительно просто упорядочивать символы, обращаясь с ними как с целочисленными величинами. Например, внутренние коды десятичных цифр в таблицах кодов ASCII упорядочены по числовым значениям, поэтому несложно перебирать символы десятичных цифр в нужном порядке.
Пример: печать латинского алфавита.
/* печать латинского алфавита */
#include <sdtio.h>
void main()
{
char z;
for (z=’A’; z <= ‘Z’; z++)
printf(“%c”, z);
}
Результат выполнения программы:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Строки и строковые константы.Транслятор отводит каждой строке отдельное место в памяти ЭВМ даже в тех случаях, когда несколько строк полностью совпадают (стандарт это не регламентирует). Размещая строку в памяти, транслятор автоматически добавляет в ее конце символ ‘0\’ т.е. нулевой байт. В записи строки может быть и один символ: “A”, однако в отличие от символьной константы ‘A’ длина строки “A” равна двум байтам. Принято, что строка – это массив символов, т.е. она всегда имеет тип char[ ]. Количество элементов в таком массиве на 1 больше, чем в изображении соответствующей строковой константы, так как в конец строки добавлен нулевой байт.
Строки и указатели.
char A[ 20 ]; /* массив, в который можно записать строку */
char *B; /* указатель, с которым можно связать строку */
scanf(“%s” A); /* оператор верен */
scanf(“%s”, B); /* оператор не корректен */
До выполнения второго оператора ввода с указателем В нужно связать некоторый участок памяти. Для этого существует несколько возможностей. Во-первых, переменной В можно присвоить адрес уже определенного символьного массива. Во-вторых, указатель В можно «настроить» на участок памяти, выделяемый с помощью средств динамического распределения памяти. Например, оператор
B=(char *) malloc(80);
Выделяет 80 байт и связывает этот блок памяти с указателем В. Только теперь применение приведенного выше оператора ввода допустимо.
Пример:
#include <stdio.h>
void main()
{
char * point[ ] = {“один”, “два” , “три”, “четыре”);
int i, n;
n = sizeof(poin) / sizeof(point[ 0 ]);
for (i = 0; i < n ; i++)
printf(“\n%s”, poin[i]);
}
Результат:
один
два
три
четыре
Контрольные вопросы.
1. Стадии препроцессорной обработки.
2. Директивы препроцессора.
3. Назначение директивы #define.
4. Как включить текст из файла в программу на языке Си?
5. Как осуществить макроподстановки средствами препроцессора?
6. Назначение указателей в языке Си.
7. Допустимые операции над указателями.
8. Как осуществить реализацию массивов динамической памяти в программе на Си?
9. Применение строк и строковых констант в языке Си.
Глава 9. Функции
9.1. Общие сведения о функциях
Определение функции.О функциях в языке Си нужно говорить, рассматривая это понятие с двух сторон. Во-первых, функция – это одни из производных типов, наряду с массивом и указателем). С другой стороны, функции – это минимальный исполняемый модуль программы на языке Си. Синонимами этого второго понятия в других языках программирования являются процедуры, подпрограммы, подпрограммы-функции, процедуры-функции. Все функции в языке Си имеют рекомендуемый стандартами языка единый форма определения:
тип имя_функции (спецификация_параметров)
тело_функции
Первая строках - это, по существу, заголовок функции, который отличается от ее прототипа только отсутствием точки с запятой в конце и обязательным присутствием имен формальных параметров.
Здесь тип либо void(для функций, не возвращающих значения), либо обозначение типа возвращаемого функцией значения.
Имя_функции либо main – для основной (главной) функции программы, либо произвольно выбираемое программистом имя (идентификатор), не совпадающее со служебными словами и с именами других объектов (и функций) программы.
Спецификация_параметров – это либо пусто, либо список формальных параметров, каждый элемент которого имеет вид:
обозначение_тип имя_параметра
Список параметров функции может заканчиваться запятой с последуюим многоточием «...». Многоточие обозначает возможность обращаться к функции с большим количеством фактических параметров, чем явно указано в в спецификации параметров. Такая возможность должна быть «подкреплена» специальными средствами в теле функции.
Пример: прототипы известных функций.
int printf(const char * format, ...);
int scanf(const char * format, ...);
Указанные функции форматированного вывода и форматированного ввода позволяют применять теоретически неограниченное количество фактических параметров. Обязательным является только параметр char * format – «форматная строка», внутри которой с помощью спецификаций преобразования определяется реальное количество параметров, участвующих в обменах.
Тело_функции это часть определения функции, ограниченная фигурными скобками и непосредственно размещенная вслед за заголовком функции. Тело функции может быть либо составным оператором, либо блоком. Напоминаем, что в отличие от составного оператора блок включает определения объектов (переменных, массивов и т.д.). Особенность языка Си состоит в невозможности внутри тела функции определить другую функцию. Другими словами, определения функций не могут быть вложенными.
Обязательным, но не всегда явно используемым оператором тела функции является оператор возврата из функции в точку вызова, имеющий две формы:
return;
return выражение;
Первая форма соответствует завершению функции, не возвращающей никакого значения, т.е. функции, перед именем которой в ее определении указан тип void. Выражение во второй форме оператора return должно иметь тип, указанный перед именем функции в ее определении, либо иметь тип, допускающий автоматическое преобразование к типу возвращаемого функцией значения.
Описание функции и ее тип.Для корректного обращения к функции сведения о ней должны быть известны компилятору, т.е. до вызова функции в том же файле стандартом рекомендуется помещать ее определение или описание. Для функции, определенной стандартным образом, описанием служит ее прототип:
тип имя_функции (спецификация_параметров);
В отличие от заголовка функции в ее прототипе могут не указываться имена формальных параметров. Например, допустимы и эквивалентны следующие прототипы одной и той же функции:
double f (int n, float x);
double f(int, float);
Вызов функции.Для обращения к функции используется выражение с операцией «круглые скобки»:
обозначение_функции (список_фактических_параметров)
операндами операции ‘()’ служат обозначение_функции и список_фактических_параметров. Наиболее естественное и понятное обозначение_функции – это ее имя. Кроме того, функцию можно обозначить, разыменовав указатель на нее.
Список фактических параметров, называемых аргументами, - это список выражений, количество которых равно числу формальных параметров функци (иключение составляют функции с переменным количеством параметров). Соответствие между формальными и фактическими параметрами устанавливается по их взаимному расположению в списках. Порядок вычисления значений фактических параметров (слева направо или справа налево) стандарт языка Си не определяет.
Между формальными и фактическими параметрами должно быть соответствие по типам. Лучше всего, когда тип фактического параметра совпадает с типом формального параметра. В противном случае компилятор автоматически добавляет команды преобразования типов, что возможно только в том случае, если такое приведение типов допустимо. Например, пусть определена функция с прототипом:
int g (int, long);
Далее в программе использован вызов:
g (3.0+m, 6.4e+2);
Оба фактических параметра в этом вызове имеют тип double. Компилятор, ориентируясь на прототип функции, автоматически предусмотрит такие преобразования:
g ((int) (3.0+m), (long) 6.4e+2);
Так как вызов функции является выражением, то после выполнения операторов тела функции в точку вызова возвращается некоторое значение, тип которого строго соответствует типу, указанному перед именем функции в ее определении (и прототипе). Например, функция
float ft (double x, intn)
{
if (x < n) return x;
return n;
}
всегда возвращает значение типа float. В выражения, помещенные в операторы return, компилятор автоматически добавит средства для приведения типов, т.е. получи (невидимые программисту) операторы:
return (float) x;
return(float) n;
Особое внимание нужно уделить правилам передачи параметров при обращении к функциям. Синтаксис языка Си предусматривает только один способ передачи параметров – передачу по значениям. Это означает, что формальные параметры функции локализованы в ней, т.е. недоступны вне определения функции и никакие операции над формальными параметрами в теле функции не изменяют значений фактических параметров.
Передача параметров по значению предусматривает следующие шаги:
1. При компиляции функции (точнее при подготовке к ее выполнению) выделяются участки памяти для формальных параметров, т.е. формальные параметры оказываются внутренними объектами функции. Если параметром является массив, то формируется указатель на начало этого массива и он служит представлением массива – параметра в теле функции.
2. Вычисляются значения выражений, использованных в качестве фактических параметров при вызове функции.
3. Значения выражений – фактических параметров заносятся в участки памяти, выделенные для формальных параметров функции.
4. В теле функции выполняется обработка с использованием значений внутренних объектов-параметров, и результата передается в точку вызова функции как возвращаемое ею значение.
5. Никакого влияния на фактические параметры (на их значения) функция не оказывает.
6. После выхода из функции освобождается память, выделенная для ее формальных параметров.
Вызов функции всегда является выражением, однако размещение такого выражения в тексте программы зависит от типа возвращаемого функцией значения. Если в качестве типа возвращаемого значения указан тип void, то функция является функцией без возвращаемого результата. Такая функция не может входить ни в какие выражения, требующие значения, а должна вызываться в виде отдельного выражения-оператора:
имя_функции (список_фактических_параметров);
Пример: возвращает значение типа void:
voidprint(int gg, int mm, int dd)
{
printf(“\n год: %d”, gg);
printf(“\n месяц: %d”, mm);
printf(“\n день: %d”, dd);
}
Обращение к ней
print(1966, 11, 22);
приведет к такому выводу на экран:
год 1966 месяц 11 день 22
Может оказать полезной и функция, которая не только не возвращает никакого значения (имеет возвращаемое значение типа void), но и не имеет параметров. Например, такая:
#include <stdio.h>
void RealTime (void)
{
printf(“\n Текущее время: %s”, _ _TIME_ _ “(час: мин: сек.)”);
}
При обращении
RealTime ();
в результате выполнения функции будет выведено на экран дисплея сообщение:
Текущее время: 14:16:25 (час: мин: сек.)
9.2. Указатели в параметрах функций
Указатель-параметр.Схема передачи параметров по значениям не оставляет никаких надежд на возможность непосредственно изменить фактический параметр за счет выполнения операторов тела функции. Однако существует косвенная возможность изменять значения объектов вызывающей программы действиями в вызванной функции. Эту возможность обеспечивает аппарат указателей. С помощью указателя в вызываемую функцию можно передать адрес любого объекта из вызывающей программы. С помощью выполняемого в тексте функции разыменования указателя мы получает доступ к адресуемому указателем объекту из вызывающей программы.
Тем самым, не изменяя самого параметра (указатель–параметр постоянно содержит только адрес одного и того же объекта), можно изменять объект вызывающей программы.
Пример:
#include <stdio.h>
void positive(int * m) /* определение функции */
{
*m = *m > 0 ? * m : -*m;
}
void main()
{
int k =-3;
positive(&k);
printf(“\nk=%d”, k);
}
Результат выполнения программы:
k = 3
Параметр функции positive() – указатель типа int * . При обращении к ней из основной программы main() в качестве фактического параметра используется адрес &k переменной типа int. Внутри функции значение аргумента (т.е. адрес &k) «записывается» в участок памяти, выделенный для указателя int *m. Разыменование *m обеспечивает доступ к тому участку памяти, на который в этот момент «смотрит» указатель m. Тем самым в выражении
*m = *m > 0 ? * m : -*m
все действия выполняются над значениями той переменной основной программы (int k), адрес которой (&k) использован в качестве фактического параметра.
Имитация подпрограмм. Подпрограммы отсутствуют в языке Си, однако если использовать обращение к функции в виде оператора-выражения, то получим аналог оператора вызова подпрограммы.
Например, void RealTime() очень похожа на подпрограммы других языков программирования.
Пример: имитация подпрограммы (функции) для вычисления периметра и площади треугольника:
#include <stdio.h>
#include <math.h>
void main()
{
float x, y, z, pp, ss;
/* описание прототипа */
int triangle(float, float, float, float *, float *);
printf("\n введите x=");
scanf("%f",&x);
printf("\t y=");
scanf("%f", &y);
printf("\t z=");
scanf("%f", &z);
if (triangle(x, y, z, &pp, &ss) == 1)
{
printf(" периметр = %f", pp);
printf(", площадь = %f", ss);
}
else
printf("\n ошибка в данных ");
}
/* определение функции */
int triangle(float a, float b, float c, float * perimetr, float * area)
{
float e;
*perimetr = *area = 0.0;
if (a+b <= c || a+c <= b || b+c <= a)
return 0;
*perimetr = a+b+c;
e=*perimetr / 2;
*area = sqrt(e*(e-a)*(e-b)*(e-c));
return 1;
}
Результаты:
Введите х=3
у=4
z=5
периметр=12.000000 площадь=6.000000
9.3. Массивы и строки как параметры функций
Массивы в параметрах. Если в качестве параметра функции используется обозначение массива, то на самом деле внутрь функции передается только адрес начала массива. Например, заголовок функции для вычисления скалярного произведения векторов выглядел так:
float Scalar_Product(intn, float a[ ], floatb[ ])
а можно и так:
float Scalar_Product(intn, float *a, float *b)
Конструкции float b[ ] и float *b совершенно равноценны.
Так как массив всегда передается в функции как указатель, то внутри функции можно изменять значения элементов массива - фактического параметра, определенного в вызывающей программе. Это возможно и при использовании индексирования, и при разыменовании указателей на элементы массива.
Строки как параметры функций.Строки в качестве фактических параметров могут быть специфицированы либо как одномерные массивы типа char[ ], либо как указатели типа char * . В обоих случаях с помощью параметра в функцию передается адрес начала символьного массива, содержащего строку. В отличие от обычных массивов для параметров-строк нет необходимости явно указывать их длину. Признак “\0”, размещаемый в конце каждой строки, позволяет всегда определить ее длину, точнее, позволяет перебирать символы строки и не выйти за ее пределы. Как примеры использования строк в качестве параметров функций рассмотрим несколько функций, решающих типовые задачи обработки строк. Аналоги большинства из приводимых ниже функций имеются в библиотеке стандартных функций компилятора.