Скачиваний:
2
Добавлен:
03.01.2024
Размер:
2.1 Mб
Скачать

Передача динамического массива в качестве аргумента

Если двумерный массив создан динамически, то можно передавать указатель на указатель.

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

Можно вместо того, чтобы возвращать указатель на массив, передавать массив, который необходимо заполнить:

#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

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

Функции с переменным числом параметров

(Продолжение) См. функцию 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

Функции с переменным числом параметров. Неправильное использование

Функции 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

5. Функции. Что ещё?..

5.1 Встраиваемые функции

 

5.2 Рекурсия

Вызов функции, хоть в си он очень быстрый, отнимает

Рекурсия — это способ определения множества объектов

некоторое время. В современном си есть возможность объявлять

 

 

через само это множество на основе заданных простых

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

 

 

базовых случаев.

заменён её телом.

 

Рекурсивное определение позволяет определить

Для объявления встраиваемой функции используется

 

бесконечное множество объектов с помощью конечного

ключевое слово inline (или __inline, __forceinline в зависимости

 

 

выражения (высказывания)

от компилятора)

 

 

 

inline функции имеют ряд недостатков:

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

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

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

Числа Фибоначчи: 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

 

 

N 1

 

 

 

1,

 

 

Факториал:

N!

 

 

 

 

 

N (N 1)!,

N 1

 

 

 

 

 

 

int Fact ( int N )

 

 

 

 

 

{

 

 

-> N = 3

 

int F;

 

 

 

 

 

-> N = 2

 

printf ( "-> N=%d\n", N );

 

 

 

-> N = 1

 

if ( N <= 1 )

 

 

 

F = 1;

 

 

<- N = 1

 

else F = N * Fact(N - 1);

 

 

 

<- N = 2

 

printf ( "<- N=%d\n", N );

 

 

 

<- N = 3

 

return F;

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Как работает рекурсия

Стек – область памяти, в которой хранятся локальные переменные и адреса возврата.

Рекурсия – «за» и «против»:

с каждым новым вызовом расходуется память в стеке (возможно переполнение стека)

затраты на выполнение служебных операций при рекурсивном вызове

«+»: программа становится более короткой и понятной « »: возможно переполнение стека « »: замедление работы

NB: Любой рекурсивный алгоритм можно заменить нерекурсивным! (итерационным алгоритмом)

f(f(x))

«Прямой» ход рекурсии

?

Условие выхода из рекурсии

y=f(f(x))

«Обратный» ход рекурсии

26

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

5.4 Функциональное программирование

В программировании из множества парадигм выделяют два больших подхода:

Императивное (ИП)

Функциональное (ФП)

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

Функциональное программирование

Примеры языков: Haskell, Lisp, Erlang, Clojure, F#

Смысл ФП в том, что мы задаём не последовательность нужных нам команд, а описываем взаимодействие между ними и подпрограммами.

Это похоже на то, как работают объекты в объектноориентированном программировании (ООП), только здесь это реализуется на уровне всей программы.

В ФП весь код — это правила работы с данными. Вы просто

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

Если мы сравним принципы ФП с императивным, то единственное, что совпадёт, — и там, и там есть команды, которые язык может выполнять. Всё остальное — разное.

Команды можно собирать в подпрограммы, но их последовательность не имеет значения. Нет разницы, в каком порядке вы напишете подпрограммы — это же просто правила, а правила применяются тогда, когда нужно, а не когда про них сказали.

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

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

Последовательность выполнения подпрограмм определяет сам код и компилятор, а не программист. Каждая команда — это какое-то правило, поэтому нет разницы, когда мы запишем это правило, в начале или в конце кода. Главное, чтобы у нас это правило было, а компилятор сам разберётся, в какой момент его применять.

28

Функциональное программирование

Функциональное программирование — парадигма

программирования, в которой процесс вычисления

трактуется как вычисление значений функций в

математическом понимании последних (в отличие от

функций как подпрограмм в процедурном программировании).

Функциональное программирование предполагает обходиться

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

Соответственно, не предполагает оно и изменяемость этого

состояния (в отличие от императивного, где одной из базовых

концепций является переменная, хранящая своё значение и

позволяющая менять его по мере выполнения алгоритма).

На практике отличие математической функции от понятия

«функции» в императивном программировании заключается в том, что императивные функции могут опираться не только на

аргументы, но и на состояние внешних по отношению к функции переменных, а также иметь побочные эффекты и менять

состояние внешних переменных.

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

разные данные на выходе из-за влияния на функцию состояния переменных.

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

Это позволяет средам выполнения программ на функциональных языках кешировать результаты функций и вызывать их в порядке, не определяемом алгоритмом и

распараллеливать их без каких-либо дополнительных действий

со стороны программиста.

Лямбда-исчисление является основой для функционального

программирования, многие функциональные языки можно

рассматривать как «надстройку» над ним.

Лямбда-исчисление (λ-исчисление) — формальная система,

разработанная американским математиком Алонзо Чёрчем для формализации и анализа понятия вычислимости.

ФП требует перестройки мышления.

Но стоит ли оно того? Вот мои за и против.

За:

Читаемость кода. Код очень легок в понимании.

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

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

пункт!). Каждая (каждая!) функция может быть вызвана несколькими потоками безопасно. Без средств синхронизации.

Против:

Только одна ложка дегтя: производительность по сравнению с

императивным — низкая.

Вывод:

Интуиция подсказывает, что для большинства приложений ФП

окажется вполне конкурентоспособной техникой.

Имея на руках несколько техник (парадигм) программирования, почему бы не попробовать совместить их?

29

30

Соседние файлы в папке Лекции