2015-08-17 1 views
-6

Следующее another question which caused much confusion, вот вопрос о семантике указателя, который, мы надеемся, прояснит ситуацию:Является ли memcpy указателя таким же, как присваивание?

Эта программа действительно во всех случаях? Единственная интересная часть - в ветке pa1 == pb.

#include <stdio.h> 
#include <string.h> 

int main() { 
    int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b; 
    if (memcmp (&pa1, &pb, sizeof pa1) == 0) { 
     int *p; 
     printf ("pa1 == pb\n"); // interesting part 
     memcpy (&p, &pa1, sizeof p); // make a copy of the representation 
     memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now 
     // and the bytes of pa1 happens to be the bytes of pb 
     *pa1 = 2; // does pa1 legally point to b? 
    } 
    else { 
     printf ("pa1 != pb\n"); // failed experiment, nothing to see 
     pa1 = &a[0]; // ensure well defined behavior in printf 
    } 
    printf ("b = %d *pa1 = %d\n", b, *pa1); 
    return 0; 
} 

Я хотел бы получить ответ на основе стандартных котировок.

EDIT

По многочисленным просьбам, вот что я хочу знать:

  • является указателем в семантическом «значение» (его поведение в соответствии со спецификацией) определяется только его числовым значением (численный адрес, который он содержит), для указателя данного типа?
  • Если нет, , то можно скопировать только физический адрес, содержащийся в указателе, при этом оставляя связанную семантику?

Здесь, допустим, кто-то мимо конечного указателя случайно случайно указывает на другой объект; как я могу использовать такой один за конечным указателем для доступа к другому объекту?

Я имею право делать что-либо, кроме использования копии адреса другого объекта. (Это игра для понимания указателей в C.)

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

+0

Да, это то же самое –

+1

Что такое 'memcpy (& p, & pa1, sizeof p)' и 'memcpy (& pa1, & p, sizeof p)', который должен делать своими словами? Кроме того, вы действительно ** должны ** добавить некоторые строки о своих намерениях с помощью memcmp (см. Мой комментарий к Sourav Ghosh). – DevSolar

+0

@DevSolar копирует физическое значение, точно так же, как присвоение 'int'; не передавать семантику – curiousguy

ответ

4

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

Так что да, действуя, например, memcpy (&p, &pa1, sizeof p) эквивалентен присвоению p = pa1, но может быть менее эффективным.


Давайте попробуем это немного по-другому, вместо:

У вас есть pa1, который указывает на какой-то объект (или, вернее, один за какой-нибудь предмет), то у вас есть указатель &pa1, который указывает на переменную pa1 (т.е. где переменная pa1 находится в памяти).

Графически это будет выглядеть примерно так:

 
+------+  +-----+  +-------+ 
| &pa1 | --> | pa1 | --> | &a[1] | 
+------+  +-----+  +-------+ 

[Примечание: &a[0] + 1 такое же, как &a[1]]

+0

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

+0

ОК, так же как вы не обнаруживаете неопределенного поведения при разыменовании прошлого указателя конца, который случайно может указывать на какой-то объект? – curiousguy

+0

@curiousguy Но нет разыменования любого указателя. Если вы написали, например. 'pa1' или' p' (без адреса-оператора), то да, у вас было бы неопределенное поведение. –

2
*pa1 = 2; // does pa1 legally point to b? 

Нет, что указывает на pa1b является чисто случайным. Обратите внимание, что программа должна соответствовать при компиляции, что указатель имеет одинаковое значение во время выполнения, не имеет значения.

Никто не может отличить, нет?

Оптимизатор компилятора может отличить! Оптимизатор компилятора может видеть (через статический анализ кода), что b и никогда не доступен через «легальный» указатель, поэтому он допускает сохранение b в регистре. Это решение принимается при компиляции.

Итог:

указатели «Правовые» указатели, полученные от юридического указателя по заданию или путем копирования памяти. Вы также можете получить «легальный» указатель, используя арифметику указателя, если результирующий указатель находится в пределах допустимого диапазона блока/блока памяти, которому он был назначен/скопирован. Если результат арифметики указателя указывает на действительный адрес в другом блоке памяти, использование такого указателя по-прежнему является UB.

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

+0

«Также обратите внимание, что сравнение указателей действует только в том случае, если два указателя указывают на тот же массив/блок памяти». «Что вы имеете в виду? – curiousguy

+1

@curiousguy: 'int x = 1, y = 2, * px = & x, * py = & y;' - поскольку 'x' и' y' не находятся в одном массиве, 'if (px DevSolar

+1

Если вы сравниваете [i] с & b, результат не определяется стандартом. Стандарт допускает магию, так что, даже если они сравнивают равные, они не должны указывать на одну и ту же память. –

2

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

Если мы вернемся к эпохе MS-DOS, у нас были около указателей (относительно определенного сегмента) и дальних указателей (содержащих как сегмент, так и смещение).

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

В этом случае у вас может быть два указателя с одним и тем же битовым рисунком, где один указатель указывает на сегмент массива (pa), а другой указатель указывает на сегмент стека (pb). Указатели сравнивали равные, но все же указывали на разные вещи.

Чтобы сделать это хуже, дальние указатели с сегментом: смещенная пара может быть сформирована с перекрывающимися сегментами, так что разные битовые шаблоны все еще указывали на один и тот же физический адрес памяти. Например, 0100:0210 - это тот же адрес, что и 0120:0010.

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

+0

Итак, компилятор должен был бы отслеживать, какие указатели указывали на какой массив? – curiousguy

+0

@curiousguy: в моделях с малой и средней памятью указатели с «далеким» определителем занимали четыре байта для хранения и могли получить доступ к чему-либо; те, у кого нет квалификатора, взяли два байта для хранения и могли получить доступ ко всем объектам, созданным в коде, любыми средствами, кроме специальных вызовов «дальнего malloc»; доступ через почти указатели часто был в 2-3 раза быстрее, чем доступ через «дальние» указатели; в то время как необходимость указывать дальние указатели в некоторых местах была немного неприятной, выгоды от использования небольшой или средней модели вместо большой модели часто были довольно огромными. – supercat

2

Неопределенное поведение: игра в n части.

Compiler1 и Compiler2 ввод, stage правый.

int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b; 

[Compiler1] Здравствуйте, a, pa1, b, pb. Как приятно познакомиться. Теперь вы просто сидите прямо здесь, мы рассмотрим остальную часть кода, чтобы узнать, можем ли вы выделить вам какое-то хорошее пространство стека.

Компилятор1 просматривает остальную часть кода, иногда нахмурившись и делая некоторые отметки на бумаге. Компилятор2 выбирает нос и смотрит в окно.

[Compiler1] Ну, боюсь, b, что я решил вас оптимизировать. Я просто не мог обнаружить, что изменило вашу память. Возможно, ваш программист сделал некоторые трюки с Undefined Behavior, чтобы обойти это, но мне позволено предположить, что такого UB нет. Прости.

Выход b, преследуемый медведем.

[Compiler2] Wait! Держись на секунду, b. Я не мог потрудиться, чтобы оптимизировать этот код, поэтому я решил дать вам приятное уютное место в стеке.

b прыгает в ликовании, но убит носовыми демонами, как только он изменен с помощью неопределенного поведения.

[Рассказчик] Таким образом, заканчивается печальная, грустная история перемен b. Мораль этой истории заключается в том, что никогда не может полагаться на неопределенное поведение.

+0

Ну, он берет адрес 'b', который впоследствии передается в функции, или это действительно будет четким. ;-) – DevSolar

+0

Я не уверен, что здесь есть UB! – curiousguy

2

До C99 ожидается, что реализации должны вести себя так, как если бы значение каждой переменной любого типа хранилось в последовательности значений unsigned char; если бы лежащие в основе представления двух переменных одного и того же типа были рассмотрены и признаны равными, это означало бы, что если бы не определено неопределенное поведение , то уже было бы, их значения, как правило, были бы равными и взаимозаменяемыми. В двух местах было немного двусмысленности, например. учитывая

char *p,*q; 
p = malloc(1); 
free(p); 
q = malloc(1); 
if (!memcmp(&p, &q, sizeof p)) 
    p[0] = 1; 

каждую версию C сделала совершенно ясно, что q может или не может равно p, и если q не равен p кода следует ожидать, что что-нибудь может случиться, когда p[0] написано. Хотя в стандарте C89 явно не указано, что реализация может иметь только p, сравнивают поразрядное значение с q, если запись в p будет эквивалентна записи в q, такое поведение обычно подразумевается моделью полной инкапсуляции последовательностей от unsigned char значений.

C99 добавил ряд ситуаций, в которых переменные могут сравниваться поразрядно, но не быть эквивалентными. Рассмотрим, например:

extern int doSomething(char *p1, char *p2); 
int act1(char * restrict p1, char * restrict p2) 
    { return doSomething(p1,p2); } 
int act2(char * restrict p) 
    { return doSomething(p,p); } 
int x[4]; 
int act3a(void) { return act1(x,x); } 
int act3b(void) { return act2(x); } 
int act3c(void) { return doSomething(x,x); } 

Вызов act3a, act3b или act3c заставит doSomething() быть вызвана с двумя указателями, которые сравнивают равно x, но если вызывается через act3a, любой элемент x, который написан в doSomething сусле доступ к ним осуществляется исключительно с использованием x, исключительно с использованием p1 или исключительно с использованием p2.Если он вызван через act3b, метод получит свободу писать элементы с использованием p1 и получить к ним доступ через p2 или наоборот. При доступе через act3c способ может использовать p1, p2 и x взаимозаменяемо. Ничто в двоичных представлениях p1 или p2 бы указать, могут ли они быть использованы взаимозаменяемо с x, но компилятор будет разрешен в линии расширения doSomething в пределах act1 и act2 и имеет поведение этих разложений варьируются в зависимости от того, что указателя доступов были разрешено и запрещено.