Лекция 2

1. Переопределение функций

Функции в 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); // вторая функция


2. Переопределение операторов


Операторы C/C++ могут быть переопределены в C++ как для базовых типов, так и для вновь выведенных классов


Ограничения на переопределяемые операторы:


a) нельзя изменить приоритет операторов (a+b*c – сначала умножение, потом сложение)

b) нельзя изменить количество операндов (a/b – всегда два операнда)

c) нельзя переопределить операторы «,», «::», «.*», «?»


Вопрос. Как переопределить префиксную и постфиксную формы операторов инкрементации и декрементации? (++a; a++; ar[n++]; ar[++n];)


Ответ:

operator++() - префиксная форма

operator++(int x) – постфиксная форма: лишний ненужный (dummy) аргумент


3. Ссылки


Ссылка — это объект, указывающий на данные, но не хранящий их.

Фактически это адрес данных, с которым оперируют как с объектом (то есть операция * не нужна для извлечения данных)


Пример 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)++; }



4. Конструктор копирования



Вопрос. В каких случаях вызывается конструктор копирования?

Ответ:

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);

}

};



5. Наследование


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


Пример 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; }

};