Пересмотр базовой терминологии
Это обычно достаточно хорошо - если вы не программируете сборку - чтобы предусмотреть указатель , содержащий числовой адрес памяти, где 1 относится ко второму байту в памяти процесса, 2 третий, 3 четвертый и т. д.
- Что случилось с 0 и первым байтом? Что ж, мы вернемся к этому позже - см. нулевые указатели ниже.
- Более точное определение того, что хранят указатели и как соотносятся память и адреса, см. «Подробнее об адресах памяти и почему вам, вероятно, не нужно знать» * .
Если вы хотите получить доступ к данным / значению в памяти, на которые указывает указатель - к содержанию адреса с этим числовым индексом - тогда вы разыменовываете указатель.
Разные компьютерные языки имеют разные нотации, чтобы сообщить компилятору или интерпретатору, что вы сейчас заинтересованы в указанном значении - ниже я сосредоточусь на C и C ++.
Сценарий указателя
Рассмотрим в C, учитывая указатель, такой как p
ниже ...
const char* p = "abc";
... четыре байта с числовыми значениями, используемыми для кодирования букв «a», «b», «c» и 0 байтов для обозначения конца текстовых данных, хранятся где-то в памяти, а числовые адрес этих данных хранится в p
.
Например, если строковый литерал оказался по адресу 0x1000 и p
32-битным указателем по адресу 0x2000, содержимое памяти будет:
Memory Address (hex) Variable name Contents
1000 'a' == 97 (ASCII)
1001 'b' == 98
1002 'c' == 99
1003 0
...
2000-2003 p 1000 hex
Обратите внимание, что для адреса 0x1000 нет имени / идентификатора переменной, но мы можем косвенно ссылаться на строковый литерал, используя указатель, хранящий его адрес: p
.
Разыменование указателя
Чтобы сослаться на символы, на которые указывает p
, мы разыменовываем p
, используя одно из следующих обозначений (опять же, для C):
assert(*p == 'a'); // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
// p and 1 times the size of the things to which p points:
// In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b'); // Another notation for p[1]
Вы также можете перемещать указатели по указанным данным, разыменовывая их по мере продвижения:
++p; // Increment p so it's now 0x1001
assert(*p == 'b'); // p == 0x1001 which is where the 'b' is...
Если у вас есть данные, в которые можно записать данные, вы можете сделать следующее:
int x = 2;
int* p_x = &x; // Put the address of the x variable into the pointer p_x
*p_x = 4; // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4
Выше, во время компиляции, вы должны были знать, что вам потребуется переменная с именем x
, и код просит компилятор указать, где он должен храниться, гарантируя, что адрес будет доступен через &x
.
Разыменование и доступ к элементу данных структуры
В C, если у вас есть переменная, которая является указателем на структуру с элементами данных, вы можете получить доступ к этим элементам с помощью оператора разыменования ->
:
typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159; // Dereference and access data member x.d_
(*p).d_ *= -1; // Another equivalent notation for accessing x.d_
Многобайтовые типы данных
Чтобы использовать указатель, компьютерной программе также необходимо получить представление о типе данных, на которые он указывает - если для этого типа данных требуется более одного байта, то указатель обычно указывает на байт с наименьшим номером в данные.
Итак, рассмотрим немного более сложный пример:
double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3); // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4); // Actually looks at bytes from address p + 1 * sizeof(double)
// (sizeof(double) is almost always eight bytes)
assert(++p); // Advance p by sizeof(double)
assert(*p == 13.4); // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8; // Change sizes[3] from 19.4 to 29.8
// Note: earlier ++p and + 2 here => sizes[3]
Указатели на динамически выделяемую память
Иногда вы не знаете, сколько памяти вам понадобится, пока ваша программа не запустится и не увидит, какие данные выбрасываются в нее ... тогда вы можете динамически распределять память, используя malloc
. Обычной практикой является сохранение адреса в указателе ...
int* p = malloc(sizeof(int)); // Get some memory somewhere...
*p = 10; // Dereference the pointer to the memory, then write a value in
fn(*p); // Call a function, passing it the value at address p
(*p) += 3; // Change the value, adding 3 to it
free(p); // Release the memory back to the heap allocation library
В C ++ выделение памяти обычно выполняется с помощью оператора new
, а освобождение - с delete
:
int* p = new int(10); // Memory for one int with initial value 10
delete p;
p = new int[10]; // Memory for ten ints with unspecified initial value
delete[] p;
p = new int[10](); // Memory for ten ints that are value initialised (to 0)
delete[] p;
См. Также C ++ интеллектуальные указатели ниже.
Потеря и утечка адресов
Часто указатель может быть единственным указанием того, где в памяти существуют некоторые данные или буфер. Если требуется постоянное использование этих данных / буфера или возможность вызова free()
или delete
, чтобы избежать утечки памяти, то программист должен работать с копией указателя ...
const char* p = asprintf("name: %s", name); // Common but non-Standard printf-on-heap
// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
if (!isprint(*q))
*q = '_';
printf("%s\n", p); // Only q was modified
free(p);
... или тщательно организовать отмену любых изменений ...
const size_t n = ...;
p += n;
...
p -= n; // Restore earlier value...
C ++ умные указатели
В C ++ рекомендуется использовать интеллектуальные указатели объекты для хранения и управления указателями, автоматически освобождая их при запуске деструкторов интеллектуальных указателей.Начиная с C ++ 11, стандартная библиотека предоставляет два, unique_ptr
для случая, когда для выделенного объекта существует один владелец ...
{
std::unique_ptr<T> p{new T(42, "meaning")};
call_a_function(p);
// The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete
... и shared_ptr
для владения акциями (используя подсчет ссылок ) ...
{
std::shared_ptr<T> p(new T(3.14, "pi"));
number_storage.may_add(p); // Might copy p into its container
} // p's destructor will only delete the T if number_storage didn't copy
Нулевые указатели
В С, NULL
и 0
-и дополнительно в C ++ nullptr
- может использоваться, чтобы указать, что указатель в настоящее время не содержит адрес памяти переменной, и не должен разыменовываться или использоваться в арифметике указателя.Например:
const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
char c;
while ((c = getopt(argc, argv, "f:")) != EOF)
switch (c) {
case f: p_filename = optarg; break;
}
if (p_filename) // Only NULL converts to false
... // Only get here if -f flag specified
В C и C ++, так как встроенные числовые типы не обязательно по умолчанию имеют значение 0
или bools
- false
, указатели не всегда устанавливаются на NULL
,Все они устанавливаются в 0 / false / NULL, когда они являются static
переменными или (только C ++) прямыми или косвенными переменными-членами статических объектов или их баз, или проходят нулевую инициализацию (например, new T();
и new T(x, y, z);
выполняют ноль).-инициализация на элементах T, включая указатели, тогда как new T;
нет).
Далее, когда вы назначаете 0
, NULL
и nullptr
указателю, биты в указателе не обязательно всесброс: указатель может не содержать «0» на аппаратном уровне или ссылаться на адрес 0 в вашем виртуальном адресном пространстве.Компилятору разрешается хранить что-то еще, если у него есть для этого причина, но что бы он ни делал - если вы подойдете и сравните указатель с 0
, NULL
, nullptr
или другим указателем, которому был назначен любой из них,сравнение должно работать как положено.Итак, ниже исходного кода на уровне компилятора «NULL» потенциально немного «волшебен» в языках C и C ++ ...
Подробнее об адресах памяти и о том, почему вам, вероятно, не нужнознать
Более строго, инициализированные указатели хранят битовый шаблон, идентифицирующий либо NULL
, либо (часто виртуальный ) адрес памяти.
Простой случай, когда эточисловое смещение во всем виртуальном адресном пространстве процесса;в более сложных случаях указатель может относиться к некоторой конкретной области памяти, которую ЦП может выбирать на основе регистров «сегмента» ЦП или некоторого вида идентификатора сегмента, закодированного в битовой комбинации, и / или просматривая в разных местах в зависимости отинструкции машинного кода с использованием адреса.
Например, int*
, правильно инициализированный для указания на переменную int
, может - после приведения к float*
- получить доступ к значению в памяти "GPU", совершенно отличномуиз переменной int
затем приведение к указателю на функцию может ссылаться на отдельную память, содержащую машинные коды операций для функции.
Языки программирования 3GL, такие как C и C ++, как правило, скрывают эту сложность, например:
Если компилятор дает вам указатель на переменную или функцию, вы можете разыменовать его свободно (если переменная не уничтожена / не освобождена), и это проблема компилятора, например, конкретныйРегистр ЦП должен быть восстановлен заранее, или отдельная машинаИспользуемая инструкция кода
Если вы получаете указатель на элемент в массиве, вы можете использовать арифметику указателей для перемещения в другое место в массиве или даже для формирования адреса в прошломконец массива, который можно сравнивать с другими указателями на элементы в массиве (или которые арифметикой указателей аналогичным образом перемещены в одно и то же значение «один за другим»);опять же, в C и C ++, компилятор должен гарантировать, что это «просто работает»
Определенные функции ОС, например отображение общей памяти, могут дать вам указатели, и они будут простоработать "в пределах диапазона адресов, который имеет для них смысл
Попытки переместить законные указатели за эти границы, или привести произвольные числа к указателям, или использовать указатели, приведенные к несвязанным типам, обычно имеют неопределенное поведение , поэтому следуетИзбегайте использования в библиотеках и приложениях более высокого уровня, но код для операционных систем, драйверов устройств и т. д., возможно, должен полагаться на поведение, оставленное неопределенным в C или C ++, что, тем не менее, хорошо определяется их конкретным оборудованием.