В программы на языке Си можно передавать некоторые аргументы. Когда вначале вычислений производится обращение к main(), ей передаются три параметра. Первый из них определяет число командных аргументов при обращении к программе. Второй представляет собой массив указателей на символьные строки, содержащие эти аргументы (в одной строке - один аргумент). Третий тоже является массивом указателей на символьные строки, он используется для доступа к параметрам операционной системы (к переменным окружения).
Любая такая строка представляется в виде:
переменная = значение\0
Последнюю строку можно найти по двум заключительным нулям.
Назовем аргументы функции main() соответственно: argc, argv и env (возможны и любые другие имена). Тогда допустимы следующие описания:
main(int argc, char *argv)
main(int argc, char *argv, char *env)
Предположим, что на диске A: есть некоторая программа prog.exe. Обратимся к ней следующим образом:
A:\>prog.exe
file1 file2 file3
Тогда argv - это указатель на строку A:\prog.exe, argv - на строку file1 и т.д. На первый фактический аргумент указывает argv, а на последний - argv. Если argc=1, то после имени программы в командной строке параметров нет. В нашем примере argc=4.
Рекурсия
Рекурсией называется такой способ вызова, при котором функция обращается к самой себе.
Важным моментом при составлении рекурсивной программы является организация выхода. Здесь легко допустить ошибку, заключающуюся в том, что функция будет последовательно вызывать саму себя бесконечно долго. Поэтому рекурсивный процесс должен шаг за шагом так упрощать задачу, чтобы в конце концов для нее появилось не рекурсивное решение. Использование рекурсии не всегда желательно, так как это может привести к переполнению стека.
Библиотечные функции
В системах программирования подпрограммы для решения часто встречающихся задач объединяются в библиотеки. К числу таких задач относятся: вычисление математических функций, ввод/вывод данных, обработка строк, взаимодействие со средствами операционной системы и др. Использование библиотечных подпрограмм избавляет пользователя от необходимости разработки соответствующих средств и предоставляет ему дополнительный сервис. Включенные в библиотеки функции поставляются вместе с системой программирования. Их объявления даны в файлах *.h (это так называемые включаемые или заголовочные файлы). Поэтому, как уже упоминалось выше, в начале программы с библиотечными функциями должны быть строки вида:
#include <включаемый_файл_типа_h>
Например:
#include
Существуют также средства для расширения и создания новых библиотек с программами пользователя.
Для глобальных переменных отводится фиксированное место в памяти на все время работы программы. Локальные переменные хранятся в стеке. Между ними находится область памяти для динамического распределения.
Функции malloc() и free() используются для динамического распределения свободной памяти. Функция malloc() выделяет память, функция free() освобождает ее. Прототипы этих функций хранятся в заголовочном файле stdlib.h и имеют вид:
void *malloc(size_t size);
void *free(void *p);
Функция malloc() возвращает указатель типа void; для правильного использования значение функции надо преобразовать к указателю на соответствующий тип. При успешном выполнении функция возвращает указатель на первый байт свободной памяти размера size. Если достаточного количества памяти нет, возвращается значение 0. Чтобы определить количество байтов, необходимых для переменной, используют операцию sizeof().
Пример использования этих функций:
#include
#include
p = (int *) malloc(100 * sizeof(int)); /* Выделение памяти для 100
целых чисел */
printf("Недостаточно памяти\n");
for (i = 0; i < 100; ++i) *(p+i) = i; /* Использование памяти */
for (i = 0; i < 100; ++i) printf("%d", *(p++));
free(p); /* Освобождение памяти */
Перед использованием указателя, возвращаемого malloc(), необходимо убедиться, что памяти достаточно (указатель не нулевой).
Препроцессор
Препроцессор Си - это программа, которая обрабатывает входные данные для компилятора. Препроцессор просматривает исходную программу и выполняет следующие действия: подключает к ней заданные файлы, осуществляет подстановки, а также управляет условиями компиляции. Для препроцессора предназначены строки программы, начинающиеся с символа #. В одной строке разрешается записывать только одну команду (директиву препроцессора).
Директива
#define идентификатор подстановка
вызывает замену в последующем тексте программы названного идентификатора на текст подстановки (обратите внимание на отсутствие точки с запятой в конце этой команды). По существу, эта директива вводит макроопределение (макрос), где "идентификатор" - это имя макроопределения, а "подстановка" - последовательность символов, на которые препроцессор заменяет указанное имя, когда находит его в тексте программы. Имя макроопределения принято набирать прописными буквами.
Рассмотрим примеры:
Первая строка вызывает замену в программе идентификатора MAX на константу 25. Вторая позволяет использовать в тексте вместо открывающей фигурной скобки ({) слово BEGIN.
Отметим, что поскольку препроцессор не проверяет совместимость между символическими именами макроопределений и контекстом, в котором они используются, то рекомендуется такого рода идентификаторы определять не директивой #define, а с помощью ключевого слова const с явным указанием типа (это в большей степени относится к Си++):
const int MAX = 25;
(тип int можно не указывать, так как он устанавливается по умолчанию).
Если директива #define имеет вид:
#define идентификатор(идентификатор, ..., идентификатор) подстановка
причем между первым идентификатором и открывающей круглой скобкой нет пробела, то это определение макроподстановки с аргументами. Например, после появления строки вида:
#define READ(val) scanf("%d", &val)
оператор READ(y); воспринимается так же, как scanf("%d",&y);. Здесь val - аргумент и выполнена макроподстановка с аргументом.
При наличии длинных определений в подстановке, продолжающихся в следующей строке, в конце очередной строки с продолжением ставится символ \.
В макроопределение можно помещать объекты, разделенные знаками ##, например:
#define PR(x, у) x##y
После этого PR(а, 3) вызовет подстановку а3. Или, например, макроопределение
#define z(a, b, c, d) a(b##c##d)
приведет к замене z(sin, x, +, y) на sin(x+y).
Символ #, помещаемый перед макроаргументом, указывает на преобразование его в строку. Например, после директивы
#define PRIM(var) printf(#var"= %d", var)
следующий фрагмент текста программы
преобразуется так:
printf("year""= %d", year);
Опишем другие директивы препроцессора. Директива #include уже встречалась ранее. Ее можно использовать в двух формах:
#include "имя файла"
#include <имя файла>
Действие обеих команд сводится к включению в программу файлов с указанным именем. Первая из них загружает файл из текущего или заданного в качестве префикса каталога. Вторая команда осуществляет поиск файла в стандартных местах, определенных в системе программирования. Если файл, имя которого записано в двойных кавычках, не найден в указанном каталоге, то поиск будет продолжен в подкаталогах, заданных для команды #include <...>. Директивы #include могут вкладываться одна в другую.
Следующая группа директив позволяет избирательно компилировать части программы. Этот процесс называется условной компиляцией. В эту группу входят директивы #if, #else, #elif, #endif, #ifdef, #ifndef. Основная форма записи директивы #if имеет вид:
#if константное_выражение последовательность_операторов
Здесь проверяется значение константного выражения. Если оно истинно, то выполняется заданная последовательность операторов, а если ложно, то эта последовательность операторов пропускается.
Действие директивы #else подобно действию команды else в языке Си, например:
#if константное_выражение
последовательность_операторов_2
Здесь если константное выражение истинно, то выполняется последовательность_операторов_1, а если ложно - последовательность_операторов_2.
Директива #elif означает действие типа "else if". Основная форма ее использования имеет вид:
#if константное_выражение
последовательность_операторов
#elif константное_выражение_1
последовательность_операторов_1
#elif константное_выражение_n
последовательность_операторов_n
Эта форма подобна конструкции языка Си вида: if...else if...else if...
Директива
#ifdef идентификатор
устанавливает определен ли в данный момент указанный идентификатор, т.е. входил ли он в директивы вида #define. Строка вида
#ifndef идентификатор
проверяет является ли неопределенным в данный момент указанный идентификатор. За любой из этих директив может следовать произвольное число строк текста, возможно, содержащих инструкцию #else (#elif использовать нельзя) и заканчивающихся строкой #endif. Если проверяемое условие истинно, то игнорируются все строки между #else и #endif, а если ложно, то строки между проверкой и #else (если слова #else нет, то #endif). Директивы #if и #ifndef могут "вкладываться" одна в другую.
Директива вида
#undef идентификатор
приводит к тому, что указанный идентификатор начинает считаться неопределенным, т.е. не подлежащим замене.
Рассмотрим примеры. Три следующие директивы:
проверяют определен ли идентификатор WRITE (т.е. была ли команда вида #define WRITE...), и если это так, то имя WRITE начинает считаться неопределенным, т.е. не подлежащим замене.
Директивы
#define WRITE fprintf
проверяют является ли идентификатор WRITE неопределенным, и если это так, то определятся идентификатор WRITE вместо имени fprintf.
Директива #error записывается в следующей форме:
#error сообщение_об_ошибке
Если она встречается в тексте программы, то компиляция прекращается и на экран дисплея выводится сообщение об ошибке. Эта команда в основном применяется на этапе отладки. Заметим, что сообщение об ошибке не надо заключать в двойные кавычки.
Директива #line предназначена для изменения значений переменных _LINE_ и _FILE_, определенных в системе программирования Си. Переменная _LINE_ содержит номер строки программы, выполняемой в текущий момент времени. Идентификатор _FILE_ является указателем на строку с именем компилируемой программы. Директива #line записывается следующим образом:
#line номер "имя_файла"
Здесь номер - это любое положительное целое число, которое будет назначено переменной _LINE_, имя_файла - это необязательный параметр, который переопределяет значение _FILE_.
Директива #pragma позволяет передать компилятору некоторые указания. Например, строка
говорит о том, что в программе на языке Си имеются строки на языке ассемблера. Например:
Рассмотрим некоторые глобальные идентификаторы или макроимена (имена макроопределений). Определены пять таких имен: _LINE_, _FILE_, _DATE_, _TIME_, _STDC_. Два из них (_LINE_ и _FILE_) уже описывались выше. Идентификатор _DATE_ определяет строку, в которой сохраняется дата трансляции исходного файла в объектный код. Идентификатор _TIME_ задает строку, сохраняющую время трансляции исходного файла в объектный код. Макрос _STDC_ имеет значение 1, если используются стандартно - определенные макроимена. В противном случае эта переменная не будет определена.
При автоматизированном создании консольного приложения в языке программирования С++, автоматически создается главная функция очень похожая на эту:
int
main(int
argc, char
* argv)
{…}
Заголовок функции содержит сигнатуру главной функции main()
с аргументами argс
и argv
.
Если программу запускать через командную строку, то существует возможность передать какую-либо информацию этой программе. Для этого существуют аргументы командной строки argc
и argv
.
Параметр argc
имеет тип int
, и содержит количество параметров, передаваемых в функцию main
. Причем argc
всегда не меньше 1, даже когда функции main
не передается никакой информации, так как первым параметром считается имя приложения.
Параметр argv
представляет собой массив указателей на строки. Через командную строку можно передать только данные строкового типа.
При запуске программы через командную строку Windows можно передавать ей некоторую информацию. При этом командная строка будет иметь вид:
Диск:\путь\имя.exe аргумент1 аргумент2 …
Аргументы командной строки разделяются одним или несколькими пробелами.
Аргумент argv содержит полное имя приложения:
#include
using namespace
std;
cout << argv << endl;
Return
0;
}
Результат выполнения
Пример
: вычисление произведения двух целых чисел
В программе используется функция преобразования строки в целое число StrToInt()
отсюда .
#include
using namespace
std;
int
StrToInt(char
*s) {…}
int
main(int
argc, char
* argv) {
Int a = 0, b=0;
If (argc > 1)
a = StrToInt(argv);
If (argc > 2)
b = StrToInt(argv);
cout << a <<«*» << b << «= « << a*b << endl;
Return
0;
}
Запуск программы осуществляется как
Результат выполнения
Отладка программы с аргументами командной строки
Для передачи аргументов командной строки при отладке программы необходимо обратиться к меню Свойства
проекта.
На вкладке Свойства конфигурации ->Отладка
выбрать Аргументы команды
и задать их значения.
При запуске программы в режиме отладки введенные аргументы будут восприниматься программой как аргументы командной строки.
Пожалуйста, приостановите работу AdBlock на этом сайте.
Итак, зачем нужны пользовательские функции? Пользовательские функции нужны для того, чтобы программистам было проще писать программы.
Помните, мы говорили о парадигмах программирования, а точнее о структурном программировании. Основной идеей там было то, что любую программу можно можно написать используя только три основных конструкции: следование, условие и цикл. Теперь к этим конструкциям мы добавим ещё одну – «подпрограммы» – и получим новую парадигму процедурное программирование» .
Отличие лишь в том, что отдельные кусочки нашей основной программы (в частности, повторяющиеся) мы будем записывать в виде отдельных функций (подпрограмм, процедур) и по мере необходимости их вызывать. По сути, программа теперь будет описывать взаимодействие различных функций.
Итак, в этом уроке мы подробно обсудим то, как функции устроены изнутри. А также научимся создавать свои собственные пользовательские функции.
Как устроены функции
Вспомним информацию с первого урока. Все функции, в том числе и те, которые пишет пользователь, устроены сходным образом. У них имеется две основных составных части: заголовок функции и тело функции.
Листинг 1.
Int main(void){ // заголовок функции // в фигурных скобках записано тело функции }
С телом функции всё ясно: там описывается алгоритм работы функции. Давайте разберёмся с заголовком. Он состоит из трёх обязательных частей:
- тип возвращаемого значения;
- имя функции;
- аргументы функции.
Сначала записывается тип возвращаемого значения, например, int
, как в функции main
. Если функция не должна возвращать никакое значение в программу, то на этом месте пишется ключевое слово void
. Казалось бы, что раз функция ничего не возвращает, то и не нужно ничего писать. Раньше, кстати, в языке Си так и было сделано, но потом для единообразия всё-таки добавили. Сейчас современные компиляторы будут выдавать предупреждения/ошибки, если вы не укажете тип возвращаемого значения.
В некоторых языках программирования функции, которые не возвращают никакого значения, называют процедурами (например, pascal). Более того, для создания функций и процедур предусмотрен различный синтаксис. В языке Си такой дискриминации нет.
После типа возвращаемого значения записывается имя функции. Ну а уж после имени указываются типы и количество аргументов, которые передаются в функцию.
Давайте посмотрим на заголовки уже знакомых нам функций.
Листинг 2.
// функция с именем srand, принимающая целое число, ничего не возвращает void srand(int) //функция с именем sqrt, принимающая вещественное число типа float, возвращает вещественное число типа float float sqrt(float) //функция с именем rand, которая не принимает аргументов, возвращает целое число int rand(void) //функция с именем pow, принимающая два аргумента типа double, возвращает вещественное число типа double double pow(double, double)
Как создать свою функцию
Для того чтобы создать свою функцию, необходимо её полностью описать. Тут действует общее правило: прежде чем использовать – объяви и опиши, как должно работать. Для этого вернёмся к схеме структуры программы на языке Си, которая у нас была в самом первом уроке. Отметим на ней те места, где можно описывать функции.
Рис.1 Уточнение структуры программы. Объявление функций.
Как видите, имеется аж два места, где это можно сделать.
Давайте посмотрим на пример, который иллюстрируют создание пользовательской функции вычисления максимального из двух чисел.
Листинг 3.
#include
Давайте я подробно опишу, как будет работать эта программа. Выполняется тело функции main . Создются целые переменные x , y и m . В переменные x и y считываются данные с клавиатуры. Допустим мы ввели 3 5 , тогда x = 3 , y = 5 . Это вам всё и так должно быть понятно. Теперь следующая строчка
Листинг 4.
M = max_num(x,y);
Переменной m надо присвоить то, что находится справа от знака = . Там у нас указано имя функции, которую мы создали сами. Компьютер ищет объявление и описание этой функции. Оно находится выше. Согласно этому объявлению данная функция должна принять два целочисленных значения. В нашем случае это значения, записанные в переменных x и y . Т.е. числа 3 и 5 . Обратите внимание, что в функцию передаются не сами переменные x и y , а только значения (два числа), которые в них хранятся. То, что на самом деле передаётся в функцию при её вызове в программе, называется фактическими параметрами функции.
Теперь начинает выполняться функция max_num . Первым делом для каждого параметра, описанного в заголовке функции, создается отдельная временная переменная. В нашем случае создаются две целочисленных переменных с именами a и b . Этим переменным присваиваются значения фактических параметров. Сами же параметры, описанные в заголовке функции, называются формальными параметрами. Итак, формальным параметрам a и b присваиваются значения фактических параметров 3 и 5 соответственно. Теперь a = 3 , b = 5 . Дальше внутри функции мы можем работать с этими переменными так, как будто они обычные переменные.
Создаётся целочисленная переменная с именем max , ей присваивается значение b . Дальше проверяется условие a > b . Если оно истинно, то значение в переменной max следует заменить на a .
Далее следует оператор return , который возвращает в вызывающую программу (функцию main ) значение, записанное в переменной max , т.е. 5 . После чего переменные a , b и max удаляются из памяти. А мы возвращаемся к строке
Листинг 5.
M = max_num(x,y);
Функция max_num вернула значение 5 , значит теперь справа от знака = записано 5 . Это значение записывается в переменную m. Дальше на экран выводится строчка, и программа завершается.
Внимательно прочитайте последние 4 абазаца ещё раз, чтобы до конца уяснить, как работает программа.
А я пока расскажу, зачем нужен нижний блок описания функций. Представьте себе, что в вашей программе вы написали 20 небольших функций. И все они описаны перед функцией main . Не очень-то удобно добираться до основной программы так долго. Чтобы решить эту проблему, функции можно описывать в нижнем блоке.
Но просто так перенести туда полностью код функции не удастся, т.к. тогда нарушится правило: прежде чем что-то использовать, необходимо это объявить. Чтобы избежать подобной проблемы, необходимо использовать прототип функции.
Прототип функции полностью повторяет заголовок функции, после которого стоит ; . Указав прототип в верхнем блоке, в нижнем мы уже можем полностью описать функцию. Для примера выше это могло бы выглядеть так:
Листинг 6.
#include
Всё очень просто. Обратите внимание, что у прототипа функции можно не указывать имена формальных параметров, достаточно просто указать их типы. В примере выше я именно так и сделал.
Возможности языков семейства Си по истине безграничны, однако, в этой свободе кроются и недостатки: всегда нужно программисту держать ухо востро и контроллировать "переполнение буфера", чтобы потом программа не вылетала в "синий экран" на массе разнообразных версий Windows и железа у пользователей. Те же крэкеры и реверсеры специально ищут в коде программ на Си уязвимости, куда можно подсадить любой вирусный код, об этом более подробно автор рассказывал в своём видеокурсе . Я там многое узнал и теперь мой код стал значительно более безопасный.