Bagaimana cara kerja kode C yang mencetak dari 1 hingga 1000 tanpa loop atau pernyataan kondisional bekerja?

148

Saya telah menemukan Ckode yang mencetak dari 1 hingga 1000 tanpa loop atau kondisi : Tapi saya tidak mengerti cara kerjanya. Adakah yang bisa membaca kode dan menjelaskan setiap baris?

#include <stdio.h>
#include <stdlib.h>

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}
ob_dev
sumber
1
Apakah Anda mengkompilasi sebagai C atau sebagai C ++? Kesalahan apa yang Anda lihat? Anda tidak dapat memanggil mainC ++.
ninjalj
@ninjalj Saya telah membuat proyek C ++ dan menyalin / melewati kode kesalahannya adalah: ilegal, operan kiri memiliki tipe 'void (__cdecl *) (int)' dan ekspresi harus menjadi penunjuk ke tipe objek lengkap
ob_dev
1
@ninjalj kode ini bekerja pada ideone.org tapi tidak di visual studio ideone.com/MtJ1M
ob_dev
@oussama serupa, tapi sedikit lebih sulit untuk memahami: ideone.com/2ItXm Terima kasih. :)
Tandai
2
saya telah menghapus semua karakter '&' dari baris ini (& main + (& keluar - & utama) * (j / 1000)) (j + 1); dan kode ini masih berfungsi.
ob_dev

Jawaban:

264

Jangan pernah menulis kode seperti itu.


Karena j<1000, j/1000adalah nol (pembagian integer). Begitu:

(&main + (&exit - &main)*(j/1000))(j+1);

setara dengan:

(&main + (&exit - &main)*0)(j+1);

Yang mana:

(&main)(j+1);

Yang menelepon maindengan j+1.

Jika j == 1000, maka baris yang sama keluar sebagai:

(&main + (&exit - &main)*1)(j+1);

Yang intinya adalah

(&exit)(j+1);

Yang mana exit(j+1)dan meninggalkan program.


(&exit)(j+1)dan exit(j+1)pada dasarnya hal yang sama - mengutip C99 §6.3.2.1 / 4:

Designator fungsi adalah ekspresi yang memiliki tipe fungsi. Kecuali ketika itu adalah operan dari sizeof operator atau unary & operator , penunjuk fungsi dengan tipe " function return type " dikonversi ke ekspresi yang memiliki tipe " pointer ke function return type ".

exitadalah penunjuk fungsi. Bahkan tanpa &operator alamat- unary , itu diperlakukan sebagai pointer ke fungsi. ( &Hanya membuatnya eksplisit.)

Dan panggilan fungsi dijelaskan dalam §6.5.2.2 / 1 dan berikut:

Ekspresi yang menunjukkan fungsi yang dipanggil harus memiliki tipe pointer untuk berfungsi mengembalikan kekosongan atau mengembalikan tipe objek selain tipe array.

Jadi exit(j+1)berfungsi karena konversi otomatis dari tipe fungsi ke tipe pointer-to-function, dan (&exit)(j+1)bekerja dengan konversi eksplisit ke tipe pointer-to-function.

Yang sedang berkata, kode di atas tidak sesuai ( mainmengambil dua argumen atau tidak sama sekali), dan &exit - &main, saya percaya, tidak terdefinisi sesuai dengan §6.5.6 / 9:

Ketika dua pointer dikurangi, keduanya harus menunjuk ke elemen dari objek array yang sama , atau satu melewati elemen terakhir dari objek array; ...

Penambahan itu (&main + ...)akan berlaku dengan sendirinya, dan dapat digunakan, jika jumlah yang ditambahkan adalah nol, karena §6.5.6 / 7 mengatakan:

Untuk keperluan operator-operator ini, sebuah penunjuk ke objek yang bukan merupakan elemen dari array berperilaku sama dengan sebuah penunjuk ke elemen pertama dari array yang panjangnya dengan tipe objek sebagai tipe elemennya.

Jadi menambahkan nol &mainakan baik-baik saja (tapi tidak banyak digunakan).

Tikar
sumber
4
foo(arg)dan (&foo)(arg)setara, mereka memanggil foo dengan argumen arg. newty.de/fpt/fpt.html adalah halaman yang menarik tentang pointer fungsi.
Mat
1
@ Krishnabhadra: dalam kasus pertama, fooadalah sebuah pointer, &fooadalah alamat dari pointer itu. Dalam kasus kedua, fooadalah array, dan &foosetara dengan foo.
Mat
8
Kompleks yang tidak perlu, setidaknya untuk C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Per Johansson
1
&footidak sama dengan fooketika datang ke array. &fooadalah pointer ke array, fooadalah pointer ke elemen pertama. Mereka memiliki nilai yang sama. Untuk fungsi, fundan &funkeduanya adalah petunjuk fungsi.
Per Johansson
1
FYI, jika Anda melihat jawaban yang relevan untuk pertanyaan lain yang dikutip di atas , Anda akan melihat ada variasi yang sebenarnya C99 comformant. Menakutkan, tapi benar.
Daniel Pryden
41

Ini menggunakan rekursi, aritmatika pointer, dan mengeksploitasi perilaku pembulatan divisi integer.

The j/1000putaran jangka turun ke 0 untuk semua j < 1000; sekali jmencapai 1000, itu dievaluasi menjadi 1.

Sekarang jika Anda memiliki a + (b - a) * n, di mana n0 atau 1, Anda berakhir dengan ajika n == 0, dan bjika n == 1. Menggunakan &main(alamat main()) dan &exituntuk adan b, istilah (&main + (&exit - &main) * (j/1000))kembali &mainketika di jbawah 1000, &exitjika tidak. Pointer fungsi yang dihasilkan kemudian diumpankan argumen j+1.

Keseluruhan konstruksi ini menghasilkan perilaku rekursif: sementara di jbawah 1000, mainmenyebut dirinya secara rekursif; ketika jmencapai 1000, ia memanggil exitsebaliknya, membuat program keluar dengan kode keluar 1001 (yang agak kotor, tetapi berfungsi).

tammmer
sumber
1
Jawaban bagus, tapi satu keraguan..Bagaimana jalan keluar utama dengan kode keluar 1001? Utama tidak mengembalikan apa pun..Apakah nilai pengembalian standar?
Krishnabhadra
2
Ketika j mencapai 1000, main tidak berulang lagi; sebaliknya, ia memanggil fungsi libc exit, yang mengambil kode keluar sebagai argumennya dan, yah, keluar dari proses saat ini. Pada titik itu, j adalah 1000, jadi j + 1 sama dengan 1001, yang menjadi kode keluar.
tdammers