Опубликован: 26.06.2003 | Доступ: свободный | Студентов: 39509 / 6475 | Оценка: 4.07 / 3.80 | Длительность: 15:08:00
ISBN: 978-5-9556-0017-8
Лекция 12:

Классы – конструкторы и деструкторы

< Лекция 11 || Лекция 12: 123 || Лекция 13 >
Аннотация: Конструкторы и деструкторы классов. Возможности инициализации объектов. Копирующий конструктор. Операции new и delete.

При определении класса имеется возможность задать для объекта начальное значение. Специальный метод класса, называемый конструктором, выполняется каждый раз, когда создается новый объект этого класса. Конструктор – это метод, имя которого совпадает с именем класса. Конструктор не возвращает никакого значения.

Для класса String имеет смысл в качестве начального значения использовать пустую строку:

class String
{
public:
     String();   // объявление конструктора
};
// определение конструктора
String::String()
{
     str = 0;
     length = 0;
}

Определив такой конструктор, мы гарантируем, что даже при создании автоматической переменной объект будет соответствующим образом инициализирован (в отличие от переменных встроенных типов).

Конструктор без аргументов называется стандартным конструктором или конструктором по умолчанию. Можно определить несколько конструкторов с различными наборами аргументов. Возможности инициализации объектов в таком случае расширяются. Для нашего класса строк было бы логично инициализировать переменную с помощью указателя на строку.

class String
{
public:  
      String(); // стандартный конструктор
      String(const char* p);  
             // дополнительный конструктор     

};
// определение второго конструктора
String::String(const char* p)
{
     length = strlen(p);
     str = new char[length + 1];
     if (str == 0) {
          // обработка ошибок
     }
     strcpy(str, p);   // копирование строки 
}

Теперь можно, создавая переменные типа String, инициализировать их тем или иным образом:

char* cp;
// выполняется стандартный конструктор
String s1;
// выполняется второй конструктор
String s2("Начальное значение");
// выполняется стандартный конструктор
String* sptr = new String;
// выполняется второй конструктор
String* ssptr = new String(cp);

Копирующий конструктор

Остановимся чуть подробнее на одном из видов конструктора с аргументом, в котором в качестве аргумента выступает объект того же самого класса. Такой конструктор часто называют копирующим , поскольку предполагается, что при его выполнении создается объект-копия другого объекта. Для класса 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=0) :
     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;
}
< Лекция 11 || Лекция 12: 123 || Лекция 13 >
Андрей Одегов
Андрей Одегов
Язык программирования C++
Елена Шумова
Елена Шумова

Здравствуйте! Я у Вас прошла курс Язык программировая Си++.

Заказала сертификат. Хочу изменить способ оплаты. Как это сделать?