
Эти два понятия — массивы и указатели — неразрывно связаны между собой.
Указатель — это переменная для хранения адреса другой переменной. Его объявление содержит тип и знак *. Чтобы узнать адрес переменной, нужно указать знак амперсанд и её имя: &foo. Чтобы работать со значением, записанным по адресу в указателе, воспользуемся операцией, которая называется разыменование указателя: *ptr.
int foo = 1;
int *ptr = &foo; // Адрес переменной сохраняем в указатель.
printf("%d %d", foo, *ptr); // Прямое и косвенное обращение.
Как будет выглядеть ввод чисел при прямом и косвенном обращении?
scanf("%d", &foo);
scanf("%d", ptr); // ptr уже адрес, знак & не нужен.
Не имеет значения, записываете вы int* ptr, int *ptr или int * ptr. А при объявлении в одну строку звёздочка распространяется только на первое имя.
int* ptr1, ptr2;
Предыдущая запись эквивалентна следующей.
int* ptr1;
int ptr2;
Давайте напишем простую функцию, которая меняет местами значения двух переменных.
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
В функцию передаются не значения переменных, а их адреса (указатели). Операция разыменования *a позволяет обращаться к значению, которое скрывается за указателем. Вызов функции будет выглядеть так (если аргумент является указателем, то операция взятия адреса & будет лишней).
swap(&foo, &bar);
Сейчас вам должно быть понятно, когда знак & в функции scanf() требуется, а когда нет. Если вы передаёте указатель, то этот знак не требуется.
Если нужно инициализировать указатель, значение которого пока не известно, нужно воспользоваться NULL (может быть определено как 0, 0l или (void*)0).
int* ptr = NULL;
Некоторые функции, которые принимают указатель в качестве аргумента, обязательно должны проверять, равен этот указатель NULL или нет. То же нужно делать с указателями, которые возвращают некоторые функции.
if (ptr != NULL)
{
// ...
}
NULL можно привести к любому типу-указателю, это значит, что ptr в этом выражении может быть указателем на любой тип.
Теперь перейдём к массивам. Массив — набор переменных, расположенных в памяти непосредственно друг за другом. К элементам массива обращаются по индексу (номеру). Начальный элемент имеет индекс 0. Размер массива должен быть константой, хотя некоторые компиляторы поддерживают массивы переменной длины.
Где-то в начале программы, ещё до функции main(), нужно определить размер массива с помощью директивы препроцессора (он подробнее рассматривается позднее).
#define SIZE 10u
Тогда в функции main() или любой другой можно написать объявление массива.
int data[SIZE]; // Массив из десяти целых.
Примечание. В языке C константы не являются константами в полном смысле этого слова, поэтому недостаточно написать const size_t size = 10u;, хотя это возможно в языке C++. (Тип size_t является псевдонимом для типа unsigned Можете вместо него для простоты писать unsigned или даже int.)
За объявлением массива может следовать список инициализации.
int data[SIZE] = {5, 1, 3, 34, 0, 21, 2, 1, 8, 13};
Если указать не все элементы, то остальные будут нулями.
int data[SIZE] = {5, 1, 3, 34}; // {5, 1, 3, 34, 0, 0, 0, 0, 0, 0}
Можно воспользоваться выделенным инициализатором.
int data[SIZE] = {5, [5] = 21, 2, [1] = 1}; // {5, 1, 0, 0, 0, 21, 2, 0, 0, 0}
Повторно инициализировать массив нельзя.
int data[SIZE] = {5, 1, 3, 34, 0, 21, 2, 1, 8, 13};
data = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34};// Ошибка.
Зато можно не указывать размер, он будет вычислен автоматически.
int data[] = {5, 1, 3, 34, 0, 21, 2, 8, 1, 13};
Вот небольшая программа, которая перебирает все элементы массива, ищет в нём самый большой элемент и его индекс (INT_MIN определено в заголовочном файле limits.h).
int max = INT_MIN;
size_t index = 0;
for (size_t i = 0; i < size; ++i)
{
if (data[i] > max)
{
max = data[i];
index = i;
}
}
printf("Max: %d, index: %zu", max, index); // Max: 34, index: 3
Имя массива, использованное без квадратных скобок, означает адрес нулевого элемента (константный указатель на нулевой элемент). Следующие вызовы приведут к одинаковому результату.
printf("0x%p\n", data);
printf("0x%p\n", &data[0]);
Теперь загадка: что будет выведено?
printf("%d", 7[data]);
А будет выведено 8. Запись data[7] на самом деле является синтаксическим сахаром для двух операций: *(data + 7). Она расшифровывается следующим образом: к адресу нулевого элемента прибавляется семь элементов — получается адрес седьмого элемента; разыменовываем, и получается сам седьмой элемент.
Запись 7[data] тогда означает *(7 + data). От перестановки мест слагаемых сумма не меняется.
Для этого будем передавать указатель на начало массива и его размер. (К слову, первый аргумент может выглядеть как int data[].)
void max(const int* data, size_t size) // Указатель на массив и его размер.
{
int max = INT_MIN;
size_t index = 0;
for (size_t i = 0; i < size; ++i)
{
if (data[i] > max)
{
max = data[i];
index = i;
}
}
printf("Max: %d, index: %zu", max, index); // Max: 34, index: 3
}
Вызов будет иметь следующий вид.
max(arr, size);
Мы не просто написали в аргументе int*, хотя и могли бы. А написали мы const int*, тем самым давая компилятору понять, что мы не собираемся менять элементы массива (это избавляет от случайных ошибок).
Поэкспериментируйте с разными типами аргументов.
Если размер массива на момент компиляции ещё не известен либо массив должен быть очень большим, то память можно выделить прямо во время выполнения программы.
size_t size = 10u;
int* data = malloc(size * sizeof(int)); // Количество запрашиваемых байтов.
if (data == NULL)
{
// Памяти недостаточно, принимаем меры.
}
// Работа как с обычным массивом.
free(data);
Примечание. В языке C++ неявного приведения типа void*, возвращаемого malloc(), к типу int* не произойдёт. Поэтому запись будет выглядеть как int* data = (int*)malloc(size * sizeof(int));.
Можно воспользоваться и функцией calloc(). Её первый аргумент — количество элементов, второй — их размер.
int* data = calloc(size, sizeof(int));
Несмотря на то, что работа с обоими видами массивов выглядит одинаково, различия всё же есть.
#define SIZE 10u
int data1[SIZE];
size_t size = 10u;
int* data2 = malloc(size * sizeof(int));
Если мы попытаемся узнать размер динамического массива, то у нас ничего не получится: data2 — это указатель, а его размер на моей машине равен 4 байтам.
printf("%zu %zu\n", sizeof(data1), sizeof(data2)); // 40 4
Как мы уже сказали, адрес «настоящего» массива, использованный без квадратных скобок, означает адрес нулевого элемента. И сколько мы не будем пытаться брать адрес, будет получаться то же самое. А вот в случае динамического массива «имя массива» всего лишь указатель, то есть адрес, а у него в свою очередь тоже есть адрес.
printf("%p %p\n", data1, &data1); // Одинаковые адреса.
printf("%p %p\n", data2, &data2); // Разные адреса.
Двумерные (трёхмерные и так далее) массивы определяются следующим образом.
#define ROWS 4u
#define COLUMNS 5u
int matrix[ROWS][COLUMNS] =
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
};
Мы уже говорили, что квадратные скобки в одномерном массиве (data[i]) лишь синтаксический сахар (*(data + i)). За двойными квадратными скобками (matrix[i][j]) скрывается то же самое: *(*(matrix + i) + j).
А вот способов организовать двумерный (или даже трёхмерный) динамический массив множество. У каждого свои достоинства и недостатки. Вот один из них (вытягивание массива в змейку).
size_t rows = 4u, columns = 5u;
int* matrix = malloc(rows * columns * sizeof(int));
if (matrix == NULL)
{
// Памяти недостаточно, принимаем меры.
}
printf("%d", matrix[3 * columns + 2]); // Обращение к элементу [3][2].
free(matrix);
Тема указателей была бы раскрыта не полностью без указателей на функции. Допустим, дана такая функция.
void foo(int);
Тогда указатель на такую функцию будет иметь следующий вид. Сперва указывается тип возвращаемого значения, затем в скобках после знака * произвольное имя указателя, дальше в отдельных скобках через запятую перечисляются типы аргументов. Это значит, что конкретно в этот указатель нельзя записать адрес функции, которая, скажем, не имеет аргументов.
int (*ptr)(int) = foo; // Можно писать &foo.
Прямой и косвенный вызов функции приводит к одинаковому результату.
foo(7); // Прямой вызов.
ptr(7); // Косвенный вызов.
В этот момент вы могли начать паниковать из-за увеличивающейся сложности объявлений — все эти указатели, константные указатели, указатели на функцию... Первое, что нужно найти, это имя (идентификатор). Далее будем двигаться вправо и влево, отталкиваясь от скобок и замечая, что * — это указатель, [] — массив, а () — функция.
int (*(*(*ptr)())[7])(); // ptr — это...
«ptr — это...» Однако дальше ), потому идём влево. Символ * означает указатель.
int (*(*(*ptr)())[7])(); // ptr — это указатель...
Наткнулись на (, движемся вправо. Скобки () означают функцию.
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию...
Дальше снова ), движемся влево. Там *.
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию, возвращающую
// указатель...
Двигаясь вправо, встречаем []. Это массив.
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию, возвращающую
// указатель на массив из 7...
И так далее.
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию, возвращающую
// указатель на массив из 7 указателей...
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию, возвращающую
// указатель на массив из 7 указателей на функции...
int (*(*(*ptr)())[7])(); // ptr — это указатель на функцию, возвращающую
// указатель на массив из 7 указателей на функции,
// возвращающие int.
Есть хороший сервис, с помощью которого можно переводить объявления языка C на английский, и наоборот.