- •Глава 12
- •Конструкторы и деструкторы. Список инициализации элементов
- •Конструкторы по умолчанию и конструкторы копирования
- •Указатель this
- •Встраиваемые (inline-) функции
- •Статические члены класса
- •Использование указателей на функции-члены класса
- •Массивы объектов класса
- •Дружественные функции и друзья класса
- •Глава 13
- •Множественное наследование
- •Виртуальные базовые классы
- •Глава 14
- •Перегрузка функций
- •Перегрузка конструкторов
- •Создание и использование конструкторов копирования
- •Определение адреса перегруженной функции
Создание и использование конструкторов копирования
Ранее мы уже встречались с этим понятием. Здесь мы подробно рассмотрим, как и зачем используются конструкторы копирования. Вначале рассмотрим вопрос: зачем понадобилось вводить такое понятие, как конструктор копирования.
В случае, если в функцию в качестве аргумента передается объект (то есть происходит передача аргумента по значению), делается точная (или побитовая) копия этого объекта, которая и передается параметру функции. Однако, если объект содержит указатель на выделенную область памяти, то в копии указатель будет указывать на ту же самую область памяти, на которую указывает исходный указатель. Поэтому, если в функции будет использован указатель из копии, - будет изменена область памяти, связанная с исходным объектом. Кроме того, когда функция завершается, - вызывается деструктор, который уничтожает копию объекта. Опять это приведет к изменению содержимого области памяти, связанной с исходным объектом.
Если объект является возвращаемым значением функции, то перед возвратом из функции компилятор генерирует временный объект для хранения, возвращаемого функцией значения. Когда происходит возврат из функции, этот временный объект выходит из области видимости. Поэтому компилятор вызывает для него деструктор. Однако если деструктор вызывает освобождение динамической области памяти, выделенной самому объекту или одному из содержащихся в нем объектов, это приведет к ошибке.
В основе этих проблем лежит использование сгенерированного компилятором конструктора копирования по умолчанию. В случае, если программист предоставляет конструктор копирования, именно он будет использоваться компилятором при инициализации одного объекта другим (и, в частности, при передаче объекта в функцию).
Замечание. Следует отличать конструктор копирования от оператора присваивания. Например,
AnyClass ob1;
AnyClass func(); //объявление функции
//оb2 инициализируется оb1 с помощью конструктора копирования
AnyClass ob2(ob1);
//Следующий оператор эквивалентен предыдущему
AnyClass оb2 = оb1;
//Возврат объекта из функции с помощью оператора копирования
оb1 = func();
//Напротив, следующая инструкция использует оператор присваивания оb2 = оb1;
Таким образом, несмотря на наличие в инструкции знака присваивания, она не всегда означает выполнение присваивания. Инструкция
AnyClass оb2 = оb1 ;
представляет собой просто другую форму записи конструктора копирования.
Устаревшее ключевое слово overload
В старых версиях C++ для создания перегруженных функций требовалось ключевое слово overload. Синтаксис его использования был следующим:
overload <имя_функции>;
Упомянутая в overload функция должна была объявляться до использования этого оператора. Таким образом, ключевое слово overload сообщало компилятору о намерении перегрузить упомянутую после него функцию. Хотя ключевое слово overload в современных компиляторах больше не поддерживается, его все еще можно встретить в старых программах, поэтому полезно знать как и для чего оно использовалось.
Перегрузка и неоднозначность. Ключевое слово explicit
Перегруженные функции выбираются по принципу наилучшего соответствия объявлений функций в текущей области видимости аргументам, предоставленным в вызове функции. Если подходящая функция найдена, эта функция и вызывается. Слово "подходящая" в этом контексте означает одно из следующего (в порядке ухудшения соответствия):
• найдено точное соответствие;
• выполнено тривиальное преобразование;
• выполнено преобразование целочисленных типов;
• существует стандартное преобразование к желаемому типу аргумента;
• существует определенное программистом преобразование (оператор преобразования или конструктор) к требуемому типу аргумента;
• были найдены аргументы, представленные многоточием.
Компилятор создает совокупность функций-кандидатов на соответствие для каждого аргумента. Затем для каждого аргумента строится совокупность функций, соответствующих наилучшим образом. Наконец, определяется функция, которая наилучшим образом соответствует вызову, как пересечение всех этих совокупностей. Если это пересечение содержит несколько функций, - перегрузка неоднозначна, и компилятор генерирует сообщение об ошибке.
Еще одно важное замечание. Неоднозначность перегруженных функций не может быть определена, пока не встретится вызов функции.
Функция с n аргументами по умолчанию, с точки зрения соответствия аргументов, рассматривается как совокупность из п+1 функций, каждая из которых отличается от предыдущей заданием одного дополнительного аргумента. Многоточие (...) действует как произвольный символ: оно соответствует любому заданному аргументу. Это может служить источником неоднозначности при выборе перегруженной функции.
Все сказанное выше относилось ко всем перегруженным функциям, безотносительно к тому, являются ли они функциями-членами или нет. Рассмотрим теперь специфику перегруженных Функций-членов.
Функции-члены класса рассматриваются различным образом в зависимости от того, объявлены ли они статическими или нет, потому что нестатические функции имеют неявный аргумент, через который передается указатель this. При определении функции, которая наилучшим образом соответствует вызову, для нестатических функций-членов рассматриваются только те перегруженные функции-члены, у которых скрытый указатель this соответствует типу объекта, который передан функции при вызове. В отличие от других аргументов при попытке установить соответствие аргумента с указателем this никакие преобразования не производятся.
В отношении перегруженных конструкторов язык C++ предоставляет дополнительную возможность по управлению процессом поиска соответствия вызываемого конструктора. Ключевое слово explicit представляет собой спецификатор объявления, который может применяться только в объявлениях конструкторов (но не в определениях конструкторов вне класса). Например:
class Т
{
public:
explicit Т (int);
explicit T(double)
{
//тело конструктора
}
};
Т::Т (int)
{
// …
}
Конструктор, объявленный со спецификатором explicit, не принимает участия в неявных преобразованиях типа. Он может быть использован только в том случае, когда никакого преобразования типа аргументов не требуется. Например, для класса, объявленного выше:
void f(T) {}; //Определение функции
void g(int i)
{
//Следующая инструкция вызовет ошибку, т.к. неявное преобразование типа int в тип Т запрещено.
f(i);
}
void h()
{
Т ob(10); // А это - верно.
}
Замечание. Нет смысла применять ключевое слово explicit к конструкторам с несколькими параметрами, так как такие конструкторы не принимают участия в неявных преобразованиях типа.
