Отправляет email-рассылки с помощью сервиса Sendsay

Статьи по Visual C++

  Все выпуски  

Порядок инициализации C++ объекта - это важно!


Домашняя страница www.devdoc.ru

DevDoc - это новые статьи по программированию каждую неделю.

Заходи и читай!

Домашняя страница Письмо автору Архив рассылки Публикация статьи

Выпуск №39

Здравствуйте уважаемые подписчики, сегодня в номере:

Ответ на задачу

В предыдущем выпуске была опубликована задача: "Требуется написать код для обмена значений двух переменных без создания временной копии. Задача имеет несколько решений"

Наиболее любознательные читатели прислали свои варианты. Они сводятся к применению обратимых операций. Если есть операторы @ и $ такие, что a@b$b = a, то обмен значений переменных выполняется так:

a = a@b
b = a$b
a = a$b

Наиболее развернутый ответ, прислал «Mr. Wanderer». Его можно прочитать по ссылке: http://www.devdoc.ru/index.php/content/view/e_exchange.htm

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

Задача на сообразительность: Циклический сдвиг массива

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

  • выделять буфер соизмеримый по размеру с N и K
  • использовать рекурсию
  • Сложность алгоритма должна быть o(N).

Задача намного сложнее предыдущего задания. Как обычно, наиболее интересные решения будут опубликованы, если в письме не будет прямого запрета. Свое решение можно отправить мне по ссылки вверху страницы. Большая просьба – подписывайтесь, чтобы все знали кто автор :)


Постоянная ссылка на статью (с картинками): http://www.devdoc.ru/index.php/content/view/cpp_init.htm

Автор: Кудинов Александр
Последняя модификация: 2007-03-26 22:24:48

Порядок инициализации C++ объекта – это важно!

Введение

Я уверен, что начинающие программисты в теории знают, как создаются C++ объекты, выполняется их инициализация и в каком порядке. Существует множество материалов, по этой теме.

Данная статья рассчитана на программистов начального и среднего уровня. Мы заглянем по ту сторону возможных проблем и выполним анализ наиболее важных аспектов инициализации и конструирования объектов. Я настоятельно рекомендую прочитать статью про виртуальные функции. Она поможет понять все тонкости связанные с инициализацией таблиц виртуальных функций в примерах этой статьи.

За основу статьи взят материал от Herb Sutter. Он публиковал код с намеренно допущенными ошибками, а потом подробно их разбирал. В этой статье вы найдете альтернативные примеры с подробным анализом. Это не реферат работы Herb Sutter'a, а скорее дополнение к ней. Итак, приступим.

Делаем ошибку!

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

#include <string>
using namespace std;
 
class A
{
public:
 A( const string& s ) { /* ... */ }
 string foo() { return "Найди ошибку!"; }
};
 
class B : public A
{
public:
 B() : A( s = foo() ) {}
private:
 string s;
};
 
int main(int argc, char *argv[])
{
 B b;
 return 0;
}

Класс A является родительским для класса B. У A есть конструктор, который принимает ссылку на строку s. Строка s не используется внутри конструктора. Функция foo возвращает объект типа string проинициализированный строкой.

Конструктор класса B вызывает конструктор класса A с параметром. В качестве значения класс использует собственную строку B::s. Обратите внимание, что сначала выполняется инициализация B::s строкой, которую возвращает A::foo, а уже потом это значение передается в конструктор родительского класса.

Если скомпилировать этот код и запустить – программа упадет. Как вы думаете, что вызывает ошибку? Ниже дано объяснение этому явлению.

Анализ ошибки

Вы нашли ошибку? Давайте ее проанализируем.

Баг находится в строке:

B() : A( s = foo() ) {}

На самом деле в одной строчке кода сразу две проблемы. Обе связаны со временем жизни объекта и попыткой использования до того, как объект создан. Первая ошибка приводит к краху программы. Вторая не столь фатальна, но имеет все шансы стать таковой в реальной программе. Также вторая ошибка более интересна сама по себе. Рассмотрим их по очереди.

Выражение s = foo(), которое используется в конструкторе объекта B, выполняется до того как выполнится инициализация подобъекта A. Надо заметить, что в этой точке инициализация объекта B также еще не сделана.

Таким образом, первая ошибка в том, что мы пытаемся использовать объект B::s еще до того как объект B был проинициализирован. Очевидно, что вторая проблема в том, что вызов члена класса A::foo также выполняется до того, как подобъект класса A был проинициализирован. Как я уже сказал, вторая ошибка не приводит к краху, но подобные "трюки" - плохая практика.

Пора залезть глубже!

Начнем с первой ошибки.

B() : A( s = foo() ) {}

При выполнении этой строки операции выполняются в такой последовательности:

  1. Вызывается A::foo
  2. Создается временная копия объекта string, который содержит строку "Найди ошибку!".
  3. Выполняется операция присваивания, т.е. вызывается string::operator=(). В качестве параметра в оператор передается временный объект string, который был сделан ранее.
  4. Как было замечено выше, член B::s еще не сконструирован, а для него уже вызван оператор присваивания. Это вызывает крах.

Давайте перепишем код следующим образом:

class B : public A
{
public:
 B() : s("Ошибка?"), A( s = foo() ) {} 
private:
 string s;
};

Как вы думаете, это избавит от ошибки? Конечно нет! Причина все та же. Надо знать последовательность инициализации объектов. Сначала конструируется родительский подобъект, а потом его потомки. Т.е. в нашем примере все равно сначала будет вызван конструктор базового класса (член B::s не проинициализирован).

Компилятор сначала вызывает конструкторы всех базовых классов (в том порядке, в котором классы присутствуют в списке наследования), а потом конструкторы для всех своих членов (в порядке объявления), а только затем выполняется тело конструктора. Точную процедуру можно найти в стандарте языка C++.

Для исправления ошибки код можно переписать так:

B() : A( foo() ) { s = foo(); }

Или так:

B() : s(foo()), A( foo() ) { }

Возможно, эти фрагменты не позволят решить все проблемы, однако задачи инициализации подобъекта A и строки B::s они решают. Сначала инициализируется подобъект A, а затем инициализируется B::s.

Теперь рассмотрим вторую проблему – вызов A::foo. Ключевое слово "вызов", т.к. сама функция A::foo не имеет ошибок. Когда выполняется код:

s = foo()

Метод A::foo уже существует (см. статью по виртуальным функциям). Методы имеют глобальный характер – они существуют, даже если нет ни одного объекта нужного типа. Компилятор и линкер заботятся о том, чтобы образ программы содержал ссылки на методы. Т.е. метод A::foo находится где-то в памяти сразу же после того, как программа запущена.

Все нестатические методы работаю с указателем this. При вызове метода в строке:

B() : A( foo() ) { s = foo(); }

Указатель this уже имеет правильное значение, несмотря на то, что объекты A и B еще не созданы.

На заметку: В компиляторе MS VC 6.0 и MS VC 2003 .NET указатель this храниться в регистре ECX.

Возможная проблема напрашивается сама собой. Если метод A::foo попытается использовать члены класса A – может возникнуть крах, т.к. объект А еще не создан.

Ситуация намного усложняется если объекты имеют виртуальные методы, которые используются во время конструирования.

В нашем случае, указатель B::this будет указывать на скрытый указатель на таблицу виртуальных функций класса B.

Модифицируем код следующим образом:

#include <string>
using namespace std;
 
class A
{
public:
 A( const string& s ) { /* ... */ }
 
 string foo()
 {
  bar();  
  return "Строка";
 }
 
 virtual void bar()
 {
 }
};
 
class B : public A
{
public:
 B() : A( s = foo() ) {}
 
private:
 string s;
};
 
int main()
{
 B b;
 return 0;
}

Разница с предыдущим кодом в том, что появилась виртуальная функция bar(), которая вызывается из метода foo().

Рассмотрим, что происходит в строке:

B() : A( s = foo() ) {}
  1. Будет вызван метод foo. В момент вызова указатель this уже проинициализирован и указывает на адрес объекта типа B.
  2. Во время выполнения тела foo, вызывается метод виртуальный bar(). Вызов осуществляется косвенным образом через таблицу виртуальных функций.

Давайте рассмотрим этот момент подробнее. Компилятор берет указатель this, получает указатель на таблицу виртуальных функций, считывает из нее адрес метода bar() и выполняет вызов. Вот тут то и "зарыта" собака. Указатель this указывает на объект B, который еще не был создан. Т.е. у него указатель на таблицу виртуальных функций содержит "мусор"!

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

Инициализация указателя таблицы виртуальных функций выполняется в конструкторе (перед инициализацией членов класса, но после вызова конструкторов родительских классов). Это накладывает существенное ограничение на использование виртуальных функций при инициализации. Думаю, что на основании этого материала, вы сами сможете промоделировать, как будут работать косвенные вызовы на разных этапах инициализации объекта.

Замечание. Порядок инициализации таблицы виртуальных функций и ее месторасположение может зависеть от конкретной реализации компилятора.

Заключение

Язык C++ содержит множество низкоуровневых тонкостей. В практике программирования случается, что найти ошибку в программе невозможно без досконального понимания всех происходящих процессов. Сложность языка - это плата за те великолепные вещи, которые можно сделать с его помощью.

При использовании объектов, Вы всегда должны помнить, как выполняется его инициализация. Это особенно актуально для объектов с наследованием и виртуальными методами.

Статьи по теме:

Copyright (C) Kudinov Alexander, 2006-2007

Перепечатка и использование материалов запрещена без писменного разрешения автора.


В избранное