C ++ değişkenleri türlerini nasıl depolar?


42

Belli bir tipte bir değişken tanımlarsam (ki bildiğim kadarıyla sadece değişkenin içeriği için veri tahsis eder), değişkenin hangi tip değişken olduğunu nasıl takip eder?


8
Kim / sen tarafından atıfta ne " o " bölümündeki " nasıl takip etmez "? Derleyici veya CPU veya dil veya program gibi başka bir şey?
Erik Eidt


8
@ErikEidt IMO OP açıkçası "değişken" ile "değişken" anlamına gelir. Tabii ki sorunun iki kelimeli cevabı "değil" dir.
alephzero

2
Harika bir soru! Bugün özellikle ilgili, türlerini depolayan tüm süslü diller göz önüne alındığında.
Trevor Boyd Smith

@ alephzero Bu açıkça bir lider soruydu.
Luaan

Yanıtlar:


105

Değişkenler (veya daha genel olarak: "C" anlamındaki "nesneler") çalışma türlerinde türlerini depolamaz. Makine kodu ile ilgili olarak, yalnızca yazılmamış bellek var. Bunun yerine, bu verilerdeki işlemler verileri belirli bir tür (örneğin bir şamandıra veya işaretçi olarak) olarak yorumlar. Tipler sadece derleyici tarafından kullanılır.

Örneğin, bir yapıya veya sınıfa struct Foo { int x; float y; };ve değişkene sahip olabiliriz Foo f {}. Bir saha erişimi auto result = f.y;nasıl derlenebilir? Derleyici bu ftür bir nesne olduğunu Foobilir ve Foo-objects düzenini bilir . Platforma özgü ayrıntılara bağlı olarak, bu, “İşaretçiyi başlangıcına götür, f4 bayt ekle, sonra 4 bayt yükle ve bu verileri bir kayan nokta olarak yorumla ” olarak derlenebilir . Birçok makine kodu komut setinde (x86-64 dahil) ) Şamandıralar veya ints yüklemek için farklı işlemci talimatları vardır.

C ++ tip sisteminin bizim için tipini takip edemediği bir örnek, bunun gibi bir birlikteliktir union Bar { int as_int; float as_float; }. Bir sendika, çeşitli türde birden fazla nesne içerir. Bir nesneyi bir sendikada saklarsak, bu sendikanın aktif türüdür. Bu türden sadece sendikanın dışına çıkmaya çalışmalıyız, başka herhangi bir şey tanımsız davranış olacaktır. Aktif tipin ne olduğunu programlarken “biliyoruz” veya bir tip etiketini (genellikle enum) ayrı ayrı sakladığımız etiketli bir birlik oluşturabiliriz . Bu, C'de yaygın bir tekniktir, ancak birliği ve type etiketini eşitlemede tutmamız gerektiğinden, bu oldukça yanlıştır. Bir void*işaretçi bir birliğe benzer ancak işlev işaretçileri dışında yalnızca işaretçi nesnelerini tutabilir.
C ++, bilinmeyen türdeki nesnelerle uğraşmak için iki daha iyi mekanizma sunar: Tip silme işlemi yapmak için nesne yönelimli teknikler kullanabiliriz (nesne ile yalnızca gerçek metodları bilmemize gerek kalmadan sanal yöntemlerle etkileşime girebiliriz) veya Kullanım std::variant, tip-güvenli birliğin bir tür.

C ++ 'ın bir nesnenin türünü sakladığı bir durum vardır: nesnenin sınıfı herhangi bir sanal yönteme sahipse (bir “polimorfik tip”, aka. Arayüz). Sanal yöntem çağrısının hedefi derleme zamanında bilinmiyor ve nesnenin dinamik türüne (“dinamik gönderme”) dayalı çalışma zamanında çözüldü. Çoğu derleyici bunu, nesnenin başlangıcında sanal bir işlev tablosu (“değişken”) depolayarak gerçekleştirir. Vtable, çalışma zamanında nesnenin türünü almak için de kullanılabilir. Daha sonra, bir ifadenin derleme zamanı olarak bilinen statik türü ile çalışma zamanında bir nesnenin dinamik türü arasında bir ayrım yapabiliriz.

C ++, bize bir nesne typeid()veren işleci ile bir nesnenin dinamik türünü incelememize izin verir std::type_info. Derleyici derleme zamanında nesnenin türünü bilir veya derleyici nesnenin içinde gerekli tip bilgisini saklar ve çalışma zamanında alabilir.


3
Çok kapsamlı.
Deduplicator

9
Bir polimorfik nesne türüne erişmek için derleyicinin hala nesnenin belirli bir kalıtım ailesine ait olduğunu bilmesi gerekir (yani nesneye yazılan bir referans / işaretçi var void*).
Ruslan

5
+0, çünkü ilk cümle doğru değil, son iki paragraf bunu düzeltir.
Marcin

3
Genellikle polimorfik bir nesnenin başında depolanan şey, tablonun kendisinin değil, sanal yöntem tablosunun bir göstergesidir.
Peter Green,

3
@ v.oddou Paragrafımda bazı detayları görmezden geldim. typeid(e)İfadenin statik türünü inceler e. Statik tür polimorfik bir tür ise, ifade değerlendirilir ve o nesnenin dinamik tipi alınır. Türün kimliğini bilinmeyen türün belleğine işaret edemez ve yararlı bilgiler edemezsiniz. Örneğin, bir sendikanın kimliği, sendikadaki nesneyi değil, birliği tanımlar. A void*türü sadece geçersiz bir işaretçidir. Ve bir a void*içeriğini almak için caydırmak mümkün değildir . C ++ 'da açıkça bu şekilde programlanmadıkça boks yoktur.
amon

51

Diğer cevap teknik yönü iyi açıklıyor, ancak bazı genel "makine kodu hakkında nasıl düşünüleceğini" eklemek istiyorum.

Derlemeden sonraki makine kodu oldukça aptalca ve gerçekten her şeyin planlandığı gibi çalıştığını varsayar. Diyelim ki basit bir işleve sahipsiniz.

bool isEven(int i) { return i % 2 == 0; }

Bir int alır ve bir bool tükürür.

Derlemeden sonra, bu otomatik portakal sıkacağı gibi bir şey düşünebilirsiniz:

otomatik portakal sıkacağı

Portakal alır ve meyve suyu verir. İçeri girdiği nesne tipini tanıyor mu? Hayır, sadece portakal olması gerekiyordu. Portakal yerine elma alırsa ne olur? Belki kırılacak. Önemli değil, çünkü sorumlu bir sahibi bu şekilde kullanmaya çalışmaz.

Yukarıdaki işlev benzerdir: giriş yapmak için tasarlanmıştır ve başka bir şeyi beslediğinde alakasız bir şey kırabilir veya yapabilir. Bu (genellikle) farketmez, çünkü derleyici (genel olarak) asla gerçekleşmeyeceğini kontrol eder - ve gerçekten de hiçbir zaman iyi biçimlendirilmiş kodda olmaz. Derleyici, bir işlevin yanlış yazılmış değer alma olasılığını tespit ederse, kodu derlemeyi reddeder ve bunun yerine tür hatalarını döndürür.

Uyarı, derleyicinin geçeceği bazı kötü biçimli kod vakaları olduğudur. Örnekler:

  • Yanlış tür çarpıtması: açık yayınları doğru olduğu varsayılır ve o döküm değil emin olmak için programcısı olduğunu edilmektedir void*için orange*pointer diğer ucunda bir elma varken,
  • boş işaretçiler, sarkan işaretçiler veya kapsam sonrası kullanım gibi hafıza yönetimi sorunları; derleyici çoğunu bulamıyor,
  • Kaçırdığım başka bir şey olduğuna eminim.

Söylediğim gibi, derlenen kod aynı meyve sıkacağı makinesi gibi - ne işlediğini bilmiyor, sadece talimatlar veriyor. Ve talimatlar yanlışsa, kırılır. Bu nedenle C ++ 'daki problemlerin kontrolsüz çökmelere yol açması budur.


4
Derleyici , işlevin doğru türde bir nesneden geçirildiğini kontrol etmeye çalışır , ancak hem C hem de C ++ derleyicinin her durumda ispatlayamayacağı kadar karmaşıktır. Bu nedenle, elma ve portakallarınızın meyve sıkacağı ile karşılaştırılması oldukça öğreticidir.
Calchas

@Calchas Yorumunuz için teşekkürler! Bu cümle gerçekten bir aşırı basitleştirmeydi. Olası problemler üzerinde biraz durdum, aslında soru ile oldukça ilgili.
Frax

5
makine kodu için büyük metafor vay! senin metafor da resim tarafından 10 kat daha iyi hale getirildi!
Trevor Boyd Smith

2
"Eminim eksik bir şey daha var." - Tabii ki! C'nin void*için zorlar foo*zamanki aritmetik promosyonlar, uniontipi cinaslı, NULLvs nullptr, hatta sadece sahip kötü işaretçi vb UB olduğunu Ama izne en iyisi bu yüzden, maddi olarak cevabınızı artıracak dışarı bunların hepsi listeleme sanmıyorum olduğu gibi.
Kevin

@Kevin Buraya C eklemenin gerekli olduğunu sanmıyorum, çünkü soru sadece C ++ ile etiketlenmiş. Ve C ++ ' void*da örtük olarak dönüştürülmez foo*ve uniontip yazın desteklenmez (UB vardır).
Ruslan

3

Bir değişkenin C gibi bir dilde bir takım temel özellikleri vardır:

  1. Bir isim
  2. Bir tür
  3. Bir dürbün
  4. Bir ömürboyu
  5. Bir yer
  6. Bir değer

Kaynak kodunuzda , konum (5) kavramsaldır ve bu yer adıyla (1) belirtilir. Bu nedenle, değerin yerini ve alanını oluşturmak için değişken bildirimi kullanılır (6) ve diğer kaynak satırlarında, değişkeni bir ifadede adlandırarak bu konuma ve içerdiği değeri belirtiriz.

Program, derleyici tarafından makine koduna çevrildikten sonra, konum, (5), bir miktar bellek veya CPU kayıt yeri ve değişkeni referans alan herhangi bir kaynak kodu ifadesi, bu belleği referans alan makine kodu dizilerine çevrilir. veya CPU kayıt yeri.

Bu nedenle, çeviri tamamlandığında ve program işlemci üzerinde çalıştığında, değişkenlerin adları makine kodunda etkin bir şekilde unutulur ve derleyici tarafından oluşturulan talimatlar yalnızca değişkenlerin atanmış konumlarına atıfta bulunur. adları). Hata ayıklama yapıyorsanız ve hata ayıklama istiyorsanız, adla ilişkilendirilen değişkenin konumu programın meta verilerine eklenir, ancak işlemci yine de konumlarını (makine verileri değil) kullanarak makine kodu talimatlarını görür. (Bazı isimler bağlantı, yükleme ve dinamik arama amacıyla programın meta verilerinde yer aldığından, bu aşırı bir basitleştirmedir - hala işlemci sadece program için söylenen makine kodu talimatlarını yerine getirir ve bu makine kodunda isimler konumlara dönüştürüldü.)

Aynısı tip, kapsam ve ömür boyu da geçerlidir. Derleyici tarafından oluşturulan makine kodu yönergeleri, değeri depolayan konumun makine sürümünü bilir. Tür gibi diğer özellikler, değişkenin konumuna erişen belirli talimatlar olarak çevrilmiş kaynak kodunda derlenir. Örneğin, söz konusu değişken, imzasız bir 8 bit bayta karşı imzalı bir 8 bit baytsa, bu durumda değişkeni referans alan kaynak kodundaki ifadeler, örneğin imzalı bayt yüklerine karşı imzalı bayt yüklerine çevrilir. (C) dilinin kurallarını yerine getirmek için gerektiği gibi. Böylece değişkenin türü, kaynak kodun makine komutlarına çevrilmesine kodlanır ve bu da CPU'ya, her defasında değişkenin konumunu kullandığında bellek veya CPU kayıt yerini nasıl yorumlayacağını emreder.

Bunun özü, CPU'ya, işlemcinin makine kodu talimat setindeki talimatlar (ve daha fazla talimatlar) yoluyla ne yapması gerektiğini söylememiz gerektiğidir. İşlemci az önce yaptığı veya söylenenleri çok az hatırlıyor - sadece verilen talimatları yerine getiriyor ve değişkenleri uygun şekilde işleyebilmesi için tam bir komut dizisi seti vermek derleyici veya derleme dili programcısının görevi.

Bir işlemci doğrudan bayt / word / int / uzun imzalı / imzasız, kayan nokta, çift vb. Gibi bazı temel veri türlerini destekler. İmzalı veya imzasız olarak aynı bellek yerine dönüşümlü olarak davranırsanız, işlemci genellikle şikayet etmez veya itiraz etmez. Örneğin, bu programda genellikle bir mantık hatası olurdu rağmen. Değişkenle her etkileşimde işlemciye talimat vermek programlama işidir.

Bu temel ilkel türlerin ötesinde, veri yapılarındaki şeyleri kodlamamız ve bunları bu ilkeller açısından manipüle etmek için algoritmalar kullanmamız gerekir.

C ++ 'da, polimorfizm için sınıf hiyerarşisinde yer alan nesneler, genellikle nesnenin başlangıcında, sanal gönderme, döküm, vb.

Özetle, işlemci aksi takdirde depolama yerlerinin kullanım amacını bilmiyor veya hatırlamıyor - CPU kaydında ve ana bellekte depolamayı nasıl değiştireceğini açıklayan programın makine kodu talimatlarını yerine getiriyor. Programlama, o zaman, depolamayı anlamlı bir şekilde kullanmak ve programı bir bütün olarak güvenilir bir şekilde yürüten işlemciye tutarlı bir dizi makine kodu talimatı sunmak için yazılımın (ve programcıların) işidir.


1
"Çeviri tamamlandığında adın unutulması" konusunda dikkatli olun, ... ... adlandırma ("undefined symbol xy") üzerinden bağlantı yapılır ve çalışma zamanında dinamik bağlantı ile olabilir. Bkz blog.fesnel.com/blog/2009/08/19/... . Hiçbir hata ayıklama sembolü yok, hatta sıyrılıyor: Dinamik bağlantı için fonksiyona (ve, genel değişken) isminin gerekli olduğunu söylemelisin. Böylece sadece iç nesnelerin isimleri unutulabilir. Bu arada, değişken özelliklerinin iyi bir listesi.
Peter - Monica

@ PeterA.Schneider, kesinlikle haklısınız, olayların büyük resminde, bağlayıcılar ve yükleyiciler de kaynak koddan gelen (global) fonksiyonların ve değişkenlerin adlarına katılır ve bunları kullanır.
Erik Eidt

Ek bir komplikasyon bazı derleyiciler, Standard başına, izin amaçlanan kuralları derleyiciler bazı şeyleri farz yorumlamak olmasıdır olmaz onları unsequenced gibi farklı tiplerini içeren operasyonları görüyoruz izin olarak takma bile yazılı olarak aliasing içermeyen durumlarda . Gibi bir şey göz önüne alındığında useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);, clang ve gcc her ikisi de aynı türetilmiş olsa bile işaretçinin unionArray[j].member2erişemediğini varsaymaya eğilimlidir . unionArray[i].member1unionArray[]
supercat

Derleyici dil özelliklerini doğru yorumlasa da yorumlasa da, onun işi de programı yürüten makine kodu talimatı dizileri oluşturmaktır. Bu, kaynak koddaki her bir değişken erişimi için (modulo optimizasyonu ve diğer birçok faktör) işlemciye, depolama yeri için hangi boyut ve veri yorumlamasını kullanacağını söyleyen bazı makine kodu talimatları üretmesi gerektiği anlamına gelir. İşlemci değişken hakkında hiçbir şey hatırlamıyor, bu yüzden değişkene her erişmesi gerekiyorsa, tam olarak nasıl yapılacağı konusunda talimat verilmesi gerekiyor.
Erik Eidt

2

eğer belli bir tipin değişkenini tanımlarsam, değişken tipini nasıl takip eder.

Burada iki ilgili aşama vardır:

  • Derleme zamanı

C derleyicisi C kodunu makine diline derler. Derleyici, kaynak dosyanızdan (ve kütüphanelerden ve işini yapması için gereken diğer şeylerden) alabileceği tüm bilgilere sahiptir. C derleyicisi ne anlama geldiğini izler. C derleyicisi, bir değişken olduğunu bildirirseniz char, bunun char olduğunu bilir .

Bunu, değişkenlerin adlarını, türlerini ve diğer bilgileri listeleyen bir "sembol tablosu" kullanarak yapar. Oldukça karmaşık bir veri yapısıdır, ancak bunu sadece insan tarafından okunabilen adların ne anlama geldiğini takip etmek olarak düşünebilirsiniz. Derleyiciden gelen ikili çıktıda, bunun gibi hiçbir değişken adı görünmez (programlayıcı tarafından istenebilecek isteğe bağlı hata ayıklama bilgisini yok sayarsak).

  • Çalışma süresi

Derleyicinin çıktısı - derlenebilir çalıştırılabilir - işletim sisteminiz tarafından RAM'e yüklenen ve doğrudan CPU'nuz tarafından yürütülen makine dilidir. Makine dilinde, hiçbir şekilde "tür" kavramı yoktur - yalnızca RAM'deki bazı yerlerde çalışan komutlar vardır. Komutlar aslında onlar ile faaliyet sabit türü var mı (yani bir makine dili komutu "RAM yerleri 0x100 ve 0x521 saklanan bu iki 16 bitlik tamsayılar eklemek" olabilir), ancak hiçbir bilgi yoktur yerde o sistemde Bu konumlardaki baytlar aslında tam sayıları temsil ediyor. Tip hatalardan koruma yok hiç burada.


Herhangi bir ihtimal, "bayt kodu yönelimli diller" ile C # veya Java'ya atıfta bulunuyorsanız, işaretçiler hiçbir şekilde onlardan yoksundur; tam tersine: İşaretçiler C # ve Java'da çok daha yaygındır (ve sonuç olarak, Java'daki en yaygın hatalardan biri "NullPointerException" dır). "Referans" olarak adlandırıldıkları sadece bir terminoloji meselesidir.
Peter - Monica'yı yeniden

@ PeterA.Schneider, elbette, NullPOINTERException var, ancak bahsettiğim dillerde bir referans ile bir işaretçi arasında kesin bir ayrım var (Java, ruby, muhtemelen C #, hatta bir ölçüde Perl gibi) - referans birlikte gidiyor tip sistemiyle birlikte, çöp toplama, otomatik bellek yönetimi vb. ile; Bir hafıza konumunu açıkça belirtmek bile mümkün değildir ( char *ptr = 0x123C'deki gibi ). "İşaretçi" kelimesini kullanmamın bu bağlamda oldukça açık olması gerektiğine inanıyorum. Olmazsa, bana yardımcı olmaktan çekinmeyin, ben de cevaba bir cümle ekleyeyim.
AnoE

işaretçiler C ++ 'da "tip sistemle birlikte gider" ;-). (Aslında, Java'nın klasik jenerik özellikleri, C ++ 'dan daha az güçlü bir şekilde yazılmıştır.) Çöp toplama, C ++' ın zorunlu kılmaya karar vermediği bir özelliktir, ancak bir uygulamanın bir tane sağlaması mümkündür ve işaretçiler için kullandığımız kelime ile hiçbir ilgisi yoktur.
Peter - Monica'yı yeniden yerleştirme

Tamam, @ PeterA.Schneider, burada seviye atladığımızı sanmıyorum. İşaretçilerden bahsettiğim paragrafı kaldırdım, yine de cevap için hiçbir şey yapmadı.
AnoE

1

C ++ 'ın çalışma zamanında bir tür depoladığı birkaç önemli özel durum vardır.

Klasik çözüm, ayrımcılığa uğramış bir birlikteliktir: çeşitli nesne türlerinden birini içeren bir veri yapısı ve şu anda hangi tür içerdiğini söyleyen bir alan. Şablonlu versiyonu olarak C ++ standart kütüphanede std::variant. Normalde, etiket bir olur enum, ancak verileriniz için tüm depolama alanlarına ihtiyacınız yoksa, bir bit alanı olabilir.

Bunun diğer bir yaygın örneği dinamik yazmadır. When classa sahip virtualişlevi, program o işlev işaretçisi saklayacak sanal fonksiyon tablosunun o her örneği için başlatır, classbunun inşa edildiğinde. Normalde bu, tüm sınıf örnekleri için bir sanal işlev tablosu anlamına gelir ve her örnek uygun tabloya bir işaretçi tutar. (Bu, tablo tek bir işaretçiden çok daha büyük olacağından zaman ve bellek tasarrufu sağlar.) Bu virtualişlevi bir işaretçi veya başvuru yoluyla çağırdığınızda , program sanal tablodaki işlev işaretçisini arar. (Derleme zamanında tam türü biliyorsa, bu adımı atlayabilir.) Bu, kodun temel sınıfın yerine türetilmiş bir türün uygulamasını çağırmasını sağlar.

Burada bu alakalı kılar şeydir: Her ofstreambir gösterici içerir ofstreamsanal masa, her ifstreamüzere ifstreamsanal masa, vb. Sınıf hiyerarşileri için, sanal tablo işaretçisi, programa bir sınıf nesnesinin ne tür olduğunu söyleyen etiket görevi görebilir!

Dil standardı, derleyiciler tasarlayanlara çalışma süresini kaputun altında nasıl uygulamaları gerektiğini söylemese de, beklediğiniz dynamic_castve typeofçalıştığınız şey budur.


muhtemelen söz konusu "kodlayıcılar" millet vurgulamalıdır "dil standart coder'lara söylemez" yazılı , insanlar değil gcc, clang, msvc vb kullanarak bu ++ onların C derlemek için.
Caleth

@Caleth İyi öneri!
Davislor
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.