Neden C'deki yapıları döndüren birçok işlev aslında işaretçileri yapılara döndürür?


49

returnİşlev yapısında tüm yapıyı döndürmenin aksine bir göstericiyi bir yapıya döndürmenin avantajı nedir ?

Fonksiyonlar fopenve diğer düşük seviyeli fonksiyonlar hakkında konuşuyorum ama muhtemelen işaretçileri yapılara döndüren daha yüksek seviyeli fonksiyonlar var.

Bunun sadece bir programlama sorusundan ziyade bir tasarım seçeneği olduğuna inanıyorum ve iki yöntemin avantajları ve dezavantajları hakkında daha fazla şey bilmek istiyorum.

İşaretçiyi bir yapıya döndürmenin bir avantaj olacağını düşündüğüm sebeplerden biri, işaretçi döndürerek fonksiyonun başarısız olması durumunda daha kolay bir şekilde söyleyebilmek NULL.

Geri Gelen dolu olan yapıyı NULLolurdu zor herhalde az çok verimli. Bu geçerli bir sebep mi?


9
@ JohnR.Strohm Denedim ve gerçekten işe yarıyor. Bir fonksiyon bir yapıya dönebilir .... Öyleyse sebebi ne yapılmadı?
yoyo_fun

27
Ön standardizasyon C yapıların kopyalanmasına veya değere göre aktarılmasına izin vermedi. C standart kütüphanesi, o çağdan bugünün bu şekilde yazılmayacak pek çok başarıya sahip, örneğin tamamen yanlış tasarlanan gets()işlevin kaldırılması için C11'e kadar sürdü . Bazı programcılar hala yapıların kopyalanmasına karşı bir nefret duyuyorlar, eski alışkanlıklar zorlaşıyor.
amon

26
FILE*etkili bir opak tutamaçtır. Kullanıcı kodu, iç yapısının ne olduğu ile ilgilenmemelidir.
CodesInChaos,

3
Referansa göre geri dönmek, yalnızca çöp koleksiyonunuz olduğunda makul bir varsayılandır.
Idan Arye

6
@ JohnR.Strohm Profilinizdeki "çok kıdemli", 1989'dan önce geri dönüyor gibi görünüyor ;-) - ANSI C K&R C'nin yapmadığı şeylere izin verdiğinde: Yapıları atamalarda kopyala, parametre geçişi ve dönüş değerleri. K & R'ın asıl kitabı gerçekten açıkça ifade edildi (Ben yine ifade ediyorum): "Bir yapıda tam olarak iki şey yapabilir, adresini ele alabilir & ve bir üyeye erişebilirsiniz .."
Peter - Monica'yı yeniden birleştirin

Yanıtlar:


61

fopenTür örnekleri yerine dönüş işaretçisi gibi işlevlerin uygulanmasının birkaç pratik nedeni structvardır:

  1. Türün gösterimini structkullanıcıdan gizlemek istiyorsunuz ;
  2. Bir nesneyi dinamik olarak tahsis ediyorsunuz;
  3. Bir nesnenin tek bir örneğine birden fazla referans aracılığıyla atıfta bulunuyorsunuz;

Bunun gibi bir tür olması durumunda, türün FILE *kullanıcıya gösterimini ayrıntılarıyla açıklamak istemezsiniz - FILE *nesne opak bir tutamaç işlevi görür ve bu tutamacı çeşitli G / Ç yordamlarına geçirirsiniz (ve FILEgenellikle bir şekilde uygulanan structtürü, o gelmez sahip ) olmak.

Böylece, bir başlığın eksik bir struct türünü bir yerde gösterebilirsiniz :

typedef struct __some_internal_stream_implementation FILE;

Eksik bir türün bir örneğini bildiremezseniz de, ona bir işaretçi bildirebilirsiniz. Bu yüzden bir oluşturabilir FILE *ve buna yoluyla atamak fopen, freopenvb ama doğrudan o işaret nesne manipüle edilemez.

Ayrıca, fopenfonksiyonun bir FILEnesneyi dinamik olarak, kullanarak mallocveya benzer şekilde tahsis etmesi de muhtemeldir . Bu durumda, imleci döndürmek mantıklıdır.

Son olarak, bir structnesneyi bir tür devlette saklamanız mümkündür ve bu durumu birkaç farklı yerde kullanılabilir hale getirmeniz gerekir. Türün örneklerini döndürdüyseniz struct, bu örnekler bellekteki birbirinden ayrı nesneler olur ve sonunda senkronizasyondan çıkar. Bir işaretçiyi tek bir nesneye döndürerek herkes aynı nesneye atıfta bulunur.


31
İşaretçiyi opak tip olarak kullanmanın özel bir avantajı, yapının kendisinin kütüphane sürümleri arasında değişebilmesi ve arayanları yeniden derlemenize gerek olmamasıdır.
Barmar

6
@Barmar Gerçekten de, ABI stabilitesi olan C büyük bir satış noktası ve opak işaretçiler olmadan stabil olmaz.
Matthieu M.

37

“Bir yapıyı geri getirmenin” iki yolu vardır. Verilerin bir kopyasını döndürebilir veya bir referans (işaretçi) ile döndürebilirsiniz. Genelde, bir sebepten dolayı bir göstericiyi döndürmek (ve genel olarak dolaşmak) tercih edilir.

İlk olarak, bir yapıyı kopyalamak bir işaretçiyi kopyalamaktan çok daha fazla CPU zaman alır. Bu, kodunuzun sıkça yaptığı bir şeyse, belirgin bir performans farkına neden olabilir.

İkincisi, etrafta bir işaretçiyi kaç kez kopyaladığınızdan bağımsız olarak, hala bellekteki aynı yapıya işaret ediyor. Tüm modifikasyonlar aynı yapıya yansıtılacaktır. Ancak, yapının kendisini kopyalar ve ardından bir değişiklik yaparsanız, değişiklik yalnızca bu kopyada gösterilir . Farklı bir kopyaya sahip olan herhangi bir kod değişikliği görmez. Bazen, çok nadiren, istediğiniz budur, ancak çoğu zaman değildir ve yanlış yaparsanız hatalara neden olabilir.


54
İşaretçi ile geri dönmenin dezavantajı: şimdi bu nesnenin sahipliğini izlemeniz ve onu serbest bırakmanız gerekir. Ayrıca, işaretçi indirme, hızlı bir kopyadan daha maliyetli olabilir. Burada birçok değişken var, bu yüzden işaretçileri kullanmak evrensel olarak daha iyi değil.
amon,

17
Ayrıca, bugünlerde işaretçiler çoğu masaüstü ve sunucu platformunda 64 bit. Kariyerimde 64 bite uyacak birkaç yapı daha gördüm. Dolayısıyla, bir işaretçiyi kopyalamanın bir yapı kopyalamaktan daha düşük bir maliyet olduğunu her zaman söyleyemezsiniz.
Solomon Yavaş

37
Bu çoğunlukla iyi bir cevap, ama bazen bu konuya katılmıyorum , çok nadiren, istediğin şey bu ama çoğu zaman değil - tam tersi. Bir işaretçiyi döndürmek, istenmeyen yan etkilerin birkaç türünü ve bir işaretçinin yanlış sahipliğini almanın çeşitli kötü yollarına izin verir. CPU zamanının bu kadar önemli olmadığı durumlarda, kopya seçeneğini tercih ederim, eğer bir seçenekse, hataya daha az açıktır.
Doktor Brown

6
Bunun gerçekten sadece harici API'ler için geçerli olduğuna dikkat edilmelidir. Dahili işlevler için, son on yılda marjinal olarak yetkin olan her derleyici, bir işaretçiyi ek bir argüman olarak almak ve nesneyi doğrudan oraya inşa etmek için büyük bir yapı döndüren bir işlevi yeniden yazar. Değişmezler ve değişkenler argümanları yeterince sık yapılmıştır, ancak değişmez veri yapılarının neredeyse hiçbir zaman istediğiniz şey olmadığı iddiasının doğru olmadığı konusunda hemfikir olduğumuzu düşünüyorum.
Voo

6
Derleme ateş duvarlarını işaretçiler için bir profesyonel olarak da belirtebilirsiniz. Yaygın olarak paylaşılan başlıklara sahip büyük programlarda, işlevlerin eksik olduğu türler, bir uygulama ayrıntısı her değiştiğinde yeniden derleme gerekliliğini önler. Daha iyi derleme davranışı aslında arayüz ve uygulama ayrıldığında elde edilen kapsüllemenin bir yan etkisidir. Değere göre geri dönmek (ve geçmek, atamak) uygulama bilgisine ihtiyaç duyar.
Peter - Monica'yı yeniden

12

Diğer cevaplara ek olarak, bazen küçük bir struct değere sahip olmak önemlidir. Örneğin, bir çift bir veri ve bununla ilgili bir hata (veya başarı) kodu verebilir.

Bir örnek almak için, fopenyalnızca bir veri (açık FILE*) döndürür ve hata durumunda, errnosözde global değişkeni hata kodunu verir . Ancak structiki üyeden birini geri göndermek belki daha iyi olurdu : FILE*tanıtıcı ve hata kodu (eğer dosya tanıtıcısı ayarlanmışsa NULL). Tarihsel nedenlerden ötürü durum böyle değildir (ve errnogünümüzde makro olan küresel hatalar bildirilmektedir ).

Go dilinin iki (veya birkaç) değer döndürmek için hoş bir notasyonu olduğuna dikkat edin .

Ayrıca, Linux / x86-64'te ABI ve çağrı kurallarının (bakınız x86-psABI sayfasına) struct, iki skaler üyeden birinin (örneğin bir işaretçi ve bir tam sayı veya iki işaretçi veya iki tam sayı) iki kayıt arasında döndürüldüğünü belirttiğine dikkat edin. (ve bu çok verimli ve hafızaya girmiyor).

Bu nedenle, yeni C kodunda küçük bir C döndürmek structdaha okunaklı, iş parçacığı dostu ve daha verimli olabilir.


Aslında küçük yapılar içine paketlenirrdx:rax . Böylece struct foo { int a,b; };paketlenmiş olarak rax(örneğin vardiya / veya ile) iade edilir ve vardiya / mov ile ambalajından çıkarılması gerekir. İşte Godbolt'a bir örnek . Fakat x86, yüksek bitlere dikkat etmeden 32 bitlik işlemler için 64 bitlik bir yazıcının düşük 32 bitini kullanabilir, bu nedenle her zaman çok kötüdür, ancak 2 üyeli yapılarda çoğu zaman 2 yazmaç kullanmaktan daha kötüdür.
Peter Cordes

İlgili: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> , booleanı ilk yarısında döndürür rax, bu nedenle test etmek için sabit bir 64-bit maskeye ihtiyacınız vardır test. Ya da kullanabilirsiniz bt. Ancak arayan ve callee'yi kullanmak için karşılaştırır dl, derleyicilerin "özel" işlevler için yapması gerekenler. Ayrıca, ilgili: libstdc ++ 'lar std::optional<T>T olduğunda bile önemsiz şekilde kopyalanamaz, bu nedenle her zaman gizli işaretçiyle döner: stackoverflow.com/questions/46544019/… . (libc ++ 'ı önemsiz şekilde kopyalanabilir)
Peter Cordes

@PeterCordes: ilgili şeyleriniz C ++ değil C
Basile Starynkevitch 20:17

Hata! Arayan booleanı test etmek istiyorsa, aynı şey tam olarak struct { int a; _Bool b; };C'ye de uygulanacaktır , çünkü önemsizce kopyalanabilen C ++ yapıları C ile aynı ABI'yi kullanır
Peter Cordes

1
Klasik örnekdiv_t div()
chux

6

Doğru yoldasın

Bahsettiğiniz nedenlerin her ikisi de geçerlidir:

İşaretçi bir yapıya döndürmek için bir avantaj olacağını düşündüğüm nedenlerden biri, işlev NULL işaretçi döndürülerek başarısız olursa daha kolay bir şekilde söyleyebilmek.

NULL olan bir FULL yapı döndürmek, sanırım daha zor veya daha az verimli olacaktır. Bu geçerli bir sebep mi?

Örneğin bellekte bir yerde bir dokuya sahipseniz ve bu dokuya programınızın birçok yerinde başvurmak istiyorsanız; Her başvuruda bulunmak istediğinizde bir kopya çıkarmak akıllıca olmaz. Bunun yerine, dokuyu referans almak için basitçe bir işaretçiyi dolaştırırsanız, programınız çok daha hızlı çalışacaktır.

Yine de en büyük sebep dinamik bellek tahsisi. Çoğu zaman, bir program derlendiğinde, belirli veri yapıları için tam olarak ne kadar hafızaya ihtiyacınız olduğundan emin değilsiniz. Bu olduğunda, kullanmanız gereken bellek miktarı çalışma zamanında belirlenir. 'Malloc' kullanarak hafıza talep edebilir ve 'free' işlemini bitirdiğinizde boşaltabilirsiniz.

Buna güzel bir örnek, kullanıcı tarafından belirtilen bir dosyadan okumaktır. Bu durumda, programı derlerken dosyanın büyüklüğü hakkında hiçbir fikriniz olmaz. Yalnızca program gerçekten çalışırken ne kadar belleğe ihtiyacınız olduğunu belirleyebilirsiniz.

Hem malloc hem de serbest dönüş işaretçileri bellekteki konumlara işaret eder. Bu nedenle, dinamik bellek tahsisini kullanan işlevler, işaretçileri yapılarını bellekte oluşturdukları yere döndürür.

Ayrıca, yorumlarda, bir yapıdan bir işlevden dönüp dönmeyeceğinize ilişkin bir soru olduğunu görüyorum. Bunu gerçekten yapabilirsin. Aşağıdakilerin çalışması gerekir:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

Belirli bir değişkenin ne kadar belleğe ihtiyaç duyduğunu bilmemek nasıl tanımlanmış yapı tipine sahipseniz nasıl mümkün olabilir?
yoyo_fun

9
@JenniferAnderson C tamamlanmamış tipler kavramına sahiptir: bir tür adı bildirilebilir, ancak henüz tanımlanmadı, bu yüzden boyutu kullanılamıyor. Ben bu tür değişkenleri bildirmek, ancak bildirebilirsiniz işaretçileri örneğin o tipe struct incomplete* foo(void). Bu şekilde bir başlıktaki işlevleri bildirebilirim, ancak yalnızca bir C dosyasındaki yapıları tanımlayarak enkapsülasyona izin verebilirim.
amon

@ amon Bu, nasıl çalıştıklarını açıklamadan önce fonksiyon başlıklarını (prototipler / imzalar) ilan etmenin aslında C ile yapıldığını nasıl ifade ediyor? Ve C yapıları ve sendikalara aynı şeyi yapmak mümkündür
yoyo_fun

@JenniferAnderson , üstbilgi dosyalarında işlev prototiplerini (gövdeler olmadan işlevler) bildirirsiniz ve daha sonra bu işlevleri işlevlerin gövdesini bilmeden başka bir kodda çağırırsınız, çünkü derleyicinin sadece argümanları nasıl düzenleyeceğini ve nasıl kabul edeceğini bilmesi gerekir geri dönüş değeri. Programı bağladığınız zaman, aslında fonksiyon tanımını bilmek zorundasınız (yani bir bedenle), ancak bunu sadece bir kez işlemeniz gerekir. Basit olmayan bir tür kullanıyorsanız, bu türün yapısını da bilmesi gerekir, ancak işaretçiler genellikle aynı boyuttadır ve bir prototipin kullanımı için önemli değildir.
basit kullanıcı

6

Bir gibi bir şey FILE*gerçekten müşteri kodu söz konusu olduğunda bir yapı için bir işaretçi değil, bunun yerine bir dosya gibi diğer bazı varlık ile ilişkili bir opak tanımlayıcı biçimidir . Bir program çağrıldığında fopen, genellikle iade edilen yapının içeriğini umursamaz - tek umurunda olacak, diğer fonksiyonların freadonunla ne yapmaları gerekiyorsa onu yapacaklarıdır.

Standart bir kütüphane, FILE*örneğin o dosyanın içindeki mevcut okuma pozisyonu hakkında bir bilgi içerisinde tutulursa, bir aramanın freadbu bilgiyi güncelleyebilmesi gerekir. Having freadbir gösterici almak FILEo kadar kolay yapar. Eğer freadyerine alınan FILEbu güncelleme yolu yoktur FILEarayan tarafından düzenlenen nesneyi.


3

Bilgi gizleme

İşlevin geri dönüş bildiriminde tüm yapıyı döndürmenin aksine bir göstericiyi bir yapıya döndürmenin avantajı nedir?

En yaygın olanı bilgi gizlemedir . C, structözel bir alan oluşturma yeteneğine sahip değil, onlara erişmek için yöntemler sağlasa bile.

Bu nedenle, geliştiricilerin bir pointee'nin içeriğini görmesini ve kurcalamasını zorla engellemek istiyorsanız FILE, o zaman tek ve tek yol, işaretçiyi pointee büyüklüğü ve tanımı dış dünya tarafından bilinmiyor. O zaman tanım, FILEyalnızca tanımını gerektiren işlemleri yapanlara fopengörünürken, sadece yapı bildirimi kamu başlığına görünür.

İkili Uyumluluk

Yapı tanımını gizlemek, dylib API'lerde ikili uyumluluğu korumak için nefes alma odası sağlamaya da yardımcı olabilir. Kütüphane uygulayıcılarının saydamlık yapısındaki alanları, kütüphaneyi kullananlarla ikili uyumu bozmadan değiştirebilmelerini sağlar, çünkü kodlarının niteliğinin yalnızca yapıyla ne yapabileceklerini, ne kadar büyük olduklarını bilmeleri gerekir var.

Örnek olarak, bugün Windows 95 döneminde inşa edilmiş bazı eski programları çalıştırabilirim (her zaman mükemmel değil, ama şaşırtıcı bir şekilde hala çalışıyor). Muhtemelen, bu eski ikili dosyaların bazı kodlarında, boyutu ve içeriği Windows 95 döneminden değişmiş yapılara opak işaretçiler kullanılmış. Yine de programlar, bu yapıların içeriğine maruz kalmadığından yeni pencerelerde çalışmaya devam ediyor. İkili uyumluluğun önemli olduğu bir kitaplıkta çalışırken, müşterinin maruz kalmadığı şeylerin genellikle geriye dönük uyumluluktan kaçmadan değişmesine izin verilir.

verim

NULL olan bir tam yapı döndürmek, sanırım daha zor ya da daha az verimli olacaktır. Bu geçerli bir sebep mi?

Tipik olarak mallochalihazırda tahsis edilmiş değişken boyutlu bir ayırıcı havuz hafızasından ziyade, sabit bir boyutta olduğu gibi , sahnelerin arkasında kullanılan tipik olarak çok daha az genelleştirilmiş bir bellek ayırıcısı olmadıkça, türün pratik olarak sığabileceği ve istifte tahsis edilebileceği varsayımıyla daha az verimlidir . Bu durumda, büyük olasılıkla, kütüphane geliştiricilerinin ilgili ile ilgili değişmezleri (kavramsal garantileri) korumalarına izin vermek bir güvenlik riskidir FILE.

En azından performans açısından, fopengöstericiyi geri döndürmek bu kadar geçerli bir neden değildir çünkü döndürdüğü tek neden NULLbir dosyayı açmamaktır. Bu, tüm genel durum yürütme yollarını yavaşlatmak karşılığında olağanüstü bir senaryoyu optimize etmek olacaktır. Bazı durumlarda tasarımları daha kolay hale getirmek NULLiçin bazı post-koşullarında iade edilmelerine izin vermek için işaretçilere geri dönmelerini sağlamak için geçerli bir verimlilik nedeni olabilir .

Dosya işlemleri için, ek yükler dosya işlemlerinin kendilerine göre oldukça önemsizdir ve el kitabından fcloseyine de kaçınılması gerekmez . Bu nedenle, istemciyi, bir yığın tahsisini önlemek için dosya işlemlerinin göreceli maliyeti göz önüne alındığında, tanımını FILEgöstererek ve değerine göre geri döndürerek kaynağı serbest bırakma (kapatma) zorluğundan kurtarmamız gibi bir şey değil. fopen.

Sıcak Noktalar ve Düzeltmeler

Yine de, diğer durumlarda, mallocbu uygulamanın opak işaretçilerle çok sık kullanılması ve bazen yığın halinde gereksiz yere çok fazla şey tahsis edilmesi sonucunda, eski kod tabanlarında sıcak noktalara ve gereksiz önbellek eksikliklerine neden olan eski kod tabanlarında bir sürü israf C kodu belirledim. büyük döngüler.

Bunun yerine kullandığım alternatif bir uygulama, başka kimsenin alanlara dokunmaması gerektiğini bildirmek için bir adlandırma kuralı standardı kullanarak, istemcinin kurcalamadığı durumlarda bile yapı tanımlarını ortaya koymaktır:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

Gelecekte ikili uyumluluk kaygıları varsa, o zaman bunun gibi sadece fazladan bir miktar fazladan alan ayırmayı yeterince iyi buldum.

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

Bu ayrılmış alan biraz boşa harcanır, ancak ileride Fookütüphanemizi kullanan ikili dosyaları bozmadan daha fazla veri eklememiz gerekirse, hayat kurtarıcı olabilir .

Bana göre, bilginin gizlenmesi ve ikili uyumluluk tipik olarak, değişken uzunluklu yapıların yanı sıra yapıların sadece yığın tahsisine izin vermesinin tek iyi nedenidir; Yığın üzerinde VLA tahsis etmek için VLA biçiminde hafıza). Büyük yapılar bile yazılımın yığındaki sıcak bellekle çok daha fazla çalıştığı anlamına gelirse değere göre daha ucuza gelir. Ve yaratılışta değerine göre geri dönmek daha ucuz olmasalar bile, bir kişi bunu basitçe yapabilirdi:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... Foogereksiz bir kopya olasılığı olmadan yığından başlatmak . Veya müşteri Foo, bir nedenden ötürü istese bile öbek üzerinde tahsis etme özgürlüğüne bile sahiptir .

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.