В предыдущих главах переменные были описаны как место в памяти компьютера, к которому можно получить доступ через идентификатор (имя переменной). Таким образом, программе не нужно заботиться о физическом адресе данных в памяти; она просто использует идентификатор каждый раз, когда необходимо обратиться к переменной.
Для программы на C++ память компьютера представляется как последовательность ячеек памяти, каждая из которых имеет размер в один байт, и каждая имеет свой уникальный адрес. Эти однобайтовые ячейки памяти упорядочены определенным образом, позволяющим интерпретировать данные размером более одного байта, занимая ячейки памяти, имеющие последовательные адреса.
Таким образом, каждая ячейка может быть легко расположена в памяти посредством своего уникального адреса. Например, ячейка памяти с адресом 1776 всегда следует непосредственно за ячейкой с адресом 1775 и предшествует ячейке с адресом 1777, а также находится точно на тысячу ячеек после 776 и точно на тысячу перед 2776.
Когда переменная объявлена, памяти, необходимой для хранения ее значения, назначается определенное место в памяти (ее адрес памяти). Обычно, программы на C++ не принимают активного участия в определении точного адреса памяти, в которой хранится переменная. К счастью, эта задача оставлена для среды, в которой запускается программа, — обычно это операционная система, которая определяет конкретные области памяти во время выполнения. Тем не менее, программе может быть полезно иметь возможность получить адрес переменной во время исполнения, для доступа к ячейкам данных, которые находятся в определенной позиции относительно этой переменной.
Оператор взятия адреса (&)
Адрес переменной можно получить, поставив перед именем переменной знак амперсанда (&), известный как оператор взятия адреса. Например:
foo = &myvar;
Это присвоит адрес переменной myvar переменной foo; ставя оператор взятия адреса (&) перед именем переменной myvar, мы присваиваем переменной foo не содержимое переменной myvar, а её адрес.
Фактический адрес переменной в памяти неизвестен до выполнения, однако, для выяснения некоторых понятий, давайте предположим, что myvar во время выполнения расположена в памяти по адресу 1776.
В этом случае, рассмотрим следующий фрагмент кода:
myvar = 25;
foo = &myvar;
bar = myvar;
Значения, содержащиеся в каждой переменной после выполнения этого кода, показаны на следующей диаграмме:
Сначала мы присвоили значение 25 переменной myvar (переменная, адрес которой мы предположили равным 1776).
Второе выражение присваивает переменной foo адрес переменной myvar, равный 1776.
Наконец, третье выражение присваивает значение, содержащееся в переменной myvar переменной bar. Это обычная операция присваивания, которая выполнялась множество раз в предыдущих главах.
Главное отличие между вторым и третьим выражениями — появление оператора взятия адреса (&).
Переменная, хранящая адрес другой переменной (как foo в предыдущем примере), это то, что в C++ называется указателем. Указателя являются очень мощным средством языка, которое имеет множество применений в низкоуровневом программировании. Немного позже мы увидим, как объявлять и использовать указатели.
Оператор разыменования (*)
Как только что сказано, переменная, хранящая адрес другой переменной, называется указателем. Говорят, что указатели «указывают на» переменную, адрес которой они хранят.
Интересным свойством указателей является то, что они могут быть использованы для доступа к переменной, на которую они указывают, напрямую. Это делается путём добавления перед именем указателя оператора разыменования (*). Сам оператор может быть прочтен как «значение, на которое указывает».
Следовательно, следуя значениям предыдущего примера, получим выражение:
baz = *foo;
Это может быть прочтено как «baz равен значению, на которое указывает foo», а выражение, в действительности, присвоит значение 25 переменной baz, т.к. foo равна 1776, а значение по адресу 1776 равно 25.
Важно четко различать, что foo ссылается на значение 1776, тогда как *foo (со звездочкой *, предшествующей идентификатору) ссылается на значение, хранящееся по адресу 1776, в данном случае это 25. Обратите внимание на разницу включения или не включения оператора разыменования (Я добавил пояснительный комментарий о том, как можно прочитать каждое из этих двух выражений).
baz = foo; // baz равен foo (1776)
baz = *foo; // baz равен значению, на которое указывает foo (25)
Операторы взятия адреса и разыменования, таким образом, дополняют друг друга:
& это оператор взятия адреса, и может быть прочтен просто как «адрес»
* это оператор разыменования, и может быть прочтен как «значение, на которое указывает»
Таким образом, они имеют своего рода противоположные значения: адрес, полученный с помощью &, может быть разыменован с помощью *.
Ранее мы выполняли следующие две операции присваивания:
myvar = 25;
foo = &myvar;
Сразу после этих двух выражений, все следующие выражения будут иметь значение true в качестве результата:
myvar == 25
&myvar == 1776
foo == 1776
*foo == 25
Первое выражение вполне понятно, учитывая, что была произведена операция присваивания myvar=25. Второе выражение использует оператор взятия адреса (&), который возвращает адрес myvar, который мы предположили равным 1776. Третье выражение очевидно, т.к. второе выражение было истинным, и была произведена операция присваивания foo=&myvar. Четвертое выражение использует оператор разыменования (*), который может быть прочтен как «значение, на которое указывает». а значение, на которое указывает foo в действительности 25.
Таким образом, после всего этого вы также можете сделать вывод, что до тех пор, пока адрес, на который указывает foo, остается неизменным, следующее выражение также будет истинным:
*foo == myvar
Объявление указателей
Благодаря способности указателя напрямую ссылаться на значение, на которое он указывает, указатель имеет разные свойства, когда он указывает на char или когда он указывает на int или float. После разыменования тип должен быть известен. И для этого в объявлении указателя должен быть указан тип данных, на который будет указывать указатель.
Объявление указателя имеет следующий синтаксис:
type * name;
где type — это тип данных, на которые будет ссылаться указатель. Этот тип является типом не самого указателя, а типом данных, на которые он ссылается. Например:
int * number;
char * character;
double * decimals;
Здесь объявлено три указателя. Каждый из них предназначен для указания на различные типы данных, но, в действительности, каждый из них является указателем и все они, вероятно, будут занимать одинаковое количество памяти (размер указателя в памяти зависит от платформы, на которой работает программа). Тем не менее, данные, на которые они указывают, не занимают одинаковое количество памяти и не имеют одинаковый тип: первый указывает на int, второй на char, а третий на double. Следовательно, хотя все эти три переменных являются указателями, в действительности, они имеют различные типы: int*, char* и double* соответственно, в зависимости от типа, на который они указывают.
Обратите внимание, что звездочка (*), используемая при объявлении указателя, означает только то, что это указатель (она является частью составного спецификатора его типа), и её не нужно путать с оператором разыменования, описанным немного ранее, который также записывается как звездочка (*). Это просто две разные вещи, представленные одним и тем же знаком.
Давайте посмотрим на пример с указателями:
// больше указателей
#include <iostream>
using namespace std;
int main ()
{
int firstvalue = 5, secondvalue = 15;
int * p1, * p2;
p1 = &firstvalue; // p1 = адрес firstvalue
p2 = &secondvalue; // p2 = адрес secondvalue
*p1 = 10; // значение, адресуемое p1 = 10
*p2 = *p1; // значение, адресуемое p2 = значению, адресуемому p1
p1 = p2; // p1 = p2 (копируется значение указателя)
*p1 = 20; // значение, адресуемое p1 = 20
cout << "firstvalue is " << firstvalue << '\n';
cout << "secondvalue is " << secondvalue << '\n';
return 0;
}
firstvalue is 10
secondvalue is 20
Каждая операция присваивания включает комментарий о том, как можно прочитать каждую строку: т.е. амперсанды (&) заменены на «адрес», а звездочки (*) на «значение, адресуемое».
Обратите внимание, что существуют выражения с указателями p1 и p2, как с оператором разыменования (*), так и без него. Значение выражения, использующего оператор разыменования (*), очень отличается от выражения, не использующего его. Когда этот оператор предшествует имени указателя, выражение ссылается на значение по адресу указателя, тогда как без него, выражение ссылается на значение самого указателя (т.е. адрес, на который указывает указатель).
Ещё одна строка, которая может привлечь ваше внимание:
int * p1, * p2;
Она объявляет два указателя, использованные в предыдущем примере. Но обратите внимание, что для каждого указателя есть звездочка (*), чтобы оба указателя имели тип int * (указатель на int). Это требование обусловлено правилами приоритета. Обратите внимание, что если вместо этого код был таким:
int * p1, p2;
p1 действительно будет иметь тип int *, но p2 будет иметь тип int. Пробелы не имеют значения в данном контексте. Но в любом случае, просто помнить о том, чтобы поставить одну звездочку на указатель, достаточно при объявлении множества указателей в одном выражении. Или даже лучше: используйте разные выражения для каждой переменной.
Указатели и массивы
Концепция массивов связана с концепцией указателей. На самом деле, массивы очень похожи на указатели на свои первые элементы, и, фактически, массив всегда может быть неявно преобразован в указатель соответствующего типа. Например, рассмотрим эти два объявления:
int myarray [20];
int * mypointer;
Следующая операция присваивания будет корректной:
myarray = mypointer;
Давайте посмотрим на пример, который смешивает массивы и указатели:
// больше указателей
#include <iostream>
using namespace std;
int main ()
{
int numbers[5];
int * p;
p = numbers; *p = 10;
p++; *p = 20;
p = &numbers[2]; *p = 30;
p = numbers + 3; *p = 40;
p = numbers; *(p+4) = 50;
for (int n=0; n<5; n++)
cout << numbers[n] << ", ";
return 0;
}
10, 20, 30, 40, 50,
Указатели и массивы поддерживают одинаковый набор операций с одинаковым смыслом для обоих. Основное отличие состоит в том, что указателям могут быть назначены новые адреса, а массивам — нет.
В главе о массивах скобки ( [ ] ) были объяснены как указание на индекс элемента массива. На самом деле эти скобки — это оператор разыменования, известный как оператор смещения. Они разыменовывают переменную, за которой следуют, так же, как и *, но они также добавляют число в скобках к адресу, на который делается разыменование. Например:
a[5] = 0; // a [смещение на 5] = 0
*(a+5) = 0; // адресуемый через (a+5) = 0
Эти два выражения эквивалентны и допустимы не только если a является указателем, но также и если a является массивом. Помните, имя массива можно использовать так же, как указатель на его первый элемент.
Инициализация указателей
Указатели могут быть инициализированы так, чтобы указывать на определенное место в момент определения:
int myvar;
int * myptr = &myvar;
Результирующее состояние переменных после этого кода такое же, как после:
int myvar;
int * myptr;
myptr = &myvar;
Когда указатели инициализируются, инициализируется адрес, на который они указывают (то есть myptr), а не указываемое значение (то есть * myptr). Поэтому приведенный выше код не следует путать с:
int myvar;
int * myptr;
*myptr = &myvar;
Что в любом случае не имеет большого смысла (и не является корректным кодом).
Звездочка ( * ) в объявлении указателя (строка 2) показывает только то, что это указатель, а не оператор разыменования (как в строке 3). В обоих случаях просто используется один и то же символ: *. Как всегда, пробелы не имеют значения и никогда не изменяют смысла выражения.
Указатели могут быть инициализированы либо по адресу переменной (например, в приведенном выше случае), либо по значению другого указателя (или массива):
int myvar;
int *foo = &myvar;
int *bar = foo;
Адресная арифметика
Выполнение арифметических операций с указателями немного отличается от выполнения их с обычными целочисленными типами. Для начала, разрешены только операции сложения и вычитания; другие не имеют смысла в мире указателей. Но и сложение, и вычитание ведут себя немного иначе с указателями в зависимости от размера типа данных, на который они указывают.
Когда были представлены основные типы данных, мы увидели, что типы имеют разные размеры. Например: char всегда имеет размер 1 байт, short обычно больше него, а int и long еще больше; точный размер зависит от системы. Например, давайте представим, что в данной системе char занимает 1 байт, short занимает 2 байта, а long 4.
Предположим теперь, что мы определили три указателя в этом компиляторе:
char *mychar;
short *myshort;
long *mylong;
и что мы знаем, что они указывают на ячейки памяти 1000, 2000 и 3000 соответственно.
Следовательно, если мы напишем:
++mychar;
++myshort;
++mylong;
mychar, как и следовало ожидать, будет содержать значение 1001. Но не так очевидно, что myshort будет содержать значение 2002, а mylong будет содержать 3004, хотя каждый из них был увеличен только один раз. Причина заключается в том, что при добавлении единицы к указателю, он будет указывать на следующий элемент того же типа, и поэтому к указателю добавляется размер в байтах того типа, на который он указывает.
Это применимо как при сложении, так и при вычитании любого числа в указателе. Это произошло бы точно так же, если бы мы написали:
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;
Что касается операторов инкремента (++) и декремента (--), они оба могут использоваться как префикс или суффикс выражения, с небольшим различием в поведении: в качестве префикса инкремент происходит до того, как вычислено выражение, а в качестве суффикса инкремент происходит после того, как вычислено выражение. Это также относится к выражениям, увеличивающим и уменьшающим указатели, которые могут стать частью более сложных выражений, которые также включают операторы разыменования (*). Вспоминая правила приоритета операторов, мы можем вспомнить, что постфиксные операторы, такие как инкремент и декремент, имеют более высокий приоритет, чем префиксные операторы, такие как оператор разыменования (*). Следовательно, следующее выражение:
*p++
эквивалентно *(p++). Это увеличивает значение p (так что теперь оно указывает на следующий элемент), но так как используется постфиксная запись оператора ++, все выражение вычисляется как значение, на которое указывал указатель изначально (адрес, на который он указывал до инкремента).
По сути, это четыре возможные комбинации оператора разыменования с префиксной и суффиксной версиями оператора инкремента (то же самое применимо и к оператору декремента):
*p++ // same as *(p++): инкремент указателя, разыменование изначального адреса
*++p // same as *(++p): инкремент указателя, разыменование увеличенного адреса
++*p // same as ++(*p): разыменование указателя, инкремент значения, на которое он указывает
(*p)++ // разыменование указателя, пост-инкремент значения, на которое он указывает
Типичное, но не очень простое выражение, включающее эти операторы:
*p++ = *q++;
Поскольку ++ имеет более высокий приоритет, чем *, оба значения p и q увеличиваются, но поскольку оба оператора инкремента (++) используются в качестве постфикса, а не префикса, значение, присваиваемое *p, равно *q до увеличения как p, так и q. А затем оба увеличиваются. Это было бы примерно эквивалентно:
*p = *q;
++p;
++q;
Как всегда, круглые скобки уменьшают путаницу, добавляя читаемость в выражения.
Указатели и const
Указатели могут использоваться для доступа к переменной по ее адресу, и этот доступ может включать изменение значения, на которое они указывают. Но также возможно объявить указатели, которые могут получить доступ к указанному значению, чтобы прочитать его, но не изменить его. Для этого достаточно обозначить тип, на который указывает указатель, как const. Например:
int x;
int y = 10;
const int * p = &y;
x = *p; // ok: чтение p
*p = x; // ошибка: изменение p, объявленного с квалификатором const
Здесь p указывает на переменную, но указывает на нее константно, что означает, что он может прочитать указанное значение, но не может изменить его. Также обратите внимание, что выражение &y имеет тип int*, но оно присваивается указателю типа const int*. Это разрешено: указатель на не-константу может быть неявно преобразован в указатель на константу. Но не наоборот! Из соображений безопасности указатели на const неявно не конвертируются в non-const указатели.
Один из вариантов использования указателей на константные элементы — это параметры функции: функция, которая принимает указатель на non-const в качестве параметра, может изменить значение, переданное в качестве аргумента, а функция, которая принимает указатель на const в качестве параметра, не может.
// указатели как аргументы:
#include <iostream>
using namespace std;
void increment_all (int* start, int* stop)
{
int * current = start;
while (current != stop) {
++(*current); // инкремент адресуемого значения
++current; // инкремент указателя
}
}
void print_all (const int* start, const int* stop)
{
const int * current = start;
while (current != stop) {
cout << *current << '\n';
++current; // инкремент указателя
}
}
int main ()
{
int numbers[] = {10,20,30};
increment_all (numbers,numbers+3);
print_all (numbers,numbers+3);
return 0;
}
11
21
31
Обратите внимание, что print_all использует указатели, которые указывают на константные элементы. Эти указатели указывают на константное содержимое, которое они не могут изменить, но сами они не являются константами: то есть указатели все еще могут увеличиваться или назначаться другим адресам, хотя они не могут изменять содержимое, на которое они указывают.
И здесь к указателям добавляется второе измерение константности: указатели также могут быть константами. И это указывается добавлением const к указанному типу (после звездочки):
int x;
int * p1 = &x; // неконстантный указатель на int
const int * p2 = &x; // неконстантный указатель на const int
int * const p3 = &x; // константный указатель на int
const int * const p4 = &x; // константный указатель на const int
Синтаксис с const и указателями определенно сложен, и для распознавания случаев, которые лучше всего подходят для каждого случая использования, требуется некоторый опыт. В любом случае, важно увидеть константность с указателями (и ссылками) как можно скорее, но вам не следует слишком беспокоиться о том, чтобы понять все, если вы впервые сталкиваетесь с сочетанием констант и указателей. Другие варианты использования будут показаны в следующих главах.
Чтобы еще больше запутать синтаксис const с указателями, квалификатор const может либо предшествовать, либо следовать за указанным типом с одним и тем же смыслом:
const int * p2a = &x; // неконстантный указатель на const int
int const * p2b = &x; // также неконстантный указатель на const int
Как и в случае пробелов, окружающих звездочку, порядок const в этом случае просто вопрос стиля. В этой главе используется префикс const, но оба они в точности эквивалентны. Достоинства каждого стиля до сих пор активно обсуждаются в Интернете.
Указатели и строковые литералы
Как указывалось ранее, строковые литералы — это массивы, содержащие символьные последовательности, заканчивающиеся нулем. В предыдущих разделах строковые литералы использовались для непосредственной вставки в cout, для инициализации строк и для инициализации массивов символов.
Но они также могут быть доступны напрямую. Строковые литералы — это массивы соответствующего типа, которые содержат все его символы плюс завершающий нулевой символ, причем каждый из элементов имеет тип const char (как литералы, они никогда не могут быть изменены). Например:
const char * foo = "hello";
Это объявляет массив с литеральным представлением для «hello», а затем указатель на его первый элемент присваивается foo. Если мы представим, что «hello» хранится в ячейках памяти, которые начинаются с адреса 1702, мы можем представить предыдущее объявление как:
Обратите внимание, что здесь foo является указателем и содержит значение 1702, а не «h» или «hello», хотя 1702 действительно является адресом обоих.
Указатель foo указывает на последовательность символов. И поскольку указатели и массивы ведут себя по существу одинаково в выражениях, foo может использоваться для доступа к символам таким же образом, как массивы символов с нулем на конце. Например:
*(foo+4)
foo[4]
Оба выражения имеют значение «o» (пятый элемент массива).
Указатели на указатели
C++ позволяет использовать указатели, которые указывают на указатели, которые, в свою очередь, указывают на данные (или даже на другие указатели). Синтаксис просто требует звездочки (*) для каждого уровня косвенности в объявлении указателя:
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;
Предполагая, что ячейки памяти для каждой переменной 7230, 8092 и 10502 выбраны случайно, это может быть представлено как:
При этом значение каждой переменной представлено внутри соответствующей ей ячейки, а их соответствующие адреса в памяти представлены значением под ними.
Новым в этом примере является переменная c, которая является указателем на указатель и может использоваться на трех разных уровнях косвенности, каждый из которых будет соответствовать своему значению:
- c типа char** и значением 8092
- *c типа char* и значением 7230
- **c типа char и значением ’z’
Указатели на void
Указатель типа void — это специальный тип указателя. В C ++ void представляет отсутствие типа. Следовательно, указатели void — это указатели, которые указывают на значение, которое не имеет типа (и, следовательно, также имеет неопределенную длину и неопределенные свойства разыменования).
Это дает указателям void большую гибкость, поскольку позволяет указывать на любой тип данных, от целочисленного значения или с плавающей точкой до строки символов. В обмен на это у них есть большое ограничение: данные, на которые они указывают, не могут быть напрямую разыменованы (что логично, поскольку у нас нет типа для разыменования), и по этой причине, любой адрес в указателе на void должен быть преобразован в некоторый другой тип указателя, который указывает на конкретный тип данных перед разыменованием.
Одним из возможных применений может быть передача обобщенных параметров функции. Например:
#include <iostream>
using namespace std;
void increase (void* data, int psize)
{
if ( psize == sizeof(char) )
{ char* pchar; pchar=(char*)data; ++(*pchar); }
else if (psize == sizeof(int) )
{ int* pint; pint=(int*)data; ++(*pint); }
}
int main ()
{
char a = 'x';
int b = 1602;
increase (&a,sizeof(a));
increase (&b,sizeof(b));
cout << a << ", " << b << '\n';
return 0;
}
y, 1603
sizeof — это оператор, интегрированный в язык C ++, который возвращает размер в байтах своего аргумента. Для нединамических типов данных это значение является константой. Поэтому, например, sizeof (char) равен 1, потому что char всегда имеет размер в один байт.
Недействительные и нулевые указатели
В принципе, указатели предназначены для указания на действительные адреса, такие как адрес переменной или адрес элемента в массиве. Но указатели могут фактически указывать на любой адрес, включая адреса, которые не ссылаются ни на один допустимый элемент. Типичными примерами этого являются неинициализированные указатели и указатели на несуществующие элементы массива:
int * p; // неинициализированный указатель (локальная переменная)
int myarray[10];
int * q = myarray+20; // элемент за границами массива
Ни p, ни q не указывают на адреса, о которых известно, что они содержат значение, но ни одно из приведенных выше выражений не приводит к ошибке. В C++ указатели могут принимать любое значение адреса, независимо от того, есть ли что-то по этому адресу или нет. Что может вызвать ошибку при разыменовании такого указателя (т.е. фактически при получении доступа к значению, на которое он указывает). Доступ к такому указателю вызывает неопределенное поведение — от ошибки во время выполнения до доступа к некоторому случайному значению.
Но иногда указатель действительно должен явно никуда не указывать, а не просто указывать на неверный адрес. Для таких случаев существует специальное значение, которое может принимать любой тип указателя: нулевое значение указателя. Это значение может быть выражено в C++ двумя способами: либо с целочисленным значением, равным нулю, либо с ключевым словом nullptr:
int * p = 0;
int * q = nullptr;
Здесь и p, и q являются нулевыми указателями, это означает, что они явно никуда не указывают, и они фактически равны между собой: все нулевые указатели равны любым другим нулевыми указателям. Также привычно видеть, что константа NULL используется в старом коде для ссылки на значение нулевого указателя:
int * r = NULL;
NULL объявлен в нескольких заголовочных файлах стандартной библиотеки и определяется как псевдоним некоторого значения константы нулевого указателя (например, 0 или nullptr).
Не путайте нулевые указатели с указателями на void! Нулевой указатель — это значение, которое любой указатель может принять для представления того, что он указывает на «никуда», в то время как указатель на void — это тип указателя, который может указывать куда-то без определенного типа. Первый относится к значению, хранящемуся в указателе, а второй — к типу данных, на которые он указывает.
Указатели на функции
C++ позволяет работать с указателями на функции. Типичное использование — передача функции в качестве аргумента другой функции. Указатели на функции объявляются с тем же синтаксисом, что и обычное объявление функции, за исключением того, что имя функции заключено в круглые скобки (), а перед именем стоит звездочка (*):
// указатели на функции
#include <iostream>
using namespace std;
int addition (int a, int b)
{ return (a+b); }
int subtraction (int a, int b)
{ return (a-b); }
int operation (int x, int y, int (*functocall)(int,int))
{
int g;
g = (*functocall)(x,y);
return (g);
}
int main ()
{
int m,n;
int (*minus)(int,int) = subtraction;
m = operation (7, 5, addition);
n = operation (20, m, minus);
cout <<n;
return 0;
}
8
В приведенном выше примере minus — указатель на функцию, имеющую два параметра типа int. Он непосредственно инициализируется для указания на функцию subtraction:
int (* minus)(int,int) = subtraction;