Как обратиться к элементу массива через указатель
В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее, но разобраться в нем, особенно непосвященному, довольно трудно.
int a[10];
определяет массив a , состоящий из десяти последовательных объектов с именами a[0] , a[1] , . a[9] .
Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int , т.е. объявлен как
int *pa;
то в результате присваивания
pa будет указывать на нулевой элемент a , иначе говоря, pa будет содержать адрес элемента a[0] .
x = *pa;
будет копировать содержимое a[0] в x .
Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после pa , а pa-i — на i-й элемент перед pa . Таким образом, если pa указывает на a[0] , то
есть содержимое a[1] , pa+i — адрес a[i] , а *(pa+i) — содержимое a[i] .
Сделанные замечания верны независимо от типа и размера элементов массива a . Смысл слов «добавить 1 к указателю», как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, а pa+i — на i-й после pa .
Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания
pa и a имеют одно и то же значение. Поскольку имя массива является синонимом адреса его начального элемента, присваивание pa = &a[0] можно также записать в следующем виде:
pa = a;
Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i) . Вычисляя a[i] , Си сразу преобразует его в *(a+i) ; указанные две формы записи эквивалентны. Из этого следует,что записи &a[i] и a+i также будут эквивалентными, т.е. и в том и в другом случае это адрес i-го элемента массива a . С другой стороны, если pa — указатель, то его можно использовать с индексом, т.е. запись pa[i] эквивалентна записи *(pa+i) . Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.
Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель — это переменная, поэтому можно написать pa = a или pa++ . Но имя массива не является переменной, и записи вроде a = pa или a++ не допускаются.
Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Мы можем воспользоваться отмеченным фактом и написать еще одну версию функции strlen , вычисляющей длину строки.
/* strlen: возвращает длину строки */ int strlen(char *s)
Так как переменная s — указатель, к ней применима операция ++ ; s++ не оказывает никакого влияния на строку символов в функции, которая обратилась к strlen . Просто увеличивается на 1 некоторая копия указателя,находящаяся в личном пользовании функции strlen . Это значит,что все вызовы, такие как:
strlen("Здравствуй, мир"); /* строковая константа */ strlen(array); /* char array[100]; */ strlen(ptr); /* char *ptr; */
char s[];
char *s;
в определении функции эквивалентны. Мы отдаем предпочтение последнему варианту, поскольку он более явно сообщает,что s есть указатель. Если функции в качестве аргумента передается имя массива, то она может рассматривать его так, как ей удобно — либо как имя массива,либо как указатель, и поступать с ним соответственно. Она может даже использовать оба вида записи, если это покажется уместным и понятным.
Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a — массив, то в записях
функции f передается адрес подмассива, начинающегося с элемента a[2] . Внутри функции f описание параметров может выглядеть как
f(int arr[])
f(int *arr)
Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.
Если есть уверенность, что элементы массива существуют, то возможно индексирование и в «обратную» сторону по отношению к нулевому элементу; выражения p[-1] , p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0] . Разумеется, нельзя «выходить» за границы массива и тем самым обращаться к несуществующим объектам.
Массивы и указатели
П усть есть массив
int A[5] = ;
Мы уже показали, что указатели очень похожи на массивы. В частности, массив хранит адрес, откуда начинаются его элементы. Используя указатель можно также получить доступ до элементов массива
int *p = A;
тогда вызов A[3] эквивалентен вызову *(p + 3)
На самом деле оператор [ ] является синтаксическим сахаром – он выполняет точно такую же работу. То есть вызов A[3] также эквивалентен вызову *(A + 3)
#include #include void main() < int A[5] = ; int *p = A; printf("%d\n", A[3]); printf("%d\n", *(A + 3)); printf("%d\n", *(p + 3)); getch(); >
Тем не менее, важно понимать – указатели — это не массивы!
Отличие массива от указателя
Массив — непосредственно указывает на первый элемент, указатель – переменная, которая хранит адрес первого элемента.
Тогда почему возможна следующая ситуация?
#include #include void main() < int A[5] = ; int *p = A; printf("%d\n", *(A+1)); printf("%d\n", *(p+1)); getch(); >
Это правильный код, который будет работать. Дело в том, что компилятор подменяет массив на указатель. Данный пример работает, потому что мы действительно работаем с указателем (хотя помним, что массив отличается от указателя). То же самое происходит и при вызове функции. Если функция требует указатель, то можно передавать в качестве аргумента массив, так как он будет подменён указателем.
В си существует одна занимательная особенность. Если A[i] это всего лишь синтаксический сахар, и A[i] == *(A + i) , то от смены слагаемых местами ничего не должно поменяться, т. е. A[i] == *(A + i) == *(i + A) == i[A] . Как бы странно это ни звучало, но это действительно так. Следующий код вполне валиден:
int a[] = ; printf("%d\n", a[3]); printf("%d\n", 3[a]);
Многомерные массивы и указатели на многомерные массивы.
Т еперь рассмотрим такой пример
void main() < int A[][3] = ; int **p = A; >
Этот код не скомпилируется. Дело в том, что правило подмены массива на указатель на рекурсивное. Поэтому при определении многомерного массива нужно указывать размер явно, а пустыми оставлять можно только первые скобки. Этот пример можно переписать так
#include #include void main() < int A[][3] = ; int *p = A[0]; printf("%d\n", p[2]); getch(); >
Мы получили указатель на первую строку. Далее вывели третий элемент. Либо так — создать массив указателей на строки
#include #include void main() < int A[][3] = ; int (*p)[3] = A; printf("%d\n", p[0][2]); //Или так printf("%d\n", *(*( p + 0 ) + 2)); getch(); >
Только здесь уже p будет именем массива, каждый элемент которого является указателем. И точно так же, как мы обращались к элементам массива через массив указателей *p[3], через имя массива можно обратиться к элементу массива
printf("%d\n", *(*( A + 1 ) + 2));
Тоже самое правило действует и при вызове функций. Если функция требует указателя на указатель, то нельзя просто передать двумерный массив, потому что он не будет подменён указателем на указатель, а будет заменён массивом указателей.
Подмена имени массива на указатель связана с тем, что структура одномерного массива в памяти идентична структуре динамически созданного массива — это просто последовательность байт в памяти. Другое дело — двумерный массив.
Структура динамически созданного и статического одномерного массива идентичны
Статический двумерный массив представляет собой одномерный массив, в котором элементы расположены друг за другом по рядам. Динамически созданный двумерный массив — это массив указателей. Каждый элемент этого массива хранит адрес динамически созданного массива. Поэтому нельзя просто присвоить указателю на указатель имя статического двумерного массива — он не будет знать, как с ним работать.
Двумерный статический и двумерный динамический массив имеют разную структуру
Чтобы динамически созданный двумерный массив имел структуру статического двумерного массива, необходимо, чтобы он знал «число столбцов» двумерного массива, то есть длину одной строки. Для этого можно воспользоваться указателем на одномерный массив. Неудобство такого подхода в том, что необходимо заранее знать число элементов каждого подмассива. Однако, многомерный массив всегда можно создать из одномерного, тогда вообще никаких проблем не обнаружится.
Указатель на одномерные массивы будет вести себя как статический двумерный массив
ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students

Всё ещё не понятно? – пиши вопросы на ящик
Как обратиться к элементу массива через указатель
В языке Си массивы и указатели тесно связаны. С помощью указателей мы также легко можем манипулировать элементами массива, как и с помощью индексов.
Имя массива без индексов в Си является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include int main(void) < int array[] = ; printf("array[0] = %d", *array); // array[0] = 1 return 0; >
Прибавляя определенное число к имени массива, мы можем получить указатель на соответствующий элемент массива:
#include int main(void) < int array[] = ; int second = *(array + 1); // получим второй элемент printf("array[1] = %d", second); // array[1] = 2 return 0; >
Более того, когда мы в обращаемся к определенному элементу массива, используя квадратные скобки, например:
array[2]
компилятор рассмотривает эту запись как прибавление индекса к указателю на начальный элемент:
array+2
Поэтому мы даже можем написать 2[array] , что также будет валидным обращением к элементу массива:
#include int main(void) < int array[] = ; int third = 2[array]; printf("array[2] = %d", third); // array[2] = 3 return 0; >
Соответственно мы можем пробежаться по всем элементом массива, прибавляя к адресу определенное число:
#include int main(void) < int array[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] = ; array++; // так сделать нельзя int b = 8; array = &b; // так тоже сделать нельзя
Использование указателя для работы с массивом
Имя массива всегда хранит адрес самого первого элемента, соответственно его можно присвоить другому указателю и затем через указатель обращаться к элеиментам массива:
#include int main(void) < int array[5] = ; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array printf("value: %d \n", *ptr); // 1 return 0; >
Прибавляя (или вычитая) определенное число от адреса указателя, можно переходить по элементам массива. Например, перейдем к третьему элементу:
#include int main(void) < int array[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 int main(void) < int array[5] = ; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array ptr = ptr + 2; // переходим к третьему элементу *ptr = 8; // меняем значение элемента, на который указывает указатель printf("array[2]: %d \n", array[2]); // array[2] : 8 return 0; >
Стоит отметить, что указатель также может использовать индексы, как и массивы:
#include int main(void) < int array[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 int main(void) < char *hello = "Hello METANIT.COM!"; // указатель на char - фактически строка printf("%s", hello); return 0; >
В данном случае оба определения строки — с помощью массива и указателя будут в равнозначны.
Перебор массива с помощью указателей
С помощью указателей легко перебрать массив:
int array[5] = ; for(int *ptr=array; ptr
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента ( ptr
Аналогичным образом можно перебрать и многомерный массив:
#include int main(void) < int array[3][4] = < , , >; 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 > 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
Стоит отметить, что в языке Си для работы со строками применяется такой механизм как string interning или интернирование строк . В этом случае строки в виде строковых литералов сохраняются в приложении в секции .rodata (read-only data), которые предназначены для данных только для чтения, а строковые литералы рассматриваются как неизменяемые данные. Например, возьмем следующую программу:
#include char* str1 = «Hello»; char* str2 = «World»; int main(void)
Здесь определены две строки — str1 и str2, в функции main выводим адрес первого символа каждой из этих строк. Так, в моем случае я получу вывод:
str1 = 00007FF75E674000 str2 = 00007FF75E674006
По выводу видно, что первый символ второй строки расположен через 6 байтов начала первой строки. То есть строки расположены в памяти рядом друг с другом.
Но теперь сделаем строки одинаковыми:
#include char* str1 = «Hello World»; char* str2 = «Hello World»; int main(void)
Теперь вывод будет другим:
str1 = 00007FF75F674000 str2 = 00007FF75F674000
Оба адреса одинаковые, потому что обе переменных в реальности указывают на одну и ту же строку. Интернирование строк позволяет избежать дублирования строк, более эффективно использовать память. Причем здесь не важно, что переменные являются глобальными. Они также могут быть локальными, но все равно будут указывать на один и тот же адрес в .rodata .
#include int main(void)
Как обратиться к элементу массива через указатель
В C++ указатели и массивы тесно связаны. Обычно компилятор преобразует массив в указатели. С помощью указателей можно манипулировать элементами массива, как и с помощью индексов.
Имя массива по сути является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include int main() < int nums[] ; std::cout
Так, в моем случае я получу следующий консольный вывод:
nums[0] address: 0x1f1ebffe60 nums[0] value: 1
Прибавляя к адресу первого элемента некоторое число, мы можем получить определенный элемент массива.
#include int main() < int nums[] ; int num2 = *(nums + 1); // второй элемент int num3 = *(nums + 2); // третий элемент std::cout int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 = 8. И так далее.Например, в цикле пробежимся по всем элементам:
#include int main() < int nums[] ; for(unsigned i<>; i < std::size(nums); i++) < std::cout nums[0]: address=0xd95adffc30 value=1 nums[1]: address=0xd95adffc34 value=2 nums[2]: address=0xd95adffc38 value=3 nums[3]: address=0xd95adffc3c value=4 nums[4]: address=0xd95adffc40 value=5Но при этом имя массива это не стандартный указатель, и мы не можем изменить его адрес, например, так:
int nums[] ; nums++; // так сделать нельзя int b ; nums = &b; // так тоже сделать нельзяУказатели на массивы
Имя массива всегда хранит адрес самого первого элемента. И нередко для перемещения по элементам массива используются отдельные указатели:
int nums[] ; int *ptr ; int num3 = *(ptr+2); std::coutЗдесь указатель ptr изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и перейдем к элементу nums[2] .
Можно сразу присвоить указателю адрес конкретного элемента массива:
int nums[] ; int *ptr ; // адрес третьего элемента std::cout #include int main() < const int n = 5; int nums[n]; for(int *ptr; ptr <=&nums[n-1]; ptr++) < std::cout << "address=" << ptr << "\tvalue brush:cpp;">#include int main() < int nums[3][4] < , , >; unsigned int n < sizeof(nums)/sizeof(nums[0]) >; // число строк unsigned int m < sizeof(nums[0])/sizeof(nums[0][0]) >; // число столбцов int *end ; // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11 int *ptr ; // указатель на первый элемент for( unsigned i; ptr > >Поскольку в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение a[0] . Соответственно указатель указывает на этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в указателе end.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика:
#include int main() < const unsigned n ; // число строк const unsigned m ; // число столбцов int nums[n][m] < , , >; const unsigned count ; // общее количество элементов int *ptr; // указатель на первый элемент первого массива for(unsigned i; i > >Но в обоих случаях программа вывела бы следующий результат:
1 2 3 4 5 6 7 8 9 10 11 12Указатель на строки и массивы символов
Поскольку массив символов может интерпретироваться как строка, то указатель на значения типа char тоже может интерпретироваться как строка:
#include int main() < char hello[] ; char *phello ; std::coutПри выводе на консоль значения указателя фактически будет выводиться строка.
Также можно применять операцию разыменовывания для получения отдельных символов, например, выведем первый символ:
std::coutЕсли же необходимо вывести на консоль адрес указателя, то его надо преобразовать к типу void*:
std::coutВ остальном работа с указателем на массив символов производится также, как и с указателями на массивы других типов.
Также поскольку указатель типа char тоже может интерпретироваться как строка, то теоретически мы можем написать следующим образом:
char *phello ;Однако следует учитывать, что строковые литералы в С++ рассматриваются как константы. Поэтому предыдущее определение указателя может при компиляции вызвать как минимум предупреждение, а попытка изменить элементы строки через указатель - к ошибке компиляции. Поэтому при определении указателя на строку, следует определять указатель как указатель на константу:
#include int main() < const char *phello ; // указатель на константу std::coutМассивы указателей
Также можно определять массивы указателей. В некотором смысле массив указателей будет похож на массив, который содержит другие массивы. Однако массив указателей имеет преимущества.
Например, возьмем обычный двухмерный символьный массив - массив, который хранит строки:
#include int main() < char langs[][20] < "C++", "Python", "JavaScript">; std::coutДля определения двухмерного массива мы должны указать как минимум размер вложенных массивов, который будет достаточным, чтобы вместить каждую строку. В данном случае размер каждого вложенного массива - 20 символов. Однако зачем для первой строки - "C++", которая содержит 4 символа (включая концевой нулевой байт) выделять аж 20 байтов? Это - ограничение подобных массивов. Массивы указателей же позволяют обойти подобное ограничение:
#include int main() < const char *langs[] < "C++", "Python", "JavaScript">; // перебор массива for(unsigned i<>; i < std::size(langs); i++) < std::cout >В данном случае элементами массива langs являются указатели: 3 указателя, каждый из которых занимает 4 или 8 байт в зависимости от архитекутуры (размер адреса). Каждый из этих указателей указывает на адрес в памяти, где расположены соответствующие строки: "C++", "Python", "JavaScript". Однако каждая из этих строк будет занимать именно то пространство, которое ей непосредственно необходимо. То есть строка "С++" будет занимать 4 байта. С одной стороны, мы здесь сталкиваемся с дополнительными издержками: дополнительно выделяется память для хранения адресов в указателях. С другой стороны, когда строки в массиве сильно различаются по длине , то мы можем получить общий выигрыш в количестве потребляемой памяти.