Fungsi C ini harus selalu mengembalikan false, tetapi tidak

317

Saya menemukan sebuah pertanyaan yang menarik di sebuah forum sejak lama dan saya ingin tahu jawabannya.

Pertimbangkan fungsi C berikut:

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

Ini harus selalu kembali falsesejak itu var3 == 3000. The mainFungsi terlihat seperti ini:

main.c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

Karena f1()harus selalu kembali false, orang akan mengharapkan program untuk mencetak hanya satu yang salah ke layar. Tetapi setelah mengkompilasi dan menjalankannya, dieksekusi juga ditampilkan:

$ gcc main.c f1.c -o test
$ ./test
false
executed

Mengapa demikian? Apakah kode ini memiliki semacam perilaku yang tidak terdefinisi?

Catatan: Saya kompilasi dengan gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

Dimitri Podborski
sumber
9
Yang lain menyebutkan Anda memerlukan prototipe karena fungsi Anda berada di file yang terpisah. Tetapi bahkan jika Anda menyalin f1()ke file yang sama dengan main(), Anda akan mendapatkan beberapa keanehan: Meskipun benar di C ++ untuk digunakan ()untuk daftar parameter kosong, di C yang digunakan untuk fungsi dengan daftar parameter yang belum ditentukan ( pada dasarnya mengharapkan daftar parameter K & R-style setelah )). Agar benar C, Anda harus mengubah kode Anda ke bool f1(void).
uliwitness
1
The main()dapat disederhanakan ke int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- ini akan menunjukkan perbedaan yang lebih baik.
Palec
@uliwitness Bagaimana dengan K&R 1st ed. (1978) ketika tidak ada void?
Ho1
@uliwitness Tidak ada truedan falsedi K&R 1st ed., jadi tidak ada masalah sama sekali. Itu hanya 0 dan bukan nol untuk benar dan salah. Bukan? Saya tidak tahu apakah prototipe tersedia saat itu.
Ho1
1
K&R 1st Edn mendahului prototipe (dan standar C) lebih dari satu dekade (1978 untuk buku vs 1989 untuk standar) - memang, C ++ (C dengan Kelas) masih di masa depan ketika K&R diterbitkan. Juga, sebelum C99, tidak ada _Booltipe dan tidak ada <stdbool.h>header.
Jonathan Leffler

Jawaban:

396

Seperti disebutkan dalam jawaban lain, masalahnya adalah Anda menggunakan gcctanpa opsi kompiler yang ditetapkan. Jika Anda melakukan ini, standarnya adalah apa yang disebut "gnu90", yang merupakan implementasi non-standar dari standar C90 lama yang ditarik dari tahun 1990.

Dalam standar C90 lama ada kelemahan utama dalam bahasa C: jika Anda tidak mendeklarasikan prototipe sebelum menggunakan fungsi, itu akan default ke int func ()(di mana ( )berarti "menerima parameter apa pun"). Ini mengubah konvensi pemanggilan fungsi func, tetapi tidak mengubah definisi fungsi sebenarnya. Karena ukuran booldan intberbeda, kode Anda memanggil perilaku yang tidak terdefinisi ketika fungsi dipanggil.

Perilaku omong kosong berbahaya ini diperbaiki pada tahun 1999, dengan merilis standar C99. Deklarasi fungsi implisit dilarang.

Sayangnya, GCC hingga versi 5.xx masih menggunakan standar C lama secara default. Mungkin tidak ada alasan mengapa Anda ingin mengkompilasi kode Anda sebagai apa pun kecuali standar C. Jadi, Anda harus secara eksplisit memberi tahu GCC bahwa ia harus mengkompilasi kode Anda sebagai kode C modern, alih-alih omong kosong GNU berusia 25+ tahun, non-standar .

Perbaiki masalah dengan selalu mengompilasi program Anda sebagai:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 memintanya untuk melakukan upaya setengah hati untuk mengkompilasi sesuai dengan standar C (saat ini) (secara informal dikenal sebagai C11).
  • -pedantic-errors memberitahu itu untuk sepenuh hati melakukan hal di atas, dan memberikan kesalahan penyusun ketika Anda menulis kode yang salah yang melanggar standar C.
  • -Wall berarti memberi saya beberapa peringatan tambahan yang mungkin baik untuk dimiliki.
  • -Wextra berarti memberi saya beberapa peringatan tambahan lain yang mungkin baik untuk dimiliki.
Lundin
sumber
19
Jawaban ini secara keseluruhan benar, tetapi untuk program yang lebih rumit -std=gnu11jauh lebih mungkin bekerja seperti yang diharapkan daripada -std=c11, karena salah satu atau semua: membutuhkan fungsi perpustakaan di luar C11 (POSIX, X / Open, dll.) Yang tersedia di "gnu" mode diperpanjang tetapi ditekan dalam mode kesesuaian ketat; bug di header sistem yang disembunyikan dalam mode perluasan, misalnya dengan asumsi ketersediaan typedef tidak standar; penggunaan trigraph yang tidak disengaja (kesalahan standar ini dinonaktifkan dalam mode "gnu").
zwol
5
Untuk alasan yang sama, sementara saya umumnya mendorong penggunaan tingkat peringatan tinggi, saya tidak bisa mendukung penggunaan mode peringatan-adalah-kesalahan. -pedantic-errorskurang merepotkan daripada -Werrortetapi keduanya dapat dan memang menyebabkan program gagal dikompilasi pada sistem operasi yang tidak termasuk dalam pengujian penulis asli, bahkan ketika tidak ada masalah yang sebenarnya.
zwol
7
@Lundin Sebaliknya, masalah kedua yang saya sebutkan (bug di header sistem yang terpapar oleh mode kepatuhan ketat) ada di mana - mana ; Saya telah melakukan pengujian yang luas dan sistematis, dan tidak ada sistem operasi yang banyak digunakan yang tidak memiliki setidaknya satu bug seperti itu (setidaknya dua tahun yang lalu). Program C yang hanya membutuhkan fungsionalitas C11, tanpa tambahan lebih lanjut, juga merupakan pengecualian daripada aturan dalam pengalaman saya.
zwol
6
@ joop Jika Anda menggunakan standar C bool/ _Boolmaka Anda dapat menulis kode C Anda dengan cara "C ++ - esque", di mana Anda menganggap bahwa semua perbandingan dan operator logis mengembalikan boolseperti dalam C ++, meskipun mereka mengembalikan nilai intdalam C ++ , karena alasan historis . Ini memiliki keuntungan besar bahwa Anda dapat menggunakan alat analisis statis untuk memeriksa keamanan jenis semua ekspresi seperti itu, dan mengekspos semua jenis bug pada waktu kompilasi. Ini juga merupakan cara untuk mengekspresikan niat dalam bentuk kode dokumentasi diri. Dan yang kurang penting, ini juga menghemat beberapa byte RAM.
Lundin
7
Perhatikan bahwa sebagian besar barang baru di C99 berasal dari omong kosong GNU berusia 25+ tahun itu.
Shahbaz
141

Anda tidak memiliki prototipe yang dideklarasikan f1()di main.c, jadi secara implisit didefinisikan sebagai int f1(), artinya itu adalah fungsi yang mengambil jumlah argumen yang tidak diketahui dan mengembalikan sebuah int.

Jika intdan booldari ukuran yang berbeda, ini akan menghasilkan perilaku yang tidak terdefinisi . Sebagai contoh, pada mesin saya, intadalah 4 byte dan boolsatu byte. Karena fungsi ini didefinisikan untuk kembali bool, ia menempatkan satu byte di stack ketika kembali. Namun, karena secara implisit dinyatakan untuk kembali intdari main.c, fungsi panggilan akan mencoba membaca 4 byte dari stack.

Opsi kompiler default di gcc tidak akan memberi tahu Anda bahwa ia melakukan ini. Tetapi jika Anda mengompilasinya -Wall -Wextra, Anda akan mendapatkan ini:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

Untuk memperbaikinya, tambahkan deklarasi untuk f1di main.c, sebelum main:

bool f1(void);

Perhatikan bahwa daftar argumen secara eksplisit diatur ke void, yang memberitahu kompiler bahwa fungsi tidak mengambil argumen, sebagai lawan dari daftar parameter kosong yang berarti jumlah argumen yang tidak diketahui. Definisi f1dalam f1.c juga harus diubah untuk mencerminkan hal ini.

dbush
sumber
2
Sesuatu yang biasa saya lakukan dalam proyek saya (ketika saya masih menggunakan GCC) ditambahkan -Werror-implicit-function-declarationke opsi GCC, sehingga cara ini tidak melewati lagi. Pilihan yang lebih baik adalah -Werrormengubah semua peringatan menjadi kesalahan. Memaksa Anda untuk memperbaiki semua peringatan saat muncul.
uliwitness
2
Anda juga tidak boleh menggunakan tanda kurung kosong karena hal itu adalah fitur usang. Berarti mereka dapat melarang kode tersebut dalam versi standar C yang berikutnya.
Lundin
1
@uli saksi Ah. Informasi bagus untuk mereka yang datang dari C ++ yang hanya mencoba-coba dalam bahasa C.
SeldomNeedy
Nilai pengembalian biasanya tidak dimasukkan ke dalam tumpukan, tetapi ke dalam register. Lihat jawaban Owen. Juga, Anda biasanya tidak pernah memasukkan satu byte ke dalam tumpukan, tetapi merupakan kelipatan dari ukuran kata.
rsanchez
Versi GCC (5.xx) yang lebih baru memberikan peringatan itu tanpa bendera tambahan.
Overv
36

Saya pikir ini menarik untuk melihat di mana ukuran-ketidakcocokan yang disebutkan dalam jawaban sempurna Lundin sebenarnya terjadi.

Jika Anda mengompilasinya --save-temps, Anda akan mendapatkan file rakitan yang dapat Anda lihat. Inilah bagian di mana f1()melakukan == 0perbandingan dan mengembalikan nilainya:

cmpl    $0, -4(%rbp)
sete    %al

Bagian yang kembali adalah sete %al. Dalam konvensi pemanggilan x86 C, nilai kembali 4 byte atau lebih kecil (yang mencakup intdan bool) dikembalikan melalui register %eax. %aladalah byte terendah dari %eax. Jadi, 3 byte atas %eaxdibiarkan dalam keadaan tidak terkendali.

Sekarang di main():

call    f1
testl   %eax, %eax
je  .L2

Ini memeriksa apakah seluruh dari %eaxnol, karena berpikir itu menguji sebuah int.

Menambahkan perubahan deklarasi fungsi eksplisit main()ke:

call    f1
testb   %al, %al
je  .L2

yang kami inginkan.

Owen
sumber
27

Silakan kompilasi dengan perintah seperti ini:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Keluaran:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

Dengan pesan seperti itu, Anda harus tahu apa yang harus dilakukan untuk memperbaikinya.

Sunting: Setelah membaca komentar (sekarang dihapus), saya mencoba mengkompilasi kode Anda tanpa tanda. Yah, ini membuat saya kesalahan linker tanpa peringatan kompiler bukan kesalahan kompiler. Dan kesalahan-kesalahan linker itu lebih sulit untuk dipahami, jadi meskipun -std-gnu99tidak diperlukan, cobalah untuk selalu menggunakan setidaknya -Wall -Werroritu akan menghemat banyak rasa sakit di pantat.

jdarthenay
sumber