Головная программа с обращениями к функциям

}

Описание функций

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

int summa(int a, int b);

void vorm_mass(int n, int m, int mass[n][m]);

Внимание! После закрывающих скобок точка с запятой ставится обязательно!

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

Сами функции описываются после головной программы:

int summa(int a, int b)

{

int s; // s – локальная переменная

s = a + b;

return s; // возврат вычисленного значения

}

void vorm_mass(int n, int m, int mass[n][m])

{

int i, j;

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

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

{

// задание значений элементам массива mass[n][m]

}

}

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

В последнем случае оператор return не нужен, так как функция vorm_mass имеет тип void – ничего не возвращает.

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

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

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

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

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

int summa(int a, int b)

{

return a + b; // возврат вычисленного значения

}

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

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

int summa(int a, int b)

{

int s; // s – локальная переменная

a++;

b++;

s = a + b;

return s; // возврат вычисленного значения

}

Внутри тела функции формальные параметры a и b увеличились на единицу, и сумма определяется при их новых значениях. При выходе из этой функции фактические (входные) параметры, соответствующие этим формальным параметрам, не изменятся.

Таким образом, функция в Си вычисляет единственное значение, передаваемое вовне оператором return .

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

Создадим функцию, которая обменивает значениями две переменных:

void swap(int a, int b)

{

int temp; // temp – локальная переменная

temp=a; // алгоритм циклического обмена

a=b;

b=temp;

}

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

void swap(int *a, int *b) // используем значения переменных

{ // по адресам переменных a и b

int temp;

temp=*a; // алгоритм циклического обмена

*a=*b; // значениями, находящимися

*b=temp; // по этим адресам

}

Эта функция использует не формальные параметры, а значения, находящиеся по адресам формальных параметров. Сами адреса переменных функцией не изменяются, как это и положено формальным параметрам. Меняются только значения, находящиеся по данным адресам, а эти значения не являются формальными параметрами. Хитро придумано!

Обратимся к этой функции, например, из головной программы:

int x, y;

x = 5;

y = 3;

swap(&x, &y); // используем адреса фактических переменных

В этом случае переменные x и y обменяются своими значениями.

Если в качестве формальных параметров используются имена массивов (строк), то в списке фактических параметров перед ними знаки амперсанда &не ставятся: имя массива в Си является адресом его первого элемента. Поэтому в функцию передается не массив со всеми значениями его элементов, а только адрес его первого элемента. Адреса всех остальных элементов вычисляются автоматически:

void poplavok(int n, int vector[n])

{

int top, bottom, temp;

for (top=0, bottom = n-1; top<bottom; top++, bottom--)

{

temp = vector[top];

vector[top] = vector[bottom];

vector[bottom] = temp;

}

}

Эта функция переворачивает вектор vector[n] – выполняет «поплавок».

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

void poplavok(int n, int vector[])

Обратимся к этой функции из головной программы:

#include <stdio.h>

#include <conio.h>

void poplavok(int n, int vector[]); // прототип функции

Int main()

{

int i, k=5;

int vect[k] = {1,2,3,4,5}; // инициализация вектора

printf("\n"); // вывод исходного вектора

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

printf("%5d", vect[i]);

printf("\n");

poplavok(k, vect); // обращение к функции

for (i=0; i<k; i++) // вывод полученного вектора

printf("%5d", vect[i]);

printf("\n");

printf("\n");

}

void poplavok(int n, int vector[]) // описание функции

{

int top, bottom, temp;

for (top=0, bottom = n-1; top<bottom; top++, bottom--)

{

temp = vector[top];

vector[top] = vector[bottom];

vector[bottom] = temp;

}

}

На экран будет выведено:

1 2 3 4 5

5 4 3 2 1

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

void vorm_mass(int n, int m, int mass[n][m])

{

int i, j;

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

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

{

mass[i][j]=i + j;

}

}

Обратимся к этой функции из головной программы:

#include <stdio.h>

#include <conio.h>

void vorm_mass(int n, int m, int mass[n][m]); // прототип

Int main()

{

int i, j;

int k=2, d=3;

int massiv[k][d];

vorm_mass(k, d, massiv); // обращение к функции

printf("\n");

for (i=0; i<k; i++) // вывод полученного массива

{ // построчно

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

printf("%5d", massiv[i][j]);

printf("\n");

}

}

void vorm_mass(int n, int m, int mass[n][m])

{ // описание функции

int i, j;

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

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

{

mass[i][j]=i + j;

}

}

Результат работы программы:

0 1 2

1 2 3

Рекурсия

Использование рекурсии в программировании базируется на рекурсивных математических определениях. Считается, что в математике рекурсивность как принцип определений используется с 1890 года. Впервые ее применил Д. Гильберт.

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

Головная программа с обращениями к функциям - student2.ru Например, вычисление факториала целого неотрицательного числа n! = 1·2·3·…·(n-1) · n . Кроме того, по определению, 0! = 1. Рекурсивное математическое определение факториала имеет вид

1 при n = 0,

n!=(n – 1)!·n при n > 0.

Последовательность чисел Фибоначчи имеет вид 1, 1, 2, 3, 5, 8, 13…

Головная программа с обращениями к функциям - student2.ru В ней два первых числа фиксированы и равны единице, а каждое последующее число равно сумме двух предыдущих. Рекурсивное математическое определение числа Фибоначчи с порядковым номером n имеет вид:

1 при n = 1,

1 при n = 2,

Fn=Fn-2 + Fn-1 при n > 2.

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

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

Рассмотрим последовательность факториалов целых чисел 0!, 1!, 2!, 3!, …, в которой ai = i!, i = 1, 2, 3, …. Эту же последовательность можно представить в виде рекуррентной формулы: ai = ai-1·i, a0 = 1, i = 1, 2, 3… Эта формула задает последовательность, в которой каждый очередной член зависит непосредственно от предшествующего. Начальный член последовательности a0 задан прямою. Найдя член последовательности с порядковым номером i = n, мы тем самым решим задачу вычисления n!

Рекуррентная формула для вычисления числа Фибоначчи с заданным порядковым номером i = n практически не отличается от рекурсивного определения:

Fi = Fi-2 + Fi-1, F1 = 1, F2 = 1, i = 3, 4, 5,…

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

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

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

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

При рекурсивном обращении каждый раз приходится запоминать не только адрес возврата, но и всю совокупность данных вызывающей подпрограммы (локальные переменные и параметры-значения). С этой целью используется автоматически выделяемая область памяти – стек, структура, работающая по принципу LIFO (Last in – first out: последним пришел – первым вышел). Такой метод работы с памятью обеспечивает строгое соответствие прямого порядка записи данных обратному порядку их чтения. Только с помощью стека можно достаточно просто обеспечить корректное завершение работы цепочки подпрограмм, каждая из которых вызывает следующую: сначала должна быть завершена последняя, затем – предпоследняя и так далее. Максимальный размер стека – 65520 байт. Поэтому последовательность рекурсивных обращений не может быть бесконечной. В любой рекурсивной подпрограмме должна быть нерекурсивная (терминальная) ветвь, обеспечивающая выход из рекурсии. При переполнении стека работа программы прерывается и появляется сообщение об ошибке.

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

Long factorial(int n)

{

if (n <= 1)

return 1; // выход из рекурсии – терминальная ветвь

else return n * factorial(n-1);

}

При n = 5 эта функция будет работать следующим образом:

factorial := 5 * factorial(4)

5 * 4 * factorial(3)

5 * 4 * 3 * factorial(2)

5 * 4 * 3 * 2 * factorial(1)

5 * 4 * 3 * 2 * 1 = 120

В данном случае реализована так называемая нисходящая рекурсия: вызов factorial(5) означает, что функция factorial вызывает себя раз за разом: factorial(4), factorial(3), … – до тех пор, пока не будет достигнута терминальная ситуация – ситуация окончания рекурсии. При каждом вызове текущие вычисления откладываются, локальные переменные и адрес возврата остаются в стеке. Терминальная ситуация factorial = 1 достигается при n = 1. При этом рекурсивный спуск заканчивается, начинается рекурсивный возврат изо всех вызванных в данный момент копий функции: начинает строиться ответ n*factorial(n-1). Сохраненные локальные параметры выбираются из стека в обратной последовательности, а получаемые промежуточные результаты: 1*1, 2*1, 3*2*1, 4*3*2*1, 5*4*3*2*1 – передаются вызывающим функциям.

Рекурсивная функция, вычисляющая n-й член ряда Фибоначчи, может иметь вид:

Int fibo(int n)

{

if ((n == 1) || (n == 2))

return 1; // выход из рекурсии

else return fibo(n-2) + fibo(n-1);

}

Примеры

1. Составить функцию, рекурсивно определяющую значение биномиального коэффициента Головная программа с обращениями к функциям - student2.ru при 0<m<n по формулам:

Головная программа с обращениями к функциям - student2.ru = Головная программа с обращениями к функциям - student2.ru = 1, Головная программа с обращениями к функциям - student2.ru = Головная программа с обращениями к функциям - student2.ru + Головная программа с обращениями к функциям - student2.ru

Int binom(int m, int n)

{

if ((m == 0) || (m == n))

return 1; // выход из рекурсии

else return binom(m, n-1) + binom(m-1, n-1);

}

2. Составить функцию, рекурсивно определяющую максимальный элемент в заданной части целочисленного массива vectorn , начиная с k-го и до n-го элемента:

int max_element(int k, int n, int vector[])

{

int temp;

if (k == n-1)

return a[n-1]

Else

{

temp = max_element(k+1, n, vector[]);

if (a[k] > temp)

return a[k];

else return temp;

}

}

3. Составить функцию, реализующую рекурсивный алгоритм К. Хоара быстрой сортировки массива vectorn. Сравниваются элементы vectori и vectorj , причем i = 1, j = n-1. Если vectori< vectorj, то эти элементы уже отсортированы по возрастанию, поэтому значение правого индекса уменьшается на единицу, и алгоритм повторяется. Если vectori> vectorj, то они меняются местами, останавливается правый индекс и начинает увеличиваться левый. Обмен значениями с изменением направления движения после каждого обмена продолжается до тех пор, пока левый и правый индексы не встретятся друг с другом: i = j. В этом случае элемент vectori будет стоять на своем месте в массиве: слева от него стоят элементы меньше его, а справа – больше. После этого алгоритм рекурсивно повторяется для левой и правой частей массива:

void quick_sort(int left, int right, int vector[])

{

int i, last;

if (left >= right) // в векторе меньше двух элементов

return;

swap(left, (left + right)/2, vector);

last= left;

for (i=left+1; i<=right; i++)

if (vector[i]<vector[left])

swap(++last, i, vector);

swap(left, last, vector);

quick_sort(left, last-1, vector);

quick_sort(last+1, right, vector);

}

Операцию перестановки i-го и j-го элементов массива можно оформить функцией

void swap(int i, int j, int vector[])

{

int temp;

temp=vector[i];

vector[i]=vector[j];

vector[j]=temp;

}

Особенности рекурсии:

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

· недостатки рекурсии состоят в следующем:

1) если глубина рекурсии велика, то программа будет требовать во время исполнения много памяти, что может привести к переполнению стека,

2) рекурсивные алгоритмы, как правило, выполняются более медленно,

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

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

АДРЕСА И УКАЗАТЕЛИ

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

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

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

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

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

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

Оперативная память компьютера – это последовательность байтов, каждый из которых имеет номер (адрес). Адреса байтов памяти идут по возрастанию: если к – это номер текущего байта, то к - 1 – номер предыдущего, а к + 1 – последующего байтов. Указатель как раз и содержит адрес некоторого байта памяти. Меняя значение указателя, можно перемещаться по памяти, адресуясь к различным данным, записанным в просматриваемых ячейках.

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

Применение указателей полезно:

- при работе с массивами – обеспечивается использование сразу всех элементов массива,

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

- при работе с файлами – обеспечивается быстрый доступ к компонентам файла,

- при создании новых переменных в процессе выполнения программы – можно динамически выделять память для них.

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

* – доступ по адресу (обращение по адресу),

& – получение адреса.

Знак * , стоящий перед именем переменной-указателя, означает «взять (записать) значение по данному адресу»: *ptr – записать значение по адресу ptr.

Знак & , стоящий перед именем обычной переменной, означает «получить адрес переменной»: &x – получить адрес переменной x.

Перед использованием в программе указатели, как и любые другие переменные, должны быть описаны – задан тип объекта, адрес которого будет хранить указатель, поставлена звездочка * и задано имя указателя:

int i, j, *ptr;

float x, y, *ukaz;

char c, d, *adr;

Описаны указатели:

рtr – на любой объект (переменную, массив, функцию) целого типа,

ukaz – на любой объект вещественного типа,

adr – на любой объект символьного типа.

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

ptr = &i;

ukaz = &x;

adr = &c;

Сейчас указатели ptr, ukaz, adr будут хранить адреса (номера) первых (младших) байтов памяти, отведенной для переменных i, x, c (адрес байта памяти – это шестнадцатеричное число).

Присвоим этим переменным некоторые значения:

i = 5;

x = 3.1416;

c = ‘@’;

Обратимся к ним по адресу:

j = *ptr;

y = *ukaz;

d = *adr;

Содержимое ячейки памяти с адресом ptr (а там записано число 5 – ведь это адрес переменной i) будет присвоено переменной j, содержимое ячейки памяти с адресом ukaz – переменной y, содержимое ячейки памяти с адресом adr – переменной d.

Таким образом, два оператора присваивания:

ptr = &i;

j = *ptr;

выполняют то же самое, что и один оператор:

j = i;

Значит, можно организовать не только прямую передачу данных от одной переменной к другой (j = i), но и косвенную – через адреса переменных, даже не упоминая их имен. Указатели позволяют обращаться к конкретным ячейкам памяти и изменять их содержимое, не интересуясь тем, значения каких именно переменных хранятся в этих ячейках:

*adr = ‘+’;

переменной по адресу adr присвоено значение символа ‘+’,

(*ptr)++;

значение переменной по адресу ptr увеличивается на единицу,

(*ptr) += 3;

значение переменной по адресу ptr увеличивается на 3,

j = (*ptr)++;

значение переменной по адресу ptr присваивается переменной j и после этого увеличивается на единицу.

Если это выражение записать без скобок:

j = *ptr++;

то сначала значение адреса ptr увеличивается на единицу (получаем адрес следующей ячейки памяти), а затем содержимое новой ячейки памяти присваивается переменной j : операции доступа по адресу * и инкремента ++ имеют одинаковый приоритет, но выполняются справа налево.

Операции над указателями

Помимо операций доступа по адресу * и получения адреса &, над указателями определены следующие операции:

- сложение с константой,

- вычитание,

- инкремент,

- декремент,

- операции отношений.

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

char a; // a - переменная типа char

char *ptr; // выделение памяти под указатель ptr

*ptr = ‘@’; // по адресу ptr записано значение @

a = *ptr; // переменной а присвоено значение,
// записанное по адресу ptr

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

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

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

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

Внимание! Суммирование двух указателей недопустимо!

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

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

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

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

int mass[5];

то в оперативной памяти для его элементов выделяется пять подряд идущих ячеек:

Головная программа с обращениями к функциям - student2.ru

К i-му элементу этого массива можно обратиться, назвав его индекс: mass[i] .

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

int *ptr;

В результате присваивания

ptr = &mass[0];

эта переменная будет содержать адрес начального (нулевого) элемента этого массива, то есть указатель ptr будет указывать на элемент mass[0]:

Головная программа с обращениями к функциям - student2.ru

Адрес начального элемента любого массива называется базовым адресом этого массива. Таким образом, сейчас указатель ptr содержит базовый адрес массива mass. Если увеличить значение указателя на единицу, то ptr + 1 будет указывать на следующий элемент массива, то есть на mass[1], ptr + 2 – на элемент mass[2] и так далее. В общем случае, если значение указателя увеличить на k , то можно получить адрес k-го элемента массива mass.

Значит, адрес любого элемента массива равен сумме его базового адреса, который является адресом его начального элемента, и смещения этого элемента от начала массива. Для начального (нулевого) элемента массива это смещение равно нулю, для первого элемента – единице, для второго – двум, для k-го оно равно k. Это верно для массива любого типа. Смысл выражения «увеличить указатель ptr на единицу», как и смысл любой арифметики с указателями, заключается в том, что ptr + 1 указывает на следующий за ptr элемент, а ptr + k – на k-й после ptr элемент массива.

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

ptr = &mass[0];

можно записать в другом виде:

ptr = mass;

Это будет одно и то же: записи &mass[0] и mass эквивалентны.

Из всего этого следует, что в общем случае запись &mass[k] будет эквивалентна записи (mass + k), а сам k-й элемент массива можно определить как mass[k] или как *(mass + k). С другой стороны, если ptr – указатель, то в выражениях его можно использовать с индексом, то есть запись ptr[k] эквивалентна записи *(ptr + k).

Таким образом, элемент массива в Си разрешается изображать и в виде указателя со смещением, и в виде имени массива с индексом.

Между именем массива и указателем, выступающим в роли имени массива, однако существует различие. Указатель – это переменная, поэтому можно записать ptr = mass или ptr++. Но имя массива не является переменной, и записи типа mass = ptr или mass++ не допускаются.

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

- складывать и вычитать указатели и целые данные,

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

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

- присваивать указателю нуль и сравнивать его с нулем.

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

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

- складывать указатели со значениями типа float и double,

- присваивать указателю одного типа значение указателя другого типа (исключение составляют указатели типа void).

Указатели можно использовать и при работе с многомерными массивами:

int trio[5][2][3];

int *i_ptr;

Описан трехмерный массив trio целого типа и указатель ptr на данные целого типа. Присвоим этому указателю значение базового адреса массива:

i_ptr=&trio[0][0][0];

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

i_ptr=trio;

как это имеет место для векторов (одномерных массивов).

Доступ к j-му элементу i-й строки k-го слоя массива trio может быть осуществлен либо с помощью индексов:

trio[k][i][j]=1;

либо с помощью указателей:

*(i_ptr + k*(2*3) + i*3 + j)=1;

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

float r[2][2], s[2][2];

r = s; // ошибка!

Эти ограничения можно обойти с помощью указателя:

float *f_ptr;

f_ptr = &s[0][0];

r = *f_ptr;

При этом элементам массива r будут присвоены значения соответствующих элементов массива s.

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

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

float func(int x, int y); // объявление функции func,

// возвращающей вещественное значение

float (*f_ptr)(); // описание указателя f_ptr

// на любую функцию, возвращающую

// вещественное значение

f_ptr = func; // в указателе – адрес функции func

r = func(a,b); // вызов функции func по ееимени

r = (*f_ptr)(a, b); // вызов функции func по ееадресу

В последнем случае переменной r будет присвоено значение функции, имеющей адрес f_ptr.

При работе с указателями на функции имена этих указателей обязательно заключаются в скобки. Описание

float *f_ptr();

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

Аналогично:

r

Наши рекомендации