#cpp #c
Вопрос по поводу арифметики указателей С. Пусть int *p; int *q; int *o; int i; o = NULL; Допустим p и q указывают на разные элементы одного массива. Допустимы выражения (i, естественно, где-то определено): p+i; q-i; p-q; А допустимо ли: p-o; o-p; И что получится, если допустимы? Т.е. с одной стороны o не указывает на тот же массив и вроде нет, но он же равен и обычному 0, и первое вроде бы да.
Ответы
Ответ 1
Нет, не допустимы. Стандарт С++ говорит (С++11 [expr.add]p6), что если два указателя не принадлежат одному массиву, то поведение не определено. Результат вычитания двух указателей - это количество элементов между ними. Если p не выровнен на sizeof(int), то выражение p-o должно вернуть дробное количество элементов, чего не может быть. При этом выражение p-NULL может быть валидно только если NULL - это #define NULL 0, и не валидно если, например, NULL определено как (void*)0 (в Си). Поскольку стандарт не говорит как именно должен быть определен NULL, то следует считать, что выражение p-NULL не валидно. Примечание: выражение p-0 валидно и равно p, но это работает только для целого числа 0.Ответ 2
Краткий ответ: Вычисление разности между двумя указателями, которые не ссылаются на элементы одного массива, приводит к неопределенному поведению. Подробный ответ: В соответствии со Стандартом C++ в п. 18.2/3 сказано: The macro NULL is an implementation-defined C++ null pointer constant in this International Standard (4.10). При этом в сноске указаны возможные и однозначно невозможные варианты реализации: Possible definitions include 0 and 0L, but not (void*)0. п.4.10/1 говорит, что такое null pointer constant (выделено мной): A null pointer constant is an integer literal (2.14.2) with value zero or a prvalue of type std::nullptr_t. Т.о. NULL развовачивается либо в целочисленный тип, но не ясно в какой (int, long и т.д.), либо в константу nullptr (это единственно возможное значение для типа std::nullptr_t). Более подробно тут. И хотя на "бумаге" NULL может быть определён как nullptr, фактически все известные мне реализации определяют его именно как целое число. Исходя из этого строятся все дальнейшие рассуждения. Возвращаясь к выражениям из вопроса, позволю себе немного модифицировать пример для большей самодостаточности кода: int arr[] = {1, 2, 3}; int* p = arr; int* o = NULL; auto s1 = p - o; auto s2 = o - p; auto s3 = p - NULL; auto s4 = o - NULL; auto s5 = NULL - NULL; auto s6 = o - o; Выражения s1 и s2 имеют тип такой же как std::ptrdiff_t изи они приводят к неопределенному поведению (UB) на основании того, что это вычитание двух указателей, не принадлежащих одному массиву (5.7/6) (ведь NULL не может указывать ни на один существующий в программе объект): Unless both pointers point to elements of the same array object, or one past the last element of the array object, the behavior is undefined Выражения s3 и s4 имеют тип такой же, как у p и o соответственно. В данном случае это int* и они валидны, на основании того, что это вычитание целочисленного аргумента (см. что такое NULL в начале ответа) из указателя (5.7/8): If the value 0 is added to or subtracted from a pointer value, the result compares equal to the original pointer value. При этом значение s3 такое же как у p, а значение s4 такое же как у o. Выражение s5 имеет целочисленный тип, зависимый от того, как определен NULL на основании продвижения (promotions) целочисленных аргументов (4.5) и, разумеется, валидно, т.к. представляет собой разность двух целых значений. Выражение s6 валидно и имеет тип std::ptrdiff_t на основании п. 5.7/8 (выделено мной): If two pointers point to the same object or both point one past the end of the same array or both are null, and the two pointers are subtracted, the result compares equal to the value 0 converted to the type std::ptrdiff_t. Ответ 3
Для традиционных систем с линейным адресным пространством, размер которого задан разрядностью указателя, например, x86 (и по крайней мере в Linux/gcc/g++ (в других не проверял)) все компилируется и как ни странно вполне осмысленно. Посмотрим небольшую программку avp@avp-xub11:hashcode$ cat c2.c #ifdef __cplusplus #include#endif #include #include #include #ifndef T #define T int #endif int main (int ac, char *av[]) { T arr[] = {1, 2, 3}; T* p = arr; T* o = NULL; long i = p - o, j = o - p; printf("%p %lx (%lx) %lx (%lx) [%lx + %lx = %lx]\n", p, (long)(p - o), (long)((p - o)) * sizeof(*p), (long)(o - p), (long)((o - p)) * sizeof(*p), i, j, j + i); }; avp@avp-xub11:hashcode$ uname -a Linux avp-xub11 3.13.0-74-generic #118-Ubuntu SMP Thu Dec 17 22:52:02 UTC 2015 i686 i686 i686 GNU/Linux avp@avp-xub11:hashcode$ g++ c2.c avp@avp-xub11:hashcode$ ./a.out 0xbfd3daa4 eff4f6a9 (bfd3daa4) 100b0957 (402c255c) [eff4f6a9 + 100b0957 = 0] avp@avp-xub11:hashcode$ scp c2.c avp@nas: c2.c 100% 429 0.4KB/s 00:00 avp@avp-xub11:hashcode$ ssh nas uname -a Linux nas.inlinegroup.ru 2.6.32-573.7.1.el6.x86_64 #1 SMP Thu Sep 10 13:42:16 EDT 2015 x86_64 x86_64 x86_64 GNU/Linux avp@avp-xub11:hashcode$ ssh nas gcc c2.c avp@avp-xub11:hashcode$ ssh nas ./a.out 0x7ffcd89e7530 1fff36279d4c (7ffcd89e7530) ffffe000c9d862b4 (ffff800327618ad0) [1fff36279d4c + ffffe000c9d862b4 = 0] avp@avp-xub11:hashcode$ Все это можно интерпретировать так -- p - o это количество элементов типа int от начала адресного пространства до arr[], а o - p это тоже самое, но "в другую сторону", т.е. от arr[] до начала (отрицательная величина). А i + j естественно дает 0 из-за переполнения (т.к. мы прошли все адреса "по кругу"). В стандарте же (только С++ ?) отражается довольно естественная перестраховка для более общего случая (гипотетического?), когда объекты находятся в разных адресных пространствах и тогда часть указателя (в машинном виде) будет адресовать конкретное пространство, но вот арифметика между такими указателями уже не будет осмысленной (или по крайней мере очевидной). Ответ 4
Допустимы но бессмысленны. Вы получите расстояние в интах между двумя указателями. Так как в указатели может хранится адрес не только данных в оперативной памяти, то такое расстояние бесполезно. Но так как компилятор полагаться на человека в плане смысла (и проверки выхода за границы массива есно), то это не запрещено. Интересней ситуация, если у вас будут указатели разных типов, вот тут уже "неопределенно" приобретает совершенно неопределенное значение. P.S. Операции с NULL ЕМНИП запрещены.
Комментариев нет:
Отправить комментарий