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 false
sejak itu var3 == 3000
. The main
Fungsi 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
.
f1()
ke file yang sama denganmain()
, 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 kebool f1(void)
.main()
dapat disederhanakan keint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }
- ini akan menunjukkan perbedaan yang lebih baik.void
?true
danfalse
di 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._Bool
tipe dan tidak ada<stdbool.h>
header.Jawaban:
Seperti disebutkan dalam jawaban lain, masalahnya adalah Anda menggunakan
gcc
tanpa 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 fungsifunc
, tetapi tidak mengubah definisi fungsi sebenarnya. Karena ukuranbool
danint
berbeda, 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:
-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.sumber
-std=gnu11
jauh 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").-pedantic-errors
kurang merepotkan daripada-Werror
tetapi 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.bool
/_Bool
maka Anda dapat menulis kode C Anda dengan cara "C ++ - esque", di mana Anda menganggap bahwa semua perbandingan dan operator logis mengembalikanbool
seperti dalam C ++, meskipun mereka mengembalikan nilaiint
dalam 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.Anda tidak memiliki prototipe yang dideklarasikan
f1()
di main.c, jadi secara implisit didefinisikan sebagaiint f1()
, artinya itu adalah fungsi yang mengambil jumlah argumen yang tidak diketahui dan mengembalikan sebuahint
.Jika
int
danbool
dari ukuran yang berbeda, ini akan menghasilkan perilaku yang tidak terdefinisi . Sebagai contoh, pada mesin saya,int
adalah 4 byte danbool
satu byte. Karena fungsi ini didefinisikan untuk kembalibool
, ia menempatkan satu byte di stack ketika kembali. Namun, karena secara implisit dinyatakan untuk kembaliint
dari 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:Untuk memperbaikinya, tambahkan deklarasi untuk
f1
di main.c, sebelummain
: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. Definisif1
dalam f1.c juga harus diubah untuk mencerminkan hal ini.sumber
-Werror-implicit-function-declaration
ke opsi GCC, sehingga cara ini tidak melewati lagi. Pilihan yang lebih baik adalah-Werror
mengubah semua peringatan menjadi kesalahan. Memaksa Anda untuk memperbaiki semua peringatan saat muncul.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 manaf1()
melakukan== 0
perbandingan dan mengembalikan nilainya:Bagian yang kembali adalah
sete %al
. Dalam konvensi pemanggilan x86 C, nilai kembali 4 byte atau lebih kecil (yang mencakupint
danbool
) dikembalikan melalui register%eax
.%al
adalah byte terendah dari%eax
. Jadi, 3 byte atas%eax
dibiarkan dalam keadaan tidak terkendali.Sekarang di
main()
:Ini memeriksa apakah seluruh dari
%eax
nol, karena berpikir itu menguji sebuah int.Menambahkan perubahan deklarasi fungsi eksplisit
main()
ke:yang kami inginkan.
sumber
Silakan kompilasi dengan perintah seperti ini:
Keluaran:
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-gnu99
tidak diperlukan, cobalah untuk selalu menggunakan setidaknya-Wall -Werror
itu akan menghemat banyak rasa sakit di pantat.sumber