Что делает компилятор с [i], который является массивом? А что, если a - указатель?
Мне сказали c-faq, что компилятор делает разные вещи для работы с [i], а a - это массив или указатель, Вот пример из c-faq:
char a[] = "hello";
char *p = "world";
Учитывая вышеприведенные объявления, когда компилятор видит выражение a [3], он испускает код для начала в местоположении `` a '', перемещает три мимо него и выбирает там символ. Когда он видит выражение p [3], он испускает код для начала в месте "p", извлекает значение указателя там, добавляет три к указателю и, наконец, извлекает символ, на который указывает.
Но мне сказали, что при работе с [i] компилятор стремится преобразовать (который является массивом) в указатель на массив. Поэтому я хочу видеть коды сборки, чтобы узнать, что правильно.
EDIT:
Здесь источник этого утверждения. c-faq
И обратите внимание на это предложение:
выражение формы a [i] заставляет массив распадаться на указатель, следуя приведенному выше правилу, а затем индексироваться так же, как и указательная переменная в выражении p [i] (хотя возможная память доступ будет иным, "
Я довольно смущен этим: поскольку a заглох на указатель, то почему он имеет в виду, что "доступ к памяти будет другим?"
Здесь мой код:
// array.cpp
#include <cstdio>
using namespace std;
int main()
{
char a[6] = "hello";
char *p = "world";
printf("%c\n", a[3]);
printf("%c\n", p[3]);
}
А вот часть кода сборки, которую я получил, использовал g++ -S array.cpp
.file "array.cpp"
.section .rodata
.LC0:
.string "world"
.LC1:
.string "%c\n"
.text
.globl main
.type main, @function
main:
.LFB2:
leal 4(%esp), %ecx
.LCFI0:
andl $-16, %esp
pushl -4(%ecx)
.LCFI1:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ecx
.LCFI4:
subl $36, %esp
.LCFI5:
movl $1819043176, -14(%ebp)
movw $111, -10(%ebp)
movl $.LC0, -8(%ebp)
movzbl -11(%ebp), %eax
movsbl %al,%eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
movl -8(%ebp), %eax
addl $3, %eax
movzbl (%eax), %eax
movsbl %al,%eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
movl $0, %eax
addl $36, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
Я не могу понять механизм [3] и p [3] из приведенных выше кодов. Например:
- где был "привет" инициализирован?
- Что означает $1819043176? возможно, это адрес памяти "привет" (адрес a)?
- Я уверен, что "-11 (% ebp)" означает [3], но почему?
- В "movl -8 (% ebp),% eax", содержимое poniter p сохраняется в EAX, правильно? Таким образом, $.LC0 означает содержимое указателя p?
- Что означает "movsbl% al,% eax"?
-
И, обратите внимание на эти 3 строки кодов:
movl $1819043176, -14 (% ebp)
movw $111, -10 (% ebp)
movl $.LC0, -8 (% ebp)
Последний использует "movl", но почему он не перезаписал контент -10 (% ebp)? (Теперь я знаю anser:), адрес является инкрементным, и "movl $.LC0 -8 (% ebp) будет только перезаписывать {-8, -7, -6, -5} (% ebp))
Извините, но я полностью смущен этим механизмом, а также код сборки...
Большое спасибо за вашу помощь.
Ответы
Ответ 1
a
- указатель на массив символов. p
- это указатель на char, который, в данном случае, указывает на строковый литерал.
movl $1819043176, -14(%ebp)
movw $111, -10(%ebp)
Инициализирует локальный "привет" в стеке (поэтому на него ссылается через ebp
). Поскольку в приветствии содержится более 4 байтов, требуется две инструкции.
movzbl -11(%ebp), %eax
movsbl %al,%eax
Ссылки a[3]
: двухэтапный процесс связан с ограничением в отношении доступа к памяти, на которую ссылается хотя бы ebp
(мой x86-fu немного ржавый).
movl -8(%ebp), %eax
действительно ссылается на указатель p
.
LC0
ссылается на местоположение "относительной памяти": фиксированная ячейка памяти будет выделена после загрузки программы в память.
movsbl %al,%eax
означает: "перемещайте один байт, ниже" (дайте или возьмите... Мне нужно будет посмотреть его... Я немного ржавый на этом фронте). al
представляют собой байты из регистра eax
.
Ответ 2
Поступая на стороне языка, поскольку сторона ассемблера уже обработана:
Обратите внимание на это предложение: "выражение формы a [i] приводит к распаду массива в указатель, следуя приведенному выше правилу, а затем индексируется так же, как и указательная переменная в выражении p [i] (хотя возможные обращения к памяти будут разными:" Я довольно смущен этим: поскольку a заглох на указатель, то почему он имеет в виду, что "обращения к памяти будут разными?"
Это происходит потому, что после разложения доступ равен для (теперь значения указателя) и указателя. Но разница в том, как именно это значение указателя получает в первую очередь. Рассмотрим пример:
char c[1];
char cc;
char *pc = &cc;
Теперь у вас есть массив. Этот массив не принимает никакого хранилища, кроме одного char! Для него нет указателя. И у вас есть указатель, указывающий на char. Указатель принимает размер одного адреса, и у вас есть один char, на который указывает указатель. Теперь посмотрим, что произойдет для случая массива, чтобы получить значение указателя:
c[0] = 'A';
// #1: equivalent: *(c + 0) = 'A';
// #2: => 'c' appears not in address-of or sizeof
// #3: => get address of "c": This is the pointer value P1
Случай с указателем отличается:
pc[0] = 'A';
// #1: equivalent: *(pc + 0) = 'A';
// #2: => pointer value is stored in 'pc'
// #3: => thus: read address stored in 'pc': This is the pointer value P1
Как вы видите, для случая массива для получения значения указателя, в котором мы добавляем значение индекса (в данном случае скучное 0
), нам не нужно читать из памяти, потому что адрес array уже является значением указателя. Но для случая указателя нужный нам указатель хранится в указателе: нам нужен один из памяти, чтобы получить этот адрес.
После этого путь равен для обоих:
// #4: add "0 * sizeof(char)" to P1. This is the address P2
// #5: store 'A' to address P2
Вот код ассемблера, сгенерированный для массива и case-указателя:
add $2, $0, 65 ; write 65 into r2
stb $2, $0, c ; store r2 into address of c
# pointer case follows
ldw $3, $0, pc ; load value stored in pc into r3
add $2, $0, 65 ; write 65 into r2
stb $2, $3, 0 ; store r2 into address loaded to r3
Мы можем просто сохранить 65
(ASCII для 'A'
) по адресу c
(который будет известен уже во время компиляции или ссылки, когда он является глобальным). Для случая с указателем сначала нужно загрузить адрес, хранящийся в нем, в регистр 3
, а затем записать 65
на этот адрес.
Ответ 3
Хотя верно, что массивы не являются указателями, они ведут себя очень похоже. В обоих случаях компилятор внутренне сохраняет адрес в типизированном элементе, и в обоих случаях может быть один или несколько элементов.
В обоих массивах и указателях при разыменовании оператором [] компилятор оценивает адрес элемента, который вы индексируете, умножая индекс на размер типа данных и добавляя его к адресу указателя или массив.
Фундаментальное различие между указателем и массивами состоит в том, что массив по существу является ссылкой. Если законным является инициализация указателя на нуль или изменение значения, которое хранит указатель, массивы не могут быть нулевыми, и они не могут быть установлены в другие массивы; они по сути являются постоянными указателями, которые не могут быть установлены в нуль.
Кроме того, можно выделять массивы в стеке, и это невозможно для указателей (хотя указатели могут быть установлены на адреса в стеке, но это может стать уродливым).
Ответ 4
Эти определения выглядят одинаково, но на самом деле совершенно разные.
Предположим, что ваши массивы объявлены внутри функции:
void f()
{
char a[] = "hello";
char *p = "world";
}
В первом случае 'a' распадается на указатель const, который указывает на 6 символов в STACK.
Во втором случае "p" является неконстантным указателем, который указывает на 6 символов в области CONST (сегмент данных).
Его вполне законно писать:
a[3] = 'L';
но
p[3] = 'L';
выглядит корректно, но вызывает нарушение памяти, потому что массив символов находится не в стеке, а в разделе только для чтения.
Кроме того,
a++
является незаконным ('a' распадается на указатель const, который является r-значением), но
p++
является законным (p является l-значением).