Некоторые основные вопросы по конструкторам (и множественному наследованию) в C ++? - PullRequest
4 голосов
/ 23 февраля 2010

(извините, если раньше об этом спрашивали; функция поиска, похоже, не работает: область результатов полностью пуста, хотя в ней указано несколько страниц результатов ... в Chrome, FireFox, и Safari)

Итак, я только изучаю C ++ ... и книга, которую я прохожу, делает действительно плохую работу по объяснению конструкторов таким образом, чтобы я мог их понять. До сих пор я почти все понял, но не могу понять, как на самом деле работает синтаксис для конструкторов.

Например, мне сказали, что следующее заставит конструктор вызвать конструктор назначенного суперкласса:

class something : something_else {
  something(int foo, double bar) : something_else(int foo) {}
};

С другой стороны, тот же самый синтаксис был использован позже в книге при описании инициализации const членов:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

Так ... э-э ... что, черт возьми, там происходит? Что означает синтаксис rv signature(param) : something_else(what); на самом деле ? Я не могу понять, что это за something_else(what) относительно кода вокруг него. Кажется, что он принимает несколько значений; Я уверен, что должен быть какой-то базовый элемент языка, которому он соответствует, я просто не могу понять, что .

Редактировать: Кроме того, я должен отметить, что очень странно, что what в предыдущем примере иногда представляет собой список параметров (так что something_else(what) выглядит как сигнатура функции) ... а иногда и константа -значение выражения (поэтому something_else(what) выглядит как вызов функции).

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

class something : something_else {
//something(int foo, double bar) : something_else() {}
  something(int foo, double bar) {}
};

Любая помощь в поиске этих тем будет очень признательна; Мне не нравится это чувство, что я не понимаю чего-то базового. Мне не нравится вообще .

Редактировать 2: Хорошо, приведенные ниже ответы на данный момент действительно полезны. Они поднимают еще одну часть этого вопроса: как аргументы вызовов базового класса-конструктора в «списках инициализации» связаны с определяемым вами конструктором? Должны ли они соответствовать ... должны ли быть значения по умолчанию? Сколько они должны соответствовать? Другими словами, что из перечисленного незаконно :

class something_else {
  something_else(int foo, double bar = 0.0) {}
  something_else(double gaz) {}
};


class something : something_else {
  something(int foo, double bar)  : something_else(int foo, double bar) {}   };
class something : something_else {
  something(int foo)              : something_else(int foo, double bar) {}   };
class something : something_else {
  something(double bar, int foo)  : something_else(double gaz) {}   };

Ответы [ 7 ]

5 голосов
/ 23 февраля 2010

Синтаксис для определения конструктора:

Type( parameter-list ) : initialization-list 
{
   constructor-body
};

Где 'initialization-list' - это список вызовов конструкторов для разделенных запятыми атрибутов баз и / или атрибутов-членов. Требуется инициализировать любой подобъект (основание или член), для которого нет конструктора по умолчанию, константных подобъектов и ссылочных атрибутов, и во всех остальных случаях его следует отдавать предпочтению перед присваиванием в блоке конструктора.

struct base {
   base( int ) {};
};
struct base2 {
   base2( int ) {};
};
struct type : base, base2
{
   type( int x ) 
      : member2(x), 
        base2(5), 
        base(1), 
        member1(x*2) 
   { f(); }
   int member1;
   int member2;
};

Порядок, в котором выполняется список инициализации, определяется в объявлении класса: базы в порядке, в котором они объявлены, атрибуты-члены в порядке, в котором они объявлены. В примере выше перед выполнением f() в теле конструктора класс инициализирует свои базовые классы и атрибуты в следующей последовательности:

  1. вызов base(int) конструктор с параметром 1
  2. вызов base2(int) конструктор с параметром 5
  3. инициализировать member1 со значением x*2
  4. инициализировать member2 со значением x

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

class unrelated {};
class base {};
class vd1 : virtual base {};
class vd2 : virtual base {};
struct derived : unrelated, vd1, vd2 {
   derived() : unrelated(), base(), vd1(), vd2() {} // in actual order
};

Вкл. Редактировать 2

Я думаю, вы не читаете подробности в ответах. Элементы в списке инициализации являются вызовами конструктора, а не объявлениями. При необходимости компилятор будет применять обычные правила преобразования для вызова.

struct base {
   base( int x, double y );
   explicit base( char x );
};
struct derived : base {
   derived() : base( 5, 1.3 ) {}
   derived( int x ) : base( x, x ) {} 
      // will convert x into a double and call base(int,double)
   derived( double d ) : base( 5 ) {} 
      // will convert 5 to char and call base(char)
// derived( base b ) {} // error, base has no default constructor
// derived( base b, int x ) : base( "Hi" ) {} 
      // error, no constructor of base takes a const char *
};
3 голосов
/ 23 февраля 2010

Эта идиома называется список инициализации .

В основном с каждым элементом, который вы называете конструктором:

class C: public A, public B {
    int a;
    std::string str;

public:
    C(): 
        A(5),            // 1
        B('c'),          // 2
        a(5),            // 3
        str("string")    // 4
    {};
};

В (1) вы вызываете конструктор базового класса, который принимает int в качестве параметра или может выполнить соответствующее преобразование.

В (2) вы вызываете конструктор базового класса, который принимает char в качестве параметра

В (3) вы вызываете «конструктор» для инициализации int, который в этом случае является простым присваиванием

В (4) вы вызываете std::string(const char*) конструктор.

1 голос
/ 23 февраля 2010

В вашем конструкторе вы можете использовать конструкторы вызовов explicity для ваших переменных-членов.

class FileOpener
{
public:
  // Note: no FileOpener() constructor
  FileOpener( string path ){ //Opens a file }
};
class A
{
public:
  A():b("../Path/To/File.txt"){}
  FileOpener b;
};

Это важно, когда ваши переменные-члены не имеют конструкторов по умолчанию.

Аналогично, вы можете явно вызывать конструктор для вашего родительского класса, когда его конструктор по умолчанию не существует или не существует.

class F
{
public:
  // Note: No default constructor again.
  F( int arg ){ var = arg;}
private:
  int var;
};
class D : public F
{
  D(){} //Compiler error! Constructors try to use the parent's default C 
        // constructor by default.
  D( int arg ):C(arg){} //This works!
};

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

Редактировать: Обязательно инициализируйте элементы в том порядке, в котором вы их объявили в заголовке, иначе ваш код будет иметь предупреждения во время компиляции.

1 голос
/ 23 февраля 2010

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

Пример 1:

class something : something_else {
  void something(int foo, double bar) : something_else(int foo) {}
};

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

Пример 2:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

Компилятор может видеть, что у вас есть переменная-член с именем constant_member как часть вашего класса, поэтому он инициализирует ее с предоставленным значением.

Вы можете инициализировать члены и вызывать конструкторы базового класса в одном и том же списке инициализации (вот что такое синтаксис объявления функции в конструкторе - список инициализации).

0 голосов
/ 23 февраля 2010

C ++ имеет очень неоднозначный синтаксис, так что будьте готовы встретить множество похожих синтаксических конструкций с разной симметрией.

Например:

A a(b);

Это может означать создание объекта «a» класса «A» путем вызова его конструктора «A :: A» со значением параметра «b». Но также это может быть объявление функции «a», имеющее формальный параметр типа «b» и возвращающее значение типа «A».

Для «простой» грамматики компилятор может быть реализован в виде конвейера, содержащего почти независимые модули: Lexer, Parser, Semantic Analyzer и т. Д. Обычно такие языки не только просты для принятия решения компиляторами, но и легки для понимания людьми (программистами).

C ++ имеет очень сложную грамматику (и семантику). Следовательно, без семантической информации синтаксический анализатор C ++ не может решить, какое правило грамматики применить. Это приводит к трудностям в разработке и реализации компилятора C ++. Кроме того, C ++ затрудняет понимание программ программистами.

Так что корень ваших проблем в понимании синтаксиса не в вашей голове, а в грамматике C ++.

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

0 голосов
/ 23 февраля 2010

Книга пытается объяснить C ++ списки инициализации . В общем, список инициализации состоит из вызовов конструктора, как для конструкторов родительских классов, так и для конструкторов свойств классов.

Список инициализации должен состоять из (по порядку):

  1. Конструкторы базового класса
  2. Конструкторы свойств класса

Сначала должны быть вызваны все конструкторы базовых классов. Порядок вызовов конструктора базового класса определяется компилятором. Как указано в C ++ FAQ :

[Конструкторы базовых классов] ... являются выполняется в порядке их появления в первый слева направо обход глубины график базовых классов, где осталось право ссылаться на порядок появление имен базовых классов.

Таким образом, порядок конструкторов базового класса в списке инициализации не имеет значения. Если конструктор базового класса не указан явно в списке инициализации, будет вызван конструктор по умолчанию.

После вызовов конструктора базового класса идут вызовы конструктора свойства класса. Они похожи на вызовы функций, но по сути являются допустимым способом инициализации переменных, который называется Инициализация конструктора . Например, следующий фрагмент кода C ++ является совершенно допустимым:

int i(0);

Обратите внимание, что порядок свойств класса в списке инициализации должен соответствовать порядку определения в заголовке класса.

Наконец, стоит упомянуть, что использование списков инициализации является хорошей практикой. Это более эффективно, чем использование присваиваний в теле конструктора, поскольку устраняет начальную конструкцию свойства класса со значением по умолчанию или неопределенным значением и обеспечивает строгий порядок инициализации.

0 голосов
/ 23 февраля 2010

В списках инициализатора конструктора вы пишете data_member(val), чтобы инициализировать data_member с val. Обратите внимание, что val может быть выражением, даже тем, которое может быть оценено только во время выполнения. Если элемент данных является объектом, его конструктор будет вызываться с этим значением. Кроме того, если у него есть конструктор, ожидающий несколько аргументов, вы можете передать их все, как при вызове функции, например, data_member(i, j, k). Теперь для целей такой инициализации вы должны думать о части объекта базового класса как о члене данных, имя которого является просто именами базового класса. Отсюда MyBase(val) или MyBase(i, ,j ,k). Будет вызван конструктор базового класса. Множественное наследование работает одинаково. Просто инициализируйте те базовые классы, которые вы хотите, как отдельные элементы в списке: MyBase1(x), MyBase2(y). Базовые классы, чьи конструкторы вы не вызываете явно, будут инициализированы их конструкторами по умолчанию, если они существуют. Если они этого не сделают, код не скомпилируется, если вы не инициализируете явно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...