Локальные и глобальные имена
Существенной особенностью функций языка С++, упрощающей их изучение, является то, что функции не могут быть вложенными. Другими словами, невозможно определить (описать) одну функцию внутри другой. В таком случае говорят, что все функции находятся на одном уровне видимости. Поэтому вопрос области действия имён решается проще, чем на языке Pascal.
Есть несколько мест объявления локальных переменных.
В скобках в прототипе и в заголовке при описании функции перечисляются только те переменные, которые должны быть переданы в функцию, то есть входные, и её результаты (выходные параметры), если такие есть. Такие переменные, названные формальными параметрами, доступны (видны, их можно использовать) только в той функции, в заголовке которой они описаны. Повторно в теле функции формальные параметры объявлять не надо! В прототипе функции имена параметров можно не писать, достаточно указать только их типы. Эта информация используется компилятором при проверке соответствия типов и количества фактических и формальных параметров. При описании функции в её заголовке имена параметров и повторно их типы записываются обязательно; Как видно из предыдущих примеров, здесь объявляются не все используемые в функции переменные.
Переменные, содержащие некоторые промежуточные значения, объявляются в любом месте тела функции до первого их использования, доступны только в ней и являются также локальными. Они не являются формальными параметрами, в заголовке функции не записываются, при вызове функции не передаются. В теле функции, отличной от void, можно объявить также переменную для результата, возвращаемого с помощью return переменная.
Как частный случай последней возможности переменные можно объявить внутри блока. Блок или составной оператор — это последовательность операторов, заключённая в фигурные скобки. Таким блоком может быть, например, тело любого из циклов, одна из ветвей if или switch. Например,
if (…) { float t; …}. Такую переменную (t) можно использовать только в этом блоке и во всех внутренних (вложенных) по отношению к нему. Вне данного блока она недоступна. Этим мы гарантируем, что вне блока такая переменная не будет изменена. Такая локальная переменная часто используется в заголовке for.
…… //Здесь i недоступна
for (int i=0; i<n;i++) // i видна в заголовке
{ …… // i видна здесь
}
…… // Здесь i использовать нельзя
При таком объявлении переменную (в примере i) можно использовать только внутри цикла и в любом из трёх выражений заголовка цикла.
Объявив её вне заголовка цикла, получим:
……// Здесь i использовать нельзя
int i;
… ... //Здесь i видна
for (i=0; i<n;i++) // i видна и в заголовке цикла
{ …… /* i видна и в цикле */ }
… … // i доступнаи после цикла здесь
Тогда она видна не только в цикле, а и в любом месте после объявления, как перед for, так и после фигурных скобок для цикла.
Локальные переменные имеют следующие особенности:
· их можно объявить в любом месте функции или блока до первого их использования, то есть не обязательно в самом начале. Это, конечно, не относится к формальным параметрам, которые объявляются всегда в скобках заголовка;
· локальные переменные создаются при входе в функцию или блок и уничтожаются при выходе из них. При этом не просто теряются значения, но и освобождается память;
· локальные переменные в разных функциях или блоках, не вложенных друг в друга, могут иметь одинаковые идентификаторы. Но на самом деле это разные переменные, для них отводится разная память.
Переменные, объявленные в main, также являются локальными и их можно использовать только в основной функции.
Глобальные переменные можно объявить вне всех функций, в том числе вне main, например, в самом начале перед прототипами функций. Кроме этого их разрешается объявлять также между прототипами, непосредственно перед функцией main или между текстами функций. Особенности таких переменных:
· “видны” из любой функции, размещенной после описания переменной;
· память занята на всё время выполнения проекта;
· как правило, использование локальных переменных предпочтительнее глобальных. Например, из двух вариантов функции
/* Вариант 1 */ float MyMax1 (float x, float y) { return x>y?x:y; }
/* Вариант 2 */ float x, y; float MyMax2 () { return x>y?x:y; }
первый лучше, так как использует локальные переменные. Во втором варианте используются глобальные переменные. Противоречие в том, что второй вариант легче для написания. Это относится и к функциям типа void. Объясняется это тем, что не надо заботиться о передаче входных параметров в функцию и можно не знать, как вернуть результаты из функции. То есть можно не знать ни ссылочный тип, ни указатели для этих целей.
Сказанное выше относится к константам и типам. Эти элементы также могут быть локальными, то есть объявленными в функции или в блоке, и глобальными, которые доступны всем или нескольким функциям. Понятно, что не имеет смысла говорить о формальных константах или формальных типах.
§ 5. Встраиваемые (inline) функции
Обычные функции вызываются следующим образом. Если встречается обращение к ней, запоминается место (точка) вызова, управление передаётся на код функции, она выполняется и управление возвращается в место вызова. В функцию можно передать или из нё возвратить значения одного или нескольких параметров. При этом код функции хранится в единственном экземпляре. Поэтому преимущество таких функций в экономии памяти. Но генерация вызова функции, передача параметров и возвращение результатов занимает определённое время.
Для встраиваемой функции не разделяется прототип и текст функции. На месте прототипа после заголовка сразу в фигурных скобках записываем и текст функции. Перед заголовком функции при этом можно писать ключевое слово inline, которое не является обязательным. Главная особенность встраиваемых функций в том, что она не вызывается, а тело такой функции встраивается в программу в каждую точку вызова.
Пример 5
int InFun (int n, int k) /*Символ ‘;’ в конце заголовка не пишется */
{ return ! (n%k); }
int main() { int N=15; if ( InFun(N, 2))
cout<< N <<” -- чётное”;
else cout<<N <<” – нечётное”;
getch(); return 0; }
Если встретился вызов так оформленной функции, то на самом деле на место вызова функции, то есть в скобки оператора if подставляется текст функции. В нашем примере это простое выражение. Тогда текст main функционально идентичен следующему
int main() { int N=15; if (!(N% 2))
cout<< N <<” -- чётное”;
else cout<<N <<” – нечётное”;
getch(); return 0; }
Текст функции копируется во все точки вызова с учётом фактических параметров, если функция вызывается несколько раз. Например, если в main встретится ещё раз, например, такой вызов r= InFun(N*N, 10); где r объявлена как int r; то фактически выполняться будет оператор r=!(N*N%10);
А что будет выведено с помощью cout<<endl<<r; для того же значения N=15? Так как N*N%10=15*15%10=5, а операция логического отрицания (“ ! ”) для любых ненулевых чисел даёт в результате 0, то это число, то есть 0 и будет выведено. Для N=10 будет выведена 1, так как 10*10%10=0, а !0=1.
Упражнение. Что будет выведено для N=4? N=1? N=0?
Такие функции выполняются быстрее, так как не надо тратить время на поиск кода функции, не надо передавать управление в функцию и обратно. Недостаток в том, что если встраиваемые функции часто вызываются и(или) они большие, то возрастает объём наших программ. Поэтому встраиваемыми функциями имеет смысл оформлять относительно небольшие функции. Некоторые компиляторы запрещают оформлять функцию как встраиваемую, если, например, она содержит вложенные циклы, оператор switch и другие конструкции, увеличивающие объём кода.
Параметры по умолчанию
В прототипе или при описании функции в её заголовке одному или нескольким формальным параметрам может быть назначено значение по умолчанию по тем же правилам, что при инициализации. При вызове таких функций фактические параметры, соответствующие умалчиваемым, могут быть опущены, и тогда функция будет выполняться с теми значениями, которые указаны в заголовке. Значение по умолчанию можно изменить, записав при вызове фактический параметр вместо умалчиваемого. Остальные фактические параметры без умалчиваемых значений надо обязательно задать.
Пример 6. Дана функция
void fun6 (float f, char ch='*', int i=2)
{ cout<<endl<<f<<" "<<ch<<" "<< i; }
Приведём несколько вариантов её вызова.
main() { fun6 (2.5, ‘-‘, 11); /* f=2.5, ch=’-‘, i=11 */
fun6 (2.5, 196); /* f=2.5, ch — символ с кодом 196 (‘-‘), i=2 по умолчанию. */
fun6 (196); /* f=196 как вещественное, сh=’*’ и i=2 по умолчанию. */
fun6 (0.01, ‘*’, 33); /* f=0.01, ch=’*’, i=33; */ getch(); }
Правила передачи параметров по умолчанию:
1) значение по умолчанию может быть задано либо в прототипе, либо при описании функции, но только в одном месте один раз;
2) в качестве умалчиваемых значений должны быть либо константы, либо глобальные переменные;
3) умалчиваемые параметры должны быть последними в списке;
4) если при вызове функции опустили аргумент для параметра по умолчанию, то не надо писать аргументы и для всех оставшихся в списке. Например, если для функции fun6 надо изменить iна 196,то fun6 (0.11, 196); компилируется из-за совместимости целого и символьного типов, но неправильно выполняется, так как i останется по умолчанию, а fun6(0.11 , , 196); не компилируется. Правильным будет такой вызов: fun6(0.11 , ’*’ , 196);, в котором второй параметр (’*’) записываем, несмотря на то, что это умалчиваемое значение.
Рекомендуется упорядочить умалчиваемые параметры по частоте их изменения.
Пример 7. Пусть вещественный параметр f1 меняется почти при каждом вызове. Из остальных трёх параметров вещественный параметр f2 меняется чаще, чем символ, а вероятность изменения целого параметра наименьшая. Тогда задаём следующий порядок параметров:
int test=2;
void Out7 ( float f1, float f2=1.1,char c3='*', int p4=test )
{ textcolor(11); cprintf("%f %f %c %d\n\r", f1, f2, c3, p4); }
int main() { Out7(11); Out7(11,2.2); Out7(11,2.2, 65);
int q1=11; Out7(q1, 'A' ,196 ,9);
getch(); return 0; }
В качестве умалчиваемого значения для целого p4 задали значение глобальной переменной, а не константы, как для других параметров. Целое число q1 и символ 'A' можно передать вместо вещественных f1 и f2. Будет выведено 11.000000 и код символа 'A' , то есть целое число, преобразованное в вещественное: 65.000000.
Упражнение. Определить остальные результаты вызова функции Out7.
Перегрузка функций
В “старом” языке С все имена функций должны быть уникальны в одном проекте. Это плохо и неудобно при работе с функциями, которые выполняют одинаковые или похожие действия с разными типами данных. Классический пример этого — стандартные функции с разными именами abs(x) и fabs(x), которые возвращают абсолютное значение, соответственно, целого и вещественного типа. В С++ можно определить несколько функций с одним и тем же именем, которые отличаются типами параметров и реже их количеством. Тогда говорят, что функции перегружены.
Пример 8. Опишем и будем использовать три функции, которые переставляют значения двух переменных разных типов:
void RR ( int &, int &);
void RR ( float &, float &);
void RR ( char &, char &);
/* Напомним, что в прототипе имена переменных можно не писать, а обязательны только их типы */
int main()
{ int i1=11, i2=22; RR(i1,i2);
cout<<"\ni1="<<i1<<" i2="<<i2<<endl;
float f1=3.4, f2=5.6; RR(f1,f2);
cout<<"\nf1="<<f1<<" f2="<<f2<<endl;
char c1='a', c2='b'; RR(c1,c2);
cout<<"\nc1="<<c1<<" c2="<<c2<<endl;
getch(); return 0; }
void RR(int &u, int &v) { int t=u; u=v; v=t; }
void RR(float &u, float &v)
{ float t=u; u=v; v=t; }
void RR(char &c1, char &c2)
{ char c=c1; c1=c2; c2=c; }
Какой вариант функции RR из трёх вызывается в main()? Компилятор автоматически выберет необходимую версию функции на основании типа используемых в функции фактических параметров. Первый раз вызывается первый вариант функции для целых параметров, второй раз — для вещественных значений и, наконец, для символьных.
Разрешается перегружать функции, отличающиеся количеством параметров. Тогда конкретный вариант функции компилятор выбирает на основании количества используемых в функции фактических параметров. Например, можно перегрузить функцию для вывода даты в виде строки или в виде трёх целых чисел.
Нельзя, чтобы перегружаемые функции отличались только типом возвращаемых значений. Например, такая перегрузка функций
int FUN2(int ); float FUN2 (int); компилятору “не понравится”!
Упражнения и тесты
Изменить функцию SINCOS (§ 2) таким образом, чтобы оба результата, и y и z, вычислялись в одном цикле одновременно.
Вфункции SINCOS (§ 2) заменить оператор do … while на while.
Вфункции SINCOS (§ 2) заменить оператор do … while на for.
В функции main (см. 2.1) вместо оператора for записать while.
Вместо одной функции SINCOS типа void cоставить и использовать две отличные от void функции для вычисления y и z.
Сравнение параметров ссылочного типа и параметров - значений.
Что будет выведено?
void fun1(int a, int &b, int &c) { int d; a=…; b=…; c=…; d=…;
/* Вместо многоточиячисла или выражения */
cout<<a%c<<" "<<(b / d )<<endl; }
int main()
{ int u=…, v=…, w=…, z=…;
/* Вместо многоточия числа или выражения*/ fun1(u,v,w);
cout<<u<<" "<<v<<" "<<(w + z)<<endl;
getch(); return 0; }
Заголовок функции может быть другим, зависит от того, какие параметры объявлены со ссылочным типом:
void fun1(int a, int b, int &c)
// или void fun1(int &a, int &b, int c) и т.п.
7. Сравнение функций типа void и отличных от типа void.
Пусть описаны функции
void FVoid (int x, int y, int &r) { r=x+y; }
int FInt (int x, int y, int &r) { r=x-y; return x/y; }
Укажите номера строк, в которых правильные вызовы функций. Что будет выведено для правильных вызовов?
int main() { int R;
if (FVoid (12, 12, R)) //1
cout<<" Yes "<<R; else cout<<" No "<<R;
if (FInt (14, 12, R)) //2
cout<<" Yes "<<R; else cout<<" No "<<R;
FVoid (12, 12, R); //3
R ? cout<<" Yes ": cout<<" No "; cout<<R;
FInt (4, 14, R); cout<<" "<<R<<" "; //4
int RES=0, x=12; RES= FVoid (x, 12, 3); //5
RES ? cout<<"Yes": cout<<"No"; cout<<RES;
RES=0; x=12; RES= FInt (x, 12, 3); //6
RES ? cout<<"Yes": cout<<"No"; cout<<RES;
getch(); return 0; }
В других более простых вариантах функции Fint записано return r; т. е. одно и то же выражение получается и с помощью переменной, и возвращается с помощью return.
В тексте функции может быть более сложное логическое выражение или использоваться другие операции (см. упражнения и тесты гл.1).