в vector нет
vector v0(10); // OK
void f(const vector&);
f(10); // ошибка: преобразования int в vector нет
f(vector(10)); // OK
Для того чтобы избежать неожиданных преобразований, мы — и стандарт языка — потребовали, чтобы конструктор класса vector с одним аргументом имел спецификатор explicit. Очень жаль, что все конструкторы не имеют спецификатора explicit по умолчанию; если сомневаетесь, объявляйте конструктор, который может быть вызван с одним аргументом, используя ключевое слово explicit.
118.3.2. Отладка конструкторов и деструкторов
Конструкторы и деструкторы вызываются в точно определенных и предсказуемых местах программы. Однако мы не всегда пишем явные вызовы, например vector(2); иногда мы пишем объявление объекта класса vector, передаем его как аргумент функции по значению или создаем в свободной памяти с помощью оператора new. Это может вызвать замешательство у людей, думающих в терминах синтаксиса. Не существует синтаксической конструкции, которая осуществляла бы диспетчеризацию вызовов конструкторов. О конструкторах и деструкторах проще думать следующим образом.
• Когда создается объект класса X, вызывается один из его конструкторов.
• Когда уничтожается объект типа X, вызывается его деструктор.
Деструктор вызывается всегда, когда уничтожается объект класса; это происходит, когда объект выходит из области видимости, программа прекращает работу или к указателю на объект применяется оператор delete. Подходящий конструктор вызывается каждый раз, когда создается объект класса; это происходит при инициализации переменной, при создании объекта с помощью оператора new (за исключением встроенных типов), а также при копировании объекта.
Что же при этом происходит? Для того чтобы понять это, добавим в конструкторы, операторы копирующего присваивания и деструкторы операторы вывода. Рассмотрим пример.
struct X { // простой тестовый класс
int val;
void out(const string& s)
{ cerr << this << "–>" << s << ": " << val << "n"; }
X(){ out("X()"); val=0; } // конструктор по умолчанию
X(int v) { out( "X(int)"); val=v; }
X(const X& x){ out("X(X&) "); val=x.val; } // копирующий
// конструктор
X& operator=(const X& a) // копирующее присваивание
{ out("X::operator=()"); val=a.val; return *this; }
~X() { out("~X()"); } // деструктор
};
Проследим, что происходит при выполнении операций над объектом класса X. Рассмотрим пример.
X glob(2); // глобальная переменная
X copy(X a) { return a; }
X copy2(X a) { X aa = a; return aa; }
X& ref_to(X& a) { return a; }
X* make(int i) { X a(i); return new X(a); }
struct XX { X a; X b; };
int main()
{
X loc(4); // локальная переменная
X loc2 = loc;
loc = X(5);
loc2 = copy(loc);
loc2 = copy2(loc);
X loc3(6);
X& r = ref_to(loc);
delete make(7);
delete make(8);
vector<X> v(4);
XX loc4;
X* p = new X(9); // объект класса Х в свободной памяти
delete p;
X* pp = new X[5]; // массив объектов класса X
// в свободной памяти
delete[]pp;
}
Попробуйте выполнить эту программу.
ПОПРОБУЙТЕ
Мы имеем в виду следующее: выполните эту программу и убедитесь, что понимаете результаты ее работы. Если понимаете, то вы знаете почти все, что требуется знать о создании и уничтожении объектов.
В зависимости от качества вашего компилятора вы можете заметить пропущенные копии, связанные с вызовами функций copy() и copy2(). Мы (люди) видим, что эти функции ничего не делают; они просто копируют значение из потока ввода в поток вывода без каких-либо изменений. Если компилятор настолько хорош, что заметит это, то сможет удалить эти вызовы конструктора копирования. Иначе говоря, компилятор может предполагать, что конструктор копирования только копирует и ничего больше не делает. Некоторые компиляторы настолько “умны”, что могут исключить фиктивные копии.
Так зачем же возиться с этим “глупым классом X”? Это напоминает упражнения для пальцев, которые выполняют музыканты. После этих упражнений многие вещи, которые обладают намного большим смыслом, становятся понятнее и легче. Кроме того, если у вас возникнут проблемы с конструкторами и деструкторами, рекомендуем вставить в них операторы вывода и посмотреть, как они работают. Для более крупных программ такая отладка становится утомительной, но для них изобретены аналогичные технологии отладки. Например, мы можем выявить, происходит ли утечка памяти, определив, равна ли нулю разность между количеством вызовов конструктора и деструктора. Программисты часто забывают определить копирующие конструкторы и копирующее присваивание для классов, выделяющих память или содержащих указатели на объекты. Это порождает проблемы (которые, впрочем, легко устранить).
Если ваши проблемы слишком велики, чтобы решить их с помощью таких простых средств, освойте профессиональные средства отладки; они называются детекторами утечек (leak detectors). В идеале, разумеется, следует не устранять утечки, а программировать так, чтобы они вообще не возникали.
18.4. Доступ к элементам вектора
До сих пор (см. раздел 17.6) для доступа к элементам вектора мы использовали функции-члены set() и get(). Но этот способ слишком громоздок и некрасив. Мы хотим использовать обычную индексацию: v[i]. Для этого следует определить функцию-член с именем operator[]. Вот ее первая (наивная) версия.
class vector {
int sz; // размер
double* elem; // указатель на элементы
public:
// ...
double operator[](int n) { return elem[n]; } // возвращаем
// элемент
};
Все выглядит хорошо и просто, но, к сожалению, слишком просто. Разрешив оператору индексирования (operator[]()) возвращать значение, мы разрешили чтение, но не запись элементов.
vector v(10);
int x = v[2]; // хорошо
v[3] = x; // ошибка: v[3] не может стоять в левой
// части оператора =
Здесь выражение v[i] интерпретируется как вызов оператора v.operator[](i), который возвращает значение элемента вектора v с номером i. Для такого слишком наивного варианта класса vector значение v[3] является числом с плавающей точкой, а не переменной, содержащей число с плавающей точкой.
ПОПРОБУЙТЕ
Создайте вариант класса vector, скомпилируйте его и