Теоретическая часть



Принципы программирования на языке С основаны на понятии функции. Например, к системным функциям относятся printf(), scanf(), gets(), putchar() и др. Функции – это строительные элементы языка С и то место, в котором выполняется вся работа программы.

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

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

Ни одна программа в языке С++ не может обойтись без функций.

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

Рассмотрим модельный пример программы, в которой, кроме функции main(), содержатся еще три функции.

 

#include <stdio.h>int main(void) /* Главная функция */{ /* Начало тела функции */function1(); /* вызов первой функции */function2(); /* вызов второй функции */function3(); /* вызов третьей функции */} /* Конец тела функции main() */ /* Начало определения первой функции */function1() { /* Начало тела первой функции *//* Операторы первой функции *//* Конец тела первой функции */} /* Начало определения второй функции */function2(){ /* Начало тела второй функции*//* Операторы второй функции *//* Конец тела второй функции*/} /* Начало определения третьей функции */function3(){ /* Начало тела третьей функции*//* Операторы третьей функции *//* Конец тела третьей функции*/}

 

В условной (модельной) программе имеются четыре функции: main(), function1(), function2(), function3(). Эти функции не имеют аргументов. Позднее рассмотрим функции, которые имеют аргументы. Аргументы функции – это величины, которые передаются функции во время ее вызова. Аргумент, стоящий в операторе вызова функции, называется фактическим параметром. Аргументы, стоящие в заголовке функции, называются формальными параметрами. В языке С++ функция может возвращать значение в вызывающую программу посредством оператора return. Оператор возврата из функции в точку вызова имеет две формы:

return;return выражение;

В общем виде функция выглядит следующим образом:

возвр-тип имя-функции(список параметров){Тело_функции}

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

Возвращаемый тип возвр-тип функции определяет тип данного, возвращаемого функцией. Например, это могут быть int, float, double и т.д. В случае, когда функция ничего не возвращает, ей присваивается тип void.

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

fun(тип имя_перем1, тип имя_перем2,..., тип имя_перем N)

Например:

fun(int i, int j, float k, char str1, char str2)

 

Рассмотрим пример программы с выводом сообщения не в главной функции main(), а в другой:

 

#include <stdio.h>#include <conio.h> void printMessage (void){printf("\n\t hello, world\n");return;printf("\n\t 123\n");} int main(void){printMessage(); printf("\n Press any key: ");_getch();return 0;}

Результат выполнения программы показан на рис. 7.1.

 


Рис. 7.1. Вывод сообщения с помощью двух функций

 

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

printf("\n\t hello, world\n");

Несмотря на то, что в функции printMessage() есть еще одно утверждение printf("\n\t 123\n"), которое не выполняется, поскольку используется утверждение возврата (return) из функции.

В языке С функция введена как один из производных типов.

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

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

тип имя_функции(тип имя_парам1, тип имя_парам2,..., тип им_парамN);

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

Вышеприведенная программа с использованием прототипа функции printMessage() будет выглядеть следующим образом:

 

#include <stdio.h>#include <conio.h> //void printMessage (void);//Прототип функции int main(void) { void printMessage (void); //Прототип функции printMessage(); // Вызов функции printf("\n Press any key: ");_getch();return 0; } // Определение функцииvoid printMessage (void){printf("\n\t hello, world\n");return;printf("\n\t 123\n"); }

В листинге программы показаны две возможности использования прототипа функции printMessage(). При этом, сама функция printMessage() объявлена после функции main().

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

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

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

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

1. При компиляции функции выделяются участки памяти для формальных параметров, т.е. формальные параметры оказываются внутренними объектами функции. При этом для параметров типа float формируются объекты типа double, а для параметров типов char и short int создаются объекты типа int. Если параметром является массив, то формируется указатель на начало этого массива, и он служит представлением массива-параметра в теле функции.

2. Вычисляются значения выражений, использованных в качесстве фактических параметров при вызове функции.

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

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

5. Никакого влияния на фактические параметры (на их значения) функция не оказывает.

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

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

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

float fun(int n, float A[ ], float B[ ]);float fun(int n, float *a, float *b);

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

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

int fun(int n, ј);

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

Макросы и определения заголовочного файла переменных аргументов stdarg.h (табл. 7.1) предоставляют программисту средства, необходимые для построения функций со списком аргументов переменной длины.

 

Таблица 7.1. Макросы заголовочного файла stdarg.h

 

Идентификатор Объяснение
va_list Тип, предназначающийся для хранения информации, необходимой макросам v_start, va_arg и va_end. Чтобы получить доступ к аргументам в списке переменной длины, необходимо объявить объект типа va_list
va_start Макрос, который вызывается перед обращением к аргументам списка переменной длины. Он инициализирует объект, объявленный с помощью va_list, для использования макросами va_arg и va_end
va_arg Макрос, расширяющийся до выражения со значением и типом следующего аргументов списке переменной длины. Каждый вызов его изменяет объект, объявленный с помощью va_list так, что объект указаывает на следующий аргумент списка
va_end Макрос обеспечивает нормальный возврат из функции, на список аргументов которой ссылается макрос va_start

 

Примеры обращений к функции с фактическими аргументами:

 

double k;double v1 = 1.5, v2 = 2.5, v3 = 3.5;// Первый вариант, где 3 – количество аргументов k = fun(3,v1, v2, v3);// Второй вариант, где 0.0 – завершающий нуль списка аргументов k = fun(v1, v2, v3, 0.0);

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

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

Общая форма определения функции, которая возвращает указатель, следующая:

тип *имя_функции (аргументы функции) {// тело функции тип *имя_указателя;? return имя_указателя;}

Рассмотрим пример, в котором осуществляется сложение двух одномерных массивов и результат возвращается через указатель.

 

Программный код решения примера:

 

#include <stdio.h>#include <conio.h>#include <stdlib.h>int *out2(int A[], int B[], int); int main (void) { int i, n; int A[] = {1,2,3,4,5}; int B[] = {2,2,2,2,2}; int *ptrAB = NULL; n = (sizeof(A)/sizeof(A[0])); puts("\n The initial arrays: "); for (i = 0; i < n; i++) printf(" %d", A[i]); puts(""); for (i = 0; i < n; i++) printf(" %d", B[i]); ptrAB = out2(A, B, n); puts("\n\n Result from function: "); for (i = 0; i < n; i++) printf(" %d", ptrAB[i]); puts("\n\n Control of the arrays: "); for (i = 0; i < n; i++) printf(" %d", A[i]); puts(""); for (i = 0; i < n; i++) printf(" %d", B[i]); free(ptrAB); // освобождение выделенной памяти printf("\n\n... Press any key: "); _getch(); return 0;} int *out2(int A[], int B[], int n){ int i;int *ptr = (int *)calloc(n, sizeof(int)); //выделение памяти for (i = 0; i < n; i++) ptr[i] = A[i] + B[i]; return ptr;}

Программа не требует особых пояснений.

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

Указатели возвращаются подобно значениям любых других типов данных. Чтобы вернуть указатель, функция должна объявить его тип в качестве типа возвращаемого значения. Таким образом, если функция возвращает указатель, то значение, используемое в ее инструкции return, также должно быть указателем. В частности, многие библиотечные функции, предназначенные для обработки строк, возвращают указатели на символы.

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

Указатель на функцию – это переменная, содержащая адрес в памяти, по которому расположена функция. Имя функции – это адрес начала программного кода функции. Указатели на функции могут быть переданы функциям в качестве аргументов, могут возвращаться функциями, сохраняться в массивах и присваиваться другим указателям на функции.

 

Типичное определение указателя на функцию следующее:

 

тип_возвращаемый_функцией(*имя_указателя_на_функцию)(аргументы);

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

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

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

Практическая часть

Пример 1. Напишите программу сортировки по возрастанию заданного массива случайных чисел, равномерно распределенных в интервале [–6;6], с помощью вспомогательной функции.

 

Программный код решения примера:

 

#include <stdio.h>#include <conio.h>#include <stdlib.h>#include <time.h>#define MAX 10 // Прототип функции с формальными параметрамиvoid sort(double arr[], int n); int main (void) { double M[MAX]; int i, size = MAX; long int L; unsigned int some; L = (long) time(NULL); srand((unsigned)L); for (i = 0; i < MAX; ++i) M[i] = 12.0*rand()/RAND_MAX - 6.0; printf("\n\t The original array:\n"); for (i = 0; i < MAX; ++i) printf("\t%8.4f\n", M[i]); // Обращение к функции с фактическими параметрами sort(M, size);// Распечатка отсортированного массива printf("\n\t After sorting: \n");for (i = 0; i < MAX; ++i) printf("\t%8.4f\n", M[i]); printf("\n Press any key: "); _getch(); return 0; }// Вспомогательная функция сортировки void sort(double Array[], int m) {int i, j;double tmp;for (i = 0; i < m-1; ++i) for (j = 0; j < m-i-1; ++j)if (Array[j+1] < Array[j]) {tmp = Array[j];Array[j] = Array[j+1];Array[j+1] = tmp;}}

 

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

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

Возможный результат выполнения программы показан на рис. 7.2.

 


Рис. 7.2. Пример сортировки числового массива

 

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

#include <stdio.h>#include <conio.h> // Прототип функцииvoid swap(int*, int*); int main (void) { int a = 10, b = -20; // Вывод на консоль исходных значений переменных printf("\n Initial values:\n a = %d, b = %d\n", a, b); // Вызов функции swap() с фактическими параметрами swap(&a, &b); // Результат после обращения функции swap() printf("\n New values:\n a = %d, b = %d\n", a, b); printf("\n... Press any key: "); _getch(); return 0;} // Определение функцииvoid swap(int *pa, int *pb){int temp;temp = *pa;*pa = *pb;*pb = temp;}

 

В программе в качестве фактических параметров функции swap() выступают адреса заданных переменных. Можно было в главной функции определить указатели и инициализировать их адресами заданных переменных, а потом передать эти указатели в функцию swap.

Результат выполнения программы показан нa рис. 7.3.

 


Рис. 7.3. Результат обмена данными, выполненного функцией swap()

Пример 4. Напишите программу с функцией пузырьковой сортировки, использующей вызов по ссылке.

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

 

Программный код решения примера:

 

#include <stdio.h>#include <conio.h> // Прототип функцииvoid bsort (int* const, const int); int main (void) { int A[] = {56, 34, 2, 0, 1, -21, 6, 8, 7}; int i, n; //Размерность массива n = sizeof(A)/sizeof(A[0]); puts("\n Data items in original order:"); for (i = 0; i < n; i++) printf(" %3d", A[i]); // Вызов функции сортировки - bsort() bsort (A, n); puts("\n\n Data items in ascending order:"); for (i = 0; i < n; i++) printf(" %3d", A[i]); printf("\n\n... Press any key: "); _getch(); return 0;}// Определение функцииvoid swap(int *pa, int *pb){int temp;temp = *pa;*pa = *pb;*pb = temp;} void bsort (int *const arr, const int size){int pass, //счетчик проходов j; // счетчик сравнений// Прототип функции обмена - swap()void swap (int*, int*); // Цикл для контроля проходовfor (pass = 0; pass < size - 1; pass++) { // цикл для контроля сравнений на данном проходе for (j = 0; j < size - 1; j++) { // обмен значений при нарушении порядка возрастания if (arr[j] > arr[j + 1]) { swap(&arr[j], &arr[j+1]); } }}}

 

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

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

Пример выполнения программы показан на рис. 7.4.

 


Рис. 7.4. Пример сортировки массива методом пузырька

 

Пример 5. Напишите программу построения на экране дисплея графика следующей функции:

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

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

 

Программный код решения примера:

 

// Заголовочные файлы#include <stdio.h>#include <conio.h>#include <stdlib.h>#include <math.h> // Размеры диаграммы по ширине и высоте экрана#define SCREENW 79#define SCREENH 25 // Функция построения графика заданной функцииvoid plot (FILE *fout, double a, double b, double (*f) (double)){// Формальные параметры функции plot// FILE *fout – указатель на поток вывода // double a – левая граница оси абсцисс// double b – правая граница оси абсцисс// double (*f) (double) – указатель на функцию char screen[SCREENW][SCREENH]; double x, y[SCREENW]; double ymin = 0, ymax = 0; double hx, hy; int i, j; int xz, yz;// hx – шаг по оси абсцисс hx = (b - a) / (SCREENW - 1); for (i = 0, x = a; i < SCREENW; ++i, x += hx) {// вычисляем значение функции y[i] = f (x);// запоминаем минимальное и максимальное значения if (y[i] < ymin) ymin = y[i]; if (y[i] > ymax) ymax = y[i]; } hy = (ymax - ymin) / (SCREENH - 1); yz = (int)floor (ymax / hy + 0.5); xz = (int)floor (-a / hx + 0.5); // рисование осей координат for (j = 0; j < SCREENH; ++j) { for (i = 0; i < SCREENW; ++i) { if (j == yz && i == xz) screen[i][j] = '+'; else if (j == yz) screen[i][j] = '-'; else if (i == xz) screen[i][j] = '|'; else screen[i][j] = ' '; } } // рисование графика функции for (i = 0; i < SCREENW; ++i) { j = (int)floor ((ymax - y[i]) / hy + 0.5); screen[i][j] = '.'; // символ начертания графика } // вывод результата в файл или в стандартный поток stdout for (j = 0; j < SCREENH; ++j) { for (i = 0; i < SCREENW; ++i) fputc (screen[i][j], fout); fprintf (fout, "\n"); }} // Заданная функцияdouble f (double x){ return sin (3.0*x) * exp (-x / 3.0);} int main (void) {// Вывод графика в стандартный поток (консоль) plot (stdout, 0.0, 10.0, f); printf("\n\n … Press any key: "); _getch(); return 0;}

В программе используется указатель на файл, который может быть стандартным потоком, т. е. экран дисплея. В главной функции main() происходит обращение к функции рисования графика plot(), в которую вводят фактические параметры, в частности файл – это stdout, т. е. стандартный поток, 0.0 – это левая граница оси абсцисс, 10.0 – правая граница оси абсцисс, f – имя функции с описанием зависимости y = f(x).

Пример выполнения программы показан на рис. 7.5.

 


Рис. 7.5. Пример построения графика функции на консоли


Дата добавления: 2015-12-20; просмотров: 20; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!