Neden printf (“% f”, 0); tanımsız davranışlar mı veriyor?


87

İfade

printf("%f\n",0.0f);

0 yazdırır.

Ancak ifade

printf("%f\n",0);

rastgele değerler yazdırır.

Bir tür tanımlanmamış davranış sergilediğimin farkındayım, ancak nedenini tam olarak anlayamıyorum.

Tüm bitlerin 0 olduğu bir kayan nokta değeri, hala 0 floatdeğeriyle geçerlidir
floatve intmakinemde aynı boyuttadır (bu bile alakalıysa).

Neden printfbu davranışa neden olarak kayan noktalı değişmez değer yerine bir tamsayı değişmezi kullanmak oluyor?

PS kullanırsam aynı davranış görülebilir

int i = 0;
printf("%f\n", i);

37
printfa bekliyor doubleve siz ona bir veriyorsunuz int. floatve intmakinenizde aynı boyutta olabilir, ancak 0.0faslında doublebir değişken argüman listesine gönderildiğinde (ve bunu printfbeklediğinde) a'ya dönüştürülür . Kısacası, printfkullandığınız belirteçlere ve sağladığınız argümanlara dayanarak pazarlığın amacını yerine getirmiyorsunuz.
WhozCraig

22
Varargs işlevleri, işlev bağımsız değişkenlerini otomatik olarak karşılık gelen parametrenin türüne dönüştürmez, çünkü yapamazlar. Bir prototipe sahip varargs olmayan işlevlerin aksine, gerekli bilgiler derleyici tarafından kullanılamaz.
EOF

3
Oooh ... "değişkenler." Yeni bir kelime öğrendim ...
Mike Robinson


3
Denemede yanındaki şey geçmektir (uint64_t)0yerine 0ve hala rasgele davranışı elde (varsayarak olmadığını görmek doubleve uint64_taynı boyut ve hizalama var). Farklı kayıtlarda geçirilen farklı türler nedeniyle, bazı platformlarda (örneğin x86_64) çıktının yine de rastgele olma ihtimali vardır.
Ian Abbott

Yanıtlar:


121

"%f"Biçimi türü bir argüman gerektirir double. Ona bir tür argüman veriyorsun int. Bu yüzden davranış tanımsızdır.

Standart hepsi bitleri sıfır geçerli bir temsilidir garanti etmez 0.0(genellikle olsa) veya herhangi bir doubledeğeri ya da bu intve double(öyle hatırlıyorum aynı boyutta doubledeğil, floataynı olsa bile, ya) boyut, aynı şekilde değişken bir işleve argüman olarak aktarılır.

Sisteminizde "çalışıyor" olabilir. Bu, tanımlanmamış davranışın olası en kötü belirtisidir çünkü hatayı teşhis etmeyi zorlaştırır.

N1570 7.21.6.1 paragraf 9:

... Herhangi bir bağımsız değişken, karşılık gelen dönüştürme belirtimi için doğru türde değilse, davranış tanımsızdır.

Tür argümanlarına floatyükseltilir double, bu yüzden printf("%f\n",0.0f)işe yarar. Daha dar tamsayı türleri intterfi intveya unsigned int. Bu promosyon kuralları (N1570 6.5.2.2 paragraf 6 ile belirtilmiştir) olması durumunda yardımcı olmaz printf("%f\n", 0).

0Bir doubleargüman bekleyen değişken olmayan bir işleve bir sabit iletirseniz , işlevin prototipinin görünür olduğu varsayılarak davranışın iyi tanımlandığını unutmayın. Örneğin, sqrt(0)(sonra #include <math.h>) argümanı örtük olarak - 0' intye dönüştürür doubleçünkü derleyici bildiriminden sqrtbir doubleargüman beklediğini görebilir . İçin böyle bir bilgi yok printf. Gibi çeşitli işlevler printfözeldir ve onlara çağrı yazarken daha fazla özen gerektirir.


13
Burada birkaç mükemmel temel nokta var. Öncelikle, olduğunu doubledeğil floatOP'ın genişlik varsayım olmayabilir (muhtemelen yapmıyor ki) beklemede yüzden. İkinci olarak, tamsayı sıfır ve kayan noktalı sıfırın aynı bit modeline sahip olduğu varsayımı da geçerli değildir. İyi iş
Orbit'te Hafiflik Yarışları

2
@LucasTrzesniewski: Tamam, ama cevabımın soruyu nasıl sorduğunu anlamıyorum. Bunun nedenini açıklamadan floatterfi ettirildiğini söyledim double, ama asıl mesele bu değildi.
Keith Thompson

2
@ robertbristow-Johnson: Derleyiciler için özel kanca olması gerekmez printfgcc olsa, örneğin, (bazıları teşhis böylece hataları var, eğer biçim dizesi bir hazır olduğunu). Derleyici , ilk parametrenin a olduğunu ve geri kalanının ile gösterildiğini söyleyen printffrom bildirimini görebilir . Hayır, içindir (ve yükseltilir ) ve içindir . C standardı bir yığın hakkında hiçbir şey söylemiyor. Yalnızca doğru çağrıldığında davranışını belirtir . <stdio.h>const char*, ...%fdoublefloatdouble%lflong doubleprintf
Keith Thompson

2
@ robertbristow-johnson: Eski şaşkınlık döneminde, "lint" gcc'nin şu anda gerçekleştirdiği ekstra kontrollerin bir kısmını sıklıkla gerçekleştirdi. İçin floatgeçen bir şuna printfyükseltilir double; bunda büyülü bir şey yok, sadece çeşitli işlevleri çağırmak için bir dil kuralı. printfkendisi, arayanın kendisine neyi ilettiğini iddia ettiği biçim dizesi aracılığıyla bilir ; bu iddia yanlışsa, davranış tanımsızdır.
Keith Thompson

2
Küçük düzeltme: luzunluk değiştirici "bir Aşağıdakilerden üzerinde etkisi yoktur a, A, e, E, f, F, g, veya Gdönüşüm belirteci" Bir için uzunluk değiştirici long doubledönüşüm olduğunu L. (@ robertbristow-johnson da ilgilenebilir)
Daniel Fischer

58

Diğer bazı yanıtlar üzerinde dokundu ancak olarak Öncelikle, açıkça yeterince dile aklıma, aygıtıdır: does bir tamsayı sağlamak üzere çalışmalarını en kütüphane işlevi alır bağlamlarda doubleveya floatargüman. Derleyici otomatik olarak bir dönüşüm ekleyecektir. Örneğin sqrt(0), iyi tanımlanmıştır ve tam olarak sqrt((double)0)aynı şekilde davranacaktır ve aynı şey, burada kullanılan diğer tam sayı tipi ifadeler için de geçerlidir.

printffarklı. Farklıdır çünkü değişken sayıda argüman alır. İşlev prototipi

extern int printf(const char *fmt, ...);

Bu nedenle yazarken

printf(message, 0);

derleyici, ikinci bağımsız değişkenin ne tür printf olmasını beklediğine dair herhangi bir bilgiye sahip değildir . Yalnızca geçilmesi gereken argüman ifadesinin türüne sahiptir int. Bu nedenle, çoğu kitaplık işlevinin aksine, bağımsız değişken listesinin biçim dizesinin beklentilerini karşıladığından emin olmak programcı olarak sizindir.

(Modern derleyiciler olabilir bir biçim dizesi içine bakmak ve bir tür uyuşmazlığı var söyleyecektir, ama onlar daha iyi kod artık ayrılmalıyız, çünkü fark edeceksiniz zaman, ne anlama geldiğini gerçekleştirmek için dönüşümleri takmadan başlamak etmeyeceğiz , yıllar sonra daha az yararlı bir derleyiciyle yeniden oluşturulduğunda.)

Şimdi, sorunun diğer yarısı şuydu: (int) 0 ve (float) 0.0'ın, çoğu modern sistemde, her ikisi de sıfır olan 32 bit olarak temsil edildiği göz önüne alındığında, neden yine de kazara çalışmıyor? C standardı sadece "bunun çalışması gerekli değil, kendi başınasınız" diyor, ancak işe yaramamasının en yaygın iki nedenini açıklayayım; bu muhtemelen neden gerekli olmadığını anlamanıza yardımcı olacaktır .

Bir geçerken Birincisi, tarihi nedenlerden ötürü, floatdeğişken argüman listesi aracılığıyla bu olur terfi için doubleen modern sistemlerde, hangi, 64 bit genişliğinde. Yani printf("%f", 0)64 bit bekleyen bir aranan uca sadece 32 sıfır bit aktarır.

İkinci, eşit derecede önemli neden, kayan noktalı fonksiyon argümanlarının tamsayı argümanlarından farklı bir yerde geçirilebilmesidir . Örneğin, çoğu CPU'nun tamsayılar ve kayan nokta değerleri için ayrı yazmaç dosyaları vardır, bu nedenle, 0'dan 4'e kadar olan argümanların tamsayılarsa r0'dan r4'e, ancak kayan noktalı iseler f0'dan f4'e kadar olan yazmaçlara gitmesi bir kural olabilir. Yani printf("%f", 0)bu sıfır için f1 yazmacına bakar, ama orada değildir.


1
Normal işlevler için kullananlar arasında bile, çeşitli işlevler için yazmaçları kullanan mimariler var mı? Diğer işlevler [float / short / char bağımsız değişkenleri olanlar dışında] ile bildirilebilecek olsa bile, variadic işlevlerin düzgün bir şekilde bildirilmesinin gerekliliğinin bu olduğunu düşündüm ().
Random832

3
@ Random832 Günümüzde, bir değişken ile normal bir işlevin çağırma kuralı arasındaki tek fark, bir değişkene sağlanan gerçek argüman sayısı gibi bazı ekstra verilerin olabileceğidir . Aksi takdirde, her şey normal bir işlev için olduğu yere tam olarak gider. Örneğin, x86-64.org/documentation/abi.pdf bölüm 3.2'ye bakın , burada variadikler için tek özel yaklaşım, aktarılan bir ipucudur AL. (Evet, bu, uygulamasının va_argeskisinden çok daha karmaşık olduğu anlamına geliyor .)
zwol

@ Random832: Her zaman nedeninin, bilinen sayıda ve türde argümana sahip bazı mimarilerde işlevlerin özel komutlar kullanılarak daha verimli bir şekilde gerçekleştirilebileceğini düşündüm.
celtschk

@celtschk SPARC ve IA64'teki "kayıt pencerelerini", az sayıda argümanla genel işlev çağrılarını hızlandırması beklenen (ne yazık ki pratikte tam tersini yapıyorlar) düşünüyor olabilirsiniz. Derleyicinin çeşitli işlev çağrılarını özel olarak ele almasını gerektirmezler, çünkü herhangi bir çağrı sitesindeki argüman sayısı , aranan ucun değişken olup olmadığına bakılmaksızın her zaman bir derleme zamanı sabitidir.
zwol

@zwol: Hayır, sabit kodlanmış bir tamsayı ret nolan 8086 talimatını düşünüyordum n, bu nedenle çeşitli fonksiyonlar için geçerli değildi. Bununla birlikte, herhangi bir C derleyicisinin bundan faydalanıp yararlanmadığını bilmiyorum (C olmayan derleyiciler kesinlikle kullandı).
celtschk

13

Normalde, a bekleyen bir işlevi çağırdığınızda double, ancak bir sağladığınızda int, derleyici otomatik olarak sizin için a'ya dönüşür double. Bu durumla olmaz printf, çünkü argümanların türleri fonksiyon prototipinde belirtilmez - derleyici bir dönüşümün uygulanması gerektiğini bilmez.


4
Ayrıca, printf() özellikle argümanlarının herhangi bir türde olabileceği şekilde tasarlanmıştır. Biçim dizesindeki her öğe tarafından hangi türün beklendiğini bilmeli ve bunu doğru bir şekilde sağlamalısınız.
Mike Robinson

@MikeRobinson: Herhangi bir ilkel C tipi. Bu, tüm olası türlerin çok, çok küçük bir alt kümesidir.
MSalters

13

Neden float değişmezi yerine bir tamsayı değişmezi kullanmak bu davranışa neden oluyor?

Çünkü 1. olanın printf()dışında herhangi bir parametre yazılmamıştır const char* formatstring. Geri ...kalan her şey için c tarzı bir üç nokta ( ) kullanır .

Sadece oraya iletilen değerlerin biçim dizesinde verilen biçimlendirme türlerine göre nasıl yorumlanacağına karar verir.

Denerken yaptığınız gibi aynı tür tanımsız davranışa sahip olacaksınız.

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

3
Bazı belirli uygulamaları printfbu şekilde çalışabilir (aktarılan öğelerin adresler değil değerler olması dışında). C standardı diğer değişken fonksiyonların nasıl printf ve nasıl çalıştığını belirtmez , sadece davranışlarını belirtir. Özellikle, yığın çerçevelerinden söz edilmiyor.
Keith Thompson

Küçük bir kelime oyunu: printfvar bir tiptedir Yazılan parametre, biçim dizesi, const char*. BTW, soru hem C hem de C ++ olarak etiketlendi ve C gerçekten daha alakalı; Muhtemelen reinterpret_castörnek olarak kullanmazdım.
Keith Thompson

Sadece ilginç bir gözlem: Aynı tanımlanmamış davranış ve büyük olasılıkla özdeş mekanizma nedeniyle, ancak ayrıntıda küçük bir farkla: Soruya benzer bir int geçirerek, UB, int'i double olarak yorumlamaya çalışırken printf içinde olur - örneğinizde , pf referansı kaldırılırken zaten dışarıda oluyor ...
Aconcagua

@Aconcagua Açıklama eklendi.
πάντα ῥεῖ

Bu kod örneği, tam olarak takma ad ihlali için UB'dir ve sorunun sorulduğundan tamamen farklı bir sorundur. Örneğin, kayan değerlerin farklı kayıtlarda tamsayılara geçirilme olasılığını tamamen göz ardı edersiniz.
MM

12

Yanlış eşleşen bir printf()belirtici "%f"ve tür (int) 0kullanmak, tanımsız davranışa yol açar.

Bir dönüşüm belirtimi geçersizse, davranış tanımsızdır. C11dr §7.21.6.1 9

UB'nin aday nedenleri.

  1. Spesifikasyon başına UB'dir ve derleme ilginçtir - 'nuf dedi.

  2. doubleve intfarklı boyutlardadır.

  3. doubleve intdeğerlerini farklı yığınlar kullanarak geçirebilir (genel vs. FPU yığını).

  4. Bir double 0.0 olabilecek bir all sıfır bitlik modeliyle tanımlanamaz. (nadir)


10

Bu, derleyici uyarılarınızdan öğrenmek için harika fırsatlardan biridir.

$ 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);
  ^

veya

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

Öyleyse, printftanımsız davranış üretiyor çünkü ona uyumsuz bir argüman türü iletiyorsunuz.


9

Neyin kafa karıştırdığından emin değilim.

Biçim dizeniz a double; bunun yerine bir int.

İki türün aynı bit genişliğine sahip olup olmadığı tamamen önemsizdir, ancak bunun gibi bozuk koddan sabit bellek ihlali istisnalarını önlemenize yardımcı olabilir.


3
@Voo: Bu biçim dizesi değiştiricisinin adı maalesef ama yine de neden intburada bir kabul edilebilir olduğunu düşündüğünüzü anlamıyorum .
Orbit'te Hafiflik Yarışları

1
@Voo: "(aynı zamanda geçerli bir kayan desen olarak da nitelendirilir)" Neden bir intkayan nokta modeli olarak nitelendirilsin? İkinin tümleyen ve çeşitli kayan noktalı kodlamaların neredeyse hiçbir ortak yanı yoktur.
Orbit'te Hafiflik Yarışları

2
Bu kafa karıştırıcı çünkü çoğu kütüphane işlevi için, 0yazılan bir argümana tam sayı sağlamak doubleDoğru Şeyi yapacaktır. Yeni başlayanlar için derleyicinin, printftarafından ele alınan argüman yuvaları için aynı dönüşümü yapmadığı açık değildir %[efg].
zwol

1
@Voo: Bunun ne kadar korkunç bir şekilde yanlış gidebileceğiyle ilgileniyorsanız, x86-64 SysV ABI'de kayan nokta argümanlarının tamsayı argümanlarından farklı bir kayıt kümesinde geçirildiğini düşünün.
EOF

1
@LightnessRacesinOrbit Bir şeyin neden UB olduğunu tartışmanın her zaman uygun olduğunu düşünüyorum , bu genellikle hangi uygulama enlemine izin verildiği ve yaygın durumlarda gerçekte ne olduğu hakkında konuşmayı içerir.
zwol

4

"%f\n"yalnızca ikinci printf()parametrenin türü olduğunda tahmin edilebilir sonucu garanti eder double. Daha sonra, değişken işlevlerin fazladan bağımsız değişkenleri, varsayılan bağımsız değişken yükseltmesinin konusudur. Tamsayı bağımsız değişkenleri, tamsayı yükseltmesinin kapsamına girer ve bu hiçbir zaman kayan nokta türü değerlerle sonuçlanmaz. Ve floatparametreler yükseltilir double.

Üstüne üstlük: standart, ikinci argümanın veya floatveya doublebaşka hiçbir şeyin olmamasına izin verir .


4

Neden resmi olarak UB olduğu birkaç cevapta tartışıldı.

Özellikle bu davranışı almanızın nedeni platforma bağlıdır, ancak muhtemelen şudur:

  • printfargümanlarını standart vararg yayılımına göre bekler. Aracı bir O floatbir olacak doubleve bir daha küçük bir şey intbir olacaktır int.
  • Bir geçiyoruz intişlevi bekler nerede double. Sizin intiçin, muhtemelen 32 bit double64 bit. Bu, bağımsız değişkenin durması gereken yerde başlayan dört yığın baytın olduğu 0, ancak aşağıdaki dört baytın keyfi içeriğe sahip olduğu anlamına gelir. Görüntülenen değeri oluşturmak için kullanılan şey budur.

0

Bu "belirsiz değer" sorununun ana nedeni, işaretçinin değişken parametreler bölümüne intiletilen değerde , makronun gerçekleştirdiği türlerde printfbir işaretçiye atılmasıdır.doubleva_arg

Bu, değer doublebellek arabellek alanı intboyuttan büyük olduğundan , printf parametresine parametre olarak iletilen değerle tamamen başlatılmamış bir bellek alanına başvurmaya neden olur .

Bu nedenle, bu işaretçi referansı kaldırıldığında, belirlenmemiş bir değer veya daha iyisi kısmen parametre olarak iletilen değeri içeren bir "değer" döndürülür printfve geri kalan kısım başka bir yığın arabellek alanından veya hatta bir kod alanından gelebilir ( bir bellek hatası istisnası oluşturma), gerçek bir arabellek taşması .


"Printf" ve "va_arg" için basitleştirilmiş kod uygulamalarının bu belirli kısımlarını dikkate alabilir ...

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


çift ​​değerli parametrelerin vprintf'deki gerçek uygulaması (gnu impl. dikkate alınarak) kod durum yönetimi şöyledir:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



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);



Referanslar

  1. gnu projesi glibc "printf" uygulaması (vprintf))
  2. printf'in sadeleştirme kodu örneği
  3. va_arg sadeleştirme kodu örneği
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.