C'deki ok (->) operatörü neden var?


264

Dot ( .) operatörü bir yapının ->bir üyesine erişmek için kullanılırken C'deki ok operatörü ( ), söz konusu imleç tarafından başvurulan bir yapının bir üyesine erişmek için kullanılır.

İşaretçinin kendisinde nokta operatörü ile erişilebilen herhangi bir üye yoktur (aslında sanal bellekteki bir konumu tanımlayan bir sayıdır, bu nedenle herhangi bir üyesi yoktur). Bu nedenle, nokta operatörünü bir işaretçi üzerinde kullanılıyorsa, işaretçiyi otomatik olarak referanstan arındırmak üzere tanımlasak hiçbir belirsizlik olmazdı (derleyici tarafından derleme zamanında bilinen bir bilgi).

Öyleyse neden dil oluşturucuları bu gereksiz görünen operatörü ekleyerek işleri daha karmaşık hale getirmeye karar verdiler? Büyük tasarım kararı nedir?


1
İlgili: stackoverflow.com/questions/221346/… - ayrıca, geçersiz kılabilirsiniz ->
18'de Krease

16
@Chris Tabii ki büyük bir fark yaratan C ++ hakkında. Ancak C'nin neden bu şekilde tasarlandığından bahsettiğimiz için, 1970'lerde C ++ var olmadan önce olduğumuzu varsayalım.
Gizemli

5
En iyi tahminim, ok operatörünün "izlemek! Burada bir işaretçi ile uğraştığınızı" görsel olarak ifade etmek için var olmasıdır
Chris

4
Bir bakışta, bu sorunun çok garip olduğunu hissediyorum. Her şey düşünceli bir şekilde tasarlanmamıştır. Tüm yaşamınız boyunca bu stili korursanız, dünyanız sorularla doludur. En çok oy alan cevap gerçekten bilgilendirici ve açıktır. Ancak sorunuzun kilit noktasını vurmuyor. Sorunuzun stilini takip edin, çok fazla soru sorabilirim. Örneğin, 'int' anahtar kelimesi 'integer' kelimesinin kısaltmasıdır; 'double' anahtar kelimesi neden daha kısa olmasın?
junwanghe

1
@junwanghe Bu soru aslında geçerli bir endişeyi temsil ediyor - .operatör neden operatörden daha yüksek önceliğe sahip *? Olmasaydı * ptr.member ve var.member olabilirdi.
milleniumbug

Yanıtlar:


358

Sorunuzu iki soru olarak yorumlayacağım: 1) neden ->var bile ve 2) neden .işaretçiyi otomatik olarak iptal etmiyor? Her iki soruya da cevapların tarihsel kökleri vardır.

Neden ->var?

C dilinin ilk sürümlerinden birinde ( Mayıs 1975'te 6. Baskı Unix ile birlikte gelen " C Referans Kılavuzu " için CRM olarak anacağım), operatör ->çok özel bir anlama sahipti, eş anlamlı değil *ve .kombinasyon

CRM tarafından tanımlanan C dili, birçok açıdan modern C'den çok farklıydı. CRM yapısında üyeler , herhangi bir adres kısıtlamasına tür kısıtlaması olmadan eklenebilecek küresel bayt uzaklığı kavramını uyguladılar . Yani, tüm yapı mensuplarının tüm adları bağımsız küresel anlamlara sahipti (ve bu nedenle, benzersiz olmak zorundaydı). Örneğin,

struct S {
  int a;
  int b;
};

ve ad aofset 0 için, ad ise bofset 2 anlamına gelir ( intboyut 2 tipi ve dolgu yok). Çeviri birimindeki tüm yapıların tüm üyelerinin benzersiz adları vardır veya aynı uzaklık değerini temsil eder. Örneğin, aynı çeviri biriminde ayrıca

struct X {
  int a;
  int x;
};

ve bu sorun olmaz, çünkü isim asürekli olarak 0 ofsetini temsil eder. Fakat bu ek deklarasyon

struct Y {
  int b;
  int a;
};

aofset 2 ve bofset 0 olarak "yeniden tanımlamaya" çalıştığından resmi olarak geçersiz olacaktır .

İşte bu noktada ->operatör devreye girer. Her yapı üyesi adının kendi kendine yeterli küresel anlamı olduğundan, dil bu gibi ifadeleri destekledi

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

İlk atama olarak derleyici tarafından yorumlandı "take adresi 5, ofset ekleyin 2buna ve atamak için 42için intçıkan adreste değeri". Yani yukarıda atayın 42için intadreste değeri 7. Bu kullanımın ->sol taraftaki ifadenin türünü umursamadığını unutmayın . Sol taraf bir rvalue sayısal adresi olarak yorumlandı (bir işaretçi veya bir tam sayı olsun).

Bu tür bir hile *ve .kombinasyon mümkün değildi . Yapamazsın

(*i).b = 42;

çünkü *izaten geçersiz bir ifade. *Operatör, bu ayrıdır beri .kendi işlenen üzerine, yüklemektedir daha sıkı tip gereksinimleri. CRM, bu sınırlamanın üstesinden gelmek ->için sol operatörün türünden bağımsız olarak operatörü tanıttı .

Keith'in yorumlarda belirttiği gibi, ->ve *+ .kombinasyonu arasındaki bu fark , CRM'in 7.1.8'de "gereksinimin gevşemesi" olarak adlandırdığı şeydir: İşaretçi türü olan gereksinimin gevşemesi dışında E1, ifade E1−>MOStam olarak eşdeğerdir(*E1).MOS

Daha sonra, K&R C'de CRM'de başlangıçta açıklanan birçok özellik önemli ölçüde yeniden işlendi. "Global ofset tanımlayıcısı olarak yapı üyesi" fikri tamamen kaldırıldı. Ve ->operatörün işlevselliği *ve .kombinasyonunun işlevselliği ile tamamen aynı oldu .

.İşaretçiyi neden otomatik olarak iptal edemiyorsunuz ?

Yine, dilin CRM sürüm sol işlenen .operatörü bir olması gerekirdi lvalue . Bu işlenene uygulanan tek gereklilik buydu (ve ->yukarıda açıklandığı gibi onu farklı kılan da budur ). CRM ki Not değil sol işlenen gerektiren .bir yapı tipini olması. Sadece bir lvalue, herhangi bir lvalue olmasını gerektiriyordu . Bu, C'nin CRM sürümünde böyle bir kod yazabileceğiniz anlamına gelir

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

Bu durumda derleyici , tipte bir alan olmamasına rağmen , sürekli bellek bloğunda byte-offset 2'de konumlandırılmış 55bir intdeğere yazacaktır . Derleyici gerçek türünü hiç umursamaz . Tek ilgilendiği şey bir değerdi: bir tür yazılabilir bellek bloğu.cstruct Tbcc

Şimdi bunu yaparsanız

S *s;
...
s.b = 42;

kod geçerli sayılır ( sayrıca bir değer olduğu için) ve derleyici sadece bayt-offset 2'de işaretçinin skendisine veri yazmaya çalışacaktır. kendisini bu tür konularla ilgilendirmedi.

Yani dilin bu sürümünde, .işaretçi türleri için aşırı yükleme operatörü hakkında önerdiğiniz fikir işe yaramaz: operatör .işaretçilerle (değer değeri işaretçileriyle veya herhangi bir değerle) kullanıldığında çok özel bir anlama sahipti. Çok garip bir işlevsellikti, şüphesiz. Ama o sırada oradaydı.

Tabii ki, bu garip işlevsellik, .C - K&R C'nin elden geçirilmiş versiyonunda işaretçiler için aşırı yüklenen operatörün (önerdiğiniz gibi) tanıtımına karşı çok güçlü bir neden değil. Belki de o sırada C'nin CRM sürümünde desteklenmesi gereken bazı eski kodlar vardı.

(1975 C Başvuru Kılavuzu'nun URL'si sabit olmayabilir. Muhtemelen bazı küçük farklılıklar içeren başka bir kopya buradadır .)


10
Ve atıfta bulunulan C Referans Kılavuzunun 7.1.8 kısmı, "E1'in işaretçi türünde olması gerekliliğinin gevşemesi dışında, '' E1−> MOS '' ifadesinin tam olarak '' (* E1) ile eşdeğer olduğunu ifade eder. '."
Keith Thompson

1
Neden *iadres 5'teki bazı varsayılan türlerin (int?) Bir değeri olmamıştır ? O zaman (* i) .b aynı şekilde çalışırdı.
Random832

5
@Leo: Peki, bazı insanlar C dilini üst düzey montajcı olarak süslüyor. C tarihinde o dönemde dil aslında daha üst düzey bir toplayıcıydı.
AnT

29
Huh. Bu nedenle, UNIX'teki (ör. struct stat) Birçok yapının alanlarına neden önek (ör. ) Açıklanmaktadır st_mode.
icktoofay

5
@ perfectionm1ng: Görünüşe göre bell-labs.com Alcatel-Lucent tarafından devralındı ​​ve orijinal sayfalar kayboldu. Ne kadar süre kalacağını söyleyemem, ancak başka bir sitenin bağlantısını güncelledim. Her neyse, "ritchie c başvuru kılavuzu" için googling genellikle belgeyi bulur.
AnT

46

Tarihsel (iyi ve zaten rapor edilmiş) nedenlerin ötesinde, operatörlerin önceliğinde de küçük bir sorun vardır: nokta operatörü yıldız operatörüne göre daha yüksek önceliğe sahiptir, bu nedenle yapıya işaretçi içeren yapıya işaretçi içeren yapıya sahipseniz ... Bu ikisi eşdeğerdir:

(*(*(*a).b).c).d

a->b->c->d

Ancak ikincisi açıkça daha okunabilir. Ok operatörü en yüksek önceliğe sahiptir (tıpkı nokta gibi) ve soldan sağa ilişkilendirir. Sanırım bu nokta operatörünün hem yapı hem de yapı için nokta operatörünü kullanmaktan daha net olduğunu düşünüyorum, çünkü başka bir dosyada bile olabilen bildirime bakmak zorunda kalmadan ifadeden türü biliyoruz.


2
Hem yapıları hem de yapılara işaretçiler içeren iç içe veri türleriyle, her alt erişim için doğru operatörü seçmeyi düşünmeniz gerektiğinden işleri daha zorlaştırabilir. Sen ab-> c-> d veya a-> bc-> d ile sonuçlanabilir (freetype kütüphanesini kullanırken bu sorunu yaşadım - her zaman kaynak kodunu aramak gerekiyordu). Ayrıca bu, işaretçilerle uğraşırken derleyicinin işaretçiyi otomatik olarak bırakmasına neden olanak vermeyeceğini açıklamamaktadır.
Askaga

3
Belirttiğiniz gerçekler doğru olsa da, orijinal soruma hiçbir şekilde cevap vermiyorlar. A-> ve * (a) 'nın eşitliğini açıklarsınız. (diğer sorularda daha önce birçok kez açıklanmış olan) gösterimlerin yanı sıra dil tasarımının biraz keyfi olmasıyla ilgili belirsiz bir ifade vermek. Cevabınızı çok yararlı bulamadım, bu yüzden downvote.
Askaga

16
@effeffe, OP dili kolayca yorumlanabilir olabileceğini söylediğini a.b.c.dolarak (*(*(*a).b).c).drender ->yararsız operatörü. Böylece OP'nin sürümü ( a.b.c.d) eşit olarak okunabilir (ile karşılaştırıldığında a->b->c->d). Bu yüzden cevabınız OP'nin sorusuna cevap vermiyor.
Shahbaz

4
Bu bir java programcısı için duruma göre @Shahbaz, bir C / C ++ programcısı anlayacak a.b.c.dve a->b->c->diki olarak çok farklı şeyler: İlk iç içe geçmiş bir alt nesneye tek bellek erişimi olan (bu durumda yalnızca tek bir hafıza nesne yoktur ), ikincisi, dört olası farklı nesneden işaretçileri kovalayan üç hafıza erişimidir. Bu, bellek düzeninde büyük bir fark ve C'nin bu iki durumu çok görünür bir şekilde ayırt etmekte haklı olduğuna inanıyorum.
cmaster - eski haline monica

2
@Shahbaz Java programcılarına hakaret olarak, tamamen örtük işaretçileri olan bir dile alıştıklarını kastetmedim. Bir java programcısı olarak yetiştirilmiş olsaydım, muhtemelen aynı şekilde düşünürdüm ... Her neyse, aslında C'de gördüğümüz aşırı yükleme operatörünün optimalden daha az olduğunu düşünüyorum. Ancak, hepimizin hemen hemen her şey için operatörlerini liberal olarak aşırı yükleyen matematikçiler tarafından bozulduğunu kabul ediyorum. Motivasyonlarını da anlıyorum, çünkü mevcut semboller seti oldukça sınırlı. Sanırım, sonunda sadece çizgiyi çizdiğiniz soru ...
cmaster - monins'i eski haline getirmek

19

C ayrıca belirsiz bir şey yapmamakta da iyi bir iş çıkarıyor.

Noktanın her iki şey için de aşırı yüklenebileceğinden emin olun, ancak ok, programcının bir işaretçi üzerinde çalıştığını bildiğinden emin olur, tıpkı derleyici iki uyumsuz türü karıştırmanıza izin vermediğinde olduğu gibi.


4
Bu basit ve doğru cevaptır. C çoğunlukla C hakkında en iyi şeylerden biri olan IMO'nun aşırı yüklenmesinden kaçınmaya çalışır
jforberg

10
C'deki birçok şey belirsiz ve belirsizdir. Örtük tür dönüşümleri var, matematik operatörleri aşırı yüklenmiş, zincirleme indeksleme, çok boyutlu bir diziyi veya bir işaretçi dizini dizine ekleyip eklemediğinize bağlı olarak tamamen farklı bir şey yapıyor ve her şey herhangi bir şeyi gizleyen bir makro olabilir (üstteki adlandırma kuralı orada yardımcı olur, ancak C t).
PSkocik
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.