Лекция 2
Функции в C++ могут иметь одинаковые имена и отличаться лишь типом/количеством аргументов.
Пример 1. Переопределение функции abs()
int abs(int x, int y);
double abs(double x, double y);
Вызов функции abs():
int a, b;
abs(a, b); // первая функция
double x, y;
abs(x, y); // вторая функция
Операторы C/C++ могут быть переопределены в C++ как для базовых типов, так и для вновь выведенных классов
Ограничения на переопределяемые операторы:
a) нельзя изменить приоритет операторов (a+b*c – сначала умножение, потом сложение)
b) нельзя изменить количество операндов (a/b – всегда два операнда)
c) нельзя переопределить операторы «,», «::», «.*», «?»
Вопрос. Как переопределить префиксную и постфиксную формы операторов инкрементации и декрементации? (++a; a++; ar[n++]; ar[++n];)
Ответ:
operator++() - префиксная форма
operator++(int x) – постфиксная форма: лишний ненужный (dummy) аргумент
Ссылка — это объект, указывающий на данные, но не хранящий их.
Фактически это адрес данных, с которым оперируют как с объектом (то есть операция * не нужна для извлечения данных)
Пример 2. Создание и использование ссылки:
int a = 1;
int &ref = a;
cout << a << ref << “\n”;
++a;
cout << a << ref << “\n”;
Результат работы кода:
1 1
2 2
Любопытно, но бесполезно.
Ссылки в основном используются для передачи объекта в функцию в виде параметра:
int increment(int &x) { x++; }
Это использование аналогично следующему:
int increment(int *px) { (*px)++; }
Вопрос. В каких случаях вызывается конструктор копирования?
Ответ:
1) При инициализации объекта оператором копирования: MyClass A = B; (аналогично MyClass A(B))
2) При передаче в функцию копии параметра: MyClass A; f(A);
3) При создании временного объекта, возвращаемого функцией: MyClass f() { MyClass A; … return A; }
Пример 3. Вызов конструктора копирования при передаче параметра в функцию
class A {
int i;
public:
A(int n):
i(n)
{ cout << “Создание, i: “ << i << “\n”; }
~A() { cout << “Уничтожение, i: ” << i << “\n”;
void set_i(int n) { i = n; }
int get_i() { return i; }
};
void f(A ob) {
ob.set_i(2);
cout << “Это локальная переменная i: “ << ob.get_i() << “\n”;
}
int main() {
A o(1); // Создается объект “o”
f(o); // Объект «o» передается в качестве параметра в функцию f()
cout << “это переменная I в функции main: “ << o.get_i() << “\n”;
return 0;
}
В результате работы этой программы на экране появятся следующие строки:
Создание, i: 1
Это локальная переменная i: 2
Уничтожение, i: 2
Это переменная I в функции main: 1
Уничтожение, i: 1
В ходе работы этой программы конструктор вызывался лишь однажды, тогда как деструктор вызвался дважды.
Вопрос. Почему деструктор вызвался дважды?
Ответ. Когда объект передается в функцию, создается его копия, которая и становится параметром функции. При создании копии аргумента вызывается не обычный конструктор, а конструктор копирования. Если в классе нет явного определения конструктора копирования, то вызывается автоматический конструктор копирования, предусмотренный по умолчанию. Этот конструктор создает побитовую копию объекта. При завершении работы функции f копия объекта выходит из зоны видимости, поэтому вызывается его деструктор. При завершении работы функции main деструктор вызывается для уничтожения объекта o.
Вопрос. Какие проблемы могут возникнуть из-за двойного вызова деструктора?
Ответ. Например, если в обычном конструкторе выделяется динамическая память, а в деструкторе она освобождается, то локальная копия будет освобождать эту выделенную память уже при завершении работы функии f, что приведет к повреждению оригинала!
Вопрос. Что делать, чтобы избежать подобных проблем?
Ответ. В классе необходимо явно определить конструктор копирования.
Конструктор копирования выглядит следующим образом:
<имя класса> (const <имя_класса> &<ссылка на объект>) {
<тело конструктора>
}
Пример 4. Определение конструктора копирования
class array {
int *p; // Указатель на выделяемую память, в которой хранится содержимое массива
int size; // Размер массива
public:
array (int sz): size(sz) {
p = new int[sz];
}
array(const array &a) : size(a.size) {
int i;
p = new int[a.size];
for (i = 0; i < a.size; i++) p[i] = a.p[i];
}
~array() { delete [] p; }
void put(int i, int val) {
if ((i >= 0) && (i < size)) p[i] = val;
}
int get(int i) {
return p[i];
}
};
int main() {
array ar(10);
int i;
for (i = 0; i < 10; i++) ar.put(i, i);
array x(ar); // Вызов конструктора копирования
}
Вопрос. Будет ли вызываться конструктор копирования в следующем коде:
array a(10);
array b(10);
b = a;
Ответ. Нет, в данном случае оператор «=» выполняет присваивание одного объекта другому.
Если оператор присваивания не перегружен, то создается побитовая копия объекта, расположенного в его правой части. В этом случае опять возникает проблема двойного вызова деструктора для памяти, захваченной динамически.
Следовательно, необходимо также переопределить оператор присваивания:
Пример 5. Перегрузка оператора присваивания в классе array
class array {
…
array operator=(array op2) {
int i;
size = op2.size;
p = new int[size];
for (i = 0; i < size; i++) p[i] = op2.get(i);
}
};
На базе каждого класса можно построить новый, выведенный, класс, который обладает всеми возможностями, реализованными в базовом классе, а также некоторыми новыми возможностями.
Пример 6. Выведение класса:
Класс A реализует 2-мерные вектора с операциями сложения и умножения на действительное число, класс B добавляет к классу A скалярное произведение, класс C добавляет к классу A векторное произведение, класс D добавляет к классу В векторное произведение.
A -----> B -----> C
|
v
D
Пример 7. Выведение класса
class A {
int a;
public:
A(int aa):
a(aa)
{}
};
class B : public A {
int b;
public:
B(int aa, int bb):
A(aa),
b(bb)
{ b = a * a; }
};