Декларация против определения
Совет: Этот первый раздел, «Декларация против определения», немного плотный. Понимание этих концепций перед просмотром образца поможет вам понять образец. В свою очередь, просмотр образца поможет вам понять эти понятия. Я рекомендую вам прочитать это, а затем просмотреть образцы в следующих двух разделах. Если части этого раздела не были ясны, вернитесь, чтобы перечитать этот раздел.
В C # классы и другие типы объявляются и определяются одновременно. Даже с частичным ключевым словом определение класса просто может распространяться на несколько файлов; это не меняет комбинацию объявления и определения. Единственное исключение из этого правила — при выполнении взаимодействия (в котором используется DllImportAttribute и ключевое слово extern для объявления функции, определенной во внешней DLL). В этом случае определение отсутствует в C #, но почти наверняка в какой-то не-.NET библиотеке. (Если DLL была сборкой .NET, вы можете просто добавить ссылку на нее и использовать ее без необходимости какого-либо кода взаимодействия.)
Я пишу это потому, что в C ++ объявление и определение обычно могут быть разделены, и часто это так. Распространено видеть класс, объявленный в файле заголовка (который, по соглашению, имеет суффикс .H) и определенный в исходном файле (который, по соглашению, имеет суффикс .CPP). Это верно не только для классов, но и для автономных функций и даже структур и объединений, когда у них есть связанные с ними функции-члены.
Ожидайте увидеть одну или несколько строк #include «SomeHeader.h» в верхней части файла .CPP. Эти операторы сообщают компилятору (или, точнее, препроцессору), что в этом файле или включенных в него файлах есть объявления и, возможно, определения, которые необходимы компилятору для понимания частей кода C ++, который следует.
В Visual C ++ при включении заголовка, который является частью вашего проекта или не найден в пути включения системы сборки, используйте синтаксис #include «HeaderFile.h». При включении системного включаемого файла, такого как Windows.h, используйте синтаксис #include <Windows.h>. Наконец, при включении включаемого файла, который является частью стандартной библиотеки C ++ (о чем мы поговорим позже), используйте синтаксис #include <vector> (т. Е. Не включать .h). Значение «» по сравнению с синтаксисом <> для включения файлов определяется реализацией, хотя в GCC и Visual C ++ используется синтаксис в кавычках для локальных заголовочных файлов и синтаксис в квадратных скобках для системных заголовочных файлов.
Примечание . Причина, по которой суффикс .H был исключен из включаемых файлов стандартной библиотеки C ++, заключался в том, чтобы избежать коллизий имен с компиляторами C ++, которые уже предоставили файлы заголовков, которые использовали эти имена при представлении стандартной библиотеки C ++. Это обычные заголовочные файлы, не бойтесь.
Чтобы понять, почему разница между объявлением и определением имеет значение в C ++, важно иметь общее представление о процессе сборки C ++. Вот что обычно происходит:
- Препроцессор проверяет исходный файл, вставляет текст файлов, указанных операторами включения (и текст файлов, указанных их операторами включения и т. Д.), А также оценивает и действует на любые другие директивы препроцессора (например, расширяющиеся макросы). ) и любые директивы прагмы.
- Компилятор берет выходные данные препроцессора и компилирует этот код в машинный код, который он сохраняет вместе с другой информацией, необходимой для этапа компоновки, в файле OBJ.
Шаги 1 и 2 повторяются для каждого исходного файла в проекте. - Шаги 1 и 2 повторяются для каждого исходного файла в проекте.
- Компоновщик проверяет выходные файлы компилятора и библиотечные файлы, на которые ссылается ваш проект. Он находит все места, где компилятор идентифицировал что-то как объявленное, но не определенное в этом конкретном исходном файле. Затем он находит соответствующий адрес для определения и патчи, которые адрес в.
- После того, как все было успешно связано, компоновщик связывает все вместе и выводит готовый продукт (обычно это исполняемая программа или файл библиотеки).
Конечно, ошибка на любом из этих этапов остановит процесс сборки, и предыдущее описание является лишь приблизительным наброском цепочки сборки Visual C ++. Авторы компилятора имеют некоторую гибкость в том, как они делают вещи. Например, не требуется создавать какие-либо промежуточные файлы, поэтому теоретически весь процесс сборки может быть выполнен в памяти, хотя на практике я сомневаюсь, что кто-нибудь когда-либо сделает это. Так что рассматривайте этот список как грубую схему, а не точное описание.
Я имел в виду все как исходные файлы, чтобы сохранить простую терминологию. В стандарте C ++ эти комбинации исходного файла и всех его включаемых файлов называются модулем компиляции. Я упоминаю об этом сейчас только потому, что я буду использовать термин чуть дальше. Давайте рассмотрим три этапа сборки по очереди.
Препроцессор не заботится о объявлениях и определениях C ++. На самом деле, даже не важно, находится ли ваша программа на C ++. Единственное, что имеет дело с вашими исходными файлами, — это заботиться о всех строках, начинающихся с #, помечая их как директивы препроцессора. Пока эти строки правильно сформированы, и он может найти все включенные файлы, если таковые имеются, препроцессор будет выполнять свою работу, добавляя и удаляя текст в соответствии с указаниями. Он будет передавать результаты компилятору, как правило, не записывая свой результат в файл, так как компиляция следует сразу за предварительной обработкой.
Компилятор заботится об объявлениях и определениях и очень обеспокоен тем, является ли ваша программа допустимым кодом C ++ или нет. Однако ему не нужно знать, что делает функция, когда сталкивается с ней. Ему просто нужно знать, что такое сигнатура функции, например int AddTwoNumbers (int, int) ;.
То же самое верно для классов, структур и союзов; до тех пор, пока компилятор знает объявление (или, в случае указателя, просто то, что конкретный токен является классом, структурой, объединением или перечислением), ему не нужны никакие определения. С помощью всего лишь объявления он знает, является ли ваш вызов AddTwoNumbers синтаксически правильным и что это класс Vehicle; на самом деле это класс, поэтому он может создать указатель на него, когда увидит Vehicle * v ;, и это все, что его волнует.
Компоновщик заботится об определениях. В частности, важно, чтобы было одно и только одно определение, соответствующее каждому объявлению в вашем проекте. Единственным исключением являются встроенные функции, которые в конечном итоге создаются в каждом модуле компиляции, в котором они используются. Однако они созданы таким образом, чтобы избежать проблем с несколькими определениями.
Вы можете иметь дубликаты объявлений среди модулей компиляции для вашей программы; это обычная уловка для улучшения времени сборки, если только одно определение соответствует объявлению (кроме встроенных). Чтобы обеспечить соблюдение этого единого правила определения, компиляторы C ++ обычно используют что-то, называемое искажение имени.
Это гарантирует, что каждое объявление соответствует своему правильному определению, включая такие проблемы, как перегруженные функции и пространства имен (которые позволяют использовать одно и то же имя, если использования находятся в разных пространствах имен), а определения классов, структур, объединений и перечислений, вложенные в классы, структуры или союзы.
Это искажение имени приводит к ужасным ошибкам компоновщика, пример которых мы увидим в разделе «Встроенные функции-члены».
Отделимость объявлений от определений позволяет создавать проекты C ++ без перекомпиляции каждого исходного файла каждый раз. Он также позволяет создавать проекты, использующие библиотеки, для которых у вас нет исходного кода. Конечно, есть и другие способы достижения этих целей (например, C # использует другой процесс сборки). Так C ++ делает это; понимание того, что основной поток помогает понять многие особенности C ++, с которыми вы не сталкиваетесь в C #.
функции
В C ++ есть два типа функций: автономные функции и функции-члены. Основное различие между ними заключается в том, что функция-член принадлежит классу, структуре или объединению, тогда как автономная функция — нет.
Автономные функции — это самые основные типы функций. Они могут быть объявлены в пространствах имен, они могут быть перегружены и могут быть встроенными. Давайте посмотрим на несколько.
Образец: FunctionsSample \ Utility.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
#pragma once
namespace Utility
{
inline bool IsEven(int value)
{
return (value % 2) == 0;
}
inline bool IsEven(long long value)
{
return (value % 2) == 0;
}
void PrintIsEvenResult(int value);
void PrintIsEvenResult(long long value);
void PrintBool(bool value);
}
|
Образец: FunctionsSample \ Utility.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include «Utility.h»
#include <iostream>
#include <ostream>
using namespace std;
using namespace Utility;
void Utility::PrintIsEvenResult(int value)
{
wcout << L»The number » << value << L» is » <<
(IsEven(value) ? L»» : L»not «) << L»even.»
<< endl;
}
void Utility::PrintIsEvenResult(long long value)
{
wcout << L»The number » << value << L» is » <<
(IsEven(value) ? L»» : L»not «) << L»even.»
<< endl;
}
void Utility::PrintBool(bool value)
{
wcout << L»The value is» <<
(value ? L»true.» : L»false.») << endl;
}
|
Образец: FunctionsSample \ FunctionsSample.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include «Utility.h»
#include «../pchar.h»
using namespace Utility;
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
int i1 = 3;
int i2 = 4;
long long ll1 = 6;
long long ll2 = 7;
bool b1 = IsEven(i1);
PrintBool(b1);
PrintIsEvenResult(i1);
PrintIsEvenResult(i2);
PrintIsEvenResult(ll1);
PrintIsEvenResult(ll2);
return 0;
}
|
Заголовочный файл Utility.h объявляет и определяет две встроенные функции, обе называются IsEven (что делает IsEven перегруженной функцией). Он также объявляет еще три функции: две с именем PrintIsEvenResult и одна с именем PrintBool. Исходный файл Utility.cpp определяет эти последние три функции. Наконец, исходный файл FunctionsSample.cpp использует этот код для создания простой программы.
Любые функции, определенные в заголовочном файле, должны быть объявлены встроенными; в противном случае вы получите несколько определений и ошибку компоновщика. Кроме того, перегрузки функций должны отличаться не только их типом возврата; в противном случае компилятор не может убедиться, что вы действительно получаете ту версию метода, которую вы хотели. C # тоже самое, так что в этом не должно быть ничего нового.
Как видно из Utility.cpp, когда вы определяете автономную функцию, которая находится в пространстве имен, вам нужно поместить пространство имен перед именем функции и отделить его с помощью оператора разрешения области. Если вы использовали вложенные пространства имен, вы включаете всю цепочку вложений пространства имен — например, void RootSpace :: SubSpace :: SubSubSpace :: FunctionName (int param) {…} ;.
Простой класс
Следующий пример включает класс, разбитый на файл заголовка и исходный файл.
Пример: SimpleClassSample \ VehicleCondition.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
#pragma once
#include <string>
namespace Inventory
{
enum class VehicleCondition
{
Excellent = 1,
Good = 2,
Fair = 3,
Poor = 4
};
inline const std::wstring GetVehicleConditionString(
VehicleCondition condition
)
{
std::wstring conditionString;
switch (condition)
{
case Inventory::VehicleCondition::Excellent:
conditionString = L»Excellent»;
break;
case Inventory::VehicleCondition::Good:
conditionString = L»Good»;
break;
case Inventory::VehicleCondition::Fair:
conditionString = L»Fair»;
break;
case Inventory::VehicleCondition::Poor:
conditionString = L»Poor»;
break;
default:
conditionString = L»Unknown Condition»;
break;
}
return conditionString;
}
}
|
Образец: SimpleClassSample \ Vehicle.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#pragma once
#include <string>
namespace Inventory
{
enum class VehicleCondition;
class Vehicle
{
public:
Vehicle(
VehicleCondition condition,
double pricePaid
);
~Vehicle(void);
VehicleCondition GetVehicleCondition(void)
{
return m_condition;
};
void SetVehicleCondition(VehicleCondition condition);
double GetBasis(void) { return m_basis;
private:
VehicleCondition m_condition;
double m_basis;
};
}
|
Образец: SimpleClassSample \ Vehicle.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
#include «Vehicle.h»
#include «VehicleCondition.h»
using namespace Inventory;
using namespace std;
Vehicle::Vehicle(VehicleCondition condition, double pricePaid) :
m_condition(condition),
m_basis(pricePaid)
{
}
Vehicle::~Vehicle(void)
{
}
void Vehicle::SetVehicleCondition(VehicleCondition condition)
{
m_condition = condition;
}
|
Пример: SimpleClassSample \ SimpleClassSample.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <ostream>
#include <string>
#include <iomanip>
#include «Vehicle.h»
#include «VehicleCondition.h»
#include «../pchar.h»
using namespace Inventory;
using namespace std;
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
auto vehicle = Vehicle(VehicleCondition::Excellent, 325844942.65);
auto condition = vehicle.GetVehicleCondition();
wcout << L»The vehicle is in » <<
GetVehicleConditionString(condition).c_str() <<
L» condition. Its basis is $» << setw(10) <<
setprecision(2) << setiosflags(ios::fixed) <<
vehicle.GetBasis() << L».»
return 0;
}
|
В Vehicle.h мы начинаем с предварительного объявления перечисляемого класса VehicleCondition. Мы обсудим эту технику подробнее в конце главы. На данный момент ключевыми моментами являются (1) то, что мы можем использовать это предварительное объявление или включить заголовочный файл VehicleCondition.h и (2) что объявление VehicleCondition должно предшествовать определению класса для Vehicle.
Чтобы компилятор выделил достаточно места для экземпляров Vehicle, он должен знать, насколько велик каждый элемент данных Vehicle. Мы можем сообщить об этом либо включив соответствующий заголовочный файл, либо, при определенных обстоятельствах, используя предварительное объявление. Если объявление VehicleCondition пришло после определения Vehicle, тогда компилятор откажется компилировать код, поскольку компилятор не будет знать, насколько велика VehicleCondition или даже какой тип данных.
В этом случае достаточно простого объявления, чтобы сообщить компилятору, что такое VehicleCondition (класс enum) и насколько оно велико. Классы enum по умолчанию используют int в качестве вспомогательного поля, если не указано иное. Если мы оставим поле поддержки пустым, но затем скажем, что нужно использовать короткое, длинное или какое-либо другое поле типа поддержки где-то еще, компилятор выдаст другое сообщение об ошибке, сообщающее нам, что у нас есть несколько конфликтующих объявлений.
Затем мы переходим к определению класса Vehicle. Определение включает в себя объявление своих функций-членов и переменных-членов. По большей части мы не определяем функции-члены. Исключениями являются функция-член GetVehicleCondition и функция-член GetBasis, которые мы обсудим в разделе «Встроенные функции-члены».
Мы определяем другие функции-члены Vehicle в Vehicle.cpp. В этом случае функциями-членами являются конструктор, деструктор и SetVehicleCondition. Обычно такая функция, как SetVehicleCondition, будет встроенной, как и простые конструкторы и деструкторы в классе Vehicle. Они определены здесь отдельно, чтобы проиллюстрировать, как вы определяете эти типы функций-членов, когда они не являются встроенными функциями. Мы обсудим странный синтаксис конструктора в главе, посвященной конструкторам. Остальная часть кода класса транспортного средства должна быть четкой.
Примечание. Хотя вы не обязаны принимать соглашение об именах файлов ClassName.h или ClassName.cpp, вы увидите, что оно используется практически повсеместно, поскольку это облегчает использование и обслуживание кода.
Встроенная функция GetVehicleConditionString в VehicleCondition.h возвращает копию std :: wstring, созданную в этой функции, а не само локальное значение. Исходя из C #, вы можете подумать, что это немного странно без использования нового ключевого слова. Мы рассмотрим это, когда будем обсуждать автоматический тип продолжительности в главе о продолжительности хранения.
Функция точки входа использует некоторые функции форматирования ввода / вывода стандартной библиотеки C ++.
Функции члена
Как обсуждалось ранее, функции-члены являются частью класса, структуры или объединения. Просто я буду говорить о них как об участниках класса.
Статические функции-члены могут вызывать другие функции-члены статического класса независимо от уровня защиты. Статические функции-члены могут также получать доступ к данным статического члена класса либо явно (т. SomeClass::SomeFloat = 20.0f;
), либо неявно (т. SomeFloat = 20.0f;
), независимо от уровня защиты.
Явная форма полезна, если у вас есть параметр с тем же именем, что и у члена класса. Префикс данных члена с m_, такой как m_SomeFloat, устраняет эту проблему и проясняет, когда вы работаете с данными члена класса по сравнению с локальными переменными или параметрами. Это просто выбор стиля, а не требование.
Членским функциям экземпляра (т.е. не статичным) автоматически присваивается указатель this на данные экземпляра для экземпляра, для которого они были вызваны. Функции-члены экземпляра могут вызывать другие функции-члены класса и получать доступ ко всем данным члена класса либо явно — так же, как статические члены, использующие this-> m_count ++; например данные — или неявно — такие же, как статические данные и данные экземпляра (например, m_data ++;), независимо от уровня защиты.
Встроенные функции-члены
В SampleClass \ Vehicle.h объявлены и определены функции-члены GetVehicleCondition
и GetBasis
. Эта комбинация объявления и определения называется встроенной функцией-членом в C ++. Так как это похоже на написание методов в C #, это может быть также полезно для C ++. За некоторыми исключениями, вы не должны этого делать.
Как мы уже говорили ранее, когда вы создаете проект C ++, компилятор просматривает каждый из ваших исходных файлов только один раз. Он может сделать много проходов в одних и тех же исходных файлах, чтобы оптимизировать их, но он не вернется после завершения.
Напротив, компилятор будет возвращаться к вашим заголовочным файлам каждый раз, когда они включены в другой файл, независимо от того, является ли это исходным файлом или другим заголовочным файлом. Это означает, что компилятор может выполнить код в заголовочных файлах много-много раз во время сборки.
В начале заголовочного файла SampleClass \ Vehicle.h вы видите директиву #pragma Once. Это полезная и важная строка. Если вы включите заголовочный файл Ah в исходный файл, а затем включите в него другой заголовочный файл с Ah, директива #pragma Once сообщит препроцессору не включать содержимое Ah снова. Это предотвращает подпрыгивание препроцессора между двумя заголовочными файлами, которые бесконечно включают друг друга. Это также предотвращает ошибки компилятора. Если Ah был включен несколько раз, компилятор не будет работать, когда он достигнет определения типа из второго включения Ah
Даже с этой директивой компилятору по-прежнему необходимо включать и анализировать этот код файла заголовка для каждого исходного файла, который его включает. Чем больше вещей вы поместите в заголовочный файл, тем больше времени потребуется для создания каждого исходного файла. Это увеличивает время компиляции, которое, как вы обнаружите, может быть довольно продолжительным в проектах C ++ по сравнению с C #.
Когда вы включаете определение функции-члена, встроенное в заголовочный файл, компилятор C ++ может сделать этот код встроенным в любом исходном файле, где используется эта функция. Как правило, это приводит к более быстрому выполнению программы, поскольку вместо необходимости вызывать функцию программа может просто выполнить код на месте.
Область действия сохраняется компилятором, поэтому вам не нужно беспокоиться о именовании коллизий между переменными, определенными во встроенной функции и в функции, где она используется. При работе с кодом, например в предыдущих примерах, где вы просто извлекаете значение переменной-члена, встроенные определения могут повысить скорость, особенно если код выполняется внутри цикла.
Существует альтернативный способ определения встроенной функции-члена. Если вы хотите, чтобы ваши определения классов были аккуратными и чистыми, без определений функций-членов внутри них, но при этом все же хотели бы иметь некоторые встроенные функции-члены, вы можете вместо этого сделать что-то вроде следующего:
Пример: SimpleClassSample \ Vehicle.h (альтернативный код закомментирован внизу файла).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#pragma once
#include <string>
namespace Inventory
{
enum class VehicleCondition;
class Vehicle
{
public:
Vehicle(
VehicleCondition condition,
double pricePaid
);
~Vehicle(void);
inline VehicleCondition GetVehicleCondition(void);
void SetVehicleCondition(VehicleCondition condition);
inline double GetBasis(void);
private:
VehicleCondition m_condition;
double m_basis;
};
VehicleCondition Vehicle::GetVehicleCondition(void)
{
return m_condition;
}
double Vehicle::GetBasis(void)
{
return m_basis;
}
}
|
Кстати, ошибки компоновщика всегда выглядят ужасно. Причина в том, что компоновщик больше не знает, как ваши переменные и функции были названы в исходном файле. Он знает только то, во что компилятор преобразовал эти имена, чтобы сделать все имена уникальными. Это включает методы перегрузки, которым необходимо уникальное имя на этапе компоновки, чтобы компоновщик мог подключить вызов перегруженной функции-члена к правильной версии перегрузки этой функции.
Ошибки на рисунке 1 просто говорят нам, что мы определили Inventory::Vehicle::GetVehicleCondition(void)
более одного раза. Теперь мы знаем, что мы определили его только один раз, просто в файле заголовка, но мы включили файл заголовка и в Vehicle.cpp, и в Main.cpp в проекте SimpleClassSample.
Поскольку мы намеренно забыли добавить ключевое слово inline в объявление функции Vehicle :: GetVehicleCondition, компилятор не делает код встроенным. Вместо этого он компилирует его как функцию в Main.cpp и Vehicle.cpp.
Это, конечно, хорошо для компилятора, потому что он рассматривает каждый исходный файл как уникальный модуль компиляции. Компилятор не знает ничего лучше, так как к тому времени, когда код достигает его, код уже вставлен на этапе препроцессора. Только когда компоновщик получает весь скомпилированный код и пытается сопоставить все, мы достигаем фазы, когда процесс сборки говорит: «Эй, у меня уже есть другая версия этой функции!», А затем происходит сбой.
Как видите, есть два способа сделать функции-члены встроенными. И то, и другое должно быть сделано в файле заголовка, поскольку компилятор будет оценивать код файла заголовка столько раз, сколько они включены, но он будет проходить через исходные файлы только один раз. Если вы используете второй метод и забудете встроенное ключевое слово, то у вас будут ужасные ошибки компоновщика. Если вы используете второй метод и запомните ключевое слово inline, но определите функции в исходном файле, вы получите ужасные ошибки компоновщика — на этот раз говорят, что определения нет.
Совет: не пытайтесь все встроить. Вы просто получите медленное время компиляции, которое снизит вашу производительность. Делайте встроенные вещи, которые имеют смысл, такие как простые функции получения и установки для переменных-членов. Как и все остальное, сначала профиль, а затем оптимизировать при необходимости.
Уровни защиты и спецификаторы доступа
Функции-члены и данные-члены имеют три возможных спецификатора доступа:
- общественности
- защищенный
- частный
Эти спецификаторы доступа обозначают уровень доступности, который имеет член. В SampleClass \ Vehicle.h вы можете увидеть два примера их использования. Обратите внимание, что в отличие от C #, вы не пересчитываете спецификатор доступа перед каждым членом. Вместо этого вы указываете спецификатор доступа, за которым следует двоеточие (например, public :), а затем каждому объявлению и определению, которое следует после, предоставляется этот уровень доступности, пока вы не достигнете другого спецификатора доступа.
По умолчанию члены класса являются частными. Это означает, что если у вас нет спецификатора доступа в начале объявления класса, то все объявленные члены будут приватными, пока не будет достигнут спецификатор доступа. Если ничего не достигнуто, у вас будет полностью закрытый класс, что будет очень странно.
Члены структуры по умолчанию являются открытыми, поэтому иногда вы видите структуру без каких-либо спецификаторов доступа. Если вы хотите использовать их в структуре, они работают так же, как в классе.
Наконец, вы можете использовать один и тот же спецификатор доступа более одного раза; если вы хотите организовать свой класс, чтобы сначала определить функции-члены, а затем переменные-члены (или наоборот), вы можете легко сделать что-то вроде этого:
Примечание: этот код является только пояснительным; он не включен ни в один из образцов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
#include <string>
class SomeClass
{
public:
SomeClass(void);
virtual ~SomeClass(void);
int AddTwoInts(int, int);
void StoreAString(const wchar_t*);
private:
bool CheckForIntAdditionOverflow(int, int);
public:
int SomePublicInteger;
protected:
std::wstring m_storedString;
};
|
Предыдущее определение класса не определяет ничего особенно полезного. Однако это служит примером использования всех трех спецификаторов доступа. Это также демонстрирует, что вы можете использовать спецификаторы более одного раза, например public в предыдущем примере.
наследование
При указании классов, из которых происходит ваш класс В C ++ вы также должны указать спецификатор доступа. Если нет, вы получите уровни доступа по умолчанию: private для класса и public для структуры. Обратите внимание, что я сказал классы. C ++ поддерживает множественное наследование. Это означает, что класс или структура могут иметь более одного прямого базового класса или структуры, в отличие от C #, где класс может иметь только одного родителя.
C ++ не имеет отдельного типа интерфейса. В общем случае следует избегать множественного наследования, за исключением случаев, когда существует обходной путь отсутствия отдельного интерфейса. Другими словами, класс должен иметь только ноль или один реальный базовый класс вместе с нулем или более чисто абстрактными классами (интерфейсами). Это всего лишь личная рекомендация по стилю.
Есть несколько хороших аргументов для множественного наследования. Например, скажем, у вас есть три группы функциональности. Каждый состоит из функций и данных. Затем скажите, что каждая группа не связана с другой — между ними нет никакой связи, но они не являются взаимоисключающими. В этом случае вы можете поместить каждую функциональную группу в отдельный класс. Затем, если у вас возникнет ситуация, когда вы захотите создать класс, которому нужны две из этих групп или даже все три, вы можете просто создать класс, который наследует от всех трех, и все готово.
Или вы закончили, если у вас не было конфликтов имен в функциях и переменных вашего открытого и защищенного членов. Например, что если все три функциональные группы имеют функцию-член void PrintDiagnostics(void);
? Вы были бы обречены, да? Что ж, получается, что нет, вы не обречены (как правило). Вам нужно использовать какой-то странный синтаксис, чтобы указать, какую функцию базового класса PrintDiagnostics вы хотите. И даже тогда вы еще не закончили.
C ++ позволяет вам указать, хотите ли вы, чтобы класс был простым базовым классом или виртуальным базовым классом. Вы делаете это, помещая или не помещая ключевое слово virtual перед именем класса в спецификаторе базового класса. Вскоре мы рассмотрим пример, который решает все эти вопросы, но прежде чем мы это сделаем, важно понимать, что если вы наследуете класс как минимум дважды, а два или более наследования не являются виртуальными, вы получите несколько копии членов данных этого класса
Это вызывает массу проблем при попытке указать, какой из них вы хотите использовать. По-видимому, решение состоит в том, чтобы извлекать практически все, но с этим связано снижение производительности во время выполнения из-за того, что реализации C ++ имеют тенденцию разрешать виртуальные члены. Более того, постарайтесь не допустить, чтобы это когда-либо происходило, но так как это не всегда возможно, помните о виртуальном наследовании.
А теперь образец, чтобы помочь сделать все это имеет смысл:
Пример: InheritanceSample \ InheritanceSample.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
|
#include <iostream>
#include <ostream>
#include <string>
#include <typeinfo>
#include «../pchar.h»
using namespace std;
class A
{
public:
A(void) : SomeInt(0) { }
virtual ~A(void) { }
const wchar_t* Id(void) const { return L»A»;
virtual const wchar_t* VirtId(void) const { return L»A»;
int GetSomeInt(void) const { return SomeInt;
int SomeInt;
};
class B1 : virtual public A
{
public:
B1(void) :
A(),
m_fValue(10.0f)
{
// Because SomeInt isn’t a member of B, we
// cannot initialize it in the initializer list
// before the open brace where we initialize the
// A base class and the m_fValue member data.
SomeInt = 10;
}
virtual ~B1(void) { }
const wchar_t* Id(void) const { return L»B1″;
virtual const wchar_t* VirtId(void) const override
{
return L»B1″;
}
const wchar_t* Conflict(void) const { return L»B1::Conflict()»;
private:
float m_fValue;
};
class B2 : virtual public A
{
public:
B2(void) : A() { }
virtual ~B2(void) { }
const wchar_t* Id(void) const { return L»B2″;
virtual const wchar_t* VirtId(void) const override
{
return L»B2″;
}
const wchar_t* Conflict(void) const { return L»B2::Conflict()»;
};
class B3 : public A
{
public:
B3(void) : A() { }
virtual ~B3(void) { }
const wchar_t* Id(void) const { return L»B3″;
virtual const wchar_t* VirtId(void) const override
{
return L»B3″;
}
const wchar_t* Conflict(void) const { return L»B3::Conflict()»;
};
class VirtualClass : virtual public B1, virtual public B2
{
public:
VirtualClass(void) :
B1(),
B2(),
m_id(L»VirtualClass»)
{ }
virtual ~VirtualClass(void) { }
const wchar_t* Id(void) const { return m_id.c_str();
virtual const wchar_t* VirtId(void) const override
{
return m_id.c_str();
}
private:
wstring m_id;
};
// Note: If you were trying to inherit from A before inheriting from B1
// and B3, there would be a Visual C++ compiler error.
// tried to inherit from it after B1 and B3, there would still be a
// compiler warning.
// from a class, it is impossible to get at the direct inheritance
// version of it.
class NonVirtualClass : public B1, public B3
{
public:
NonVirtualClass(void) :
B1(),
B3(),
m_id(L»NonVirtualClass»)
{ }
virtual ~NonVirtualClass(void) { }
const wchar_t* Id(void) const { return m_id.c_str();
virtual const wchar_t* VirtId(void) const override
{
return m_id.c_str();
}
//// If we decided we wanted to use B1::Conflict, we could use
//// a using declaration.
//// calling NonVirtualClass::Conflict means call B1::Conflict
//using B1::Conflict;
//// We can also use it to resolve ambiguity between member
//// data.
//// NonVirtualClass::SomeInt means B3::SomeInt, so
//// the nvC.SomeInt statement in
//// DemonstrateNonVirtualInheritance would be legal, even
//// though IntelliSense says otherwise.
//using B3::SomeInt;
private:
wstring m_id;
};
void DemonstrateNonVirtualInheritance(void)
{
NonVirtualClass nvC = NonVirtualClass();
//// SomeInt is ambiguous since there are two copies of A, one
//// indirectly from B1 and the other indirectly from B3.
//nvC.SomeInt = 20;
// But you can access the two copies of SomeInt by specifying which
// base class’ SomeInt you want.
// directly inherited from A, then this too would be impossible.
nvC.B1::SomeInt = 20;
nvC.B3::SomeInt = 20;
//// It is impossible to create a reference to A due to ambiguity.
//A& nvCA = nvC;
// We can create references to B1 and B3 though.
B1& nvCB1 = nvC;
B3& nvCB3 = nvC;
// If we want a reference to some particular A, we can now get one.
A& nvCAfromB1 = nvCB1;
A& nvCAfromB3 = nvCB3;
// To demonstrate that there are two copies of A’s data.
wcout <<
L»B1::SomeInt = » << nvCB1.SomeInt << endl <<
L»B3::SomeInt = » << nvCB3.SomeInt << endl <<
endl;
++nvCB1.SomeInt;
nvCB3.SomeInt += 20;
wcout <<
L»B1::SomeInt = » << nvCB1.SomeInt << endl <<
L»B3::SomeInt = » << nvCB3.SomeInt << endl <<
endl;
// Let’s see a final demo of the result.
// member function is also ambiguous because both B1 and B3 have
// a member function named Conflict with the same signature.
wcout <<
typeid(nvC).name() << endl <<
nvC.Id() << endl <<
nvC.VirtId() << endl <<
//// This is ambiguous between B1 and B3
//nvC.Conflict() << endl <<
// But we can solve that ambiguity.
nvC.B3::Conflict() << endl <<
nvC.B1::Conflict() << endl <<
//// GetSomeInt is ambiguous too.
//nvC.GetSomeInt() << endl <<
endl <<
typeid(nvCB3).name() << endl <<
nvCB3.Id() << endl <<
nvCB3.VirtId() << endl <<
nvCB3.Conflict() << endl <<
endl <<
typeid(nvCB1).name() << endl <<
nvCB1.Id() << endl <<
nvCB1.VirtId() << endl <<
nvCB1.GetSomeInt() << endl <<
nvCB1.Conflict() << endl <<
endl;
}
void DemonstrateVirtualInheritance(void)
{
VirtualClass vC = VirtualClass();
// This works since VirtualClass has virtual inheritance of B1,
// which has virtual inheritance of A, and VirtualClass has virtual
// inheritance of A, which means all inheritances of A are virtual
// and thus there is only one copy of A.
vC.SomeInt = 20;
// We can create a reference directly to A and also to B1 and B2.
A& vCA = vC;
B1& vCB1 = vC;
B2& vCB2 = vC;
// To demonstrate that there is just one copy of A’s data.
wcout <<
L»B1::SomeInt = » << vCB1.SomeInt << endl <<
L»B3::SomeInt = » << vCB2.SomeInt << endl <<
endl;
++vCB1.SomeInt;
vCB2.SomeInt += 20;
wcout <<
L»B1::SomeInt = » << vCB1.SomeInt << endl <<
L»B3::SomeInt = » << vCB2.SomeInt << endl <<
endl;
// Let’s see a final demo of the result.
// member function is still ambiguous because both B1 and B2 have
// a member function named Conflict with the same signature.
wcout <<
typeid(vC).name() << endl <<
vC.Id() << endl <<
vC.VirtId() << endl <<
vC.B2::Id() << endl <<
vC.B2::VirtId() << endl <<
vC.B1::Id() << endl <<
vC.B1::VirtId() << endl <<
vC.A::Id() << endl <<
vC.A::VirtId() << endl <<
// This is ambiguous between B1 and B2
//vC.Conflict() << endl <<
// But we can solve that ambiguity.
vC.B2::Conflict() << endl <<
vC.B1::Conflict() << endl <<
// There’s no ambiguity here because of virtual inheritance.
vC.GetSomeInt() << endl <<
endl <<
typeid(vCB2).name() << endl <<
vCB2.Id() << endl <<
vCB2.VirtId() << endl <<
vCB2.Conflict() << endl <<
endl <<
typeid(vCB1).name() << endl <<
vCB1.Id() << endl <<
vCB1.VirtId() << endl <<
vCB1.GetSomeInt() << endl <<
vCB1.Conflict() << endl <<
endl <<
typeid(vCA).name() << endl <<
vCA.Id() << endl <<
vCA.VirtId() << endl <<
vCA.GetSomeInt() << endl <<
endl;
}
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
DemonstrateNonVirtualInheritance();
DemonstrateVirtualInheritance();
return 0;
}
|
Примечание. Многие функции-члены в предыдущем примере объявляются как const путем включения ключевого слова const после списка параметров в объявлении. Это обозначение является частью концепции константности, которую мы обсудим в другом месте. Единственное, что означает запись const-member-function, это то, что функция-член не изменяет никакие данные-члены класса; вам не нужно беспокоиться о побочных эффектах при вызове его в многопоточном сценарии. Компилятор применяет эту нотацию, чтобы вы могли быть уверены, что функция, которую вы пометили как const, действительно является const.
Предыдущий пример демонстрирует разницу между виртуальными функциями-членами и не виртуальными функциями-членами. Функция Id в классе A не является виртуальной, а функция VirtId — виртуальной. В результате при создании ссылки на базовый класс на NonVirtualClass
и вызове Id мы получаем версию Id для базового класса, тогда как при вызове VirtId мы получаем версию VirtId для NonVirtualClass.
То же самое относится и к VirtualClass
, конечно. Несмотря на то, что в примере тщательно указывается виртуальное и переопределение для переопределений VirtId (и вы должны это делать), пока A::VirtId
объявлен как виртуальный, все методы производного класса с одинаковой сигнатурой будут считаться виртуальными. переопределяет VirtId.
Предыдущий пример также демонстрирует проблему алмаза, которую может породить множественное наследование, а также то, как виртуальное наследование решает ее. Название проблемы алмазов исходит из того, что если класс Z происходит от класса X и класса Y, оба из которых происходят от класса W, диаграмма этого отношения наследования будет выглядеть как ромб. Без виртуального наследования отношения наследования фактически не образуют алмаза; вместо этого он образует вилку с двумя зубцами, причем каждый зубец имеет свой собственный W.
NonVirtualClass
имеет не виртуальное наследование от B1, который имеет виртуальное наследование от A, и от B3, который имеет не виртуальное наследование от A. Это приводит к проблеме алмаза, когда две копии данных члена класса A становятся частью NonVirtualClass
. Функция DemonstrateNonVirtualInheritance
показывает проблемы, которые вытекают из этого, а также показывает синтаксис, используемый для решения, какой A вы хотите, когда вам нужно использовать один из членов A.
VirtualClass
имеет виртуальное наследование как от B1, который имеет виртуальное наследование от A, так и от B2, который также имеет виртуальное наследование от A. Поскольку все цепочки наследования, которые переходят из VirtualClass в A, являются виртуальными, существует только одна копия данных A; Таким образом, проблема с алмазами исключается. Функция DemonstrateVirtualInheritance
показывает это.
Даже с виртуальным наследованием, у VirtualClass все еще есть одна неопределенность. B1::Conflict
и B2::Conflict
имеют одинаковое имя и одинаковые параметры (в данном случае ни одного), поэтому невозможно определить, какой из них вы хотите, без использования синтаксиса спецификатора базового класса.
Именование очень важно при работе с множественным наследованием, если вы хотите избежать двусмысленности. Есть, однако, способ разрешить неоднозначность. Два закомментированных использования объявлений в NonVirtualClass
демонстрируют этот механизм разрешения. Если мы решили, что хотим всегда разрешать неоднозначность определенным образом, использование using позволяет нам это делать.
Примечание. Объявление using полезно и для разрешения неоднозначности вне класса (например, в пространстве имен или функции). Это также полезно, если вы хотите перенести только определенные типы из пространства имен в область, не вводя все пространство имен в область с помощью директивы using namespace. Можно использовать объявление using в заголовке при условии, что оно находится внутри определения класса, структуры, объединения или функции, поскольку использование объявлений ограничено областью, в которой они существуют. Вы не должны использовать их вне этого, так как вы будете вводить этот тип в область действия в глобальном пространстве имен или в любом пространстве имен, в котором вы были.
Одна вещь, которую я не затронул в этом примере, это спецификаторы доступа к наследованию, отличные от public. Если вы хотите, вы можете написать что-то вроде класса B: защищенный класс A {…}. Тогда члены класса A будут доступны из методов B и доступны любому классу, производному от B, но не будут доступны для общественности. Вы также можете сказать класс B: закрытый класс A {…}. Тогда члены класса A будут доступны из методов B, но не будут доступны для любых классов, производных от B, и при этом они не будут публично доступны.
Я упоминаю их попутно просто потому, что они редко используются. Тем не менее, вы можете столкнуться с ними, и вы даже можете найти их применение. Если это так, помните, что класс, который наследуется в частном порядке от базового класса, все еще имеет полный доступ к этому базовому классу; Вы просто говорите, что никакие классы, производные от дальнейшего использования, не должны иметь доступа к функциям и переменным-членам базового класса.
Чаще встречаются ошибки, когда вы или кто-то другой забыл напечатать public перед спецификатором базового класса, что привело к частному наследованию по умолчанию. Вы узнаете это по множеству сообщений об ошибках, сообщающих, что вы не можете получить доступ к закрытым функциям-членам или данным некоторого базового класса, если вы не пишете библиотеку и не тестируете класс. В этом случае вы узнаете проблему по гневным ревам ваших пользователей. Еще одна причина, по которой юнит-тестирование — это хорошая идея.
Абстрактные классы
Абстрактный класс имеет по крайней мере одну чисто виртуальную функцию-член. В следующем примере показано, как имитировать интерфейс C #.
Образец: AbstractClassSample \ IWriteData.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
#pragma once
class IWriteData
{
public:
IWriteData(void) { }
virtual ~IWriteData(void) { }
virtual void Write(const wchar_t* value) = 0;
virtual void Write(double value) = 0;
virtual void Write(int value) = 0;
virtual void WriteLine(void) = 0;
virtual void WriteLine(const wchar_t* value) = 0;
virtual void WriteLine(double value) = 0;
virtual void WriteLine(int value) = 0;
};
|
Образец: AbstractClassSample \ ConsoleWriteData.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
#pragma once
#include «IWriteData.h»
class ConsoleWriteData :
public IWriteData
{
public:
ConsoleWriteData(void) { }
virtual ~ConsoleWriteData(void) { }
virtual void Write(const wchar_t* value);
virtual void Write(double value);
virtual void Write(int value);
virtual void WriteLine(void);
virtual void WriteLine(const wchar_t* value);
virtual void WriteLine(double value);
virtual void WriteLine(int value);
};
|
Образец: AbstractClassSample \ ConsoleWriteData.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#include <iostream>
#include <ostream>
#include «ConsoleWriteData.h»
using namespace std;
void ConsoleWriteData::Write(const wchar_t* value)
{
wcout << value;
}
void ConsoleWriteData::Write(double value)
{
wcout << value;
}
void ConsoleWriteData::Write(int value)
{
wcout << value;
}
void ConsoleWriteData::WriteLine(void)
{
wcout << endl;
}
void ConsoleWriteData::WriteLine(const wchar_t* value)
{
wcout << value << endl;
}
void ConsoleWriteData::WriteLine(double value)
{
wcout << value << endl;
}
void ConsoleWriteData::WriteLine(int value)
{
wcout << value << endl;
}
|
Образец: AbstractClassSample \ AbstractClassSample.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include «IWriteData.h»
#include «ConsoleWriteData.h»
#include «../pchar.h»
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
//// The following line is illegal since IWriteData is abstract.
//IWriteData iwd = IWriteData();
//// The following line is also illegal.
//// instance of IWriteData.
//IWriteData iwd = ConsoleWriteData();
ConsoleWriteData cwd = ConsoleWriteData();
// You can create an IWriteData reference to an instance of a class
// that derives from IWriteData.
IWriteData& r_iwd = cwd;
// You can also create an IWriteData pointer to an instance of a
// class that derives from IWriteData.
IWriteData* p_iwd = &cwd;
cwd.WriteLine(10);
r_iwd.WriteLine(14.6);
p_iwd->WriteLine(L»Hello Abstract World!»);
return 0;
}
|
В предыдущем примере показано, как реализовать класс интерфейса в C ++. Класс IWriteData
может наследоваться классом, который записывает данные в файл журнала, в сетевое соединение или в любой другой вывод. Передав указатель или ссылку на IWriteData, вы можете легко переключать механизмы вывода.
Синтаксис абстрактной функции-члена, называемой чисто виртуальной функцией, заключается в простом добавлении = 0 после объявления, как в классе IWriteData
: void Write(int value) = 0;
, Вам не нужно делать класс чисто абстрактным; Вы можете реализовать функции-члены или включить данные-члены, общие для всех экземпляров класса. Если класс имеет хотя бы одну чисто виртуальную функцию, то он считается абстрактным классом.
Visual C ++ предоставляет специфический для Microsoft способ определения интерфейса. Вот эквивалент IWriteData
с использованием синтаксиса Microsoft:
Образец: AbstractClassSample \ IWriteData.h
01
02
03
04
05
06
07
08
09
10
11
12
13
|
#pragma once
__interface IWriteData
{
virtual void Write(const wchar_t* value) = 0;
virtual void Write(double value) = 0;
virtual void Write(int value) = 0;
virtual void WriteLine(void) = 0;
virtual void WriteLine(const wchar_t* value) = 0;
virtual void WriteLine(double value) = 0;
virtual void WriteLine(int value) = 0;
};
|
Вместо того, чтобы определять его как класс, вы определяете его с помощью ключевого слова __interface. Вы не можете определить конструктор, деструктор или другие функции-члены, кроме чисто виртуальных функций-членов. Вы также не можете наследовать от чего-либо, кроме других интерфейсов. Вам не нужно включать спецификатор открытого доступа, так как все функции-члены являются публичными.
Скомпилированные заголовочные файлы
Предварительно скомпилированный файл заголовка — это специальный тип файла заголовка. Как и обычный заголовочный файл, вы можете вставить в него как операторы, так и определения кода. Что он делает по-другому, так это помогает ускорить время компиляции.
Предварительно скомпилированный заголовок будет скомпилирован при первой сборке вашей программы. С тех пор, пока вы не внесете изменения в предварительно скомпилированный заголовок или что-либо, прямо или косвенно включенное в предварительно скомпилированный заголовок, компилятор может повторно использовать существующую скомпилированную версию предварительно скомпилированного заголовка. Время компиляции сократится, потому что большая часть кода (например, заголовки Windows.h и стандартной библиотеки C ++) не будет перекомпилирована для каждой сборки.
Если вы используете предварительно скомпилированный заголовочный файл, вам необходимо включить его в качестве первого оператора включения каждого файла исходного кода. Вы не должны, однако, включать его в какие-либо заголовочные файлы. Если вы забудете включить его или поставить над ним другие операторы include, компилятор выдаст ошибку. Это требование является следствием работы скомпилированных заголовков.
Предварительно скомпилированные заголовочные файлы не являются частью стандарта C ++. Их реализация зависит от поставщика компилятора. Если у вас есть какие-либо вопросы о них, вам следует ознакомиться с документацией поставщика компилятора и обязательно указать, какой компилятор вы используете, если спросите его на онлайн-форуме.
Форвардные декларации
Как мы уже обсуждали, когда вы включаете заголовочный файл, препроцессор просто берет весь код и вставляет его прямо в файл исходного кода, который он сейчас компилирует. Если этот заголовочный файл включает в себя другие заголовочные файлы, то все они тоже входят.
Некоторые заголовочные файлы огромны. Некоторые включают много других заголовочных файлов. Некоторые из них огромны и включают в себя множество других заголовочных файлов. В результате много кода может быть скомпилировано снова и снова просто потому, что вы включили заголовочный файл в другой заголовочный файл.
Один из способов избежать необходимости включать заголовочные файлы в другие заголовочные файлы — это использовать предварительные объявления. Рассмотрим следующий код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#pragma once
#include «SomeClassA.h»
#include «Flavor.h»
#include «Toppings.h»
class SomeClassB
{
public:
SomeClassB(void);
~SomeClassB(void);
int GetValueFromSomeClassA(
const SomeClassA* value
);
bool CompareTwoSomeClassAs(
const SomeClassA& first,
const SomeClassA& second
);
void ChooseFlavor(
Flavor flavor
);
void AddTopping(
Toppings topping
);
void RemoveTopping(
Toppings topping
);
private:
Toppings m_toppings;
Flavor m_flavor;
// Other member data and member functions…
};
|
Мы включили заголовочные файлы SomeClassA.h, Flavor.h и Toppings.h. SomeClassA — это класс. Flavor — это enum с ограниченным доступом (в частности, enum-класс). Начинка — это перечисление без границ.
Посмотрите на наши определения функций: у нас есть указатель на SomeClassA в GetValueFromSomeClassA
. У нас есть две ссылки на SomeClassA в CompareTwoSomeClassAs
. Тогда у нас есть различные варианты использования вкуса и начинок.
В этом случае мы можем исключить все три из этих включаемых утверждений. Почему? Потому что для компиляции этого определения класса компилятору просто нужно знать тип SomeClassA и базовые типы данных Flavor и Toppings. Все это мы можем сообщить компилятору с помощью предварительных объявлений.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#pragma once
class SomeClassA;
enum class Flavor;
enum Toppings : int;
class SomeClassB
{
public:
SomeClassB(void);
~SomeClassB(void);
int GetValueFromSomeClassA(
const SomeClassA* value
);
bool CompareTwoSomeClassAs(
const SomeClassA& first,
const SomeClassA& second
);
void ChooseFlavor(
Flavor flavor
);
void AddTopping(
Toppings topping
);
void RemoveTopping(
Toppings topping
);
private:
Toppings m_toppings;
Flavor m_flavor;
// Other member data and member functions…
};
|
Три строки после #pragma однажды сообщают компилятору все, что ему нужно знать. Говорят, что SomeClassA является классом, поэтому он может установить его тип для целей связывания. Говорят, что Flavor является классом enum, и поэтому он знает, что ему нужно зарезервировать место для int (базовый тип по умолчанию для класса enum). И, наконец, сказано, что Toppings — это перечисление с базовым типом int, и поэтому оно также может зарезервировать для него место.
Если определения этих типов в SomeClassA.h, Flavor.h и Toppings.h не соответствуют этим прямым объявлениям, вы получите ошибки компилятора. Если вы хотите, чтобы экземпляр SomeClassA был переменной-членом SomeClassB, или если вы хотите передать его в качестве аргумента напрямую, а не в качестве указателя или ссылки, вам нужно будет включить SomeClassA. Затем компилятору потребуется зарезервировать пространство для SomeClassA и потребуется его полное определение, чтобы определить его размер в памяти. Наконец, вам все еще нужно включить эти три заголовочных файла в файл исходного кода SomeClassB.cpp, так как вы будете работать с ними в определениях функций-членов SomeClassB.
Итак, что мы получили? Каждый раз, когда вы включаете SomeClassB.h в файл исходного кода, этот файл кода не будет автоматически содержать весь код из SomeClassA.h, Flavor.h и Toppings.h и компилировать его. Вы можете включить их, если они вам нужны, но вы исключили их автоматическое включение и включение всех заголовочных файлов, которые они включают.
Допустим, SomeClassA.h включает в себя Windows.h, потому что, помимо предоставления некоторой ценности, он также работает с окном в вашем приложении. Вы неожиданно сократили количество строк кода (на тысячи и тысячи), которые должны быть скомпилированы в любом файле исходного кода, который включает SomeClassB.h, но не включает SomeClassA.h или Windows.h. Если вы включите SomeClassB.h в несколько десятков файлов, вы вдруг скажете о десятках или сотнях тысяч строк кода.
Предварительные объявления могут сэкономить несколько миллисекунд, или минут, или часов (для больших проектов). Конечно, они не являются волшебным решением всех проблем, но они являются ценным инструментом, который может сэкономить время при правильном использовании.
Вывод
Это было очень важно. Мы рассмотрели много важных аспектов языка C ++, поэтому обязательно посетите эту статью, если вам нужно освежить память.
Этот урок представляет собой главу из C ++ Succinctly , бесплатной книги от команды Syncfusion .