1.05M
Категория: ПрограммированиеПрограммирование

Функции в Си

1.

СПбГУТ им. проф. М.А. Бонч–Бруевича
Кафедра программной инженерии и вычислительной техники (ПИ и ВТ)
ПРОГРАММИРОВАНИЕ
Единственный способ изучать новый язык
программирования – писать на нем программы.
Брайэн Керниган
Лекция 8: Функции
1.
2.
3.
4.
5.
Функции в Си
Формальные и фактические параметры
Передача массивов в функцию
Функции с переменным числом параметров
Функции. Что ещё?..
Санкт–Петербург, 2021г.

2.

Введение: Что такое функция?
Чем дальше мы изучаем Си, тем больше становятся программы.
Мы собираем все действия в одну функцию main и по несколько
раз копируем одни и те же действия, создаём десятки
переменных с уникальными именами.
Наши программы распухают и становятся всё менее и менее
понятными, ветвления становятся всё длиннее и ветвистее.
Но из сложившейся ситуации есть выход!
Использование вспомогательных алгоритмов ─ ПОДПРОГРАММ
Основная (вызывающая)
программа
Вызов подпрограммы
Продолжение основной
программы
Есть выход:
Divide et impera –
Разделяй и властвуй!
Подпрограмма
Структурная
декомпозиция
программы!
Подпрограммы применяются когда:
часть алгоритма неоднократно повторяется в программе;
можно использовать фрагменты разработанных ранее
алгоритмов;
для разбиения крупных программ на части в соответствии
с модульным принципом программирования.
В языке Си подпрограммы реализованы в виде функций.
Теперь мы научимся создавать функции на Си.
Функции:
во-первых, помогут выделить в отдельные подпрограммы
дублирующийся код,
во-вторых, помогут логически разбить программу на части,
в-третьих, с функциями в си связано много особенностей,
которые позволят использовать новые подходы к
структурированию приложений.
Процедура( функция) представляет собой
последовательность операторов, которая имеет имя,
список параметров и может быть вызвана из различных
частей программы.
Имя процедуры в тексте программы называется вызовом.
Вызов активирует процедуру (функцию) ─ начинают
выполняться её операторы.
После выполнения процедуры программа продолжается с
оператора стоящего за вызовом.
Отличие процедур от функций в том, что функции
возвращают значение.
При совместной работе функции должны обмениваться
информацией.
Это можно осуществить с помощью:
глобальных переменных;
через параметры;
через возвращаемое функцией значение.

3.

1. Функции в Си
Что такое функция?
Функция – это именованная последовательность описаний
и операторов, выполняющее какое-либо законченное
действие.
Функция может принимать параметры и возвращать
значение.
Подпрограмма или, другими словами, функция должна быть
связана (интегрирована) с основной программой, так сказать,
со своим внешним окружением.
С целью обеспечения взаимодействия с остальной частью
программы для функции можно предусмотреть так
называемые вход и выход.
Вход в функцию — это передача ей аргументов — данных,
полученных во внешней части программы. Получив данные из
своего внешнего окружения (внешней программы), функция
должна их как-то обработать: выполнить некоторые действия,
вычислить какое-то значение.
Выход из функции — значение, вычисленное блоком кода
данной функции и передаваемое во внешнюю часть
программы.
Входные данные называют параметрами, а выходные —
возвращаемым значением.
Впрочем, функция может и не принимать никаких
параметров, а также ничего не возвращать.
Что принимает в качестве параметров и что возвращает
функция в результате своей работы, определяет программист,
т. е. автор-разработчик программного кода.
Функция — важнейший элемент структурного
программирования, позволяющий группировать и обобщать
программный код, который может позднее использоваться
произвольное число раз. Она является законченной подпрограммой,
поэтому у нее есть свои "ввод" и "вывод" — параметры (аргументы) и
возвращаемое значение:
С точки зрения внешней программы функция — это "черный
ящик". Функция определяет собственную (локальную) область
видимости, куда входят входные параметры, а, также, те
переменные, которые объявляются непосредственно в теле самой
функции.
Главное, что должно быть можно сделать с функцией — это
возможность ее вызвать.
Перед использованием функция должна быть объявлена и
соответствующим образом определена.
Объявление (declaration) функции содержит список параметров
вместе с указанием типа каждого параметра, а, также, тип
возвращаемого функцией значения.
Определение (definition) функции содержит исполняемый код
функции.
Вызов функции может встретиться в программе до определения,
но обязательно после объявления.
Функции, которые не возвращают значений, иногда называют
процедурами.
3

4.

Функции в Си
Подпрограмма (ПП) – это поименованный или иным
образом идентифицированный фрагмент компьютерной
программы, которому можно передать управление
(вызвать) в любой её точке и который имеет возможность
вернуть управление в точку, следующую за точкой своего
вызова
Плюсы ПП:
Уменьшение размера памяти, занимаемой кодом программы
– почти неактуально в настоящее время.
Структуризация программы с целью удобства её понимания и
сопровождения:
Исправление ошибок, оптимизация, расширение
функциональности в ПП автоматически отражается на
всех её вызовах
Вынесение в ПП даже однократно выполняемого набора
действий делает программу более понятной и обозримой
В языке Си подпрограммы называются функциями
Описание функции делится на:
Заголовок – тип результата, имя функции и список
параметров функции
Если тип-результата есть void, то функция не возвращает
результата – аналог процедуры в языке Паскаль
Тело – это набор инструкций, который будет выполнен, когда
функция будет вызвана
Вызов – это частный случай постфиксного выражения языка
Си (A + B → A B +)
Вам уже знакомы функции:
Функции ввода-вывода – <stdio.h>:
printf() – форматный вывод в stdout
scanf() – форматный ввод (чтение данных) из потока stdin
getchar(), putchar(int c) и т.д.
Функции работы с файлами:
FILE *fopen(char *filename, char *mode)
int fflush(FILE *f)
int fclose(FILE *f) и т.д.
***
Функции работы со строками – <string.h>
Математические функции – <math.h>
Функции общего назначения – <stdlib.h>
Функции работы с датой и временем – <time.h>
***
Функции – это самостоятельные единицы программы,
предназначенные для решения конкретных подзадач,
обычно повторяющиеся несколько раз.
Перед использованием функция должна быть объявлена
Все функции в языке Си – глобальные, т.е. функция не
может быть объявлена внутри другой функции
В Си можно объявить функцию с помощью прототипа, т.е.
заголовка функции, а полное ее описание сделать после
функции main()
4

5.

Функции в Си
Функция может принимать фиксированное либо переменное
число аргументов, а может не иметь аргументов.
Функция может как возвращать значение, так и быть пустой
(void) и ничего не возвращать (аналог процедуры в Pascal)
Мы уже знакомы с многими функциями и знаем, как их
вызывать – это функции библиотек stdio, stdlib, string, conio и
пр.
Более того, main – это тоже функция.
Она отличается от остальных только тем, что является точкой
входа при запуске приложения.
Функция в Си определяется в глобальном контексте.
Синтаксис функции:
тип_функции имя_функции ([список_параметров]), ...)
{
тело функции
}
Функции — это средство проектирования, которое позволяет
осуществить декомпозицию программы на достаточно
простые и легко управляемые части.
Значительно проще написать решение маленьких задач по
отдельности, чем реализовать весь процесс целиком.
Устранение избыточности программного кода улучшает
сопровождаемость кода — если что-то необходимо будет
исправить, достаточно будет внести изменения всего в одном
месте, а не во многих.
С использованием функций в языке Си связаны три понятия:
1. Определение функции (описание действий, выполняемых
функцией)
2. Объявление функции (задание формы обращения к
функции)
3. Вызов функции.
В языке Си нет требования, чтобы определение функции
обязательно предшествовало ее вызову.
Определения используемых функций могут следовать за
определением функции main, перед ним, или находится в
другом файле.
// Примеры:
// Объявление функции:
int sum (int a, int b);
// Определение функции:
int sum (int a, int b)
{
return (a+b);
}
5

6.

Функция. Структурная декомпозиция программы
Информация в функцию передается с помощью аргументов (фактических параметров),
задаваемых при ее вызове.
Эти аргументы должны соответствовать формальным параметрам, указанным в описании
функции.
аргументы
int k = add_ints (2,3);
Значения аргументов заменяют соответствующие
параметры в определении функции
Возвращается
значение 5
int add_ints (int a, int b)
{
return a+b;
}
формальные
параметры
6

7.

Функция. Структурная декомпозиция программы
Выполнение
начинается с main
int main()
{

my_f1 (a,n);

void my_f1 (double v, int dim)
{

return;
}
void my_f2 (double v1, double v2,
double res, int dim)
{

my_f2 (a,b,c,n);

output_vect (c,n);

return 0
}
Возврат из main в ОС
return;
}
void output_vect (double v, int dim)
{

return;
}
7

8.

Функция. Пример
Дублирование кода является признаком «низкого» или «ленивого»
стиля программирования.
Хороший стиль программирования обычно основан на повторном
использовании кода.
Проблемы, к которым приводит дублирование кода:
большое количество кода: дублирование часто приводит к
созданию длинных, повторяющихся последовательностей кода,
которые отличаются лишь несколькими строками или
символами, что в итоге затрудняет понимание программы;
скрытое значение: трудно уловить разницу в повторяющихся
участках кода и поэтому становится тяжелее понимать, для чего
именно предназначен тот или иной кусок кода, зачастую
единственная разница заключается в параметрах;
аномалии обновления: при обновлении дублированного кода
необходимо обновить несколько аналогичных участков, что
сильно увеличивает затраты на обслуживание;
размер исходного текста: без применения какого-либо сжатия
исходный текст занимает больше места.
// Вычисляет среднее значение массива целых чисел:
int array1[N];
int array2[N];
int sum1 = 0; int sum2 = 0;
int average1; int average2;
int i;
for (i = 0; i < 4; ++i) sum1 += array1[i];
average1 = sum1 / 4;
for (i = 0; i < 4; ++i) sum2 += array2[i];
average2 = sum2 / 4;
Существует определённое количество алгоритмов, позволяющих
отыскать дубликаты кода, среди них:
алгоритм Бейкер;
алгоритм Рабина-Карпа;
использование абстрактных синтаксических деревьев.
В ряде случаев эффективно визуальное определение дубликатов
NB: Одна из основных причин проявления дублирования —
программирование копированием-вставками, при котором участки
кода копируются просто потому, что «это работает».
// Два цикла могут быть выделены в отдельную функцию:
int calcAverage (int* Array_of_4)
{
int sum = 0;
for (int i = 0; i < 4; ++i)
sum += Array_of_4[i];
return sum / 4;
}
// Использование этой функции избавит код от дубликатов:
int array1[N];
int array2[N];
int average1 = calcAverage(array1);
int average2 = calcAverage(array2);
8

9.

Объявление функции и определение функции
Определение функции задает тип возвращаемого
значения, имя функции, типы и число формальных
параметров, а также объявления переменных и операторы,
называемые телом функции, и определяющие действие
функции (сама функция и её полный текст).
Пример:
Оператор return
int max ( int a, int b) – вызывает немедленный выход из текущей
функции и возврат в вызывающую функцию
{ if (a>b)
– используется для возврата значения функции
return a;
– в теле функции может быть несколько
else
операторов return, но может не быть ни одного
return b;
}
В данном примере определена функция с именем max,
имеющая 2 параметра. Функция возвращает целое
максимальное значение из а и b.
Чтобы компилятор мог осуществить проверку соответствия
типов передаваемых фактических параметров типам
формальных параметров до вызова функции нужно поместить
объявление (прототип) функции.
Объявление (прототип) функции имеет такой же вид, что
и определение функции, с той лишь разницей, что тело
функции отсутствует, и имена формальных параметров
тоже могут быть опущены.
Для функции, определенной в последнем примере, прототип
может иметь вид:
int max (int a, int b);
– если нет return, возврат в вызывающую
программу происходит после выполнения
последнего оператора тела функции
В программах на языке Си широко используются, так
называемые, библиотечные функции, т.е. функции
предварительно разработанные и записанные в библиотеки.
Прототипы библиотечных функций находятся в специальных
заголовочных файлах, поставляемых вместе с библиотеками в
составе систем программирования, и включаются в
программу с помощью директивы #include.
В Си можно объявить функцию до её определения.
Объявление функции, её прототип, состоит из возвращаемого
значения, имени функции и типа аргументов. Имена аргументов
можно не писать.
Например:
#include <stdio.h>
//Прототипы функций. Имена аргументов можно не писать
int odd(int);
Это смешанная рекурсия – функция odd
int even(int);
возвращает 1, если число нечётное и 0,
void main()
если чётное.
{
printf("if %d odd? %d\n", 11, odd(11));
printf("if %d odd? %d\n", 10, odd(10));
getch();
}
//Определение функций. Смешанная рекурсия
int even(int a)
{
if (a) odd(--a);
else return 1; int odd(int a)
}
{
if (a) even(--a);
else return 0;
9
}

10.

Объявление функции и определение функции
Обычно объявление функции помещают отдельно, в .h файл,
а определение функций в .c файл.
Таким образом, заголовочный файл представляет собой
интерфейс библиотеки и показывает, как с ней работать, не
вдаваясь в содержимое кода.
Создание простой библиотеки
Для этого нужно будет создать два файла – один с
расширением .h и поместить туда прототипы функций, а другой с
расширением .c и поместить туда определения этих функций.
Если вы работаете с IDE, то .h файл необходимо создавать в
каталоге «Заголовочные файлы», а файлы кода в каталоге
«Файлы исходного кода» (условные имена каталогов).
Пусть файлы называются File1.h и File1.c Перепишем код с
предыдущего слайда:
Заголовочный файл
File1.h:
#ifndef _FILE1_H_
#define _FILE1_H_
int odd(int);
int even(int);
#endif
Макрозащита #define
_FILE1_H_ и т.д.
используется для
предотвращения
повторного копирования
кода библиотеки при
компиляции.
Эти строчки можно
заменить одной:
#pragma once
Содержимое файла
исходного кода File1.c:
#include "File1.h"
int even(int a)
{ if (a) odd(--a);
else return 1;
}
int odd(int a) //*******
{ if (a) even(--a);
else return 0;
}
Или File1.h:
#pragma once
#include "File1.c"
int odd(int);
int even(int);
Особенности каждого файла в каталоге (с библиотекой):
Наш файл, который содержит функцию main, подключает
необходимые ему библиотеки а также заголовочный файл
File1.h.
Теперь компилятору известны прототипы функций, то есть он
знает возвращаемый тип, количество и тип аргументов и
имена функций.
Заголовочный файл, как и оговаривалось ранее, содержит
прототип функций.
Также здесь могут быть подключены используемые
библиотеки.
Макрозащита #define _FILE1_H_ и т.д. используется для
предотвращения повторного копирования кода библиотеки
при компиляции.
Эти строчки можно заменить одной #pragma once
Файл File1.c исходного кода подключает свой заголовочный
файл.
Всё как обычно логично и просто.
В заголовочные файлах принято кроме прототипов функций
выносить константы, макроподстановки и определять новые
типы данных.
Именно в заголовочных файлах можно
обширно комментировать код и писать
примеры его использования.
Функция main:
#include <stdio.h>
#include "File1.h"
void main()
{ printf("if %d odd? %d\n", 11, odd(11));
printf("if %d odd? %d\n", 10, odd(10));
getch();
}
10

11.

Область видимости переменных
Область действия (видимости) переменной – это правила,
которые устанавливают, какие данные доступны из
данного места программы.
С точки зрения области действия переменных различают три
типа переменных:
глобальные
локальные
формальные параметры.
Глобальные переменные:
Это переменные, объявленные вне какой-либо функции.
Могут быть использованы в любом месте программы, но
перед их использованием они должны быть объявлены.
Область действия глобальной переменной – вся программа.
Локальные переменные:
Это переменные, объявленные внутри функции.
Локальная переменная доступна внутри блока, в котором она
объявлена.
Локальная переменная существует пока выполняется блок, в
котором эта переменная объявлена. При выходе из блока эта
переменная (и ее значение) теряется.
Формальные параметры:
Используются в теле функции так же, как локальные
переменные.
Область действия формальных параметров - блок,
являющийся телом функции.
Недостатки использования глобальных переменных:
Они занимают память в течение всего времени работы
программы.
Делает функции менее общими и затрудняет их
использование в других программах.
Использование внешних переменных делает возможным
появление ошибок из-за побочных явлений.
Эти ошибки, как правило, трудно отыскать.
Параметры-значения:
Все аргументы функции передаются по значению
При вызове функции в стеке выделяется место для
формальных параметров функции, и туда заносится значение
фактического параметра, т. е. значение параметра при вызове
функции.
Далее функция использует и меняет значения в стеке. При
выходе из функции измененные значения параметров
теряются.
В языке Си вызванная функция не может изменить
переменные, указанные в качестве фактических параметров в
функции при обращении к ней.
Параметры-ссылки:
Для возможности изменения внутри функции значений
переменных, являющихся параметрами этой функции,
необходимо передавать в функцию не значения этих
переменных, а их адреса
11

12.

Структура программы с использованием функций
Глобальные переменные
Функция А
Локальные переменные
{
...
}
Функция В
Локальные переменные
{
...
}
Главная (main)
Глобальными называются переменные, которые
описаны в главной программе.
Время жизни глобальных переменных — с начала
программы и до ее завершения.
Располагаются в сегменте данных.
В функциях описываются локальные переменные.
Они располагаются в сегменте стека, причем
распределение памяти происходит в момент вызова
подпрограммы, а ее освобождение — по завершении
подпрограммы.
Локальные переменные автоматически не обнуляются.
Обмен данными:
Через глобальные переменные
Через параметры
Параметры передаются:
По значению
По адресу (по указателю)
{
Вызов А ... Вызов А ... Вызов В ...
...
}
12

13.

Область видимости переменных. Упрощенная структура исполняемого файла (приложения) в ОП
Статические данные распределены в специальном
статическом сегменте памяти программы
Сегмент данных
Динамическая память
Сегмент стека
Динамические
данные
1 Мбайт
Локальные
Глобальные данные объявленные вне функций.
Данные, объявленные внутри функций как static –
статические локальные данные:
o доступны только функции, в которой описаны, но
существуют (занимают память) во время выполнения
всей программы.
Сегмент кода
Локальные переменные,
объявленные внутри функций
1 Мбайт
данные
Код программы
Стек – область памяти, в
которой хранятся локальные
переменные и адреса возврата
BSS-сегмент (block started by symbol) содержит неинициализированные глобальные
переменные, или статические переменные без явной инициализации.
Этот сегмент начинается непосредственно за data-сегментом.
Обычно загрузчик программ инициализирует bss область при загрузке приложения
нулями.
Дело в том, что в data области переменные инициализированы – то есть затирают
своими значениями выделенную область памяти.
Так как переменные в bss области не инициализированы явно, то они теоретически
могли бы иметь значение, которое ранее хранилось в этой области, а это уязвимость,
которая предоставляет доступ
до приватных (возможно) данных.
Поэтому загрузчик вынужден обнулять все значения.
За счёт этого и неинициализированные глобальные переменные, и статические
переменные по умолчанию равны нулю.
13

14.

2. Функции. Механизм вызова
Вызов ПП делится на:
Подготовительные служебные действия вызывающей
программы (накладные временные расходы)
Собственно работу ПП
Заключительные служебные действия вызывающей
программы (накладные временные расходы)
Каждому вызову ПП соответствует отдельная область
памяти – стековый кадр
Стековый кадр:
Существует на протяжении всего вызова ПП:
Включая вложенные вызовы других ПП
Перестаёт существовать после завершения вызова ПП
Стековым кадром могут пользоваться:
Программа
ПП
Другие ПП, вызванные из ПП
Стековый кадр содержит:
Адрес возврата – адрес команды, которая получит
управление после завершения работы (выхода) из ПП:
Вычисляется процессором или компилятором
Обычно адрес первой команды заключительных действий
Параметры ПП – переменные ПП, значения которых
вызывающая программа устанавливает перед вызовом:
Могут частично находиться в регистрах процессора
Внутренние переменные ПП
Результат работы ПП – ячейка памяти, значение которой
устанавливается ПП перед выходом и после этого может
использоваться вызывающей программой:
Может находиться в регистре процессора
Пример стековых кадров:
void f(int *px)
{ *px = 1; }
int g()
{ int x;
f(&x);
return x;
}
int main ()
{ int x = g();
return 0;
}
int main()
{
int a[10], i;
for (i=0;i<=10;i++)
a[i]=0;
return 0;
}
Вызов функции – это
оператор.
У вызова функции есть
приоритет – один из самых
высоких.
Список аргументов
функции считают за один
операнд, так что оператор
оказывается бинарным
(первый операнд – сама
функция, второй – список
ее аргументов).
14

15.

Формальные и фактические параметры
При объявлении функции указываются формальные
параметры, которые потом используются внутри самой
функции.
При вызове функции мы используем фактические
параметры.
Фактическими параметрами могут быть переменные любого
подходящего типа или константы.
Пример:
Пусть есть функция, которая возвращает квадрат числа и
функция, которая суммирует два числа (см. код справа).
Обратите внимание, что приведение типов происходит
неявно и только тогда, когда это возможно.
Если функция получает число в качестве аргумента, то нельзя
ей передать переменную строку, например "20" и т.д.
Вообще, лучше всегда использовать верный тип или явно
приводить тип к нужному.
Передача аргументов
При передаче аргументов происходит их копирование.
Это значит, что любые изменения, которые функция
производит над переменными, имеют место быть только внутри
функции.
Пример: См. код ниже.
#include <stdio.h>
void change(int a)
{
a = 100;
printf("%d\n", a);
}
void main()
{
int d = 200;
printf("%d\n", d);
change(d);
printf("%d", d);
getch();
}
#include <stdio.h>
/* Формальные параметры имеют имена a и b
по ним мы обращаемся к переданным аргументам внутри функции */
int sum(int a, int b)
{
return a+b;
}
float square(float x)
{
return x*x;
}
void main()
{/* Фактические параметры могут иметь любое имя, в том числе и не
иметь имени */
int one = 1;
float two = 2.0;
/* Передаём переменные, вторая переменная приводится к нужному
типу */
printf("%d\n", sum(one, two));
// Передаём числовые константы
printf("%d\n", sum(10, 20));
/* Передаём числовые константы неверного типа, они автоматически
приводится к нужному */
printf("%d\n", sum(10, 20.f));
// Переменная целого типа приводится к типу с плавающей точкой
printf("%.3f\n", square(one));
/* В качестве аргумента может выступать и вызов функции, которая
возвращает нужное значение */
}
printf("%.3f\n", square(sum(2 + 4, 3)));
getch();
15

16.

Передача аргументов
1й способ: передача по значению
#include <stdio.h>
void change(int a)
{
a = 100;
printf("%d\n", a);
}
void main()
{
int d = 200;
printf("%d\n", d);
change(d);
printf("%d", d);
getch();
}
Внутри функции мы работаем с переменной а, которая
является копией переменной d.
Мы изменяем локальную копию, но сама переменная d при
этом не меняется.
После выхода из функции локальная переменная будет
уничтожена.
Переменная d при этом никак не изменится.
2й способ: передача по указателю
#include <stdio.h>
void change(int *a)
{
*a = 100;
printf("%d\n", *a);
}
void main()
{
int d = 200;
printf("%d\n", d);
change(&d);
printf("%d", d);
getch();
}
Каким образом тогда можно
изменить переменную?
Для этого нужно передать адрес
этой переменной. Перепишем
функцию, чтобы она принимала
указатель типа int:
Здесь также была создана
локальная переменная, но так как
передан был адрес, то мы изменили
значение переменной d, используя
её адрес в оперативной памяти.
В программировании первый способ передачи параметров
называют передачей по значению, второй – передачей по
указателю.
Простое правило: если вы хотите изменить переменную,
необходимо передавать функции указатель на эту
переменную.
Следовательно, чтобы изменить указатель, необходимо
передавать указатель на указатель и т.д.
NB: Этот способ рассмотреть позже, после лекции по
указателям
16

17.

Передача аргументов
Пример: напишем функцию, которая будет принимать
размер массива типа int и создавать его.
#include <stdlib.h>
void init(int *a, unsigned size)
{
a = (int*) malloc(size * sizeof(int));
}
void main()
{
int *a = NULL;
init(a, 100);
if (a == NULL)
{
printf("ERROR");
} else {
printf("OKAY...");
free(a);
}
getch();
}
Для изменения объекта необходимо передавать указатель
на него, в данном случае – указатель на указатель:
Эта функция выведет ERROR.
Мы передали адрес переменной.
Внутри функции init была создана
локальная переменная a, которая
хранит адрес массива.
После выхода из функции эта локальная
переменная была уничтожена.
Кроме того, что мы не смогли добиться
нужного результата, у нас обнаружилась
утечка памяти: была выделена память
на куче, но уже не существует
переменной, которая бы хранила адрес
этого участка.
#include <stdlib.h>
void init (int **a, unsigned size)
{
*a = (int*) malloc(size * sizeof(int));
}
void main()
{
int *a = NULL;
init(&a, 100);
if (a == NULL)
printf("ERROR");
else
{
printf("OKAY...");
free(a);
}
getch();
}
NB: Этот способ рассмотреть позже, после лекции по указателям
17

18.

Передача аргументов по значению
Пример: Обмен значениями переменных
#include <stdio.h>
//прототип функции
void swap(int a, int b);
/* Переменные, определенные вне блока, являются
глобальными переменными */
int x = 5, y = 10;
void main()
{
printf ("At the beginning x = %d and y = %d\n", x, y);
swap(x, y);
printf ("Now x = %d and y = %d\n", x, y);
return 0;
}
void swap(int a, int b) //реализация функции
{
int tmp;
tmp = a;
x = b;
y = tmp;
}
x
y
int x = 5, y = 10;
5
main ()
{
printf (“Вначале x = %d и y = %d\n”, x, y);
swap(x, y);
printf (“Теперь x = %d и y = %d\n”, x, y);
}
10
a
b
temp
510
10
5
5
void swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
18

19.

Передача аргументов по указателю
адрес1
Для возможности изменения внутри функции значений
переменных, являющихся параметрами этой функции,
необходимо передавать в функцию не значения этих
переменных, а их адреса
#include <stdio.h>
void swap1(int *a, int *b); // прототип функции
void main( )
{
int x = 5, y = 10;
printf ("At the beginning x = %d and y = %d\n", x, y);
swap1 (&x, &y);
printf ("Now x = %d and y = %d\n", x, y);
}
void swap1 (int *a, int *b) // реализация функции
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
адрес2
x
y
a
b
адрес1
адрес2
main ()
10
{
105
5
int x = 5, y = 10;
printf (“Вначале x = %d и y = %d\n”, x, y);
swap1 (&x, &y);
printf (“Теперь x = %d и y = %d\n”, x, y);
}
temp
5
void swap1 (int *a, in t*b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
NB: Этот способ рассмотреть позже, после лекции по указателям
19

20.

3. Передача массивов в функцию
Передача массива в качестве аргумента
Если в качестве аргумента функции используется массив, то
необходимо в качестве формального параметра передать
адрес начала массива.
Адрес любого другого элемента массива можно вычислить по
его индексу и типу элементов массива.
Имя массива подменяется на указатель, поэтому передача
одномерного массива эквивалентна передаче указателя.
Правило подмены массива на указатель не рекурсивное. Это
значит, что необходимо указывать размерность двумерного
массива при передаче
Пример: функция получает массив и его размер и выводит
на печать:
#include <stdio.h>
#define n 10
void printArray(int *arr, unsigned size)
// или: void printArray(int arr[], unsigned size)
{
unsigned i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
}
void main()
{
int x[n] = {1, 2, 3, 4, 5};
printArray(x, n);
getch();
}
Передача матриц в функцию через параметры
Для передачи матрицы в функцию в качестве параметров
нужно указать:
адрес начала вспомогательного массива указателей на
начала срок матрицы
размерность матрицы: количество строк и столбцов
NB: Рассмотреть
позже, после лекции
по массивам и
указателям
#include <stdio.h>
void printArray(int arr[][5], unsigned size)
// или: void printArray(int (*arr)[5], unsigned size)
{
unsigned i, j;
for (i = 0; i < size; i++)
{
for (j = 0; j < 5; j++)
printf("%d ", arr[i][j]);
printf("\n");
}
}
void main()
{
int x[][5] =
{
{ 1, 2, 3, 4, 5},
{ 6, 7, 8, 9, 10}
};
printArray(x, 2);
getch();
}
20

21.

Передача динамического массива в качестве аргумента
Если двумерный массив создан динамически, то можно
передавать указатель на указатель.
Пример: функция получает массив слов и возвращает
массив целых, равных длине каждого слова (см. код справа)
Можно вместо того, чтобы возвращать указатель на массив,
передавать массив, который необходимо заполнить:
#include <string.h>
#include <stdlib.h>
#define SIZE 10
void getLengths(const char **words, unsigned size, unsigned *out)
{ unsigned i;
for (i = 0; i < size; i++)
out[i] = strlen(words[i]);
}
void main()
{
char **words = NULL;
char buffer[128];
unsigned i;
unsigned *len = NULL;
words = (char**) malloc(SIZE * sizeof(char*));
}
for (i = 0; i < SIZE; i++)
{ printf("%d. ", i);
scanf("%127s", buffer);
words[i] = (char*) malloc(128);
strcpy(words[i], buffer);
}
len = (unsigned*) malloc(SIZE * sizeof(unsigned));
getLengths(words, SIZE, len);
for (i = 0; i < SIZE; i++)
{ printf("%d ", len[i]);
free(words[i]);
}
free(words);
free(len);
getch();
#include <string.h>
#include <stdlib.h>
#define SIZE 10
unsigned *getLengths(const char **words, unsigned size)
{ unsigned *lengths = NULL; // unsigned int <==> unsigned
unsigned i;
lengths = (unsigned*) malloc(size * sizeof(unsigned));
for (i = 0; i < size; i++)
lengths[i] = strlen(words[i]);
return lengths;
}
void main()
{ char **words = NULL;
char buffer[128];
unsigned i;
unsigned *len = NULL;
words = (char**) malloc(SIZE * sizeof(char*));
for (i = 0; i < SIZE; i++)
{ printf("%d. ", i);
scanf("%127s", buffer);
words[i] = (char*) malloc(128);
strcpy(words[i], buffer);
}
len = getLengths(words, SIZE);
for (i = 0; i < SIZE; i++)
{ printf("%d ", len[i]);
free(words[i]);
}
free(words);
free(len);
getch();
}
21

22.

4. Функции с переменным числом параметров
По умолчанию параметры передаются функции через стек.
Поэтому, технически, нет ограничения на количество
передаваемых параметров – “запихать” можно сколько
угодно.
Проблема в том, как потом функция будет разбирать
переданные параметры.
Функции с переменным числом параметров объявляются как
обычные функции, но вместо недостающих аргументов
ставится многоточие.
Пример: Пусть мы хотим сделать функцию, которая
складывает переданные ей числа, чисел может быть
произвольное количество (см. код справа)
Необходимо каким-то образом передать функции число
параметров.
Во-первых, можно явно передать число параметров
обязательным аргументом.
Во-вторых, последний аргумент может иметь некоторое
«терминальное» значение, наткнувшись на которое функция
закончит выполнение.
Общий принцип работы следующий:
внутри функции берём указатель на аргумент;
далее двигаемся к следующему аргументу, увеличивая
значение указателя.
См. функцию summ
Первый параметр – число аргументов. Это обязательный
параметр.
Второй аргумент – это первое переданное число, это тоже
обязательный параметр. Получаем указатель на первое
число:
unsigned *p = &first;
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define UNSIGNED_OVERFLOW -4
unsigned summ(unsigned char num, unsigned first, ...)
{ // unsigned int <==> unsigned
unsigned sum = 0;
unsigned testsum = 0;
unsigned *p = &first; // Указатель на первое число
while (num--)
{ testsum += *p;
if (testsum >= sum)
sum = testsum;
else
exit(UNSIGNED_OVERFLOW);
p++;
}
return sum;
}
void main()
{
int sum = summ(5, 1u, 2u, 3u, 4u, 5u);
printf("summ = %u\n", sum);
sum = summ(7, 0u, 27u, 0u, 4u, 5u, 60u, 33u);
printf("summ = %u\n", sum);
getch();
}
22

23.

Функции с переменным числом параметров
(Продолжение) См. функцию summ
Далее считываем все числа и складываем их.
В этой функции мы также при сложении проверяем на
переполнение типа unsigned.
Можно сделать первый аргумент необязательным и
«перешагнуть» аргумент unsigned char num, но тогда
возникнет большая проблема: аргументы располагаются друг
за другом, но не факт, что непрерывно.
Например, в нашем случае первый аргумент будет сдвинут не
на один байт, а на 4 относительно num:
unsigned int <==> unsigned (unsigned int = 4 байта)
Это сделано для повышения производительности.
На другой платформе или с другим компилятором, или с
другими настройками компилятора могут быть другие
результаты.
Можно сделать по-другому:
в качестве «терминального» элемента передавать ноль и
считать, что если мы встретили ноль, то больше аргументов
нет.
Пример: см. код справа.
Но теперь уже передавать нули в качестве аргументов нельзя.
Здесь также есть один обязательный аргумент – первое
переданное число.
Если его не передавать, то мы не сможем найти адрес, по
которому размещаются переменные в стеке.
Некоторые компиляторы (Borland Turbo C) позволяют
получить указатель на …, но такое поведение не является
стандартным и его нужно избегать.
#include <stdlib.h>
#define UNSIGNED_OVERFLOW -4
unsigned summ(unsigned first, ...)
{ // unsigned int <==> unsigned
unsigned sum = 0;
unsigned testsum = 0;
unsigned *p = &first; // Указатель на первое число
while (*p)
{
testsum += *p;
if (testsum >= sum)
sum = testsum;
else
exit(UNSIGNED_OVERFLOW);
p++;
}
return sum;
}
void main()
{
int sum = summ(1u, 2u, 3u, 4u, 5u, 0);
printf("summ = %u\n", sum);
sum = summ(1u, 27u, 1u, 4u, 5u, 60u, 33u, 0);
printf("summ = %u\n", sum);
getch();
}
NB: Можно воспользоваться макросом va_arg библиотеки
stdarg.h. Он делает практически то же самое: получает указатель
23
на первый аргумент а затем двигается по стеку.

24.

Функции с переменным числом параметров. Неправильное использование
Функции printf и scanf типичные примеры функций с
переменным числом параметров.
Они имеют один обязательный параметр типа const char* строку формата и остальные необязательные.
Пусть мы вызываем эти функции и передаём им неверное
количество аргументов:
Если аргументов меньше, то функция пойдёт дальше по
стеку и покажет какое-то значение, которое лежит «ниже»
последнего аргумента, например:
printf("%d\n%d\n%d\n%d\n%d", 1, 2, 3);
Если передано больше аргументов, то функция выведет
только те, которые ожидала встретить:
printf("%d\n%d\n%d\n%d\n%d", 1, 2, 3, 4, 5, 6, 7);
Так как очистку стека производит вызывающая функция, то
стек не будет повреждён.
Получается, что если изменить схему вызова и сделать так,
чтобы вызываемый объект сам чистил стек после себя, то в
случае неправильного количества аргументов стек будет
повреждён.
То есть, буде функция объявлена как __stdcall, в целях
безопасности она не может иметь переменного числа
аргументов.
Однако, если добавить спецификатор __stdcall к нашей
функции summ она будет компилироваться. Это связано с тем,
что компилятор автоматически заменит __stdcall на __cdecl.
Использование ... в объявлении функции не является
обязательным (не во всех компиляторах!).
То есть, если вы передадите функции больше параметров, то
компилятор выдаст замечание, но код останется вполне
рабочим (см. код справа-вверху).
#include <stdio.h> // правильно:
void allmyvars (int num, ...)
{
int *p = &num + 1;
while (num--)
{
printf("%d ", *p);
p++;
}
}
void main()
{
allmyvars(4, 1, 2, 3, 4);
getch();
}
Пример:
#include <stdio.h> // неправильное использование:
void main()
{
printf("\n");
printf("%d\n%d\n%d\n%d\n%d\n", 1, 2, 3);
printf("\n");
printf("%d\n%d\n%d\n%d\n%d\n", 1, 2, 3, 4, 5, 6, 7);
getch();
}
24

25.

5. Функции. Что ещё?..
5.1 Встраиваемые функции
Вызов функции, хоть в си он очень быстрый, отнимает
некоторое время. В современном си есть возможность объявлять
встраиваемые функции. При компиляции вызов функции будет
заменён её телом.
Для объявления встраиваемой функции используется
ключевое слово inline (или __inline, __forceinline в зависимости
от компилятора)
inline функции имеют ряд недостатков:
Во-первых, компилятор может отказать во встраивании
функции, если это снижает скорость выполнения. Снижение
может происходить в том числе и из-за того, что кеш инструкций
будет переполняться. Вообще, inline следует скорее
рассматривать как подсказку компилятору, а не руководство к
действию.
Во-вторых, для встраиваемых систем, в которых разные
функции могут располагаться в разных сегментах памяти, это
недопустимо, так как вызов может произойти не в том сегменте,
в котором ожидалось.
В-третьих, это даёт достаточно малый прирост
производительности, но усложняет процесс сборки, оптимизации
и увеличивает время компиляции. Во время внешнего
связывания (external linkage) также могут возникнуть проблемы,
если функция не была объявлена inline во всех компилируемых
модулях. Поэтому часто встраиваемые функции объявляют также
статическими.
5.2 Рекурсия
Рекурсия — это способ определения множества объектов
через само это множество на основе заданных простых
базовых случаев.
Рекурсивное определение позволяет определить
бесконечное множество объектов с помощью конечного
выражения (высказывания)
Числа Фибоначчи: 1, 1, 2, 3, 5, 8, 13, 21, 34, …
Рекурсивная процедура (функция) — это процедура
(функция), которая вызывает сама себя напрямую или через
другие процедуры и функции.
Пример: Вычисление суммы цифр числа
int sumDig ( int n )
{
int sum;
sum = n %10; // последняя цифра
if ( n >= 10 )
sum += sumDig ( n / 10 ); // рекурсивный вызов
return sum;
}
25

26.

1, N 1
N
!
Факториал:
N ( N 1)!, N 1
int Fact ( int N )
{
int F;
printf ( "-> N=%d\n", N );
if ( N <= 1 )
F = 1;
else F = N * Fact(N - 1);
printf ( "<- N=%d\n", N );
return F;
}
-> N = 3
-> N = 2
-> N = 1
<- N = 1
<- N = 2
<- N = 3
Как работает рекурсия
Стек – область памяти, в которой хранятся локальные
переменные и адреса возврата.
Рекурсия – «за» и «против»:
с каждым новым вызовом расходуется память в стеке
(возможно переполнение стека)
затраты на выполнение служебных операций при
рекурсивном вызове
«+»: программа становится более короткой и понятной
« »: возможно переполнение стека
« »: замедление работы
NB: Любой рекурсивный алгоритм можно заменить
нерекурсивным! (итерационным алгоритмом)
y=f(f(x))
f(f(x))
«Прямой» ход
рекурсии
?
«Обратный» ход
рекурсии
Условие выхода
из рекурсии
26

27.

5.3 Параметры командной строки
После сборки программа представляет собой исполняемый
файл (мы не рассматриваем создание динамических
библиотек, драйверов и т.д.).
Наши программы очень простые и не содержат библиотек
времени выполнения (Runtime libraries), поэтому могут быть
перенесены на компьютер с такой же операционной
системой (и подобной архитектурой) и там запущены.
Программа во время запуска может принимать параметры.
Они являются аргументами функции main().
Общий вид функции main() следующий:
void main(int argc, char **argv)
{
...
}
Первым аргументом argc является число переданных функции
параметров.
Второй аргумент – массив строк – собственно сами
параметры. Так как параметры у функции могут быть любыми,
то они передаются как строки, и уже сама программа должна
их разбирать и приводить к нужному типу.
Первым аргументом (argv[0]) всегда является имя программы.
При этом имя выводится в зависимости от того, откуда была
запущена программа.
#include <stdio.h>
void main(int argc, char *argv[])
{
for(int i=0; i<argc; i++)
printf("%d: %s\n", i, argv[i]);
}
Функция main() может иметь параметры:
int argc
количество параметров командной строки
char *argv[]
массив строк – значений параметров
argv[0] – путь к
исполняемому файлу
/* Программа получает два аргумента-числа и выводит их
сумму */
#include <stdio.h>
#include <stdlib.h>
void main(int argc, char **argv)
{
int a, b;
if (argc != 3)
{
printf("Error: found %d arguments. Needs exactly 2", argc-1);
exit(1);
}
a = atoi(argv[1]); // преобразование строки в число типа int
b = atoi(argv[2]);
printf("%d", a + b);
}
27

28.

5.4 Функциональное программирование
В программировании из множества парадигм выделяют два
больших подхода:
Императивное (ИП)
Функциональное (ФП)
Они существенно отличаются логикой работы, ещё и создают
путаницу в названиях.
Функциональное программирование
Примеры языков: Haskell, Lisp, Erlang, Clojure, F#
Смысл ФП в том, что мы задаём не последовательность
нужных нам команд, а описываем взаимодействие между
ними и подпрограммами.
Это похоже на то, как работают объекты в объектноориентированном программировании (ООП), только здесь это
реализуется на уровне всей программы.
В ФП весь код — это правила работы с данными. Вы просто
задаёте нужные правила, а код сам разбирается, как их
применять.
Если мы сравним принципы ФП с императивным, то
единственное, что совпадёт, — и там, и там есть команды,
которые язык может выполнять. Всё остальное — разное.
Команды можно собирать в подпрограммы, но их
последовательность не имеет значения. Нет разницы, в каком
порядке вы напишете подпрограммы — это же просто правила,
а правила применяются тогда, когда нужно, а не когда про них
сказали.
Переменных нет. Вернее, они есть, но не в том виде, к
которому мы привыкли. В ФП мы можем объявить переменную
только один раз, и после этого значение переменной
измениться не может. Это как константы — записали и всё,
теперь можно только прочитать. Сами же промежуточные
результаты хранятся в функциях — обратившись к нужной, вы
всегда получите искомый результат.
Смысл ФП в том, чтобы описать не сами чёткие шаги к цели, а
правила, по которым компилятор сам должен дойти до нужного
результата.
Последовательность выполнения подпрограмм определяет сам
код и компилятор, а не программист. Каждая команда — это
какое-то правило, поэтому нет разницы, когда мы запишем это
правило, в начале или в конце кода. Главное, чтобы у нас это
правило было, а компилятор сам разберётся, в какой момент
его применять.
28

29.

Функциональное программирование
Функциональное программирование — парадигма
программирования, в которой процесс вычисления
трактуется как вычисление значений функций в
математическом понимании последних (в отличие от
функций как подпрограмм в процедурном программировании).
Функциональное программирование предполагает обходиться
вычислением результатов функций от исходных данных и
результатов других функций, и не предполагает явного хранения
состояния программы.
Соответственно, не предполагает оно и изменяемость этого
состояния (в отличие от императивного, где одной из базовых
концепций является переменная, хранящая своё значение и
позволяющая менять его по мере выполнения алгоритма).
На практике отличие математической функции от понятия
«функции» в императивном программировании заключается в
том, что императивные функции могут опираться не только на
аргументы, но и на состояние внешних по отношению к функции
переменных, а также иметь побочные эффекты и менять
состояние внешних переменных.
Таким образом, в императивном программировании при
вызове одной и той же функции с одинаковыми параметрами,
но на разных этапах выполнения алгоритма, можно получить
разные данные на выходе из-за влияния на функцию состояния
переменных.
А в функциональном языке при вызове функции с одними и
теми же аргументами мы всегда получим одинаковый
результат: выходные данные зависят только от входных.
Это позволяет средам выполнения программ на
функциональных языках кешировать результаты функций и
вызывать их в порядке, не определяемом алгоритмом и
распараллеливать их без каких-либо дополнительных действий
со стороны программиста.
Лямбда-исчисление является основой для функционального
программирования, многие функциональные языки можно
рассматривать как «надстройку» над ним.
Лямбда-исчисление (λ-исчисление) — формальная система,
разработанная американским математиком Алонзо Чёрчем
для формализации и анализа понятия вычислимости.
ФП требует перестройки мышления.
Но стоит ли оно того? Вот мои за и против.
За:
Читаемость кода. Код очень легок в понимании.
Тестируемость. Так как результат функции не зависит от
окружения мы уже имеем все необходимое для тестирования.
Великолепная возможность распараллеливания (самый важный
пункт!). Каждая (каждая!) функция может быть вызвана
несколькими потоками безопасно. Без средств синхронизации.
Против:
Только одна ложка дегтя: производительность по сравнению с
императивным — низкая.
Вывод:
Интуиция подсказывает, что для большинства приложений ФП
окажется вполне конкурентоспособной техникой.
Имея на руках несколько техник (парадигм) программирования,
почему бы не попробовать совместить их?
29

30.

30
English     Русский Правила