Pertimbangkan kode "C" berikut:
#include<stdio.h>
main()
{
printf("func:%d",Func_i());
}
Func_i()
{
int i=3;
return i;
}
Func_i()
didefinisikan di akhir kode sumber dan tidak ada pernyataan yang diberikan sebelum digunakan dalam main()
. Pada waktu ketika kompilator melihat Func_i()
di main()
, itu keluar dari main()
dan tahu Func_i()
. Kompiler entah bagaimana menemukan nilai yang dikembalikan oleh Func_i()
dan memberikannya kepada printf()
. Saya juga tahu bahwa compiler tidak dapat menemukan jenis kembali dari Func_i()
. Ini, secara default mengambil (dugaan?) Yang tipe kembali dari Func_i()
menjadi int
. Itu jika kode memiliki float Func_i()
maka kompiler akan memberikan kesalahan: Jenis konflik untukFunc_i()
.
Dari diskusi di atas kita melihat bahwa:
Kompiler dapat menemukan nilai yang dikembalikan oleh
Func_i()
.- Jika kompiler dapat menemukan nilai yang dikembalikan dengan
Func_i()
keluar darimain()
dan mencari kode sumber, maka mengapa ia tidak dapat menemukan tipe Func_i (), yang secara eksplisit disebutkan.
- Jika kompiler dapat menemukan nilai yang dikembalikan dengan
Compiler harus tahu bahwa itu
Func_i()
adalah tipe float - itu sebabnya ia memberikan kesalahan tipe yang saling bertentangan.
- Jika kompiler tahu bahwa itu
Func_i
adalah tipe float, lalu mengapa ia masih menganggapFunc_i()
tipe int, dan memberikan kesalahan tipe yang saling bertentangan? Mengapa itu tidak dipaksaFunc_i()
menjadi tipe float.
Saya memiliki keraguan yang sama dengan deklarasi variabel . Pertimbangkan kode "C" berikut:
#include<stdio.h>
main()
{
/* [extern int Data_i;]--omitted the declaration */
printf("func:%d and Var:%d",Func_i(),Data_i);
}
Func_i()
{
int i=3;
return i;
}
int Data_i=4;
Kompiler memberikan kesalahan: 'Data_i' tidak dideklarasikan (digunakan pertama kali dalam fungsi ini).
- Ketika kompilator melihat
Func_i()
, ia pergi ke kode sumber untuk menemukan nilai yang dikembalikan oleh Func_ (). Mengapa kompiler tidak dapat melakukan hal yang sama untuk variabel Data_i?
Edit:
Saya tidak tahu detail kerja bagian dalam dari kompiler, assembler, prosesor, dll. Gagasan dasar dari pertanyaan saya adalah bahwa jika saya memberi tahu (tulis) nilai-kembali fungsi dalam kode sumber pada akhirnya, setelah penggunaan dari fungsi itu maka bahasa "C" memungkinkan komputer untuk menemukan nilai itu tanpa memberikan kesalahan. Sekarang mengapa komputer tidak dapat menemukan jenis yang sama. Mengapa tipe Data_i tidak dapat ditemukan karena nilai pengembalian Func_i () ditemukan. Bahkan jika saya menggunakan extern data-type identifier;
pernyataan itu, saya tidak mengatakan nilai yang harus dikembalikan oleh pengenal itu (fungsi / variabel). Jika komputer dapat menemukan nilai itu maka mengapa tidak dapat menemukan jenisnya. Mengapa kita perlu deklarasi maju sama sekali?
Terima kasih.
sumber
Func_i
tidak sah. Tidak pernah ada aturan untuk secara implisit mendeklarasikan variabel yang tidak terdefinisi, sehingga fragmen kedua selalu cacat. (Ya, kompiler masih menerima sampel pertama karena valid, jika ceroboh, di bawah C89 / C90.)Jawaban:
Karena C adalah bahasa pas tunggal , diketik secara statis , diketik dengan lemah , dikompilasi .
Single-pass berarti kompiler tidak melihat ke depan untuk melihat definisi fungsi atau variabel. Karena kompiler tidak melihat ke depan, deklarasi fungsi harus datang sebelum penggunaan fungsi, jika tidak kompiler tidak tahu apa jenis tanda tangannya. Namun, definisi fungsi bisa nanti di file yang sama, atau bahkan di file yang berbeda sama sekali. Lihat poin # 4.
Satu-satunya pengecualian adalah artefak historis yang fungsi dan variabel yang tidak dideklarasikan dianggap bertipe "int". Praktik modern adalah untuk menghindari pengetikan tersirat dengan selalu menyatakan fungsi dan variabel secara eksplisit.
Ketikan statis berarti bahwa semua jenis informasi dikomputasi pada waktu kompilasi. Informasi itu kemudian digunakan untuk menghasilkan kode mesin yang dijalankan pada saat dijalankan. Tidak ada konsep dalam C pengetikan run-time. Sekali int, selalu int, sekali float, selalu float. Namun, fakta itu agak dikaburkan oleh poin berikutnya.
Ketikan yang lemah berarti bahwa kompiler C secara otomatis menghasilkan kode untuk mengkonversi antara tipe numerik tanpa mengharuskan programmer untuk secara eksplisit menentukan operasi konversi. Karena pengetikan statis, konversi yang sama akan selalu dilakukan dengan cara yang sama setiap kali melalui program. Jika nilai float dikonversi ke nilai int di tempat tertentu dalam kode, nilai float akan selalu dikonversi ke nilai int di tempat itu dalam kode. Ini tidak dapat diubah pada saat run-time. Nilai itu sendiri dapat berubah dari satu eksekusi program ke yang berikutnya, tentu saja, dan pernyataan kondisional dapat mengubah bagian kode mana yang dijalankan dalam urutan apa, tetapi satu bagian kode tertentu tanpa pemanggilan fungsi atau kondisional akan selalu melakukan yang tepat operasi yang sama setiap kali dijalankan.
Dikompilasi berarti bahwa proses menganalisis kode sumber yang dapat dibaca manusia dan mengubahnya menjadi instruksi yang dapat dibaca mesin sepenuhnya dilakukan sebelum program berjalan. Ketika kompiler sedang mengkompilasi suatu fungsi, ia tidak memiliki pengetahuan tentang apa yang akan ditemui lebih lanjut dalam file sumber yang diberikan. Namun, begitu kompilasi (dan perakitan, penautan, dll) selesai, setiap fungsi yang dapat dieksekusi berisi pointer numerik ke fungsi yang akan dipanggil saat dijalankan. Itulah sebabnya main () dapat memanggil fungsi lebih jauh di dalam file sumber. Pada saat main () benar-benar dijalankan, itu akan berisi pointer ke alamat Func_i ().
Kode mesin sangat, sangat spesifik. Kode untuk menambahkan dua bilangan bulat (3 + 2) berbeda dari kode untuk menambahkan dua pelampung (3.0 + 2.0). Keduanya berbeda dari menambahkan int ke float (3 + 2.0), dan sebagainya. Compiler menentukan untuk setiap titik dalam suatu fungsi operasi apa yang perlu dilakukan pada titik itu, dan menghasilkan kode yang melakukan operasi yang tepat. Setelah selesai, itu tidak dapat diubah tanpa mengkompilasi ulang fungsi.
Menyatukan semua konsep ini, alasan bahwa main () tidak dapat "melihat" lebih jauh ke bawah untuk menentukan tipe Func_i () adalah bahwa analisis tipe terjadi pada awal proses kompilasi. Pada saat itu, hanya bagian dari file sumber hingga definisi main () telah dibaca dan dianalisis, dan definisi Func_i () belum diketahui oleh kompiler.
Alasan mengapa main () dapat "melihat" di mana Func_i () memanggilnya adalah bahwa panggilan terjadi pada saat run time, setelah kompilasi telah menyelesaikan semua nama dan tipe semua pengidentifikasi, majelis telah mengubah semua fungsi ke kode mesin, dan menautkan telah memasukkan alamat yang benar dari setiap fungsi di setiap tempat namanya.
Saya, tentu saja, telah meninggalkan sebagian besar detail yang mengerikan. Proses yang sebenarnya jauh, jauh lebih rumit. Saya harap saya telah memberikan ikhtisar tingkat tinggi yang cukup untuk menjawab pertanyaan Anda.
Selain itu, harap diingat, apa yang saya tulis di atas berlaku khusus untuk C.
Dalam bahasa lain, kompiler dapat membuat beberapa melewati kode sumber, dan kompiler dapat mengambil definisi Func_i () tanpa itu sudah dideklarasikan sebelumnya.
Dalam bahasa lain, fungsi dan / atau variabel dapat diketik secara dinamis, sehingga satu variabel dapat bertahan, atau satu fungsi dapat dilewati atau dikembalikan, integer, float, string, array, atau objek pada waktu yang berbeda.
Dalam bahasa lain, pengetikan mungkin lebih kuat, yang membutuhkan konversi dari floating-point ke integer harus ditentukan secara eksplisit. Dalam bahasa lain, pengetikan mungkin lebih lemah, yang memungkinkan konversi dari string "3.0" ke float 3.0 ke integer 3 dilakukan secara otomatis.
Dan dalam bahasa lain, kode dapat diinterpretasikan satu baris pada satu waktu, atau dikompilasi ke byte-code dan kemudian diinterpretasikan, atau dikompilasi just-in-time, atau dimasukkan melalui berbagai skema eksekusi lainnya.
sumber
Func_()+1
: di sini pada waktu kompilasi, kompiler harus mengetahui tipeFunc_i()
sehingga dapat menghasilkan kode mesin yang sesuai. Mungkin tidak mungkin bagi rakitan untuk menanganiFunc_()+1
dengan memanggil tipe pada saat run time, atau mungkin tetapi melakukan hal itu akan membuat program lambat pada saat run-time. Saya pikir, itu cukup bagi saya untuk saat ini.int func(...)
... yaitu mereka mengambil daftar argumen variadik. Ini berarti jika Anda mendefinisikan suatu fungsi sebagaiint putc(char)
tetapi lupa untuk mendeklarasikannya, ia akan dipanggil sebagaiint putc(int)
(karena char yang melewati daftar argumen variadic dipromosikan keint
). Jadi, sementara contoh OP berhasil karena tanda tangannya cocok dengan deklarasi implisit, dapat dimengerti mengapa perilaku ini tidak disarankan (dan ditambahkan peringatan yang sesuai).Kendala desain bahasa C adalah bahwa itu seharusnya dikompilasi oleh kompiler satu-pass, yang membuatnya cocok untuk sistem yang sangat terbatas pada memori. Oleh karena itu, kompiler tahu pada titik mana saja hanya tentang hal-hal yang disebutkan sebelumnya. Kompiler tidak dapat melompat maju di sumber untuk menemukan deklarasi fungsi dan kemudian kembali untuk mengkompilasi panggilan ke fungsi itu. Karena itu, semua simbol harus dideklarasikan sebelum digunakan. Anda dapat mendeklarasikan fungsi seperti
di bagian atas atau dalam file header untuk membantu kompilator.
Dalam contoh Anda, Anda menggunakan dua fitur bahasa C yang meragukan yang harus dihindari:
Jika suatu fungsi digunakan sebelum dideklarasikan dengan benar, ini digunakan sebagai “deklarasi implisit”. Kompiler menggunakan konteks langsung untuk mengetahui tanda tangan fungsi. Kompiler tidak akan memindai seluruh kode untuk mencari tahu apa deklarasi sebenarnya.
Jika sesuatu dideklarasikan tanpa tipe, tipe tersebut dianggap
int
. Ini adalah contoh kasus untuk variabel statis atau tipe pengembalian fungsi.Jadi
printf("func:%d",Func_i())
, kami memiliki deklarasi implisitint Func_i()
. Ketika kompiler mencapai definisi fungsiFunc_i() { ... }
, ini kompatibel dengan tipe. Tetapi jika Anda menulisfloat Func_i() { ... }
pada titik ini, Anda memiliki implikasi yang dinyatakanint Func_i()
dan dinyatakan secara eksplisitfloat Func_i()
. Karena dua deklarasi tidak cocok, kompiler memberi Anda kesalahan.Membersihkan beberapa kesalahpahaman
Kompiler tidak menemukan nilai yang dikembalikan oleh
Func_i
. Tidak adanya tipe eksplisit berarti bahwa tipe kembaliint
secara default. Bahkan jika Anda melakukan ini:maka jenisnya akan
int Func_i()
, dan nilai kembali akan terpotong secara diam-diam!Kompiler akhirnya mengetahui tipe sebenarnya
Func_i
, tetapi tidak mengetahui tipe sebenarnya selama deklarasi implisit. Hanya ketika nanti mencapai deklarasi nyata yang bisa mengetahui apakah tipe yang dinyatakan secara implisit itu benar. Tetapi pada saat itu, rakitan untuk pemanggilan fungsi mungkin telah ditulis dan tidak dapat diubah dalam model kompilasi C.sumber
Unit
membuat tipe default yang bagus dari sudut pandang tipe-teori, tetapi gagal dalam kepraktisan dari dekat dengan pemrograman sistem logam yang dirancang untuk B dan C.Func_i()
, ia segera menghasilkan dan menyimpan kode untuk prosesor untuk melompat ke lokasi lain, kemudian menerima beberapa integer, dan kemudian melanjutkan. Ketika kompiler kemudian menemukanFunc_i
definisi, itu memastikan tanda tangan cocok, dan jika mereka lakukan, itu menempatkan perakitanFunc_i()
di alamat itu, dan memberitahu itu untuk mengembalikan beberapa bilangan bulat. Saat Anda menjalankan program, prosesor kemudian mengikuti instruksi tersebut dengan nilai3
.Pertama, program Anda valid untuk standar C90, tetapi tidak untuk yang mengikuti. implisit int (memungkinkan untuk mendeklarasikan fungsi tanpa memberikan tipe pengembaliannya), dan deklarasi fungsi implisit (memungkinkan untuk menggunakan fungsi tanpa mendeklarasikannya) tidak lagi valid.
Kedua, itu tidak berfungsi seperti yang Anda pikirkan.
Jenis hasil adalah opsional di C90, tidak memberikan
int
hasil berarti . Itu juga berlaku untuk deklarasi variabel (tetapi Anda harus memberikan kelas penyimpanan,static
atauextern
).Apa yang dilakukan oleh kompiler ketika melihat
Func_i
dipanggil tanpa deklarasi sebelumnya, mengasumsikan bahwa ada deklarasiitu tidak melihat lebih jauh dalam kode untuk melihat seberapa efektif
Func_i
dinyatakan. JikaFunc_i
tidak dideklarasikan atau didefinisikan, kompiler tidak akan mengubah perilakunya saat kompilasimain
. Deklarasi implisit hanya untuk fungsi, tidak ada untuk variabel.Perhatikan bahwa daftar parameter kosong dalam deklarasi tidak berarti fungsi tidak mengambil parameter (Anda perlu menentukan
(void)
untuk itu), itu berarti bahwa kompiler tidak harus memeriksa jenis parameter dan akan sama konversi implisit yang diterapkan pada argumen yang diteruskan ke fungsi variadic.sumber
extern int Func_i()
. Itu tidak terlihat di mana pun.-S
(jika Anda menggunakangcc
) akan memungkinkan Anda untuk melihat kode perakitan yang dibuat oleh kompiler. Kemudian Anda dapat memiliki gagasan tentang bagaimana nilai-kembali ditangani pada saat dijalankan (biasanya menggunakan register prosesor, atau beberapa ruang pada tumpukan program).Anda menulis dalam komentar:
Itu kesalahpahaman: Eksekusi bukan don baris demi baris. Kompilasi dilakukan baris demi baris, dan resolusi nama dilakukan selama kompilasi, dan itu hanya menyelesaikan nama, bukan mengembalikan nilai.
Model konseptual yang membantu adalah ini: Ketika kompiler membaca baris:
itu memancarkan kode yang setara dengan:
Kompiler juga membuat catatan di beberapa tabel internal yang
function #2
merupakan fungsi belum dideklarasikan bernamaFunc_i
, yang mengambil sejumlah argumen yang tidak ditentukan dan mengembalikan int (default).Nantinya, saat mem-parsing ini:
kompiler mencari
Func_i
di tabel yang disebutkan di atas dan memeriksa apakah parameter dan tipe pengembalian cocok. Jika tidak, itu berhenti dengan pesan kesalahan. Jika ya, itu menambahkan alamat saat ini ke tabel fungsi internal dan melanjutkan ke baris berikutnya.Jadi, kompiler tidak "mencari"
Func_i
ketika menguraikan referensi pertama. Itu hanya membuat catatan di beberapa meja, melanjutkan penguraian baris berikutnya. Dan pada akhir file, ia memiliki file objek, dan daftar alamat lompat.Kemudian, linker mengambil semua ini, dan mengganti semua pointer ke "fungsi # 2" dengan alamat lompatan yang sebenarnya, sehingga memancarkan sesuatu seperti:
Jauh kemudian, ketika file yang dapat dieksekusi dijalankan, alamat lompatan sudah diselesaikan, dan komputer bisa langsung beralih ke alamat 0x1215. Tidak diperlukan pencarian nama.
Penafian : Seperti yang saya katakan, itu adalah model konseptual, dan dunia nyata lebih rumit. Compiler dan linker melakukan semua jenis optimasi gila hari ini. Mereka bahkan mungkin "melompat turun" untuk mencari
Func_i
, meskipun saya ragu. Tetapi bahasa C didefinisikan dengan cara yang Anda bisa menulis kompiler super sederhana seperti itu. Jadi sebagian besar waktu, ini adalah model yang sangat berguna.sumber
1. call "function #2", put the return-type onto the stack and put the return value on the stack?
printf(..., Func_i()+1);
- kompiler harus mengetahui jenisnyaFunc_i
, sehingga ia dapat memutuskan apakah ia harus memancarkanadd integer
atauadd float
instruksi. Anda mungkin menemukan beberapa kasus khusus di mana kompiler dapat berjalan tanpa informasi jenis, tetapi kompiler harus bekerja untuk semua kasus.float
nilai dapat hidup dalam register FPU - maka tidak akan ada instruksi sama sekali. Kompilator hanya melacak nilai yang disimpan di register mana selama kompilasi, dan memancarkan hal-hal seperti "tambahkan konstanta 1 ke register FP X". Atau bisa hidup di stack, jika tidak ada register gratis. Kemudian akan ada "peningkatan stack pointer dengan 4" instruksi, dan nilainya akan "direferensikan" sebagai sesuatu seperti "stack pointer - 4". Tetapi semua hal ini hanya berfungsi jika ukuran semua variabel (sebelum dan sesudah) pada stack diketahui pada waktu kompilasi.Func_i()
atau / danData_i
, ia harus menentukan jenisnya; tidak mungkin dalam bahasa assembly untuk melakukan panggilan ke tipe data. Saya perlu mempelajari berbagai hal secara terperinci agar saya yakin.C dan sejumlah bahasa lain yang membutuhkan deklarasi dirancang di era ketika waktu dan memori prosesor mahal. Pengembangan C dan Unix berjalan seiring untuk beberapa waktu, dan yang terakhir tidak memiliki memori virtual sampai 3BSD muncul pada tahun 1979. Tanpa ruang ekstra untuk bekerja, kompiler cenderung menjadi urusan single-pass karena mereka tidak membutuhkan kemampuan untuk menyimpan beberapa representasi seluruh file dalam memori sekaligus.
Compiler single-pass, seperti kita, dibebani dengan ketidakmampuan untuk melihat ke masa depan. Ini berarti satu-satunya hal yang dapat mereka ketahui dengan pasti adalah apa yang telah mereka katakan secara eksplisit sebelum baris kode dikompilasi. Jelas bagi salah satu dari kita yang
Func_i()
dinyatakan kemudian dalam file sumber, tetapi kompiler, yang beroperasi pada sepotong kecil kode pada suatu waktu, tidak memiliki petunjuk itu datang.Pada awal C (AT&T, K&R, C89), penggunaan fungsi
foo()
sebelum deklarasi menghasilkan deklarasi de facto atau implisit dariint foo()
. Contoh Anda berfungsi saatFunc_i()
dideklarasikanint
karena cocok dengan yang dikompilasikan oleh kompiler atas nama Anda. Mengubahnya ke tipe lain akan menghasilkan konflik karena tidak lagi cocok dengan apa yang dipilih kompilator tanpa adanya deklarasi eksplisit. Perilaku ini dihapus di C99, di mana penggunaan fungsi yang tidak dideklarasikan menjadi kesalahan.Jadi bagaimana dengan tipe pengembalian?
Konvensi pemanggilan untuk kode objek di sebagian besar lingkungan hanya membutuhkan mengetahui alamat fungsi yang dipanggil, yang relatif mudah untuk ditangani oleh kompiler dan penghubung. Eksekusi melompat ke awal fungsi dan kembali ketika kembali. Ada hal lain, terutama pengaturan lewat argumen dan nilai balik, sepenuhnya ditentukan oleh penelepon dan mundur dalam pengaturan yang disebut konvensi pemanggilan . Selama keduanya berbagi set konvensi yang sama, menjadi mungkin bagi suatu program untuk memanggil fungsi dalam file objek lain apakah mereka dikompilasi dalam bahasa apa pun yang berbagi konvensi tersebut. (Dalam komputasi ilmiah, Anda mengalami banyak C memanggil FORTRAN dan sebaliknya, dan kemampuan untuk melakukan itu berasal dari memiliki konvensi panggilan.)
Satu fitur lain dari C awal adalah bahwa prototipe seperti yang kita tahu sekarang tidak ada. Anda bisa mendeklarasikan tipe pengembalian fungsi (misalnya,
int foo()
), tetapi bukan argumennya (yaitu,int foo(int bar)
bukan opsi). Ini ada karena, sebagaimana diuraikan di atas, program selalu menempel pada konvensi panggilan yang dapat ditentukan oleh argumen. Jika Anda memanggil fungsi dengan jenis argumen yang salah, itu adalah sampah, situasi sampah keluar.Karena kode objek memiliki gagasan pengembalian tetapi bukan tipe pengembalian, kompiler harus mengetahui jenis pengembalian untuk menangani nilai yang dikembalikan. Ketika Anda menjalankan instruksi mesin, itu semua hanya bit dan prosesor tidak peduli apakah memori di mana Anda mencoba untuk membandingkan
double
sebenarnya adaint
di dalamnya. Itu hanya melakukan apa yang Anda minta, dan jika Anda memecahkannya, Anda memiliki keduanya.Pertimbangkan bit kode ini:
Kode di sebelah kiri mengkompilasi ke panggilan untuk
foo()
diikuti dengan menyalin hasil yang disediakan melalui konvensi panggilan / kembali ke mana punx
disimpan. Itu kasus yang mudah.Kode di sebelah kanan menunjukkan konversi tipe dan itulah sebabnya kompiler perlu mengetahui tipe pengembalian fungsi. Angka floating-point tidak dapat dibuang ke memori di mana kode lain akan mengharapkan untuk melihat
int
karena tidak ada konversi ajaib yang terjadi. Jika hasil akhirnya harus berupa bilangan bulat, harus ada instruksi yang memandu prosesor untuk melakukan konversi sebelum penyimpanan. Tanpa mengetahui jenis pengembalianfoo()
sebelumnya, kompiler tidak akan tahu bahwa kode konversi diperlukan.Kompiler multi-pass memungkinkan segala macam hal, salah satunya adalah kemampuan untuk mendeklarasikan variabel, fungsi dan metode setelah mereka pertama kali digunakan. Ini berarti bahwa ketika kompiler berkeliling untuk mengkompilasi kode, ia telah melihat masa depan dan tahu apa yang harus dilakukan. Java, misalnya, mengamanatkan multi-pass berdasarkan fakta bahwa sintaksnya memungkinkan deklarasi setelah digunakan.
sumber
Func_i()
nilai pengembalian ditemukan.double foo(); int x; x = foo();
hanya memberikan kesalahan. Saya tahu kita tidak bisa melakukan ini. Pertanyaan saya adalah bahwa dalam pemanggilan fungsi prosesor hanya menemukan nilai pengembalian; mengapa tidak bisa juga menemukan tipe pengembalian juga?foo()
, jadi kompiler tahu apa yang harus dilakukan dengannya.