Bagaimana potongan kode ini menentukan ukuran array tanpa menggunakan sizeof ()?

134

Melalui beberapa pertanyaan wawancara C, saya telah menemukan pertanyaan yang menyatakan "Bagaimana menemukan ukuran array di C tanpa menggunakan sizeof operator?", Dengan solusi berikut. Itu berhasil, tetapi saya tidak mengerti mengapa.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

Seperti yang diharapkan, ini mengembalikan 5.

edit: orang-orang menunjukkan jawaban ini , tetapi sintaksnya sedikit berbeda, yaitu metode pengindeksan

size = (&arr)[1] - arr;

jadi saya yakin kedua pertanyaan itu valid dan memiliki pendekatan masalah yang sedikit berbeda. Terima kasih atas bantuan yang sangat besar dan penjelasan yang menyeluruh!

janojlic.dll
sumber
13
Yah, tidak dapat menemukannya, tetapi sepertinya memang benar. Lampiran J.2 secara eksplisit menyatakan: Operand dari operator unary * memiliki nilai yang tidak valid adalah perilaku yang tidak ditentukan. Di sini &a + 1tidak menunjuk ke objek yang valid, jadi tidak valid.
Eugene Sh.
5
Terkait: Apakah *((*(&array + 1)) - 1)aman digunakan untuk mendapatkan elemen terakhir dari larik otomatis? . tl; dr *(&a + 1)memanggil Undefined Behvaior
Spikatrix
5
Kemungkinan duplikat dari Temukan ukuran larik tanpa menggunakan sizeof di C
Alma Do
@AlmaDo baik sintaksnya sedikit berbeda, yaitu bagian pengindeksan, jadi saya yakin bahwa pertanyaan ini masih valid sendiri, tetapi saya mungkin salah. Terimakasih telah menunjukkan itu!
janojlic
1
@janojlicz Pada dasarnya mereka sama, karena (ptr)[x]sama dengan *((ptr) + x).
SS Anne

Jawaban:

137

Saat Anda menambahkan 1 ke penunjuk, hasilnya adalah lokasi objek berikutnya dalam urutan objek jenis yang diarahkan ke (yaitu, larik). Jika pmenunjuk ke suatu intobjek, maka p + 1akan menunjuk ke yang berikutnya intsecara berurutan. Jika pmenunjuk ke larik 5 elemen dari int(dalam hal ini, ekspresi &a), maka p + 1akan menunjuk ke larik 5 elemenint berikutnya secara berurutan.

Mengurangi dua pointer (asalkan keduanya menunjuk ke objek larik yang sama, atau satu menunjuk satu melewati elemen terakhir dari larik) menghasilkan jumlah objek (elemen larik) di antara dua penunjuk tersebut.

Ekspresi &amenghasilkan alamat a, dan memiliki tipe int (*)[5](penunjuk ke array 5-elemen int). Ekspresi tersebut &a + 1menghasilkan alamat dari larik 5 elemen intberikutnya a, dan juga memiliki tipe int (*)[5]. Ekspresi tersebut *(&a + 1)membedakan hasil dari &a + 1, sehingga menghasilkan alamat dari elemen pertama intsetelah elemen terakhir a, dan memiliki tipe int [5], yang dalam konteks ini "meluruh" menjadi ekspresi tipe int *.

Demikian pula, ekspresi a"meluruh" menjadi penunjuk ke elemen pertama dari array dan memiliki tipe int *.

Sebuah gambar dapat membantu:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

Ini adalah dua tampilan dari penyimpanan yang sama - di sebelah kiri, kami melihatnya sebagai urutan array 5 elemen int, sementara di sebelah kanan, kami melihatnya sebagai urutan int. Saya juga menunjukkan berbagai ekspresi dan tipenya.

Sadarilah, ekspresi *(&a + 1)menghasilkan perilaku tidak terdefinisi :

...
Jika hasil menunjuk satu melewati elemen terakhir dari objek array, itu tidak akan digunakan sebagai operan dari operator * unary yang dievaluasi.

C 2011 Online Draft , 6.5.6 / 9

John Bode
sumber
13
Teks "tidak boleh digunakan" itu resmi: C 2018 6.5.6 8.
Eric Postpischil
@EricPostpischil: Apakah Anda memiliki tautan ke draf pra-pub 2018 (mirip dengan N1570.pdf)?
John Bode
1
@ JohnBode: Jawaban ini memiliki tautan ke Mesin Wayback . Saya memeriksa standar resmi dalam salinan yang saya beli.
Eric Postpischil
7
Jadi apakah salah menulis size = (int*)(&a + 1) - a;kode ini akan benar-benar valid? : o
Gizmo
@Gizmo mereka mungkin awalnya tidak menulis itu karena dengan cara itu Anda harus menentukan jenis elemen; aslinya mungkin ditulis didefinisikan sebagai makro untuk penggunaan tipe-umum pada tipe elemen yang berbeda.
Leushenko
35

Baris ini yang paling penting:

size = *(&a + 1) - a;

Seperti yang Anda lihat, alamat tersebut mengambil alamat adan menambahkan satu alamat . Kemudian, itu membedakan penunjuk itu dan mengurangi nilai aslinya adarinya.

Aritmatika pointer di C menyebabkan ini mengembalikan jumlah elemen dalam array, atau 5. Menambahkan satu dan &amerupakan penunjuk ke larik berikutnya dari 5 intdetik setelahnya a. Setelah itu, kode ini membedakan pointer yang dihasilkan dan mengurangi a(tipe array yang telah meluruh menjadi pointer) dari itu, memberikan jumlah elemen dalam array.

Detail tentang cara kerja aritmatika penunjuk:

Katakanlah Anda memiliki penunjuk xyzyang menunjuk ke suatu inttipe dan berisi nilainya (int *)160. Saat Anda mengurangi angka apa pun xyz, C menentukan bahwa jumlah sebenarnya yang dikurangi xyzadalah angka tersebut dikalikan ukuran jenis yang dituju. Misalnya, jika Anda dikurangi 5dari xyz, nilai xyzyang dihasilkan akan xyz - (sizeof(*xyz) * 5)jika aritmetik pointer tidak berlaku.

Seperti aarray 5 inttipe, nilai yang dihasilkan adalah 5. Namun, ini tidak akan bekerja dengan pointer, hanya dengan array. Jika Anda mencoba ini dengan penunjuk, hasilnya akan selalu seperti ini 1.

Berikut adalah contoh kecil yang menunjukkan alamat dan bagaimana ini tidak ditentukan. Sisi kiri menunjukkan alamat:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Ini berarti bahwa kode tersebut mengurangkan adari &a[5](atau a+5), memberikan 5.

Perhatikan bahwa ini adalah perilaku yang tidak ditentukan, dan tidak boleh digunakan dalam keadaan apa pun. Jangan berharap perilaku ini konsisten di semua platform, dan jangan menggunakannya dalam program produksi.

SS Anne
sumber
27

Hmm, saya curiga ini adalah sesuatu yang tidak akan berhasil di masa-masa awal C. Meskipun pintar.

Mengambil langkah satu per satu:

  • &a mendapat penunjuk ke objek berjenis int [5]
  • +1 mendapatkan objek seperti itu berikutnya dengan asumsi ada array dari mereka
  • * secara efektif mengubah alamat itu menjadi tipe pointer ke int
  • -a mengurangi dua penunjuk int, mengembalikan jumlah instance int di antara keduanya.

Saya tidak yakin itu sepenuhnya legal (dalam hal ini yang saya maksud adalah hukum pengacara bahasa - tidak akan berhasil dalam praktiknya), mengingat beberapa jenis operasi sedang berlangsung. Misalnya Anda hanya "diizinkan" untuk mengurangi dua pointer saat menunjuk ke elemen dalam larik yang sama. *(&a+1)disintesis dengan mengakses larik lain, meskipun larik induk, jadi sebenarnya bukan penunjuk ke larik yang sama seperti a. Selain itu, saat Anda diizinkan untuk mensintesis penunjuk melewati elemen terakhir dari sebuah larik, dan Anda dapat memperlakukan objek apa pun sebagai larik dari 1 elemen, operasi dereferencing ( *) tidak "diizinkan" pada penunjuk yang disintesis ini, meskipun itu tidak memiliki perilaku dalam kasus ini!

Saya menduga bahwa pada hari-hari awal C (sintaks K&R, siapa?), Sebuah array membusuk menjadi pointer jauh lebih cepat, jadi *(&a+1)mungkin hanya mengembalikan alamat pointer berikutnya tipe int **. Definisi yang lebih ketat dari C ++ modern pasti memungkinkan pointer ke tipe array ada dan mengetahui ukuran array, dan mungkin standar C mengikutinya. Semua kode fungsi C hanya menggunakan pointer sebagai argumen, jadi perbedaan teknis yang terlihat minimal. Tapi saya hanya menebak-nebak di sini.

Pertanyaan legalitas terperinci semacam ini biasanya berlaku untuk juru bahasa C, atau alat jenis lint, daripada kode yang dikompilasi. Seorang juru bahasa mungkin mengimplementasikan larik 2D sebagai larik penunjuk ke larik, karena ada satu fitur runtime yang lebih sedikit untuk diterapkan, dalam hal ini dereferensi +1 akan berakibat fatal, dan bahkan jika berhasil akan memberikan jawaban yang salah.

Kelemahan lain yang mungkin adalah kompilator C mungkin menyelaraskan larik terluar. Bayangkan jika ini adalah array 5 chars ( char arr[5]), ketika program melakukan &a+1itu memanggil perilaku "array of array". Kompilator mungkin memutuskan bahwa array dari array 5 chars ( char arr[][5]) sebenarnya dibuat sebagai array dari 8 chars ( char arr[][8]), sehingga array luar sejajar dengan baik. Kode yang kita diskusikan sekarang akan melaporkan ukuran array sebagai 8, bukan 5. Saya tidak mengatakan kompiler tertentu pasti akan melakukan ini, tetapi mungkin saja.

Permata Taylor
sumber
Cukup adil. Namun untuk alasan yang sulit dijelaskan, semua orang menggunakan sizeof () / sizeof ()?
Permata Taylor
5
Kebanyakan orang melakukannya. Misalnya, sizeof(array)/sizeof(array[0])memberikan jumlah elemen dalam sebuah array.
SS Anne
Kompiler C diizinkan untuk menyelaraskan array, tetapi saya tidak yakin itu diizinkan untuk mengubah jenis array setelah melakukannya. Alignment akan diimplementasikan secara lebih realistis dengan memasukkan byte padding.
Kevin
1
Pengurangan pointer tidak dibatasi hanya pada dua pointer ke dalam larik yang sama — pointer juga diperbolehkan berada satu melewati ujung larik. &a+1didefinisikan. Seperti yang dicatat oleh John Bollinger, *(&a+1)tidak, karena ia mencoba untuk membedakan objek yang tidak ada.
Eric Postpischil
5
Kompiler tidak dapat mengimplementasikan char [][5]sebagai char arr[][8]. Sebuah array hanyalah objek yang berulang di dalamnya; tidak ada bantalan. Tambahannya, ini akan mematahkan contoh (non-normatif) 2 di C 2018 6.5.3.4 7, yang memberi tahu kita bahwa kita dapat menghitung jumlah elemen dalam array dengan sizeof array / sizeof array[0].
Eric Postpischil