Перейти к содержимому

Int arr c что это

  • автор:

Массивы и функции в языке Си. Передача указателя на массив

Массивы, также как остальные переменные, можно передавать в функции в качестве аргументов. Рассмотрим такую программу:

#include #include #include #define N 10 void rand_fill(int arr[], int min, int max); int main() { int numbers[N]; rand_fill(numbers, 30, 90); for (int i = 0; i  N; i++) printf("%d ", numbers[i]); printf("\n"); } void rand_fill(int arr[], int min, int max) { srand(time(NULL)); for (int i = 0; i  N; i++) arr[i] = rand() % (max - min + 1) + min; }

В теле функции main объявляется массив, состоящий из 10 элементов. Далее вызывается функция rand_fill() , которой передаются в качестве аргументов имя массива и два целых числа.

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

На прошлом уроке мы выяснили, что имя массива — это константный указатель на первый элемент массива; т.е. имя массива содержит адрес. Выходит, что мы передаем в функцию копию адреса, а не копию значения. Как мы уже знаем, передача адреса приводит к возможности изменения локальных переменных в вызывающей функции из вызываемой. Ведь на одну и ту же ячейку памяти могут ссылаться множество переменных, и изменение значения в этой ячейке с помощью одной переменной неминуемо отражается на значениях других переменных.

Описание вида arr[] в параметрах функций говорит о том, что в качестве значения мы получаем указатель на массив, а не обычную (скалярную) переменную типа int , char , float и т.п.

Если в функцию передается только адрес массива, то в теле функции никакого массива не существует, и когда там выполняется выражение типа arr[i] , то на самом деле arr ‒ это не имя массива, а переменная-указатель, к которой прибавляется смещение. Поэтому цикл в функции rand_fill можно переписать на такой:

for (int i = 0; i  N; i++) *arr++ = rand() % (max - min + 1) + min;

В теле цикла результат выражения справа от знака присваивания записывается по адресу, на который указывает arr . За это отвечает выражение *arr . Затем указатель arr начинает указывать на следующую ячейку памяти, т.к. к нему прибавляется единица ( arr++ ). Еще раз: сначала выполняется выражение записи значения по адресу, который содержится в arr ; после чего изменяется адрес, содержащийся в указателе (сдвигается на одну ячейку памяти определенного размера).

Поскольку мы можем изменять arr , это доказывает, что arr — обычный указатель, а не имя массива. Тогда зачем в заголовке функции такой гламур, как arr[] ? Действительно, чаще используют просто переменную-указатель:

void rand_fill(int *arr, int min, int max);

Хотя в таком случае становится не очевидно, что принимает функция ‒ указатель на обычную переменную или все-таки на массив. В любом случае она будет работать.

Часто при передаче в функцию массива туда же через отдельный аргумент передают и количество его элементов. В примере выше N является глобальной константой, поэтому ее значение доступно как из функции main , так и rand_fill . Иначе, можно было бы определить rand_fill так:

void rand_fill(int *arr, int n, int min, int max) { srand(time(NULL)); for (int i = 0; i  n; i++) *arr++ = rand() % (max - min + 1) + min; }

В данном случае параметр n — это количество обрабатываемых элементов массива.

Следует еще раз обратить внимание на то, что при передаче имени массива в функцию, последняя может его изменять. Однако такой эффект не всегда является желательным. Конечно, можно просто не менять значения элементов массива внутри функции, как в данном примере, где вычисляется сумма элементов массива; при этом сами элементы никак не изменяются:

int arr_sum(int *arr) { int s = 0, i; for(i = 0; i  N; i++) { s = s + arr[i]; } return s; }

Но если вы хотите написать более надежную программу, в которой большинство функций не должны менять значения элементов массивов, то лучше в заголовках этих функций объявлять параметр-указатель как константу, например:

int arr_sum(const int *arr);

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

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

#include #include #include #define N 10 void rand_fill(int *arr, int min, int max); void arr_inc_dec(int arr[], char sign); void arr_print(int *arr); int main() { int numbers[N], i, minimum, maximum; char ch; printf("Enter minimum & maximum: "); scanf("%d %d", &minimum, &maximum); rand_fill(numbers, minimum, maximum); arr_print(numbers); scanf("%*c"); // избавляемся от \n printf("Enter sign (+, -): "); scanf("%c", &ch); arr_inc_dec(numbers, ch); arr_print(numbers); } void rand_fill(int *arr, int min, int max) { srand(time(NULL)); for (int i = 0; i  N; i++) *arr++ = rand() % (max - min + 1) + min; } void arr_inc_dec(int *arr, char sign) { for (int i = 0; i  N; i++) { if (sign == '+') arr[i]++; else if (sign == '-') arr[i]--; } } void arr_print(int *arr) { printf("The array is: "); for (int i = 0; i  N; i++) printf("%d ", *arr++); printf("\n"); }

Теперь у пользователя запрашивается минимум и максимум, затем создается массив из элементов, значения которых лежат в указанном диапазоне. Массив выводится на экран с помощью функции arr_print() . Далее у пользователя запрашивается знак + или -. Вызывается функция arr_inc_dec() , которая в зависимости от введенного знака увеличивает или уменьшает на единицу значения элементов массива.

В функциях rand_fill и arr_print используется нотация указателей. Причем в теле функций значения указателей меняются: они указывают сначала на первый элемент массива, затем на второй и т.д. В функции arr_inc_dec используется вид обращения к элементам массива. При этом значение указателя не меняется: к arr прибавляется смещение, которое увеличивается на каждой итерации цикла. Ведь на самом деле запись arr[i] означает *(arr+i) .

При использовании нотации обращения к элементам массива программы получаются более ясные, а при использовании записи с помощью указателей они компилируются чуть быстрее. Это связано с тем, что когда компилятор встречает выражение типа arr[i] , он тратит время на преобразование его к виду *(arr+i) .

Напишите программу, в которой из функции main в другую функцию передаются два массива: «заполненный» и «пустой». В теле этой функции элементам «пустого» массива должны присваиваться значения, так или иначе преобразованные из значений элементов «заполненного» массива, который не должен изменяться.

Курс с решением задач:
pdf-версия

C — почему &arr не равно arr и как мы получаем размер?

Помогите понять, каким образом у нас arr — это массив(указатель на первый элемент массива). А вот &arr — это указатель на весь целочисленный массив из 6 элементов. Что есть тогда указатель &arr на весь массив при условии, что в size мы получаем размер массива. Как указатель &arr может указывать на весь массив? Пример нашел тут, но понять как тут работает адресная арифметика пока не могу.

Отслеживать
2,279 1 1 золотой знак 7 7 серебряных знаков 27 27 бронзовых знаков
задан 1 апр 2022 в 3:54
771 5 5 серебряных знаков 24 24 бронзовых знака

2 ответа 2

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

arr имеет тип int [6] — массив из 6 элементов int , это не указатель на первый элемент. Соответственно &arr имеет тип int ( * ) [6] — указатель на массив из 6 элементов int (тот самый «указатель на весь целочисленный массив»). В выражении *(&arr + 1) — arr сначала при сложении происходит адресная арифметика с указателем на массив из 6 элементов int , при этом указатель рассматривается как указывающий на первый элемент массива, элементы которого имеют тип int [6] , далее результат (&arr + 1) разыменовывается и получается int ( & ) [6] — ссылка на массив из 6 элементов int и наконец при вычитании происходит адресная арифметика с двумя массивами из 6 элементов — для этого создаются два временных объекта — указатели на первые элементы этих массивов.

Проверить, что есть что, можно используя std::is_same :

#include int main() < int arr []; static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); return 0; > 

Отслеживать
ответ дан 1 апр 2022 в 6:24
user7860670 user7860670
29.7k 3 3 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков
Проверка азов языка (имею ввиду std::is_same_v) лишнее.
1 апр 2022 в 8:42

int arr[] = ; int size = *(&arr + 1) - arr; 
  • Массив arr имеет тип int [6] , т.е. массив из шести элементов типа int .
  • Выражение &arr имеет тип int (*)[6] , т.е. указатель на (массив из шести элементов типа int ).
  • В выражении &arr + 1 к указателю применяется арифметика указателей. Значение указателя наращивается на sizeof(int[6]) байт, что равно sizeof(int) * 6 байт. Получившийся указатель указывает на гипотетический массив из шести элементов типа int , следующий непосредственно за массивом arr .
  • В выражении *(&arr + 1) происходит разыменование указателя. Т.е. мы получаем lvalue «ссылающееся» на гипотетический массив после массива arr UB-1) . Тип этого выражения — int[6] 3) .
  • В выражении *(&arr + 1) — arr оба операнда бинарного оператора — имеют тип массива — int [6] . И к обоим операндам применяется неявное преобразование из типа массива к типу указателя на первый элемент массива.
    Т.е гипотетический массив *(&arr + 1) после массива arr типа int [6] преобразуется к указателю типа int* на свой первый элемент.
    Аналогично массив arr преобразуется к указателю на свой первый элемент.
  • Разность двух указателей типа int* даёт знаковое целочисленное значение типа std::ptrdiff_t, равное количеству объектов типа int между двумя указателями UB-2) .
  1. Разыменование указателя на гипотетический элемент после массива вызывает неопределённое поведение, так как такой указатель не является указателем на объект.
  • a pointer to an object or function (the pointer is said to point to the object or function), or
  • a pointer past the end of an object ([expr.add]), or
  • the null pointer value for that type, or
  • an invalid pointer value.

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.

constexpr static int arr[] = ; constexpr std::ptrdiff_t diff = *(&arr + 1) - arr; 

может вызвать следующую ошибку компиляции:

//cannot access array element of pointer past the end of object constexpr std::ptrdiff_t diff = *(&arr + 1) - arr; ^ 

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

constexpr static int arr[2][2] = , >; constexpr int val = arr[0][2]; 

может вызвать следующую ошибку компиляции:

//read of dereferenced one-past-the-end pointer is not allowed in a constant expression constexpr int val = arr[0][2]; ^ 
  1. Разность двух указателей, указывающих на элементы разных массивов, вызывает неопределённое поведение.
  • If P and Q both evaluate to null pointer values, the result is 0 .
  • Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x , the expression P — Q has the value i − j .
  • Otherwise, the behavior is undefined.
    [Note 1: If the value i − j is not in the range of representable values of type std​::​ptrdiff_­t , the behavior is undefined. — end note]
constexpr static int arr[2][2] = , >; constexpr const int* p00 = &arr[0][0]; constexpr const int* p10 = &arr[1][0]; constexpr std::ptrdiff_t diff = p10 - p00; 
//subtracted pointers are not elements of the same array constexpr std::ptrdiff_t diff = p10 - p00; ^ 
  1. Результат разыменования указателя на тип T — это lvalue типа T (не ссылка).

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points. If the type of the expression is “pointer to T ”, the type of the result is “ T ”.

Выражение наподобие std::is_same_v < int ( & ) [6], decltype(*(&arr + 1)) >возвращает true в силу особенностей работы спецификатора decltype , который для lvalue типа T в качестве типа выводит T& .

For an expression E , the type denoted by decltype(E) is defined as follows:
.
otherwise, if E is an lvalue, decltype(E) is T& , where T is the type of E ;
.

Итого: приведённый в вопросе код содержит неопределённое поведение. Результат его выполнения (если вообще скомпилируется) не предсказуем.

Различие между arr и &arr – как в C определить размер массива без sizeof

Hey folks, Long time no C.
Обычно в C мы находим длину массива arr так:

int n = sizeof ( arr ) / sizeof ( arr [ 0 ] ) ;

Здесь мы получаем размер массива в байтах; затем происходит деление этого размера на размер каждого элемента в массиве. Давайте попробуем избавиться от sizeof .

Никто из вас никогда не задавался вопросом насчёт разницы между arr и &arr? Это не одно и то же.

1 из 3 Давайте проверим это выведением адресов этих двух указателей

и стандартный выводКак можно увидеть в консоли, оба arr и &arr указывают точно на одно место в памяти (адрес 0x244fdc4 ).

1 из 2 Теперь попробуем инкрементировать оба указателя на 1 и снова проверить их адреса в памяти

и стандартный вывод
Мы можем определиться в следующем:

(arr + 1) указывает на 0x244fdc8 – это значение уходит на 4 байта вперёд от arr , который указывает на 0x244fdc4 .
Так как переменные типа int занимают 4 байта, (arr + 1) указывает на второй элемент массива .

(&arr + 1) указывает на 0x244fdd8 – это значение уходит на 20 байт вперёд от arr , который указывает на 0x244fdc4 .
(0x244fdd8 — 0x244fdc4 = 0x14 = 20)

Теперь можно сделать вывод о том, что не смотря на то, что arr и &arr указывают на одно и то же место в памяти, их применение совершенно разное.

arr имеет тип int * , в то время как &arr имеет тип int (*)[ size ] .

&arr указывает на весь массив, в то время как arr указывает на первый элемент массива.

Из этого можно извлечь один полезный опыт – получение длины массива.

*(&arr + 1) даёт нам адрес за последним элементом массива, а arr адрес первого элемента массива.
Таким образом, вычитание второго из первого может дать нам длину массива.

Мы можем это упростить, используя индексы массива (так как x[1] == *(x + 1) ):

PS :
Этот метод работает только для массивов, но не для указателей (вроде char *str).

Массивы и указатели в языке C

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

0x7ffe7d9ffe10 0x7ffe7d9ffe14 0x7ffe7d9ffe18 0x7ffe7d9ffe1c 0x7ffe7d9ffe20

Обратите внимание на то, что значение адреса каждого последующего элемента массива больше значения адреса предыдущего элемента на 4 единицы. В вашей системе эта разница может составлять 2 единицы. Такой результат вполне объясним, если вспомнить, сколько байтов отводится на одно данное типа int , и что элементы массива сохраняются в памяти друг за другом.

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

int *p; p = &arr[0];

Цикл for изменим таким образом:

for (i = 0; i  N; i++) printf("%p\n", p + i);

Здесь к значению p , которое является адресом ячейки памяти, прибавляется сначала 0, затем 1, 2, 3 и 4. Можно было бы предположить, что прибавление к p единицы в результате дает адрес следующего байта за тем, на который указывает p . А прибавление двойки вернет адрес байта, через один от исходного. Однако подобное предположение не верно.

Вспомним, что тип указателя сообщает, на сколько байт простирается значение по адресу, на который он указывает. Таким образом, хотя p указывает только на один байт (первый), но «знает», что его «собственность» простирается на все четыре. Когда мы прибавляем к указателю единицу, то получаем указатель на следующее значение, но никак не на следующий байт. А следующее значение начинается только через 4 байта. Поэтому результат выполнения приведенного цикла с указателем правильно отобразит адреса элементов массива.

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

Напишите программу, в которой объявлен массив вещественных чисел из десяти элементов. Присвойте указателю адрес четвертого элемента, затем, используя цикл, выведите на экран адреса 4, 5 и 6-ого элементов массива.

Имя массива — это указатель на адрес его первого элемента

Да, это именно так, данный факт следует принять как аксиому. Вы можете убедиться в этом выполнив такое выражение:

printf("%p = %p\n", arr, &arr[0]);

Отсюда следует, что имя массива – это ничто иное, как указатель. (Хотя это немного особенный указатель, о чем будет упомянуто ниже.) Поэтому выражения p = &arr[0] и p = arr дают одинаковый результат: присваивают указателю p адрес первого элемента массива.

Раз имя массива — это указатель, ничего не мешает получать адреса элементов вот так:

for (int i = 0; i  N; i++) printf("%p\n", arr + i);

Соответственно значения элементов массива можно получить так:

for (int i = 0; i  N; i++) printf("%.1f ", *(arr + i));

Примечание. Если массив был объявлен как автоматическая переменная (т.е. не глобальная и не статическая) и при этом не был инициализирован (не присваивались значения), то в нем будет содержаться «мусор» (случайные числа).

Получается, что запись вида arr[3] является сокращенным (более удобным) вариантом выражения *(arr+3) .

Взаимозаменяемость имени массива и указателя

Если имя массива является указателем, то почему бы не использовать обычный указатель в нотации обращения к элементам массива также, как при обращении через имя массива:

#include #define N 5 int main() { int arr[N] = {4, 0, 3, 10, 9}; int *p; p = arr; for (int i = 0; i  N; i++) printf("%d\n", p[i]); }

Отсюда следуют выводы. Если arr — массив, а p — указатель на его первый элемент, то пары следующих выражений дают один и тот же результат:

  • arr[i] и p[i] ;
  • &arr[i] и &p[i] ;
  • arr + i и p + i ;
  • *(arr + i) и *(p + i) .

Указателю можно присвоить адрес любого из элементов массива. Например, так p = &arr[2] или так p = arr + 2 . В таком случае результат приведенных выше пар выражений совпадать не будет. Например, когда будет выполняться выражение arr[i] , то будет возвращаться i -ный элемент массива. А вот выражение p[i] уже вернет не i -ный элемент от начала массива, а i -ный элемент от того, адрес которого был присвоен p . Например, если p был присвоен адрес третьего элемента массива ( p = arr + 2 ), то выражение arr[1] вернет значение второго элемента массива, а p[1] ‒ четвертого.

Имя массива — это указатель-константа

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

Это значит, что выражение p = arr допустимо, а arr = p — нет. Имя массива является константой. При этом не надо путать имя массива (адрес) и значения элементов массива. Последние константами не являются. Действительно, ведь для всех переменных мы не можем менять их адрес в процессе выполнения программы, можем менять лишь их значения. В этом смысле имя массива — это обычная переменная, хотя и содержащая адрес.

Как следствие в программном коде выражения присваивания, инкрементирования и декрементирования допустимы для указателей, а для имени массива — запрещены.

#include int main() { char str[20], *ps = str, n = 0; printf("Enter word: "); scanf("%s", str); while(*ps++ != '\0') n++; printf("%d\n", n); }

Она подсчитывает количество букв в полученном слове. Указатель ps получает ссылку на первый элемент массива, то есть первую букву. В условии цикла while сначала извлекается символ по адресу указателя и только потом значение указателя инкрементируется. Извлеченный символ проверяется на неравенство символу конца строки. И только в этом случае счетчик букв увеличивается.

Курс с решением задач:
pdf-версия

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

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