Функции Любая C-программа состоит из одной или более функций Одна и только одна функция должна называться main() Функции нельзя определять внутри других функций Общий вид функции тип имя_функции (список аргументов) { «тело» функции } Обратите внимание Все аргументы функции должны объявляться отдельно, то есть для каждого из них надо указывать и тип, и имя f(double a, double b, int j) /* правильный список */ f(double a, b, int j) /* неправильный */ Если тип возвращаемого значения или тип аргумента не указан, то считается, что int Устаревшее поведение, не рекомендуется! add_one(i) { return i+1; } > gcc test.c warning: return type defaults to ‘int’ [-Wimplicit-int] add_one(i) { ^ In function ‘add_one’: warning: type of ‘i’ defaults to ‘int’ [-Wimplicit-int] Передача параметров по значению Что будет напечатано? void f(int a) { a++; } int main() { int a = 5; f(a); /* a++ */ printf(" %d \n",a); return 0; } Передача параметров по значению Что будет напечатано? void f(int a) { a++; } int main() { int a = 5; f(a); /* a++ */ printf(" %d \n",a); return 0; } Output: 5 В С в функцию передаются только значения переменных! Аргументы функции получают значения в момент вызова и изменение этих переменных внутри функции не распространяется «наружу» Оператор return и возвращаемое значение return 1 обеспечивает немедленный выход из функции 2 возвращает значение (если функция не void) Основные виды возвращаемых значений: результаты вычислений: sqrt(), sin() значения, показывающее успешно ли была выполнена функция: main(), fclose(), printf() нет возвращаемых значений: exit(),perror(),free() Обратите внимание Использовать возвращаемое значение не обязательно, как, например, в случае printf() Объявление и определение функций Определение функции — собственно вся функция целиком Объявление функции или прототип — информация о функции: список параметров и тип возвращаемого значения; задача прототипа – описать интерфейс функции Пример: int main() { int i = 2; int j = sqr(i); /* ERROR: What is sqr()? */ printf(" i = %d j = %d\n",i,j); return 0; } Объявление функций /* prototypes: */ double sqr(double x); double fun1(void); double fun2(); /* 1 argument double */ /* 0 arguments */ /* undefined number of arguments */ int main() { int i = 2; int j = sqr(i); /* OK: sqr( (double) i) */ printf(" i = %d j = %d\n",i,j); return 0; } Определение функции double sqr(double x) return x*x; } { /* called function */ Заголовочные файлы стандартной библиотеки Интерфейс для библиотек описывается с помощью прототипов функций в заголовочных файлах (.h = header): stdio.h – ввод/вывод math.h – математические функции stdlib.h – функции общего назначения limits.h – пределы и константы для целых типов данных float.h – параметры типов для чисел с плавающей точкой Пример включения заголовочных файлов #include <math.h> #include <stdio.h> #include <stdlib.h> Встраиваемые (inline) функции (C99) Оптимизация вызова функции Предполагает, что вызов inline функции будет максимально быстрым: код функции будет вставляться на место вызова Синтаксически нет различия в вызове обычной и inline функции Пример static inline double Sqr(double x) {return x*x;} Обратите внимание static для любой функции означает, что эта функция видна только в том файле, где она определена. Такие функции можно целиком помещать в заголовочный файл. Может привести к «раздуванию» кода. Следует использовать для действительно маленьких функций. Обычная и встраиваемая версии функции (C99) Иногда необходимо иметь две «версии» одной функции встраиваемую и обычную, (например, что бы передать указатель на функцию) Для таких случаев общая стратегия в С99: Определяем в заголовочном файле inline function и включаем этот файл везде, где требуется эта функция В одном единственном .c-файле добавляем объявление: external function 1 в заголовочном файле header.h: inline double Sqr(double x) {return x*x;} 2 в main.c -файле: #include "header.h" extern double Sqr(double x); /* prototype */ 3 в остальных .c -файлах: #include "header.h" Несколько возвращаемых значений? Найти действительный корни уравнения: x 2 + px + q = 0 Возможное решение: глобальные переменные double xmin, xmax; int quad_eq(double p, double q) { double d = p*p-4*q; if( fabs(d) < 2.2e-16 ) { /* almost zero */ xmin = xmax = -0.5*p; return 1; } else if( d > 0 ) { d = sqrt(d); xmin = -0.5*(p+d); xmax = -0.5*(p-d); return 2; } return 0; } . . . продолжение void test_quad_eq(double p, double q) { int ret = quad_eq(p,q); switch( ret ) { case 0: printf(" no solution\n"); break; case 1: printf(" one root: %f\n", xmin); break; case 2: printf(" two roots: %f, %f\n", xmin,xmax); } } int main() { test_quad_eq(-3,2); test_quad_eq(-2,1); test_quad_eq(-3,5); return 0; } two roots: 1.000000, 2.000000 one root: 1.000000 no solution Глобальные переменные – pro et contra Следует избегать использовать глобальные переменные! Затрудняют понимание программы Скрывают явные связи между функциями и переменными Невозможно установить правила использования Делают невозможным параллелизм исполнения Когда глобальные переменные имеет смысл использовать? Небольшие, «изолированные» программы Слишком велики затраты чтобы избежать использование глобальных переменных: простые вещи следует делать просто Глобальные переменные – pro et contra Следует избегать использовать глобальные переменные! Затрудняют понимание программы Скрывают явные связи между функциями и переменными Невозможно установить правила использования Делают невозможным параллелизм исполнения Когда глобальные переменные имеет смысл использовать? Небольшие, «изолированные» программы Слишком велики затраты чтобы избежать использование глобальных переменных: простые вещи следует делать просто Плохие мотивы для глобальных переменных Меньше набирать текста, программа становится короче . . . Передавать переменные в функции так утомительно . . . Не знаю куда бы поместить эту переменную . . . Адреса и указатели Схема организации памяти /* variables b and c */ int b = 0x01101101; int c = 0; /* a - pointer to an int */ int* a = NULL; /* assign address of b to a */ a = &b; /* to dereference косвенный доступ */ c = *a; /* c=0x01101101 */ *a = 0; /* now b is 0 */ Адреса и указатели Схема организации памяти /* variables b and c */ int b = 0x01101101; int c = 0; /* a - pointer to an int */ int* a = NULL; /* assign address of b to a */ a = &b; /* to dereference косвенный доступ */ c = *a; /* c=0x01101101 */ *a = 0; /* now b is 0 */ Byte addressing: 1KiB = 1024B; 1MiB = 1024KiB = 1048576B; . . . x86-32: max address (32-bit) = 232 − 1 = 4,294,967,295 B (4GiB); x86-64: max address (48-bit/52-bit) = 256 TiB / 4 PiB; Объявление указателей int *a, *b; char* c; void *pv; int **ppa; /* /* /* /* указатели указатель указатель указатель на на на на int */ char */ void */ указатель на int */ int (*f)(double) /* указатель на функцию которая возвращает int и имеет один аргумент типа double */ Объявление указателей int *a, *b; char* c; void *pv; int **ppa; /* /* /* /* указатели указатель указатель указатель на на на на int */ char */ void */ указатель на int */ int (*f)(double) /* указатель на функцию которая возвращает int и имеет один аргумент типа double */ Функция scanf() – новый взгляд int i; int j; int* pj = &j; scanf("%d",&i); /* читаем i */ scanf("%d",pj); /* читаем j */ Несколько возвращаемых значений: адреса корни уравнения: x 2 + px + q = 0 int QuadEq(double p, double q, double* x1, double* x2) { double d = p*p-4*q; if( fabs(d) < 2.2e-16 ) { /* almost zero */ *x1 = *x2 = -0.5*p; return 1; } else if( d > 0. ) { d = sqrt(d); *x1 = -0.5*(p+d); *x2 = -0.5*(p-d); return 2; } return 0; } void TestQuadEq(double p, double q) { double x1 = 0, x2 = 0; int ret = QuadEq(p,q,&x1,&x2); switch( ret ) { case 0: printf(" no solution\n"); break; case 1: printf(" one root: %f\n", x1); break; case 2: printf(" two roots: %f, %f\n", x1,x2); } } TestQuadEq(-3,2); TestQuadEq(-2,1); TestQuadEq(-3,5); two roots: 1.000000, 2.000000 one root: 1.000000 no solution Еще один пример: функция swap() void swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } int main() { int i = 2; int j = 40; printf("before swap: i = %d, j = %d\n",i,j); swap(&i,&j); printf(" after swap: i = %d, j = %d\n",i,j); return 0; } Output: before swap: i = 2, j = 40 after swap: i = 40, j = 2 Рекурсия От латинского recursio – круговорот Рекурсивная функция – функции вызывающая сама себя. Пример: Гамма-функция Γ(z) = Γ(z + 1)/z double gamma_recursion(double z) { if( z < 10.5 ) { if( fabs(z) < 2.e-16) { return HUGE_VAL; /* a large value, possibly +oo */ } return gamma_recursion(z+1)/z; } return exp(log_gam(z)); } Преимущества рекурсии Простота написания некоторых алгоритмов Недостатки рекурсивных функций в С Обычно приводит к замедлению выполнения Может вызвать «переполнение стека» (Stack overflow) Числа Фибоначчи: f0 = 1; f1 = 1; fn+1 = fn + fn−1 unsigned int fibonacci(unsigned int n) { return (n < 2) ? 1 : fibonacci(n-1) + fibonacci(n-2); } fibonacci(46) – время выполнения ∼ 20 секунд . . . 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 fibonacci(46) = 1836311903 «Правильная» рекурсия для чисел Фибоначчи typedef unsigned int Uint; Uint fib_toc(Uint n, Uint current, Uint next) { if( n == 0 ) return current; return fib_toc(n-1,next,current+next); } Uint fibonacci_toc(Uint n) { return (n < 2) ? n : fib_toc(n,0,1); } fibonacci_toc(46) = 1836311903 Сравните вызов fib_toc(n,0,1) с циклом: for(current=0,next=1; n != 0; n--) { Uint tmp = current; current = next; next += tmp; }