2010-10-26 3 views
2

Я начинаю играть с C++, исходя из C и Objective C (и немного Java). Я думал, что неплохо начать строить свои навыки, написав простую хеш-таблицу с нуля, используя связанные списки для коллизий. Поэтому я начал писать скелеты для каждого класса.C++ class ordering

class HashTable 
{ 
    public: 
    ... 
    private: 
    ... 
}; 

class LinkedList 
{ 
    public: 
    ... 
    private: 
    Node *root; 
}; 

class Node 
{ 
    public: 
    Node *next; 
    string key; 
    int value; 
    Node() 
    { 
     ... 
    } 
}; 

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

error: expected type-specifier before ‘Node’ 

относительно корневого узла в классе LinkedList.

Когда я просто переупорядочил классы, так что это было Node{...}; LinkedList{...}; HashTable{...}; все работало, как хорошо смазанный грузовик мороженого.

Теперь я не стану подвергать сомнению дизайн C++, но есть ли причина для этого ограничения? Если я правильно помню, Obj. Класс C по существу превращается в таблицы и выглядит «на лету». Итак, в чем причина такого поведения?

+1

Я не знаю. Если у меня есть одна жалоба на C++, это так. Другие люди жалуются на уродливый синтаксис, я этого не понимаю, мне кажется, что синтаксис довольно изящный. Или они жалуются, что у него нет функций высокого уровня. Который, на мой взгляд, полный вздор, для чего нужны библиотеки. Если бы я мог исправить одну вещь о C++, это была бы эта проблема, которую вы описали. –

+0

Вы прочитали книгу на C++, верно? – GManNickG

+1

Я действительно разместил это, а затем отправился в библиотеку, чтобы схватить его! – kodai

ответ

4

Требование к объявлениям такого рода исходит от двух сил. Во-первых, это упрощает разработку компилятора. Поскольку типы и переменные имеют одну и ту же структуру идентификатора, компилятор должен знать, с чем он сталкивается, когда он анализирует идентификатор. Есть два способа сделать это. Один из способов - потребовать, чтобы каждый идентификатор был объявлен, прежде чем он может использоваться в других определениях. Это означает, что код должен пересылать объявить любое имя, которое он намеревается использовать, прежде чем давать свое определение. Это очень простой способ написать компилятор с иначе неоднозначной грамматикой.

Другой способ сделать это - обработать его в несколько проходов. Каждый раз, когда встречается необъявленный идентификатор, он пропускается, и компилятор пытается его решить, как только он проанализирует весь файл. Оказывается, что грамматика C++ делает это очень сложно сделать правильно. Писатели-компиляторы не хотели идти на эту проблему, и поэтому у нас есть декларации.

Другая причина заключается в том, что вы действительно можете иметь передовые декларации, чтобы рекурсивные структуры были детерминированы как внутреннее свойство языка. Это немного более тонко. Предположим, вы написали взаимно рекурсивную сеть класса:

class Bar; // forward declaration 
class Foo { 
    Bar myBar; 
}; 

class Bar { 
    int occupySpace; 
    Foo myFoo; 
}; 

Это, очевидно, невозможно, так как occupySpace член появится в бесконечно вложенной рекурсии. требуя, чтобы для каждого участника в определении было указано конкретное количество информации для этого. В частности, он позволяет компилятору достаточно информации для формирования ссылки на класс, но не для создания экземпляра класса (поскольку его размер неизвестен).Передовые декларации делают это особенностью синтаксиса языка, подобно тому, как lvalues ​​назначаются как функция синтаксиса языка, а не более тонкие семантические требования или требования времени выполнения.

4

Компилятор бросает следующую ошибку

error: expected type-specifier before ‘Node’ 

, потому что он (компилятор) не (пока)

Node *root; 

, что Node есть. (Поскольку Node определена позже.)

Два возможных решения:

  • Put определение Node класса перед тем LinkedList класса (вы уже знаете)

  • вперед объявить класс Node перед тем класс LinkedList путем помещения этой строки

    класс Узел;

    Это говорит компилятору, что существует класс Node.

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

PS: Nitpick, для LinkedList, переменное имя head может быть более подходящим, чем root.

+2

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

0

Вы заявили, что ваш класс LinkedList имеет тип Node, но компилятор не знает, что такое Node, потому что он еще не объявлен.

Просто объявить Node до того LinkedList

3

Причиной такого поведения является историческим. Файл обрабатывается последовательно.В то время, когда речь идет о первой ссылке на идентификатор, этот идентификатор должен быть уже объявлен.

Компилятор не обрабатывает весь файл первым.

Вместо повторного заказ определения класса, вы часто можете уйти с упреждающим объявлением

class Node; 

class List 
{ 
    public: 
    //... 
    private: 
    Node *root; 
    //... 
}; 

//...
0

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

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

1

Подумайте об этом наоборот; если разумно принять класс, объявленный в любом месте файла как ОК, почему бы не класс, объявленный в другом файле, который еще не встречен?

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

+0

-1 IMHO, я не думаю, что это правильный аргумент. C++ требует, чтобы все использовалось для определения в одном файле (препроцессор обрабатывает все #includes и, следовательно, не является частью фактического этапа компиляции C++, поэтому все заканчивается в одном виртуальном файле), и это можно рассматривать как довольно логичное условие так как нет ничего, что могло бы рассказать компилятору, какие другие файлы компилируются в один двоичный файл (более того, иногда они не компилируются в исполняемый файл/библиотеку, а вместо этого распределяется файл «object»). (Продолжение ...) –

+0

(... продолжение) С другой стороны, компилятор знает обо всех данных в одном виртуальном файле (так как это все его задано), и поэтому его не слишком необоснованно предположить, что современный компилятор сможет выполнить сканирование сначала, чтобы обнаружить все типы и пространства имен и создать свою информацию за пару проходов. Единственная проблема, которую я вижу с добавлением этого стандарта, будет заключаться в том, что он, вероятно, заставит полностью переписать большинство компиляторов, что просто невозможно, из-за 1000 человеческих часов, которые вошли в оптимизацию и разработку указанных компиляторов. –

+0

C# (и java) отлично справляется с наличием классов, определенных в любом порядке и в любом файле. В этом нет ничего невозможного; вам просто нужно сделать несколько проходов – pm100

2

Нет никаких технических оснований для этого ограничения - это подтверждается тем фактом, что компилятор делает то, что, по вашему мнению, ожидается в контексте одного класса. Тем не менее удаление «ограничения» еще больше усложняет компиляторы, замедляет их, увеличивает использование памяти и, что крайне важно, не будет обратно совместимо (поскольку совпадения в более локализованной области (пространстве имен), по-видимому, будут выбраны над другими символами, замеченными ранее) ,

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