6 - Томский политехнический университет

реклама
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное бюджетное образовательное учреждение высшего
профессионального образования
«НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ ТОМСКИЙ
ПОЛИТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ»
Учебно-методические указания к
лабораторной работе №7 по курсу Информатика
МАТРИЧНЫЕ ИСЧИСЛЕНИЯ С ИСПОЛЬЗОВАНИЕМ ФУНКЦИЙ
ПОЛЬЗОВАТЕЛЯ
Томск 2011 г.
СОДЕРЖАНИЕ
1
Понятие функции
4
2
Определение пользовательской функции
5
3
Передача значений в функцию: фактические и формальные параметры
7
4
Локальные и глобальные переменные
8
5
Возврат результатов из функции
9
6
Адреса и указатели
9
7
Размещение функций в файле
11
8
Массивы как параметры функций
12
9
Двумерный массив как параметр функции
12
10
Краткие выводы
14
2
ВВЕДЕНИЕ
Данные
учебно-методические указания
содержат
теоретический
материал
необходимый для выполнения лабораторной работы №7 «Матричные исчисления с
использованием функций пользователя» по курсу Информатика для бакалавров
направления 140800 «Ядерные физика и технологии». В частности, рассматриваются
понятие функции в языке СИ, операторы описания, определения и вызова функций,
возвращение значений из функции, адреса и указатели, правила размещения функций в
файле.
3
1 Понятие функции
Принципы программирования на языке Си основаны на понятии функции. В
выполненных ранее программах уже использовались некоторые функции: printf( ),
scanf( ), fopen( ), встроенные математические функции (например, sin( ) ) и т.д. Эти
функции являются системными, однако было создано и несколько собственных
функций под общим именем main( ). Выполнение программы всегда начинается с
команд, содержащихся в функции main( ), затем последняя вызывает другие функции,
например printf( ). В процессе выполнения данной работы необходимо научиться
создавать свои собственные функции и делать их доступными для функции main(), а
также друг для друга.
Функция – самостоятельная единица программы, спроектированная для
реализации конкретной задачи. Функции в языке Си играют ту же роль, какую играют
функции, подпрограммы и процедуры в других языках. Вызов функции приводит к
выполнению некоторых действий. Например, при обращении к функции printf()
осуществляется вывод данных на экран. Другие же функции позволяют получать
некоторую величину, используемую затем в программе. В общем функции могут
выполнять действия и получать значения величин, используемых в программе.
Для чего надо пользоваться функциями? Во-первых, они избавляют нас от
повторного программирования. Если конкретную задачу необходимо выполнить в
программе несколько раз, можно написать соответствующую функцию только один
раз, а затем вызывать её всегда, когда это требуется. Во-вторых, можно применять одну
функцию в различных программах, просто включая файл с её текстом с помощью
команды препроцессора #include (например, #include "my_f.cpp") в создаваемый файл
или другим способом (например, используя такое средство, как "проект" Borland C++).
Даже в том случае, если некоторая задача выполняется только в одно й программе,
лучше оформить её решение в виде функции, поскольку функции повышают
модульность программы и, следовательно, облегчают её чтение, внесение изменений и
коррекцию ошибок.
Например, необходимо последовательно организовать ввод и поиск наибольшего
значения в трех разных векторах, при этом собственно алгоритм поиска наибольшего
значения используется один и тот же. Тогда можно соответствующую программу
записать так:
4
void main(void)
{
double a[10], b[20], c[30], maxa, maxb, maxc;
readvektor(a, 10, ’A’);
maxa = poiskmax(a, 10);
readvektor(b, 20, ’B’);
maxb = poiskmax(b, 20);
readvektor(c, 30, ’C’);
maxc = poiskmax(c, 30);
}
Разумеется, при этом надо не забыть написать программную реализацию и самих
функций readvektor()и poiskmax().
Используя смысловые имена функций, можно четко определить, что программа
делает и как она организована. После этого можно заниматься каждой функцией
отдельно и совершенствовать её до тех пор, пока она не будет правильно выполнять
требуемую задачу. Дополнительное преимущество указанного подхода заключается в
том, что если создана функция достаточно общего вида, то её можно использовать и в
других программах.
На начальной стадии разработки можно считать функцию "чёрным ящиком",
которому необходимо задать поступающую информацию (вход) и полученные
результаты (выход). Все, что происходит внутри черного ящика, не касается
разработчика до тех пор, пока не нужно писать программу, реализующую эту функцию.
например, при использовании библиотечных функций необходимо иметь информацию
о входных и выходных параметрах, а о конкретной реализации такой функции речь
вообще не идёт.
2 Определение пользовательской функции
Типичное определение функции имеет следующий вид:
тип имя (список аргументов)
// – заголовок функции
{
тело функции
}
5
Наличие списка аргументов не является обязательным, однако при его наличии
каждый аргумент должен быть описан соответствующим образом. Тип функции
определяется типом возвращаемого ею значения. Не обязательно описывать тип
функции, если она возвращает результат целого типа; если функция ничего не
возвращает с помощью оператора return, то желательно описать её тип как void; по
умолчанию тип функции принимается как int. Переменные, отличные от аргументов,
описываются внутри тела функции, которое заключается в фигурные скобки.
При написании своей функции следуйте тем же правилам, что и при написании
main(): вначале указывается заголовок функции, затем идёт открывающая фигурная
скобка,
приводится
описание
используемых
переменных,
даются
операторы,
определяющие работу функции, и, наконец, закрывающая фигурная скобка. Обратите
внимание, что за заголовком функции не следует символ "точка с запятой"; его
отсутствие служит указанием компилятору, что здесь определяется функция, а не
используется.
Пример 1 (к программе, приведенной выше):
void readvektor(double x[], int n, char nam)
{
int i;
printf (”Введите %d элементов массива %c:\n”, n, nam);
for ( i=0; i<n; i++ )
{
scanf (”%lf”,&x[i]);
}
}
/*
сама функция вызывалась в main():
readvektor(a, 10, ’A’);
readvektor(b, 20, ’B’);
readvektor(c, 30, ’C’);
*/
Данная функция предназначена для ввода данных в одномерный массив. Здесь
выводится приглашение для ввода некоторого количества элементов массива (это
число передается сюда при вызове), имя которого также в виде символа передается из
внешней функции. Затем организован собственно ввод с клавиатуры элементов
6
массива. Так как размер массива заранее неизвестен, то конкретная размерность не
указывается (для одномерных массивов!). Выходным параметром служит сам массив.
Пример 2 (к программе, приведенной выше):
double poiskmax(double y[], int m)
{
double max;
int i;
for (max = y[0], i=1; i < m; i++) if (max < y[i]) max = y[i];
return max;
}
Результатом выполнения данной функции является найденное в одномерном
массиве наибольшее значение, которое присваивается переменной max. Ключевое
слово return указывает на то, что значение выражения, стоящего после него, будет
присвоено функции, содержащей это ключевое слово.
3 Передача значений в функцию: фактические и формальные параметры
Аргументы, передаваемые в функцию при вызове, называются фактическими
параметрами, а аргументы, перечисленные в заголовке функции при её описании,
называются формальными параметрами.
Так, в функции main(), приведенной выше, организовано 3 вызова функции
readvektor(), каждый раз со своими фактическими параметрами. При этом каждый раз
при выполнении функции вместо массива x будет “подставлен” соответствующий
фактический массив, переменная n примет значение, равное значению второго по счету
фактического параметра, а в символьную переменную nam "подставлено" значение,
приведенное в качестве третьего по счету параметра.
ВНИМАНИЕ!
Формальные
и
фактические
параметры
должны
согласовываться по количеству, порядку следования и типу!!!
Естественно, в списке формальных параметров не могут присутствовать
константы.
7
4 Локальные и глобальные переменные
Переменные, объявленные в функции, являются её внутренними переменными и
"не известны" вызывающей функции. Аналогично переменные, объявленные в
вызывающей функции, не известны внутри других функций. Переменные, известные
только одной функции, а именно той, которая их содержит, называются локальными
переменными; они действуют только в пределах этой функции. В случае, когда в двух
или больше функциях встречаются одинаковые имена переменных, они будут
соответствовать совершенно различным ячейкам памяти.
Кроме этого, в языке Си допускается наличие переменных, известных нескольким
функциям. Такие переменные называются глобальными. Переменная является
глобальной, если она описана вне тела какой-либо функции. Обычно глобальные
переменные описываются в самом начале, сразу после команд препроцессора.
ВНИМАНИЕ! Значение глобальной переменной можно изменить внутри любой
функции, поэтому с ними надо обращаться осторожно и не злоупотреблять их
использованием.
Пример:
#include <stdio.h>
float a[10][20];
//массив описан как глобальный
void my_func()
{
float b[10];
//массив описан как локальный
b[0] = 0;
a[0][0] = 0;
...
}
void main ()
{
my_fuc();
printf("a00 = %f\n",a[0][0]);
//правильно, т.к. используется
//элемент массива а, описанного глобально
printf("b0 = %f\n",b[0]); //неправильно, т.к. массив b здесь просто
// "неизвестен" (он известен только внутри функции my_funk()!)
}
8
5 Возврат результатов из функции
В том случае, когда результатом выполнения функции является одна величина
простого типа, для возврата этого результата можно воспользоваться оператором return
(см. Пример 2 из п. 9.2.2). В этом случае функция должны иметь тот же тип, что и
значение, которое она возвращают в качестве результата. Его структура выглядит
следующим образом:
return выражение;
где в качестве выражения может выступать как отдельная переменная, так и
выражение (арифметическое или логическое), которое сначала будет вычислено, а
затем этот результат будет возвращен в вызывающую функцию.
Примеры:
return x;
return a[0]+a[1]*x;
return z < w;
Таким образом, с помощью оператора return в вызывающую функцию можно
передать только одну величину.
Так как в языке Си при обращении к функции через параметры передаются
конкретные значения соответствующих аргументов, то вернуть полученные значение
через аргументы, перечисленные в заголовке функции, просто так не удастся.
Если в вызывающую функцию требуется передать (вернуть) более чем одно
значение, необходимо в качестве аргументов использовать указатели.
6 Адреса и указатели
Вообще говоря, указатель – это некоторое символическое представление адреса
переменной. В том случае, если переменная описана, например, как p, то
использование в программе p означает использование значения переменной
(например, 3.14), а &p означает "указатель на переменную p", т.е. это – "адрес
переменной p". Фактический адрес – это число (например, 32550). Тогда выполнение
оператора p = 2.5; можно "перевести" как фразу "разместить число 2.5 по адресу 32550".
9
Адрес ячейки, отводимой переменной, в процессе выполнения программы не
меняется, поэтому символическое представление адреса &p является константой типа
указатель (вспомните параметры функции scanf()!).
В языке Си имеются и переменные типа указатель. Точно так же, как значением
переменной типа float является вещественное число, значением переменной типа
указатель служит адрес некоторой величины.
При описании переменной как указателя необходимо задать, на переменную
какого типа ссылается данный указатель:
тип *имя;
Символ звездочка (*) определяет саму переменную как указатель (вспомните
описание переменной, связанной с файлом).
Например:
float *p;
int *k;
Тогда p – это указатель (его величина – это адрес), а *p – величина типа float.,
которую можно использовать, например, в арифметических выражениях:
*p = 3.1;
float a = *p + 1;
Так как значение адреса переменной не изменяется, то если в качестве
аргумента передать функции адрес переменной, то внутри этой функции можно
изменить значение этой переменной, и это новое значение будет известно после
выполнения и в вызывающей функции.
Пример:
void obmen(int* a, int* b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void main()
{
int x, y;
x = 2;
10
y = 3;
printf("здесь x = %d, а y = %d\n", x, y);
obmen( &x, &y );
printf("теперь x = %d, а y = %d\n", x, y);
}
В приведенном примере функции obmen() передаются в качестве аргументов
адреса локальных переменных x и y, а сама функция obmen() занимается обменом
значений, расположенных по этим адресам. Указатели позволяют обойти тот факт, что
что переменные функции obmen() являются локальными. Они дают возможность этой
функции "добраться" до функции main() и изменить величины описанных в ней
объектов.
7 Размещение функций в файле
1) Каждая функция описывается ОТДЕЛЬНО, это отдельные "строительные
блоки".
2) В принципе все функции равноправны, т.е. из любой функции можно вызвать
любую другую. Но не надо забывать при этом, что функция main() выполняется первой,
и обращаться к ней из других функций без веских причин, мягко говоря, не стоит.
3) Описана функция (её тип и типы аргументов) должна быть ДО первого (от
начала файла) её использования. Здесь возможно 2 варианта:
a)
привести описание самой функции ДО той функции, из которой она
вызывается;
b) привести описание прототипа функции ДО вызова – в этом случае описание
прототипа может быть как локальным (внутри вызывающей функции), так и
глобальным (например, до описаний всех функций). Под прототипом здесь понимается
описание типа функции, её имени и типов всех аргументов (имена аргументов можно
не приводить). В этом случае сама функция может быть описана в любом месте файла.
Не забудьте, что описание прототипа функции – это отдельный оператор, который
должен заканчиваться соответствующим символом.
Оба варианта эквивалентны.
Пример:
Вариант а)
Вариант b)
int pr (float a, int* b)
int pr (float, int*);
11
{
... тело функции
void main ()
}
{...
a = pr (bb, &cc);
void main ()
}
{...
a = pr (bb, &cc);
int pr (float a, int* b)
}
{
... тело функции
}
8 Массивы как параметры функций
Имя массива является указателем на первый элемент массива. Поэтому при
вызове функций readvektor()и poiskmax() в примере п.9.2.1 в качестве первого
фактического параметра передавалось на самом деле не имя соответствующего
массива, а адрес его первого элемента. Соответственно в описании этих функций
использовалась форма тип имя_массива[], что является эквивалентом оператору тип
*имя. То есть можно было, например, заголовок функции написать следующим
образом:
double poiskmax(double *y, int m);
а не
double poiskmax(double y[], int m);
просто во втором случае описание y[] лишний раз напоминает о том, что
указатель y ссылается на массив. Все остальные действия с массивом y в функции
poiskmax() остаются без изменений, в частности, обращение к элементам массива.
Более того, вообще говоря, вместо y[i] можно использовать *(y+i).
9 Двумерный массив как параметр функции
Двумерный массив можно рассматривать как массив указателей или указатель на
указатель.
Одним из способов избежать неприятностей при необходимости работать с
двумерными массивами как аргументами функции является явное указание, как
12
минимум, величины второй размерности (т.е. количества столбцов) при объявлении
функции:
void vv (int x[][10], int m, int n)
{...
.. x[i][j] .. // использование элемента массива x[i][j]
}
void main ()
{
int a[20][10], k1=5, k2=4;
vv (a, k1, k2);
...
}
Таким образом, компилятор "поймет", что массив следует разбить на строки по 10
столбцов. Или, если описать формальный параметр как одномерный массив,
необходимо будет обращаться к каждому элементу массива, предварительно вычисляя
его адрес, например:
void vv (int x[], int m, int n)
{...
.. x[i*n+j] .. // использование элемента массива x[i][j]
...
}
...
vv (a[0], k, 10); /* обращение к функции; здесь в качестве первого аргумента
передаём адрес первой строки; количество столбцов должно соответствовать заданному
в описании массива!!! */
...
Это связано с тем, что в памяти массив располагается, занимая последовательные
ячейки памяти. Так, массив z[2][3] располагается в памяти следующим образом:
z[0][0] z[0][1] z[0][2] z[1][0] z[1][1] z[1][2]
Тогда z[0]&z[0][0], z[1]&z[1][0] и т.д. Вычислить адрес z[1][0] можно,
добавив к адресу самого первого элемента массива количество столбцов из описания:
z+3.
Пример:
# include <stdio.h>
13
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; /* массив, описанный глобально, можно
инициализировать таким способом */
void vv (int x[], int m, int k, int n)
{int i, j;
for (i=0; i<m; i++)
{
for (j=0; j<k; j++)
printf ("x[%d][%d]=%d ",i,j,x[i*n+j]); //или *(x+i*n+j)
printf("\n");
}
}
void main ()
{
int nn = 2, kk = 3;
vv (a[0], nn, kk, 4);
}
10 Краткие выводы
ИТАК. Для создания больших программ используйте функции в качестве
"строительных
блоков"!!!
Каждая
функция
должна
выполнять
одну
вполне
определенную задачу. Используйте аргументы для передачи значений функции и
ключевое слово return для передачи результирующего значения в вызывающую
программу. Если возвращаемое функцией значение не принадлежит типу int, вы
должны указать тип функции в ее определении и в разделе описаний вызывающей
программы. Если вы хотите, чтобы при выполнении функции происходило изменение
значений переменных в вызывающей программе, вы должны пользоваться адресами и
указателями.
14
Скачать