Копирующий конструктор
Остановимся чуть подробнее на одном из видов конструктора с аргументом, в котором в качестве аргумента выступает объект того же самого класса. Такой конструктор часто называют копирующим, поскольку предполагается, что при его выполнении создается объект-копия другого объекта. Для класса String он может выглядеть следующим образом:
class String { public: String(const String s); }; String::String(const String s) { length = s.length; str = new char[length + 1]; strcpy(str, s.str); }
Очевидно, что новый объект будет копией своего аргумента. При этом новый объект независим от первоначального в том смысле, что изменение значения одного не изменяет значения другого.
// первый объект с начальным значением // "Astring" String a("Astring"); // новый объект – копия первого, // т.е. со значением "Astring" String b(a); // изменение значения b на "AstringAstring", // значение объекта a не изменяется b.Concat(a);
Столь логичное поведение объектов класса String на самом деле обусловлено наличием копирующего конструктора. Если бы его не было, компилятор создал бы его по умолчанию, и такой конструктор просто копировал бы все атрибуты класса, т.е. был бы эквивалентен:
String::String(const String s) { length = s.length; str = s.str; }
При вызове метода Concat для объекта b произошло бы следующее: объект b перераспределил бы память под строку str, выделив новый участок памяти и удалив предыдущий (см. определение метода выше). Однако указатель str объекта a по-прежнему указывает на первоначальный участок памяти, только что освобожденный объектом b. Соответственно, значение объекта a испорчено.
Для класса Complex, который мы рассматривали ранее, кроме стандартного конструктора можно задать конструктор, строящий комплексное число из целых чисел:
class Complex { public: Complex(); Complex(int rl, int im = 0); Complex(const Complex c); // прибавить комплексное число Complex operator+(const Complex x) const; private: int real; // вещественная часть int imaginary; // мнимая часть
}; // // Стандартный конструктор создает число (0,0) // Complex::Complex() : real(0), imaginary(0) {} // // Создать комплексное число из действительной // и мнимой частей. У второго аргумента есть // значение по умолчанию — мнимая часть равна // нулю Complex::Complex(int rl, int im) : real(rl), imaginary(im) {} // // Скопировать значение комплексного числа // Complex::Complex(const Complex c) : real(c.real), imaginary(c.imaginary) {}
Теперь при создании комплексных чисел происходит их инициализация:
Complex x1; // начальное значение – ноль Complex x2(3); // мнимая часть по умолчанию равна 0 // создается действительное число 3 Complex x3(0, 1); // мнимая единица Complex y(x3); // мнимая единица
Конструкторы, особенно копирующие, довольно часто выполняются неявно. Предположим, мы бы описали метод Concat несколько иначе:
Concat(String s);
вместо
Concat(const String s);
т.е. использовали бы передачу аргумента по значению вместо передачи по ссылке. Конечный результат не изменился бы, однако при вызове метода
b.Concat(a)
компилятор создал бы временную переменную типа String – копию объекта a, и передал бы ее в качестве аргумента. При выходе из метода String эта переменная была бы уничтожена. Представляете, насколько снизилось бы быстродействие метода!
Второй пример вызова конструктора – неявное преобразование типа. Допустима запись вида:
b.Concat("LITERAL");
хотя сам метод определен только для аргумента – объекта типа String. Поскольку в классе String есть конструктор с аргументом – указателем на байт (а литерал – как раз константа такого типа), компилятор произведет автоматическое преобразование. Будет создана автоматическая переменная типа String с начальным значением "LITERAL", ссылка на нее будет передана в качестве аргумента метода String, а по завершении Concat временная переменная будет уничтожена.
Чтобы избежать подобного неэффективного преобразования, можно определить отдельный метод для работы с указателями:
class String { public: void Concat(const String s); void Concat(const char* s); }; void String::Concat(const char* s) { length += strlen(s); char* tmp = new char[length + 1]; if (tmp == 0) { // обработка ошибки } strcpy(tmp, str); strcat(tmp, s); delete [] str; str = tmp; }