Pernyataan
printf("%f\n",0.0f);
mencetak 0.
Namun demikian, pernyataan tersebut
printf("%f\n",0);
mencetak nilai acak.
Saya menyadari bahwa saya menunjukkan semacam perilaku yang tidak jelas, tetapi saya tidak tahu mengapa secara spesifik.
Nilai floating point di mana semua bitnya 0 masih valid float
dengan nilai 0.
float
dan int
berukuran sama pada mesin saya (bahkan jika itu relevan).
Mengapa menggunakan literal integer alih-alih literal floating point printf
menyebabkan perilaku ini?
PS perilaku yang sama dapat dilihat jika saya menggunakan
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
sumber
sumber
printf
mengharapkandouble
, dan Anda memberikannyaint
.float
danint
mungkin berukuran sama di komputer Anda, tetapi0.0f
sebenarnya diubah menjadidouble
ketika didorong ke daftar argumen variadic (danprintf
mengharapkan itu). Singkatnya, Anda tidak memenuhi akhir dari penawaranprintf
berdasarkan spesifikasi yang Anda gunakan dan argumen yang Anda berikan.(uint64_t)0
bukan0
dan melihat apakah Anda masih mendapatkan perilaku acak (dengan asumsidouble
danuint64_t
memiliki ukuran dan keselarasan yang sama). Kemungkinan keluarannya akan tetap acak pada beberapa platform (misalnya x86_64) karena jenis yang berbeda diteruskan di register yang berbeda.Jawaban:
The
"%f"
Format membutuhkan sebuah argumen tipedouble
. Anda memberikan argumen tipeint
. Itulah mengapa perilakunya tidak terdefinisi.Standar tidak menjamin bahwa semua-bit-nol adalah representasi yang valid
0.0
(meskipun sering), atau daridouble
nilai apa pun , atau ituint
dandouble
berukuran sama (ingat itudouble
, bukanfloat
), atau, bahkan jika mereka sama size, yang diteruskan sebagai argumen ke fungsi variadic dengan cara yang sama.Ini mungkin terjadi untuk "bekerja" di sistem Anda. Itu kemungkinan gejala terburuk dari perilaku tidak terdefinisi, karena itu menyulitkan untuk mendiagnosis kesalahan.
N1570 7.21.6.1 paragraf 9:
Argumen tipe
float
dipromosikandouble
, itulah sebabnyaprintf("%f\n",0.0f)
berhasil. Argumen jenis integer lebih sempit daripadaint
yang dipromosikan keint
atau keunsigned int
. Aturan promosi ini (ditentukan oleh N1570 6.5.2.2 paragraf 6) tidak membantu dalam kasusprintf("%f\n", 0)
.Perhatikan bahwa jika Anda meneruskan konstanta
0
ke fungsi non-variadic yang mengharapkandouble
argumen, perilakunya didefinisikan dengan baik, dengan asumsi prototipe fungsi terlihat. Misalnya,sqrt(0)
(setelah#include <math.h>
) secara implisit mengonversi argumen0
dariint
menjadidouble
- karena kompilator dapat melihat dari deklarasisqrt
bahwa ia mengharapkandouble
argumen. Tidak ada informasi seperti itu untukprintf
. Fungsi variadik sepertiprintf
itu istimewa, dan membutuhkan lebih banyak perhatian dalam menulis panggilan kepada mereka.sumber
double
tidakfloat
begitu mungkin tidak (mungkin tidak) terus asumsi lebar OP. Kedua, asumsi bahwa integer zero dan floating-point zero memiliki pola bit yang sama juga tidak berlaku. Kerja bagusfloat
dipromosikan menjadidouble
tanpa menjelaskan mengapa, tetapi itu bukan poin utamanya.printf
, meskipun gcc, misalnya, memiliki beberapa sehingga dapat mendiagnosis kesalahan ( jika format string adalah literal). Kompilator dapat melihat deklarasiprintf
from<stdio.h>
, yang memberitahukannya bahwa parameter pertama adalah aconst char*
dan sisanya ditandai dengan, ...
. Tidak,%f
untukdouble
(danfloat
dipromosikan menjadidouble
), dan%lf
untuklong double
. Standar C tidak mengatakan apa-apa tentang tumpukan. Ini menentukan perilakuprintf
hanya jika dipanggil dengan benar.float
diteruskan keprintf
dipromosikan menjadidouble
; tidak ada yang ajaib tentang itu, itu hanya aturan bahasa untuk memanggil fungsi variadic.printf
sendiri tahu melalui string format apa yang diklaim pemanggil untuk diteruskan kepadanya; jika klaim itu salah, perilakunya tidak ditentukan.l
panjang modifier "tidak berpengaruh pada pengikuta
,A
,e
,E
,f
,F
,g
, atauG
specifier konversi", panjang pengubah untuklong double
konversiL
. (@ robertbristow-johnson mungkin juga tertarik)Off pertama, seperti yang disinggung di beberapa jawaban yang lain tetapi tidak, pikiran saya, terbilang cukup jelas: Ini tidak bekerja untuk memberikan sebuah integer dalam kebanyakan konteks di mana fungsi perpustakaan membutuhkan
double
ataufloat
argumen. Kompilator secara otomatis akan memasukkan konversi. Misalnya,sqrt(0)
terdefinisi dengan baik dan akan berperilaku persis sepertisqrt((double)0)
, dan hal yang sama berlaku untuk ekspresi tipe bilangan bulat lainnya yang digunakan di sana.printf
berbeda. Ini berbeda karena membutuhkan sejumlah variabel argumen. Prototipe fungsinya adalahextern int printf(const char *fmt, ...);
Karena itu, saat Anda menulis
printf(message, 0);
kompilator tidak mempunyai informasi apapun tentang tipe apa yang
printf
mengharapkan argumen kedua itu. Ini hanya memiliki tipe ekspresi argumen, yaituint
, untuk dilalui. Oleh karena itu, tidak seperti kebanyakan fungsi perpustakaan, Anda, programmer, untuk memastikan daftar argumen sesuai dengan harapan format string.(Kompiler modern dapat melihat ke dalam string format dan memberi tahu Anda bahwa Anda memiliki jenis ketidakcocokan, tetapi mereka tidak akan mulai memasukkan konversi untuk mencapai apa yang Anda maksud, karena lebih baik kode Anda harus rusak sekarang, ketika Anda akan melihatnya , dibandingkan beberapa tahun kemudian ketika dibangun kembali dengan kompilator yang kurang membantu.)
Sekarang, separuh pertanyaan lainnya adalah: Mengingat bahwa (int) 0 dan (float) 0.0, pada kebanyakan sistem modern, keduanya direpresentasikan sebagai 32 bit yang semuanya nol, mengapa tetap tidak berfungsi, secara tidak sengaja? Standar C hanya mengatakan "ini tidak diharuskan untuk bekerja, Anda sendiri", tapi izinkan saya menjelaskan dua alasan paling umum mengapa ini tidak akan berhasil; yang mungkin akan membantu Anda memahami mengapa hal itu tidak diwajibkan.
Pertama, karena alasan historis, ketika Anda melewati
float
melalui daftar argumen variabel itu akan dipromosikan untukdouble
, yang, pada kebanyakan sistem modern, adalah 64 bit lebar. Jadiprintf("%f", 0)
hanya mengirimkan 32 bit nol ke callee mengharapkan 64 dari mereka.Kedua, alasan yang sama penting adalah bahwa argumen fungsi floating-point dapat dilewatkan di berbagai tempat dari argumen integer. Misalnya, sebagian besar CPU memiliki file register terpisah untuk bilangan bulat dan nilai floating-point, jadi mungkin ada aturan bahwa argumen 0 hingga 4 masuk dalam register r0 hingga r4 jika bilangan bulat, tetapi f0 hingga f4 jika berupa floating-point. Jadi
printf("%f", 0)
lihat di register f1 untuk nol itu, tapi tidak ada sama sekali.sumber
()
.AL
. (Ya, ini berarti penerapannyava_arg
jauh lebih rumit daripada sebelumnya.)ret n
instruksi 8086, di manan
adalah bilangan bulat hard-code, yang karenanya tidak berlaku untuk fungsi variadic. Namun saya tidak tahu apakah ada kompilator C yang benar-benar memanfaatkannya (tentu saja kompiler non-C melakukannya).Biasanya ketika Anda memanggil fungsi yang mengharapkan a
double
, tetapi Anda menyediakannyaint
, kompilator akan secara otomatis mengonversi menjadidouble
untuk Anda. Itu tidak terjadi denganprintf
, karena jenis argumen tidak ditentukan dalam prototipe fungsi - kompilator tidak tahu bahwa konversi harus diterapkan.sumber
printf()
secara khusus dirancang sedemikian rupa sehingga argumennya dapat berupa jenis apa pun. Anda harus mengetahui tipe mana yang diharapkan oleh setiap elemen dalam format-string, dan Anda harus menyediakannya dengan benar.Karena
printf()
tidak ada parameter yang diketik selainconst char* formatstring
sebagai yang pertama. Ini menggunakan elipsis gaya-c (...
) untuk yang lainnya.Ini hanya memutuskan bagaimana menafsirkan nilai yang diteruskan di sana sesuai dengan jenis pemformatan yang diberikan dalam format string.
Anda akan memiliki jenis perilaku tidak terdefinisi yang sama seperti saat mencoba
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB
sumber
printf
mungkin berfungsi seperti itu (kecuali bahwa item yang diteruskan adalah nilai, bukan alamat). Standar C tidak menentukan bagaimanaprintf
dan fungsi variadic lainnya bekerja, itu hanya menentukan perilakunya. Secara khusus, frame stack tidak disebutkan.printf
memang memiliki satu parameter yang diketik, format string, yang bertipeconst char*
. BTW, pertanyaannya diberi tag C dan C ++, dan C benar-benar lebih relevan; Saya mungkin tidak akan menggunakanreinterpret_cast
sebagai contoh.Menggunakan mis-matched
printf()
specifier"%f"
dan ketik(int) 0
mengarah ke perilaku undefined.Kandidat penyebab UB.
Ini adalah UB per spesifikasi dan kompilasi rumit - 'kata nuf.
double
danint
memiliki ukuran yang berbeda.double
danint
dapat meneruskan nilainya menggunakan tumpukan yang berbeda (tumpukan umum vs. FPU .)A
double 0.0
mungkin tidak ditentukan oleh semua pola bit nol. (langka)sumber
Ini adalah salah satu peluang bagus untuk belajar dari peringatan kompiler Anda.
$ gcc -Wall -Wextra -pedantic fnord.c fnord.c: In function ‘main’: fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=] printf("%f\n",0); ^
atau
$ clang -Weverything -pedantic fnord.c fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat] printf("%f\n",0); ~~ ^ %d 1 warning generated.
Jadi,
printf
menghasilkan perilaku tidak terdefinisi karena Anda meneruskannya dengan jenis argumen yang tidak kompatibel.sumber
Saya tidak yakin apa yang membingungkan.
String format Anda mengharapkan a
double
; Anda memberikan sebagai gantinyaint
.Apakah kedua jenis memiliki lebar bit yang sama sama sekali tidak relevan, kecuali hal itu dapat membantu Anda menghindari pengecualian pelanggaran memori keras dari kode rusak seperti ini.
sumber
int
akan diterima di sini.int
memenuhi syarat sebagai pola pelampung yang valid? Dua komplemen dan berbagai pengkodean floating-point hampir tidak memiliki kesamaan.0
ke argumen yang diketikdouble
akan melakukan Hal yang Benar. Tidak jelas bagi pemula bahwa kompilator tidak melakukan konversi yang sama untukprintf
slot argumen yang dialamatkan oleh%[efg]
."%f\n"
menjamin hasil yang dapat diprediksi hanya jikaprintf()
parameter kedua memiliki tipedouble
. Selanjutnya, argumen tambahan dari fungsi variadic adalah subjek dari promosi argumen default. Argumen bilangan bulat termasuk dalam promosi bilangan bulat, yang tidak pernah menghasilkan nilai tipe floating-point. Danfloat
parameter dipromosikan menjadidouble
.Untuk melengkapi: standar memungkinkan argumen kedua menjadi atau
float
ataudouble
dan tidak ada yang lain.sumber
Mengapa secara formal UB kini telah dibahas dalam beberapa jawaban.
Alasan mengapa Anda secara khusus mendapatkan perilaku ini bergantung pada platform, tetapi mungkin adalah sebagai berikut:
printf
mengharapkan argumennya sesuai dengan propagasi vararg standar. Itu berartifloat
wasiat menjadi adouble
dan apa pun yang lebih kecil dariint
wasiat menjadiint
.int
dimana fungsi mengharapkan adouble
. Andaint
mungkin 32 bit,double
64 bit Anda. Itu berarti bahwa empat byte tumpukan dimulai dari tempat argumen seharusnya berada0
, tetapi empat byte berikut memiliki konten yang berubah-ubah. Itulah yang digunakan untuk membangun nilai yang ditampilkan.sumber
Penyebab utama dari masalah "nilai tidak ditentukan" ini berdiri di cast penunjuk pada
int
nilai yang diteruskan ke bagianprintf
parameter variabel ke penunjuk padadouble
jenis yangva_arg
dijalankan makro.Hal ini menyebabkan referensi ke area memori yang tidak sepenuhnya diinisialisasi dengan nilai yang diteruskan sebagai parameter ke printf, karena
double
ukuran area buffer memori lebih besar dariint
ukuran.Oleh karena itu, ketika penunjuk ini didereferensi, ia mengembalikan nilai yang belum ditentukan, atau lebih baik "nilai" yang berisi sebagian nilai yang diteruskan sebagai parameter ke
printf
, dan untuk bagian yang tersisa dapat berasal dari area buffer tumpukan lain atau bahkan area kode ( meningkatkan pengecualian kesalahan memori), buffer overflow nyata .Itu dapat mempertimbangkan bagian spesifik dari implementasi kode semplificated dari "printf" dan "va_arg" ...
printf
va_list arg; .... case('%f') va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf.. ....
va_arg
char *p = (double *) &arg + sizeof arg; //printf parameters area pointer double i2 = *((double *)p); //casting to double because va_arg(arg, double) p += sizeof (double);
sumber