Вы находитесь на странице: 1из 28

Федеральное агентство связи

Ордена Трудового Красного Знамени

Федеральное государственное бюджетное образовательное учреждение


высшего образования

«Московский технический университет связи и информатики»

Кафедра «Математической кибернетики и информационных технологий»

Отчет по лабораторной работе

по дисциплине «Низкоуровневое программирование»

на тему:

«Основы работы с указателями в языке программирования С»

Выполнил:
Студент группы БВТ1801
Тимофеев И.И.
Проверил:
Фатхуллин Т.Д.

Москва 2021
Цель работы: Изучить и практически освоить основы работы с
указателями в языке программирования C.

2
Задание

 Создайте проект для ознакомительного использования указателей.


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

3
Краткая теория
ПЕРЕМЕННЫЕ И ИХ ПРЕДСТАВЛЕНИЕ В ПАМЯТИ
Переменная — это поименованная область памяти под определенный
тип данных. Как известно, каждая переменная хранится в памяти компьютера
и имеет свой адрес, по которому она записана. Память компьютера можно
представить в виде таблицы с ячейками (рисунок 1) по одному байту, каждая
из которых имеет свой адрес, который записывается цифрами
шестнадцатеричной системы.

Рисунок 1 – Пример представления памяти компьютера


Для того чтобы получить адрес переменной необходимо перед её
именем написать символ “&”. Кроме того, каждая переменная в зависимости
от её типа, занимает в памяти различное количество байт. Для того, чтобы
узнать размеры различных типов обычно используется функция sizeof().
В качестве примера, напишем программу, которая бы получала адрес
переменных a,b и количество байт, отведенных для каждой из этих
переменных (рисунок 2).

Рисунок 2 – Получение адреса переменных в памяти


4
На нашем компьютере мы получили два адреса 0x12ff60 и 0x12ff54.
Так как это переменные целого типа (int) они занимают в памяти компьютера
4 байта. Как видно из рисунка, переменные указанного типа располагаются в
памяти компьютера не подряд, а произвольным образом. Это выглядит
примерно так (рисунок 3):

Рисунок 3 – Пример расположения переменных в памяти


Для хранения адресов переменных и возможности получения данных
непосредственно по адресу в памяти существуют специальные переменные,
которые называются указателями.
Указатель – переменная, предназначенная для хранения адреса
какоголибо объекта (переменной, массива и тд.) в памяти компьютера.
Согласно определению, указатель это переменная, следовательно, как и
любая переменная, он должен быть как-то объявлен. Делается это в языке С
следующим образом:
- сначала указывается Базовый тип переменной, которая будет
храниться в памяти, по адресу указателя.
-далее следует специальный символ «*», называемый оператором
косвенной адресации, который сообщает компилятору, что мы собираемся
объявить указатель на переменную определенного типа (например, int).
После звездочки пишется идентификатор (имя) указателя.
Базовый тип указателя (т.е. l-value выражения объявления указателя)
может принимать любой тип данных (как простой, определенный стандартом
языка, так и составной, определенный стандартом языка или пользователем).
После объявления локального указателя до первого присвоения он
содержит неопределенное значение. Указатель, не ссылающийся в текущий
момент времени на конкретный объект, должен содержать нулевое значение.
Нуль (NULL или 0) используется, потому что С, в соответствии со
стандартом, гарантирует отсутствие чего-либо по нулевому адресу.
Следовательно, если указатель равен нулю, то это значит, во-первых, что он
ни на что не ссылается, а во-вторых – что его сейчас нельзя использовать.

5
Выполним листинг (рисунок 4):

Рисунок 4 – Пример обнуления указателя


ОПЕРАЦИИ НАД УКАЗАТЕЛЯМИ
В языке С определены две операции для работы с указателями:
операция разыменования (*) и операция взятия адреса (&). Оператор & - это
унарный оператор, возвращающий адрес своего операнда (r-value). Оператор
* - это унарный оператор, возвращающий значение переменной,
расположенной по указанному адресу. Для лучшего понимания выполним
следующий листинг (рисунок 5):

6
Рисунок 5 – Операции для работы с указателями
В рамках данной программы мы выполнили операции взятия адреса,
с последующим присвоением его указателю и операцию разыменования т.е.
получения данных по адресу, на который «указывает указатель». Результат
выполнения листинга представлен ниже на рисунке 6:

Рисунок 6 – Пример использования операций взятия адреса и разыменования


Модифицируем предыдущий листинг и посмотрим, какое количество
памяти занимают переменные разных типов (рисунок 7):

7
Рисунок 7 – Определение размеров переменных
Как видно из рисунка 7 переменная «х» типа char занимает в памяти 1
байт. Но при этом, указатель «*р» на переменную х типа char занимает в
памяти 8 байт. Этот пример иллюстрирует еще одну особенность указателей:
указатель в памяти компьютера, в зависимости от архитектуры
установленного процессора (32х-64х) занимает 4-8 байт. Объем занимаемой
указателем памяти НЕ зависит от типа данных, на которые он указывает!
Адресная арифметика
В языке С допустимы только две арифметические операции над
указателями: суммирование и вычитание. Все остальные арифметические
операции запрещены. А именно: нельзя делить и умножать указатели,
8
суммировать два указателя, выполнять над указателями побитовые операции,
суммировать указатель со значениями, имеющими тип float или doubleи т.д.
Операции адресной арифметики подчиняются следующим правилам:
После выполнения операции увеличения над указателем, данный указатель
будет ссылаться на следующий объект своего базового типа. После
выполнения операции уменьшения – на предыдущий объект. Применительно
к указателям на char, операции адресной арифметики выполняются как
обычные арифметические операции, потому что длина объекта char всегда
равна 1 (рисунок 8).

Рисунок 8 – Количество байт, занимаемое в памяти переменными различных


типов
Операции адресной арифметики не ограничены увеличением
(инкрементом) и уменьшением (декрементом). Например, к указателям
можно добавлять целые числа или вычитать из них целые числа. В качестве
примера выполним следующий листинг (рисунок 9):

9
Рисунок 9 – Пример выполнения операций адресной арифметики
Кроме суммирования и вычитания указателя и целого, разрешена еще
одна операция адресной арифметики: можно вычитать два указателя.
Вычитание указателей друг из друга определено только в том случае, если
оба указателя указывают на элементы одного и того же массива. Результатом
вычитания одного указателя из другого будет количество элементов массива
(целое число) между этими указателями.
Помимо всего названного ранее, указатели можно сравнивать. Для
сравнения указателей используются операции = =, >, <. При сравнении
указателей важно, чтобы эти указатели имели некоторую связь между собой.
Обычно сравнение указателей используется, когда два или более указателя
указывают на один объект, например, массив. Выполним следующий листинг
(рисунок 10):

10
Рисунок 10 – Пример использования операции сравнения указателей
Помимо описанных операций, указатель можно использовать в правой
части оператора присваивания для присваивания его значения другому
указателю. Если оба указателя имеют один и тот же тип, то выполняется
простое присваивание, без преобразования типа. Также допускается
присваивание указателя одного типа указателю другого типа. Однако для
этого необходимо выполнять явное преобразование типа указателя.

11
УКАЗАТЕЛИ НА УКАЗАТЕЛИ (МНОГОУРОВНЕВАЯ
АДРЕСАЦИЯ)
Указатель на указатель является формой многочисленного
перенаправления или цепочки указателей. Рассмотрим рисунок 11:

Рисунок 11 – Схематичное представление работы указателей

В случае обычных указателей, указатель содержит адрес некоторого


участка памяти, содержащего некоторое значение. В случае указателя на
указатель, первый указатель содержит адрес второго, который в свою
очередь содержит адрес участка памяти, содержащего некоторое значение.
Переменная, являющаяся указателем на указатель, должна быть описана
определенным образом. Это выполняется путем помещения двух звездочек
перед именем. Рассмотрим пример (рисунок 12):

Рисунок 12 – Пример использования указателя на указатель

12
Указатели на указатели нашли широкое применение для создания и
передачи динамических двумерных массивов (рисунок 13).

Рисунок 13 – Использование многоуровневой адресации для создания и


передачи динамических двумерных массивов

УКАЗАТЕЛЬ ОБЩЕГО НАЗНАЧЕНИЯ


Как было указано ранее, переменная указателя декларируется с
указанием типа данных содержимого, которое хранится в том месте памяти,
на которое ссылается указатель. Такая переменная указателя, не может
содержать в себе адрес переменной другого типа. Это приведет к сообщению
об ошибке при компиляции. Но что делать, когда нужно реализовать
функцию, принимающую значения разных типов, и, в зависимости от этого,
выполняющую различные действия? Специально для решения таких задач
существует специальный тип указателей - указатель на void или указатель
общего назначения.
В языке C есть возможность создать указатель на неопределенный
тип, так называемый "пустой указатель" (void pointer). Когда указатель
декларируется с ключевым словом void, он становится универсальным. Это
значит, что ему может быть присвоен адрес переменной любого типа (char,
int, float и т. д.), и это не будет ошибкой. Пустые указатели нашли основное
применение при вызове функций. Можно написать функцию общего
назначения, которая будет работать с любым типом.

13
ОПЕРАЦИИ НАД VOID-УКАЗАТЕЛЯМИ
Набор операций, которые можно совершить с void-указателем сильно
ограничен в связи с его особенностями. Для указателей void* доступен
следующий набор операций:
Указатель на объект любого типа можно присвоить переменной типа
void*;
Один void-указтель можно присвоить другому void-указателю;
Несколько указателей на void можно сравнивать на равенство и
неравенство;
Прочие операции могут оказаться опасными, потому что компилятор
не знает, на какого сорта объект ссылается указатель на самом деле. Поэтому
другие операции вызывают сообщение об ошибке на этапе компиляции.
случае указателя на void существует ключевая особенность этого типа
указателей, отличающих их от всех остальных - нужно использовать
приведение типа переменной указателя, чтобы выполнить её разыменование.
Разыменовывать указатель на void без приведения типов нельзя!
Причина в том, что с void-указателем не связан никакой тип, и для
компилятора нет способа автоматически узнать, как обращаться к
содержимому памяти, связанному с void-указателем. Таким образом, чтобы
получить данные, на который ссылается void-указатель, мы делаем
приведение указателя к корректному типу данных, которые находятся по
адресу, содержащемуся в void-указателе. Рассмотрим пример (рисунок 14):

14
Рисунок 14 а) – Пример использования void-указателя, для вывода
переменных типа int и float

Рисунок 14 б) – Пример использования void-указателя, для вывода


переменных типа int и float

15
УКАЗАТЕЛИ НА ФУНКЦИИ
Как известно, функции – это набор команд, которые расположены в
соответствующей области памяти по определенному адресу, который можно
присвоить указателю в качестве его значения. Указатели на функции
позволяют упростить решение многих задач. Совместно с void указателями
можно создавать функции общего назначения (например, сортировки и
поиска). Указатели позволяют создавать функции высших порядков
(функции, принимающие в качестве аргументов функции). В программе на С
адресом функции служит ее имя без скобок и аргументов.

ПРЕИМУЩЕСТВА И НЕДОСТАТКИ ПРИМЕНЕНИЯ


УКАЗАТЕЛЕЙ
Преимущество применения указателей заключается в том, что они
позволяют передать в функцию значение по ссылке. Слово “ссылка”
означает, что мы не передаем значение, а ссылаемся на адрес этого значения.
В этом случае можно внутри функции изменять значение элемента
данных. Главные недостатки указателей:
 Нарушение принципов изоляции кода
Ошибка в указателе может привести к тому, что будет память в
случайном месте. Хорошо если программа «упадет» еще на этапе
компиляции, тогда программист сразу заметит ошибку. Но если программа
продолжит работу, то найти ошибку будет очень сложно, ведь она не сразу
проявляется.
 Отвлечение внимания на детали реализации
При использовании указателей программисту нужно держать в уме
принципы работы с памятью, а это отвлекает от сути задачи, которая решает
программист. При правильном подходе к программированию программист
должен думать только о решаемой задаче, и не отвлекаться на посторонние
детали.
 Плохая читаемость кода
Прямое использование переменной является самоочевидной вещью.
Если мы видим x++, то сразу понимаем, что происходит, а вот если мы видим
(*px)++ или *px++, то чтобы понять процесс, нужно вдумываться.

16
Выполнение
Использование указателей
Запишем предложенный код в отдельный файл, откомпилируем его и
посмотрим на результат выполнения данного кода.

Рисунок 15 – Содержание файла

Рисунок 16 – Создание и запуск программы

Вывод: в результате вывода переменных sum и num мы получили их


значения присвоенное в коды. Что касается указателей, то чтобы вывести их
значения необходимо разыменовать данные указатели (*указатель). Иначе
будет получен адрес, на который ссылается данный указатель
17
Объявление и использование указателей

Рисунок 17 – Содержание файла

18
Рисунок 18 – Создание и запуск программы

Вывод: первые три строки вывода представляет собой вывод самого


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

Далее происходит вывод массива, где указывается индекс данного


числа в массиве, адрес по которому лежит данное значение и само значение
элемента.

19
Побайтовое заполнение области памяти

Рисунок 19 – Содержание файла

20
Рисунок 20 – Создание и запуск программы

Вывод: значение первого вывода переменной number получается


таким из-за того, что int занимает в памяти 4 байта и в каждый байт памяти,
через цикл, мы записываем «1». В результате мы получаем 00000001
00000001 00000001 000000012, что в обычном нам представлении 1684300910.

Далее мы обнуляем значение number и во 2й байт записываем 2, в


результате чего получим 00000000 00000000 00000010 000000002, что в
десятичном представлении 51210

21
Использование указателя общего назначения

Рисунок 21 – Содержание файла

Рисунок 22 – Создание и запуск программы

Вывод: через указатель на void можно использовать корректно с


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

22
Побайтовое заполнение целочисленного значения с
использованием указателя общего назначения

Рисунок 23 – Содержание файла

Рисунок 24 – Создание и запуск программы

Вывод: Изначально в переменную number, через указатель pnt,


записываем результат сдвига 1 на 8 и получаем 00000000 00000000 00000001
000000002 = 25610. Далее записываем в первый байт число 3 и получаем
00000000 00000000 00000001 000000112 = 25910

И дальнейший вывод – вывод значений из каждого байта, где в


первый это 000000112 = 310, второй 000000012 = 110, а оставшиеся два
являются нулевыми
23
Многоуровневая адресация

Рисунок 25 – Содержание файла

24
Рисунок 26 – Создание и запуск программы

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


которые указывают на другой указатель. И для вывода значений по данному
адресу необходимо Nное количество разыменований

25
Вывод
Я ознакомился с основами работы с указателями и адресной
арифметикой в языках Си.

26
Контрольные вопросы

1.Каково назначение указателей?


Указатель предназначен для хранения адреса какого-либо объекта в
памяти компьютера.

2.Каким образом можно получить значение адреса, по которому


записана переменная?
Чтобы получить адрес переменной необходимо перед её именем
написать символ “&”.

3.Каким образом можно получить доступ (для чтения или записи)


к значению, записанному по адресу, хранящемуся в указателе?
Чтобы получить доступ к значению необходимо разыменовать
указатель, добавив перед ним символ *.

4.Как объявляются указатели, каким образом они используются?


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

5.Как осуществить побайтовое заполнение области памяти?


Используя цикл проходить по 0го до Nго байта в памяти присваивая
ему значение
6.Зачем используются указатели общего назначения?
Указатель на void может принимать другие виды указателей, что
может быть полезно при написании функций, работающих с различными
типами указателей.

7.Поясните использование указателей общего назначения.


Указателю на void может быть присвоена любая ссылка:

Рисунок 27 – присвоение к void указателю


Но чтобы выполнять какие-либо действия мы должны привести его к
нужному нам типу

Рисунок 28 – Действие с такими указателями


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

27
8.Поясните побайтовое заполнение целочисленного значения с
использованием указателя общего назначения.
Создаем указатель на void, которому присваиваем значение к-л
переменной. И далее, через байтовый сдвиг заполняем байты.
9.Зачем нужна многоуровневая адресация?
С помощью данного механизма мы имеем возможность ссылаться на
другие указатели, что дает нам возможность, к примеру, создавать
динамические двумерные массивы.
10.Как реализовывается многоуровневая адресация?
«Уровень» адресации можно задать количеством символов *. И
ссылаться данный указатель сможет на указатель имеющего уровень
адресации N-1.
К примеру, второй уровень адресации - **ptr2
И он может ссылаться на *ptr1 -> ptr2=&ptr1, который в свою очередь
будет ссылаться на значение

28