Mengapa * deklarasi * data dan fungsi diperlukan dalam bahasa C, ketika definisi ditulis di akhir kode sumber?

15

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:

  1. Kompiler dapat menemukan nilai yang dikembalikan oleh Func_i().

    • Jika kompiler dapat menemukan nilai yang dikembalikan dengan Func_i()keluar dari main()dan mencari kode sumber, maka mengapa ia tidak dapat menemukan tipe Func_i (), yang secara eksplisit disebutkan.
  2. 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_iadalah tipe float, lalu mengapa ia masih menganggap Func_i()tipe int, dan memberikan kesalahan tipe yang saling bertentangan? Mengapa itu tidak dipaksa Func_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.

pengguna106313
sumber
7
Kompiler tidak "menemukan" nilai yang dikembalikan oleh Func_i. ini dilakukan pada waktu eksekusi.
James McLeod
26
Saya tidak mengundurkan diri, tetapi pertanyaannya didasarkan pada beberapa kesalahpahaman serius tentang bagaimana kompiler bekerja, dan tanggapan Anda dalam komentar menyarankan Anda masih memiliki beberapa kendala konseptual untuk diatasi.
James McLeod
4
Perhatikan bahwa kode sampel pertama belum valid, kode sesuai standar selama lima belas tahun terakhir; C99 membuat tidak adanya tipe pengembalian dalam definisi fungsi dan pernyataan implisit Func_itidak 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.)
Jonathan Leffler
19
@ user31782: Intinya pertanyaan: Mengapa bahasa X melakukan / mengharuskan Y? Karena itu adalah pilihan yang dibuat oleh para desainer. Anda tampaknya berpendapat bahwa perancang salah satu bahasa paling sukses yang seharusnya membuat pilihan yang berbeda beberapa dekade yang lalu daripada mencoba memahami pilihan-pilihan itu dalam konteks mereka dibuat. Jawaban atas pertanyaan Anda: Mengapa kami perlu meneruskan pernyataan? telah diberikan: Karena C menggunakan kompiler sekali pakai. Jawaban paling sederhana untuk sebagian besar pertanyaan tindak lanjut Anda adalah Karena itu tidak akan menjadi kompiler satu-pass.
Mr.Mindor
4
@ user31782 Anda benar-benar ingin membaca buku naga untuk mendapatkan pemahaman tentang bagaimana sebenarnya kompiler dan prosesor bekerja - tidak mungkin untuk menyaring semua pengetahuan yang diperlukan menjadi satu jawaban SO (atau bahkan 100 pada saat itu). Buku bagus untuk siapa saja yang tertarik pada kompiler.
Voo

Jawaban:

26

Karena C adalah bahasa pas tunggal , diketik secara statis , diketik dengan lemah , dikompilasi .

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Clement Cherlin
sumber
1
Terima kasih atas jawaban lengkapnya. Jawaban Anda dan nikie adalah apa yang ingin saya ketahui. Contoh Func_()+1: di sini pada waktu kompilasi, kompiler harus mengetahui tipe Func_i()sehingga dapat menghasilkan kode mesin yang sesuai. Mungkin tidak mungkin bagi rakitan untuk menangani Func_()+1dengan 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.
user106313
1
Detail penting dari fungsi C yang dinyatakan secara implisit: Mereka dianggap bertipe int func(...)... yaitu mereka mengambil daftar argumen variadik. Ini berarti jika Anda mendefinisikan suatu fungsi sebagai int putc(char)tetapi lupa untuk mendeklarasikannya, ia akan dipanggil sebagai int putc(int)(karena char yang melewati daftar argumen variadic dipromosikan ke int). Jadi, sementara contoh OP berhasil karena tanda tangannya cocok dengan deklarasi implisit, dapat dimengerti mengapa perilaku ini tidak disarankan (dan ditambahkan peringatan yang sesuai).
uliwitness
37

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

int Func_i();

di bagian atas atau dalam file header untuk membantu kompilator.

Dalam contoh Anda, Anda menggunakan dua fitur bahasa C yang meragukan yang harus dihindari:

  1. 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.

  2. 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 implisit int Func_i(). Ketika kompiler mencapai definisi fungsi Func_i() { ... }, ini kompatibel dengan tipe. Tetapi jika Anda menulis float Func_i() { ... }pada titik ini, Anda memiliki implikasi yang dinyatakan int Func_i()dan dinyatakan secara eksplisit float 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 kembali intsecara default. Bahkan jika Anda melakukan ini:

    Func_i() {
        float f = 42.3;
        return f;
    }

    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.

amon
sumber
3
@ user31782: Urutan kode penting pada waktu kompilasi, tetapi tidak pada saat dijalankan. Kompilator keluar dari gambar saat program berjalan. Pada saat dijalankan, fungsi akan telah dikumpulkan dan ditautkan, alamatnya akan dipecahkan dan dimasukkan ke dalam placeholder alamat panggilan. (Ini sedikit lebih rumit dari itu, tapi itu ide dasarnya.) Prosesor dapat berkembang maju atau mundur.
Blrfl
20
@ user31782: Kompiler tidak mencetak nilai. Kompiler Anda tidak menjalankan program !!
Lightness Races with Monica
1
@LightnessRacesinOrbit Saya tahu itu. Saya keliru menulis compiler di komentar saya di atas karena saya lupa nama pengolah .
user106313
3
@Carcigenicate C sangat dipengaruhi oleh bahasa B, yang hanya memiliki satu tipe: tipe numerik integral lebar kata yang juga dapat digunakan untuk pointer. C awalnya menyalin perilaku ini, tetapi sekarang sepenuhnya dilarang sejak standar C99. Unitmembuat 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.
amon
2
@ user31782: Kompilator harus mengetahui jenis variabel untuk menghasilkan perakitan yang benar untuk prosesor. Ketika kompiler menemukan implisit 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 menemukan Func_idefinisi, itu memastikan tanda tangan cocok, dan jika mereka lakukan, itu menempatkan perakitan Func_i()di alamat itu, dan memberitahu itu untuk mengembalikan beberapa bilangan bulat. Saat Anda menjalankan program, prosesor kemudian mengikuti instruksi tersebut dengan nilai 3.
Mooing Duck
10

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.

  1. Jenis hasil adalah opsional di C90, tidak memberikan inthasil berarti . Itu juga berlaku untuk deklarasi variabel (tetapi Anda harus memberikan kelas penyimpanan, staticatau extern).

  2. Apa yang dilakukan oleh kompiler ketika melihat Func_idipanggil tanpa deklarasi sebelumnya, mengasumsikan bahwa ada deklarasi

    extern int Func_i();

    itu tidak melihat lebih jauh dalam kode untuk melihat seberapa efektif Func_idinyatakan. Jika Func_itidak dideklarasikan atau didefinisikan, kompiler tidak akan mengubah perilakunya saat kompilasi main. 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.

Pemrogram
sumber
Jika kompiler dapat menemukan nilai yang dikembalikan oleh Func_i () dengan keluar dari main () dan mencari kode sumber, maka mengapa ia tidak dapat menemukan jenis Func_i (), yang disebutkan secara eksplisit.
user106313
1
@ user31782 Jika tidak ada pernyataan Func_i sebelumnya, ketika melihat bahwa Func_i digunakan dalam ekspresi panggilan, berperilaku seolah-olah ada extern int Func_i(). Itu tidak terlihat di mana pun.
Pemrogram
1
@ user31782, kompiler tidak melompat ke mana pun. Ini akan memancarkan kode untuk memanggil fungsi itu; nilai yang dikembalikan akan ditentukan pada saat run-time. Nah, dalam kasus fungsi sederhana seperti itu yang ada di unit kompilasi yang sama, fase optimasi mungkin sejalan fungsi, tapi itu bukan sesuatu yang harus Anda pikirkan ketika mempertimbangkan aturan bahasa, itu adalah optimasi.
Pemrogram
10
@ user31782, Anda memiliki kesalahpahaman serius tentang cara kerja program. Sangat serius sehingga saya tidak berpikir p.se adalah tempat yang baik untuk memperbaikinya (mungkin obrolan, tetapi saya tidak akan mencoba melakukannya).
Pemrogram
1
@ user31782: Menulis cuplikan kecil dan mengompilasinya -S(jika Anda menggunakan gcc) 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).
Giorgio
7

Anda menulis dalam komentar:

Eksekusi dilakukan baris demi baris. Satu-satunya cara untuk menemukan nilai yang dikembalikan oleh Func_i () adalah dengan melompat keluar dari main

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:

  printf("func:%d",Func_i());

itu memancarkan kode yang setara dengan:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

Kompiler juga membuat catatan di beberapa tabel internal yang function #2merupakan fungsi belum dideklarasikan bernama Func_i, yang mengambil sejumlah argumen yang tidak ditentukan dan mengembalikan int (default).

Nantinya, saat mem-parsing ini:

 int Func_i() { ...

kompiler mencari Func_idi 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_iketika 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:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

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.

nikie
sumber
Terima kasih atas jawaban Anda. Tidak bisakah kompiler memancarkan kode:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313
1
(Lanj.) Juga: Bagaimana jika Anda menulis printf(..., Func_i()+1);- kompiler harus mengetahui jenisnya Func_i, sehingga ia dapat memutuskan apakah ia harus memancarkan add integeratau add floatinstruksi. Anda mungkin menemukan beberapa kasus khusus di mana kompiler dapat berjalan tanpa informasi jenis, tetapi kompiler harus bekerja untuk semua kasus.
nikie
4
@ user31782: Instruksi mesin, sebagai suatu peraturan, sangat sederhana: Tambahkan dua register integer 32-bit. Memuat alamat memori ke register integer 16-bit. Lompat ke alamat. Juga, tidak ada tipe : Anda dapat dengan senang hati memuat lokasi memori yang mewakili angka float 32bit ke register integer 32bit dan melakukan aritmatika dengannya. (Ini jarang masuk akal.) Jadi tidak, Anda tidak dapat memancarkan kode mesin secara langsung. Anda bisa menulis kompiler yang melakukan semua hal itu dengan pemeriksaan runtime dan data tipe tambahan pada stack. Tapi itu bukan kompiler C.
nikie
1
@ user31782: Tergantung, IIRC. floatnilai 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.
nikie
1
Dari semua diskusi saya telah mencapai pemahaman ini: Agar kompiler membuat kode perakitan yang masuk akal untuk pernyataan apa pun termasuk Func_i()atau / dan Data_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.
user106313
5

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 dari int foo(). Contoh Anda berfungsi saat Func_i()dideklarasikan intkarena 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 doublesebenarnya ada intdi dalamnya. Itu hanya melakukan apa yang Anda minta, dan jika Anda memecahkannya, Anda memiliki keduanya.

Pertimbangkan bit kode ini:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

Kode di sebelah kiri mengkompilasi ke panggilan untuk foo()diikuti dengan menyalin hasil yang disediakan melalui konvensi panggilan / kembali ke mana pun xdisimpan. 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 intkarena 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 pengembalian foo()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.

Blrfl
sumber
Terima kasih atas jawaban Anda (+1). 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 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 Func_i()nilai pengembalian ditemukan.
user106313
Saya masih belum puas. 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?
user106313
1
@ user31782: Seharusnya tidak. Ada prototipe untuk foo(), jadi kompiler tahu apa yang harus dilakukan dengannya.
Blrfl
2
@ user31782: Prosesor tidak memiliki gagasan tentang tipe pengembalian.
Blrfl
1
@ user31782 Untuk pertanyaan waktu kompilasi: Dimungkinkan untuk menulis bahasa di mana semua analisis tipe ini dapat dilakukan pada waktu kompilasi. C bukan bahasa seperti itu. Kompiler C tidak dapat melakukannya karena tidak dirancang untuk melakukannya. Mungkinkah itu dirancang secara berbeda? Tentu, tetapi akan membutuhkan lebih banyak daya pemrosesan dan memori untuk melakukannya. Intinya adalah tidak. Itu dirancang dengan cara yang paling bisa ditangani oleh komputer saat itu.
Mr.Mindor