Массивы это удобный способ хранить данные одного типа.

Помимо высокоуровневых классов типа std::vector или std::array в C++ существует низкоуровневое представление массивов.

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

Самые низкоуровневые массивы могут быть динамическими и статическими.

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

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

Про динамические массивы сложно говорить в отрыве от указателей и динамической памяти, поэтому мы вернёмся к ним после изучения указателей.

Стоит также знать, что понятие "статический массив" не связано с модификатором static, а связано с тем, что размер известен на этапе компиляции и не меняется во время выполнения. Это разные вещи.

Объявление

int a[5];

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

Для учебных примеров достаточно будет указать "магическое число" в качестве размера, но в реальных ситуациях такого лучше избегать и использовать именованную константу.

int size = 5;
int a[size]; // expression must have a constant value

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

Для статических массивов это недопустимо.

Это исправляется добавлением модификатора const к переменной.

const int size = 5;
int a[size];

В данном случае size - константное выражение, поэтому его значение известно во время компиляции.

Инициализация

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

Если массив объявлен глобально, то значения гарантированно будут нулями.

int a[5];

int main() {
    std::cout << a[0];
}
0

У нас даже ошибка не возникает при компиляции.

То же самое будет, если объявить массив с определённым модификатором static int a[5];. В этом случае, как и с глобальным массивом, память будет выделена в секции бинарного файла.

int main() {
    int a[5];
    std::cout << a[0];
}
-858993460

А вот так это уже локальный массив, память выделяется на стеке и обращение в такую память это неопределённое поведение.

На самом деле, если мы выведем число в HEX используя выражение std::cout << std::hex << a[0];, то мы увидим значение cccccccc, которое используется в MSVC как маркер неинициализированной стековой памяти в отладочной сборке (Debug). Если мы переключим в Release, то обнаружим 0, что опять таки не является гарантией.

Чтобы избежать неопределённого поведения можно явно задать значения элементов в 0 используя агрегатную инициализацию.

int a[5] = {};

Чтобы инициализировать каждый мы можем задать эти значения через запятую.

int a[5] = {1, 2, 3, 4, 5};

Если мы инициализируем массив таким образом

int a[5] = {1, 2, 3};

то последние два элемента будут равны нулю.

Инициализировать можно каждый по индексу.

int a[5];
a[0] = 1;
a[1] = 2;
a[2] = 3;
a[3] = 4;
a[4] = 5;

Индексация

Если мы храним несколько элементов в одном месте - у них должны быть номера.

Индекс - порядковый номер элемента в массиве.

Хоть и существуют языки с нестандартной индексацией, начинающейся с единицы или вообще с любого числа, но в C++, как и в большинстве языков индексация элементов массива начинается с индекса 0.

int a[5];
std::cout << a[2] << std::endl;
a[2] = 123;
std::cout << a[2] << std::endl;
0
123

Тут мы объявляем массив a из пяти элементов, выводим элемент по индексу 2, инициализируем элемент массива a по индексу 2 и снова выводим его же.

Если мы хотим изменить первый элемент, то мы обращаемся по индексу 0, если мы хотим изменить последний элемент, то мы обращаемся по индексу меньшим на единицу длины массива.

В нашем случае длина массива это 55, а индекс последнего элемента это 5−1=45 - 1 = 4

Таким образом обращение к первому и последнему элементу будет выглядеть так:

int a[5] = {-1,0,0,0,1};
std::cout << a[0] << std::endl;
std::cout << a[4] << std::endl;
-1
1

Если мы обратимся по индексу 5 то мы получим неопределённое поведение.

int a[5] = {};
std::cout << a[5] << std::endl; // Reading invalid data from 'a'.
0

Мы получаем предупреждение от компилятора, но не ошибку.

В случае MSVC и текущих параметров компиляции мы получаем 0, но это поведение ничем не гарантированно в общем случае.

int a[5];
std::cout << a[-1] << std::endl;
a[-1] = 1;
std::cout << a[-1] << std::endl;
32758
1

То же самое происходит, когда мы обращаемся по индексу -1.

int main()
{
    int a[5];
    std::cout << a[-1] << std::endl;
    a[-1] = 1;
    std::cout << a[-1] << std::endl;
    return 0;
} // Run-Time Check Failure #2 - Stack around the variable 'a' was corrupted.

Такое обращение также является невалидным.

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

В дальнейшем мы разберёмся, почему обращение по индексу фактически связано с указателями и чтением значения по определённому адресу памяти.

Важно понимать, что неопределённое поведение может проявляться по-разному.

Код может вывести какое-то значение из памяти, может вывести 0, может завершиться с ошибкой, а может внешне работать нормально.

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

Массивы и циклы

Цикл - основной инструмент для работы с массивами.

Допустим, у нас есть массив из пяти элементов и мы можем пройти по всем его элементам с помощью цикла.

int main()
{
    int a[5] = {5, 4, 3, 3, 3};
	for (size_t i = 0; i < 5; i++)
	{
		std::cout << a[i];
	}
    return 0;
}
54333

Переменная i в данном случае является индексом элемента массива.

Она последовательно принимает значения 0, 1, 2, 3 и 4, после чего цикл завершается.

Точно также можно не только читать элементы, но и записывать в них значения.

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

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

Эта публикация содержит только часть статьи.

Полные уроки по C++, реверс-инжинирингу и разработке софта вы можете найти тут: