Программирование на языке C в Microsoft Visual Studio 2010

Указатели и функции в языке программирования С

Разбить на страницы
Показывать лекцию целиком

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

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

Ранее было отмечено, что в языке С аргументы передаются в функции по значению и не существует прямого способа изменить переменную вызывающей функции, действуя внутри вызываемой функции. Благодаря аргументам-указателям функция может обращаться к объектам в вызвавшей ее функции, в том числе модифицировать их [11.1]. В качестве примера рассмотрим функцию 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 рис 11.1.

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

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

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

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

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

тип *имя_указателя;
?

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, также должно быть указателем. В частности, многие библиотечные функции, предназначенные для обработки строк, возвращают указатели на символы.

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

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

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

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

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

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

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

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

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

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

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

#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().

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

(рис 11.2) Пример сортировки массива методом пузырька

Задание 1

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

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

    Среднее выборочное значение одномерного массива размерностью N

    $$mean=\frac 1 N \sum^N_{i=1}ai,$$

    где – элементы массива.

    Исправленная выборочная дисперсия одномерного массива размерностью N

    $$D=\frac 1 {N-1} \sum^N_{i=1}(a_i-mean)^2,$$

    где mean – среднее значение данного массива

    Исправленное среднеквадратичное отклонение S

    $$S=+\sqrt D,$$

    где D – дисперсия данного массива.

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

    #include <stdio.h>
    #include <conio.h>
    #include <stdlib.h>
    #include <math.h>
    
    // Прототип функции
    double *mean_D_S(int arr[], int n);
    
    int main(void)
    {
    	int mass[] = {2, -3, 5, 6, 7, 8, 9,-1};
    	int i, n;
    	double *R;
    	n = sizeof(mass)/sizeof(mass[0]);
    	puts("\n\t Initial array:");
    	for (i = 0; i < n; i++)
    		printf(" %3d", mass[i]);
    
         // Вызов функции 
         R = mean_D_S(mass, n);
    
    // Вывод расчтных характеристик массива
    printf("\n\n The average value of an array: %g\n", R[0]);
    printf(" The dispersion of the array: %g\n", R[1]);
    printf(" The standard deviation of the array: %g\n", R[2]);
    
    // Освобождение памяти
         free(R);
    
    	printf("\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определение функции
    double *mean_D_S(int arr[], int N)
    {
    int j;
    double *PTR3 = (double *)calloc(N, sizeof(double));
    double mean, D, S;
    
    mean = 0.0;
    for (j = 0; j < N; j++)
    mean += arr[j];
    
    mean /= N;
    
    D = 0.0;
    for (j = 0; j < N; j++)
    D += (arr[j] - mean)*(arr[j] - mean);
    
    D /= (N-1);
    
    S = sqrt(D);
    
    PTR3[0] = mean;
    PTR3[1] = D;
    PTR3[2] = S;
    
    return PTR3;
    
    }

    В программе используется функция динамического распределения памяти calloc(), которая выделенные ячейки памяти обнуляет. Расчетные характеристики одномерного массива размещаются последовательно друг за другом в выделенной памяти для указателя *PTR3. Сформированный указатель функция возвращает в точку вызова функции mean_D_S().

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

    (рис 11.3) Расчет статистических характеристик числового массива

    Задание 2

  • В качестве первого аргумента функции mean_D_S() используйте указатель на числовой массив.
  • Воспользуйтесь справкой по математическим функциям и в программе примените функцию, которая осуществляет возведение в степень.
  • Дополните возврат функцией mean_D_S() еще исходного массива, поэлементно возведенного в квадрат. В главной функции main() результаты выведите на консоль.
  • Вместо типа double используйте тип float.
  • Пример 3. Напишите программу с указателем на функции, которые рассчитывают следующие статистические характеристики одномерного числового массива: среднее арифметическое значение, медиану и модус (моду).

    Среднее арифметическое рассчитывалось в предыдущем примере.

    Приведем определения медианы и модуса, взятые из книги [11.3], из которой также взяты основные фрагменты программных кодов (в книге программы написаны на С++).

    Медиана – это серединное значение в наборе данных – т. е. такое, что ровно половина значений располагается выше, и ровно половина ниже его.

    Модус – это значение, наиболее часто встречающееся в наборе данных.

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

    #include <stdio.h>
    #include <conio.h>
    
    double mean(int*, int);
    double median(int*, int);
    double mode(int*, int);
    
    int main(void) {
    	int mass[] = {5, 6, 5, 5, 3,
    	              6, 5, 3, 1, 4,
    	              5, 3, 1, 6, 5, 
    	              2, 5, 2, 3, 4}; 
    	int *ptr = mass;
    	int temp, i, j, k, n;
    // Указатель на функции
    	double (*fun[3])(int*, int) = {mean, median, mode};
    	
         n = sizeof(mass)/sizeof(mass[0]);
    	puts("\n The original array:");
    	j = 0;
    	for (i = 0; i < n; i++) {
    		j++;
    		
    		if ( j%6 )
    			printf(" %2d", mass[i]);
    		else
    		{puts(""); printf(" %2d", mass[i]); j = 1;}
    	}
    
    // Сортировка методом выбора
    for (i = 0; i < (n - 1); ++i) {
     temp = ptr[i]; k = i;
    for (j = i + 1; j < n; ++j)
    if (ptr[j] < temp) { k = j; temp = ptr[k]; }
    ptr[k] = ptr[i]; ptr[i] = temp;
    }
    	puts("\n\n Results - mean, mediana, modus: ");
    for (i = 0; i < 3; i++)
    printf("%6g\n",(*fun[i])(ptr, n));
    
    	printf("\n\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определения функций
    double mean(int* arr, int N)
    {
    int i;
    double aver = 0.0;
    
    for (i = 0; i < N; i++)
    aver += arr[i];
    
    return (aver/N);
    
    }
    
    double median(int* arr, int N)
    {
    int i;
    double med = 0.0;
    
    for (i = 0; (i < N/2); i++)
    med = arr[i];
    if ( N % 2)
    med = arr[i];
    
    else
    med = (med + arr[i])/2;
    return med;
    }
    
    double mode(int* arr, int N)
    {
    int instances = 0,
        tempinst = 1,
               i = 1;
    double tempmode,
           mode_return = 1.0;
    tempmode = (double)arr[0];
    
    while (i < N) 
    {
       while ((double)arr[i] == tempmode )
       {
       i++;
       tempinst++;
       }
       if (tempinst > instances)
       {
       mode_return = tempmode;
       instances = tempinst;
       }
       tempinst = 1;
       tempmode = (double)arr[i];
       i++;
    }
    
    return (mode_return);
    }

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

    Для понимания работы функции по расчету модуса алгоритм вычислений рекомендуется в [11.3] формулировать следующим образом: "Разбить сортированный список значений на ряд меньших списков, каждый из которых содержит одинаковые значения. Пересчитать число элементов в этих списках, и список с наибольшим числом элементов будет соответствовать модусу данных".

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

    (рис 11.4) Значения среднего, медианы и модуса

    Задание 3

  • В функции расчета модуса приведение типов примените только один раз.
  • Массив данных задайте случайным образом (целыми числами). Проанализируйте результат работы программы.
  • Создайте три указателя на функцию – на функцию расчета среднего арифметического, на функцию расчета медианы и на функцию расчета модуса. При этом для массива целых чисел функция расчета модуса должна возвращать целое значение.
  • Объедините три функции – mean(), median(), mode() в одну функцию и определите необходимый тип возвращаемого значения, чтобы в главной функции main() можно была распечатать результаты расчетов статистических характеристик.
  • Напишите программу, в которой с помощью указателя на функции можно было вывести на консоль средние оценки за последние три года обучения в школе.
  • Пример 4. Напишите программу сортировки массива строк с использованием указателей на функции.

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

    #include <stdio.h>
    #include <conio.h>
    #include <string.h>
    
    // Прототипы функций
    void bsort (char **arr, int size, 
    	int (*comp) (const char *s1, const char *s2));
    
    int less (const char *s1, const char *s2);
    int greater (const char *s1, const char *s2);
    
    int main (void)
    {
    	char *Lines[] = { "asd", "aza", "baza", "qwerty", "hello", "world", "aza" };
    	
    
         int n = sizeof (Lines) / sizeof (Lines[0]);
    	int i;
    
    // Вызов функции сортировки по возрастанию в алфавитном порядке
    	puts("\n The sorting in ascending order:");
    	bsort (Lines, n, less);
    	for (i = 0; i < n; ++i)
    		printf("\t %s\n", Lines[i]);
    	
    // Вызов функции сортировки по убыванию в алфавитном порядке
    	puts("\n The sorting in descending order:");
    	bsort (Lines, n, greater);
    	for (i = 0; i < n; ++i)
    		printf("\t %s\n", Lines[i]);
    
    	printf("\n\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определение функции сортировки строк
    void bsort (char **arr, int size, 
    	int (*comp) (const char *s1, const char *s2))
    {
    	int i, j;
    
    	for (i = 0; i < size - 1; ++i)
    		for (j = 0; j < size - 1; ++j)
    			if (comp (arr[j], arr[j + 1]) > 0)
    			{
    				char *s = arr[j];
    				arr[j] = arr[j + 1];
    				arr[j + 1] = s;
    			}
    }
    
    // Определение функции сравнения строк по возрастанию
    int less (const char *s1, const char *s2)
    {
    	return strcmp (s1, s2);
    }
    
    // Определение функции сравнения строк по убыванию
    int greater (const char *s1, const char *s2)
    {
    	return -strcmp (s1, s2);
    }

    В программе используются указатель на функцию для вызова двух функций – less() и greater() в процессе сортировки для определения порядка расположения элементов (слов).

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

    (рис 11.5) Пример сортировки строк

    Задание 4

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

    $$y=sin(3x)e^{\frac x 3}.$$

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

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

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

    / Заголовочные файлы
    #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);
    	//return x * x - 3;
    }
    
    
    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).

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

    (рис 11.6) Пример построения графика функции на консоли

    Задание 5

  • Внесите в программу изменения для вывода графика в текстовый файл с именем compX.txt, где Х – номер компьютера, на котором выполняется лабораторная работа.
  • Проанализируйте программу с целью возможного улучшения вида графика заданной функции.
  • В программу добавьте описание кубической и параболической функций. Напишите необходимые строчки программного кода для запроса о построении графика соответствующей функции.
  • Контрольные вопросы

  • Каким образом можно вернуть из функции несколько значений?
  • Каким образом определяется тип функции?
  • Как выглядит описание функции, которая возвращает указатель на заданный тип, например, char?
  • Можно ли использовать многоуровневую адресацию для функции, которая возвращает указатель на заданный тип? Если можно, то как происходит определение такой функции?
  • В каком месте программы можно определить указатель на функцию?
  • Имеет ли указатель на функцию прототип и определение?
  • Как осуществляется вызов функции с помощью указателя?
  • Как взаимосвязаны между собой объявление функции, ее определение и вызов функции?
  • Страницы:

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

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

    Ранее было отмечено, что в языке С аргументы передаются в функции по значению и не существует прямого способа изменить переменную вызывающей функции, действуя внутри вызываемой функции. Благодаря аргументам-указателям функция может обращаться к объектам в вызвавшей ее функции, в том числе модифицировать их [11.1]. В качестве примера рассмотрим функцию 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 рис 11.1.

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

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

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

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

    тип  *имя_функции ( аргументы функции ) 
    {
    // тело функции
    
    тип *имя_указателя;
    ?
    
    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, также должно быть указателем. В частности, многие библиотечные функции, предназначенные для обработки строк, возвращают указатели на символы.

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

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

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

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

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

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

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

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

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

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

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

    #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().

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

    (рис 11.2) Пример сортировки массива методом пузырька

    Задание 1

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

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

    Среднее выборочное значение одномерного массива размерностью N

    $$mean=\frac 1 N \sum^N_{i=1}ai,$$

    где – элементы массива.

    Исправленная выборочная дисперсия одномерного массива размерностью N

    $$D=\frac 1 {N-1} \sum^N_{i=1}(a_i-mean)^2,$$

    где mean – среднее значение данного массива

    Исправленное среднеквадратичное отклонение S

    $$S=+\sqrt D,$$

    где D – дисперсия данного массива.

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

    #include <stdio.h>
    #include <conio.h>
    #include <stdlib.h>
    #include <math.h>
    
    // Прототип функции
    double *mean_D_S(int arr[], int n);
    
    int main(void)
    {
    	int mass[] = {2, -3, 5, 6, 7, 8, 9,-1};
    	int i, n;
    	double *R;
    	n = sizeof(mass)/sizeof(mass[0]);
    	puts("\n\t Initial array:");
    	for (i = 0; i < n; i++)
    		printf(" %3d", mass[i]);
    
         // Вызов функции 
         R = mean_D_S(mass, n);
    
    // Вывод расчтных характеристик массива
    printf("\n\n The average value of an array: %g\n", R[0]);
    printf(" The dispersion of the array: %g\n", R[1]);
    printf(" The standard deviation of the array: %g\n", R[2]);
    
    // Освобождение памяти
         free(R);
    
    	printf("\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определение функции
    double *mean_D_S(int arr[], int N)
    {
    int j;
    double *PTR3 = (double *)calloc(N, sizeof(double));
    double mean, D, S;
    
    mean = 0.0;
    for (j = 0; j < N; j++)
    mean += arr[j];
    
    mean /= N;
    
    D = 0.0;
    for (j = 0; j < N; j++)
    D += (arr[j] - mean)*(arr[j] - mean);
    
    D /= (N-1);
    
    S = sqrt(D);
    
    PTR3[0] = mean;
    PTR3[1] = D;
    PTR3[2] = S;
    
    return PTR3;
    
    }

    В программе используется функция динамического распределения памяти calloc(), которая выделенные ячейки памяти обнуляет. Расчетные характеристики одномерного массива размещаются последовательно друг за другом в выделенной памяти для указателя *PTR3. Сформированный указатель функция возвращает в точку вызова функции mean_D_S().

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

    (рис 11.3) Расчет статистических характеристик числового массива

    Задание 2

  • В качестве первого аргумента функции mean_D_S() используйте указатель на числовой массив.
  • Воспользуйтесь справкой по математическим функциям и в программе примените функцию, которая осуществляет возведение в степень.
  • Дополните возврат функцией mean_D_S() еще исходного массива, поэлементно возведенного в квадрат. В главной функции main() результаты выведите на консоль.
  • Вместо типа double используйте тип float.
  • Пример 3. Напишите программу с указателем на функции, которые рассчитывают следующие статистические характеристики одномерного числового массива: среднее арифметическое значение, медиану и модус (моду).

    Среднее арифметическое рассчитывалось в предыдущем примере.

    Приведем определения медианы и модуса, взятые из книги [11.3], из которой также взяты основные фрагменты программных кодов (в книге программы написаны на С++).

    Медиана – это серединное значение в наборе данных – т. е. такое, что ровно половина значений располагается выше, и ровно половина ниже его.

    Модус – это значение, наиболее часто встречающееся в наборе данных.

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

    #include <stdio.h>
    #include <conio.h>
    
    double mean(int*, int);
    double median(int*, int);
    double mode(int*, int);
    
    int main(void) {
    	int mass[] = {5, 6, 5, 5, 3,
    	              6, 5, 3, 1, 4,
    	              5, 3, 1, 6, 5, 
    	              2, 5, 2, 3, 4}; 
    	int *ptr = mass;
    	int temp, i, j, k, n;
    // Указатель на функции
    	double (*fun[3])(int*, int) = {mean, median, mode};
    	
         n = sizeof(mass)/sizeof(mass[0]);
    	puts("\n The original array:");
    	j = 0;
    	for (i = 0; i < n; i++) {
    		j++;
    		
    		if ( j%6 )
    			printf(" %2d", mass[i]);
    		else
    		{puts(""); printf(" %2d", mass[i]); j = 1;}
    	}
    
    // Сортировка методом выбора
    for (i = 0; i < (n - 1); ++i) {
     temp = ptr[i]; k = i;
    for (j = i + 1; j < n; ++j)
    if (ptr[j] < temp) { k = j; temp = ptr[k]; }
    ptr[k] = ptr[i]; ptr[i] = temp;
    }
    	puts("\n\n Results - mean, mediana, modus: ");
    for (i = 0; i < 3; i++)
    printf("%6g\n",(*fun[i])(ptr, n));
    
    	printf("\n\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определения функций
    double mean(int* arr, int N)
    {
    int i;
    double aver = 0.0;
    
    for (i = 0; i < N; i++)
    aver += arr[i];
    
    return (aver/N);
    
    }
    
    double median(int* arr, int N)
    {
    int i;
    double med = 0.0;
    
    for (i = 0; (i < N/2); i++)
    med = arr[i];
    if ( N % 2)
    med = arr[i];
    
    else
    med = (med + arr[i])/2;
    return med;
    }
    
    double mode(int* arr, int N)
    {
    int instances = 0,
        tempinst = 1,
               i = 1;
    double tempmode,
           mode_return = 1.0;
    tempmode = (double)arr[0];
    
    while (i < N) 
    {
       while ((double)arr[i] == tempmode )
       {
       i++;
       tempinst++;
       }
       if (tempinst > instances)
       {
       mode_return = tempmode;
       instances = tempinst;
       }
       tempinst = 1;
       tempmode = (double)arr[i];
       i++;
    }
    
    return (mode_return);
    }

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

    Для понимания работы функции по расчету модуса алгоритм вычислений рекомендуется в [11.3] формулировать следующим образом: "Разбить сортированный список значений на ряд меньших списков, каждый из которых содержит одинаковые значения. Пересчитать число элементов в этих списках, и список с наибольшим числом элементов будет соответствовать модусу данных".

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

    (рис 11.4) Значения среднего, медианы и модуса

    Задание 3

  • В функции расчета модуса приведение типов примените только один раз.
  • Массив данных задайте случайным образом (целыми числами). Проанализируйте результат работы программы.
  • Создайте три указателя на функцию – на функцию расчета среднего арифметического, на функцию расчета медианы и на функцию расчета модуса. При этом для массива целых чисел функция расчета модуса должна возвращать целое значение.
  • Объедините три функции – mean(), median(), mode() в одну функцию и определите необходимый тип возвращаемого значения, чтобы в главной функции main() можно была распечатать результаты расчетов статистических характеристик.
  • Напишите программу, в которой с помощью указателя на функции можно было вывести на консоль средние оценки за последние три года обучения в школе.
  • Пример 4. Напишите программу сортировки массива строк с использованием указателей на функции.

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

    #include <stdio.h>
    #include <conio.h>
    #include <string.h>
    
    // Прототипы функций
    void bsort (char **arr, int size, 
    	int (*comp) (const char *s1, const char *s2));
    
    int less (const char *s1, const char *s2);
    int greater (const char *s1, const char *s2);
    
    int main (void)
    {
    	char *Lines[] = { "asd", "aza", "baza", "qwerty", "hello", "world", "aza" };
    	
    
         int n = sizeof (Lines) / sizeof (Lines[0]);
    	int i;
    
    // Вызов функции сортировки по возрастанию в алфавитном порядке
    	puts("\n The sorting in ascending order:");
    	bsort (Lines, n, less);
    	for (i = 0; i < n; ++i)
    		printf("\t %s\n", Lines[i]);
    	
    // Вызов функции сортировки по убыванию в алфавитном порядке
    	puts("\n The sorting in descending order:");
    	bsort (Lines, n, greater);
    	for (i = 0; i < n; ++i)
    		printf("\t %s\n", Lines[i]);
    
    	printf("\n\n ... Press any key: ");
    	_getch();
    	return 0;
    }
    
    // Определение функции сортировки строк
    void bsort (char **arr, int size, 
    	int (*comp) (const char *s1, const char *s2))
    {
    	int i, j;
    
    	for (i = 0; i < size - 1; ++i)
    		for (j = 0; j < size - 1; ++j)
    			if (comp (arr[j], arr[j + 1]) > 0)
    			{
    				char *s = arr[j];
    				arr[j] = arr[j + 1];
    				arr[j + 1] = s;
    			}
    }
    
    // Определение функции сравнения строк по возрастанию
    int less (const char *s1, const char *s2)
    {
    	return strcmp (s1, s2);
    }
    
    // Определение функции сравнения строк по убыванию
    int greater (const char *s1, const char *s2)
    {
    	return -strcmp (s1, s2);
    }

    В программе используются указатель на функцию для вызова двух функций – less() и greater() в процессе сортировки для определения порядка расположения элементов (слов).

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

    (рис 11.5) Пример сортировки строк

    Задание 4

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

    $$y=sin(3x)e^{\frac x 3}.$$

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

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

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

    / Заголовочные файлы
    #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);
    	//return x * x - 3;
    }
    
    
    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).

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

    (рис 11.6) Пример построения графика функции на консоли

    Задание 5

  • Внесите в программу изменения для вывода графика в текстовый файл с именем compX.txt, где Х – номер компьютера, на котором выполняется лабораторная работа.
  • Проанализируйте программу с целью возможного улучшения вида графика заданной функции.
  • В программу добавьте описание кубической и параболической функций. Напишите необходимые строчки программного кода для запроса о построении графика соответствующей функции.
  • Контрольные вопросы

  • Каким образом можно вернуть из функции несколько значений?
  • Каким образом определяется тип функции?
  • Как выглядит описание функции, которая возвращает указатель на заданный тип, например, char?
  • Можно ли использовать многоуровневую адресацию для функции, которая возвращает указатель на заданный тип? Если можно, то как происходит определение такой функции?
  • В каком месте программы можно определить указатель на функцию?
  • Имеет ли указатель на функцию прототип и определение?
  • Как осуществляется вызов функции с помощью указателя?
  • Как взаимосвязаны между собой объявление функции, ее определение и вызов функции?
  • Вернуться к учебному плану