План лекции
  • Некоторые подробности о том, что мы уже обсуждали (под видом разбора теста)
    • Конструкторы
      • Конструктор по умолчанию, когда он создается автоматически, что он в таком случае делает
      • Возможность задания значений параметров по умолчанию
      • Инициализация полей в конструкторе
    • Деструктор
      • Автоматически создающийся деструктор, что он делает
    • Оператор присваивания
      • Автоматически создающийся оператор присваивания, что он делает
      • Возможность написания оператора присваивания с другими типами параметров
    • Ключевое слово const
      • Слово const у параметра метода add
      • Константная ссылка -- полезная идиома
      • Слово const у полей класса, необходимость инициализации
      • Слово const у типа возвращаемого значения (в операторе присваивания)
      • Во что компилируется вызов метода класса
      • Слово const у метода


Разбор теста

Задание:

Напишите класс GaussNumber для «целых» комплексных чисел. (Напишите отдельно объявление класса и определения необходимых методов.)
Этот класс должен позволять писать такой код:

GaussNumber A(1, 1);           // A = 1 + i
GaussNumber B  = 5;            // B = 5
GaussNumber C;                 // C = 0;

C = A;                         // C = 1 + i
C = 1;                         // C = 1;

C.add(A);                      // C += A
C.multiply(A);                 // C *= A

Решение:

1. Значения параметров конструктора по умолчанию.

Для решения этой задачи нужно создать класс «целых» комплексных чисел со следующими private - полями:

private:
    int myRe;  //Вещественная часть
    int myIm;  //Мнимая часть
Также нужно реализовать конструкторы 3-х типов:

• GaussNumber();
Конструктор без параметров. Этот конструктор также называется дефолтным конструктором (default constructor) или конструктор по умолчанию.
Если не объявлено ни одного конструктора, то при компиляции именно этот конструктор будет создан автоматически.
NB! Он создается автоматически только если не создано ни одного конструктора.

• GaussNumber(int re);
Конструктор с одним параметром

• GaussNumber(int re, int im);
Конструктор с двумя параметрами.

Определения этих конструкторов будут выглядеть следующим образом:

• GaussNumber::GaussNumber() {
    myRe = 0;
    myIm = 0;
  }
			
• GaussNumber::GaussNumber(int re) {
    myRe = re;
    myIm = 0;    //Важно не забыть эту строчку
  }
			
• GaussNumber::GaussNumber(int re, int im) {
    myRe = re;
    myIm = im;
   }
Все три конструктора очень похожи между собой, поэтому есть возможность сократить их количество до одного. В его объявлении нужно будет указать значения по умолчанию, которые будут использованы, если количество аргументов в инициализации и объявлении не совпадет.
NB! Значения по умолчанию нужно указывать именно в объявлении конструктора, потому что в момент компиляции вызова видно именно объявление конструктора, а не его определение.
Пример:
Объявление:

GaussNumber(int re = 0, int im = 0);
Инициализация:

GaussNumber B = 5; // Такая запись скомпилируется в B(5, 0) и в стек положатся два параметра
NB! Значения по умолчанию необходимо записывать, начиная с последнего параметра.

2. Инициализация полей в определении конструктора

Поля можно инициализировать прямо в определении конструктора:

GaussNumber::GaussNumber(int re, int im) : myRe(re), myIm(im) {}
В нашем примере не имеет значения, какой из двух вариантов инициализации полей выбрать (в заголовке конструктора или в теле конструктора). Однако существуют ситуации, в которых необходимо инициализировать поля именно в заголовке конструктора. Такие ситуации будут описаны далее.

В нашем примере не важно, в каком порядке параметрам будут присваиваться значения. Но в ситуациях, когда параметры взаимозависимы, это может оказаться важным.
Например:

GaussNumber::GaussNumber(int re, int im) : myRe(re), myIm(myRe + im) {}
Параметры в определении конструктора инициализируются в том порядке, в котором они написаны в списке объявления полей класса (в том порядке, в котором для них отведено место в памяти).
Т.е. в нашем примере важно, чтобы в списке объявления полей класса сперва объявлялось поле myRe, а потом уже поле myIm. Не имеет значения то, в каком порядке происходит инициализация полей в определении конструктора.

3. Деструктор

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

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

В нашем примере конструктор копирования не требуется, так как не происходит выделение памяти в куче, и по умолчанию будет создан именно тот конструктор копирования, который требуется.

5. Оператор присваивания

Можно реализовать возможность присваивать переменной типа GaussNumber значение другого типа – int.

GaussNumber & GaussNumber::operator = (int value) {
    myRe = value;
    myIm = 0;
    return *this;
}
Но необходимости написания данного кода нет.
В этом случае создается оператор присваивания по умолчанию (для присваивания значений одной переменной типа GaussNumber другой переменной этого же типа). Например в случае записи C = 1 будут реализованы следующие действия:
1. Поиск опертора присваивания для int.
2. Если такого нет - поиск какого-либо другого оператора присваивания. Есть конструктор по умолчанию: но у него в правой части не int, а GaussNumber.
3. Вызовется конструктор GaussNumber(int re = 0, int im = 0) который приведет 1 к типу GaussNumber.
4. Выполнится оператор присваивания по умолчанию.
5. Вызовется деструктор по умолчанию. В этом случае результат будет тем же, но время работы - больше, чем если написать оператор присваивания вручную.

6. Лирическое отступление: как компилируется вызов функции A.add(B)

Стоит заметить, что с точки зрения скомпилированного кода вызов функции A.add(B) – это обычная функция с двумя параметрами : ‘адрес А’ и ‘адрес В’. В этой функции первый параметр (научно говоря, нулевой параметр) записан немного необычным способом: впереди функции и с ".", но эта такая же обычная функция (с точки зрения скомпиллированного кода), как и, например, функция foo(1, 3). В объектном коде нет ничего объектно-ориентированного.

7. Метод add


void GaussNumber::add( GaussNumber & N) {
    myRe += N.myRe;
    myIm += N.myIm;
}
NB! Если забыть ссылку при передаче параметра, то будет происходить излишнее копирование на стек. Поэтому хорошей привычкой является передавать объекты по ссылке.

Этот код можно улучшить, используя слово const.

void add(const GaussNumber & N);
При такой записи мы избегаем возможности случайно изменить передаваемый параметр в теле метода (теперь N точно не меняется). Если мы случайно напишем N.myRe = 10, код не скомпилируется и компилятор выдаст ошибку, что нельзя менять поле константного объекта.

8. Ключевое слово const

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

• У параметра
В этом случае слово const запрещает изменять параметр объекта, у которого оно вызывается.
Почти всегда объекты лучше передавать по константной ссылке, чтобы избежать его непреднамеренного изменения. Например, по умолчанию оператор присваивания работает с константной ссылкой, также как и конструктор копирования:

GaussNumber & operator = (const GaussNumber & N);

• У возвращаемого значения
В этом случае слово const запрещает изменять возвращаемое значение метода, у которого оно вызывается.
Напрмер, по умолчанию оператор присваивания возвращает также константную ссылку. В таком случае, мы не сможем случайно изменить возвращаемое значение в теле метода.

const GaussNumber & operator = (const GaussNumber & N);

• У поля
В этом случае слово const запрещает изменять поле класса, у которого оно вызывается.
Используем пример из предыдущей лекции:

class Array {
 private:
    int mySize;   //Размер массива
Предположим, что мы хотим запретить изменять размер массива в процессе работы программы. Для этого можно просто сделать поле mySize константным:

const int mySize;

Такая запись означает, что поле mySize нельзя менять.Ключевое слово const означает, что мы нигде не можем написать, например, mySize = 10. Но для начала все-таки надо присвоить этому полю значение. В таком случае инициализация полей в определении конструктора является необходимой (см. выше).
Array::Array(int mySize) : mySize(size) {}

• У метода
В этом случае слово const запрещает изменять все поля того класса, у которого оно вызывается.
Иногда необходимо исключить возможность изменения объекта, у которого вызывается метод, из этого метода. Для этого в объявлении метода используется слово const :

int getValue (int index) const; //Метод, возвращающий значение определенного элемента массива

Если мы случайно напишем в этом методе mySize = 10, то компилятор выдаст ошибку о том, что нельзя менять поля этого класса.

9. Метод multiply

Умножение комплексных чисел производится по следующей схеме:
(a + bi)(с + di):    ac – bd = re;
                     bc + ad = im;

void GaussNumber::multiply(const GaussNumber & N) {
    myRe = myRe * N.myRe - myIm * N.myIm;
    myIm = myRe * N.myIm + myIm * N.myRe;
}
Это неправильный код, так как при вычислении первой строчки myRe будет изменено, необходимо ввести дополнительную переменную, в которой сохранится изначальное значение myRe, для его дальнейшего использования.

void GaussNumber::multiply(const GaussNumber & N) {
    int re = myRe;
    myRe = myRe * N.myRe - myIm * N.myIm;
    myIm = re * N.myIm + myIm * N.myRe;
}
Этот код уже лучше, но в нем есть еще одна проблема: при умножении переменной на саму себя произойдет ошибка. Поэтому нужно сохранить также поле N.myRe.

void GaussNumber::multiply(const GaussNumber & N) {
    int re = myRe;
    int re1 = N.myRe;
    myRe = myRe * N.myRe - myIm * N.myIm;
    myIm = re * N.myIm + myIm * re1;
}
Возможны и другие варианты:

void GaussNumber::multiply(const GaussNumber & N) {
    int re = myRe * N.myRe - myIm * N.myIm;
    int im = myRe * N.myIm + myIm * N.myRe;
    myRe = re;
    myIm = im;
}

10. Код всей программы


GaussNumber.h

#ifndef _GAUSSNUMBER_H_ #define _GAUSSNUMBER_H_ class GaussNumber { private: int myRe; int myIm; public: GaussNumber(int re = 0, int im = 0); GaussNumber & operator = (int value); void add(const GaussNumber & N); void multiply(const GaussNumber & N); }; #endif

GaussNumber.cpp

#include "GaussNumber.h" GaussNumber::GaussNumber(int re, int im) : myRe(re), myIm(im) { } GaussNumber & GaussNumber::operator = (int value) { myRe = value; myIm = 0; return *this; } void GaussNumber::add(const GaussNumber & N) { myRe += N.myRe; myIm += N.myIm; } void GaussNumber::multiply(const GaussNumber & N) { int re = myRe * N.myRe - myIm * N.myIm; int im = myRe * N.myIm + myIm * N.myRe; myRe = re; myIm = im; }