Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Аргументы, принимаемые по умолчанию.




В C++ разрешается задавать значение параметров по умолчанию. Для простых типов при передаче параметров по значению синтаксис присвоения значения по умолчанию выглядит следующим образом:

тип имя=выражение

Как мы видим, объявление параметра совпадает по синтаксису с инициализацией переменных. Но C++ допускает другой вид инициализации переменной:

тип имя (значение)

Такая форма присвоения значения для встроенных типов не допускается.

Параметры со значениями по умолчанию должны объявляться последними (крайними правыми) параметрами в списке. При вызове функции такие параметры можно не указывать – используется заданное по умолчанию. Если же значение фактического параметра явно задано при вызове, то оно и используется.

Напишем функцию, выводящую на экран строку одинаковых символов. Такая функция часто бывает нужна при выводе на экран таблиц. Очевидно, функция должна иметь два параметра: выводимый символ и количество, причем, оба параметра можно сделать параметрами по умочанию: символ — это обычно минус, а количество сделаем равным 60:

void repch(char ch = ‘-‘, int k = 60)

{ for(int i = 0; i < k; ++i) cout << ch; }

С++ разрешает такие вызовы этой функции

repch(); //--выводится 60 минусов

repch(‘+’); //--выводится 60 плюсов

repch(‘=’,52); //--выводится 52 равно

Вызов функции с параметрами по умолчанию считается корректным, если не указываются самые правые параметры. Язык C++ запрещает писать вызовы с пропущенными первыми и заданными последними параметрами. Поэтому вызов repch(,54);

писать нельзя. Аналогично при определении не допускается такой заголовок:

void repch(char ch = ’-‘, int k)

К сожалению, без сообщений об ошибках проходит вызов

repch(54);

Хотя очевидно, что предполагается вывод 54-х минусов, мы получим совершенно другой результат — на экране появится строка из 60 шестерок. Здесь мы сталкиваемся с неявным преобразованием типа: целое число 54 — это код символа ‘6’. Избежать подобной ошибки в данном случае невозможно, так как при передаче по значению обязательно выполняются неявные преобразования.

Перегрузка функций.

Часто бывает удобно, чтобы функции, реализующие один и тот же алгоритм для различных типов данных, имели одно и то же имя. Если это имя мнемонично, то есть несет нужную информацию, это делает программу более понятной, поскольку для каждого действия требуется помнить только одно имя. Использование нескольких функций с одним и тем же именем, но с различными типами параметров,

называется перегрузкой функций.

Компилятор определяет, какую именно функцию требуется вызвать, по типу фактических параметров. Этот процесс называется разрешением перегрузки (перевод английского слова resolution в смысле «уточнение»). Тип возвращаемого функцией значения в разрешении не участвует. Механизм разрешения основан на достаточно сложном наборе правил, смысл которых сводится к тому, чтобы использовать функцию с наиболее подходящими аргументами и выдать сообщение, если такой не найдется. Допустим, имеется четыре варианта функции, определяющей наибольшее значение:

/ / Возвращает наибольшее из двух целых:

int max(int, int);

// Возвращает подстроку наибольшей длины:

char* max(char*, char*);

// Возвращает наибольшее, из первого параметра и длины второго:

int max (int, char*);

// Возвращает наибольшее из второго параметра и длины первого:

int max (char*, int);

void f(int a,. int b, char* c, char* d){

cout «max (a, b) «max(c, d) «max(a, c) «max(c, b);

}

При вызове функции max компилятор выбирает соответствующий типу фактических параметров вариант функции (в приведенном примере будут последовательно вызваны все четыре варианта функции).

Если точного соответствия не найдено, выполняются продвижения порядковых типов в соответствии с общими правилами (см. с. 38 и приложение 3), например, boo! и char в int, float в double и т. п.. Далее выполняются стандартные преобразования типов, например, int в double или указателей в void*. Следующим шагом является выполнение преобразований типа, заданных пользователем (об этих преобразованиях рассказывается во второй части книги, с. 195), а также поиск соответствий за счет переменного числа аргументов функций. Если соответствие на одном и том же этапе может быть получено более чем одним способом, вызов считается неоднозначным и выдается сообщение об ошибке.

Неоднозначность может появиться при:

• преобразовании типа;

• использовании параметров-ссылок;

• использовании аргументов по умолчанию.

Правила описания перегруженных функций:

• Перегруженные функции должны находиться в одной области видимости, иначе произойдет сокрытие аналогично одинаковым именам переменных вовложенных блоках.

• Перегруженные функции могут иметь параметры по умолчанию, при этом значения одного и того же параметра в разных функциях должны совпадать. В различных вариантах перегруженных функций может быть различное количество параметров по умолчанию.

• Функции не могут быть перегружены, если описание их параметров отличается только модификатором const или использованием ссылки (например, int и const int или int и int&).

Шаблоны функций.

В C++ есть мощное средство параметризации — шаблоны. Существуют шаблоны функций и шаблоны классов. С помощью шаблона функции можно определить алгоритм, который будет применяться к данным различных типов, а конкретный тип данных передается функции в виде параметра на этапе компиляции. Компилятор автоматически генерирует правильный код, соответствующий переданному типу. Таким образом, создается функция, которая автоматически перегружает сама себя и при этом не содержит накладных расходов, связанных с параметризацией. Формат простейшей функции-шаблона:

template <class Туре> заголовок{

/* тело функции */

}

Вместо слова Туре может использоваться произвольное имя.

В общем случае шаблон функции может содержать несколько параметров, каждый из которых может быть не только типом, но и просто переменной, например:

template <class А. class В, int i> void f(){... }

Например, функция, сортирующая методом выбора (он был рассмотрен на с. 59)

массив из п элементов любого типа, в виде шаблона может выглядеть так:

template <class Туре>

void sort__vybor(Type *b, int n){

Type a; //буферная переменная для обмена элементов

for (int o = 0; i<n-l; i++){

int imin = i;

for (int j = i + 1; j<n: j++)

i f (b[j] < b[imin]) imin = j;

}

Главная функция программы, вызывающей эту функцию-шаблон, может иметь вид:

#include <1ostream.h>

template <class Туре> void sort_vybor(Type *b, int n):

int main(){

const int n = 20;

int i. b[n];

for (i = 0; i<n; i++) c1n» b [ i ];

sort_vybor(b. n): // Сортировка целочисленного массива

for (1 = 0; i<n: 1++) cout «b[i] «' ':

cout «endl;

double a[] = {0.22. 117. -0.08. 0.21. 42.5};

sort_vybor(a. 5); // Сортировка массива вещественных чисел

for (1 = 0; 1<5; 1++) cout «а[1] «' ';

return 0;

}

Первый же вызов функции, который использует конкретный тип данных, приводит к созданию компилятором кода для соответствующей версии функции. Этот процесс называется инстанцированием шаблона (instantiation). Конкретный тип для инстанцирования либо определяется компилятором автоматически, исходя из типов параметров при вызове функции, либо задается явным образом. При

повторном вызове с тем же типом данных код заново не генерируется. На месте параметра шаблона, являющегося не типом, а переменной, должно указываться константное выражение.

Пример явного задания аргументов шаблона при вызове:

template<class X. class Y. class Z> void f(Y. Z);

void g(){

f<int. char*. double>("Vas1a". 3.0);

f<1nt. char*>("Vas1a". 3.0);// Z определяется как double

f<1nt>("Vas1a". 3.0); // Y определяется как char'^. a Z - как double

// f(“Vasia",3,0); ошибка: X определить невозможно

}

Чтобы применить функцию-шаблон к типу данных, определенному пользователем (структуре или классу), требуется перегрузить операции для этого типа данных, используемые в функции (о перегрузке операций см. с. 189). Как и обычные функции, шаблоны функций могут быть перегружены как с помощью шаблонов, так и обычными функциями. Можно предусмотреть специальную обработку отдельных параметров и типов с помощью специализации шаблона функции. Допустим, мы хотим более эффективнореализовать общий алгоритм сортировки для целых чисел. В этом случае можно «вручную» задать вариант шаблона функции для работы с целыми числами:

void sort_vibor<int>(1nt *b. int n){

... // Тело специализированного варианта функции

}

Сигнатура шаблона функции включает не только ее тип и типы параметров, но и фактический аргумент шаблона. Обычная функция никогда не считается специализацией шаблона, несмотря на то, что может иметь то же имя и тип возвращаемого значения.

Объекты и классы.

Описание класса

Класс является абстрактным типом данных, определяемым пользователем, и представляет собой модель реального объекта в виде данных и функций для работы с ними. Данные класса называются полями (по аналогии с полями структуры), а функции класса — методами. Поля и методы называются элементами класса. Описание класса в первом приближении выглядит так:

class <имя>{

[ private: ]

<описание скрытых элементов>

public:

<описание доступных элементов>

}; / / Описание заканчивается точкой с запятой

Спецификаторы доступа private и рubliс управляют видимостью элементов класса. Элементы, описанные после служебного слова private, видимы только внутри класса. Этот вид доступа принят в классе по умолчанию. Интерфейс класса описывается после спецификатора public. Действие любого спецификатора распространяется до следующего спецификатора или до конца класса. Можно задавать несколько секций private и public, порядок их следования значения не имеет.

Поля класса (члены-данные):

• могут иметь любой тип, кроме типа этого же класса (но могут быть указателями или ссылками на этот класс);

• могут быть описаны с модификатором const, при этом они инициализируются только один раз (с помощью конструктора) и не могут изменяться;

• могут быть описаны с модификатором static (об этом рассказывается в разделе «Статические поля», с. 186), но не как auto, extern и register.

Инициализация полей при описании не допускается. Классы могут быть глобальными (объявленными вне любого блока) и локальными (объявленными внутри блока, например, функции или другого класса).

Ниже перечислены некоторые особенности локального класса:

• внутри локального класса можно использовать типы, статические (static) и внешние (extern) переменные, внешние функции и элементы перечислений из области, в которой он описан; запрещается использовать автоматические переменные из этой области;

• локальный класс не может иметь статических элементов;

• методы этого класса могут быть описаны только внутри класса;

• если один класс вложен в другой класс, они не имеют каких-либо особых прав доступа к элементам друг друга и могут обращаться к ним только по общим правилам.

В качестве примера создадим класс, моделирующий персонаж компьютерной игры. Для этого требуется задать его свойства (например, количество щупалец, силу или наличие гранатомета) и поведение. Естественно, пример будет схематичен, поскольку приводится лишь для демонстрации синтаксиса.

class monstr{

int health, ammo;

public:

monstr(int he = 100. int am = 10){ health = he: ammo = am:}

void drawCint x. int y, int scale, int position):

int get_health(){return health:}

int get_ammo(){return ammo:}

}:

В этом классе два скрытых поля — health и ammo, получить значения которых извне можно с помощью методов get_health() и get_ammo(). Доступ к полям с помощью методов в данном случае кажется искусственным усложнением, но надо учитывать, что полями реальных классов могут быть сложные динамические структуры, и получение значений их элементов не так тривиально. Кроме того, очень важной является возможность вносить в эти структуры изменения, не затрагивая интерфейс класса.

Все методы класса имеют непосредственный доступ к его скрытым полям, иными словами, тела функций класса входят в область видимости private элементов класса.

В приведенном классе содержится три определения методов и одно объявление (метод draw). Если тело метода определено внутри класса, он является встроенным (inline). Как правило, встроенными делают короткие методы. Если внутри класса записано только объявление (заголовок) метода, сам метод должен быть определен в другом месте программы с помощью операции доступа к области видимости

(::):

void monstr::draw(int х. int у. int scale, int position)!

/ * тело метода */

}

Метод можно определить как встроенный и вне класса с помощью директивы inline (как и для обычных функций, она носит рекомендательный характер):

inline int monstr::get_ammo(){

return ammo:

}

В каждом классе есть хотя бы один метод, имя которого совпадает с именем класса. Он называется конструктором и вызывается автоматически при создании объекта класса. Конструктор предназначен для инициализации объекта. Автоматический вызов конструктора позволяет избежать ошибок, связанных с использованием неинициализированных переменных. Мы подробно рассмотрим конструкторы

в разделе «Конструкторы», с. 182. Типы данных struct и union являются видами класса, разница между ними будет объяснена позже, на с. 209. Другой пример описания класса, а также пример локального

класса приведен в разделе «Создание шаблонов классов» на с. 211.

Описание объектов

Конкретные переменные типа «класс» называются экземплярами класса, или объектами. Время жизни и видимость объектов зависит от вида и места их описания и подчиняется общим правилам С++.

monstr Vasia: // Объект класса monstr с параметрами по умолчанию

monstr Super(200. 300): // Объект с явной инициализацией

monstr stado[100]: // Массив объектов с параметрами по умолчанию

monstr *beavis = new monstr (10): // Динамический объект

//(второй параметр задается по умолчанию)

monstr &butthead = Vasia: // Ссылка на объект

При создании каждого объекта выделяется память, достаточная для хранения всех его полей, и автоматически вызывается конструктор, выполняющий их инициализацию. Методы класса не тиражируются. При выходе объекта из области действия он уничтожается, при этом автоматически вызывается деструктор.

Доступ к элементам объекта аналогичен доступу к полям структуры. Для этого используются операция. (точка) при обращении к элементу через имя объекта и операция -> при обращении через указатель, например:

int п = Vasid.get_ammo();

stado[5].draw;

cout «beavis->get_health();

Обратиться таким образом можно только к элементам со спецификаторам public. Получить или изменить значения элементов со спецификатором private можно только через обращение к соответствующим методам. Можно создать константный объект, значения полей которого изменять запрещается. К нему должны применяться только константные методы:

class monstr{

int get_health() const {return health;}

}:

const monstr Dead(Q,0); // Константный объект

cout «Dead.get_health();

Константный метод:

• объявляется с ключевым словом const после списка параметров;

а не может изменять значения полей класса;

• может вызывать только константные методы;

• может вызываться для любых (не только константных) объектов.

Рекомендуется описывать как константные те методы, которые предназначены для получения значений полей.

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...