1776
всегда следует сразу после ячейки с адресом1775
и предшествует одному 1777
, и составляет ровно одну тысячу ячеек после 776
и ровно одну тысячу ячеек раньше 2776
.Когда объявляется переменная, памяти, необходимой для сохранения ее значения, назначается определенное место в памяти (адрес памяти). Как правило, C ++-программы не принимают активного решения точные адреса памяти, где хранятся его переменные. К счастью, эта задача остается в среде, в которой запускается программа, — как правило, операционная система, которая решает конкретные ячейки памяти во время выполнения. Однако может оказаться полезным, чтобы программа могла получить адрес переменной во время выполнения, чтобы получить доступ к ячейкам данных, которые находятся в определенной позиции относительно нее.
Адрес-оператора (&)
Адрес переменной можно получить, указав имя переменной с символом амперсанда ( &
), известным как адрес-оператора . Например:
| |
Это будет присваивать адрес переменной myvar
к foo
; предшествуя имени переменной myvar
с адресом-operator ( &
), мы больше не назначаем содержимое самой переменной foo
, а ее адрес.
Фактический адрес переменной в памяти не может быть известен до выполнения, но предположим, чтобы помочь прояснить некоторые концепции, которые myvar
помещаются во время выполнения в адрес памяти 1776
.
В этом случае рассмотрим следующий фрагмент кода:
| |
Значения , содержащиеся в каждой переменной после выполнения этого, показаны в следующей схеме: Во-
первых, мы присвоили значение 25
для myvar
(переменной, адрес которой в памяти мы предположили , чтобы быть 1776
).
Второй оператор присваивает foo
адрес myvar
, который мы предположили 1776
.
И, наконец, третье утверждение, присваивает значение , содержащееся в myvar
к bar
. Это стандартная операция присваивания, как это уже делалось много раз в предыдущих главах.
Основное отличие второго и третьего операторов — появление адреса-оператора ( &
).
Переменная, которая хранит адрес другой переменной (например,foo
в предыдущем примере) — это то, что в C ++ называется указателем . Указатели — очень мощная функция языка, который имеет много применений в программировании более низкого уровня. Немного позже мы увидим, как объявлять и использовать указатели.
Оператор разыменования (*)
Как видно, переменная, которая хранит адрес другой переменной, называется указателем . Указатели, как говорят, «указывают» на переменную, адрес которой они хранят.
Интересным свойством указателей является то, что они могут использоваться для доступа к переменной, на которую они указывают напрямую. Это делается путем указания имени указателя с помощью оператора разыменования ( *
). Сам оператор можно считать «значением, на которое указывает».
Поэтому, следуя значениям предыдущего примера, следующий оператор:
| |
Это может быть истолковано как: « baz
равно значению , адресуемый параметром foo
», и заявление будет фактически присвоить значение 25
для baz
, так как foo
есть 1776
, а величина , на которую указывает 1776
(после приведенного выше примера) будет 25
.
Важно четко различать, что foo
относится к значению 1776
, в то время как *foo
(со звездочкой, *
предшествующей идентификатору) относится к значению, хранящемуся по адресу 1776
, что в этом случае 25
. Обратите внимание на разницу включения или отсутствия оператора разыменования (я добавил пояснительный комментарий о том, как можно прочитать каждое из этих двух выражений):
| |
Таким образом, ссылочные и разыменованные операторы дополняют друг друга:
&
является адресом оператора , и его можно просто читать как «адрес»,*
является оператором разыменования и может считаться «значением, на которое указывает« значение »,
Таким образом, они имеют противоположные значения: Полученный адрес &
может быть разыменован *
.
Ранее мы выполнили следующие две операции присваивания:
| |
Сразу после этих двух утверждений все приведенные ниже выражения дали бы результат:
| |
Первое выражение достаточно ясно, учитывая, что выполняемая операция присваивания myvar
была myvar=25
. Второй использует адрес-оператора ( &
), который возвращает адрес myvar
, который мы предположили, что он имеет значение 1776
. Третий несколько очевидна, так как второе выражение верно и операция присваивания выполняется на foo
было foo=&myvar
. В четвертом выражении используется оператор разыменования ( *
), который можно читать как «значение, на которое указывает», и значение, на которое указывает, foo
действительно 25
.
Итак, после всего этого вы также можете сделать вывод о том, что до тех пор, пока адрес, на который указывает, foo
остается неизменным, будет также выполняться следующее выражение:
| |
Объявление указателей
Из-за способности указателя напрямую ссылаться на значение, на которое оно указывает, указатель имеет разные свойства, когда указывает на a, char
чем когда он указывает на a int
или a float
. После разыменования тип должен быть известен. И для этого в декларации указателя должен быть указан тип данных, на который указывает указатель.
Объявление указателей следует этому синтаксису:
type * name;
где type
тип данных, на который указывает указатель. Этот тип не является типом самого указателя, а типом данных, на которые указывает указатель. Например:
| |
Это три объявления указателей. Каждый из них предназначен для указания на другой тип данных, но на самом деле все они являются указателями, и все они, вероятно, будут занимать одинаковое пространство в памяти (размер в памяти указателя зависит от платформы где выполняется программа). Тем не менее, данные, на которые они указывают, не занимают одинаковое пространство и не имеют одного и того же типа: первый указывает на a int
, второй на a char
и последний на a double
. Поэтому, хотя эти три переменные пример все из них указатели, они на самом деле имеют различные типы: int*
, char*
, и , double*
соответственно, в зависимости от типа они указывают.
Обратите внимание, что звездочка (*
), используемый при объявлении указателя, означает только то, что он является указателем (он является частью спецификатора типа type), и его не следует путать с оператором разыменования, который был замечен раньше, но который также написан со звездочкой ( *
). Это просто две разные вещи, представленные одним и тем же знаком.
Давайте посмотрим пример на указатели:
| | firstvalue - 10 второе значение - 20 | Редактировать и запустить |
Обратите внимание: несмотря на то, что ни одна из них firstvalue
не secondvalue
имеет прямого значения в программе, оба имеют значение, установленное косвенно посредством использования mypointer
. Вот как это происходит: во-
первых, mypointer
присваивается адрес firstvalue, используя адрес-оператора ( &
). Затем этому значению mypointer
присваивается значение 10
. Поскольку в данный момент mypointer
указывается на расположение памяти firstvalue
, это фактически изменяет значение firstvalue
.
Чтобы продемонстрировать, что указатель может указывать на разные переменные в течение его жизненного цикла в программе, пример повторяет процесс с secondvalue
и тот же указатель mypointer
.
Вот пример немного более подробно:
| | firstvalue - 10 второе значение - 20 | Редактировать и запустить |
Каждая операция присваивания включает комментарий о том, как каждая строка может быть прочитана: т. Е. Замена амперсандов ( &
) на «адрес» и звездочки ( *
) на «значение, на которое указывает».
Обратите внимание, что существуют выражения с указателями p1
и p2
, как с, так и без оператора разыменования( *
). Значение выражения, использующего оператор разыменования (*), сильно отличается от значения, которое нет. Когда этот оператор предшествует имени указателя, выражение ссылается на указанное значение, тогда как при указании имени указателя без этого оператора оно относится к значению самого указателя (то есть к адресу, на который указывает указатель).
Еще одна вещь, которая может привлечь ваше внимание, — это линия:
| |
Это объявляет два указателя, используемые в предыдущем примере. Но обратите внимание, что *
для каждого указателя есть звездочка ( ), чтобы у обоих был тип int*
(указатель на int
). Это необходимо из-за правил приоритета. Обратите внимание, что если вместо этого код был:
| |
p1
действительно будет иметь тип int*
, но p2
будет иметь тип int
. Пространства для этой цели не имеют значения. Но в любом случае просто помнить о том, чтобы поставить одну звездочку на указатель, достаточно для большинства пользователей-указателей, заинтересованных в объявлении нескольких указателей на оператор. Или еще лучше: используйте разные выражения для каждой переменной.
Указатели и массивы
Понятие массивов связано с понятием указателей. Фактически, массивы работают очень сильно, как указатели на их первые элементы, и, фактически, массив всегда может быть неявно преобразован в указатель правильного типа. Например, рассмотрите эти два объявления:
| |
Будет выполняться следующая операция присваивания:
| |
После этого, mypointer
и myarray
было бы равнозначно и имели бы очень схожие свойства. Основное различие заключается в том, что ему mypointer
может быть присвоен другой адрес, тогда как myarray
никогда нельзя назначать что-либо и всегда будет представлять собой один и тот же блок из 20 элементов типа int
. Поэтому следующее присваивание недействительно:
| |
Давайте посмотрим пример, который смешивает массивы и указатели:
| | 10, 20, 30, 40, 50, | Редактировать и запустить |
Указатели и массивы поддерживают один и тот же набор операций с тем же значением для обоих. Основное отличие состоит в том, что указателям можно назначать новые адреса, а массивы — нет.
В главе о массивах скобки ( []
) объяснялись как указание индекса элемента массива. Ну, на самом деле эти скобки являются оператором разыменования, известным как оператор смещения . Они разыгрывают переменную, которой они следуют так же, как и *
она, но также добавляют число между скобками к адресу, который разыменован. Например:
| |
Эти два выражения эквивалентны и действительны, а не только если a
это указатель, но также и a
является массивом. Помните, что если массив, его имя можно использовать так же, как указатель на его первый элемент.
Инициализация указателя
Указатели могут быть инициализированы, чтобы указывать на определенные местоположения в тот момент, когда они определены:
| |
Полученное состояние переменных после этого кода такое же, как после:
| |
Когда указатели инициализируются, инициализируется адрес, на который они указывают (т. Е. myptr
), Но никогда не указывающее значение (т. Е. *myptr
). Поэтому приведенный выше код не следует путать с:
| |
Который в любом случае не имеет смысла (и не является допустимым кодом).
Звездочка ( *
) в объявлении указателя (строка 2) указывает только, что это указатель, это не оператор разыменования (как в строке 3). Обе вещи просто случаются использовать один и тот же знак: *
. Как всегда, пробелы не имеют значения и никогда не изменяют значение выражения.
Указатели могут быть инициализированы либо по адресу переменной (например, в случае выше), либо по значению другого указателя (или массива):
| |
Арифметика указателей
Для выполнения арифметических операций с указателями немного отличается от того, чтобы проводить их по обычным целым типам. Во-первых, допускаются только операции сложения и вычитания; другие не имеют смысла в мире указателей. Но как сложение, так и вычитание имеют несколько иное поведение с указателями, в зависимости от размера типа данных, на который они указывают.
Когда были введены основные типы данных, мы увидели, что типы имеют разные размеры. Например: char
всегда имеет размер 1 байт, short
обычно больше этого int
и long
еще больше; точный размер которых зависит от системы. Например, предположим, что в данной системе char
занимает 1 байт, short
занимает 2 байта и long
занимает 4.
Предположим теперь, что мы определяем три указателя в этом компиляторе:
| |
и что мы знаем , что они указывают на ячейки памяти 1000
, 2000
и 3000
, соответственно.
Поэтому, если мы напишем:
| |
mychar
, как и следовало ожидать, будет содержать значение 1001. Но не так очевидно, myshort
будет содержать значение 2002 и mylong
будет содержать 3004, хотя каждый из них будет увеличиваться только один раз. Причина в том, что при добавлении одного к указателю указатель делается для указания на следующий элемент того же типа, и, следовательно, размер в байтах типа, который он указывает, добавляется к указателю.
Это применимо как при добавлении, так и вычитании любого числа в указатель. Это было бы точно так же, если бы мы писали:
| |
Что касается операторов increment ( ++
) и decment ( --
), они оба могут использоваться как префикс или суффикс выражения с небольшой разницей в поведении: в качестве префикса приращение происходит до того, как выражение оценивается, и как суффикс, приращение происходит после вычисления выражения. Это также относится к выражениям, увеличивающим и уменьшающим указатели, которые могут стать частью более сложных выражений, которые также включают операции разыменования ( *
). Помня о правилах приоритетов операторов, мы можем вспомнить, что операторы постфикса, такие как приращение и декремент, имеют более высокий приоритет, чем префиксные операторы, такие как оператор разыменования ( *
). Следовательно, следующее выражение:
| |
эквивалентно *(p++)
. И то, что он делает, — это увеличить значение p
(поэтому теперь оно указывает на следующий элемент), но поскольку ++
используется как постфикс, все выражение оценивается как значение, указанное первоначально указателем (адрес, на который он указывал, перед тем, как увеличиваться ).
По сути, это четыре возможные комбинации оператора разыменования как с префиксными, так и с суффиксными версиями оператора инкремента (то же самое применимо и к оператору декремента):
| |
Типичный, но не простой оператор с этими операторами:
| |
Поскольку ++
имеет более высокий приоритет , чем *
, как p
и q
увеличивается, а потому , что оба операторы приращения ( ++
) используются в качестве постфикса и не префикса, значение , присвоенное *p
это *q
перед тем, как p
и q
увеличивается. И тогда оба увеличиваются. Это будет примерно эквивалентно:
| |
Как и всегда, круглые скобки уменьшают путаницу, добавляя разборчивость к выражениям.
Указатели и константа
Указатели могут использоваться для доступа к переменной по ее адресу, и этот доступ может включать в себя изменение указанного значения. Но также можно объявить указатели, которые могут получить доступ к указанному значению, чтобы прочитать его, но не изменять его. Для этого достаточно указать тип, на который указывает указатель const
. Например:
| |
Здесь p
указывает на переменную, но указывает на нее в const
-qualified образом, что означает, что она может читать указанное значение, но оно не может его изменить. Также обратите внимание, что выражение &y
имеет тип int*
, но это назначается указателю типа const int*
. Это разрешено: указатель на не-const может быть неявно преобразован в указатель на const. Но не наоборот! В качестве функции безопасности указатели на const
неявно конвертируются в указатели на non const
.
Один из вариантов использования указателей на const
элементы — это параметры функции: функция, которая принимает указатель на const
параметр не как параметр, может изменять значение, переданное как аргумент, тогда как функция, которая принимает указатель на const
параметр, не может.
| | 11 21 31 | Редактировать и запустить |
Обратите внимание, что print_all
использует указатели, указывающие на постоянные элементы. Эти указатели указывают на постоянный контент, который они не могут изменять, но они не являются постоянными: т. Е. Указатели все еще могут увеличиваться или назначаться разными адресами, хотя они не могут изменять содержимое, на которое они указывают.
И здесь к указателям добавляется второе измерение константы: указатели также могут быть самими константами. И это определяется добавлением const к указанному типу (после звездочки):
| |
Синтаксис с const
указателями и указателями определенно сложный, и распознавание случаев, которые наилучшим образом подходят для каждого использования, как правило, требует некоторого опыта. В любом случае важно получить константу с указателями (и ссылками) прямо раньше, чем позже, но вы не должны слишком беспокоиться о том, чтобы схватить все, если это первый раз, когда вы сталкиваетесь с миксами const
и указателями. В последующих главах появятся новые примеры использования.
Чтобы добавить немного больше путаницы к синтаксису const
с указателями, const
квалификатор может либо предшествовать, либо следовать за указанным типом, с тем же значением:
| |
Как и в случае пространств, окружающих звездочку, порядок const в этом случае является просто вопросом стиля. В этой главе используется префикс const
, поскольку по историческим причинам это кажется более расширенным, но оба они в точности эквивалентны. Достоинства каждого стиля по-прежнему интенсивно обсуждаются в Интернете.
Указатели и строковые литералы
Как указывалось ранее, строковые литералы представляют собой массивы, содержащие последовательности символов с нулевым символом. В предыдущих разделах строковые литералы использовались для непосредственного вставки cout
, для инициализации строк и для инициализации массивов символов.
Но к ним также можно получить доступ напрямую. Строковые литералы представляют собой массивы соответствующего типа массива, содержащие все его символы плюс завершающий нуль-символ, причем каждый из элементов имеет тип const char
(как литералы, они никогда не могут быть изменены). Например:
| |
Это объявляет массив с литеральным представлением "hello"
, а затем присваивается указатель на его первый элемент foo
. Если мы предположим, что "hello"
это хранится в ячейках памяти, которые начинаются с адреса 1702, мы можем представить предыдущее объявление как:
Обратите внимание, что здесь foo
указатель и содержит значение 1702, а не 'h'
, и даже "hello"
, хотя 1702 действительно является адресом обоих эти.
Указатель foo
указывает на последовательность символов. И поскольку указатели и массивы ведут себя по существу одинаково в выражениях, foo
их можно использовать для доступа к символам так же, как массивы последовательностей символов с нулевым символом. Например:
| |
Оба выражения имеют значение 'o'
(пятый элемент массива).
Указатели на указатели
C ++ позволяет использовать указатели, указывающие на указатели, что они, в свою очередь, указывают на данные (или даже на другие указатели). Синтаксис просто требует звездочки ( *
) для каждого уровня косвенности в объявлении указателя:
| |
Это, предполагая , что случайно выбранных ячеек памяти для каждой переменной 7230
, 8092
и 10502
, может быть представлена в виде:
При значении каждой переменной , представленной в соответствующей ячейке, а также их соответствующие адреса в памяти , представленное значением под ними.
Новая вещь в этом примере — переменная c
, которая является указателем на указатель и может использоваться в трех разных направлениях косвенности, каждая из которых будет соответствовать другому значению:
c
имеет типchar**
и значение8092
*c
имеет типchar*
и значение7230
**c
имеет типchar
и значение'z'
указатели void
void
Тип указателя представляет собой особый тип указателя. В C ++ void
— отсутствие типа. Поэтому void
указатели — это указатели, указывающие на значение, которое не имеет типа (и, следовательно, также неопределенная длина и неопределенные свойства разыменования).
Это дает void
указателям большую гибкость, указывая на любой тип данных, от целочисленного значения или поплавка до строки символов. Взамен они имеют большое ограничение: данные, на которые они указывают, не могут быть разыменованы напрямую (что логично, поскольку у нас нет типа для разыменования), и по этой причине любой адрес в void
указателе должен быть преобразован в некоторые другой тип указателя, который указывает на конкретный тип данных перед разыменованием.
Одно из его возможных применений может заключаться в передаче общих параметров функции. Например:
| | y, 1603 | Редактировать и запустить |
sizeof
является оператором, интегрированным в языке C ++, который возвращает размер в байтах его аргумента. Для нединамических типов данных это значение является константой. Поэтому, например, sizeof(char)
равно 1, потому char
что всегда имеет размер одного байта.
Недопустимые указатели и нулевые указатели
В принципе, указатели должны указывать на действительные адреса, такие как адрес переменной или адрес элемента в массиве. Но указатели могут фактически указывать на любой адрес, включая адреса, которые не относятся к какому-либо действительному элементу. Типичными примерами этого являются неинициализированные указатели и указатели на несуществующие элементы массива:
| |
Ни то, p
ни другое не q
указывают на адреса, которые, как известно, содержат значение, но ни одно из приведенных выше инструкций не вызывает ошибку. В C ++ указатели могут принимать любое значение адреса, независимо от того, есть ли на самом деле что-то на этом адресе или нет. То, что может вызвать ошибку, — это разыменовать такой указатель (т. Е. Фактически получить доступ к значению, на которое они указывают). Доступ к такому указателю вызывает неопределенное поведение, начиная от ошибки во время выполнения и заканчивая доступом к некоторому случайному значению.
Но, иногда, указателю действительно нужно явно указать на никуда, а не только на неверный адрес. Для таких случаев существует специальное значение, которое может принимать любой тип указателя: значение нулевого указателя . Это значение может быть выражено в C ++ двумя способами: либо с целым значением нуля, либо сnullptr
ключевое слово:
| |
Здесь оба p
и q
являются нулевыми указателями , а это означает, что они явно указывают на нигде, и оба они фактически сравнивают одинаковые: все нулевые указатели сравниваются с другими нулевыми указателями . Также довольно обычно видеть, что определенная константа NULL
используется в более раннем коде для обозначения значения нулевого указателя :
| |
NULL
определяется в нескольких заголовках стандартной библиотеки и определяется как псевдоним некоторого значения константы нулевого указателя (например, 0
или nullptr
).
Не путайте нулевые указатели с void
указателями! Нулевой указатель представляет собой значение , которое любой указатель может принимать представлять , что она указывает на «нигде», в то время как void
указатель является тип указателя , который может указывать куда — то без определенного типа. Один из них относится к значению, хранящемуся в указателе, а другой — к типу данных, на которые он указывает.
Указатели на функции
C ++ позволяет работать с указателями на функции. Типичным использованием этого является передача функции в качестве аргумента другой функции. Указатели на функции объявляются с тем же синтаксисом, что и объявление регулярной функции, за исключением того, что имя функции заключено между круглыми скобками (), а звездочка ( *
) вставлена перед именем:
| | 8 | Редактировать и запустить |
В приведенном выше примере minus
указатель на функцию, которая имеет два параметра типа int
. Он непосредственно инициализируется, чтобы указать на функцию subtraction
:
| |
Источник* на английском языке
Не совсем качественный перевод. Статья очень полезная и нужная!
Реально, первая статья, где человеческим языком все разложено по полочкам. Огромное спасибо!
Спасибо большое! Что хоть кто то читает мои повести!