Язык си ссылка на переменную
Функции. Передача аргументов по значению и по ссылке
Объявление и определение функций
При изучении работы функций важно понимать, что такое локальная и что такое глобальная переменные. В языке программирования C глобальные (внешние) переменные объявляются вне какой-либо функции. С их помощью удобно организовывать обмен данными между функциями, однако это считается дурным тоном, т.к. легко запутывает программу. Локальные переменные в языке программирования C называют автоматическими. Область действия автоматических переменных распространяется только на ту функцию, в которой они были объявлены. Параметры функции также являются локальными переменными.
В теле main() функция median() вызывается три раза. Результат выполнения функции не обязательно должен быть присвоен переменной.
Вышеописанную программу можно было бы записать так:
Хотя такой способ и экономит одну строчку кода, однако главная функция, в которой отражена основная логика программы, опускается вниз, что может быть неудобно. Поэтому первый вариант предпочтительней.
Напишите функцию, возводящую куб числа, переданного ей в качестве аргумента. Вызовите эту функцию с разными аргументами.
Статические переменные
Внешние статические переменные, в отличие от обычных глобальных переменных, нельзя использовать из других файлов в случае программы, состоящей не из одного файла. Они глобальны только для функций того файла, в котором объявлены. Это своего рода сокрытие данных, по принципу «не выставлять наружу ничего лишнего, чтобы ‘что-нибудь’ нечаянно не могло ‘испортить’ данные».
Статические переменные, объявленные внутри функций имеют такую же область действия, как автоматические. Однако в отличие от автоматических, значения локальных статических переменных не теряются, а сохраняются между вызовами функции:
В этом примере в функции hello() производится подсчет ее вызовов.
Передача аргументов по ссылке
В первом примере этого урока мы передавали в функцию аргументы по значению. Это значит, что когда функция вызывается, ей передаются в качестве фактических параметров (аргументов) не указанные переменные, а копии значений этих переменных. Сами переменные к этим копиям уже никакого отношения не имеют. В вызываемой функции эти значения присваиваются переменным-параметрам, которые, как известно, локальны. Отсюда следует, что изменение переданных значений никакого влияния на переменные, переданные в функцию при вызове, не оказывают. В примере выше даже если бы в функции median() менялись значения переменных n1 и n2, то никакого влияния сей факт на переменные num1 и num2 не оказал.
Однако можно организовать изменение локальной переменной одной функции с помощью другой функции. Сделать это можно, передав в функцию адрес переменной или указатель на нее. На самом деле в этом случае также передается копия значения. Но какого значения?! Это адрес на область памяти. На один и тот же участок памяти может существовать множество ссылок, и с помощью каждой из них можно поменять находящееся там значение. Рассмотрим пример:
Кроме того, следует знать, что функция может возвращать адрес.
Важно понять механизм так называемой передачи аргументов по ссылке, т.к. это понимание пригодится при изучении массивов и строк. Использовать указатели при работе с простыми типами данных не стоит. Лучше возвращать из функции значение, чем менять локальные переменные одной функции с помощью кода другой функции. Функции должны быть достаточно автономными.
Указатели и ссылки в C и C++
Немного о памяти
Память можно представить по-разному.
Объяснение для военных на примере взвода. Есть взвод солдат. Численность — 30 человек. Построены в одну шеренгу. Если отдать им команду рассчитаться, у кажого в этой шеренге будет свой уникальный номер. Обязательно у каждого будет и обязательно уникальный. Этот взвод — доступная нам память. Всего нам здесь выделено для работы 30 ячеек. Можно использовать меньше. Больше — нельзя. К каждой ячейке можно обратиться и быть уверенным, что обратился именно к ней. Любому солдату можно дать что-то в руки. Например, цветы. То есть поместить по адресу данные.
Объяснение для Маленького Принца. Здравствуй, Маленький Принц. Представим, что твоему барашку стало одиноко. И ты попросил нарисовать ему друзей. Ты выделил для барашков целую планету (точнее, астероид) по соседству. Эта планета — доступная память. Вся она уставлена коробочками, в которых будут жить барашки. Чтобы не запутаться, все коробочки пронумерованы. Коробочки — это ячейки памяти. Барашек в коробочке — это данные. Допустим, что попался какой-то особо упитанный барашек. Ему понадобится две коробочки. Или даже больше. Барашек — неделимая структура (для нас с тобой, Маленький Принц, это точно так), а коробочки идут подряд. Нет ничего проще. Мы вынимает стенки между двумя рядом стоящими коробочками и кладем туда барашка. Места в коробочке не очень много. И барашек не может свободно развернуться. Поэтому мы всегда знаем, где его голова, а где хвост. И если нам что-то нужно будет сказать барашку, мы обратимся к той коробочке, где у него голова.
Объяснение для хулиганов. Есть забор. Забор из досок. Забор — доступная память. Доска — ячейка памяти. Забор длинный. И чтобы потом похвастаться друзьям, где ты сделал надпись, надо как-то обозначить место. Я знаю, о уважаемый хулиган, что ты нашел бы что-то поинтереснее, чем нумеровать каждую доску. Но в программировании не такие выдумщики. Поэтому доски просто пронумерованы. Возможно, твоя надпись поместится на одну доску. Например, %знак футбольного клуба%. Тогда ты просто скажешь номер и друзья увидят серьезность твоего отношения к футболу. А возможно, что одной доски не хватит. Ничего, главное, чтобы хватило забора. Пиши подряд. Просто потом скажи, с какой доски читать. А что если не подряд? Бывает и не подряд. Например, ты хочешь признаться Маше в любви. Ты назначаешь ей встречу под доской номер 40. Если все пройдет хорошо, ты возьмешь Машу и поведешь ее к доске 10, где заранее написал «Хулиган + Маша = любовь». Если что-то пошло не так, ты поведешь Машу к доске 60, на которой написано все нехорошее, что ты думаешь о Маше. Примерно так выглядит условный переход. То есть оба его исхода помещаются в память заранее. На каком-то этапе вычисляется условие. Если условие выполнилось — переходим к одному месту памяти и начинаем идти дальше подряд. Если условие не выполнилось — переходим к другому месту, с другими инструкциями. И тоже продолжаем выполнять их подряд. Инструкции всегда выполняются одна за другой, если только не встретился переход (с условием или без условия). Ну, или что-то поломалось.
Модель взаимодействия программы с памятью компьютера может быть разной. Будем считать, что для каждой программы выделяется своя обособленная область памяти. Даже если запущены два экземпляра одной программы — память у них будет разная.
В памяти хранятся числа. Ни с чем кроме чисел компьютер работать не умеет. Если вы поместили в память какую-то комплексную структуру, она все равно будет представлена числами. Даже если вы работаете с ней как со структурой. Примером комплексной структуры в терминах языков C и C++ может быть, например, экземпляр структуры или объект класса.
Наименьшей адресуемой величиной в памяти типового компьютера является байт. Это означает, что каждый байт имеет собственный адрес. Для того, чтобы обратиться к полубайту, придется обратиться сначала к байту, а затем выделить из него половину.
Возможно, подобные объяснения кажутся очевидными и даже смешными. Но в действительности имеет значение только формализация. И то, что кажется привычным, в определенных случаях может быть совсем иным. Например, запросто можно задать условие, при котором байт не будет равен 8 битам. И такие системы существуют.
Раз уж мы договорились, что минимальная адресуемая величина — байт, то всю доступную программе память можно представить в виде последовательности байтов.
Система в компьютере двоичная (хотя есть и тернарные машины). В 1 байте 8 бит. Английское bit означает binary digit, то есть двоичный разряд. Получается, что байт может принимать числовые значения от 0 до 2 в 8 степени без единицы. То есть от 0 до 255. Если представлять числа в шестнадцатеричной системе, то от 0x00 до 0xFF.
Представим область памяти.
0x01 | 0x02 | 0x03 | 0x04 |
0x05 | 0x06 | 0x07 | 0x08 |
0x09 | 0x0A | 0x0B | 0x0C |
0x0D | 0x0E | 0x0F | 0x10 |
В ней лежат числа от 1 до 16. Направление обхода обычно задается слева направо и сверху вниз. Помните, что никакой таблицы на самом деле нет (почти как ложки в Матрице). Она нужна человеку для удобства восприятия. Каждая такая ячейка описывается двумя величинами: значением и адресом. В приведенной таблице значение и адрес совпадают.
Понятие указателя
Указатель — это переменная. Такая же, как и любая другая. Со своими «можно» и со своими «нельзя». У нее есть свое значение и свой адрес в памяти.
Значение переменной-указателя — адрес другой переменной. Адрес переменной-указателя свой и независимый.
Переменная-указатель (далее будем говорить просто — указатель) объявляется также, как и любые другие переменные, но после имени типа ставится звездочка.
Здесь объявляется переменная pointerToInteger. Ее тип — указатель на переменную типа int.
Как следует писать звездочку относительно типа и имени переменной? Встречаются, например, такие формы записи, и все они имеют право на существование:
Аргументы за первую форму. Чтобы объявить переменную следует указать ее тип, а затем имя. Звездочка является частью типа, а не частью имени. Это также подтверждается тем, что при привидении типов пишется тип со звездочкой, а не тип отдельно. Следовательно, должна писаться слитно с типом. Минус в том, что при объявлении нескольких переменных после объявления int*, только первая из них будет указателем, а вторая будет просто переменной типа int. Не объявляйте несколько указателей в одной строчке. Это не очень хороший стиль.
Аргументы за вторую форму. Есть люди, которым нравится «когда код дышит» Они ставят пробел до скобок и после скобок. И здесь тоже ставят. Возможно, это просто такой компромисс.
Аргументы за третью форму. Если писать так, то с объявлением нескольких указателей в одной строчке проблем быть не должно (хотя это все равно плохой тон). Некоторая идеология нарушается. Но этот стиль — самый распространенный, так как точно видно, что переменная — указатель.
И помните, что компилятору все это безразлично.
Адрес переменной и значение переменной по адресу
Рассмотрим две переменные: целочисленную переменную x и указатель на целочисленную переменную.
Чтобы получить адрес переменной, нужно перед ее именем написать амперсанд.
Данная конструкция будет выполняться справа налево. Сначала с помощью оператора &, примененного к переменной x, будет получен адрес x. Затем адрес x будет сохранен в указателе p.
Есть и обратная операция. Чтобы получить значение переменной по ее адресу, следует написать звездочку перед именем указателя.
Такая операция в русском языке называется не слишком благозвучным словом «разыменование». В английском — dereference.
В данном примере с помощью оператора * мы получим то значение, которое находится в памяти по адресу p. Затем мы сохраним его в переменную y. В итоге получится, что значения x и y совпадают.
Все это несложно увидеть на экране.
В указанном примере значение x и y будут одинаковы. А также адрес x и значение p.
Адресная арифметика
К указателям можно прибавлять числа. Из указателей можно вычитать числа. На основе этого сделана адресация в массиве. Этот код показывает несколько важных вещей.
Первая строка простая и понятая. Объявлен массив и заполнен числами от 1 до 5.
Во второй строке объявляется указатель на int и ему присваивается адрес нулевого элемента массива. Некоторые компиляторы разрешают писать такое присвоение так, считая, что имя массива означает адрес его нулевого элемента.
Но если вы хотите избежать неоднозначности, пишите явно. Таким образом в p лежит адрес начала массива. А конструкция *p даст 1.
Третья строчка увеличивает значение p. Но не просто на 1, а на 1 * sizeof(int). Пусть в данной системе int занимает 4 байта. После увеличения p на 1, p указывает не на следующий байт, а на первый байт из следующей четверки байтов. Программисту не нужно думать в данном случае о размере типа.
С вычитанием ситуация такая же.
Последний важный момент этого кода в том, как преобразуется обращение к элементу массива. Имя массива — это указатель на его начало. Точка отсчета. Индекс, переданный в квадратных скобках, — смещение относительно начала массива.
Конструкция array[i] будет преобразована компилятором к *(array + i). К начальному адресу массива будет прибавлено число с учетом размерности типа данных. А затем будет взято значение по вычисленному адресу. Обратите внимание, что никто не запрещает написать и так i[array]. Ведь конструкция будет преобразована к виду.
С указателем можно складывать число, представленное переменной или целочисленной константой. Вычесть можно не только число, но и указатель из указателя. Это бывает полезно. А вот сложить два указателя, умножить или разделить указатель на число или на другой указатель — нельзя.
С указателями разных типов нельзя обходиться легкомысленно.
С точки зрения языка C все корректно. А вот в C++ будет ошибка, потому что типы указателей не совпадают.
Вот такая конструкция будет принята C++.
Применение указателей
Обычно функция возвращает одно значение. А как вернуть больше одного? Рассмотрим код функции, которая меняет местами две переменные.
Пусть есть переменные x и y с некоторыми значениями. Если выполнить функцию, передав в нее x и y, окажется, что никакого обмена не произошло. И это правильно.
При вызове этой функции в стеке будут сохранены значения x и y. Далее a и b получат значения x и y. Будет выполнена перестановка. Затем функция завершится и значения x и y будут восстановлены из стека. Все по-честному.
Чтобы заставить функцию работать так, как нужно, следует передавать в нее не значения переменных x и y, а их адреса. Но и саму функцию тогда нужно адаптировать для работы с адресами.
Не стоить забывать о том, что и вызов функции теперь должен выглядеть иначе.
Теперь в функцию передаются адреса. И работа ведется относительно переданных адресов.
Если функция должна вернуть несколько значений, необходимо передавать в нее адреса.
Если функция должна менять значение переменной, нужно передавать ей адрес этой переменной.
У тех, кто только начинает программировать на C, есть одна распространенная ошибка. При вводе с клавиатуры с помощью функции scanf() они передают значение переменной, а не ее адрес. А ведь scanf() должна менять значение переменной.
Еще один важный случай, когда указатели крайне полезны — это передача большого объема данных.
Пусть нам нужно передать в функцию целое число типа int. Таким образом мы передаем в функцию sizeof(int) байт. Обычно это 4 байта (размер будет зависеть от архитектуры компьютера и компилятора). 4 байта — не так много. 4 байта уйдут в стек. Потому что имеет место передача по значению.
Теперь нам нужно передать 10 таких переменных. Это уже 40 байт. Тоже невелика задача.
Вообразим себя проектировщиками Большого Адронного Коллайдера. Вы отвечаете за безопасность системы. Именно вас окружают люди с недобрыми взглядами и факелами. Нужно показать им на модели, что конца света не будет. Для этого нужно передать в функцию collaiderModel(), скажем, 1 Гб данных. Представляете, сколько информации будет сохранено в стек? А скорее всего программа не даст вам стек такого объема без специальных манипуляций.
Когда нужно передать большой объем данных, его передают не копированием, а по адресу. Все массивы, даже из одного элемента, передаются по адресу.
Указатели — это мощный инструмент. Указатели эффективны и быстры, но не слишком безопасны. Потому как вся ответственность за их использования ложится на разработчика. Разработчик — человек. А человеку свойственно ошибаться.
В большинстве компиляторов C и С++ неинициализированные локальные переменные имеют случайное значение. Глобальные обнуляются.
Если мы захотим разыменовать указатель и присвоить ему значение, скорее всего, будет ошибка.
Неинициализированный указатель p хранит случайный адрес. Мы честно можем попытаться получить значение по этому адресу и что-то туда записать. Но совсем не факт, что нам можно что-то делать с памятью по этому адресу.
Указатели можно и нужно обнулять. Для этого есть специальное значение NULL.
Это запись больше соответствует стилю C. В C++ обычно можно инициализировать указатель нулем.
Ловкость рук и никакого мошенничества. На самом деле, если изучить библиотечные файлы языка, можно найти определение для NULL.
Для C NULL — это нуль, приведенный к указателю на void. Для C++ все немного не так. Стандарт говорит: «The macro NULL is an implementation-defined C++ null pointer constant in this International Standard. Possible definitions include 0 and 0L, but not (void*)0». То есть это просто 0 или 0, приведенный к long.
Предлагаю вам такую задачку. Папа Карло дал Буратино 5 яблок. Злой Карабас Барабас отобрал 3 яблока. Сколько яблок осталось у Буратино?
Ответ: неизвестно. Так как нигде не сказано, сколько яблок у Буратино было изначально.
Мораль: обнуляйте переменные.
Ссылки
В языке C++ появился новый механизм работы с переменными — ссылки. Функция swap() была хороша, только не слишком удобно применять разыменование. С помощью ссылок функция swap() может выглядеть аккуратнее.
А вызов функции тогда будет уже без взятия адреса переменных.
Конструкция double& объявляет ссылку на переменную типа double. При таком объявлении функции в стек будут положены не значения переменных, а их адреса.
Ссылка — это указатель, с которым можно работать, как с обычной переменной.
Ссылка не может быть равна NULL. Указатель может. Ссылка не может быть непроинициализирована. Указатель может.
Для взятия адреса переменной и для объявления ссылки используется одинаковый символ — амперсанд. Но в случае взятия адреса & стоит в выражении, перед именем переменной. А в случае объявления ссылки — в объявлении, после объявления типа.
Использование ссылок и указателей — это очень широкая тема. Описание основ на этом закончим.
За мысли и замечания спасибо Юрию Борисову, @vkirkizh, @vitpetrov.
Урок №88. Ссылки
Обновл. 16 Авг 2020 |
До этого момента мы успели рассмотреть 2 основных типа переменных:
обычные переменные, которые хранят значения напрямую;
указатели, которые хранят адрес другого значения (или null), для доступа к которым выполняется операция разыменования указателя.
Ссылки — это третий базовый тип переменных в языке C++.
Ссылки
Ссылка — это тип переменной в языке C++, который работает как псевдоним другого объекта или значения. Язык C++ поддерживает 3 типа ссылок:
Ссылки на неконстантные значения (обычно их называют просто «ссылки» или «неконстантные ссылки»), которые мы обсудим на этом уроке.
Ссылки на константные значения (обычно их называют «константные ссылки»), которые мы обсудим на следующем уроке.
В C++11 добавлены ссылки r-value, о которых мы поговорим чуть позже.
Ссылка (на неконстантное значение) объявляется с использованием амперсанда ( & ) между типом данных и именем ссылки:
В этом контексте амперсанд не означает «оператор адреса», он означает «ссылка на».
Ссылки в качестве псевдонимов
Ссылки обычно ведут себя идентично значениям, на которые они ссылаются. В этом смысле ссылка работает как псевдоним объекта, на который она ссылается, например:
Результат выполнения программы:
В примере, приведенном выше, объекты ref и value обрабатываются как одно целое. Использование оператора адреса с ссылкой приведет к возврату адреса значения, на которое ссылается ссылка:
Краткий обзор l-value и r-value
На уроке №10 мы уже рассматривали, что такое l-value и r-value. l-value — это объект, который имеет определенный адрес памяти (например, переменная x ) и сохраняется за пределами одного выражения. r-value — это временное значение без определенного адреса памяти и с областью видимости выражения (т.е. сохраняется в пределах одного выражения). В качестве r-values могут быть как результаты выражения (например, 2 + 3 ), так и литералы.
Инициализация ссылок
Ссылки должны быть инициализированы при создании:
В отличие от указателей, которые могут содержать нулевое значение, ссылки нулевыми быть не могут.
Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть инициализированы константными l-values или r-values:
Обратите внимание, во втором случае вы не можете инициализировать неконстантную ссылку константным объектом. В противном случае, вы бы могли изменить значение константного объекта через ссылку, что уже является нарушением понятия «константа».
После инициализации изменить объект, на который указывает ссылка — нельзя. Рассмотрим следующий фрагмент кода:
Обратите внимание, во втором стейтменте ( ref = value2; ) выполняется не то, что вы могли бы ожидать! Вместо переприсваивания ref (ссылаться на переменную value2 ), значение из value2 присваивается переменной value1 (на которое и ссылается ref ).
Ссылки в качестве параметров в функциях
Ссылки чаще всего используются в качестве параметров в функциях. В этом контексте ссылка-параметр работает как псевдоним аргумента, а сам аргумент не копируется при передаче в параметр. Это в свою очередь улучшает производительность, если аргумент слишком большой или затратный для копирования.
На уроке №82 мы говорили о том, что передача аргумента-указателя в функцию позволяет функции при разыменовании этого указателя напрямую изменять значение аргумента.
Ссылки работают аналогично. Поскольку ссылка-параметр — это псевдоним аргумента, то функция, использующая ссылку-параметр, может изменять аргумент, переданный ей, также напрямую:
Результат выполнения программы:
Совет: Передавайте аргументы в функцию через неконстантные ссылки-параметры, если они должны быть изменены функцией в дальнейшем.
Основным недостатком использования неконстантных ссылок в качестве параметров в функциях является то, что аргумент должен быть неконстантным l-value (т.е. константой или литералом он быть не может). Мы поговорим об этом подробнее (и о том, как это обойти) на следующем уроке.
Ссылки как более легкий способ доступа к данным
Второе (гораздо менее используемое) применение ссылок заключается в более легком способе доступа к вложенным данным. Рассмотрим следующую структуру:
Таким образом, следующие два стейтмента идентичны:
Ссылки позволяют сделать ваш код более чистым и понятным.
Ссылки vs. Указатели
Ссылка — это тот же указатель, который неявно разыменовывается при доступе к значению, на которое он указывает («под капотом» ссылки реализованы с помощью указателей). Таким образом, в следующем коде:
*ptr и ref обрабатываются одинаково. Т.е. это одно и то же:
Поскольку ссылки должны быть инициализированы корректными объектами (они не могут быть нулевыми) и не могут быть изменены позже, то они, как правило, безопаснее указателей (так как риск разыменования нулевого указателя отпадает). Однако они немного ограничены в функциональности по сравнению с указателями.
Если определенное задание может быть решено с помощью как ссылок, так и указателей, то лучше использовать ссылки. Указатели следует использовать только в тех ситуациях, когда ссылки являются недостаточно эффективными (например, при динамическом выделении памяти).
Заключение
Ссылки позволяют определять псевдонимы для других объектов или значений. Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть переприсвоены после инициализации. Ссылки чаще всего используются в качестве параметров в функциях, когда мы хотим изменить значение аргумента или хотим избежать его затратного копирования.
Поделиться в социальных сетях:
Язык си ссылка на переменную
Указатели совместно с адресной арифметикой играют в Си особую роль. Можно сказать, что они определяют лицо языка. Благодаря им Си может считаться одновременно языком высокого и низкого уровня по отношению к памяти.
Если говорить о понятиях указатель, ссылка, объект, то они встречаются не только в языках программирования, но в широком смысле в информационных технологиях. Когда речь идет о доступе к информационным ресурсам, то существуют различные варианты доступа к ним:
В языках программирования термины объект (значение), указатель и ссылка имеют примерно аналогичный смысл, но касаются способов доступа и передачи значений переменных.
· при передаче формальных параметров при вызове процедур (функций) практически во всех языках программирования реализованы способы передачи по ссылке и по значению;
· в Паскале и Си определено понятие указатель как переменная особого вида, содержащая адрес размещения в памяти другой переменной. Использование указателей позволяется создавать динамические структуры данных, в которых элементы взаимно ссылаются друг на друга;
Указатель в Си
Передавать данные между программами, данные от одной части программы к другой (например, от вызывающей функции к вызываемой) можно двумя способами :
· создавать в каждой точке программы (например, на входе функции) копию тех данных, которые необходимо обрабатывать ;
Наряду с указателем в программировании также используется термин ссылка. Ссылка – содержанием ссылки также является адресная информация об объекте (переменной), но внешне она выглядит как переменная (синоним оригинала).
В языках программирования имя переменной ассоциируется с адресом области памяти, в которой транслятор размещает ее в процессе трансляции программы. Все операции над обычными переменными преобразуются в команды с прямой адресацией к соответствующим словам памяти.
· указатель, который содержит адрес переменной, ссылается на эту переменную или назначен на нее;
· переменная, адрес которой содержится в указателе, называется указуемой переменной.
рис. 52-2. Определение указателя и операции над ним
Последовательность действий при работе с указателем включает 3 шага:
1. Определение указуемых переменных и переменной-указателя. Для переменной-указателя это делается особым образом.
int a,x; // Обычные целые переменнные
2. Связывание указателя с указуемой переменной. Значением указателя является адрес другой переменной. Следующим шагом указатель должен быть настроен, или назначен на переменную, на которую он будет ссылаться.
p = &a; // Указатель содержит адрес переменной a
3. И наконец, в любом выражении косвенное обращение по указателю интерпретируется как переход от него к указуемой переменной с выполнением над ней всех далее перечисленных в выражении операций.
* p =100; // Эквивалентно a =100
x = x + *p; // Эквивалентно x = x + a
(* p )++; // Эквивалентно a ++
Замечание: при обращении через указатель имя указуемой переменной в выражении отсутствует. Поэтому можно считать, что обращение через указатель производится к «безымянной» переменной, а операцию « *» называются также операцией разыменования указателя.
Указатель дает « степень свободы» или универсальности любому алгоритму обра
ботки данных. Действительно, если некоторый фрагмент программы получает данные непосредственно в некоторой переменной, то он может обрабатывать ее и только ее. Если же данные он получает через указатель, то обработка данных (указуемых переменных) может производиться в любой области памяти компьютера (или программы). При этом сам фрагмент может и «не знать», какие данные он обрабатывает, если значение самого указателя передано программе извне.
Адресная арифметика и управление памятью
Способность указателя ссылаться на «отдельно стоящие» переменные не меняет качества языка, поскольку нельзя выйти за рамки множества указуемых переменных, определенных в программе. Такая же концепция указателя принята, например, в Паскале. Но в Си существует еще одна, расширенная интерпретация, позволяющая через указатель работать с массивами и с памятью компьютера ни низком (архитектурном) уровне без каких-либо ограничений со стороны транслятора. Это «свобода самовыражения» обеспечивается одной дополнительной операцией адресной арифметики. Но сначала определим свойства указателя в соответствии с расширенной интерпретацией.
Любой указатель в Си ссылается на неограниченную в обе стороны область памяти (массив), заполненную переменными указуемого типа с индексацией элементов относительно текущего положения указателя.
· любой указатель потенциально ссылается на неограниченную в обе стороны область памяти, заполненную переменными указуемого типа;
· результатом операции указатель+i является адрес i-ой переменной (значение указателя на i-ую переменную) в этой области относительно текущего положения указателя.
Значение указуемой переменной
Указатель на i-ю переменную после указуемой
Указатель на i-ю переменную перед указуемой
Значение i-й переменной после указуемой
Значение i-й переменной после указуемой
Переместить указатель на следующую переменную
Переместить указатель на предыдущую переменную
Переместить указатель на i переменных вперед
Переместить указатель на i переменных назад
Получить значение указуемой переменной и переместить указатель к следующей
Переместить указатель к переменной, предшествующей указуемой, и получить ее значение
Указатель на свободную память вслед за указуемой переменной
Если МАССИВ=ПАМЯТЬ+УКАЗАТЕЛЬ (начальный адрес), то УКАЗАТЕЛЬ=МАССИВ-ПАМЯТЬ, т.е. указатель это «массив без памяти», «свободно перемещающийся по памяти» массив.
Различия и сходства
Оба интерпретируются как указатели и оба имеют тип int *
Указатель требует настройки «на память»
Работа с областью памяти как с обычным массивом, так и через указатель полностью идентична вплоть до синтаксиса
Указатель может перемещаться по памяти относительно своего текущего положения
Идентификатор массива без скобок интерпретируется как адрес нулевого элемента нулевой строки, или указатель на базовый тип данных. В нашем примере идентификатору A будет соответствовать выражение &A[0][0] с типом char*.
Имя двумерного массива с единственным индексом интерпретируется как начальный адрес соответствующего внутреннего одномерного массива. A[i] понимается как &A[i][0], то есть начальный адрес i-го массива символов.
От такого многообразия возможностей работы с указателями нетрудно прийти в замешательство: как вообще с ними работать, кто за что отвечает? Действительно, при работе с указателями легко выйти «за рамки дозволенного», т.е. определенных самим же программистом структур данных. Поэтому попробуем еще раз обсудить принципиальные моменты адресной арифметики.
· наличие операции инкремента или индексации говорит о работе указателя с памятью (массивом) ;
· использование исключительно операции косвенного обращения по указателю свидетельствует о работе с отдельной переменной.
· неинициализированный указатель. После определения указатель ссылается «в никуда», тем не менее программист работает через него с переменной или массивом, записывая данные по случайным адресам;
· несколько указателей, ссылающихся на общий массив – это все-таки один массив, а не несколько. Если программа работает с несколькими массивами, то они должны либо создаваться динамически, либо браться из двумерного массива;
· выход указателя за границы памяти. Например, конец строки отмечается символов ‘\0’, начало же формально соответствует начальному положению указателя. Если в процессе работы со строкой требуется возвращение на ее начало, то начальный указатель необходимо запоминать, либо дополнительно отсчитывать символы.
Другие операции над указателями
В процессе определения указателей мы рассмотрели основные операции над ними:
· операция присваивания указателей одного типа. Назначение указателю адреса переменной p=&a есть одни из вариантов такой операции;
· операция косвенного обращения по указателю (разыменования указателя);
· операция адресной арифметики «указатель+целое» и все производные от нее.
Значение NULL может быть присвоено любому указателю. Если указатель по логике работы программы может иметь такое значение, то перед косвенным обращением по нему его нужно проверять на достоверность:
//— Симметричная перестановка символов строки
< char c; c=*p; *p=*q; *q=c; >// 3 стакана над переменными под указателями
extern int fread(void *, int, int, FILE *);
fread(A, sizeof(int), 20, fd);
extern void *malloc(int);
int *p = (int*)malloc(sizeof(int)*20); // Явное преобразование void* к int*
Указатель как формальный параметр и результат функции
Если же фактический параметр должен быть изменен, то формальный параметр можно определить как явный указатель. Тогда фактический параметр должны быть явно передан в виде указателя на ту переменную (с использованием операции &).
< (* pi )++; >// аналог вызова: pi = & a
inc (& a ); > // *( pi )++ эквивалентно a ++
int sum(int A[],int n) // Исходная программа
int sum(int *p, int n) // Эквивалент с указателем
< x = sum(B,10); >// аналог вызова : p = B, n = 10
В вызове фигурирует идентификатор массива, который интерпретируется как указатель на начало. Поэтому типы формального и фактического параметров совпадают. Совпадают также оба варианта функций вплоть до генерируемого кода.
· глобальные переменные программы;
· формальные параметры, если они являются массивами, указателями или ссылками, то есть «за ними стоят» другие переменные.
Функция не может возвратить указатель на локальную переменную или формальный параметр-значение, поскольку они разрушаются при выходе из функции. Это приводит к ошибке времени выполнения, не обнаруживаемой транслятором.
Пример: функция возвращает указатель на минимальный элемент массива. Массив передается как формальный параметр.
int *min(int A[], int n)<
int *pmin, i; // Рабочий указатель, содержащий результат
Ссылка как неявный указатель
Во многих языках программирования указатель присутствует, но в завуалированном виде в форме ссылки. Под ссылкой понимается переменная, которая не имеет самостоятельного значения, а отображается на другую переменную, т.е. является ее синонимом. Во всем остальном она не отличается от обычной переменной. В отличие от явного указателя обращение по ссылке к объекту-прототипу имеет тот же самый синтаксис, что и обращение к объекту-прототипу.
int a =5; // Переменная – прототип
b ++; // Операция над b есть операция над прототипом a
· при передаче по значению формальный параметр является копией фактического, он может быть изменен независимо от значения оригинала – фактического параметра. Такой параметр является исключительно входным;
· при передаче по ссылке формальный параметр отображается на фактический, и его изменение сопровождается изменением фактического параметра-прототипа. Такой параметр может быть как входным, так и выходным.
//—— Функция возвращает ссылку на минимальный элемент массива
int &ref_min(int A[], int n)<
//—— Функция возвращает указатель на минимальный элемент массива
int *ptr_min(int *p, int n)<
Строки, массивы символов и указатель char *
int n; // указателем на на строку char*
· создание массива символов с размерностью, достаточной для размещения строки;
· инициализацию (заполнение) массива символами строки, дополненной символом ‘\0’;
char * q = «ABCD»;; // Программа
char *q; // Эквивалент
В связи с этим в Си возможны довольно странные выражения с участием строковых констант:
char c2 = («12345» + 2)[1];
extern int strcmp(char *, char*);
Посмотрим, как все вышесказанное выглядит на практике.
//—- Поиск в строке заданного фрагмента
> // иначе продолжить поиск
Для обнаружения всех фрагментов достаточно передавать для каждого последующего вызова функции указатель на часть строки, непосредственно следующей за найденным фрагментом.
//—— Поиск всех вхождений фрагмента в строке
for (s=find(c,q); s!=NULL; s=find(s+strlen(q),q)) puts(s);
//—- Поиск слова максимальной длины посимвольная обработка
int n,lmax; char *pmax;
for (n=0,lmax=0,pmax=NULL; *s!=0;s++)<
n =0; // фиксация максимального значения
//—- Сортировка слов в строке в порядке убывания (выбором)
void sort(char *in, char *out)
* out ++= * q ; * q ++=’ ‘; // Переписать с затиранием
* out ++=’ ‘; // После слова добавить пробел
Лабораторный практикум
1. Функция находит минимальный элемент массива и возвращает указатель на него. С использованием этой функции реализовать сортировку выбором.
2. Шейкер-сортировка с использованием указателей на правую и левую границы отсортированного массива и сравнения указателей.
3. Функция находит в строке пары одинаковых фрагментов и возвращает указатель на первый. С помощью функции найти все пары одинаковых фрагментов.
4. Функция находит в строке пары инвертированных фрагментов (например «123apr» и «rpa321») и возвращает указатель на первый. С помощью функции найти все пары.
5. Функция производит двоичный поиск места размещения нового элемента в упорядоченном массиве и возвращает указатель на место включения нового элемента. С помощью функции реализовать сортировку вставками.
6. Функция находит в строке десятичные константы и заменяет их на шестнадцатеричные с тем же значением, например «aaaaa258xxx» на «aaaaa0x102xxx».
7. Функция находит в строке символьные константы и заменяет их на десятичные коды, например «aaa’6’xxx» на «aaa54xxx».
8. Функция находит в строке самое длинное слово и возвращает указатель на него. С ее помощью реализовать размещение слов в выходной строке в порядке убывания их длины.
9. Функция находит в строке самое первое (по алфавиту) слово. С ее помощью реализовать размещение слов в выходной строке в алфавитном порядке.
10. Функция находит в строке симметричный фрагмент вида » abcdcba » длиной 7 и более символов (не содержащий пробелов) и возвращает указатель на его начало и длину. С использованием функции «вычеркнуть» все симметричные фрагменты из строки.
11. «Быстрая» сортировка (разделением) с использованием указателей на правую и левую границы массива, текущих указателей на правый и левый элемент и операции сравнения указателей (см. 7.2).
Вопросы без ответов
Определите, используется ли указатель для доступа к отдельной переменной или к массиву. Напишите вызов функции с соответствующими фактическими параметрами – адресами переменных или именами массивов.
void F(int *p, int *q, int n)<
F(A,&x,5); printf(«x=%d\n»,x); > // Выведет 13