Лекция 1. Лекция Функции и модули в языке программирования C
Скачать 57.09 Kb.
|
Лекция 1. Функции и модули в языке программирования C++ Программирование функций. Рекурсивные функции. Функции с переменным количеством параметров. Стандартные функции сортировки и поиска. Определение функций. Функция представляет собой поименованную последовательность инструкций, которая выполняет определенные действия. Функции используются для того, чтобы в теле программы не повторять одни и те же блоки инструкций несколько раз. Например, определим функцию, которая вычисляет сумму двух целых чисел. int add ( int x, int y) { int z; z = x + y; return z; } Как видим, определение функции состоит из двух частей: заголовка и тела, которое представляет собой блок инструкций. Заголовок функции включает тип возвращаемого функцией значения, имя функции и список параметров. В нашем случае функция возвращает значение типа int, имеет имя add и два формальных параметра x и y. Для вычисления суммы внутри функции объявляется переменная z, которая не видна вне функции и поэтому называется локальной переменной. Полученное значение суммы возвращается в вызываемую программу инструкцией return. Эта же функция может быть определена более кратко: int add ( int x, int y) { return x + y; } То есть после ключевого слова return можно использовать выражение. Если функции не возвращает никакого значения, то в языке С считается, что эта функция возвращает значение типа void, то есть пустое значение. В этом случае для выхода из функции используется инструкция return без выражения. Если выход из функции, не возвращающей значения, происходит как выход из блока, то в этом случае инструкцию return можно опустить. Например, следующая функция напечатает сумму двух чисел: void print ( int x, int y) { printf (“x + y = %d\n”, x + y); } Если функция не имеет параметров, то вместо списка параметров пишется ключевое слово void, которое можно также опустить. Предпочтительнее использовать первый вариант, так как он явно указывает, что параметров нет. Например, следующая функция печатает сообщение: void hello ( ) { printf (“Hello.\n”); } Параметры функции можно рассматривать как локальные переменные, которые видны только в теле функции. Отсюда следует, что значения параметров можно изменять в теле функции. Например, можно написать следующую функцию: int inc ( int n) { return ++n; } которая вернет значение n+1. Заметим, что функция int inc ( int n) { return n++; } вернет значение n. Прототипы функций. Для предварительного описания функции может использоваться её объявление или прототип. В отличие от определения функции, прототип содержит только заголовок функции и не включает её тело. Прототипы функций используются в следующих случаях: - функция определена в другом файле, - функция определена после обращения к ней, - функция определена в библиотеке функций, которая подключается к программе на этапе её компоновки или выполнения. Например, функция сложения двух чисел имеет следующий прототип: int add ( int x, int y); При объявлении функции в списке параметров можно указывать только типы параметров, опуская их названия. Например, функцию add можно объявить следующим образом: int add ( int, int); Заметим, что объявление функции заканчивается символом ‘;’ Вызов функции. Для вызова функции в программе пишется имя функции, за которым в скобках следует список аргументов, которые являются переменными и константами, определенными в программе. При этом происходит выполнение тела функции, в котором параметры заменяются значениями аргументов. Важно отметить, что тип аргумента должен соответствовать типу соответствующего параметра или допускать приведение к типу параметра. Кроме того, заметим, что функция должна быть объявлена или определена до своего вызова. Ниже приведена программа, в которой вызывается функция сложения двух чисел. #include int add (int x, int y) { return x + y; } int main() { int z; // вызываем функцию z = add (1, 2); // печатаем результат printf ("z = %d\n", z); // можно вызвать функцию так printf ("1 + 2 = %d\n", add(1, 2)); // можно также и так, но в этом случае непонятно зачем add (1, 2); return 1; } Если функция не имеет параметров, то при её вызове аргументы не указываются, но круглые скобки остаются. В следующей программе показан вызов функции hello. #include void hello ( ) { printf ("Hello.\n"); } int main() { hello(); return 1; } В заключение этого параграфа сделаем два очень важных замечания. Во-первых, аргументы передаются в функцию по значению. То есть значения аргументов переписываются в параметры функции. Так как значение аргумента копируется в тело функции, то сам аргумент изменить в теле функции невозможно. Во-вторых, функция возвращает результат также по значению. То есть значение, вычисленное в инструкции return, копируется в переменную, которая определена в теле вызывающей программы и которой присваивается результат вычисления функции. Рекурсивные функции. В языке программирования С возможно определение рекурсивных функций. Например, определим рекурсивную функцию, которая вычисляет факториал натурального числа. unsigned factorial(unsigned n) { if (!n) return 1; else return n * factorial(n - 1); } Передача аргументов через указатели. Для того чтобы функция могла изменить значения переменных, которые определены в программе, нужно передать в функцию указатели на эти переменные. Например, в следующей программе определена функция, которая увеличивает на единицу значение переменной, определенной в программе. #include void inc(int* x) { ++(*x); } int main() { int x = 1; // вызываем функцию inc(&x); // печатаем результат printf("x = %d\n", x); return 1; } Отметим, что в этом случае в функцию должен передаваться адрес переменной, а не сама переменная. Функции с переменным количеством параметров. В языке программирования С допускается использование функций с переменным количеством параметров. В этом случае в конце списка параметров ставится многоточие (…). При этом предполагается, что в списке параметров присутствует хотя бы один параметр. Примерами функций с переменным количеством параметров являются стандартные функции scanf и printf. Например, можно определить функцию для сложения произвольного количества целых чисел следующим образом: int add_num ( int n, …); // n - количество слагаемых Для обработки функций с переменным количеством параметров используется переменная типа va_list и три макрокоманды va_start ( va_list ap, last_arg); // инициализирует ap, где last_arg – имя последнего параметра // в списке параметров функции перед многоточием va_arg (va_list ap, type); // возвращает следующий аргумент типа type va_end ( va_list ap); // обеспечивает правильную работу инструкции return в функции, // которая использует эти макрокоманды Эта переменная и макрокоманды определены в заголовочном файле Например, определенная ниже функция print распечатывает переменное количество целых чисел, которые передаются ей как параметры. #include #include int print(int n, ...) { va_list a; // проверяем, есть ли параметры if (n < 1) { printf("There are no numbers.\n"); return 0; } printf("Number of parameters = %d\n", n); va_start(a, n); // инициализируем ‘a’ последним заданным аргументом while (n) // распечатываем аргументы { printf("%d\n", va_arg(a, int)); --n; } va_end(a); // завершение продвижения по аргументам return 1; } int main() { print(0); print(1, 1); print(2, 10, 20); print(3, 100, 200, 300); return 1; } Указатели на функции. Имя функции является адресом этой функции в памяти. Язык программирования С позволяет определять указатели на функции. Например, следующая инструкция int (*fp)(int x, int y); объявляет переменную fp, которая является указателем на функцию. Причем эта функция должна иметь два параметра типа int и возвращать значение также типа int. В следующей программе функции сложения и вычитания двух чисел вызываются через указатель. #include int add(int x, int y) { return x + y; } int sub(int x, int y) { return x - y; } int main() { int (*p)(int, int); p = add; // вызываем функцию через указатель printf("1 + 2 = %d\n", (*p)(1, 2)); p = sub; // можно вызвать функцию и так printf("1 - 2 = %d\n", p(1, 2)); return 1; } Так как указателю на функцию могут присваиваться адреса различных функций при условии совпадения типа параметров и типа возвращаемого значения, то указатели на функции часто называют функторами. Вызов стандартных функций сортировки и поиска. На практике при работе с данными часто встречаются задачи сортировки массива и поиска элементов в отсортированном массиве. Причем массивы могут иметь разные типы. Чтобы облегчить решение таких задач, в стандартной библиотеке языка программирования С есть специальные функции qsort и bsearch, которые предназначены для решения этих задач. Функция qsort выполняют сортировку массива, элементы которого имеют произвольный тип. Эта функция имеет следующий прототип: void qsort ( void *base, size_t nelem, size_t size, int (*cmp) (const void *e1, const void *e2)); который описан в заголовочном файле base адрес массива, nelem количество элементов в массиве, size длина элемента в массиве, comp указатель на функцию сравнения, которая возвращает: - отрицательное число, если элемент e1 меньше элемента e2, - 0, если элемент e1 равен элементу e2, - положительное число, если элемент e1 больше элемента e2. Функция qsort реализует «быстрый алгоритм» сортировки массивов. Функция bsearch выполняет бинарный поиск элемента в отсортированном массиве. Эта функция имеет следующий прототип: void* bsearch (const void *key, const void *base, size_t nelem, size_t size, int (*cmp) (const void *ck, const void *ce); который также описан в заголовочном файле В следующей программе показан пример использования функций qsort и bsearch для сортировки целочисленного массива и дальнейшего поиска элементов в этом отсортированном массиве. #include #include // функция для сравнения элементов массива int comp_int(const int* e1, const int* e2) { return (*e1 < *e2) ? -1 : ((*e1 == *e2) ? 0 : 1); } // программа сортировки элементов массива и поиска целого числа int main() { int n; // размер массива int* a; // массив int i; // индекс int k; // число для поиска int* s; // адрес найденного числа printf("Input an array size: "); scanf("%d", &n); a = (int*)malloc(n*sizeof(int)); // вводим массив printf("Input elements: "); for (i = 0; i < n; ++i) scanf("%d", &a[i]); // сортируем массив qsort(a, n, sizeof(int), (int (*)(const void*, const void*))comp_int); // выводим отсортированный массив printf("The sorted array: "); for (i = 0; i < n; ++i) printf("%d ", a[i]); printf("\n"); // вводим число для поиска printf("Input a number to search.\n>"); scanf("%d", &k); // ищем это число в отсортированном массиве if(!(s = (int*) bsearch(&k, a, n, sizeof(int), (int (*)(const void*, const void*))comp_int))) printf("There is no such an integer.\n"); else printf("The integer index = %d.\n", s-a); free(a); return 1; } Модули. Структура модулей. Подключение модулей. Понятие модульности Модульность это свойство системы, которая была разложена на внутренне связных, но слабо связанные между собой модули. В традиционном структурном проектировании модульность - это искусство раскладывать подпрограммы за кучками так, чтобы к одной кучке попадали подпрограммы, которые используют друг друга или что изменяются вместе. В объектно-ориентированном программировании ситуация несколько другая: необходимо физически разделить классы и объекты, которые составляют логическую структуру проекта. На основе имеющегося опыта можно перечислить приемы и правила, что позволяют составлять модули из классов и объектов наиболее эффективным чином. Бритон и Парнас считают, что "конечной целью декомпозиции программы на модуле является снижение расходов на программирование за счет независимой разработки и тестирования. Структура модуля должна быть достаточно простою для восприятия; реализация каждого модуля не должна зависеть от реализации других модулей; должны быть приняты меры для облегчения процесса внесения изменений там, где они наиболее вероятны". Прагматичные рассуждения ставят предел этим ведущим указаниям. На практике перекомпиляция тела модуля не является трудоемкой операцией: заново компилируется лишь данный модуль, и программа перекомпонуется. Перекомпиляция интерфейсной части модуля, напротив, более трудоемкая. В строго типизирующих языках придется перекомпилировать интерфейс и тело самого измененного модуля, потом все модули, связанные из данным, модули, связанные с ними, и так далее за цепочкой. В итоге, для очень больших программ могут понадобиться много часов на перекомпиляцию (если только среда разработки не поддерживает фрагментарную компиляцию), что явно нежелательно. Поэтому стоит стремиться к тому, чтобы интерфейсная часть модулей была возможно уже (в пределах обеспечения необходимых связей). Наш стиль программирования требует спрятать все, что только возможно, в реализации модуля. Постепенный перенос описаний из реализации в интерфейсную часть намного менее опасный, чем "вычистка" избыточного интерфейсного кода. Таким образом, программист должен находить баланс между двумя противоположными тенденциями: стремлениям спрятать информацию и необходимостью обеспечения видимости тех или других абстракций в нескольких модулях. Парнас, Клеменс и Вейс, предложили следующее правило: "Особенности системы, которые подлежат изменениям, стоит скрывать в отдельных модулях; в качестве межмодульных можно использовать лишь те элементы, вероятность изменения которых имела. Все структуры данных должны быть обособлены в модуле; доступ к ним будет возможен для всех процедур этого модуля и закрыт для всех других. Доступ к данным из модуля должен осуществляться лишь через процедуры данного модуля". Иначе говоря, стоит стремиться построить модули так, чтобы объединить логично связаны абстракции и минимизировать взаимные связки между модулями. В разных языках программирования модульность поддерживается по-разному. Например, в С++ модулями являются файлы, которые компилируются отдельно. Для С/с++ традиционным является размещение интерфейсной части модулей в отдельные файлы с расширением .п (так называемые файлы-заглавия). Реализация, то есть текст модуля, сохраняется в файлах с расширением .с (в программах на С++ часто используются расширения .ее, .ср и .срр). Связь между файлами объявляется директивой макропроцессору #include. Такой подход строится исключительно на соглашении и не является строгим требованием самого языка. В языке Object Pascal принцип модульности формализированный немного строже. В этом языке определен особенный синтаксис для интерфейсной части и реализации модуля (unit). Язык Ada идет еще на шаг дальше: модуль (что называется package) также имеет две части - спецификацию и тело. Но, в отличие от Object Pascal, допускается раздельное определение связей с модулями для спецификации и тела пакета. Таким образом, допускается, чтобы тело модуля мало связки с модулями, невидимыми для его спецификации. Со временем при в проектировании программ акцент сместился с организации процедур на организацию структур данных. Помимо всего прочего это вызвано и ростом размеров программ. Модулем обычно называют совокупность связанных процедур и тех данных, которыми они управляют. Парадигма программирования приобрела вид: Определите, какие модули нужны; поделите программу так, чтобы данные были скрыты в этих модулях Эта парадигма известна также как "принцип сокрытия данных". Если в языке нет возможности сгруппировать связанные процедуры вместе с данными, то он плохо поддерживает модульный стиль программирования. Теперь метод написания "хороших" процедур применяется для отдельных процедур модуля. Поскольку данные есть единственная вещь, которую хотят скрывать, понятие упрятывания данных тривиально расширяется до понятия упрятывания информации, т.е. имен переменных, констант, функций и типов, которые тоже могут быть локальными в модуле. Хотя С++ и не предназначался специально для поддержки модульного программирования, классы поддерживают концепцию модульности). Помимо этого С++, естественно, имеет уже продемонстрированные возможности модульности, которые есть в С, т.е. представление модуля как отдельной единицы трансляции. Пример модуля. Способ использования модуля. Массивы в языке С++. Связь указателей и массивов в С++. При использовании простых переменных каждой области памяти для хранения данных соответствует свое имя. Если с группой величин одинакового типа требуется выполнять однообразные действия, им дают одно имя, а различают по порядковому номеру. Это позволяет компактно записывать множество операций с помощью циклов. Конечная именованная последовательность однотипных величин называется массивом. Описание массива в программе отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива (размерность): float а [10]; // описание массива из 10 вещественных чисел Элементы массива нумеруются с нуля. При описании массива используются те же модификаторы (класс памяти, const и инициализатор), что и для простых переменных. Инициализирующие значения для массивов записываются в фигурных скобках. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не указаны, обнуляются: int b[5] = {3, 2, 1}; / / b[0]=3, b[l]=2, b[2]=l, b[3]=0, b[4]=0 Размерность массива вместе с типом его элементов определяет объем памяти, необходимый для размещения массива, которое выполняется на этапе компиляции, поэтому размерность может быть задана только целой положительной константой или константным выражением. Если при описании массива не указана размерность, должен присутствовать инициализатор, в этом случае компилятор выделит память по количеству инициализирующих значений. В дальнейшем мы увидим, что размерность может быть опущена также в списке формальных параметров. Внимание При описании массивов квадратные скобки являются элементом синтаксиса, а не указанием на необязательность конструкции. Для доступа к элементу массива после его имени указывается номер элемента (индекс) в квадратных скобках. В следующем примере подсчитывается сумма элементов массива. #include int main(){ const int n = 10; int i,. sum; int marks[n] = {3, 4, 5, 4, 4}; for (i = 0, sum = 0; i cout << "Сумма элементов: " << sum; return 0; } Размерность массивов предпочтительнее задавать с помощью именованных констант, как это сделано в примере, поскольку при таком подходе для ее изменения достаточно скорректировать значение константы всего лишь в одном месте программы. Обратите внимание, что последний элемент массива имеет номер, на единицу меньший заданной при его описании размерности. Внимание При обращении к элементам массива автоматический контроль выхода индекса за границу массива не производится, что может привести к ошибкам. Пример. Сортировка целочисленного массива методом выбора. Алгоритм состоит в том, что выбирается наименьший элемент массива и меняется местами с первым элементом, затем рассматриваются элементы, начиная со второго, и наименьший из них меняется местами со вторым элементом, и так далее n-1 раз (при последнем проходе цикла при необходимости меняются местами предпоследний и последний элементы массива). #include int main(){ const int n = 20; // количество элементов массива int b[n]; // описание массива int i; for (i = 0; i> b[i]; // ввод массива for (i = 0; i // принимаем за наименьший первый из рассматриваемых элементов: int imin = 1; // поиск номера минимального элемента из неупорядоченных: for (int j = i + 1; j // если нашли меньший элемент, запоминаем его номер: if (b[j] < b[imin]) imin = j; int a = b[i]; // обмен элементов b[i] = b[imin]; // с номерами b[imin] = a; // i и imin } // вывод упорядоченного массива: for (i = 0; 1 return 0; } Идентификатор массива является константным указателем на его нулевой элемент. Например, для массива из предыдущего листинга имя b — это то же самое, что &Ь[0], а к i-му элементу массива можно обратиться, используя выражение *(b+1). Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель. Следующий фрагмент программы копирует все элементы массива а в массив b: int а[100], b[100]: int *ра = а; // или int *р = &а[0]; int *pb = b; for (int i = 0; i<100; i++) *pb++ =>*pa++; // или pb[1] = pa[1]; Модульное программирование — это организация программы как совокупности небольших независимых блоков, называемых модулями. Модуль — функционально законченный фрагмент программы, оформленный в виде отдельного файла с исходным кодом. Модули проектируются таким образом, чтобы предоставлять программистам удобную для многократного использования функциональность (интерфейс) в виде набора функций, классов, констант. Модули могут объединяться в пакеты и, далее, в библиотеки. Удобство использования модульной архитектуры заключается в возможности обновления (замены) модуля, без необходимости изменения остальной системы. Использование модульного программирования позволяет упростить тестирование программы и обнаружение ошибок. Модульность часто является средством упрощения задачи проектирования программы и распределения процесса разработки между группами разработчиков. При разбиении программы на модули для каждого модуля указывается реализуемая им функциональность, а также связи с другими модулями. Выделение функций в модуль Модуль в языке Си состоит из интерфейса (заголовочого файла .h) и реализации (файла .c). Код, подключающий модуль, на этапе компиляции нуждается только в интерфейсе модуля, поэтому на этапе препроцессинга заголовочный файл копируется в код директивой #include "somelib.h" . Реализация модуля должна полностью реализовывать указанный интерфейс, поэтому она также включает свой заголовочный файл. Итого, пример проекта из основного файла и одного модуля, может выглядеть так: //main.c #include #include "hello.h" int main() { hello_world(); return EXIT_SUCCESS; } //hello.h #ifndef HELLO_H #define HELLO_H void hello_world(); #endif //HELLO_H //hello.c #include "hello.h" #include void hello_world() { printf("Hello, World!\n"); } Замечание В данном примере в файле main.c не понадобилось подключать stdio.h, хотя он и используется в модуле hello.c. Причина этого в том, что никакие типы из stdio.h не нужны для корректной обработки интерфейса hello.h, оказывающегося в main.c на этапе компиляции. Если бы определения из какой-то библиотеки были необходимы для обработки интерфейса модуля, эти библиотеки должны были бы быть включены не в hello.c, а в hello.h, чтобы во всех местах, где подключается модуль hello, не возникало ошибок компиляции, так как эта библиотека автоматически подключена. |