В предыдущих главах переменные были объяснены как местоположения (ячейки) в памяти компьютера, к которым можно получить доступ по их идентификатору (их имени). Таким образом, программе не нужно заботиться о физическом адресе данных в памяти; он просто использует идентификатор всякий раз, когда ему нужно ссылаться на переменную.Для программы на C ++ память компьютера похожа на последовательность ячеек памяти, каждый побайтовый размер и каждый с уникальным адресом. Эти однобайтовые ячейки памяти упорядочены таким образом, что позволяет представления данных, превышающие один байт, занимать ячейки памяти, которые имеют последовательные адреса.Таким образом, каждая ячейка может быть легко расположена в памяти посредством ее уникального адреса. Например, ячейка памяти с адресом 1776всегда следует сразу после ячейки с адресом1775и предшествует одному 1777, и составляет ровно одну тысячу ячеек после 776и ровно одну тысячу ячеек раньше 2776.Когда объявляется переменная, памяти, необходимой для сохранения ее значения, назначается определенное место в памяти (адрес памяти). Как правило, C ++-программы не принимают активного решения точные адреса памяти, где хранятся его переменные. К счастью, эта задача остается в среде, в которой запускается программа, — как правило, операционная система, которая решает конкретные ячейки памяти во время выполнения. Однако может оказаться полезным, чтобы программа могла получить адрес переменной во время выполнения, чтобы получить доступ к ячейкам данных, которые находятся в определенной позиции относительно нее.

 

Адрес-оператора (&)

Адрес переменной можно получить, указав имя переменной с символом амперсанда ( &), известным как адрес-оператора . Например:

 
foo = &myvar;

Это будет присваивать адрес переменной myvarк foo; предшествуя имени переменной myvarс адресом-operator ( &), мы больше не назначаем содержимое самой переменной foo, а ее адрес.

Фактический адрес переменной в памяти не может быть известен до выполнения, но предположим, чтобы помочь прояснить некоторые концепции, которые myvarпомещаются во время выполнения в адрес памяти 1776.

В этом случае рассмотрим следующий фрагмент кода:

1
2
3
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. Обратите внимание на разницу включения или отсутствия оператора разыменования (я добавил пояснительный комментарий о том, как можно прочитать каждое из этих двух выражений):

1
2
baz = foo;   // baz equal to foo (1776)
baz = *foo;  // baz equal to value pointed to by foo (25)  

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

  • &является адресом оператора , и его можно просто читать как «адрес»,
  • *является оператором разыменования и может считаться «значением, на которое указывает« значение »,

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

Ранее мы выполнили следующие две операции присваивания:

1
2
myvar = 25;
foo = &myvar;

Сразу после этих двух утверждений все приведенные ниже выражения дали бы результат:

1
2
3
4
myvar == 25
&myvar == 1776
foo == 1776
*foo == 25

Первое выражение достаточно ясно, учитывая, что выполняемая операция присваивания myvarбыла myvar=25. Второй использует адрес-оператора ( &), который возвращает адрес myvar, который мы предположили, что он имеет значение 1776. Третий несколько очевидна, так как второе выражение верно и операция присваивания выполняется на fooбыло foo=&myvar. В четвертом выражении используется оператор разыменования ( *), который можно читать как «значение, на которое указывает», и значение, на которое указывает, fooдействительно 25.

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

 
*foo == myvar

 

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

Из-за способности указателя напрямую ссылаться на значение, на которое оно указывает, указатель имеет разные свойства, когда указывает на a, charчем когда он указывает на a intили a float. После разыменования тип должен быть известен. И для этого в декларации указателя должен быть указан тип данных, на который указывает указатель.

Объявление указателей следует этому синтаксису:

type * name;

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

1
2
3
int * number;
char * character;
double * decimals;

Это три объявления указателей. Каждый из них предназначен для указания на другой тип данных, но на самом деле все они являются указателями, и все они, вероятно, будут занимать одинаковое пространство в памяти (размер в памяти указателя зависит от платформы где выполняется программа). Тем не менее, данные, на которые они указывают, не занимают одинаковое пространство и не имеют одного и того же типа: первый указывает на a int, второй на a charи последний на a double. Поэтому, хотя эти три переменные пример все из них указатели, они на самом деле имеют различные типы: int*char*, и , double*соответственно, в зависимости от типа они указывают.

Обратите внимание, что звездочка (*), используемый при объявлении указателя, означает только то, что он является указателем (он является частью спецификатора типа type), и его не следует путать с оператором разыменования, который был замечен раньше, но который также написан со звездочкой ( *). Это просто две разные вещи, представленные одним и тем же знаком.

Давайте посмотрим пример на указатели:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// my first pointer #include <iostream> using namespace std;

int main ()
{
  int firstvalue, secondvalue;
  int * mypointer;

  mypointer = &firstvalue;
  *mypointer = 10;
  mypointer = &secondvalue;
  *mypointer = 20;
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}
firstvalue - 10
второе значение - 20
Редактировать и запустить

Обратите внимание: несмотря на то, что ни одна из них firstvalueне secondvalueимеет прямого значения в программе, оба имеют значение, установленное косвенно посредством использования mypointer. Вот как это происходит: во-

первых, mypointerприсваивается адрес firstvalue, используя адрес-оператора ( &). Затем этому значению mypointerприсваивается значение 10. Поскольку в данный момент mypointerуказывается на расположение памяти firstvalue, это фактически изменяет значение firstvalue.

Чтобы продемонстрировать, что указатель может указывать на разные переменные в течение его жизненного цикла в программе, пример повторяет процесс с secondvalueи тот же указатель mypointer.

Вот пример немного более подробно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// more pointers #include <iostream> using namespace std;

int main ()
{
  int firstvalue = 5, secondvalue = 15;
  int * p1, * p2;

  p1 = &firstvalue;  // p1 = address of firstvalue
  p2 = &secondvalue; // p2 = address of secondvalue
  *p1 = 10;          // value pointed to by p1 = 10
  *p2 = *p1;         // value pointed to by p2 = value pointed to by p1
  p1 = p2;           // p1 = p2 (value of pointer is copied)
  *p1 = 20;          // value pointed to by p1 = 20
  
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}
firstvalue - 10
второе значение - 20
Редактировать и запустить

Каждая операция присваивания включает комментарий о том, как каждая строка может быть прочитана: т. Е. Замена амперсандов ( &) на «адрес» и звездочки ( *) на «значение, на которое указывает».

Обратите внимание, что существуют выражения с указателями p1и p2, как с, так и без оператора разыменования*). Значение выражения, использующего оператор разыменования (*), сильно отличается от значения, которое нет. Когда этот оператор предшествует имени указателя, выражение ссылается на указанное значение, тогда как при указании имени указателя без этого оператора оно относится к значению самого указателя (то есть к адресу, на который указывает указатель).

Еще одна вещь, которая может привлечь ваше внимание, — это линия:

 
int * p1, * p2;

Это объявляет два указателя, используемые в предыдущем примере. Но обратите внимание, что *для каждого указателя есть звездочка ( ), чтобы у обоих был тип int*(указатель на int). Это необходимо из-за правил приоритета. Обратите внимание, что если вместо этого код был:

 
int * p1, p2;

p1действительно будет иметь тип int*, но p2будет иметь тип int. Пространства для этой цели не имеют значения. Но в любом случае просто помнить о том, чтобы поставить одну звездочку на указатель, достаточно для большинства пользователей-указателей, заинтересованных в объявлении нескольких указателей на оператор. Или еще лучше: используйте разные выражения для каждой переменной.

 

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

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

1
2
int myarray [20];
int * mypointer;

Будет выполняться следующая операция присваивания:

 
mypointer = myarray;

После этого, mypointerи myarrayбыло бы равнозначно и имели бы очень схожие свойства. Основное различие заключается в том, что ему mypointerможет быть присвоен другой адрес, тогда как myarrayникогда нельзя назначать что-либо и всегда будет представлять собой один и тот же блок из 20 элементов типа int. Поэтому следующее присваивание недействительно:

 
myarray = mypointer;

Давайте посмотрим пример, который смешивает массивы и указатели:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// more pointers #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, 
Редактировать и запустить

Указатели и массивы поддерживают один и тот же набор операций с тем же значением для обоих. Основное отличие состоит в том, что указателям можно назначать новые адреса, а массивы — нет.

В главе о массивах скобки ( []) объяснялись как указание индекса элемента массива. Ну, на самом деле эти скобки являются оператором разыменования, известным как оператор смещения . Они разыгрывают переменную, которой они следуют так же, как и *она, но также добавляют число между скобками к адресу, который разыменован. Например:

1
2
a[5] = 0;       // a [offset of 5] = 0
*(a+5) = 0;     // pointed to by (a+5) = 0  

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

 

Инициализация указателя

Указатели могут быть инициализированы, чтобы указывать на определенные местоположения в тот момент, когда они определены:

1
2
int myvar;
int * myptr = &myvar;

Полученное состояние переменных после этого кода такое же, как после:

1
2
3
int myvar;
int * myptr;
myptr = &myvar;

Когда указатели инициализируются, инициализируется адрес, на который они указывают (т. Е. myptr), Но никогда не указывающее значение (т. Е. *myptr). Поэтому приведенный выше код не следует путать с:

1
2
3
int myvar;
int * myptr;
*myptr = &myvar;

Который в любом случае не имеет смысла (и не является допустимым кодом).

Звездочка ( *) в объявлении указателя (строка 2) указывает только, что это указатель, это не оператор разыменования (как в строке 3). Обе вещи просто случаются использовать один и тот же знак: *. Как всегда, пробелы не имеют значения и никогда не изменяют значение выражения.

Указатели могут быть инициализированы либо по адресу переменной (например, в случае выше), либо по значению другого указателя (или массива):

1
2
3
int myvar;
int *foo = &myvar;
int *bar = foo;

 

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

Для выполнения арифметических операций с указателями немного отличается от того, чтобы проводить их по обычным целым типам. Во-первых, допускаются только операции сложения и вычитания; другие не имеют смысла в мире указателей. Но как сложение, так и вычитание имеют несколько иное поведение с указателями, в зависимости от размера типа данных, на который они указывают.

Когда были введены основные типы данных, мы увидели, что типы имеют разные размеры. Например: charвсегда имеет размер 1 байт, shortобычно больше этого intи longеще больше; точный размер которых зависит от системы. Например, предположим, что в данной системе charзанимает 1 байт, shortзанимает 2 байта и longзанимает 4.

Предположим теперь, что мы определяем три указателя в этом компиляторе:

1
2
3
char *mychar;
short *myshort;
long *mylong;

и что мы знаем , что они указывают на ячейки памяти 10002000и 3000, соответственно.

Поэтому, если мы напишем:

1
2
3
++mychar;
++myshort;
++mylong;

mychar, как и следовало ожидать, будет содержать значение 1001. Но не так очевидно, myshortбудет содержать значение 2002 и mylongбудет содержать 3004, хотя каждый из них будет увеличиваться только один раз. Причина в том, что при добавлении одного к указателю указатель делается для указания на следующий элемент того же типа, и, следовательно, размер в байтах типа, который он указывает, добавляется к указателю.

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

1
2
3
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;

Что касается операторов increment ( ++) и decment ( --), они оба могут использоваться как префикс или суффикс выражения с небольшой разницей в поведении: в качестве префикса приращение происходит до того, как выражение оценивается, и как суффикс, приращение происходит после вычисления выражения. Это также относится к выражениям, увеличивающим и уменьшающим указатели, которые могут стать частью более сложных выражений, которые также включают операции разыменования ( *). Помня о правилах приоритетов операторов, мы можем вспомнить, что операторы постфикса, такие как приращение и декремент, имеют более высокий приоритет, чем префиксные операторы, такие как оператор разыменования ( *). Следовательно, следующее выражение:

 
*p++

эквивалентно *(p++). И то, что он делает, — это увеличить значение p(поэтому теперь оно указывает на следующий элемент), но поскольку ++используется как постфикс, все выражение оценивается как значение, указанное первоначально указателем (адрес, на который он указывал, перед тем, как увеличиваться ).

По сути, это четыре возможные комбинации оператора разыменования как с префиксными, так и с суффиксными версиями оператора инкремента (то же самое применимо и к оператору декремента):

1
2
3
4
*p++   // same as *(p++): increment pointer, and dereference unincremented address
*++p   // same as *(++p): increment pointer, and dereference incremented address
++*p   // same as ++(*p): dereference pointer, and increment the value it points to
(*p)++ // dereference pointer, and post-increment the value it points to 

Типичный, но не простой оператор с этими операторами:

 
*p++ = *q++;

Поскольку ++имеет более высокий приоритет , чем *, как pи qувеличивается, а потому , что оба операторы приращения ( ++) используются в качестве постфикса и не префикса, значение , присвоенное *pэто *qперед тем, как pи qувеличивается. И тогда оба увеличиваются. Это будет примерно эквивалентно:

1
2
3
*p = *q;
++p;
++q;

Как и всегда, круглые скобки уменьшают путаницу, добавляя разборчивость к выражениям.

 

Указатели и константа

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

1
2
3
4
5
int x;
int y = 10;
const int * p = &y;
x = *p;          // ok: reading p
*p = x;          // error: modifying p, which is const-qualified 

Здесь pуказывает на переменную, но указывает на нее в const-qualified образом, что означает, что она может читать указанное значение, но оно не может его изменить. Также обратите внимание, что выражение &yимеет тип int*, но это назначается указателю типа const int*. Это разрешено: указатель на не-const может быть неявно преобразован в указатель на const. Но не наоборот! В качестве функции безопасности указатели на constнеявно конвертируются в указатели на non const.

Один из вариантов использования указателей на constэлементы — это параметры функции: функция, которая принимает указатель на constпараметр не как параметр, может изменять значение, переданное как аргумент, тогда как функция, которая принимает указатель на constпараметр, не может.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// pointers as arguments: #include <iostream> using namespace std;

void increment_all (int* start, int* stop)
{
  int * current = start;
  while (current != stop) {
    ++(*current);  // increment value pointed
    ++current;     // increment pointer
  }
}

void print_all (const int* start, const int* stop)
{
  const int * current = start;
  while (current != stop) {
    cout << *current << '\n';
    ++current;     // increment pointer
  }
}

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 к указанному типу (после звездочки):

1
2
3
4
5
int x;
      int *       p1 = &x;  // non-const pointer to non-const int const int *       p2 = &x;  // non-const pointer to const int int * const p3 = &x;  // const pointer to non-const int const int * const p4 = &x;  // const pointer to const int 

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

Чтобы добавить немного больше путаницы к синтаксису constс указателями, constквалификатор может либо предшествовать, либо следовать за указанным типом, с тем же значением:

1
2
const int * p2a = &x;  //      non-const pointer to const int int const * p2b = &x;  // also non-const pointer to const int 

Как и в случае пространств, окружающих звездочку, порядок const в этом случае является просто вопросом стиля. В этой главе используется префикс const, поскольку по историческим причинам это кажется более расширенным, но оба они в точности эквивалентны. Достоинства каждого стиля по-прежнему интенсивно обсуждаются в Интернете.

 

Указатели и строковые литералы

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

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

 
const char * foo = "hello"; 

Это объявляет массив с литеральным представлением "hello", а затем присваивается указатель на его первый элемент foo. Если мы предположим, что "hello"это хранится в ячейках памяти, которые начинаются с адреса 1702, мы можем представить предыдущее объявление как:

Обратите внимание, что здесь fooуказатель и содержит значение 1702, а не 'h', и даже "hello", хотя 1702 действительно является адресом обоих эти.

Указатель fooуказывает на последовательность символов. И поскольку указатели и массивы ведут себя по существу одинаково в выражениях, fooих можно использовать для доступа к символам так же, как массивы последовательностей символов с нулевым символом. Например:

1
2
*(foo+4)
foo[4]

Оба выражения имеют значение 'o'(пятый элемент массива).

 

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

C ++ позволяет использовать указатели, указывающие на указатели, что они, в свою очередь, указывают на данные (или даже на другие указатели). Синтаксис просто требует звездочки ( *) для каждого уровня косвенности в объявлении указателя:

1
2
3
4
5
6
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;

Это, предполагая , что случайно выбранных ячеек памяти для каждой переменной 72308092и 10502, может быть представлена в виде:

При значении каждой переменной , представленной в соответствующей ячейке, а также их соответствующие адреса в памяти , представленное значением под ними.

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

  • cимеет тип char**и значение8092
  • *cимеет тип char*и значение7230
  • **cимеет тип charи значение'z'

 

указатели void

voidТип указателя представляет собой особый тип указателя. В C ++ void— отсутствие типа. Поэтому voidуказатели — это указатели, указывающие на значение, которое не имеет типа (и, следовательно, также неопределенная длина и неопределенные свойства разыменования).

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// increaser #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что всегда имеет размер одного байта.

 

Недопустимые указатели и нулевые указатели

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

1
2
3
4
int * p;               // uninitialized pointer (local variable) int myarray[10];
int * q = myarray+20;  // element out of bounds 

Ни то, pни другое не qуказывают на адреса, которые, как известно, содержат значение, но ни одно из приведенных выше инструкций не вызывает ошибку. В C ++ указатели могут принимать любое значение адреса, независимо от того, есть ли на самом деле что-то на этом адресе или нет. То, что может вызвать ошибку, — это разыменовать такой указатель (т. Е. Фактически получить доступ к значению, на которое они указывают). Доступ к такому указателю вызывает неопределенное поведение, начиная от ошибки во время выполнения и заканчивая доступом к некоторому случайному значению.

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

1
2
int * p = 0;
int * q = nullptr;

Здесь оба pи qявляются нулевыми указателями , а это означает, что они явно указывают на нигде, и оба они фактически сравнивают одинаковые: все нулевые указатели сравниваются с другими нулевыми указателями . Также довольно обычно видеть, что определенная константа NULLиспользуется в более раннем коде для обозначения значения нулевого указателя :

 
int * r = NULL;

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

Не путайте нулевые указатели с voidуказателями! Нулевой указатель представляет собой значение , которое любой указатель может принимать представлять , что она указывает на «нигде», в то время как voidуказатель является тип указателя , который может указывать куда — то без определенного типа. Один из них относится к значению, хранящемуся в указателе, а другой — к типу данных, на которые он указывает.

 

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

C ++ позволяет работать с указателями на функции. Типичным использованием этого является передача функции в качестве аргумента другой функции. Указатели на функции объявляются с тем же синтаксисом, что и объявление регулярной функции, за исключением того, что имя функции заключено между круглыми скобками (), а звездочка ( *) вставлена ​​перед именем:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pointer to functions #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;

Источник* на английском языке