В языке Си массивы и указатели тесно связаны. С помощью указателей мы также легко можем манипулировать элементами массива, как и с помощью индексов.
Имя массива без индексов в Си является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include <stdio.h> int main(void) { int array[] = {1, 2, 3, 4, 5}; printf("array[0] = %d", *array); // array[0] = 1 return 0; }
Прибавляя определенное число к имени массива, мы можем получить указатель на соответствующий элемент массива:
#include <stdio.h> int main(void) { int array[] = {1, 2, 3, 4, 5}; int second = *(array + 1); // получим второй элемент printf("array[1] = %d", second); // array[1] = 2 return 0; }
Более того, когда мы в обращаемся к определенному элементу массива, используя квадратные скобки, например:
array[2]
компилятор рассмотривает эту запись как прибавление индекса к указателю на начальный элемент:
array+2
Поэтому мы даже можем написать 2[array]
, что также будет валидным обращением к элементу массива:
#include <stdio.h> int main(void) { int array[] = {1, 2, 3, 4, 5}; int third = 2[array]; printf("array[2] = %d", third); // array[2] = 3 return 0; }
Соответственно мы можем пробежаться по всем элементом массива, прибавляя к адресу определенное число:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; for(int i = 0; i < 5; i++) { void* address = array + i; // получаем адрес i-го элемента массива int value = *(array + i); // получаем значение i-го элемента массива printf("array[%d]: address=%p \t value=%d \n", i, address, value); } return 0; }
То есть, например, адрес второго элемента будет представлять выражение a+1
, а его значение - *(a+1)
.
Со сложением и вычитанием здесь действуют те же правила, что и в операциях с указателями. Добавление единицы означает прибавление к адресу значения, которое равно размеру типа массива. Так, в данном случае массив представляет тип int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 =8. И так далее.
В итоге в моем случае я получу следующий результат работы программы:
array[0]: address=0060FE98 value=1 array[1]: address=0060FE9C value=2 array[2]: address=0060FEA0 value=3 array[3]: address=0060FEA4 value=4 array[4]: address=0060FEA8 value=5
В то же время имя массива это не стандартный указатель, мы не можем изменить его адрес, например, так:
int array[5] = {1, 2, 3, 4, 5}; array++; // так сделать нельзя int b = 8; array = &b; // так тоже сделать нельзя
Имя массива всегда хранит адрес самого первого элемента, соответственно его можно присвоить другому указателю и затем через указатель обращаться к элеиментам массива:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array printf("value: %d \n", *ptr); // 1 return 0; }
Прибавляя (или вычитая) определенное число от адреса указателя, можно переходить по элементам массива. Например, перейдем к третьему элементу:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array ptr = ptr + 2; // перемезаем указатель на 2 элемента вперед printf("value: %d \n", *ptr); // value: 3 return 0; }
Здесь указатель ptr
изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и
перейдем к элементу array[2]
.
И как и другие данные, можно по указателю изменить значение элемента массива:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array ptr = ptr + 2; // переходим к третьему элементу *ptr = 8; // меняем значение элемента, на который указывает указатель printf("array[2]: %d \n", array[2]); // array[2] : 8 return 0; }
Стоит отметить, что указатель также может использовать индексы, как и массивы:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array int value = ptr[2]; // используем индексы - получаем 3-й элемент (элемент с индексом 2) printf("value: %d \n", value); // value: 3 return 0; }
Ранее мы рассмотрели, что строка по сути является набором символов, окончанием которого служит нулевой символ '\0'. И фактически строку можно представить в виде массива:
char hello[] = "Hello METANIT.COM!";
Но в языке Си также для представления строк можно использовать указатели на тип char:
#include <stdio.h> int main(void) { char *hello = "Hello METANIT.COM!"; // указатель на char - фактически строка printf("%s", hello); return 0; }
В данном случае оба определения строки - с помощью массива и указателя будут в равнозначны.
С помощью указателей легко перебрать массив:
int array[5] = {1, 2, 3, 4, 5}; for(int *ptr=array; ptr<=&array[4]; ptr++) { printf("address=%p \t value=%d \n", (void*)ptr, *ptr); }
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента (ptr<=&array[4]
).
Аналогичным образом можно перебрать и многомерный массив:
#include <stdio.h> int main(void) { int array[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}}; int n = sizeof(array)/sizeof(array[0]); // число строк int m = sizeof(array[0])/sizeof(array[0][0]); // число столбцов int *final = array[0] + n * m - 1; // указатель на самый последний элемент for(int *ptr=array[0], i = 1; ptr <= final; ptr++, i++) { printf("%d \t", *ptr); // если остаток от целочисленного деления равен 0, // переходим на новую строку if(i%m==0) { printf("\n"); } } return 0; }
Так как в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение array[0]
. Соответственно указатель указывает на
этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в
указателе final.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика, пока оно не станет равно общему количеству элементов (m * n):
for(int *ptr = array[0], i = 0; i < m*n;) { printf("%d \t", *ptr++); if(++i%m==0) { printf("\n"); } }
Но в любом случае программа вывела бы следующий результат:
1 2 3 4 5 6 7 8 9 10 11 12
Стоит отметить, что в языке Си для работы со строками применяется такой механизм как string interning или интернирование строк. В этом случае строки в виде строковых литералов сохраняются в приложении в секции .rodata (read-only data), которые предназначены для данных только для чтения, а строковые литералы рассматриваются как неизменяемые данные. Например, возьмем следующую программу:
#include <stdio.h> char* str1 = "Hello"; char* str2 = "World"; int main(void) { printf("str1 = %p \n", &str1[0]); printf("str2 = %p \n", &str2[0]); return 0; }
Здесь определены две строки - str1 и str2, в функции main выводим адрес первого символа каждой из этих строк. Так, в моем случае я получу вывод:
str1 = 00007FF75E674000 str2 = 00007FF75E674006
По выводу видно, что первый символ второй строки расположен через 6 байтов начала первой строки. То есть строки расположены в памяти рядом друг с другом.
Но теперь сделаем строки одинаковыми:
#include <stdio.h> char* str1 = "Hello World"; char* str2 = "Hello World"; int main(void) { printf("str1 = %p \n", &str1[0]); printf("str2 = %p \n", &str2[0]); return 0; }
Теперь вывод будет другим:
str1 = 00007FF75F674000 str2 = 00007FF75F674000
Оба адреса одинаковые, потому что обе переменных в реальности указывают на одну и ту же строку. Интернирование строк позволяет избежать дублирования строк, более эффективно использовать память. Причем здесь не важно, что переменные являются глобальными. Они также могут быть локальными, но все равно будут указывать на один и тот же адрес в .rodata.
#include <stdio.h> int main(void) { char* str1 = "Hello World"; char* str2 = "Hello World"; printf("str1 = %p \n", &str1[0]); printf("str2 = %p \n", &str2[0]); return 0; }