Ответ 1
Разница между двумя указателями означает количество элементов типа, которые бы соответствовали между целями двух указателей. Сумма двух указателей означает... er... ничего, поэтому она не поддерживается.
$5.7 -
"[..] Для добавления оба операнда должны иметь тип арифметики или перечисления, или один операнд должен быть указателем на полностью определенный тип объекта, а другой должен иметь интегральный или перечисляемый тип.
2 Для вычитания должно выполняться одно из следующих условий: - оба операнда имеют тип арифметики или перечисления; или - оба операнда являются указателями на cv-квалифицированные или cv-неквалифицированные версии одного и того же полностью определенного типа объекта; или - левый операнд является указателем на полностью определенный тип объекта, а правый операнд имеет целочисленный или перечисляемый тип.
int main(){
int buf[10];
int *p1 = &buf[0];
int *p2 = 0;
p1 + p2; // Error
p1 - p2; // OK
}
Итак, мой вопрос в том, почему "добавление указателя" не поддерживается в С++, но "вычитание указателя" есть?
Разница между двумя указателями означает количество элементов типа, которые бы соответствовали между целями двух указателей. Сумма двух указателей означает... er... ничего, поэтому она не поддерживается.
Результат вычитания - это расстояние (полезно).
Результатом добавления указателя и расстояния является другой значащий указатель.
Результат добавления 2 указателей - это другой указатель, на этот раз бессмысленный.
По той же причине в большинстве библиотек есть отдельные объекты TimeSpan и DateTime.
Первое, что приходит на ум, - это не имеет смысла делать добавление указателя, поэтому оно не поддерживается. Если у вас есть 2 указателя 0x45ff23dd, 0x45ff23ed
. Что значит добавить их? Некоторая память вне пределов. И люди в стандартном комитете не нашли достаточных оснований для поддержки подобных вещей и скорее предупреждают вас о времени компиляции о возможных проблемах. Хотя вычитание указателя прекрасное, потому что оно указывает расстояние памяти, что часто полезно.
Результат вычитания указателя - это количество объектов между двумя адресами памяти. Добавление указателя ничего не значит, поэтому его не допускают.
Поскольку добавление двух указателей не имеет смысла.
У меня есть два int
в памяти в 0x1234
и 0x1240
. Разница между этими адресами 0xc
и является расстоянием в памяти. Сумма 0x2474
и не соответствует чему-либо значимому.
Однако вы можете добавить указатель на целое число, чтобы получить другой указатель. Это то, что вы делаете, когда вы индексируете в массив: p [4] означает * (p + 4), что означает "вещь, хранящаяся по адресу 4 единицы за этот адрес".
В общем, вы можете определить "pointerness" арифметической операции, назначив каждому указателю значение 1 и каждое целое число равным нулю. Если результат равен 1, у вас есть указатель; если он равен 0, у вас есть целое число; если это какая-то другая ценность, у вас есть что-то, что не имеет смысла. Примеры:
/* here p,q,r are pointers, i,j,k are integers */
p + i; /* 1 + 0 == 1 => p+i is a pointer */
p - q; /* 1 - 1 == 0 => p-q is an integer */
p + (q-r); /* 1 + (1-1) == 1 => pointer */
N.B. Никаких претензий к стандартам C. здесь.
В качестве быстрого добавления к ответу @Brian Hooper: "[t] сумма двух указателей означает... er... ничего", однако сумма указателя и целого числа позволяет вам компенсировать исходный указатель.
Вычитание указателя более высокого значения из указателя с более низким значением дает вам смещение между ними. Обратите внимание, что я не учитываю пейджинг памяти здесь; Я предполагаю, что значения памяти находятся в пределах доступной области процесса.
Итак, если у вас есть указатель на серию последовательных ячеек памяти в куче или массив ячеек памяти в стеке (имя переменной переменной уменьшается до указателя), эти указатели (реальный указатель и тот, который распадается на указатель) укажет на вопрос о местоположении ячейки памяти (т.е. элемент [0]
). Добавление целочисленного значения в указатель эквивалентно увеличению индекса в скобках на тот же номер.
#include <stdio.h>
#include <stdlib.h>
int main()
{
// This first declaration does several things (this is conceptual and not an exact list of steps the computer takes):
// 1) allots space on the stack for a variable of type pointer
// 2) allocates number of bytes on the heap necessary to fit number of chars in initialisation string
// plus the NULL termination '\0' (i.e. sizeof(char) * <characters in string> + 1 for '\0')
// 3) changes the value of the variable from step 1 to the memory address of the beginning of the memory
// allocated in step 2
// The variable iPointToAMemoryLocationOnTheHeap points to the first address location of the memory that was allocated.
char *iPointToAMemoryLocationOnTheHeap = "ABCDE";
// This second declaration does the following:
// 1) allots space on the stack for a variable that is not a pointer but is said to decay to a pointer allowing
// us to so the following iJustPointToMemoryLocationsYouTellMeToPointTo = iPointToAMemoryLocationOnTheHeap;
// 2) allots number of bytes on the stack necessary to fit number of chars in initialisation string
// plus the NULL termination '\0' (i.e. sizeof(char) * <characters in string> + 1 for '\0')
// The variable iPointToACharOnTheHeap just points to first address location.
// It just so happens that others follow which is why null termination is important in a series of chars you treat
char iAmASeriesOfConsecutiveCharsOnTheStack[] = "ABCDE";
// In both these cases it just so happens that other chars follow which is why null termination is important in a series
// of chars you treat as though they are a string (which they are not).
char *iJustPointToMemoryLocationsYouTellMeToPointTo = NULL;
iJustPointToMemoryLocationsYouTellMeToPointTo = iPointToAMemoryLocationOnTheHeap;
// If you increment iPointToAMemoryLocationOnTheHeap, you'll lose track of where you started
for( ; *(++iJustPointToMemoryLocationsYouTellMeToPointTo) != '\0' ; ) {
printf("Offset of: %ld\n", iJustPointToMemoryLocationsYouTellMeToPointTo - iPointToAMemoryLocationOnTheHeap);
printf("%s\n", iJustPointToMemoryLocationsYouTellMeToPointTo);
printf("%c\n", *iJustPointToMemoryLocationsYouTellMeToPointTo);
}
printf("\n");
iJustPointToMemoryLocationsYouTellMeToPointTo = iPointToAMemoryLocationOnTheHeap;
for(int i = 0 ; *(iJustPointToMemoryLocationsYouTellMeToPointTo + i) != '\0' ; i++) {
printf("Offset of: %ld\n", (iJustPointToMemoryLocationsYouTellMeToPointTo + i) - iPointToAMemoryLocationOnTheHeap);
printf("%s\n", iJustPointToMemoryLocationsYouTellMeToPointTo + i);
printf("%c\n", *iJustPointToMemoryLocationsYouTellMeToPointTo + i);
}
printf("\n");
iJustPointToMemoryLocationsYouTellMeToPointTo = iAmASeriesOfConsecutiveCharsOnTheStack;
// If you increment iAmASeriesOfConsecutiveCharsOnTheStack, you'll lose track of where you started
for( ; *(++iJustPointToMemoryLocationsYouTellMeToPointTo) != '\0' ; ) {
printf("Offset of: %ld\n", iJustPointToMemoryLocationsYouTellMeToPointTo - iAmASeriesOfConsecutiveCharsOnTheStack);
printf("%s\n", iJustPointToMemoryLocationsYouTellMeToPointTo);
printf("%c\n", *iJustPointToMemoryLocationsYouTellMeToPointTo);
}
printf("\n");
iJustPointToMemoryLocationsYouTellMeToPointTo = iAmASeriesOfConsecutiveCharsOnTheStack;
for(int i = 0 ; *(iJustPointToMemoryLocationsYouTellMeToPointTo + i) != '\0' ; i++) {
printf("Offset of: %ld\n", (iJustPointToMemoryLocationsYouTellMeToPointTo + i) - iAmASeriesOfConsecutiveCharsOnTheStack);
printf("%s\n", iJustPointToMemoryLocationsYouTellMeToPointTo + i);
printf("%c\n", *iJustPointToMemoryLocationsYouTellMeToPointTo + i);
}
return 1;
}
Первая заметная вещь, которую мы делаем в этой программе, - скопировать значение указателя iPointToAMemoryLocationOnTheHeap
в iJustPointToMemoryLocationsYouTellMeToPointTo
. Итак, теперь оба они указывают на то же место памяти в куче. Мы делаем это, чтобы не потерять из виду его начало.
В первом цикле for
мы увеличиваем значение, которое мы просто скопировали в iJustPointToMemoryLocationsYouTellMeToPointTo
(увеличивая его на 1, означает, что он указывает на одну ячейку памяти дальше от iPointToAMemoryLocationOnTheHeap
).
Второй цикл похож, но я хотел более четко показать, как приращение значения связано со смещением и как работает арифметика.
Третий и четвертый циклы повторяют процесс, но работают с массивом в стеке, а не на выделенную память в куче.
Обратите внимание на звездочку *
при печати отдельного char
. Это говорит printf выводить все, на что указывает переменная, а не содержимое самой переменной. Это контрастирует с строкой выше, где баланс строки печатается, и перед переменной нет звездочки, потому что printf() просматривает серию местоположений памяти целиком до достижения NULL.
Вот вывод на ubuntu 15.10, запущенный на i7 (первый и третий выходные петли, начинающиеся со смещения 1, потому что мой выбор цикла for
увеличивается в начале цикла, а не в do{}while()
; Я просто хотел, чтобы это было просто):
Offset of: 1
BCDE
B
Offset of: 2
CDE
C
Offset of: 3
DE
D
Offset of: 4
E
E
Offset of: 0
ABCDE
A
Offset of: 1
BCDE
B
Offset of: 2
CDE
C
Offset of: 3
DE
D
Offset of: 4
E
E
Offset of: 1
BCDE
B
Offset of: 2
CDE
C
Offset of: 3
DE
D
Offset of: 4
E
E
Offset of: 0
ABCDE
A
Offset of: 1
BCDE
B
Offset of: 2
CDE
C
Offset of: 3
DE
D
Offset of: 4
E
E
Потому что результатом этой операции является undefined. Где находится p1 + p2? Как вы можете убедиться, что он указывает на правильно инициализированную память, чтобы ее можно было разыменовать позже? p1 - p2 дает смещение между этими двумя указателями, и этот результат может быть использован далее.
вычитание указателей определяется только в том случае, если они указывают на один и тот же массив объектов. Полученное вычитание масштабируется по размеру объекта, на который они указывают. т.е. вычитание указателя дает количество элементов между двумя указателями.