Как сделать указатель на массив c
Перейти к содержимому

Как сделать указатель на массив c

  • автор:

Указатели

Все определенные в программе данные, например, переменные, хранятся в памяти по определенному адресу. И указатели позволяют напрямую обращаться к этим адресам и благодаря этому манипулировать данными. Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Указатели — это неотъемлемый компонент для управления памятью в языке Си.

Определение указателя

Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *.

тип_данных* название_указателя;

Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.

Например, определим указатель на объект типа int:

int *p;

Пока указатель не ссылается ни на какой объект. Теперь присвоим ему адрес переменной:

int main(void) < int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной return 0; >

Получение адреса данных

Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция & . Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.

Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.

Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p :

#include int main(void) < int x = 10; int *p; p = &x; printf("%p \n", p); // 0060FEA8 return 0; >

В моем случае машинный адрес переменной x — 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.

То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.

Указатели в языке Си

Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.

Указатели в Си

И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.

Стоит отметить, что при выводе адреса указателя функция printf() ожидает, что указатель будет представлять void* , то есть указатель на значение типа void . Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то переданный указатель нужно преобразовать в указатель типа void * :

printf("%p \n", (void *)p);

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется операция * или операция разыменования (dereference operator). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:

#include int main(void)

Address = 0060FEA8 x = 10

Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:

int x = 10; int *p = &x; int y = *p; // присваиваем переменной y значение по адресу из указателя p printf("x = %d \n", y); // 10

Здесь присваиваем переменной y значение по адресу из указателя p , то есть значение переменной x .

И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:

int x = 10; int *p = &x; *p = 45; printf("x = %d \n", x); // 45

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

Создадим еще несколько указателей:

#include int main(void) < char c = 'N'; int d = 10; short s = 2; char *pc = &c; // получаем адрес переменной с типа char int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short printf("Variable c: address=%p \t value=%c \n", (void*) pc, *pc); printf("Variable d: address=%p \t value=%d \n", (void*) pd, *pd); printf("Variable s: address=%p \t value=%hd \n", (void*) ps, *ps); return 0; >

В моем случае я получу следующий консольный вывод:

Variable c: address=0060FEA3 value=N Variable d: address=0060FE9C value=10 Variable s: address=0060FE9A value=2

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

Как сделать указатель на массив c

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

Имя массива без индексов в Си является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:

#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(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++

И имеется следующая конструкция, указывающая на массив из 20 элементов типа short:

short (*pas)[20] = &tell 

Получившимся типом данных переменной pas является тип: short (*)[20] . Собственно вопрос, как создать переменную данного типа, и выделить ей память при помощи операции new?

Пример взят из книги: Стивен Прата — Язык программирования C++ (6 издание). Стр. 182.

Отслеживать
81.6k 9 9 золотых знаков 78 78 серебряных знаков 136 136 бронзовых знаков
задан 26 окт 2016 в 15:46
133 1 1 золотой знак 2 2 серебряных знака 9 9 бронзовых знаков

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Если у вас есть объявление массива вида

T a[N]; 

где T — это некоторый тип, а N — это число элементов в массиве, то указатель на первый элемент массива будет иметь тип T * . Например

T *p = a; 

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

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

T *p = new T[N]; 

Здесь элемент массива имеет тип T , а p как и выше показывает на первый элемент динамически выделенного безыменного массива..

Теперь представим, что T это алиас для типа short[20] , например

typedef short T[20]; 

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

T *p = new T[1]; 

Если снова вернуться к исходному типу short[20] , то получим

short( *p )[20] = &a; 
short( *p )[20] = new short[1][20]; 

Последнее предложение означает, что выделяется массив из одного элемента (вы можете выделять массив из произвольного числа элементов в соответствии с вашей задачей), элементами которого в свою очередь являются массивы из 20 элементов типа short .

Имейте в виду, что когда используется так называемая арифметика указателей, то значение указателя меняется на значение кратное sizeof( T )

Поэтому если вы, например, объявите указатель как

short( *p )[20] = &a; 

где T эквивалентно short[20] , то после применения, например, инкремента к этому указателю

указатель будет содержать адрес сразу же после последнего элемента массива a .

Ну, и напоследок пример работы с таким указателем.

#include #include #include #include #include const size_t N = 20; short ( * create_2D_array( size_t n ) )[N] < short ( *p )[N] = new short[n][N]; return p; >int main() < short a[N]; std::iota( std::begin( a ), std::end( a ), 1 ); for ( short x : a ) std::cout ( *p + N ), std::reverse_iterator( *p ), 1 ); for ( short x : *p ) std::cout

Вывод программы на консоль

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *