Оператор вызова функции: ()
Вызов функции — это тип postfix-expression , сформированный выражением, которое вычисляет функцию или вызываемый объект, за которым следует оператор вызова функции. () Объект может объявить функцию operator () , которая предоставляет семантику вызова функции для объекта.
Синтаксис
postfix-expression :
postfix-expression ( argument-expression-list opt )
Замечания
Аргументы оператора вызова функции приходят из argument-expression-list списка выражений, разделенных запятыми. Значения этих выражений передаются функции в качестве аргументов. Список аргументов-выражений может быть пустым. До C++17 порядок вычисления выражения функции и выражений аргументов не определен и может произойти в любом порядке. В C++17 и более поздних версиях выражение функции вычисляется до любых выражений аргументов или аргументов по умолчанию. Выражения аргументов вычисляются в неопределенной последовательности.
Функция postfix-expression вычисляет вызываемую функцию. Он может принимать любую из нескольких форм:
- идентификатор функции, видимый в текущей область или в область любого из указанных аргументов функции,
- выражение, которое вычисляет функцию, указатель функции, вызываемый объект или ссылку на одну,
- метод доступа к функции-члену( явный или подразумеваемый)
- разыменовыватель указателя на функцию-член.
Может postfix-expression быть перегруженный идентификатор функции или перегруженный метод доступа к функции-члену. Правила разрешения перегрузки определяют фактическую функцию для вызова. Если функция-член является виртуальной, функция для вызова определяется во время выполнения.
Некоторые примеры объявлений:
-
Функция, возвращающая тип T . Пример объявления:
T func( int i );
T (*func)( int i );
(pObject->*pmf)(); (Object.*pmf)();
Пример
В следующем примере вызывается функция стандартной библиотеки strcat_s с тремя аргументами:
// expre_Function_Call_Operator.cpp // compile with: /EHsc #include #include // C++ Standard Library name space using namespace std; int main() < enum < sizeOfBuffer = 20 >; char s1[ sizeOfBuffer ] = "Welcome to "; char s2[ ] = "C++"; strcat_s( s1, sizeOfBuffer, s2 ); cout
Welcome to C++
Результаты вызова функции
Вызов функции вычисляется на значение rvalue, если функция не объявлена как ссылочный тип. Функции с ссылочными возвращаемыми типами оцениваются на lvalues. Эти функции можно использовать в левой части инструкции назначения, как показано ниже:
// expre_Function_Call_Results.cpp // compile with: /EHsc #include class Point < public: // Define "accessor" functions as // reference types. unsigned& x() < return _x; >unsigned& y() < return _y; >private: unsigned _x; unsigned _y; >; using namespace std; int main() < Point ThePoint; ThePoint.x() = 7; // Use x() as an l-value. unsigned y = ThePoint.y(); // Use y() as an r-value. // Use x() and y() as r-values. cout
Приведенный выше код определяет класс, который Point содержит объекты частных данных, представляющие координаты x и y . Эти объекты данных необходимо изменить, а значения — извлечь. Программа — это лишь одно из нескольких решений для такого класса. Также можно использовать функции GetX и SetX или GetY и SetY .
Функции, возвращающие типы классов, указатели на типы классов или ссылки на типы классов можно использовать как левый операнд в операторах выбора члена. Следующий код является законным:
// expre_Function_Results2.cpp class A < public: A() <>A(int i) <> int SetA( int i ) < return (I = i); >int GetA() < return I; >private: int I; >; A func1() < A a = 0; return a; >A* func2() < A *a = new A(); return a; >A& func3() < A *a = new A(); A &b = *a; return b; >int main() < int iResult = func1().GetA(); func2()->SetA( 3 ); func3().SetA( 7 ); >
Функции можно вызывать рекурсивно. Дополнительные сведения о объявлениях функций см. в разделе "Функции". Связанные материалы относятся к единицам перевода и компоновке.
Определение и вызов функций
Мощность языка СИ во многом определяется легкостью и гибкостью в определении и использовании функций в СИ-программах. В отличие от других языков программирования высокого уровня в языке СИ нет деления на процедуры, подпрограммы и функции, здесь вся программа строится только из функций.
Функция - это совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Каждая функция должна иметь имя, которое используется для ее объявления, определения и вызова. В любой программе на СИ должна быть функция с именем main (главная функция), именно с этой функции, в каком бы месте программы она не находилась, начинается выполнение программы.
При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно !) значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в точку вызова функции, где бы этот вызов ни встретился. Допускается также использовать функции не имеющие аргументов и функции не возвращающие никаких значений. Действие таких функций может состоять, например, в изменении значений некоторых переменных, выводе на печать некоторых текстов и т.п..
С использованием функций в языке СИ связаны три понятия - определение функции (описание действий, выполняемых функцией), объявление функции (задание формы обращения к функции) и вызов функции.
Определение функции задает тип возвращаемого значения, имя функции, типы и число формальных параметров, а также объявления переменных и операторы, называемые телом функции, и определяющие действие функции. В определении функции также может быть задан класс памяти.
int rus (unsigned char r) < if (r>='А' && cВ данном примере определена функция с именем rus, имеющая один параметр с именем r и типом unsigned char. Функция возвращает целое значение, равное 1, если параметр функции является буквой русского алфавита, или 0 в противном случае.
В языке СИ нет требования, чтобы определение функции обязательно предшествовало ее вызову. Определения используемых функций могут следовать за определением функции main, перед ним, или находится в другом файле.
Однако для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических параметров типам формальных параметров до вызова функции нужно поместить объявление (прототип) функции.
Объявление функции имеет такой же вид, что и определение функции, с той лишь разницей, что тело функции отсутствует, и имена формальных параметров тоже могут быть опущены. Для функции, определенной в последнем примере, прототип может иметь вид
int rus (unsigned char r); или rus (unsigned char);
В программах на языке СИ широко используются, так называемые, библиотечные функции, т.е. функции предварительно разработанные и записанные в библиотеки. Прототипы библиотечных функций находятся в специальных заголовочных файлах, поставляемых вместе с библиотеками в составе систем программирования, и включаются в программу с помощью директивы #include.
Если объявление функции не задано, то по умолчанию строится прототип функции на основе анализа первой ссылки на функцию, будь то вызов функции или определение. Однако такой прототип не всегда согласуется с последующим определением или вызовом функции. Рекомендуется всегда задавать прототип функции. Это позволит компилятору либо выдавать диагностические сообщения, при неправильном использовании функции, либо корректным образом регулировать несоответствие аргументов устанавливаемое при выполнении программы.
Объявление параметров функции при ее определении может быть выполнено в так называемом "старом стиле", при котором в скобках после имени функции следуют только имена параметров, а после скобок объявления типов параметров. Например, функция rus из предыдущего примера может быть определена следующим образом:
int rus (r) unsigned char r; < . /* тело функции */ . >В соответствии с синтаксисом языка СИ определение функции имеет следующую форму:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции ([список-формальных-параметров])
Необязательный спецификатор-класса-памяти задает класс памяти функции, который может быть static или extern. Подробно классы памяти будут рассмотрены в следующем разделе.
Спецификатор-типа функции задает тип возвращаемого значения и может задавать любой тип. Если спецификатор-типа не задан, то предполагается, что функция возвращает значение типа int.
Функция не может возвращать массив или функцию, но может возвращать указатель на любой тип, в том числе и на массив и на функцию. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции.
Функция возвращает значение если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено. Для функций, не использующих возвращаемое значение, должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если функция определена как функция, возвращающая некоторое значение, а в операторе return при выходе из нее отсутствует выражение, то поведение вызывающей функции после передачи ей управления может быть непредсказуемым.
Список-формальных-параметров - это последовательность объявлений формальных параметров, разделенная запятыми. Формальные параметры - это переменные, используемые внутри тела функции и получающие значение при вызове функции путем копирования в них значений соответствующих фактических параметров. Список-формальных-параметров может заканчиваться запятой (,) или запятой с многоточием (. ), это означает, что число аргументов функции переменно. Однако предполагается, что функция имеет, по крайней мере, столько обязательных аргументов, сколько формальных параметров задано перед последней запятой в списке параметров. Такой функции может быть передано большее число аргументов, но над дополнительными аргументами не проводится контроль типов.
Если функция не использует параметров, то наличие круглых скобок обязательно, а вместо списка параметров рекомендуется указать слово void.
Порядок и типы формальных параметров должны быть одинаковыми в определении функции и во всех ее объявлениях. Типы фактических параметров при вызове функции должны быть совместимы с типами соответствующих формальных параметров. Тип формального параметра может быть любым основным типом, структурой, объединением, перечислением, указателем или массивом. Если тип формального параметра не указан, то этому параметру присваивается тип int.
Для формального параметра можно задавать класс памяти register, при этом для величин типа int спецификатор типа можно опустить.
Идентификаторы формальных параметров используются в теле функции в качестве ссылок на переданные значения. Эти идентификаторы не могут быть переопределены в блоке, образующем тело функции, но могут быть переопределены во внутреннем блоке внутри тела функции.
При передаче параметров в функцию, если необходимо, выполняются обычные арифметические преобразования для каждого формального параметра и каждого фактического параметра независимо. После преобразования формальный параметр не может быть короче чем int, т.е. объявление формального параметра с типом char равносильно его объявлению с типом int. А параметры, представляющие собой действительные числа, имеют тип double.
Преобразованный тип каждого формального параметра определяет, как интерпретируются аргументы, помещаемые при вызове функции в стек. Несоответствие типов фактических аргументов и формальных параметров может быть причиной неверной интерпретации.
Тело функции - это составной оператор, содержащий операторы, определяющие действие функции.
Все переменные, объявленные в теле функции без указания класса памяти, имеют класс памяти auto, т.е. они являются локальными. При вызове функции локальным переменным отводится память в стеке и производится их инициализация. Управление передается первому оператору тела функции и начинается выполнение функции, которое продолжается до тех пор, пока не встретится оператор return или последний оператор тела функции. Управление при этом возвращается в точку, следующую за точкой вызова, а локальные переменные становятся недоступными. При новом вызове функции для локальных переменных память распределяется вновь, и поэтому старые значения локальных переменных теряются.
Параметры функции передаются по значению и могут рассматриваться как локальные переменные, для которых выделяется память при вызове функции и производится инициализация значениями фактических параметров. При выходе из функции значения этих переменных теряются. Поскольку передача параметров происходит по значению, в теле функции нельзя изменить значения переменных в вызывающей функции, являющихся фактическими параметрами. Однако, если в качестве параметра передать указатель на некоторую переменную, то используя операцию разадресации можно изменить значение этой переменной.
/* Неправильное использование параметров */ void change (int x, int y)
В данной функции значения переменных x и y, являющихся формальными параметрами, меняются местами, но поскольку эти переменные существуют только внутри функции change, значения фактических параметров, используемых при вызове функции, останутся неизменными. Для того чтобы менялись местами значения фактических аргументов можно использовать функцию приведенную в следующем примере.
/* Правильное использование параметров */ void change (int *x, int *y)
При вызове такой функции в качестве фактических параметров должны быть использованы не значения переменных, а их адреса
Если требуется вызвать функцию до ее определения в рассматриваемом файле, или определение функции находится в другом исходном файле, то вызов функции следует предварять объявлением этой функции. Объявление (прототип) функции имеет следующий формат:
[спецификатор-класса-памяти] [спецификатор-типа] имя-функции ([список-формальных-параметров]) [,список-имен-функций];
В отличие от определения функции, в прототипе за заголовком сразу же следует точка с запятой, а тело функции отсутствует. Если несколько разных функций возвращают значения одинакового типа и имеют одинаковые списки формальных параметров, то эти функции можно объявить в одном прототипе, указав имя одной из функций в качестве имени-функции, а все другие поместить в список-имен-функций, причем каждая функция должна сопровождаться списком формальных параметров. Правила использования остальных элементов формата такие же, как при определении функции. Имена формальных параметров при объявлении функции можно не указывать, а если они указаны, то их область действия распространяется только до конца объявления.
Прототип - это явное объявление функции, которое предшествует определению функции. Тип возвращаемого значения при объявлении функции должен соответствовать типу возвращаемого значения в определении функции.
Если прототип функции не задан, а встретился вызов функции, то строится неявный прототип из анализа формы вызова функции. Тип возвращаемого значения создаваемого прототипа int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров используемых при данном вызове.
Таким образом, прототип функции необходимо задавать в следующих случаях:
1. Функция возвращает значение типа, отличного от int.
2. Требуется проинициализировать некоторый указатель на функцию до того, как эта функция будет определена.
Наличие в прототипе полного списка типов аргументов параметров позволяет выполнить проверку соответствия типов фактических параметров при вызове функции типам формальных параметров, и, если необходимо, выполнить соответствующие преобразования.
В прототипе можно указать, что число параметров функции переменно, или что функция не имеет параметров.
Если прототип задан с классом памяти static, то и определение функции должно иметь класс памяти static. Если спецификатор класса памяти не указан, то подразумевается класс памяти extern.
Вызов функции имеет следующий формат:
Поскольку синтаксически имя функции является адресом начала тела функции, в качестве обращения к функции может быть использовано адресное-выражение (в том числе и имя функции или разадресация указателя на функцию), имеющее значение адреса функции.
Список-выражений представляет собой список фактических параметров, передаваемых в функцию. Этот список может быть и пустым, но наличие круглых скобок обязательно.
Фактический параметр может быть величиной любого основного типа, структурой, объединением, перечислением или указателем на объект любого типа. Массив и функция не могут быть использованы в качестве фактических параметров, но можно использовать указатели на эти объекты.
Выполнение вызова функции происходит следующим образом:
1. Вычисляются выражения в списке выражений и подвергаются обычным арифметическим преобразованиям. Затем, если известен прототип функции, тип полученного фактического аргумента сравнивается с типом соответствующего формального параметра. Если они не совпадают, то либо производится преобразование типов, либо формируется сообщение об ошибке. Число выражений в списке выражений должно совпадать с числом формальных параметров, если только функция не имеет переменного числа параметров. В последнем случае проверке подлежат только обязательные параметры. Если в прототипе функции указано, что ей не требуются параметры, а при вызове они указаны, формируется сообщение об ошибке.
2. Происходит присваивание значений фактических параметров соответствующим формальным параметрам.
3. Управление передается на первый оператор функции.
4. Выполнение оператора return в теле функции возвращает управление и возможно, значение в вызывающую функцию. При отсутствии оператора return управление возвращается после выполнения последнего оператора тела функции, а возвращаемое значение не определено.
Адресное выражение, стоящее перед скобками определяет адрес вызываемой функции. Это значит что функция может быть вызвана через указатель на функцию.
int (*fun)(int x, int *y);
Здесь объявлена переменная fun как указатель на функцию с двумя параметрами: типа int и указателем на int. Сама функция должна возвращать значение типа int. Круглые скобки, содержащие имя указателя fun и признак указателя *, обязательны, иначе запись
int *fun (intx,int *y);
будет интерпретироваться как объявление функции fun возвращающей указатель на int.
Вызов функции возможен только после инициализации значения указателя fun и имеет вид:
В этом выражении для получения адреса функции, на которую ссылается указатель fun используется операция разадресации * .
Указатель на функцию может быть передан в качестве параметра функции. При этом разадресация происходит во время вызова функции, на которую ссылается указатель на функцию. Присвоить значение указателю на функцию можно в операторе присваивания, употребив имя функции без списка параметров.
double (*fun1)(int x, int y); double fun2(int k, int l); fun1=fun2; /* инициализация указателя на функцию */ (*fun1)(2,7); /* обращение к функции */В рассмотренном примере указатель на функцию fun1 описан как указатель на функцию с двумя параметрами, возвращающую значение типа double, и также описана функция fun2. В противном случае, т.е. когда указателю на функцию присваивается функция описанная иначе чем указатель, произойдет ошибка.
Рассмотрим пример использования указателя на функцию в качестве параметра функции вычисляющей производную от функции cos(x).
double proiz(double x, double dx, double (*f)(double x) ); double fun(double z); int main() < double x; /* точка вычисления производной */ double dx; /* приращение */ double z; /* значение производной */ scanf("%f,%f",&x,&dx); /* ввод значений x и dx */ z=proiz(x,dx,fun); /* вызов функции */ printf("%f",z); /* печать значения производной */ return 0; >double proiz(double x,double dx, double (*f)(double z) ) < /* функция вычисляющая производную */ double xk,xk1,pr; xk=fun(x); xk1=fun(x+dx); pr=(xk1/xk-1e0)*xk/dx; return pr; >double fun( double z) < /* функция от которой вычисляется производная */ return (cos(z)); >Для вычисления производной от какой-либо другой функции можно изменить тело функции fun или использовать при вызове функции proiz имя другой функции. В частности, для вычисления производной от функции cos(x) можно вызвать функцию proiz в форме
а для вычисления производной от функции sin(x) в форме
Любая функция в программе на языке СИ может быть вызвана рекурсивно, т.е. она может вызывать саму себя. Компилятор допускает любое число рекурсивных вызовов. При каждом вызове для формальных параметров и переменных с классом памяти auto и register выделяется новая область памяти, так что их значения из предыдущих вызовов не теряются, но в каждый момент времени доступны только значения текущего вызова.
Переменные, объявленные с классом памяти static, не требуют выделения новой области памяти при каждом рекурсивном вызове функции и их значения доступны в течение всего времени выполнения программы.
Классический пример рекурсии - это математическое определение факториала n! :
n! = 1 при n=0; n*(n-1)! при n>1 .Функция, вычисляющая факториал, будет иметь следующий вид:
long fakt(int n)
Хотя компилятор языка СИ не ограничивает число рекурсивных вызовов функций, это число ограничивается ресурсом памяти компьютера и при слишком большом числе рекурсивных вызовов может произойти переполнение стека.
1.5.2. Вызов функции с переменным числом параметров
При вызове функции с переменным числом параметров в вызове этой функции задается любое требуемое число аргументов. В объявлении и определении такой функции переменное число аргументов задается многоточием в конце списка формальных параметров или списка типов аргументов.
Все аргументы, заданные в вызове функции, размещаются в стеке. Количество формальных параметров, объявленных для функции, определяется числом аргументов, которые берутся из стека и присваиваются формальным параметрам. Программист отвечает за правильность выбора дополнительных аргументов из стека и определение числа аргументов, находящихся в стеке.
Примерами функций с переменным числом параметров являются функции из библиотеки функций языка СИ, осуществляющие операции ввода-вывода информации (printf,scanf и т.п.). Подробно эти функции рассмотрены во третьей части книги.
Программист может разрабатывать свои функции с переменным числом параметров. Для обеспечения удобного способа доступа к аргументам функции с переменным числом параметров имеются три макроопределения (макросы) va_start, va_arg, va_end, находящиеся в заголовочном файле stdarg.h. Эти макросы указывают на то, что функция, разработанная пользователем, имеет некоторое число обязательных аргументов, за которыми следует переменное число необязательных аргументов. Обязательные аргументы доступны через свои имена как при вызове обычной функции. Для извлечения необязательных аргументов используются макросы va_start, va_arg, va_end в следующем порядке.
Макрос va_start предназначен для установки аргумента arg_ptr на начало списка необязательных параметров и имеет вид функции с двумя параметрами:
Параметр prav_param должен быть последним обязательным параметром вызываемой функции, а указатель arg_prt должен быть объявлен с предопределением в списке переменных типа va_list в виде:
Макрос va_start должен быть использован до первого использования макроса va_arg.
Макрокоманда va_arg обеспечивает доступ к текущему параметру вызываемой функции и тоже имеет вид функции с двумя параметрами
Эта макрокоманда извлекает значение типа type по адресу, заданному указателем arg_ptr, увеличивает значение указателя arg_ptr на длину использованного параметра (длина type) и таким образом параметр arg_ptr будет указывать на следующий параметр вызываемой функции. Макрокоманда va_arg используется столько раз, сколько необходимо для извлечения всех параметров вызываемой функции.
Макрос va_end используется по окончании обработки всех параметров функции и устанавливает указатель списка необязательных параметров на ноль (NULL).
Рассмотрим применение этих макросов для обработки параметров функции вычисляющей среднее значение произвольной последовательности целых чисел. Поскольку функция имеет переменное число параметров будем считать концом списка значение равное -1. Поскольку в списке должен быть хотя бы один элемент, у функции будет один обязательный параметр.
#includeint main() < int n; int sred_znach(int. ); n=sred_znach(2,3,4,-1); /* вызов с четырьмя параметрами */ printf("n=%d",n); n=sred_znach(5,6,7,8,9,-1); /* вызов с шестью параметрами */ printf("n=%d",n); return (0); >int sred_znach(int x. ); < int i=0, j=0, sum=0; va_list uk_arg; va_start(uk_arg,x); /* установка указателя uk_arg на */ /* первый необязятельный параметр */ if (x!=-1) sum=x; /* проверка на пустоту списка */ else return (0); j++; while ( (i=va_arg(uk_arg,int))!=-1) /* выборка очередного */ < /* параметра и проверка */ sum+=i; /* на конец списка */ j++; >va_end(uk_arg); /* закрытие списка параметров */ return (sum/j); > 1.5.3. Передача параметров функции main
Функция main, с которой начинается выполнение СИ-программы, может быть определена с параметрами, которые передаются из внешнего окружения, например, из командной строки. Во внешнем окружении действуют свои правила представления данных, а точнее, все данные представляются в виде строк символов. Для передачи этих строк в функцию main используются два параметра, первый параметр служит для передачи числа передаваемых строк, второй для передачи самих строк. Общепринятые (но не обязательные) имена этих параметров argc и argv. Параметр argc имеет тип int, его значение формируется из анализа командной строки и равно количеству слов в командной строке, включая и имя вызываемой программы (под словом понимается любой текст не содержащий символа пробел). Параметр argv это массив указателей на строки, каждая из которых содержит одно слово из командной строки. Если слово должно содержать символ пробел, то при записи его в командную строку оно должно быть заключено в кавычки.
Функция main может иметь и третий параметр, который принято называть argp, и который служит для передачи в функцию main параметров операционной системы (среды) в которой выполняется СИ-программа.
Заголовок функции main имеет вид:
int main (int argc, char *argv[], char *argp[])
Если, например, командная строка СИ-программы имеет вид:
A:\>cprog working 'C program' 1
то аргументы argc, argv, argp представляются в памяти как показано в схеме на рис.1.
argc [ 4 ] argv [ ]--> [ ]--> [A:\cprog.exe\0] [ ]--> [working\0] [ ]--> [C program\0] [ ]--> [1\0] [NULL] argp [ ]--> [ ]--> [path=A:\;C:\\0] [ ]--> [lib=D:\LIB\0] [ ]--> [include=D:\INCLUDE\0] [ ]--> [conspec=C:\COMMAND.COM\] [NULL] Рис.1. Схема размещения параметров командной строкиОперационная система поддерживает передачу значений для параметров argc, argv, argp, а на пользователе лежит ответственность за передачу и использование фактических аргументов функции main.
Следующий пример представляет программу печати фактических аргументов, передаваемых в функцию main из операционной системы и параметров операционной системы.
Пример: int main ( int argc, char *argv[], char *argp[]) < int i=0; printf ("\n Имя программы %s", argv[0]); for (i=1; i>=argc; i++) printf ("\n аргумент %d равен %s", argv[i]); printf ("\n Параметры операционной системы:"); while (*argp) < printf ("\n %s",*argp); argp++; >return (0); >Доступ к параметрам операционной системы можно также получить при помощи библиотечной функции geteuv, ее прототип имеет следующий вид:
char *geteuv (const char *varname);
Аргумент этой функции задает имя параметра среды, указатель на значение которой выдаст функция geteuv. Если указанный параметр не определен в среде в данный момент, то возвращаемое значение NULL.
Используя указатель, полученный функцией geteuv, можно только прочитать значение параметра операционной системы, но нельзя его изменить. Для изменения значения параметра системы предназначена функция puteuv.
Компилятор языка СИ строит СИ-программу таким образом, что вначале работы программы выполняется некоторая инициализация, включающая, кроме всего прочего, обработку аргументов, передаваемых функции main, и передачу ей значений параметров среды. Эти действия выполняются библиотечными функциями _setargv и _seteuv, которые всегда помещаются компилятором перед функцией main.
Если СИ-программа не использует передачу аргументов и значений параметров операционной системы, то целесообразно запретить использование библиотечных функций _setargv и _seteuv поместив в СИ-программу перед функцией main функции с такими же именами, но не выполняющие никаких действий (заглушки). Начало программы в этом случае будет иметь вид:
_setargv() < return ; /* пустая функция */ >-seteuv() < return ; /* пустая функция */ >int main() < /* главная функция без аргументов */ . . renurn (0); >В приведенной программе при вызове библиотечных функций _setargv и _seteuv будут использованы функции помещенные в программу пользователем и не выполняющие никаких действий. Это заметно снизит размер получаемого exe-файла.
Вызов функции, соответствующей заданной строке
На вход подаётся отформатированная некоторым образом строка, в которой указаны имя функции, её аргументы и типы аргументов. Нужно иметь возможность вызвать соответствующий обработчик функции, корректно передав все аргументы.
Например, так ActionScript пытается вызвать функцию test с тремя аргументами str, false, 1.0(соответственно типы аргументов: String, Boolean, Number):
str 1.0 Хотелось бы, чтобы со стороны C++ была вызвана соответствующая функция:
void test_handler(const std::wstring& str, bool flag, double n);Под катом — реализация с использованием нового стандарта и, для сравнения, реализация с использованием старого стандарта(и капельки boost-а).
Приступим. Для начала нужно как-то разобрать строку. Поскольку это не суть задачи, то, для разбора xml, будем использовать Boost.PropertyTree. Всё это спрячем в спомогательный класс InvokeParser , который будет хранить имя функции и массив пар тип аргумента-его значение:
InvokeParser
#include "boost/property_tree/ptree.hpp" #include "boost/property_tree/xml_parser.hpp" namespace as3 < class InvokeParser < public: using ArgsContainer = std::vector< std::pair< std::wstring, // Argument type std::wstring // Argument value >>; public: InvokeParser() : invoke_name_() , arguments_() < >bool parse(const std::wstring& str) < using namespace boost; using namespace boost::property_tree; try < std::wistringstream stream(str); wptree xml; read_xml(stream, xml); // Are 'invoke' tag attributes and 'arguments' tag exists? auto invoke_attribs = xml.get_child_optional(L"invoke."); auto arguments_xml = xml.get_child_optional(L"invoke.arguments"); if(!invoke_attribs || !arguments_xml) return false; // Is 'name' exists ? auto name = invoke_attribs->get_optional(L"name"); if(!name) return false; invoke_name_ = *name; arguments_.reserve(arguments_xml->size()); for(const auto& arg_value_pair : *arguments_xml) < std::wstring arg_type = arg_value_pair.first; std::wstring arg_value = arg_value_pair.second.get_value(L""); if((arg_type == L"true") || (arg_type == L"false")) < arg_value = arg_type; arg_type = L"bool"; >arguments_.emplace_back(arg_type, arg_value); > return true; > catch(const boost::property_tree::xml_parser_error& /*parse_exc*/) < >catch(. ) < >return false; > std::wstring function_name() const < return invoke_name_; >size_t arguments_count() const < return arguments_.size(); >const ArgsContainer& arguments() const < return arguments_; >private: std::wstring invoke_name_; ArgsContainer arguments_; >; > // as3Теперь напишем шаблонный класс Type , который будет иметь один параметр — некий C++-тип, в который нужно будет превратить строку, а также узнать соответствующее имя со стороны ActionScript. Например:
Type::convert(L"20"); // Вернёт 20, тип short Type::name(); // short для ActionScript это "number"Код шаблонного класса Type :
template struct Type : std::enable_if< !std::is_array::value, TypeHelper< typename std::decay::type> >::type < >; template struct Type < >;Здесь мы видим минимальные предосторожности для неосторожного пользователя данного шаблона, например, нельзя инстанцировать данный шаблон указателем на какой-то тип, так как мы не используем указатели(конкретно для ActionScript — указателей попросту нет). Конечно здесь не все предострожности, но их можно легко добавить.
Как видно, основную роботу выполняет другой шаблонный класс TypeHelper . TypeHelper инстанцируется «голым» типом. Например, для const std::wstring& получится std::wstring и т.д. Это делается с помощью decay. И делается это для того, чтобы убрать различия между функциями который имеют сигнатуры типа f(const std::wstring&) и f(std::wstring) . Делаем также некоторую предосторожность для массивов, так как в этом случае послушный decay прекрасно справится с роботой и мы получим не совсем то, что хотели.Основной шаблон TypeHelper . Он будет служить для преобразования чисел. В нашем случае он будет прекрасно работать для Арифметических типов, хотя, по-хорошему, нужно бы исключить все символьные типы(это сделать не сложно немножко модифицировав is_valid_type с помощью std::is_same ).
TypeHelper
template struct TypeHelper < private: enum < is_valid_type = std::is_arithmetic::value >; public: typedef typename std::enable_if::type Type; static typename std::enable_if::type name() < return L"number"; >// Convert AS3 number type from string to @CppType static Type convert(const std::wstring& str) < double value = std::stod(str); return static_cast(value); > >;Как видно, имя всех числовых типов, в случае ActionScript — number. Для преобразования со строки, сначала аккуратно преобразовываем в double , а потом в нужный нам тип.
Также нам нужна другая обработка для типов bool , std::wstring и void :Полные специализации TypeHelper для bool, std::wstring, void
template<> struct TypeHelper < typedef bool Type; static std::wstring name() < return L"bool"; >static bool convert(const std::wstring& str) < return (str == L"true"); >>; template<> struct TypeHelper < typedef std::wstring Type; static std::wstring name() < return L"string"; >static std::wstring convert(const std::wstring& str) < return str; >>; template<> struct TypeHelper < typedef void Type; static std::wstring name() < return L"undefined"; >static void convert(const std::wstring& /*str*/) < >>;Вот и всё, по-сути! Осталось аккуратно соединить всё вместе. Поскольку нужно иметь удобный механизм создания обработчиков соответствующих функций(хранение всех обработчиков в контейнере и т.д.), создадим интерфейс IFunction :
struct IFunction < virtual bool call(const InvokeParser& parser) = 0; virtual ~IFunction() < >>;А вот классы-наследники уже будут знать, какую функцию нужно вызвать для конкретного случая. Снова шаблон:
template struct Function : public IFunction < Function(const std::wstring& function_name, ReturnType (*f)(Args. )) : f_(f) , name_(function_name) < >bool call(const InvokeParser& parser) < if(name_ != parser.function_name()) return false; const auto ArgsCount = sizeof. (Args); if(ArgsCount != parser.arguments_count()) return false; auto indexes = typename generate_sequence::type(); auto args = parser.arguments(); if(!validate_types(args, indexes)) return false; return call(args, indexes); > private: template bool validate_types(const InvokeParser::ArgsContainer& args, sequence) < std::arraycpp_types = < Type::name(). >; std::array as3_types = < args[S].first. >; return (cpp_types == as3_types); > template bool call(const InvokeParser::ArgsContainer& args, sequence) < f_(Type::convert(args[S].second). ); return true; > protected: std::function f_; std::wstring name_; >; template std::shared_ptr make_function(const std::wstring& as3_function_name, ReturnType (*f)(Args. )) < return std::make_shared>(as3_function_name, f); >Сначала покажу как использовать:
void test_handler(const std::wstring& str, bool flag, double n) < std::wcout int main() < as3::InvokeParser parser; std::wstring str = L"" L" "; if(parser.parse(str)) < auto function = as3::make_function(L"test", test_handler); function->call(parser); > >" L" str 1.0 Перейдём к деталям. Во-первых, есть вспомогательная функция as3::make_function() , которая помогает не думать о типе передаваемого callback-а.
Во-вторых, для того, чтобы пройтись(более правильно — распаковать «паттерн», смотрим ссылку Parameter pack(ниже)) по пакету параметров(как перевести? Parameter pack) используются вспомогательные структуры sequence и generate_sequence :template struct generate_sequence : generate_sequence < >; template struct sequence < >; template struct generate_sequence < typedef sequencetype; >;Подсмотрено на stackoverflow.
В двух словах generate_sequence генерирует пакет параметров 0, 1, 2, . N - 1 , который сохраняется(читать: "выводится компилятором") с помощью sequence . Это всё нужно для Pack expansion(буду переводить как "распаковка пакета").
Например:
У нас есть пакет параметров typename. Args и функция f . Мы хотим вызвать f , передав каждое значение пакета некоторой функции, а результат этой функции уже передать f :template struct Test < templatestatic bool test(const std::vector>& args, sequence) < f_(Type::convert(args[S].second). ); return true; > >; // где-то в коде test(args, typename generate_sequence::type())Вызов test для Args = превратится в вызов:
f_(Type::convert(args[0].second), Type::convert(args[1].second))Вот и вся магия!
Наша функция-член Function::validate_types создаёт два массива. Один массив содержит имена C++-типов в ActionScript-е, а другой - имена типов, со входящей строки. Если массивы не одинаковы - у нас некорректная сигнатура функции! И мы можем это диагностировать. Вот что значит мощь шаблонов!
А вспомогательная функция-член call(const InvokeParser::ArgsContainer& args, sequence) делает то, что в примере выше, только для нашего случая.
Всё - с помощью make_function() можно делать регистрацию обработчиков, поскольку мы имеем полиморфный тип IFunction , обработчики можно спокойно сохранять в массиве(да в чём угодно) и при поступлении очередной строки вызывать соответствующий обработчик.Итак, а теперь код для старого стандарта, для максимального количества аргументов равного четырём с использованием boost::function_traits и совсем немножко mpl 🙂
InvokeParser
class InvokeParser < public: typedef std::vector> ArgsContainer; typedef ArgsContainer::value_type TypeValuePair; public: InvokeParser() : invoke_name_() , arguments_() < >bool parse(const std::wstring& str) < using namespace boost; using namespace boost::property_tree; try < std::wistringstream stream(str); wptree xml; read_xml(stream, xml); optionalinvoke_attribs = xml.get_child_optional(L"invoke."); optional arguments_xml = xml.get_child_optional(L"invoke.arguments"); if(!invoke_attribs || !arguments_xml) return false; optional name = invoke_attribs->get_optional(L"name"); if(!name) return false; invoke_name_ = *name; arguments_.reserve(arguments_xml->size()); for(wptree::const_iterator arg_value_pair = arguments_xml->begin(), end = arguments_xml->end(); arg_value_pair != end; ++arg_value_pair) < std::wstring arg_type = arg_value_pair->first; std::wstring arg_value = arg_value_pair->second.get_value(L""); if((arg_type == L"true") || (arg_type == L"false")) < arg_value = arg_type; arg_type = L"bool"; >arguments_.push_back(TypeValuePair(arg_type, arg_value)); > return true; > catch(const boost::property_tree::xml_parser_error& /*parse_exc*/) < >catch(. ) < >return false; > std::wstring function_name() const < return invoke_name_; >size_t arguments_count() const < return arguments_.size(); >const ArgsContainer& arguments() const < return arguments_; >private: std::wstring invoke_name_; ArgsContainer arguments_; >;TypeHelper
template struct TypeHelper < private: // Arithmetic types are http://en.cppreference.com/w/cpp/language/types. // Need to exclude 'Character types' from this list // (For 'Boolean type' this template has full specialization) typedef boost::mpl::and_< boost::is_arithmetic, boost::mpl::not_ >, boost::mpl::not_ >, boost::mpl::not_ >, boost::mpl::not_ > > ValidCppType; public: // We can get C++ type name equivalent for AS3 "number" type only if // C++ type @CppType is @ValidCppType(see above) typedef typename boost::enable_if< ValidCppType, CppType>::type Type; // Get AS3 type name for given @CppType(see @ValidCppType) static typename boost::enable_if< ValidCppType, std::wstring>::type name() < return L"number"; >// Convert AS3 number type from string to @CppType(see @ValidCppType) static Type convert(const std::wstring& str) < double value = from_string(str); // TODO: Use boost type cast return static_cast(value); > >; template<> struct TypeHelper < typedef bool Type; // AS3 type name for boolean type static std::wstring name() < return L"bool"; >// Convert AS3 boolean value from string to our bool static bool convert(const std::wstring& str) < return (str == L"true"); >>; template<> struct TypeHelper < typedef std::wstring Type; static std::wstring name() < return L"string"; >static std::wstring convert(const std::wstring& str) < // Ok, do nothing return str; >>; template<> struct TypeHelper < typedef void Type; // AS3 type name for void type.. static std::wstring name() < return L"undefined"; >static void convert(const std::wstring& /*str*/) < // Oops.. ASSERT_MESSAGE(false, "Can't convert from sring to void"); >>; // @TypeHelper provides implementation // only for "number" type(arithmetic, without characters type), bool, string and void. // For any other type @TypeHelper will be empty. // decay is used for removing cv-qualifier. But it's not what we want for arrays. // That's why using enable_if template struct FlashType : boost::enable_if < boost::mpl::not_< boost::is_array>, TypeHelper< typename std::tr1::decay::type> >::type < >; // Partial specialization for pointers // There is no conversion from AS3 type to C++ pointer.. template struct FlashType < // static assert >;То, чего не было ранее - FunctionCaller - заменяет распаковку. :
FunctionCaller
template struct FunctionCaller < templatestatic bool call(Function /*f*/, const InvokeParser::ArgsContainer& /*args*/ #if defined(DEBUG) , const std::wstring& /*dbg_function_name*/ #endif ) < ASSERT_MESSAGE_AND_RETURN_VALUE( false, "Provide full FunctionCaller specialization for given arguments count", false); >>; template<> struct FunctionCaller < templatestatic bool call(Function f, const InvokeParser::ArgsContainer& /*args*/ #if defined(DEBUG) , const std::wstring& /*dbg_function_name*/ #endif ) < // Call function without args f(); return true; >>; template<> struct FunctionCaller < templatestatic bool call(Function f, const InvokeParser::ArgsContainer& args #if defined(DEBUG) , const std::wstring& dbg_function_name #endif ) < typedef FlashType::arg1_type> Arg1; const InvokeParser::TypeValuePair& arg = args[0]; if(Arg1::name() != arg.first) < #if defined(DEBUG) ::OutputDebugStringW(Sprintf( L"Function: \"%s\":\n" L"%s -> %s\n", dbg_function_name.c_str(), Arg1::name().c_str(), arg.first.c_str()).c_str()); #endif ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false); > // Call function with 1 arg f(Arg1::convert(arg.second)); return true; > >; template<> struct FunctionCaller < templatestatic bool call(Function f, const InvokeParser::ArgsContainer& args #if defined(DEBUG) , const std::wstring& dbg_function_name #endif ) < typedef FlashType::arg1_type> Arg1; typedef FlashType::arg2_type> Arg2; const InvokeParser::TypeValuePair& arg1 = args[0]; const InvokeParser::TypeValuePair& arg2 = args[1]; if((Arg1::name() != arg1.first) || (Arg2::name() != arg2.first)) < #if defined(DEBUG) ::OutputDebugStringW(Sprintf( L"Function: \"%s\":\n" L"%s -> %s\n" L"%s -> %s\n", dbg_function_name.c_str(), Arg1::name().c_str(), arg1.first.c_str(), Arg2::name().c_str(), arg2.first.c_str()).c_str()); #endif ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false); > // Call function with 2 args f(Arg1::convert(arg1.second), Arg2::convert(arg2.second)); return true; > >; template<> struct FunctionCaller < templatestatic bool call(Function f, const InvokeParser::ArgsContainer& args #if defined(DEBUG) , const std::wstring& dbg_function_name #endif ) < typedef FlashType::arg1_type> Arg1; typedef FlashType::arg2_type> Arg2; typedef FlashType::arg3_type> Arg3; const InvokeParser::TypeValuePair& arg1 = args[0]; const InvokeParser::TypeValuePair& arg2 = args[1]; const InvokeParser::TypeValuePair& arg3 = args[2]; if((Arg1::name() != arg1.first) || (Arg2::name() != arg2.first) || (Arg3::name() != arg3.first)) < #if defined(DEBUG) ::OutputDebugStringW(Sprintf( L"Function: \"%s\":\n" L"%s -> %s\n" L"%s -> %s\n" L"%s -> %s\n", dbg_function_name.c_str(), Arg1::name().c_str(), arg1.first.c_str(), Arg2::name().c_str(), arg2.first.c_str(), Arg3::name().c_str(), arg3.first.c_str()).c_str()); #endif ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false); > // Call function with 3 args f(Arg1::convert(arg1.second), Arg2::convert(arg2.second), Arg3::convert(arg3.second)); return true; > >; template<> struct FunctionCaller < templatestatic bool call(Function f, const InvokeParser::ArgsContainer& args #if defined(DEBUG) , const std::wstring& dbg_function_name #endif ) < typedef FlashType::arg1_type> Arg1; typedef FlashType::arg2_type> Arg2; typedef FlashType::arg3_type> Arg3; typedef FlashType::arg4_type> Arg4; const InvokeParser::TypeValuePair& arg1 = args[0]; const InvokeParser::TypeValuePair& arg2 = args[1]; const InvokeParser::TypeValuePair& arg3 = args[2]; const InvokeParser::TypeValuePair& arg4 = args[3]; if((Arg1::name() != arg1.first) || (Arg2::name() != arg2.first) || (Arg3::name() != arg3.first) || (Arg4::name() != arg4.first)) < #if defined(DEBUG) ::OutputDebugStringW(Sprintf( L"Function: \"%s\":\n" L"%s -> %s\n" L"%s -> %s\n" L"%s -> %s\n" L"%s -> %s\n", dbg_function_name.c_str(), Arg1::name().c_str(), arg1.first.c_str(), Arg2::name().c_str(), arg2.first.c_str(), Arg3::name().c_str(), arg3.first.c_str(), Arg4::name().c_str(), arg4.first.c_str()).c_str()); #endif ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false); > // Call function with 4 args f(Arg1::convert(arg1.second), Arg2::convert(arg2.second), Arg3::convert(arg3.second), Arg4::convert(arg4.second)); return true; > >;Function
struct IFunction < virtual bool call(const InvokeParser& parser) = 0; virtual ~IFunction() < >>; template struct Function : public IFunction < Function(const std::wstring& function_name, FunctionPointer f) : f_(f) , name_(function_name) < >bool call(const InvokeParser& parser) < typedef typename boost::remove_pointer::type FunctionType; enum < ArgsCount = boost::function_traits::arity >; ASSERT_MESSAGE_AND_RETURN_VALUE( name_ == parser.function_name(), "Incorrect function name", false); ASSERT_MESSAGE_AND_RETURN_VALUE( ArgsCount == parser.arguments_count(), "Incorrect function arguments count", false); return FunctionCaller::template call( f_, parser.arguments() #if defined(DEBUG) , name_ #endif ); > protected: FunctionPointer f_; std::wstring name_; >; template IFunction* CreateFunction(const std::wstring& name, FunctionPointer f) < return new Function(name, f); >Спасибо за внимание!
UPD: забыл добавить - в самом новом, грядущем, 14м стандарте есть(будет) std::make_index_sequence, который делает всё то же, что и generate_sequence .
Пользовательские функции в Си
Итак, зачем нужны пользовательские функции? Пользовательские функции нужны для того, чтобы программистам было проще писать программы.
Помните, мы говорили о парадигмах программирования, а точнее о структурном программировании. Основной идеей там было то, что любую программу можно написать используя только три основных конструкции: следование, условие и цикл. Теперь к этим конструкциям мы добавим ещё одну – «подпрограммы» – и получим новую парадигму процедурное программирование» .
Отличие лишь в том, что отдельные кусочки нашей основной программы (в частности, повторяющиеся) мы будем записывать в виде отдельных функций (подпрограмм, процедур) и по мере необходимости их вызывать. По сути, программа теперь будет описывать взаимодействие различных функций.
В принципе, мы уже используем эту парадигму. Если вам пока ещё не совсем ясно, почему это проще, то просто представьте, что вместо того чтобы вызвать функцию exp(x) из заголовочного файла math.h вам каждый раз необходимо было бы описывать подробно, как вычислить значение этой функции.
Итак, в этом уроке мы подробно обсудим то, как функции устроены изнутри. А также научимся создавать свои собственные пользовательские функции.
Как устроены функции
Вспомним информацию с первого урока. Все функции, в том числе и те, которые пишет пользователь, устроены сходным образом. У них имеется две основных составных части: заголовок функции и тело функции.
int main(void)< // заголовок функции // в фигурных скобках записано тело функции >С телом функции всё ясно: там описывается алгоритм работы функции. Давайте разберёмся с заголовком. Он состоит из трёх обязательных частей:
- тип возвращаемого значения;
- имя функции;
- аргументы функции.
Сначала записывается тип возвращаемого значения, например, int , как в функции main . Если функция не должна возвращать никакое значение в программу, то на этом месте пишется ключевое слово void . Казалось бы, что раз функция ничего не возвращает, то и не нужно ничего писать. Раньше, кстати, в языке Си так и было сделано, но потом для единообразия всё-таки добавили. Сейчас современные компиляторы будут выдавать предупреждения/ошибки, если вы не укажете тип возвращаемого значения.
В некоторых языках программирования функции, которые не возвращают никакого значения, называют процедурами (например, pascal). Более того, для создания функций и процедур предусмотрен различный синтаксис. В языке Си такой дискриминации нет.
После типа возвращаемого значения записывается имя функции. Ну а уж после имени указываются типы и количество аргументов, которые передаются в функцию.
Давайте посмотрим на заголовки уже знакомых нам функций.
// функция с именем srand, принимающая целое число, ничего не возвращает void srand(int) //функция с именем sqrt, принимающая вещественное число типа float, возвращает вещественное число типа float float sqrt(float) //функция с именем rand, которая не принимает аргументов, возвращает целое число int rand(void) //функция с именем pow, принимающая два аргумента типа double, возвращает вещественное число типа double double pow(double, double)
Как создать свою функцию
Для того чтобы создать свою функцию, необходимо её полностью описать. Тут действует общее правило: прежде чем использовать – объяви и опиши, как должно работать. Для этого вернёмся к схеме структуры программы на языке Си, которая у нас была в самом первом уроке. Отметим на ней те места, где можно описывать функции.

Рис.1 Уточнение структуры программы. Объявление функций.
Как видите, имеется аж два места, где это можно сделать.
Давайте посмотрим на пример, который иллюстрируют создание пользовательской функции вычисления максимального из двух чисел.
#include // объявляем пользовательскую функцию с именем max_num // вход: два целочисленных параметра с именами a и b // выход: максимальное из двух аргументов int max_num(int a, int b) < int max = b; if (a >b) max = a; return max; > //основная программа int main(void)
Давайте я подробно опишу, как будет работать эта программа. Выполняется тело функции main . Создются целые переменные x , y и m . В переменные x и y считываются данные с клавиатуры. Допустим мы ввели 3 5 , тогда x = 3 , y = 5 . Это вам всё и так должно быть понятно. Теперь следующая строчка
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 удаляются из памяти. А мы возвращаемся к строке
m = max_num(x,y);
Функция max_num вернула значение 5 , значит теперь справа от знака = записано 5 . Это значение записывается в переменную m. Дальше на экран выводится строчка, и программа завершается.
Внимательно прочитайте последние 4 абазаца ещё раз, чтобы до конца уяснить, как работает программа.
А я пока расскажу, зачем нужен нижний блок описания функций. Представьте себе, что в вашей программе вы написали 20 небольших функций. И все они описаны перед функцией main . Не очень-то удобно добираться до основной программы так долго. Чтобы решить эту проблему, функции можно описывать в нижнем блоке.
Но просто так перенести туда полностью код функции не удастся, т.к. тогда нарушится правило: прежде чем что-то использовать, необходимо это объявить. Чтобы избежать подобной проблемы, необходимо использовать прототип функции.
Прототип функции полностью повторяет заголовок функции, после которого стоит ; . Указав прототип в верхнем блоке, в нижнем мы уже можем полностью описать функцию. Для примера выше это могло бы выглядеть так:
#include int max_num(int, int); int main(void) < int x =0, y = 0; int m = 0; scanf("%d %d", &x, &y); m = max_num(x,y); printf("max(%d,%d) = %d\n",x,y,m); return 0; >int max_num(int a, int b) < int max = b; if (a >b) max = a; return max; >
Всё очень просто. Обратите внимание, что у прототипа функции можно не указывать имена формальных параметров, достаточно просто указать их типы. В примере выше я именно так и сделал.
Сохрани в закладки или поддержи проект.
Практика
Решите предложенные задачи:

Для удобства работы сразу переходите в полноэкранный режим
Дополнительные материалы
- пока нет