Сегодня мы рассмотрим три совершенно невзаимосвязанные темы: структуры, перечисления и объединения. Так как они не слишком сложные, мы рассмотрим их все вместе.

Структуры (struct)
Использование структур обусловлено необходимостью объединить несколько переменных.

Структуры - это, обычно, группа переменных описывающих какую-нибудь сущность.

Например, в игре танк можно описать группой переменных: количество снарядов, количество горючего и т.д. У нас в примере танк будет обладать следующими параметрами: координаты и количество топлива (от нуля до двадцати).
Зачем нам использовать какие-то структуры спросите Вы? Ведь переменные и так прекрасно хранятся.

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

Объявления у структуры нет. Её нужно сразу определять. Тело структуры должно находиться до начала main.

код на языке c++
struct tank
{
  int x, y;
  int fuel;
};

Здесь мы видим структуру с именем tank. Перед именем стоит ключевое слово struct (от structure - структура). В теле структуры находятся объявления переменных отвечающих за координаты и кол-во топлива. Обратите внимание, что после закрывающей фигурной скобки стоит точка с запятой. Не забывайте о ней в своих структурах.

На самом деле мы только что создали новый тип данных. Такие типы данных - определяемые программистом, называются пользовательскими. К пользовательским типам данных, помимо структур, относятся и классы, но об этом в следующий раз.
Когда мы определяем структуру, под неё не выделяется память. Структура - это как шаблон. Теперь на основе структуры tank можно создать много структурных переменных. Все эти переменные будут иметь тип tank, и каждая структурная переменная будет иметь доступ к своим переменным x, y, fuel.

Сейчас нам нужно определить переменную типа tank. Или даже две:

код на языке c++
tank t34 = {0,0,20}; // x, y, fuel
tank pz4 = {8,7,20};

Обе переменные мы сразу проинициализировали (хотя этого можно было и не делать). Для этого использовали список значений в фигурных скобках. Значения в скобках присваиваются переменным внутри структуры в том порядке, в котором они были объявлены при определении структуры: x, y, fuel.

Теперь у нас есть две переменные t34, pz4 типа tank. У каждой есть поля x, y, fuel. Подчёркиваю красным: каждая структурная переменная обладает своим набором переменных, которые были объявлены в структуре.

Чтобы получить доступ к полю, нужно воспользоваться операцией доступа (операцией точки). Например:

код на языке c++
pz4.fuel = 100;
cout << pz4.fuel << "\n";

Сначала мы указываем имя структурной переменной, затем ставим точку и в конце указываем имя поля структуры. Здесь мы присвоили значение полю fuel структурной переменной pz4, а затем вывели значение поля fuel на экран.

Напоследок, вот как получить доступ ко всем полям двух структурных переменных:

t34.x;
t34.y;
t34.fuel;
pz4.x;
pz4.y;
pz4.fuel;

Структурные переменные в памяти компьютера

А вот как структурные переменные располагаются в памяти компьютера (адреса в шестнадцатиричном формате):
Фото
Структурная переменная в памяти компьютера

Каждая переменная типа int занимает в памяти четыре байта. Вся структурная переменная структуры tank займёт в памяти 12 байт - три переменные типа int.

Адрес переменной - младший байт этой переменной. Например, адре переменной t34.fuel - 0012FEA2.

Кроме того стоит заметить, что адрес самой стуктурной переменной t34 совпадает с адресом первого поля x. Т.е. адрес t34.x равен 0012FE94, адрес t34 равен 0012FE94.

Также обратите внимание, что все поля t34 расположены друг за другом. Заканчивается переменная x (адрес 0012FE97) и следующий байт уже принадлежит y (0012FE98), заканчивается y (адрес 0012FEA1) и сразу же начинается fuel (адрес 0012FEA2). Запомните, поля структурной переменной хранятся в памяти друг за другом, между ними нет никаких других данных.

И ещё одно, две структурные переменные t34 и pz4 необязательно расположены в памяти рядом. В данном примере между ними больше 140-а байтов. Кроме того, переменная pz4 расположена в памяти "раньше" - адрес этой переменной меньше адреса t34.

Хорошенько разберитесь со всем этим. Когда дойдём до рассмотрения указателей, вы поймёте, зачем необходимо понимание того, как устроена память.
Перечисления (enum)

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

С помощью перечислений можно задать дни недели, месяцы, ну или что-нибудь подобное. В качестве примера рассмотрим стороны света. Стороны света на экране расположены так: вверху - север (north), справа - восток (east), слева - запад (west) и внизу - юг (south).

enum cardinal_dirs { north, west, east, south };

Здесь мы определили перечисление cardinal_dirs. В начале строки стоит ключевое слово enum (enumeration - перечисление). Затем указывается имя перечисления, после которого, через пробел, в фигурных скобках задаются значения, которые смогут принимать переменные типа cardinal_dirs. После фигурных скобок ставится точка с запятой.

После того, как определено перечисление, можно создавать переменные нового типа:

cardinal_dirs ch = north;

Обратите внимание, что переменные типа cardinal_dirs могут принимать только четыре значения: north, south, east, west. Кроме того, элементы в перечислении нумеруются от нуля. Т.е. north = 0, east = 1, south = 2, west = 3. Вместо перечисления мы могли бы создать четыре константы:

const int north = 0;
const int east = 1;
const int south = 2;
const int west = 3;

Результат одинаковый.

Как видите использовать перечисления довольно легко. Но стороны света у нас закодированы клавишами стрелочек, поэтому нам нужно инициализировать элементы перечисления числами. Как я уже писал, отсчёт ведётся с нуля. Но, к счастью, это можно изменить. Если мы переопределим какой-либо элемент значением 75, то следующий, получит значение 76. Нам нужно переопределить все четыре значения.

enum cardinal_dirs { north = 72, west = 75, east = 77, south = 80 };

Про структуры и перечисления всё. Есть ещё некоторые важные моменты по структурам, но нам они пока не нужны.

Ещё раз повторюсь, точно такого же результата можно было бы добиться с помощью четырёх констант.
Объединения (union)

Объединения используются когда необходимо получить доступ к одним и тем же данным разными способами.

Допустим мы хотим вывести все поля структуры tank. Пока что у нас есть только один способ сделать это:

cout << t34.x << "\n";
cout << t34.y << "\n";
cout << t34.fuel << "\n";

Гораздо удобнее это было бы сделать с помощью цикла. Но тогда нам пришлось бы использовать массив, а определение структуры выглядело бы вот так:

struct tank
{
int t[3];
};

Теперь вывести структурную переменную на экран проще:

for (int i; i < 3; i++)
{
cout << t34.t[i] << "\n";
}

Но при использовании массивов переменная потеряла в гибкости, и код стал не таким понятным:

t34.x = 3; // До..
t34.y = 4;

t34.t[0] = 3; // ... и после.
t34.t[1] = 4;

Можно объединить эти два способа с помощью объединения. :)

При этом структура tank будет выглядеть вот так:

код на языке с++
union tank
{
  struct
  {
    int x,y;
    int fuel;
  };
  int t[3];
};

Во-первых мы заменили struct на union (union - объединение). Внутри объединения расположена безымянная структура в которой определены все те переменные, которые были в структуре tank.

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

Теперь создадим переменную и обратимся к её полям:

код на языке с++
tank t34;

t34.x = 5;
t34.y = 1;
t34.fuel = 20;

cout << t34.t[0] << "\n";
cout << t34.t[1] << "\n";
cout << t34.t[2] << "\n";

На экран будет выведено:

5
1
20

Дело в том, что x и t[0] - разные имена одного и того же участка памяти, также как и: y и t[1], fuel и t[2].

Вот как переменная типа tank расположено в памяти:
Фото
Структурная переменная в памяти компьютера

union tank имеет тот же адрес, что и x и t[0], просто у нас картинки рисует чрезвычайно толковый парень, поэтому получается так.

На картинке перед каждым адресом стоит: 0x (ноль икс). Это обозначение используется с шестнадцатиричными числами, т.е. если перед числом вы увидите 0x - то оно представлено в шестнадцатиричном формате.

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

код на языке с++
union tank
{
  int x;
  int y;
  int fuel;
  int t[3];
};

Ничего хорошего из этого не выйдет. Здесь x, y, fuel и t[0] - имена одного участка памяти.

В будущем мы будем использовать объединения следующим образом:

код на языке с++
struct tank
{
  union
  {
    struct
    {
      int x,y;
      int fuel;
    };
    int t[3];
  };
};

Но вместо слово struct в первой строке будет стоять class. Сейчас вот этот вариант (struct tank) и предыдущий (union tank) работают одинаково.

Применение объединений очень сильно ограничено. Мы их рассмотрели, так как они особенно часто встречаются в математических библиотеках.

Новая версия программы pseudo_game

Теперь продолжим работать с программой pseudo_game. Откройте в редакторе код, программы pseudo_game_0_3 (можно найти в разделе листинги).
Сегодня мы добавим на карту ещё один танк, которым будет управлять компьютер. Но не ждите от кремниевого болвана чудес сообразительности. Пока что, он будет довольно тупым. Можно даже сказать, что чрезвычайно тупым.

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

Начнём с общего. При запуске программы, на экране мы видим два танка. Одним управляет пользователь, другим компьютер. На карте есть склад, где танки заправляются. Если топливо заканчивается у игрока, то всё - конец. Компьютер управляет танком следующим образом: каждый ход выбирается новое случайное направление.

Вот список всех объявлений функций программы. Некоторые функции вызываются из других функции, например movement_if() вызывается функциями input() и ai().

код на языке с++
void game_init(char map[s][c]);   // прототипы функций
int input(tank&, char map[s][c]);
void ai (tank& t, char map[s][c]);
void movement_if(tank&, cardinal_dirs, char map[s][c]);
void show(char map[s][c]);

Функция main()

В функции остались определения только переменной кода ошибки и массива. Здесь же происходит инициализация двух структурных переменных tank. До основного цикла (while) вызываются две функции: game_init() и show(). Первая инициализирует массив, вторая выводит массив на экран.

В основном цикле сначала вызывается функция ввода от пользователя input(). Затем проверяется, кончилось ли топливо у игрока. И напоследок вызывается функция ai(), которая управляет вторым танком.

Функция game_init()

Функция довольно простая. Обратите внимание, что после инициализации массива пробелами, на карту помещаются танки и склад - в массив добавляются соответствующие буквы.

Функция show()

Данная функция вызывается всякий раз, когда происходит движение одного из персонажей игры.

Она, опять же, довольно простая: очистка экрана и всё те же вложенные циклы.

Функция input()

В этой функции происходит ввод с клавиатуры. Здесь мы используем переменную с кодами ошибок для отслеживания нажатия Esc.

В данной функции находится ветвления с тремя проверками:

Первое условие

Была ли нажата одна из стрелочек:

код на языке с++
if (ch == north || ch == east || ch == west || ch == south)
{
  movement_if(t, static_cast<cardinal_dirs>(ch), map);
}

Две вертикальные черты - логическая операция ИЛИ. Если хотя бы одно условие из четырёх выполняется, то и всё условие истинно. Есть ещё логические операции: НЕ (восклицательный знак !) и И (двойной амперсанд &&). Логическое И используется, когда необходимо выполнение всех условий и если хотя бы одно не выполняется, то условие ложно.

В теле данного блока вызывается функция movement_if(). В неё мы передаём танк. В данном случае танк игрока. Второй аргумент - направление движения. Здесь мы приводим к созданному нами типу перечисления переменную, содержащую код клавиши. Ну и передаём массив.

Второе условие

Отслеживание клавиши Escape. Тут всё просто: выполняется условие - мы переменной e присваиваем единицу.

Третье условие

Данная ветвь самая интересная. Нажатие стрелочек посылает два ASCII кода подряд: первый - -32, второй - например, 77 или 80. Если бы мы для передвижения использовали подходы из прошлых программ - клавиши стрелочек обрабатывали две итерации основного цикла, то в данной программе компьютерный персонаж двигался бы в два раза быстрее: пользователь нажимает клавишу, посылается код -32, компьютер передвигается, начинается следующая итерация основного цикла, посылается код клавиши, компьютер передвигается.

В данной программе при получении кода -32, мы ещё раз вызываем input(). Т.е. input() вызывает сама себя: функция в этот раз уже ловит код передвижения (77,75,72,80). Данный приём - вызов функцией самой себя, называется рекурсией.

Функция ai()

Здесь мы встречаемся с новой функцией - rand(). Она объявлена в файле stdlib.h, который у нас уже включён (system("cls")).

Функция rand() генерирует случайное целое число в диапазоне от 0, до 32767.

В этом же операторе мы встречаем новую арифметическую операцию % - остаток от деления. Поясняю: например, rand() вернула 31, это число делится на 4 - получается 7 и остаток 3: 32 = 4*7 + 3. Вот операция % и берёт этот остаток, а целую часть (7) отбрасывает.

В итоге в выражении rand()%4 всегда получается четыре результата - 0 (когда число разделилось без остатка), 1, 2, 3.

Это случайное число мы используем в switch, где выбирается направление.

В последнем операторе функции мы вызываем movement_if и в неё мы передаём текущий танк (в данном случае, им управляет компьютер), направление, которое мы получили с помощью rand() и массив.

movement_if

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

Рассмотрим только блок кода в котором происходит передвижение влево.

код на языке с++
int x = t.x;  // создаём временные переменные  для хранения координат
int y = t.y;  // танка который сейчас выбран

if (ch == west) // Проверка направления
{
  if (map[x][y-1] == ' ') // провека: является ли клетка назначения пустой
  {
    map[x][y] = ' '; // "Убираем" объект  со старой координаты
    y--;             // изменяем координаты
    map[x][y] = 'Т'; // "рисуем" объект  на новом месте
    t.fuel -= 1;     // уменьшаем количество топлива текущего танка
  }
  else if (map[x][y-1] == 'С') // проверка: заехал ли танк  на склад
  {
  	t.fuel = 20;  // пополняем топливо  для текущего танка
  }
}

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

В начале мы локальным переменным x,y присваиваем координаты танка. Сделано это для краткости - в функции мы много используем x, y. Хотя, можно обойтись и t.x, t.y.

Если танк нужно переместить на запад (т.е. условие выполняется), то начинается выполнение внутреннего ветвления if.

Внутреннее if решает следующие задачи:

1. Проверка выхода за пределы карты. Теперь танк никогда не исчезнет с карты. Появляется, правда, одна интересная ошибка при попытке выезда танка за пределы карты на восток или на запад.
2. Проверяется, не заехал ли танк на занятую клетку: другой танк, склад.
3. Если танк заехал на склад, то пополняется топливо.

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

Затем вызывается функция show(). и наконец, локальные переменные x,y мы присваиваем t.x, t.y, т.к. они были изменены.

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

Напоследок ответьте на вопрос: почему структурные переменные мы передаём в функции по ссылке?