Насколько допустимо рассматривать указатели C++ как адреса памяти?

Когда вы изучаете C++ или, по крайней мере, когда я изучал его через C++ Primer, указатели назывались "адресами памяти" тех элементов, на которые они указывают. Мне интересно, в какой степени это правда.

Например, сделать два элемента *p1 а также *p2 иметь собственность p2 = p1 + 1 или же p1 = p2 + 1 если и только если они соседствуют в физической памяти?

12 ответов

Решение

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

Что касается вашего последнего утверждения, вы не можете сделать это предположение, даже в адресном пространстве виртуальной памяти. Арифметика указателей действительна только внутри блоков непрерывной памяти, таких как массивы. И хотя допустимо (как в C, так и в C++) назначить указатель на одну точку после массива (или скаляра), поведение при определении такого указателя не определено. Гипотезы о смежности в физической памяти в контексте C и C++ не имеют смысла.

Не за что.

C++ - это абстракция над кодом, который будет выполнять ваш компьютер. Мы наблюдаем эту утечку абстракции в нескольких местах (например, ссылки на члены класса, требующие хранения), но в целом вам будет лучше, если вы будете кодировать абстракцию и ничего больше.

Указатели являются указателями. Они указывают на вещи. Будут ли они реализованы как адреса памяти в реальности? Может быть. Их также можно оптимизировать или (в случае, например, указателей на члены) они могут быть несколько более сложными, чем простой числовой адрес.

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

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

*p1 и *p2 имеют свойство p2 = p1 + 1 или p1 = p2 + 1 тогда и только тогда, когда они находятся рядом в физической памяти

правильно только если p1 а также p2 имеют одинаковый тип или указывают на типы одинакового размера.

Операционная система обеспечивает абстракцию физической машины для вашей программы (т.е. ваша программа работает на виртуальной машине). Таким образом, ваша программа не имеет доступа к каким-либо физическим ресурсам вашего компьютера, будь то процессорное время, память и т. Д.; он просто должен запросить у ОС эти ресурсы.

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

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

Суть в том, что указатели являются адресами памяти. Тем не менее, они являются адресами в виртуальной памяти, и операционная система сама решает, как это сопоставить с физической памятью.

Что касается вашей программы, это не проблема. Одна из причин этой абстракции - заставить программы поверить, что они единственные пользователи машины. Представьте себе кошмар, который вам придется пережить, если при написании вашей программы вам нужно будет учитывать память, выделенную другими процессами, - вы даже не знаете, какие процессы будут выполняться одновременно с вашей. Кроме того, это хорошая техника для обеспечения безопасности: ваш процесс не может (ну, по крайней мере, не должен иметь) злонамеренно обращаться к пространству памяти другого процесса, так как они работают в 2 разных (виртуальных) пространствах памяти.

Абсолютно правильно думать об указателях как об адресах памяти. Это то, что они есть во ВСЕХ компиляторах, с которыми я работал - для ряда различных архитектур процессоров, выпускаемых несколькими разными производителями компиляторов.

Тем не менее, компилятор делает некоторую интересную магию, чтобы помочь вам вместе с тем фактом, что обычные адреса памяти [по крайней мере во всех современных процессорах основного потока] являются байтовыми адресами, а объект, на который ссылается ваш указатель, может не быть точно одним байтом. Так что если у нас есть T* ptr;, ptr++ Сделаю ((char*)ptr) + sizeof(T); или же ptr + n является ((char*)ptr) + n*sizeof(T), Это также означает, что ваш p1 == p2 + 1 требует p1 а также p2 быть того же типа T, так как +1 на самом деле +sizeof(T)*1,

Существует одно исключение из вышеупомянутых "указатели являются адресами памяти", и это указатели на функции-члены. Они "особенные", и пока, пожалуйста, просто игнорируйте, как они на самом деле реализованы, достаточно сказать, что они не "просто адреса памяти".

Я думаю, что у этого ответа есть правильная идея, но плохая терминология. То, что предоставляют указатели C, является полной противоположностью абстракции.

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

C указатели являются противоположностью этого. Они учитывают возможные трудности оборудования, даже если реальное оборудование часто проще и легче рассуждать. Они ограничивают ваши рассуждения тем, что допускается объединением самых сложных частей самого сложного оборудования, независимо от того, насколько простым на самом деле может быть имеющееся оборудование.

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

Как и другие переменные, указатель хранит данные, которые могут быть адресом памяти, где хранятся другие данные.

Таким образом, указатель является переменной, которая имеет адрес и может содержать адрес.

Обратите внимание, что указатель не обязательно всегда содержит адрес. Он может содержать неадресный идентификатор / дескриптор и т. Д. Следовательно, указывать указатель как адрес - не мудрая вещь.


По поводу вашего второго вопроса:

Арифметика указателя действительна для непрерывной порции памяти. Если p2 = p1 + 1 и оба указателя имеют тот же тип, то p1 а также p2 указывает на непрерывный кусок памяти. Итак, адреса p1 а также p2 трюмы прилегают друг к другу.

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

На самом деле, вы можете проверить это, напечатав фактическое число, сохраненное на них с printf(),

Остерегайтесь, однако, что type * Операции увеличения / уменьшения указателя выполняются sizeof(type), Убедитесь сами с этим кодом (проверено онлайн на Repl.it):

#include <stdio.h>

int main() {
    volatile int i1 = 1337;
    volatile int i2 = 31337;
    volatile double d1 = 1.337;
    volatile double d2 = 31.337;
    volatile int* pi = &i1;
    volatile double* pd = &d1;
    printf("ints: %d, %d\ndoubles: %f, %f\n", i1, i2, d1, d2);
    printf("0x%X = %d\n", pi, *pi);
    printf("0x%X = %d\n", pi-1, *(pi-1));
    printf("Difference: %d\n",(long)(pi)-(long)(pi-1));
    printf("0x%X = %f\n", pd, *pd);
    printf("0x%X = %f\n", pd-1, *(pd-1));
    printf("Difference: %d\n",(long)(pd)-(long)(pd-1));
}

Все переменные и указатели были объявлены как volatile, чтобы компилятор не оптимизировал их. Также обратите внимание, что я использовал декремент, потому что переменные помещаются в стек функций.

Выход был:

ints: 1337, 31337
doubles: 1.337000, 31.337000
0xFAFF465C = 1337
0xFAFF4658 = 31337
Difference: 4
0xFAFF4650 = 1.337000
0xFAFF4648 = 31.337000
Difference: 8

Обратите внимание, что этот код может работать не на всех компиляторах, особенно если они не хранят переменные в одном и том же порядке. Тем не менее, важно то, что значения указателя могут быть фактически прочитаны и распечатаны, и что уменьшение на единицу может / будет уменьшаться в зависимости от размера переменной, на которую ссылается указатель.

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

Это также может быть использовано для интересных трюков, таких как получение двоичных значений IEEE 754 для чисел с плавающей точкой, путем приведения float* как int*:

#include <iostream>

int main() {
    float f = -9.5;
    int* p = (int*)&f;

    std::cout << "Binary contents:\n";
    int i = sizeof(f)*8;
    while(i) {
        i--;
        std::cout << ((*p & (1 << i))?1:0);
   } 
}

Результат:

Binary contents:
11000001000110000000000000000000 

Пример взят из https://pt.wikipedia.org/wiki/IEEE_754. Проверьте на любом конвертере.

Каким-то образом в ответах здесь не упоминается одна конкретная семья указателей, то есть указатели на участников. Это, конечно, не адреса памяти.

Согласно стандарту C++14, [expr.unary.op]/3:

Результат одинарный & Оператор является указателем на свой операнд. Операндом должно быть lvalue или квалифицированный идентификатор. Если операнд является квалифицированным идентификатором, именующим нестатический член m какого-то класса C с типом T, результат имеет тип "указатель на член класса C типа T И является частным обозначением C::m, В противном случае, если тип выражения Tрезультат имеет тип "указатель на T" и является значением, которое является адресом назначенного объекта или указателем на назначенную функцию. [Примечание: в частности, адрес объекта типа "cv T "Это" указатель на резюме T С той же квалификацией. —Конечная записка]

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


"адрес" определяется [intro.memory]/1:

Память, доступная для программы на C++, состоит из одной или нескольких последовательностей непрерывных байтов. Каждый байт имеет уникальный адрес.

Таким образом, адрес может быть любым, который служит для уникальной идентификации конкретного байта памяти.

Примечание. В стандартной терминологии C++ память относится только к используемому пространству. Это не означает физическую память, виртуальную память или что-то в этом роде. Память представляет собой несвязанный набор выделений.


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

Чтобы избежать написания непереносимого кода, лучше избегать предположения, что адрес идентичен целому числу. Правила арифметики для указателей в любом случае отличаются от правил арифметики для целых чисел. Точно так же мы бы не сказали, что 5.0f такой же как 1084227584 даже если они имеют идентичные битовые представления в памяти (по IEEE754).

Указатели являются адресами памяти, но вы не должны предполагать, что они отражают физический адрес. Когда вы видите адреса, как 0x00ffb500 это логические адреса, которые MMU преобразует в соответствующий физический адрес. Это наиболее вероятный сценарий, поскольку виртуальная память является самой расширенной системой управления памятью, но могут существовать системы, которые управляют физическим адресом напрямую.

Конкретный пример, который вы приводите:

Например, имеют ли два элемента *p1 и *p2 свойство p2 = p1 + 1 или p1 = p2 + 1, если и только если они смежны в физической памяти?

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

Другие вопросы по тегам