"Присвойте одном из указателей адрес 100, другому - 108. Подсчитайте и выведите разницу между указателями. Обоснуйте полученный ответ"

У меня есть задание из лабораторной работы по C++:

Объявите две переменные типа int.

Объявите два указателя типа int. Одному указателе присвойте адрес одной из переменных при объявлении, а другому в отдельном операторе.

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

Присвойте одном из указателей адрес 100, другому — 108. Подсчитайте и выведите разницу между указателями. Обоснуйте полученный ответ.

Можно ли так присваивать адрес указателям, либо это делается с только помощью операции &? Пробовала так, но очень сомневаюсь в правильности:

int main()
{
    int c = 1, d = 2;
    int* pc = &c;
    int* pd;
    pd = &d;
    cout << "At first: c = " << c << endl;
    c = *pd;
    cout << "Then: c = " << c << endl;
    *pc = 100;
    *pd = 108;
    cout << pc - pd;
}

Ответы (4 шт):

Автор решения: SeeNax
  1. Указатели нужно объявить, вы же занимаетесь не понятно чем
  2. Указателям нужно указать на адрес
  3. Указатели нужно вычесть
int main()
{
    int* pc;
    int* pd;
    
    pc = reinterpret_cast<int*> (0x64);
    pd = reinterpret_cast<int*> (0x6C);
    std::cout << pd - pc;
    return 0;
}

Разобраться что и почему он выдает уже на вашей совести.

→ Ссылка
Автор решения: Артём Оконечников

Можно ли так присваивать адрес указателям, либо это делается с только помощью операции &?

Можно, но требуется приведение типа. Указатель это не число в привычном понимании — это адрес. И с этими адресом можно выполнять различные операции (т.н. арифметика указателей). Литерал 100 интерпретируется как целочисленный тип (int), а нам нужно интерпретировать его как указатель на целочисленный тип (int *). Можно выполнить привидение в стиле C:

pc = (int *) 100;

Или в стиле C++ при помощи reinterpret_cast:

pc = reinterpret_cast<int *>(100);

Теперь указатель pc хранит в себе адрес 100 и с ним можно выполнять различные трюки. Например вычитать:

int * x_ptr = (int *) 100;
int * y_ptr = (int *) 108;

std::cout << y_ptr - x_ptr << std::endl;

Здесь мы получим 2 так как в отличии от обычных арифметических операций, операции над указателями выполняются в контексте размера их типов. Мы знаем, что размер целочисленного обычно 4 байта (посмотреть это можно при помощи специального оператора sizeof: std::cout << sizeof(int);). Следовательно, манипуляция указателем рассматривается как манипуляция блоками по 4 байта. Например:

int * x_ptr = (int *) 100;
x_ptr++; 

Инкремент увеличит адрес не на единицу, а на 4 т.е. в контексте указателя на sizeof(int) байт. Если вы вычитаете один указатель из другого, то результат интерпретируется как количество блоков в разнице.

y_ptr - x_ptr другими словами это (108 - 100) / sizeof(int) (*)


Как было сказано в ответе @eanmos и описано в стандарте, этот код попадает под UB. Складывать и вычитать указатели можно, если они указывают либо на NULL либо на элементы одно и того же массива. В противном случае возможно неопределенное поведение.

Однако, если мы рассматриваем этот код как нечто скомпилированное и запущенное под x86, где указатель адресует на 1 байт, то скорее всего данный код вычислит ожидаемое 2. В общем, на свой страх и риск.

→ Ссылка
Автор решения: eanmos

Оба существующих ответа, как и само задание из вопроса, некорректны и содержат неопределенное поведение.


Согласно стандарту C++, вычитание двух указателей возможно тогда и только тогда, когда они указывают на элементы одного и того же массива или являются нулевыми указателями:

(5) When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined as std::ptrdiff_t in the <cstddef> header (17.2.4).

(5.1) — If P and Q both evaluate to null pointer values, the result is 0.

(5.2) — Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x, the expression P - Q has the value i − j.

(5.3) — Otherwise, the behavior is undefined.

ISO/IEC N486 — § 7.6.6


В языке C такие трюки тоже являются неопределенным поведением:

(9) When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object; the result is the difference of the subscripts of the two array elements.

ISO/IEC 9899:2017 — § 6.5.6

→ Ссылка
Автор решения: wololo

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

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

Стандарт языка утверждает (basic.compound/3), что значение указателя — это одно из следующих значений:

  • указатель на объект,
  • указатель на гипотетический элемент, следующий непосредственно за объектом,
  • нулевое указательное значение,
  • недействительное указательное значение (invalid pointer value).

Какое конкретно значение примет указатель при присваивании ему целого числа определяется реализацией (expr.reinterpret.cast/5):

A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined.

Таким образом, при присвоении указателю произвольного целого числа, он может запросто получить некоторое недействительное указательное значение. Последствия обращения к недействительному указателю определяются реализацией. В частности, в стандарте языка явно оговорено, что простое копирование недействительного указателя может привести к аварийному завершению работы программы (basic.stc.general/4):

[...] Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.28
28) Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.

Если же каким-то образом (чудом? ;)) удалось подобрать такие целочисленные константы, что после их преобразования в указательный тип, получившиеся указатели указывают на некоторые объекты, то эти объекты не должны иметь динамический класс памяти (dynamic storage duration). Это связано с тем, что константа в коде не является целочисленным представлением безопасно-наследуемого указателя (integer representation of a safely-derived pointer, см.: basic.stc.dynamic.safety/3), а значит и получившиеся указатели не являются безопасно-наследуемыми указателями (safely-derived pointer, см.: basic.stc.dynamic.safety/2). Стандарт языка разрешает реализации считать такие указатели недействительными (basic.stc.dynamic.safety/4):

[...] Alternatively, an implementation may have strict pointer safety, in which case a pointer value referring to an object with dynamic storage duration that is not a safely-derived pointer value is an invalid pointer value unless the referenced complete object has previously been declared reachable ([util.dynamic.safety]).
[...]
This is true even if the unsafely-derived pointer value would compare equal to some safely-derived pointer value.

И наконец, полученные указатели, если мы хотим найти их разность, должны указывать на элементы одного и того же массива (или оба нулевые), иначе поведение программы не определено (см.: ответ @eanmos, expr.add/5).

Как подобрать два целых числа так, чтобы они удовлетворяли всем описанным выше условиям — вопрос, конечно, интересный :)


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

#include <iostream>
#include <cstdint>
#include <algorithm>
using namespace std;

int main()
{
    char a, b;
    
    cout << (void *)&a << endl;
    cout << (void *)&b << endl;
    cout << std::min(std::uintptr_t(&b) - std::uintptr_t(&a),
                     std::uintptr_t(&a) - std::uintptr_t(&b)) << endl;
    cout << (&a + 1 == &b) << endl; //1 or 0
    cout << (&b + 1 == &a) << endl; //0 or 1
}

Здесь мы объявили две переменных a и b вывели их адреса, и нашли разность адресов между этими переменными (предварительно преобразовав указатели в целые числа, чтобы избежать неопределённого поведения). Зная значения адресов и разность между адресами мы можем судить о результатах вычисления выражений &a + 1 == &b и &b + 1 == &a: если переменные расположены одна непосредственно за другой, то одно из равенств будет истинным. Стандарт разрешает такое сравнение (expr.eq/3):

[...] Comparing pointers is defined as follows:
— If one pointer represents the address of a complete object, and another pointer represents the address one past the last element of a different complete object,75 the result of the comparison is unspecified.
75) As specified in [basic.compound], an object that is not an array element is considered to belong to a single-element array for this purpose.

Хотя результат сравнения неспецифицирован. И понятно почему. Объект b может идти непосредственно за объектом a, либо наоборот, либо между ними вообще может находиться какой-нибудь другой объект.

Итак, взглянем на результат работы clang:

0x7fff948bf6c7
0x7fff948bf6c6
1
0
1

Получили ожидаемый вывод. Объекты действительно расположены впритык и одно из сравнений истинно.

Результат работы g++:

0x7ffe20fee88e
0x7ffe20fee88f
1
0
0

Здесь два объекта также расположены впритык, один следует непосредственно за другим. Но результат обоих сравнений — false! Да, g++ считает, что если объект b следует непосредственно за объектом a, то указатель за конец объекта a не обязан успешно сравниваться с указателем на объект b. Несмотря на все наши знания об окружении, компилятор повёл себя контринтуитивным образом, и он имеет на это право, ибо the result of the comparison is unspecified.

Эти примеры я привёл для того, чтобы показать, что по-возможности не следует полагаться на поведение, определяемое реализацией, и уж тем более следует избегать неопределённого поведения. Кто знает, может внутри оптимизатора зашита проверка вида "если значение указателя меньше 128 или значение указателя равно 1000000009, то считать такой указатель гарантированно невалидным".


P.S. Весьма необычно повёл себя vc++:

00000093AF42FAF4
00000093AF42FAF2
2
0
0

Он просто вставил дополнительный байт между двумя переменными, поэтому оба сравнения были ложны. Переписав их так (теперь здесь есть UB): &a + 2 == &b и &b + 2 == &a получил следующий вывод:

000000ADF3E4FAE5
000000ADF3E4FAE2
3
0
0

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


P.P.S.

Есть ещё небольшое различие при приведении целочисленного литерала со значением 0 к указательному типу при использовании приведения в стиле C (а также функциональном стиле) и с использованием reinterpret_cast.

Приведение в стиле C сперва попытается преобразовать литерал с помощью static_cast, и это преобразование сработает, т.к. существует неявное преобразование из нулевой указательной константы целочисленного типа (null pointer constant of integral type) в указательный тип. Результат — нулевое указательное значение. См.: conv.ptr/1.

С другой стороны, reinterpret_cast отображает целочисленные значения в указательные определяемым реализацией образом: не гарантируется, что целочисленное значение ноль (в частности, литерал 0) будет преобразовано в нулевое указательное значение (expr.reinterpret.cast/9):

[...] a null pointer constant of integral type is not necessarily converted to a null pointer value.

Существуют машины, на которых нулевые указатели не состоят полностью из нулевых бит: comp.lang.c FAQ list · Question 5.17.

→ Ссылка