Дополнительные возможности функций
6.1Встраиваемые функции (inline-функции)
В первом примере § 4 такими функциями являются DIGIT и RR. Для встраиваемой функции не разделяется прототип и текст функции. На месте прототипа после заголовка сразу в фигурных скобках записываем и текст функции. Перед заголовком функции при этом можно писать ключевое слово inline, которое не является обязательным.
Обычные функции вызываются следующим образом. Если встречается обращение к ней, запоминается место (точка) вызова, управление передаётся на код функции, она выполняется и управление возвращается в место вызова. При этом функция может возвращать значения одного или нескольких параметров. При этом код функции хранится в единственном экземпляре. Поэтому преимущество таких функций в экономии памяти. Но генерация вызова функции, передача параметров и возвращение результатов занимает определённое время.
Главная особенность встраиваемых функций в том, что она не вызывается, а тело такой функции встраивается в программу в каждую точку вызова.
Например,
int InFun (int n, int k) // ‘;’ не пишется
{ return ! (n%k); }
Тогда если встретится, например, вызов
int N=15;
if ( InFun(N, 2)) cout<< N <<” -- чётное”;
else cout<<N <<” – нечётное”;
то на самом деле это функционально идентично
if (!(N% 2)) cout<< N <<” -- чётное”;
else cout<< N <<” – нечётное”;
Поэтому такие функции выполняются быстрее. Недостаток в том, что если встраиваемые функции часто вызываются и они большие, то возрастает объём наших программ. Поэтому встраиваемыми функциями имеет смысл оформлять небольшие относительно функции. Некоторые компиляторы запрещают оформлять функцию как встраиваемую, если, например, она содержит циклы, оператор switch и другие конструкции, увеличивающие объём кода.
Параметры по умолчанию
В прототипе или при описании функции в её заголовке одному или нескольким формальным параметрам может быть назначено значение по умолчанию по тем же правилам, что при инициализации. При вызове таких функций фактические параметры, соответствующие умалчиваемым, могут быть опущены, и тогда функция будет выполняться с теми значениями, которые указаны в заголовке. Значение по умолчанию можно изменить, записав при вызове фактический вместо умалчиваемого параметра. Остальные фактические параметры должны быть обязательно заданы. Например, функцию с заголовком void fun62 (float f, char ch=’*’, int i=2); можно вызвать одним из следующих способов:
а) fun62 (2.5, ‘-‘, 11); // параметры получат значения: f=2.5, ch=’-‘, i=11;
б) fun62 (2.5, 196); // f=2.5, ch — символ с кодом 196, i=2 по умолчанию;
в) fun62 (196); // f=196 как вещественное, сh=’*’ и i=2 по умолчанию;
г) fun62(0.01, ‘*’, 33); // f=0.01, ch=’*’, i=33;
При передаче параметров по умолчанию необходимо руководствоваться следующими правилами:
1) значение по умолчанию может быть задано либо в прототипе, либо при описании функции, но только в одном месте один раз;
2) в качестве умалчиваемых значений должны быть либо константы, либо глобальные переменные;
3) параметры со значениями по умолчанию должны быть последними в списке;
4) если при вызове функции опустили аргумент для параметра по умолчанию, то не надо писать аргумент и для всех оставшихся в списке. Например, если для функции fun62 надо изменить iна 196,то fun62 (0.11, 196); компилируется из-за совместимости целого и символьного типов, но неправильно выполняется, так как i останется по умолчанию (см. б)), а fun62(0.11 , , 196) не компилируется. Правильным будет следующий вызов: fun62(0.11 , ’*’ , 196);
Поэтому рекомендуется упорядочить умалчиваемые параметры по частоте их изменения. Например, если строка меняется чаще, чем символ, а вероятность изменения целого числа наименьшая, то задаём следующий порядок параметров:
int test=2;
void Out62(float f1, char str2[]="hello", char c3='*', int p4=test )
{ textcolor(12);
cprintf("%f %s %c %d\n\r", f1, str2, c3, p4);
}
int main(int argc, char* argv[])
{ Out62(11,"MMF",65);
Out62(11);
int q1=1111; Out62(q1,";",196 ,9);
// Целое число можно передать вместо вещественного f1
getch(); return 0;
}
Успешная компиляция и выполнение последней программы показывают, что в качестве параметра по умолчанию можно использовать и строку.
Упражнение. Определить результат выполнения последней программы.
Перегрузка функций
В “старом” языке С все имена функций должны быть уникальны в одном проекте. Это плохо и неудобно при работе с функциями, которые выполняют одинаковые или похожие действия с разными типами данных. Классический пример этого — стандартные функции abs(), labs(), fabs(), которые возвращают абсолютное значение, соответственно, целого, длинного целого (longint) и числа с плавающей точкой. В С++ можно определить несколько функций с одним и тем же именем, которые отличаются типами параметров и реже их количеством. Тогда говорят, что функции перегружены.
Например, опишем и будем использовать три функции, которые переставляют значения двух переменных разных типов:
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; t=u; u=v; v=t; }
void RR(float &u, float &v) {float t; t=u; u=v; v=t; }
void RR(char &c1, char &c2) { char c; c=c1; c1=c2; c2=c; }
Какой вариант функции RR из трёх вызывается в main()? Компилятор автоматически выберет необходимую версию функции на основании типа используемых в функции фактических параметров. Первый раз вызывается первый вариант функции для целых параметров, второй раз — для вещественных значений и, наконец, для символьных.
Разрешается перегружать функции, отличающиеся количеством параметров. Тогда конкретный вариант функции компилятор выбирает на основании количества используемых в функции фактических параметров. Например, можно перегрузить функцию для вывода даты в виде строки или в виде трёх целых чисел.
Нельзя, чтобы перегружаемые функции отличались только типом возвращаемых значений. Например, такая перегрузка функций int FUN2(int ); float FUN2 (int); компилятору не понравится !
Упражнение. Перегрузите три функции для нахождения наибольшего из двух целых, вещественных и символьных величин. В последнем варианте найти символ с наибольшим кодом. В головной функции проверьте каждый из вариантов.
Г л а в а 3
ВВЕДЕНИЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ
Изучение основных простейших понятий объектно-ориентированного программирования (ООП) проводится на примерах, которые можно проще запрограммировать без рассматриваемой методики. Из трёх свойств ООП в этой главе изучается только свойство инкапсуляции, которое означает следующее. Переменные (поля) и функции для работы с ними (методы) объединяются вместе с помощью специального структурированного типа данных, который называется класс. Кроме этого предполагается использование механизма защиты данных от несанкционированного доступа. Другими словами, с информацией, включённой в класс, разрешается работать только функциям данного класса.
Реальные задачи, в которых видны преимущества ООП, используют свойства наследования и полиморфизма, которые не учитываются при изложении материала данной главы и рассматриваются на втором курсе.
Примеры
П р и м е р 1. Разработаем простейший класс для работы с двумя целыми числами. В класс включим конструктор и следующие функции: вывод двух целых чисел; вычисление их суммы; проверка, делится ли одно число на другое без остатка; изменение двух целых чисел по правилу: наименьшее число или оба, если они одинаковы, умножаем на величину par, которая передаётся как параметр функции при её вызове.
class FirstCl // Описание класса с именем FirstCl
{ int a,b; // поля класса, т. е. переменные
public: // атрибут доступа(см.§ 2)
/* Заголовок конструктора. Это функция, которая имеет то же имя, что и класс; тип возвращаемого значения не записывается.*/
FirstCl(int x, int y);
/* Заголовки четырёх функций (методов) класса, оформленных как внешние. Они похожи на прототипы обычных самостоятельных функций, только поля класса, т. е. два целых числа, в качестве параметров не записываем */
void MyPrint();
int MySum();
bool MyTest();
/* Метод, который изменяет поля класса. Метод может использовать дополнительные параметры, которые не являются полями класса. У нас это число, на которое умножаем, наименьшее из двух чисел, или оба, если они одинаковы. */
void Change (int );
}; // конец описания класса
/* Текст внешнего конструктора. Перед именем внешнего конструктора и каждой внешней функции записываем имя класса и операцию “::” (разрешение области видимости). */
FirstCl::FirstCl (int x, int y)
{ a=x; b=y;
};
// Тексты четырёх внешних методов.
void FirstCl::MyPrint()
{ cout<<"\nThe first number "<<a<< " The second number "<<b <<endl;
} ;
int FirstCl::MySum()
{ return a+b;
};
/* Логическая функция, возвращающая true или false в зависимости от того, делится ли a на b без остатка (для 10 и 3 — false, 3 и 10 — false, 10 и 2 — true, 2 и 10 — false). */
bool FirstCl::MyTest()
{return !(a%b)? true : false;
};
void FirstCl::Change (int par)
{ if (a>b) b*=par;
else if (a<b) a*=par;
else { b*=par; a*=par;
}
};
/* В головной функции создаём объект, т. е. переменную типа класс, и вызываем включённые в класс методы. */
int main(int argc, char* argv[])
{ FirstCl obj(10,2);
/* Объявили объект obj и одновременно с этим вызвали конструктор, с помощью которого создаётся объект, и в класс передаются числа 10 и 2. Методы (функции) этого объекта будут выполняться для этих чисел. Повторно при вызове методов класса эти числа не передаём. Как и для самостоятельных функций, при объявлении объекта можно в качестве “фактических параметров” для конструктора передать не только константы, а и значения предварительно объявленных и определённых, например, введённых, переменных:
int A,B; cin>>A>>B;
FirstCl obj(A,B ); */
/* При вызове функций записываем имя объекта (obj), но не класса, и после точки имя функции (MyPrint). */
obj.MyPrint();
cout<<"\nSum "<<obj.MySum();
if (obj.MyTest()) cout<<"\n-----";
else cout<<"\n+++++";
obj.Change(10); cout<<"\nChanging ";
obj.MyPrint();
getch(); return 0;
}
При вызове функций класса есть аналогия с обычными самостоятельными функциями, не членами класса. Функции типа void вызываются отдельно (obj.MyPrint(); obj.Change(10);). Вызов функции с возвращаемым с помощью return единственным результатом записывается непосредственно в выражении, а значит, в операторе, в котором используется значение функции
(if (obj.MyTest() ) …).
Ещё раз обратим внимание на следующие особенности вызова методов класса:
· перед именем метода записывается имя объекта (а не класса!) и после точки имя метода. При необходимости в скобках записываем фактические параметры (см. функцию Change);
· поля класса передаются в объект с помощью конструктора. При вызове каждого метода повторно передавать эти числа, как для обычных функций, не надо. Передаём только те параметры, которые не включены в класс (параметр Par для метода Change).
П р и м е р 2. Разработать класс для работы с одномерным массивом наибольшей размерности 20. В отличие от первого примера, все функции оформим как встроенные. Их назначение приведено в комментариях:
#define MaxSize 20
/*Определили макрос, используемый в качестве наибольшей размерности массива */
class ClArray
{ int n;
float A[MaxSize];
public:
/* Конструктор инициализирует только одно поле — размерность массива. При этом осуществляется проверка передаваемого значения. Массив, который является вторым полем класса, определяется с помощью функции MyInp */
ClArray (int m)
{ n=m; if (n>MaxSize || n<1) n=10;
} ;
// Функция (метод) для ввода массива.
void MyInp()
{ int x=1, y; y=wherey();
for(int i=0; i<n; i++, x+=7)
{ gotoxy(x,y); cin>>A[i];
}
} ;
/* Функция (метод) для “цветного” вывода массива. При этом числа num выводятся цветом С1, а остальные — цветом С2 */
void MyOut(int C1, int C2, float num)
{ cout<<endl;
for ( int i=0; i<n; i++)
{ if (A[i]==num) textcolor ( C1 );
else textcolor ( C2 );
cprintf ( "%10.4f", A[i] );
}
}
/* Логическая (булевская) функция (метод), возвращающая true или false в зависимости от того, найдено число t в массиве или нет */
bool Test (float t)
{ for (int i=0; i<n; i++)
if (A[i]==t) return true;
return false;
}
/* Функция (метод), которая в массиве каждое наибольшее число меняет на наименьшее и наоборот. Кроме того, функция возвращает наибольшее и наименьшее значения. */
void MyChange ( float &mx, float &mn)
{ mx=mn=A[0];
for(int i=0; i<n; i++)
{ mx= A[i]>mx? A[i] : mx;
mn= A[i]<mn? A[i] : mn; }
for(int i=0; i<n; i++)
if (A[i]==mx) A[i]=mn;
else if (A[i]==mn) A[i]=mx;
} // end of the last function
} ; // end of class
/* В головной функции в цикле создаём объекты, то есть работаем с несколькими одномерными массивами, пока не введём в качестве размерности число 0. */
int main()
{ int N=5; float num;
do { ClArray ObArr(N);
ObArr.MyInp(); // ввод массива
while(1) // цикл для поиска нескольких чисел в массиве
{ cout<<"\n Number for find (1000 -- exit)"; cin>>num;
if (num==1000) break;
ObArr.MyOut ( 2, 5, num); // вывод массива разным цветом
if ( ObArr.Test ( num ) )
cout <<" \n Find the number " ;
else cout<<"\n Do not find the number ";
cout<<num;
}
float MyMx, MyMn;
/* Изменяем массив и возвращаем в функцию main наибольшее (MyMx) и наименьшее (MyMn) значения. */
ObArr.MyChange(MyMx, MyMn);
/* Вывод массива с выделением другим цветом наибольшего значения (MyMx). */
ObArr.MyOut(2,5,MyMx);
/* Вывод массива с выделением другим цветом наименьшего значения (MyMn). */
ObArr.MyOut(2,5,MyMn);
cout<< "\n Size of array (0 -- exit)"; cin>>N;
} while ( N);
return 0;
}
Класс. Поля и методы класса
Класс— это сложный (структурированный, составной) тип данных, объединяющий переменные, которые называют полямикласса, и функции для работы с этими полями, которые называют методами класса. При этом методы могут только использовать поля (вывод, вычисление суммы двух чисел в прим. 1), получать их значения (ввод), преобразовывать поля (функция MyChange в прим. 2) и выполнять другие виды обработки.
Классы могут быть стандартными. Их количество в современных системах резко возрастает. Как видно из примеров, есть возможность разрабатывать собственные классы.
В качестве полей класса могут быть переменные как простых, так и структурированных типов. Объявляются поля по обычным правилам. Одинаковые типы при этом можно не повторять. Поля играют роль глобальных переменных для функций данного класса. Поэтому повторно объявлять их в функциях класса или передавать в качестве параметров методов не надо. Начальные значения полей можно определить с помощью конструктора (подробности в § 3) или с помощью других функций класса, например, функции ввода (MyInp во втором примере).
Поля класса имеют атрибут доступа private (частный),который подразумевается по умолчанию, и его явно можно не записывать. Объявление
class FirstCl
{ int a,b; …}
равносильно class FirstCl
{ private:
int a,b; … }
Это означает, что поля принадлежат классу, т. е. они доступны (“видны”) только в методах данного класса. (Напомним, что наследование пока на первом курсе не учитываем). В других функциях, не принадлежащих классу, в том числе в main, эти переменные нельзя использовать ни напрямую, ни через объект данного класса. Например, в main нельзя написать ни a=5, ни cout<<obj.a. Благодаря этому реализуется принцип инкапсуляции, первый принцип ООП. Согласно ему, с полями класса можно работать только в методах этого класса, а в других функциях, не принадлежащих этому классу, в том числе и в main, они недоступны.
Методы класса, как правило, записываются после ключевого слова public (общедоступный), что означает следующее. Функции класса с таким атрибутом доступа можно вызывать как из методов данного класса напрямую, без записи имени объекта (MyOut()), так и из других функций, не являющихся членами этого класса, через объект данного класса (obj.MyPrint()).
Если поля класса объявить с ключевым словом public, то их можно использовать не только в функциях данного класса, но и в других функциях. А это противоречит принципу инкапсуляции ООП. Другими словами, несмотря на использование типа class, такое программирование не является объектно–ориентированным.
Некоторые методы можно объявить с атрибутом доступа private. Тогда их можно вызывать только из методов этого класса. Другими словами, если мы продадим наш класс, то такие функции недоступны для покупателя. Пользоваться ими может разработчик класса, если в нескольких его методах необходимо программировать одинаковые действия.
Методы класса можно оформить одним из следующих способов:
· как внутренние (встроенные) методы (см. прим. 2);
· или внешние методы (прим. 1).
Сравнительная характеристика такая же, как и для обычных функций (см. 6.1 гл. 2).