Создание массива с помощью указателя си

Создание массива с помощью указателя си

Указатели и массивы

П усть есть массив
int A[5] = <1, 2, 3, 4, 5>;
Мы уже показали, что указатели очень похожи на массивы. В частности, массив хранит адрес, откуда начинаются его элементы. Используя указатель можно также получить доступ до элементов массива
int *p = A;
тогда вызов A[3] эквивалентен вызову *(p + 3)
На самом деле оператор [ ] является синтаксическим сахаром – он выполняет точно такую же работу. То есть вызов A[3] также эквивалентен вызову *(A + 3)

Тем не менее, важно понимать – указатели — это не массивы!

Это правильный код, который будет работать. Дело в том, что компилятор подменяет массив на указатель. Данный пример работает, потому что мы действительно работаем с указателем (хотя помним, что массив отличается от указателя). То же самое происходит и при вызове функции. Если функция требует указатель, то можно передавать в качестве аргумента массив, так как он будет подменён указателем.

В си существует одна занимательная особенность. Если A[i] это всего лишь синтаксический сахар, и A[i] == *(A + i) , то от смены слагаемых местами ничего не должно поменяться, т. е. A[i] == *(A + i) == *(i + A) == i[A] . Как бы странно это ни звучало, но это действительно так. Следующий код вполне валиден:

Многомерные массивы и указатели на многомерные массивы.

Т еперь рассмотрим такой пример

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

Мы получили указатель на первую строку. Далее вывели третий элемент. Либо так — создать массив указателей на строки

Только здесь уже p будет именем массива, каждый элемент которого является указателем. И точно так же, как мы обращались к элементам массива через массив указателей *p[3], через имя массива можно обратиться к элементу массива

Тоже самое правило действует и при вызове функций. Если функция требует указателя на указатель, то нельзя просто передать двумерный массив, потому что он не будет подменён указателем на указатель, а будет заменён массивом указателей.

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

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

Чтобы динамически созданный двумерный массив имел структуру статического двумерного массива, необходимо, чтобы он знал "число столбцов" двумерного массива, то есть длину одной строки. Для этого можно воспользоваться указателем на одномерный массив. Неудобство такого подхода в том, что необходимо заранее знать число элементов каждого подмассива. Однако, многомерный массив всегда можно создать из одномерного, тогда вообще никаких проблем не обнаружится.

Можно создать указатель на первый элемент массива, указав имя массива без индекса. Пусть имеется

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

Можно также получить адрес первого элемента массива с помощью оператора &. Например, sample и &sample[0] приводят к одинаковому результату. Тем не менее в профессиональных программах нет почти нигде &sample[0].

.collapse">Содержание

«Язык Си как острая бритва: с его помощью можно сделать изящное произведение искусства или кровавое месиво.»— Керниган

Небольшое отступление про типы и переменные в C

Переменная в Си представляет собой непрерывный кусок памяти (как и в ассемблере), размер которого зависит от типа переменной. Так же, от типа зависит то, как процессор будет работать с данными лежащими в этой памяти (например знаковая или беззнаковая арифметика будет применяться к этим данным). Но вне зависимости от типа, любая переменная Си в своей сути — всего лишь набор последовательных байт памяти.

Наиболее часто встречаются следующие размеры стандартных типов:

Как видим — бардак. Один и тот же код может скомпилироваться для разных архитектур (и на разных компиляторах) по-разному.
Поэтому, когда нам нужна именно 32 битная (или еще какая-нибудь конкретная) переменная…

Читайте также:  Приложение чтобы подключить телефон к телевизору

Указатели

Указатель это целочисленная беззнаковая переменная, предназначенная для хранения адреса объекта в памяти и обеспечении доступа к нему. Размер указателя обычно равен разрядности вычислительной машины. Для 32 битного указателя диапазон его возможных значений будет: <0x00000000… 0xFFFFFFFF>, для 64 битного <0x0000000000000000… 0xFFFFFFFFFFFFFFFF>соответственно.

Важно отметить нулевой указатель — NULL. При попытке доступа к нулевому адресу контроллер виртуальной памяти MMU сгенерирует аппаратное прерывание, которое будет перехвачено ядром, и провинившийся процесс будет жестоко убит на месте.

Объявление указателей

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

Чтение/запись значения указателей

Несмотря на то, что указатель по своей сути всего лишь uint32_t/uint64_t переменная, компилятор в целях нашей же безопасности, не позволит нам делать с ним все то что мы привыкли делать с обычными переменными. Так, например, попытка умножить указатель на что-либо вызовет ошибку компиляции. Так что же мы можем делать с указателями? В первую очередь, присваивать и читать их значения, и здесь все почти как с обычными переменными:

Если мы скомпилируем такой код, то скорее всего получим предупреждение компилятора, вроде такого:
Дело в том, что мы пытаемся присвоить указателю произвольное числовое значение, что вызывает у него вполне обоснованные подозрения в том что мы что-то перепутали.
Для того чтобы развеять его сомнения, нам следует явно указать, что наше 5 — это не просто 5, а адрес переменной типа int. Для этого нам нужно сделать явное приведение типа.

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

Теперь наш код абсолютно валиден как с точки зрения компилятора, так и с точки зрения того кому доведется его читать. Однако, если мы обратимся к памяти по адресу 5, MMU обнаружит попытку доступа к не принадлежащей нам памяти, и у нас возникнет runtime error. Поэтому лучше попросим стандартную функцию malloc() выделить нам кусочек памяти, достаточный для размещения в нем одной int32_t переменной:

Работа с указуемым объектом (Разименовывание указателя)

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

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

Давайте теперь запишем какое нибудь значение в выделенное нами место:

Важно понимать, что теперь 0x1234 попадет не в сам указатель, а в то место на которое он указывает. Если сейчас мы снова распечатаем значение указателя то заметим что сам он не изменился. Но изменилась память на которую он указывал. В этом легко убедиться, разименовав и распечатав указатель еще раз:

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

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

В си массив представляет собой кусок памяти размер которого равен произведению рамзера элемента на количество элементов в массиве. Так, массив:

Читайте также:  Как подключить аутлук к телефону

Будет занимать в памяти sizeof(unsigned int) * 2 = 4 * 2 = 8 байт, и после выполнения вышеприведенного кода будет выглядеть в памяти следующим образом:

Если вышеприведенная информация не вызывает у вас никаких затруднений в понимании (см. byte ordering), значит пришло время раскрыть одну тайну:

Когда мы обращаемся к n-ному элементу массива компьютер умножает индекс (n) на размер одного элемента массива — таким образом он получает смещение начала индексируемого элемента от начала массива. Затем компьютер складывает это смещение с указателем на массив и таким образом получает адрес индексируемого элемента в памяти. Далее этот адрес разименовывается и мы работаем уже с самим элементом!

Например, для доступа к m[1] в вышеприведенном примере, компьютер умножит sizeof(unsigned int) на 1 и получит смещение: 4*1 = 4. Далее он прибавит это смещение к базе: base+0004 и получит адрес 1-го элемента массива (не путать с нулевым элементом!). Это происходит всякий раз, когда вы обращаетесь к элементу массива.

Приведенный ниже код призван продемонстрировать то, что массивы в Си являются указателями, а операция индексирования [] в си определена для указателей вообще:

Как можно заметить, массивы в Си устроены очень просто, и в их основе лежат указатели.

Арифметика указателей

Как уже было ранее сказано, с указателями мы можем делать три вещи:

  • читать
  • писать
  • разименовывать

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

Теперь k указывает на m[1]! Это означает то, что если мы сделаем

То на экране появится 11111111, а не 00000000. Указатель сместился вперед на один элемент и первый элемент массива m остался позади, а второй стал для него первым. В прочем, мы все еще можем получить доступ к первому элементу массива m через указатель k:

Забавно, не правда ли? Ладно, поигрались и хватит 🙂 Давайте вернем наш указатель назад.

Теперь все вернулось на первоначальное место и k[0] снова равен 0x00000000.

А что если мы сейчас просто разименуем указатель k?

Если вы выполните этот пример, то увидите, что *k и k[0] это одно и то же!
(Индексация это вычисление смещения до элемента, сложение смещения с указателем и разименовывание. Смещение равно 0, остается лишь разименовывание!)

Важно помнить, что прибавляя (или отнимая) к указателю единицу мы фактически прибавляем к нему единицу умноженную на размер типа на который он указывает!

Если вам необходимо сместить какой либо указатель на n байт, например указатель на int на 5 байт, то вы столкнетесь с проблемой: прибавляя к такому указателю 1 вы фактически будете прибавлять к нему 4, прибавляя 2 — 8, ит.д. Для того чтобы это сделать необходимо временно преобразовать тип указателя к указателю на char, и только тогда прибавлять к нему 5.

Еще один пример: индексация массива без помощи квадратных скобок. К примеру, хотим распечатать значение i-того элемента массива k:

Здесь мы вручную складываем указатель со смещением и разименовываем получившийся указатель.

Далее нами будут рассмотрены указатели на структуры (struct) и функции, и здесь следует заметить, что прибавление (или вычитание) единицы к указателю на структуру прибавит к нему размер этой структуры. То есть, инкрементируя/декрементируя указатель на структуру мы перемещаемся вперед-назад по массиву структур. Аналогично ведут и себя указатели на функции.

Структуры Cи (struct)

Структуры Cи — аналог записей в паскале или объектов в ООП языках. Отличие от объектов заключается в том, что функции работающие со структурой описываются отдельно от нее. Структура в своей реализации очень похожа на массив. Разница лишь в том, что составные элементы структуры (поля или свойства) могут иметь разный размер, в то время как базовой идеей массива является то что все элементы одинаковы (а как иначе организовать простое индексирование массива?). Но доступ к полям структуры осуществляется не по индексу, а по имени. Опишем новую структуру в глобальной области, где нибудь перед функцией main():

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

Читайте также:  Страница 404 html код

Доступ к элементам структуры по указателю осуществляется с помощью оператора ->:

Теперь наши данные записаны в структуру, но как они в ней организованы? Предлагаю побайтово распечатать участок памяти, принадлежащий нашей структуре, заодно попрактикуемся в использовании указателей, так как они нам сейчас понадобятся.

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

Стоп, скажете вы. Почему между полями b и с возник разрыв в 2 байта? Ответ на этот вопрос состоит в том, что современным процессорам гораздо удобнее работать с т.н. выровненными данными (Расставим точки над структурами C/C++). И данный padding в 2 байта заложенный компилятором позволит вашему процессору гораздо быстрее работать с такой слегка раздутой структурой.

В то же время, если на первом месте стоит не скорость, а память, и вы имеете over9000 таких структур, то значительный кусок памяти будет пропадать вот на таких padding’ах. Для того чтобы заставить компилятор паковать все именно так как вы написали (а это порой бывает очень важно, например в драйверах аппаратных устройств), в GCC существует атрибут packed:

В других компиляторах данный атрибут выставляется с помощью директивы компилятора #pragma pack.

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

Указатели на функции

Указатели на функции декларируются несколько замысловато:

Выше приведен указатель на функцию принимающую 2 int аргумента и возвращающую int.

Указатель на функцию возвращающую int и принимающую (функцию принимающую int и возвращающую int) и int.

Вот такой он простой, наш Cи!

А вот вам массив из ста указателей на функцию:

Спросите и кому такое может понадобиться? — На самом деле, очень полезная штука. Лично я применял такой массив при создании виртуальной машины. Работает куда быстрей, чем switch диспетчер. Думаю, и вам пригодится.

Инициализировать указатель на функцию очень просто, достаточно просто присвоить ему желаемую функцию, например, так:

Указатели на указатели

Указатели могут указывать на что угодно, что есть в памяти. Раз они сами находятся в ней, логично что и на себя. Зачем? Все очень просто — вот устройство двумерного массива:

То есть — двумерный массив в Си это массив указателей на массивы данных. Указатель указывает на массив указателей, каждый из которых указывает на свою колонку двумерной матрицы. Когда просто работаешь с массивом об этом лучше не думать, чтобы не впасть в долгие и непродуктивные размышления о природе бытия :D.

Указатели на указатели настолько распространены, что встречаются даже в обычной unix main():

Здесь argv — указатель на массив указателей на строки аргументов командной строки.

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

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

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

Операция взятия адреса

Создавать новые объекты это хорошо, но что, если мы хотим получить адрес уже имеющегося объекта, например, параметра функции, локальной или глобальной переменной? Это возможно, и си предоставляет нам для этих целей оператор & (амперсанд) позволяющий получить адрес чего угодно, если это имеет адрес. Нельзя например взять адрес литеральной константы &5 и нельзя взять адрес результата выражения &(2+2) так как константа нигде не хранится; а результат выражения находится в регистре процессора, у которого как известно нет адреса, ит.п. Далее я приведу сжатый пример того как можно брать адрес различных объектов.

Void — указатели

Язык Си не позволяет пользователю создавать void переменные (зачем?), зато позволяет создавать указатель на void. За таким указателем не закреплен конкретный тип, но каждый раз перед разименовыванием такого указателя вам придется приводить его к желаемому типу вручную. А как иначе компилятор узнает что вы хотите разименовать и как это сделать?

Функция malloc() определена как void *malloc(int size); что позволяет присваивать результат ее работы любому типу указателя без каких бы то ни было предупреждений со стороны компилятора.

Ссылка на основную публикацию
Снять пароль с роутера tp link
Домашняя беспроводная сеть Wi-Fi должна быть защищена паролем. Но ведь бывают разные случаи, скажете вы. Например, вы хотите пригласить друзей...
Скопировать контакты с андроид на компьютер
Мы уже рассказывали о том, как скопировать контакты со смартфона на смартфон. Но иногда проще перебросить контактную книгу на компьютер....
Скопировать строку таблицы значений 1с в другую
Не претендуя на полноту описания функций и методов работы с таблицей значений 1с привожу некоторые аспекты, которые в своё время...
Снять пароль с макроса excel
Здравствуйте, друзья! Последние дни бился над такой задачей: Имеется файл .xls, в нем макрос на VBA, защищенный паролем. Файл создается...
Adblock detector