Загрузка...
 
Печать

Основы языка C++


Содержание



Язык С++(external link) - компилируемый, объектно-ориентированный, статически типизированный язык программирования общего назначения. До недавнего времени подавляющее большинство коммерческих игр для ПК и консолей создавалось с его помощью. Даже приход платформы .NET и языка программирования C# (читается "Си шарп") не смог кардинально изменить ситуацию.
С языком C++ ты окажешься в новом измерении программирования.
По данной теме написана не одна сотня книг (многие из которых на русском языке). А так как данная статья не претендует на полноту изложения, настоятельно рекомендуем прочесть хотя бы пару из них. Не факт, что всё усвоишь, но основные моменты всё равно запомнишь.

Переходим с языка C на C++

Конечно, C++ построен на базе языка C и имеет с ним много общего.1 Но C++ в разы круче. Такие его фишки, как объектно-ориентированный подход позволяют создавать модульный (modular) код, который, написав однажды, можно снова и снова использовать в своих проектах практически без изменений. Среди других "плюшек" C++:
  • Новые ключевые слова (keywords).
  • Расширенные возможности вызова функций.
  • Поддержка структур.
и др.

Объектно-ориентированное программирование (ООП). Понятие класса

Объектно-ориентированный подход (ООП) подразумевает под собой создание программного кода, позволяющего группировать наборы инструкций в специальные "паки" (packages) более известные как объекты. Объект может представлять собой что угодно: персонаж игрока, оружия или даже карту локации. В то время как объекты "знают" только о существовании самих себя, они конструируются таким образом, чтобы внешние объекты могли с ними взаимодействовать. В этом вся суть: данные внутри таких объектов обособлены (self-contained) и могут быть доступны другим объектам только через специальные интерфейсы. Это означает, что объекты прячут свои данные, за исключением тех данных, которые имеют статус public, т.е. доступны для других объектов.
Например объект "мир" заботится только о самом себе. Но если я захочу узнать температуру воздуха в указанной точке земного шара, я просто запрошу у объекта "мир" эту информацию. Объект "игрок", например, может сообщить мне свой уровень здоровья (health status) или текущую позицию на карте.
ООП предоставляет абсолютно иной подход к структурированию программного кода и его понимание может занять некоторое время.
Независимо от языка программирования объектно-ориентированный подход (также применяется в Java, PHP, Python и много где ещё) имеет ряд общих принципов:
  • Возможность создавать абстрактные типы данных, позволяющая наряду с предопределёнными типами данных (integer, bool, double и др.) вводить свои собственные типы данных (классы) и объявлять совего рода "переменные" таких типов данных (объекты). В этом случае программист оперирует не машинными терминами (переменная, функция), а объектами реального мира, поднимаясь тем самым на новый абстрактный уровень. Напрмер яблоки и людей нельзя умножать друг на друга. Низкоуровневый код запросто может совершить такую логическую ошибку, тогда как при использовании абстрактных типов данных такая операция становится невозможной.
  • ИНКАПСУЛЯЦИЯ допускает взаимодействие пользователя с абстрактными типами данных только через их интерфейс. Реализация объекта остаётся закрытой от посторонних глаз с целью предотвратить модифицирование (изменение) этих ообъектов. Использование инкапсуляции позволяет разработать автономный объект и использовать его, прибегая лишь к небольшому числу интерфейсных методов и не заботясь о его внутренней реализации. Каждый объект касса ответственен только за свои действия. Поэтому игра не особо "заботится" о том, что происходит с каждым из объектов её классов.2 Игра лишь предоставляет основу (фреймворк) для создания объектов классов. Как правило, в основе данного фреймворка лежит игровое поле (экран) с объектами на нём, а также некий цикл обновления (update loop), который сообщает объектам, что прошло столько-то времени. Для неигровых персонажей (NPC) цикл обновления указывает направление движения/атаки. Для персонажа игрока вдобавок происходит обработка пользовательского ввода. Т.е. несмотря на то, что каждый объект игры в принципе автономен, каждый из них знает, что делать в каждый момент времени.
  • НАСЛЕДОВАНИЕ позволяет на основе существующего класса создавать другие классы, которые автоматически получают его возможности. Зачастую многие классы бывают достаточно сложны, поэтому прибегают к их последовательной разработке, выстраивая иерархию классов от общего к частному.
  • ПОЛИМОРФИЗМ - возможность классов содержать методы с одинаковым названием, но с разным набором аргументов. Очень полезно, например, при обработке массивов данных разного типа.
Абстрактные типы данных (в нашем случае это классы) позволяют программисту вводить в программу переменные с желаемыми свойствами, когда возможностей существующих в языке типов данных не хватает. Связи между объектами реального мира зачастую настолько сложны, что для их эффективного моделирования необходим отдельный язык программирования. Разрабатывать специализированный язык программирования для каждой прикладной задачи - очень дорогое удовольствие. Поэтому во многие языки программирования (C++, PHP, Java, Python и др.) вводится объектно-ориентированный подход, который позволяет создать свой мини-язык путём введения классов и объектов. Переменными здесь уже являются объекты, в качестве типа данных для которого выступает класс. Класс описывает состав объекта -переменные и функции, которые тем самым определяют поведение объекта. Классы являеются своего рода "шаблонами" для создания на их основе различных объектов.
Одним из самых больших преимуществ ООП является возможность повторного использования ранее написанного кода (т.н. рефакторинг, reusing). Т.е. если вдруг решишь сделать ролевую игру на основе готовой (например на просторах космоса), нет необходимости повторно создавать игровые объекты с нуля. Достаточно отредактировать уже имеющиеся.

Использование заголовочных файлов (header files)

Функционал некоторого исходного файла (с расширением .cpp) может быть представлен его заголовочным фалом (с расширением .h). К заголовочным файлам часто обращаются как к интерфейсам. Например, ты можешь создать класс с названием Animal (от англ. Animal - животное), который воспроизводит функционал некоего "базового" животного в отдельно взятой игре. Класс будет объявлен в заголовочном файле (в C++, перед использованием чего угодно, это "что угодно" необходимо сначала объявить) (например animal.h) и затем уже будет реализован (имплементирован) в исходном файле, ассоциированном с ним (Animal.cpp). Таким образом, заголовочный файл предоставляет интерфейс для класса Animal, предназначенный для того, чтобы класс и функции этого класса были доступны для других файлов в пределах данной гипотетической игры.
Следующий пример показывает объявление нашего класса Animal, которое размещается в заголовочном файле animal.h. Это объявление затем становится интерфейсом класса Animal. Данный код (вообще, он считается "псевдокодом") не нужно отправлять на компиляцию в MS Visual C++. Его надо просто прочитать и попытаться в нём разобраться.
animal.h
class Animal
{
 public:
 Animal();	//Объявление конструктора

 virtual ~Animal();	//Объявление деструктора

 private:

 bool m_alive;
};

Наш простой класс имеет конструктору, деструктору и одну переменную типа bool. Тип Boolean подразумевает всего два значения - true (истина) или false (ложь). Перед деструктором стоит служебное слово virtual, о котором мы расскажем позднее. Пока лишь отметим, что здесь оно также необходимо.
Но мы двигаемся дальше, и сейчас посмотрим на реализацию этого класса, размещённую в исходном файле Animal.cpp.
animal.cpp
#include "animal.h"

Animal::Animal()	//Реализация конструктора
{
 m_alive = true;
}

Animal::~Animal()	//Реализация деструктора
{
 m_alive = false;
}

Для того, чтобы компилятору связал объявление класса с исходным фалом Animal.cpp, где содержится реализация класса, в начале Animal.cpp необходимо указать ссылку на заголовочный файл (animal.h). Для этого в начале Animal.cpp размещают специальное выражение #include (от англ. "вложить"). Оно просто "говорит" компилятору взять всё содержимое указанного файла и разместить его на этом месте. В нашем случае в начале исходного файла мы ставим команду #include "animal.h".
Но зачем вообще нужен заголовочный файл? Нельзя ли обойтись без него, разместив весь исходный код (объявление класса + его реализация) в одном исходном файле (.cpp)? Ответ - да, можно. Более того, ты можешь разместить функции (в нашем случае это конструктор и деструктор) прямо внутри объявления класса:
Animal.cpp
class Animal
{
 public:
 Animal()
 {
  m_alive = true;
 };

 virtual ~Animal()
 {
  m_alive = false;
 };
 
 private:
  bool m_alive;
}

Но такой подход имеет свои недостатки:
  1. Если ты размещаешь объявление класса в начале исходного файла. то теряешь возможность иметь отдельно вынесенный интерфейс для своего класса. На деле это усложнит код, что, в свою очередь, снизит его пригодность к обслуживанию и внесению измемнений (Maintainability) и, как следствие, усложнит его использование (usability). Подробнее об этом читаем здесь. Это не проблема, когда приложение содержит всего один класс. Но любая игра насчитывает десятки, а то и сотни классов, размещать которые в одном исходном файле будет крайне необдуманно.
  2. Если ты размещаешь реализацию класса внутри его объявления, у тебя нет защищённого интерфейса. Вся суть интерфейсов состоит в том, чтобы позволить разработать класс, который может быть позднее использован в других проектах БЕЗ отображения его реализации. Таким образом, для того чтобы выяснить, что делает данный класс, любой желающий может просмотреть его интерфейс (обычно это заголовочный файл с расширением .h). Но никто не может увидеть исходный код с реализацией этого класса, скрытый, как правило, в недрах динамически подключаемой библиотеки (.dll). Экспонирование функций динамически подключаемой библиотеки (.dll) в заголовочных файлах (.h) -обычная практика в программировании. И хотя, вместо DLL-библиотеки, у нас вполне открытый и доступный для прочтения исходный файл (.cpp), принцип остаётся тот же.
В нашей гипотетической игре, если мы захотим создать ещё один инстансу (от англ. instance - экземпляр) нашего класса Animal (уже в другом исходном файле), всё, что нам нужно будет сделать, это вставить выражение #include с указанием на интерфейс этого класса где-то в начале исходного файла, который будет создавать этот экземпляр. Само собой, выражение #include необходимо размещать ДО того места в коде, где ты пытаешься использовать данный класс. В следующем примере мы вставляем выражение #include "animal.h" в "какой-то другой исходный файл" и создаём после этого экземпляр класса Animal (изначально определённый в animal.h), назвав его dog:
SomeOtherSourceFile
#include "animal.h"

int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow)
{
 Animal dog; return true;
}

Этот принцип мы возьмём на вооружение и будем повсеместно применять при создании движка и игры. Едва ли не каждый класс будет создаваться со своим внешним интерфейсом (в виде заголовочного файла .h). При программировании движка эти интерфейсы будут отлинкованы (скомпонованы через выражения #include) в один "центральный" заголовочный файл Engine.h, что полностью удовлетворяет нашему требованию о "единой точке контакта". Весь функционал нашего будущего движка будет представлен в одном этом файле. Более того, для использования движка (например при создании игры) достаточно включить в проект его главный заголовочный файл, указав в начале исходного файла (.cpp) всего одно выражение #include "Engine.h". Чуть позднее мы ещё раз обсудим концепцию "единой точки контакта".

Работа с функциями

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

Прототип функции

Самое важное правило в C++ - это необходимость прототипирования всех объявляемых функций. Обычно прототипы функций размещаются в заголовочных файлах с расширением .h .
К примеру возьмём функцию, складывающую два числа. Она принимает 2 числа в качестве аргументов и после своего выполнения возвращает их сумму. Пусть все они имеют тип long. Её прототип размещается в заголовочном файле .h:
// Function prototype
long AddNumbers(long Argumentl, long Argument2);

Её реализация размещается в файле исходного кода .cpp:
// Function implementation
long AddNumbers(long Argumentl, long Argument2)
{
 return Argumentl + Argument2;
}

Вместо указания в прототипе функции имён переменных, ты можешь просто указать их тип данных, оставив их имена в реализации:
// Function prototype
long AddNumbers(long, long);

// Function implementation
long AddNum(long Numl, long Num2)
{
 return Numl + Num2;
}

В языке C не требовалось писать прототип функции для её вызова. Компилятор сам создавал их, выяснял аргументы и возвращал значения нужного типа. Зачем тогда они нужны в C++? Рассмотрим каждую из причин отдельно.

Значения аргументов функций по умолчанию (Default Function Argument Values)

В C++ аргументы функции могут иметь заранее присвоенные значения по умолчанию, что часто сберегает кучу времени и системных ресурсов, т.к. не нужно их явно вбивать при каждом случае.
К примеру допустим, что у нас есть программа, которая берёт сумму денежного займа и прибавляет к ней определённый процент. Предположим, что стандартная сумма займа 10000 руб. и процентная ставка за пользование составляет 8%. В этом случае значения по умолчанию стоит прописать в аргументах прототипа функции:
float AddInterest(float Amount=l0 0 0 0.0, float Interest=0.08);

Здесь видим, что:
  • переменная Amount имеет значение по умолчанию равное 10000.0,
  • переменная Interest имеет значение по умолчанию равное 0.08.
В случае, когда функция вызывается без передаваемых параметров, компилятор автоматом присваивает переменным эти значения.
Вот та же самая функция в действии, при её вызове разными способами:
// Function prototype
float AddInterest(float Amount=l0000.0, float Interest=0.08);

// The actual function - нет необходимости явно указывать значения по умолчанию.
float AddInterest(float Amount, float Interest)
{
 return Amount*Interest;
}

main()
{
 Amount = AddInterest(30000.0, 0.07); // 30000 под 7%
 Amount = AddInterest(20000.0); // 20000 под 8%
 Amount = AddInterest(); // l0000 под 8%
}


Перегрузка функции (Function overloading)

Image
Рис.1 Функции могут иметь одно и то же имя, но отличаться друг от друга набором параметров. Прототипирование функций избавляет компилятор от путаницы.

  • Это метод указания нескольких прототипов одной и той же функции, каждый из которых имеет свой набор передаваемых аргументов.
  • Собственно то, ради чего и были придуманы прототипы функций.
  • Позволяет создавать функции с одинаковыми именами, но отличающиеся друг от друга разными наборами (количеством) параметров (см. Рис.1).
То есть ты можешь создать две (или более) функции у которых будет одинаковое имя, но они по-разному обрабатывают передаваемые параметры в зависимости от их количества.
Например ты можешь создать одну функцию, которая принимает несколько отдельных переменных в качестве параметров (arguments), либо эти параметры могут быть объединены в структуру, а, в свою очередь указатель (pointer) на эту структуру передаётся второй функции (вызываемому прототипу) с тем же именем.
Перегрузка проверяет, что компилятор знает, что за функцию он вызывает.
Закрыть
noteЛюбопытный факт

При компилировании компилятор перекодирует (encode) имена функций вместе со списком типов их параметров. Это позволяет ему отличать одну функцию (с тем же именем) от другой. Такое перекодирование называется name decoration или name mangling ("коверканье" имени).

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

// Прототипы функций

// Складываем 2 числа
long AddNumber(long Numl, long Num2); // Складываем массив чисел
long AddNumbers(long *NumArray, long NumOfNums);

// Код функций
long AddNumbers(long Numl, long Num2)
{
 return Numl + Num2;
}

long AddNumbers(long *NumArray, long NumOfNums)
{
 long Result, i;
 Result=0;

 while(NumOfNums--)
 {
  Result+=NumArray[i];
  return Result; 
 }
}

Эти две функции могут мирно сосуществовать в одной программе/классе благодаря возможности перегрузки функций. Компилятор распознаёт, какую функцию вызывать, по передаваемым аргументам. Поэтому следующие два вызова функции верны:
Result = AddNumbers(l0, 20); // Result=30
long Array[5]={l0, 20, 30, 40, 50};
Result = AddNumbers(Array, 5); // Result=l50


Встроенные функции (Inline functions)

Image
Рис.2 Встроенные (Inline) функции вставляются в компилируемый код, что увеличивает скорость выполнения и размер исполняемого файла.

Ты можешь существенно ускорить выполнение функции, применив пару проверенных способов. Однако при этом придётся пожертвовать увеличением размера исполняемого файла. Один из таких способов - указание функции в качестве встроенной (inline).
Когда мы указываем перед именем вызываемой функции ключевое слово inline, компилятор помещает текущую копию кода функции в специальную область вызовов (calling location), вместо того, чтобы просто разместить всё в стеке. Это означает, что при вызове функции 5 раз её код будет размещён в области вызовов, расположенной внутри исполняемого файла, 5 раз. Вот пример встроенной функции, складывающей 2 числа:
inline long AddNumbers(long Numl, long Num2);

long AddNumbers(long Numl, long Num2)
{
 return Numl + Num2;
}

main()
{
 long Result;
 Result = AddNumbers(10, 20);
 Result = AddNumbers(11, 6);
 Result = AddNumbers(1, 13);
}


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

Работа с переменными (variables)

  • в языке C++ была существенно расширена.

Объявление переменных (Variable declaration)

В C++ ты можешь объявлять переменную в любо месте кода, а не только в его начале. Также можно выбирать тип переменной при присвоении ей значения:
long SomeFunction(long Numl, long Num2) 
{
 long Result; // Доступна для текущей функции
 
 if(Numl < Num2)
 {
  long AddResult; // Доступна для следующих двух строк
  AddResult = Numl + Num2;
  Result = AddResult;
 }
 else
 {
  long SubResult = Numl - Num2; // Доступна для следующей строки
  Result = SubResult;
 }

 SubResult = 0; // Ошибка! Присвоение возможно только в теле функции!

 Return Result;
}

Здесь видно, что переменная AddResult объявлена прямо в блоке условий (conditional block) прямо перед своим применением. Также объявлена переменная SubResult. Оба объявления допустимы, за исключением момента присвоения значения переменной SubResult (см комментарии в коде).

Область видимости и приоритет (старшинство) переменных (Scope and Precedence)

В примере выше представлен пример области видимости переменных. Объявленные переменные действуют только в области видимости того блока, в котором они объявлены. Таким образом переменная, объявленная в начале функции (например в блоке условий) действительная (видима, к ней есть доступ) в пределах данной функции. В примере выше переменная Result объявлена в самом начале тела функции. Поэтому доступ к ней есть из любого места данной функции. Но с переменными AddResult и SubResult всё по-другому. Они видны только в блоках условного оператора if...else, где они объявлены. Любые попытки обратиться к эти переменным извне приведут к ошибке компиляции. Благодаря области видимости ты можешь одновременно иметь глобальную и локальную переменные с одинаковым именем. Также как и в C, локальные переменные имеют больший приоритет (старшинство), чем глобальные. В C++ ты можешь явно (explicitly) указать компилятору использовать переменную в качестве глобальной, разместив перед её именем два знака двоеточие (::), как в этом примере:
long Result; // Глобальная переменная (Global variable)
long SomeFunction(long Numl, long Num2)
{
 long Result; // Локальная переменная
 Result = Numl + Num2; // Локальная переменная = Numl + Num2

 // Обрати внимание на два двоеточия перед переменной в следующей строке
 ::Result = Numl + Num2; // Глобальная переменная = Numl + Num2
 Result += ::Result;

 return Result;
}

Закрыть
noteОбрати внимание

Единственный недостаток заключается в том, что в листинге ты не можешь производить операции с несколькими переменных с одинаковыми именами, но с разными областями видимости. К примеру, если у тебя есть глобальная (global) переменная Result, объявленная в начале функции, а затем ты объявляешь её повторно в блоке условий (conditional block), то по факту они будут расположены в разных областях видимости.
Поэтому для наглядности рекомендуется при объявлении ставить перед именем глобальной переменной префикс g_ (например, g_Result). Тема "говорящих" префиксов не нова и подробно рассмотрена в т.н. венгерской нотации(external link) (Hungarian Notation).

Приоритет (старшинство, precedence) также работает и с функциями.

Статические переменные (Static variables)

Переменные занимают место в памяти. Но что если тебе необходимо отследить значение переменной во время нескольких вызовов функции? Из-за особых свойств области видимости переменная теряет своё значение всякий раз, когда эта самая область видимости исчезает. Вот пример:
long IncreasePeopleCount()
{
 long NumberOfPeople = 0; NumberOfPeople += 1; return NumberOfPeople;
}

main()
{
 long Num;
 Num = IncreasePeopleCount(); //Num = 1;
 Num = IncreasePeopleCount(); // Num = 1;
}

Здесь представлена функция, которая пытается отследить некотрое число (число людей). Единственная проблема в том, что переменная NumberOfPeople всегда сбрасывается в 0 всякий раз при вызове функции. Конечно, в этом случае можно объявить переменную NumberOfPeople в качестве глобальной (global). Но раз уж эта переменная ассоциирована (применяется) только с одной функцией, её проще объявить как статическую (static):
static long NumOfPeople = 0;

Предварив объявление переменной кючевым словом static, ты информируешь компилятор отслеживать её значение даже при утрате ею области видимости. Значение, присвоенное переменной во время её объявления, является начальным и программа может изменять его в ходе своего выполнения. Теперь всякий раз при вызове функции IncreasePeopleCount (теперь со статической переменной) получаем инкрементный (увеличивающийся на единицу) счёт:
...
main()
{
 long Num;
 Num = IncreasePeopleCount; // Num=1;
 Num = IncreasePeopleCount; // Num=2;
 Num = IncreasePeopleCount; // Num=3;
}


Константы (Const, неизменяемые переменные)

Часто функции в ходе своего выполнения изменяют значение переменных, что во многих случаях совсем не желательно. В такой ситуации на помощь приходят константы. Предварив переменную служебным словом const, мы укажем компилятору, что данная переменная (вернее её значение) доступна только для чтения и не может изменяться ни при каких обстоятельствах. Значение константе присваивается всего 1 раз - только при её объявлении.
const long ReadOnlyVariable = 10; // Присваиваем значение 10 при объявлении
ReadOnlyVariable = 20; // Ошибка! Переменная доступна только для чтения!

Константы также применимы в качестве параметров функций. Правда, в этом случае переменная остаётся константой только в пределах этой функции. Вот пара примеров:
main()
{
 const long Var=10;
 long i;

 Var=20; // Ошибка! Только для чтения!
 SomeFunction(i);
}

void SomeFunction(const long Val)
{
 Val=10; // Ошибка! Только для чтения!
}


Новые ключевые слова (keywords) и улучшения (enhancements)

Недостатки языка C породили улучшения и дополнения в языке C+ + . Самая большая проблема была связана с выделением (allocating) и освобождением (deallocating) памяти, ссылками на перечисляемые списки (enumerated lists) и разночтения значения NULL в разных компиляторах. И это только начало длинного списка.
Всеми этими улучшениями можно не пользоваться, но они заметно упрощают написание кода. К старым методам уже точно не захочется возвращаться.
Закрыть
noteОбрати внимание

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


Выделение памяти (Memory allocation)

В памяти компьютера содержится весь программный код, графика, звуковые эффекты и т.д. Рассмотрим пример выделения памяти под наши собственные нужды.
Выделение памяти - одно из нововведений C+ + . Здесь на помощь приходят 2 новых суперинтеллектуальных оператора: new и delete. С их помощью можно выделить любой тип памяти, будь то переменная, класс или структура. Причём это делается всего за один вызов.
Для выделения памяти используй оператор new:
void *Ptr = new DataType; // для одного элемента
void *Ptr = new DataType[NumElements]; // для массива значений

Ptr представляет собой указатель (pointer) на участок памяти, которая выделяется. DataType - тип хранимых данных.
Указатель можно создать на что угодно. Обычно он имеет тот же тип данных, что и запрашиваемый участок памяти. К примеру, допустим мы хотим выделить массив (array) длинной 10 элементов, 100 структур, одно значение с плавающей точкой (float) и массив из 20 указателей:
long *Ptr = new long[10];
sMyStruct *Ptr2 = new sMyStruct[100];
float *Ptr3 = new float; // одно число типа float
char *Ptr4 = new char*[20];

Вся память, выделенная с помощью операторов new должна быть освобождена по завершении выполнения программы/функции. Это выполняет оператор delete. Достаточно просто вызвать его вместе с указателем на освобождаемый участок памяти. Всю остальную грязную работу сделает C+ + . При удалении массива оператор delete необходимо указывать вместе с квадратными скобками([]):
delete[] Ptr1; // Массив long[10]
delete[] Ptr2; // Структура sMyStruct[100]
delete Ptr3; // Одно значение типа float - без угловых скобок
delete[] Ptr4; // char*[20]

Оператор delete можно даже вызывать для удаления указателя с присвоенным значением NULL, без необходимости проверять его содержимое:
char *DataPtr=NULL;
delete DataPtr;

Выделение памяти для многомерного (multidimensional) массива выглядит немного по-другому:
long (*Ptr)[10][20];
Ptr = new long[5][10][20]; // Выделяем delete[]
Ptr; // Освобождаем

Если необходимо выделить память под массив указателей, то просто сделай так:
char **Pointers=NULL;
Pointers = new char*[10]; // Массив из 10 указателей типа char
delete[] Pointers; // Не забудь освободить память!


Ключевое слово NULL (ноль)

В языке C слово NULL могло означать любую переменную, какая только вздумается производителю компилятора. Но в C+ + NULL - это всегда 0 (ноль). В дебрях базовых заголовков можно отыскать соответствующий макрос.

Ключевое слово Enum (перечисление)

При работе со списками перечислений (enumeration lists) после соответствующего объявления не требуется повторно указывать ключевое слово enum.

enum Numbers
{
 First = 1,
 Second,
 Third,
 Tenth = 10
};
 short Value = Second; // Равно 2

В последней строке нет ключевого слова enum, т.к. оно есть в инициализации перечисления выше.
Закрыть
noteОбрати внимание

Как и раньше, значения типа enum имеют тип данных int. Поэтому не забудь указать его при инициализации перечисления.


Классы (Classes)

Image
Рис.3 Структуры всегда разрешают свободный доступ к своим данным, в то время как классы - нет.

Собственно, ради чего и задумывался C++ . Будучи объектно-ориентированным языком программирования, C++ оперирует классами как обычными объетками. Такие объекты самодостаточны. Каждый из них имеет свой собственный набор данных и функций. Это делает данные объекты переносимыми (portable) и заточенными под повторное использование (reusable).
Если суть классов до конца не ясна, представь, что класс - это та же структура из языка C, только на "стероидах". Класс объявляется почти так же как и структура. Сперва пишется ключевое слово class а затем имя класса. Данные класса заключены в его тело и обрамляются фигурными скобками ({}).
class cClassName
{
};

Закрыть
noteОбрати внимание

Правилом хорошего тона является указание перед именем любого класса английской буквы с. Это сильно повышает читабельность кода. Сразу видно, что cClassName - это имя класса, а, например, sStructureName - имя структуры. Венгерская нотация и Майкрософт рекомендуют писать имена классов с большой (прописной) английской буквы C.

Одно из главных отличий структуры от класса это то, что в классе можно защитить или скрыть данные внутри него, от доступа из внешенго кода. Вот пример:
struct sMyStructure
{
 long Var1, Var2, Var3;

 main()
 {
 sMyStructure TestStruct;
 TestStruct.Var1 = 1; TestStruct.Var2 = 2; TestStruct.Var3 = 3;
 }

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

Видимость классов (Class visibility)

При обращении к данным внутри класса как правило запрашиваются переменные и функции. Функции внутри класса могут выполняться как самим классом, так и внешними конструкциями кода за пределами данного класса. Вот пример:
class cClassName {

public:
 // Размещаем здесь открытые для чтения/записи извне переменные и функции.
 long m_PublicVar;
 long PublicFunction();

private:
 // Размещаем здесь закрытые для чтения/записи извне переменные и функции. long m_PrivateVar; long PrivateFunction();

protected:
 // Размещаем здесь переменные и функции, доступные только для данного класса и всех ветвящихся (derived) от него классов.
 long m_ProtectedVar;
 long ProtectedFunction();
};

Теперь ты можешь просто создать экземпляр (инстанс) данного класса (подобно тому, как создаётся структура) и начать получать доступ к данным внутри. Только в этот раз часть данных будет недоступна из внешнего кода. Видимость содержимого класса регулируют с помощью трёх идентфикаторов доступа:
public Данные класса с таким идентификатором свободно доступны из других участков исходного кода (вне данного класса).
private Данные класса с таким идентификатором доступны только для функций внутри этого класса. Доступ для кода вне данного класса закрыт.
protected Почти то же самое, что и protected. Данные доступны только для этого класса и всех других, ветвящихся от него классов.

Вот пример доступа к данным этого класса из внешнего кода:
cClassName MyClass;
MyClass.mPublicVar = 10; // OK!
MyClass.PublicFunction(); // OK!
MyClass.mPrivateVar = 11; // Недоступно.
MyClass.PrivateFunction(); // Недоступно.
MyClass.mProtectedVar = 12; // Недоступно.
MyClass.ProtectedFunction(); // Недоступно.

Закрыть
noteОбрати внимание

Имена внутренних переменных класса предваряются префиксом m (member). Это негласное правило тоже прописано в Венгерской нотации.


Переменные и функции класса

Недостаточно просто объявить переменную или функцию в классе. С переменными всё достаточно просто: достаточно объявить их под определённым идентификатором доступа. Попытка получить доступ к переменным protected или private вызовет ошибку при компиляции.
Внутри класса доступны все объявленные функции. Следующий шаг - написать их реализации в соответствующем .cpp-файле, принадлежащему данному классу. Реализация (или вызов) функций, принадлежащих классу, должны предваряться именем этго класса и двумя двоеточиями:
long cClassName::PublicFunction(void);

Пара двоеточий информирует компилятор о том, что вызываемая функция принадлежит данному классу. Файл исходного кода может содержать в себе функции, принадлежащие тому или иному классу, либо не принадлижащие никакому классу. Поэтому два двоеточия - единственный способ связать функции с их классами-собственниками.
Остаётся вопрос: как получить доступ к закрытым функциям внутри класса из внешнего кода? Один из способов - через открытые (public) функции данного класса.
Разберём пример:
long cClassName::PublicFunction()
{
 return m_PublicVar + ProtectedFunction() + m_PrivateVar;
}

long cClassName::ProtectedFunction()
{
 return m_ProtectedVar;
}

long cClassName::PrivateFunction()
{
 return m_PrivateVar;
}

Как видим, public-функция свободно получает доступ к остальным закрытым переменным и функциям своего класса. При этом она остаётся открытой для внешнего кода.
Другим бонусом использования функций в классах является возможность объявления (defining) функции при определении (declaration) класса. Обычно так объявляют небольшие функции:

class cClass
{
 public:
  long PublicFunction(void)
  {
   return m_PublicVar + ProtectedFunction() + mProtectedVar;
  }

// Другой код декларации класса.
}


Статические (Static) переменные и функции

Обычно экземпляры класса всегда имеют доступ к собственной копии внутренних данных. Каждый создаваемый экземпляр класса понятия не имеет о других экземплярах того же класса. Для того, чтобы переменные и функции были доступны во всех экземплярах (=инстансах) класса их определяют как static (статические):
class cClassName
{
 public: static long m_Variable;
 static long Function();
}

За пределами класса указывается значение статической переменной по умолчанию:
long cClassName::m_Variable=100;

Данное присвоение является глобальным (global). Теперь мы можем получить доуступ к этой переменной точно так же, как это делается для переменных внутри класса. Значение будет оставаться неизменным для всех экземпляров класса. Если какой-либо из экземпляров изменит значение статической переменной, оно изменится для всех экземпляров данного класса. Что касается функций, объявление оной как статической делает её доступной для всех экземпляров данного класса. Но сама функция "не знает" о том, какой именно экземпляр её вызвал. Как вариант, можно пометить статическую функцию указателем на вызывающий класс. Вот пример программы, где объявляются статические переменная и функция и затем демонстрируется их работа:

class cClassName
{
 public: static long m_Var;
 static long Function();
};

// Присваиваем статической переменной значение по умолчанию
long cClassName.m_Var=10;

long cClassName::Function()
{
 m_Var++; // Инкрементируем (увеличиваем на 1 с каждым проходом) значение статической переменной.
 return m_Var;
}

main()
{
 cClassName Class1, Class2;
 printf("%lu/r/n", Class1.Function()); // Выводит на экран 1
 printf("%lu/r/n", Class2.Function()); // Выводит на экран 2
}


Конструктор (constructor) и деструктор (destructor) класса

Помимо пользовательских, в каждом классе обязательно присутствуют 2 специальные функции:
  • Конструктор (constructor) класса, вызываемый для создания экземпляра класса.
  • Деструктор (destructor), вызываемый для уничтожения экземпляра класса.
Конструктор и деструктор всегда видимы (public). Имя конструктора всегда совпадает с именем класса.
Имя деструктора также совпадает с именем класса, но в целях различия к его имени добавляют префикс '(тильда).
Конструктор и деструктор можно перегружать (overload):

class cClassName
{
 public:
  cClassName(); // Конструктор по умолчанию
  cClassName(long Var1, long Var2); // Перегруженный конструктор
  cClassName(char *Data); // Перегруженный конструктор
  ~cClassName(); // Деструктор
};

cClassName::cClassName()
{
 // Проделываем здесь всякие инициализации.
}

cClassName::cClassname(long Var1, long Var2)
{
 // Проделываем здесь всякие инициализации.
 // В данном случае с двумя переменными параметрами.
 // Даже в том случае, если будет вызван другой конструктор!
 cClassName();
}

cClassName::cClassName(char *Data)
{
 // Проделываем здесь всякие инициализации.
}

cClassName::~cClassName()
{
 // Освобождаем (удаляем из памяти) все ранее задействованные переменные.
}

Констуктор вызывается всякий раз при создании экземпляра класса (instance) или объявления его нового экземпляра с помощью ключевого слова new. Конструктор может быть перегружен для предоставления нескольких способов инициализации внутренних данных.
Деструктор:
  • всегда вызывается без аргументов (=параметров),
  • доступен извне (public),
  • вызывается автоматически при удалении экземпляра класса,
  • не возвращает значений.
Вот ещё несколько примеров использования конструктора и деструктора класса:
cClassName MyClass; // Конструктор по умолчанию, вызываемый при первом выполнении программы/класса.

main()
{
 cClassName SomeClass(10, 22); // Первый перегруженный конструктор, вызываемый при выполнении функции main.
 cClassName My2ndClasss("Hello"); // Второй перегруженный конструктор, вызываемый при выполнении функции main.
 cClass *ClassPtr;
 ClassPtr = new cClassName; // Тоже вызов конструктора.

 delete ClassPtr; // Вызываем деструктор

 // По окончании выполнения функции delete вызывается деструктор
 ~My2ndClass.
}

// Теперь вызывается MyClass, завершаюший выполнение программы.


Функции-операторы (Operator Functions)

Функции класса также могут принимать форму операторов.
Операторы в C++ обычно совпадают с основными математическими операторами. Например такими как сложение (+), вычитание (-), умножение (*) и деление (/).
Вот их полный список:
Image

Для объявления оператора класса:
  1. Сперва указывается возвращаемое значение. Для операторов += ли = - это имя класса, которое передаётся в другой класс того же типа. При операциях сравнения (=) возвращаемое значение имеет один из стандартных типов данных.
  2. Вторым идёт ключевое слово operator.
  3. После символа оператора идёт стандартный набор параметров (=аргументов).
Вот пример:
class cClassName
{
 public:
  cClassName operator= (long Val);
  cClassName operator! (long Val);
 long Value()
 {
  return m_Value; // Возвращает значение защищенной (protected) переменной.
 }

 protected:
  long m_Value;
}

// Не требуется указывать тип возвращаемого значения (return type)
cClassName cClassName::operator+=(long Val)
{
 m_Value +- Val;
}

main()
{
 cClassName MyClass;
 MyClass = 10;
 myClass +- 20;
 printf("%lu\r\n", MyClassValue()); // prints 30
}


Ключевое слово this

Иногда классу необходимо знать указатель на самого себя. Для этого применяется ключевое слово this (англ. "этот"). Оно является зарезервированным (=встроенным) указателем, которым автоматически снабжается каждый сосздаваемый класс. Более того: оно автоматически (неявно, скрытно) добавляется при вызове любой функции, принадлежащией данному классу. Рассмотрим такой пример типичной функции класса C++:
long cClassName::SomeFunction(long Val)
{
 return Val + 10;
}

После компилирования и добавления скрытых (hidden) параметров выглядит так:
long cClassName::SomeFunction(cClassName *this, long Val)
{
 return Val + 10;
}

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

class cClassName
{
 public:
  cClassName() {SomeFunction(this);}
  long m_PublicVar;
};

void SomeFunction(cClassName *Ptr)
{
 Ptr->m_PublicVar=10;
}

main()
{
 cClassName cl; // cl.mPublicVar теперь равно 10.
}

Закрыть
noteОбрати внимание

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


Дружественные классы (Class Friends)

При объвлении нескольких классов часто возникает необходимость в обмене информацией между ними, и, вместе с тем, ограничить её видимость для внешнего кода. К примеру, в классе Class1 есть защищённая (protected) переменная, к которой надо получить доступ из класса Class2. Конечно, можно создать в Class1 открытую (public) функцию, предоставляющую доступ к этой переменной. Но есть способ лучше - объявить Class1 другом классу Class2. Обычно это объявляется в классе, данные которого должны быть расшарены (=доступны классу-другу). Но такое объявление невзаимно: при этом Class1 не получает доступ к данным Class2. К примеру, если первый класс объявляет второй в качестве своего друга, то после этого второй класс получает свободный доступ к данным первого. Но это не даёт первому классу доступ к данным второго (до тех пор, пока второй класс не объявит первый своим другом). Вот пример:
class cClassl
{
 friend cClass2; // cClass2 получает доступ к моим данным

 protected:
  long m_Value;

 public:
 // Следующая строка кода недоступна для cClass2, т.к. cClass2 не объявлял меня своим другом.
 cClass1()
 {
  cClass2 MyClass; MyClass.m_Value=1;
 }
};

class cClass2
{
 // cClass1 не имеет доступа к моим данным, т.к. не объявлен здесь в качестве друга.
 protected: long m_Value;

 public:
 // Следующая строка допустима
 cClass()
 {
  cClass1 MyClass;
  MyClass.m_Value=2;
 }
};

Видим, что класс cClass2 имеет доступ к данным cClassl, т.к. объявлен в cClassl другом. Но это невзаимно. cClassl не имеет доступа к данным cClass2 до тех пор, пока cClass2 не объявит cClassl своим другом.

Ветвящиеся классы (Derived classes)

Image
Рис.4 Ветвящиеся классы образуют на основе базового класса. Ветвящиеся (дочерние) классы можно также создавать на основе уже ответвлённых классов.

Когда приходит время добавить функционала в существующий класс, мы создаём дочерний класс, владеющий всеми функциями класса-родителя, и затем добавляем в потомок новые улучшенные функции и переменные. Метод создания нового класса на основе данных существующего класса называется наследованием (inheritance). Именно благодаря ему можно ответвить объект от уже имеющегося. При этом исходный класс называют базовым (base class), а созданный на его основе - дочерний или ответвлённый (derived class; см. Рис.4).
Дочерний класс определяется так:
class cDerivedClass : public cBaseClass
{
 // Данные дочернего класса.
 // Эти данные добавляются к данным, уже имеющимся в базовом (родительском) классе.
}

Раз уж данные исходного (базового) класса cBaseClass автоматически наследуются во всех дочерних, то нет необходимости добавлять их вновь. Достаточно просто добавить новые данные. Дочерний класс легко получает доступ к данным родительского (базового) класса, за исключением тех, что имеют идентификатор доступа private (закрытые).
Закрыть
noteОбрати внимание

Предварив базовый класс ключевым словом public мы указываем компилятору, что дочерние классы могут свободно обращаться данным базового класса. Для предотвращения этого используй ключевые слова private (закрытый) или protected (защищённый).

Закрыть
noteОбрати внимание

Закрытые (private) данные в базовом классе недоступны для дочерних классов. Тем не менее эти данные существуют. Обычно эти данные оч. важны и намеренно закрыты в классе-родителе.

Вот пример создания ветвящегося класса:
class cBaseClass
{
 public: long m_PublicVar;
};

class cDerivedClass : public cBaseClass
{
 public: long m_PublicVar2;
};

main()
{
 cBaseClass BClass;
 cDerivedClass CClass;
 BClass.m_PublicVar = 10;
 DClass.m_PublicVar2 = 100;
 DClass.m_PublicVar = 11;
}

Функции, вызываемые из базового класса, могут также вызываться из дочерних классов. Это и называется умным словом полиморфизм. При этом такие функции просто "считают" дочерний класс в качестве базового и получают доступ ко всем данным базового класса:
class cBase
{
 public: long m_Var;
};

class cDerived : public cBase
{

 public:
  cDerived(long Var)
  {
   m_Var = Var; // Используем m_Var из класса, наследуемого от cBase.
  }
}

long AddValue(cBase *Base, long ToAdd)
{
 m_Var += ToAdd; // Прибавить указанное значение к переменной m_Var класса cBase
                 // даже если передаваемый класс ветвится от него.
}

main()
{
 cDerived MyClass(50);
 AddValue(&MyClass, 100); // Переменная MyClass.m_Var теперь имеет значение 150.
}

Закрыть
noteОбрати внимание

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

Закрыть
noteОбрати внимание

В примере выше видим, как дочерний класс передаёт ссылку (reference = указатель) на базовый класс. Это нормально. Но в целях защиты данных базового класса от вызываемой функции, необходимо в вызываемой функции указать параметр класса в качестве переменной типа const (константа).

Данные базового класса могут быть защищены от доступа из дочернего класса путём указания в определении дочернего класса ключевого слова private или protected:

class cBaseClass{private: long Var1;};
class cDerivedClass : private cBaseClass{private: long Var2;};

class cDerivedAgainClass : public cDerivedClass
{
 public:
  long GetValue() {return Var1;} // Ошибка! Нет доступа.
}

Здесь, после определения класса cDerivedClass как наследника cBaseClass (в этот раз предварив cBaseClass ключевым словом private), доступ к переменной Var1 имеет только класс cDerivedClass.

Виртуальные функции (Virtual functions)

При использовании дочерних классов иногда возникает необходимость переписать функцию базового класса, но чтобы при этом она принадлежала исключительно дочернему классу. Для пример рассмотрим класс, выводящий на экран определённое число:
class cBaseClass
{
 public:
  void PrintIt() {PrintNum(1);}
  void PrintNum(long Num) {printf("%lu", Num);}
};

Здесь при вызове функции PrintIt из класса cBaseClass, на экран выводится число 1. Для добавления нового функционала к данной функции базового класса (скажем, для вывода перед числом какого-либо текста) обычно создают дочерний класс, в котором объявляют новую функцию PrintNum:
class cDerivedClass : public cBaseClass
{
 public:
  void PrintNum(long Num) {printf("The number is %lu", Num);}
};

В дальнейшем при создании экземпляра класса cDerivedClass и вызове функции PrintIt (которая, в свою очередь, вызывает функцию PrintNum), ожидаем, что в результате вывод будет такой:
The number is 1

Но на деле выводится только число:
1

А всё из-за того, что вызывается функция PrintNum базового класса, вместо своей тёзки из дочернего класса. Ведь компилятор ничего не знает о второй версии функции PrintNum из дочернего класса. Кроме того, базовый класс ничего не знает о своих потомках: он знает только о самом себе.
Для решения этой проблемы используют ключевое слово virtual. При объявлении функции в базовом классе с данным ключевым словом мы информируем класс о том, что дочерние классы могут (а могут и нет) переопределять (override) её. И если функция переопределена, то в этом случае будет использоваться её версия, переопределённая в дочернем классе. Зная это, перепишем определение базового класса так:
class cBaseClass
{
 public:
  void PrintIt() {PrintNum();}
  virtual void PrintNum(long Num) {printf("%lu", Num);}
};

Теперь определение дочернего класса (не нужно абсолютно всё определять как virtual) и вызов функции PrintIt теперь принесёт желаемый эффект и выведет число с текстом перед ним:
The number is 1

Компилятор, таким образом, понял, что функция PrintNum, объявленная с ключевым словом virtual, переопределена в дочернем классе и использует именно эту переопределённую версию.
Виртуальные функции необычайно полезны. Так при создании каркаса движка мы можем ответвить любой из базовых классов, добавив в класс-потомок новый функционал.

Ключевое слово const перед определением класса

Переменные, функции и даже классы, объявленные с ключевым словом const, предназначены только для чтения (read only). Класс объявляется константным так:
const cClass MyClass(Var);

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

long cClass::ReadValue() const
{
 return m_Value; // Может только возвращать значение.
}

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

Продвинутые структуры (Advanced structures)

Может показаться, что с появлением классов обычные структуры уже не нужны. Но это не так. В С++ структуры стали продвинутыми (advanced) и, наряду с классами, имеют свой собственный набор встроенных функций, включая конструктор и деструктор! Это может оказаться полезным, например, для задания переменным, хранящимся в структурах, значений по умолчанию. Например, можно создать структуру, которая резервирует (allocate) себе кусок памяти, а затем освобождает его при своём уничтожении:
typedef struct sMyStruct
{
 char *Ptr;

 sMyStruct()
 {
  Ptr = new char[100];
 }

 ~sMyStruct
 {
  delete[] Ptr;
 }
} sMyStruct;

main()
{
 sMyStruct *MyStruct;
 MyStruct = new sMyStruct;
 MyStruct->Ptr[50] = 10;
 delete MyStruct;
}

Просто относись к продвинутым структурам как к классам и тогда ты поймёшь, как они работают.

Функция assert(). Отлов ошибок и вывод их на экран

  • Не возвращает значение.
Часть времени кодеры обязательно посвящают отлову багов (=ошибок), которые так любят крашить программы.1 По дебагингу (=отладке) написана не одна сотня книг. Но в чистом виде (низкоуровневый) дебагинг - своя, довольно обширная "кухня", освоить которую весьма непросто. Кроме того, в Express редакциях MSVC++ отладчик вырезан напрочь (ограничение бесплатной версии). Поэтому для отлова ошибок воспользуемся замечательной функцией assert().

Типичные ошибки

  • необъявленные переменные;
  • некорректные значения;
  • использование неинициализированных указателей (uninitialized pointers).
Также частой ошибкой является присвоение переменным похожих имён:
char *MyName;
char *MyNames;

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

Применение функции assert()

Игрокодеры применяют Функцию assert() для отслеживания некорректных (incorrect) значений и непроинициализированных указателей. Суть функции assert() заключается в проверке выражения на истинность. Когда такая проверка возвращает значение FALSE, (в случае оконного приложения) выскакивает всплывающее окно (Popup window) с сообщением об ошибке и приложение завершает работу (aborts). Разместив вызов функции assert() в определённых участках кода, можно легко проверить, что значение переменной выбрано корректно или указатель проинициализирован. Если это не так, то в сообщении об ошибке будет содержаться подробная информация о файле исходного кода и номере строки, где данная ошибка возникла. Рассмотрим пример:
// Подключаем заголовок assert.h
#include <assert.h>

// Объявляем переменную и указатель, которые надо отследить.
long dwValue = 10;
char *pPtr = NULL;

// Проверяем, присвоено ли переменной значение 20.
assert(dwValue == 20); // Выдаст ошибку.

// Проверяем указатель на NULL-значение
assert(pPtr != NULL); // Выдаст ошибку.

Здесь обе проверки вызовут сообщение об ошибке, т.к. указатель NULL, а значение объявленной переменной - вовсе не 20.

Отключение вызова функции assert()

Самое замечательное в этой функции то, что в случае ненадобности все её вызовы можно просто запретить путём вызова специальной директивы компилятора. Это избавит кодера от необходимости рыскать по коду в поисках её ранее установленных вызовов. Достаточно разместить в начале листинга строку:
#define NDEBUG #include <assert.h>

Наличие ссылки #include <assert.h> при этом также необходимо!
Технически сообщения об ошибках появляются путём вызова функции MessageBox т.к. Функция assert() не возвращает значение.
Jim Adams в своей книге "Programming Role Playing Games with DirectX 8.0" пишет, что часто использует функцию assert() просто для вывода значений нужных ему переменных на разных этапах выполнения программы. В этом случае каждый вызов assert() является своеобразным брейкпоинтом (breakpoint, точка останова). Применять или нет функцию assert() - решать тебе. Но знать о её существовании и уметь с ней работать необходимо.

Шаблоны (Templates) функций и классов

Кратко расскажем о концепции шаблонных функций и классов в ООП и рассмотрим, для чего они используются. Тема не нова (их описание можно найти практически в каждой книге по С++), но в игрокодинге шаблонные классы используются практически повсеместно.
Ты уже знаешь, что все переменные относятся к определённому типу, называемому тип данных. Например, у тебя есть переменная типа int, float или bool (всё это основные (или "примитивные") типы данных). Переменная также может иметь тип структуры, класса, указателя на класс или экземпляра структуры. Это даёт огромные возможности по их применению, но, в то же время, часто порождает всевозможные проблемы, связанные с конфликтом типов данных. Например, что будет в результате применения определённой операции к переменной А типа int, а затем применения такой же операции к переменной B типа bool? Вот здесь-то шаблоны нам и помогут. В двух словах, шаблоны (классов и функций) используются для осуществления одних и тх же операций с различными типами данных. Рассмотрим следующий сильно упрощённый пример.
Представь, что тебе необходимо написать функцию, которая производит сложение двух переменных типа int и возвращает результат. Ты быстро решишь эту задачу, написав функцию, которая принимает в качестве параметров две переменные типа int и затем возвращает их сумму. А что если вдруг понадобится произвести такую же операцию, но уже с переменными типа float, с сохранением результата с той же точностью (то есть без дробных значений)? Очевидно, что ты не станешь использовать для этого ту же самую функцию, что ты до этого написал для сложения переменных типа int. Написать для этого ещё одну функцию, конечно же, не проблема. Но проблема кроется в другом. Что если тебе понадобится произвести ту же операцию, но уже с переменными типа double? Как видишь, мы уже написали целых 3 новых функции, которые проделывают одну и ту же операцию. Их определение может выглядеть примерно так:
Пример функции сложения значений разных типов
int AddInts(int valuel, int value2);
float AddFloats(float valuel, float value2);
double AddDoubles(double value1, double value2);

Что, если бы была возможность сжать эти 3 функции в одну, совместимую с каждым из этих типов данных? Да, такая возможность есть. Для этого и были придуманы шаблоны (templates).
При использовании шаблонов объявление новой функции выглядело бы так:
template <class Type> Type Add(Type value1, Type value2);

Эта шаблонная функция принимает значения любого типа и пытается их сложить, используя математический оператор "+", а затем возвращает значение такого же типа. Другими словами, ты можешь сложить переменные типа int, а затем переменные типа float, используя всего одну эту функцию. За определение типа данных отвечает оператор class Type, который позволяет программеру указывать, какой тип данных будет использоваться в данной функции. Затем функция просто принимает две переменные, указанного типа, и возвращает значение того же типа. Реализация этой шаблонной функции выглядела бы так:
int MyInt = Add<int>(5, 45);
float MyFloat = Add<float>(3.2, 17.47);
double MyDouble = Add<double>(2 9.4 9 67, 12.01568);

Шаблонизировать можно не только функции. Ты даже можешь создавать целые классы, которые являются шаблонами для других классов. Взгляни на этот пример:
template<class Type> class Maths
{
public:
	Type Add(Type valuel, Type value2);
	Type Multiply(Type value1, Type value2);
	Type Divide(Type valuel, Type value2);
}

Здесь показан шаблонный класс, который используется для выполнения над параметрами некоторых математических операций. Мы можем создать экземпляр этого класса, указать, с каким типом данных он будет работать, а затем выполнить любую из операций с переменными данного типа:
Maths<float> *floatMaths = new Maths<float>;
float addTest = floatMaths->Add(2.4 5f, 15.9f);
float multiplyTest = floatMaths->Multiply(10.0f, 5.5f);
float divideTest = floatMaths->Divide(16.4f, 4.0f);

В первой строке этого исходного кода создаётся экземпляр класса Maths, работающий с типом данных float. Следующие три строки выполняют операции из нашего нового класса Maths (производят операции сложение, умножение и деление) над переменными типа float (тот же самый, что указан в конструкторе).
Вообще, наиболее часто шаблоны применяются именно при создании классов. Проще всего представить шаблонный класс как некое клише. Другими словами, шаблонный класс не является объявлением класса, а служит в качестве клише (формы, болванки, трафарета и т.д.) для объявления класса. Таким образом, фактическое объявление класса происходит, когда создают экземпляр (инстанс) шаблонного класса, с указанием набора параметров. В нашем предыдущем примере мы создавали шаблонный класс с всего одним параметром. Их может быть и больше. Как в этом примере:
template <class Type, int size> class MyData
{
public:
	void SetData(type Data);
	Type GetData();
private:
	char buffer[size];
}

О шаблонах можно рассказывать ещё долго. Однако, для их использования вполне достаточно базовых знаний, приведённых в этой статье. Просто помни об этой эффективной технологии и используй в своих проектах. На основе шабонов построено множество других программерских приёмов и технологий. Один из них - связный список (Linked list), без которого наверняка не обходится ни один игрокодерский проект.

Заключение

Все учебные курсы по программированию игр на C++ почти целиком основаны на вышеизложенном материале. Данная теория является основой основ любого проекта.

Источники


1. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press. 2002
2. Morrison M. Sams Teach Yourself Game Programming in 24 hours. - Sams Publishing. 2002


Последние изменения страницы Понедельник 27 / Июнь, 2022 09:51:05 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++