Предыдущие части: Делаем поле, Какую память выделить, Структура программы, Пишем Сапёра на С + SDL
Код для этой части находится в ветке graphics на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
В языке C невозможно обойтись без указателей. Точнее, без них невозможно обойтись вообще нигде, но в таких языках как Java, JavaScript, PHP, Python указатели просто замаскированы, и мы видим только имена переменных.
В языке C указатель делается явно путём добавления "*" после типа переменной.
- int a – это переменная целого типа
- int * a – это переменная-указатель на целый тип
В чём отличие?
Как мы уже знаем из предыдущих выпусков, под каждую переменную выделяется память. Сколько памяти выделить, решаем мы, указывая для этого тип переменной.
Если мы напишем int a, то будет выделено четыре байта памяти, а если char a, то один байт. На самом деле, учитывая всевозможные оптимизации и выравнивания блоков памяти, может выделяться и больше, но суть в том, что объявив тип char, мы можем работать только с одним байтом.
Итого, после выделения памяти для переменной мы получаем 3 сущности:
- имя переменной – существует только в тексте программы, условно обозначая тот адрес, который выделен.
- адрес переменной – реальный адрес памяти, который выделен. У него есть значение, например 100.
- значение переменной – то, что записано по адресу. Если переменная типа int, значит значение состоит из четырёх байт, если char, то из одного.
Адрес переменной и значение переменной очевидно разные понятия. По адресу 100 можно записать значение 5.
Если имя переменной "a" назначено адресу 100, то когда мы выполняем операции с "a", мы берём содержимое, которое хранится по адресу 100. Например, пусть "a" это адрес 100, "b" это адрес 104, а "c" это адрес 108:
c = a + b
Это:
память[108] = память[100] + память[104]
Если по адресу 100 хранится 5, а по адресу 104 хранится 10, то в адрес 108 запишется 15.
Самое удивительное, что в языке C мы можем узнать адрес переменной. Для этого перед именем переменной надо записать "&":
- a – это содержимое памяти по адресу 100
- &a – это сам адрес 100
Если написать так:
int b = &a;
То в переменную "b" запишется число 100. Посмотрите, что изменилось:
память[104] = 100
Хотя мы и не объявляли переменную "b" как указатель, мы фактически сделали её указателем. Теперь она содержит в себе число, которое мы трактуем не как значение переменной, а как адрес, где хранится значение.
Чтобы получить значение из адреса, нужно написать так:
int c = *b
Знак "*" указывает, что переменная – это указатель, то есть адрес. Если b = 100, значит значение берется из адреса 100. То есть получается так:
память[108] = память[память[104]]
Вышеприведённые примеры не совсем корректны, так как язык C будет выдавать или ошибки, или предупреждения о несовместимости типов. Всё это можно исправить с помощью приведения типов, но я это опустил в угоду читабельности.
Короче говоря, любую переменную можно превратить в указатель и наоборот, даже если они объявлены с другими типами.
Зачем нам нужны указатели?
Если нужно передать куда-то просто содержимое адреса памяти, такое как 5, 10, и т.д., то мы просто передаём его как есть.
Но мы часто имеем дело с большими и структурированными объёмами данных: строками, массивами, собственно структурами. Нельзя запихнуть массив в один адрес. Это много адресов. Также нельзя передать куда-либо много значений за один раз.
Вот в таких ситуациях мы и передаём вместо целой структуры только указатель на эту структуру, то есть адрес её начала в памяти.
Наглядные примеры мы увидим прямо сейчас.
Рисование поля
На данном этапе крайне примитивно изобразим поле заданного размера в виде квадратиков. Если в клетке нет мины, то квадратик будет серый, если есть – чёрный.
Обратимся к функциям SDL:
SDL_Init(SDL_INIT_VIDEO);
Во-первых, мы вызываем функцию SDL_Init(), передав туда аргумент-константу SDL_INIT_VIDEO (всё это объявлено в SDL.h, как вы помните)
SDL проинициализирует всё что нужно для видео, нас это не волнует. Идём дальше:
Это создалось окно размером 640 * 480. Переменная window была объявлена так:
SDL_Window * window;
Это значит, что есть структура данных, которая описывает окно. Тип этой структуры называется SDL_Window. Мы объявляем переменную window такого типа. Но как можно заметить, эта переменная – указатель на тип. То есть в ней будет храниться не структура SDL_Window, а адрес этой структуры. Адрес нам возвращает функция SDL_CreateWindow().
Подытожим: фунция SDL_CreateWindow() где-то в памяти создаёт структуру типа SDL_Window и адрес этой структуры возвращает нам. А мы этот адрес помещаем в переменную-указатель window.
Далее,
Основной инструмент SDL это "surface", то есть "поверхность" или "графический контекст". Чтобы что-то нарисовать, мы должны получить доступ к поверхности.
Как видим, есть ещё одна структура с типом SDL_Surface, которая описывает поверхность. Как и в случае с окном, мы получаем указатель на поверхность от функции SDL_GetWindowSurface().
Подытожим: мы передаём в функцию SDL_GetWindowSurface() указатель на структуру окна, который хранится в нашей переменной window. Потому что нам нужна поверхность именно этого окна. Функция там где-то копается, и возвращает нам адрес структуры типа SDL_Surface, который мы помещаем в переменную-указатель screenSurface.
SDL_Rect – это просто структура, которая содержит описание прямоугольника: координаты x,y левого верхнего угла, ширина и высота. Эта структура понадобится нам для рисования. С помощью фигурных скобок мы статически инициализируем структуру. В данном примере x = 100, y = 100, width = 200, height = 200.
Эта структура хранится в переменной rect, но заметьте, что она не указатель. В адресе, который называется rect, хранится не адрес структуры, а непосредственно первый элемент структуры. Так как компилятор знает состав структуры (сколько в ней полей, какого они типа и в каком порядке идут), доступ к остальным элементам происходит путём автоматического изменения адреса на +4, +8 и т.д.
Мы можем получить указатель на эту структуру, например, так:
SDL_Rect * rect_ptr = ▭
В rect_ptr будет храниться адрес структуры rect.
Наконец, ещё одна функция:
SDL_FillRect(screenSurface, &rect, SDL_MapRGB(screenSurface->format, 255, 0, 0));
Она рисует закрашенный прямоугольник. В качестве аргументов мы передаём: указатель на поверхность, указатель на структуру типа SDL_Rect (собственно координаты прямоугольника), и цвет.
Смотрите: функция SDL_FillRеct() требует указатель на поверхность. У нас он есть, мы его передаём. Так как переменная уже была объявлена как указатель, мы просто пишем её имя без всяких значков: screenSurface. Это просто значение переменной. Но ведь её значение это адрес. Нужен указатель – вот вам указатель.
Далее, функция требует указатель на структуру SDL_rect. Наша переменная rect не объявлена как указатель, поэтому мы берём не содержимое переменной, а её адрес: &rect. И вот уже получился указатель, который мы передаём в функцию.
Наконец, нужно передать цвет.
Цвет задаётся с помощью трёх чисел R, G, B, но должен быть сконвертирован в тот формат цвета, который использует поверхность, то есть структура screenSurface. У неё есть поле format, которое описывает текущий формат цвета. Когда переменная не указатель, доступ к полям структуры делается через точку:
screenSurface.format
Когда же она указатель, как в нашем случае, тогда через стрелку:
screenSurface->format
Функция SDL_MapRGB() преобразует формат цвета. В неё мы передаем screenSurface->format и наше значение цвета в виде трёх чисел R, G, B.
Результат этой фунции мы и передаём как последний параметр в SDL_FillRect().
Итак, чтобы нарисовать прямоугольник, надо немножно потрудиться:
- передать указатель на поверхность (сначала получив его)
- передать указатель на структуру, описывающую прямоугольник
- передать цвет в нужном формате, предварительно переконвертировав его
Воспользовавшись этими знаниями, напишем функцию draw_field(). В неё передадим ссылку на поверхность, ссылку на поле, ширину и высоту поля.
Отметим некоторые детали.
Поле field у нас объявлено как массив типа unsigned char, но в функцию мы его передаём как указатель на тип unsigned char.
На самом деле это практически одно и то же. Первый элемент массива можно получить так:
field[0]
Второй элемент так:
field[1]
И т.д. То же самое можно сделать с помощью указателя. Первый элемент:
*field
Второй элемент:
*(field + 1)
И т.д. Далее по тексту мы видим использование field опять в роли массива: field[addr], что аналогично *(field + addr). Доступ к массиву, строке и прочим стуктурам это формула "адрес + смещение", и вы эту формулу задаёте либо смещением в квадратных скобках, либо прибавляя его к указателю. Главное – понимать, что именно и в каких случаях является адресом.
Заранее сконвертированы два цвета, чтобы не делать это каждый раз: field_color (серый) и mine_color (чёрный):
Uint32 field_color = SDL_MapRGB(surface->format, 192, 192, 192);
Uint32 mine_color = SDL_MapRGB(surface->format, 0, 0, 0);
Также заранее создана структура SDL_Rect. Мы будем рисовать одинаковые квадраты, поэтому в инициализации сразу укажем ширину и высоту, которые больше не будем трогать: 15, 15.
SDL_Rect rect = {0, 0, 15, 15};
Ну а дальше просто рисуем матрицу в цикле, меняя цвет в зависимости от содержимого клетки. И вот наш результат:
Пока криво и некрасиво, но фактически вся работа по графике уже сделана. Дальше займёмся интерфейсом пользователя.
Читайте дальше: Графический интерфейс
Читайте также: Зачем нужны указатели