C işaretçileri: sabit boyutlu bir diziye işaret etme


120

Bu soru oradaki C gurularına geliyor:

C'de bir gösterici şu şekilde bildirilebilir:

char (* p)[10];

.. temelde bu işaretçinin 10 karakterlik bir diziyi gösterdiğini belirtir. Bunun gibi bir gösterici bildirmenin en güzel yanı, p'ye farklı boyutta bir dizi gösterici atamaya çalışırsanız derleme zamanı hatası alacağınızdır. Basit bir karakter göstericisinin değerini p'ye atamaya çalışırsanız, aynı zamanda size bir derleme zamanı hatası verecektir. Bunu gcc ile denedim ve ANSI, C89 ve C99 ile çalışıyor gibi görünüyor.

Bana öyle geliyor ki, bunun gibi bir gösterici bildirmek çok faydalı olacak - özellikle bir işleve bir gösterici geçerken. Genellikle insanlar böyle bir işlevin prototipini şöyle yazarlar:

void foo(char * p, int plen);

Belirli bir boyutta bir tampon bekliyor olsaydınız, sadece plen değerini test edersiniz. Ancak, p'yi size ileten kişinin o tamponda size gerçekten tam geçerli bellek yerleri vereceği garanti edilemez. Bu işlevi çağıran kişinin doğru şeyi yaptığına güvenmelisiniz. Diğer yandan:

void foo(char (*p)[10]);

.. arayan kişiyi size belirtilen boyutta bir arabellek vermeye zorlar.

Bu çok kullanışlı görünüyor, ancak karşılaştığım hiçbir kodda böyle bildirilen bir işaretçi görmedim.

Sorum şu: İnsanların böyle işaretler söylememesinin herhangi bir nedeni var mı? Bariz bir tuzak görmüyor muyum?


3
not: C99'dan beri, dizinin başlıkta önerildiği gibi sabit boyutta 10olması gerekmez, kapsamdaki herhangi bir değişkenle değiştirilebilir
MM

Yanıtlar:


174

Gönderinizde söyledikleriniz kesinlikle doğrudur. Her C geliştiricisinin, C dili ile belirli bir yeterlilik seviyesine ulaştıklarında (eğer) tam olarak aynı keşfi ve aynı sonuca ulaştığını söyleyebilirim.

Uygulama alanınızın özellikleri, belirli bir sabit boyutta bir dizi gerektirdiğinde (dizi boyutu bir derleme zamanı sabitidir), böyle bir diziyi bir işleve geçirmenin tek uygun yolu, diziye işaretçi parametresi kullanmaktır.

void foo(char (*p)[10]);

(C ++ dilinde bu, referanslarla da yapılır

void foo(char (&p)[10]);

).

Bu, tam olarak doğru boyuttaki dizinin bağımsız değişken olarak sağlandığından emin olan dil düzeyinde tür denetimini etkinleştirir. Aslında, birçok durumda insanlar farkında bile olmadan bu tekniği örtük olarak kullanır ve dizi türünü typedef adının arkasına gizler.

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Ek olarak, yukarıdaki kodun Vector3dbir dizi veya bir türle ilişkili olarak değişmediğini unutmayın struct. Vector3dHerhangi bir zamanda tanımını bir diziden a structve geri olarak değiştirebilirsiniz ve işlev bildirimini değiştirmek zorunda kalmazsınız. Her iki durumda da işlevler "referans olarak" bir toplu nesne alacaklardır (bunun istisnaları vardır, ancak bu tartışma bağlamında bu doğrudur).

Bununla birlikte, bu dizi geçirme yönteminin açıkça çok sık kullanıldığını görmeyeceksiniz, çünkü çok fazla insan oldukça karmaşık bir sözdizimiyle kafasını karıştırıyor ve C dilinin bu tür özellikleriyle onları doğru şekilde kullanmak için yeterince rahat değil. Bu nedenle, ortalama gerçek hayatta bir diziyi ilk öğesine işaretçi olarak geçirmek daha popüler bir yaklaşımdır. Sadece "daha basit" görünüyor.

Ancak gerçekte, dizi geçişi için ilk öğeye göstericiyi kullanmak çok niş bir tekniktir, çok özel bir amaca hizmet eden bir hile: Tek ve tek amacı, farklı boyuttaki dizileri (yani çalışma zamanı boyutu) geçirmeyi kolaylaştırmaktır. . Gerçekten çalışma zamanı boyutundaki dizileri işleyebilmeniz gerekiyorsa, bu tür bir diziyi geçirmenin uygun yolu, ek bir parametre tarafından sağlanan somut boyutla ilk öğesine bir göstericidir.

void foo(char p[], unsigned plen);

Aslında, birçok durumda, çalışma zamanı boyutundaki dizileri işleyebilmek çok yararlıdır ve bu da yöntemin popülerliğine katkıda bulunur. Birçok C geliştiricisi, sabit boyutlu bir diziyi işleme ihtiyacıyla asla karşılaşmaz (veya asla fark etmez), böylece uygun sabit boyutlu tekniğe aldırış etmez.

Bununla birlikte, dizi boyutu sabitse, onu bir elemana işaretçi olarak geçirme

void foo(char p[])

maalesef günümüzde oldukça yaygın olan teknik düzeyinde büyük bir hatadır. Diziye işaretçi tekniği bu gibi durumlarda çok daha iyi bir yaklaşımdır.

Sabit boyutlu dizi geçirme tekniğinin benimsenmesini engelleyebilecek bir başka neden, dinamik olarak tahsis edilmiş dizilerin tiplendirilmesine yönelik naif yaklaşımın baskınlığıdır. Örneğin, program sabit tür dizileri çağırırsa char[10](örneğinizde olduğu gibi), ortalama bir geliştirici şu mallocdizileri verecektir:

char *p = malloc(10 * sizeof *p);

Bu dizi şu şekilde bildirilen bir işleve geçirilemez:

void foo(char (*p)[10]);

Bu, ortalama geliştiricinin kafasını karıştırır ve onları daha fazla düşünmeden sabit boyutlu parametre bildiriminden vazgeçirir. Gerçekte ise sorunun kökü saf mallocyaklaşımda yatmaktadır . mallocYukarıda gösterilen çalıştırma büyüklüğü diziler için ayrılmalıdır. Dizi türünün derleme zamanı boyutu varsa, bunun daha iyi bir yolu mallocaşağıdaki gibi görünür

char (*p)[10] = malloc(sizeof *p);

Bu, elbette, yukarıda beyan edilenlere kolayca aktarılabilir foo

foo(p);

ve derleyici uygun tip kontrolünü gerçekleştirecektir. Ancak yine, hazırlıksız bir C geliştiricisi için bu aşırı derecede kafa karıştırıcıdır, bu yüzden onu "tipik" ortalama günlük kodda çok sık göremezsiniz.


2
Cevap sizeof () 'un nasıl başarılı olduğu, ne sıklıkla başarısız olduğu ve her zaman başarısız olduğu yollarla ilgili çok kısa ve bilgilendirici bir açıklama sunar. Çoğu C / C ++ mühendisinin anlamadığına dair gözlemleriniz ve bu nedenle bunun yerine anladıklarını düşündükleri bir şeyi yapmak, bir süredir gördüğüm en kehanet şeylerinden biridir ve örtü, tanımladığı doğrulukla karşılaştırıldığında hiçbir şey değildir. cidden, efendim. mükemmel cevap.
WhozCraig

Bu yanıta dayalı olarak bazı kodları yeniden düzenledim, bravo ve hem Q hem de A için teşekkürler
Perry

1
constBu teknikle mülkiyeti nasıl idare ettiğinizi merak ediyorum . Bir const char (*p)[N]argüman bir gösterici ile uyumlu görünmüyor char table[N];Aksine, basit bir char*ptr const char*argümanla uyumlu kalır .
Camgöbeği

4
Dizinizin bir öğesine erişmek için yapmanız (*p)[i]ve yapmanız gerekmediğini unutmamak faydalı olabilir *p[i]. İkincisi, neredeyse kesinlikle istediğiniz gibi olmayan dizinin boyutuna göre atlayacaktır. En azından benim için, bu sözdizimini öğrenmek engellenmekten çok bir hataya neden oldu; Sadece bir float * geçerken doğru kodu daha hızlı alırdım.
Andrew Wagner

1
Evet @ mickey, önerdiğiniz şey const, değiştirilebilir öğeler dizisine bir işaretçi. Ve evet, bu bir göstericiden değişmez öğeler dizisine tamamen farklıdır.
Cyan

11

AndreyT'nin cevabına eklemek istiyorum (bu konu hakkında daha fazla bilgi arayan birisinin bu sayfaya rastlaması durumunda):

Bu bildirimlerle daha fazla oynamaya başladığımda, C'de bunlarla ilişkili büyük bir handikap olduğunu anlıyorum (görünüşe göre C ++ da değil). Arayan kişiye yazdığınız bir arabelleğe sabit bir işaretçi vermek istediğiniz bir duruma sahip olmak oldukça yaygındır. Maalesef, C'de böyle bir gösterici ilan ederken bu mümkün değildir. Başka bir deyişle, C standardı (6.7.3 - Paragraf 8) şunun gibi bir şeyle çelişmektedir:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Bu kısıtlama C ++ 'da görünmüyor, bu da bu tür bildirimleri çok daha kullanışlı hale getiriyor. Ancak C durumunda, sabit boyutlu arabelleğe bir const işaretçisi istediğinizde normal bir işaretçi bildirimine geri dönmek gerekir (arabelleğin kendisi başlamak için const olarak bildirilmediği sürece). Bu posta dizisinde daha fazla bilgi bulabilirsiniz: bağlantı metni

Bu benim görüşüme göre ciddi bir kısıtlama ve C'de insanların genellikle böyle işaretçiler beyan etmemelerinin ana nedenlerinden biri olabilir. Diğeri, çoğu insanın böyle bir göstericiyi şu şekilde açıklayabileceğinizi bile bilmemesidir. AndreyT işaret etti.


2
Bu, derleyiciye özgü bir sorun gibi görünüyor. Gcc 4.9.1 kullanarak çoğaltma yapabildim, ancak clang 3.4.2, const olmayan bir sürümden sabit sürümüne sorunsuz geçebildi. C11 spesifikasyonunu okudum (benim versiyonumda p 9 ... iki nitelikli tipin uyumlu olduğundan bahseden kısım) ve bu dönüşümlerin yasadışı olduğunu söylediği anlaşılıyor. Bununla birlikte, pratikte her zaman otomatik olarak char * 'dan char const *' a herhangi bir uyarı yapmadan dönüştürebileceğinizi biliyoruz. IMO, clang buna izin verme konusunda gcc'den daha tutarlıdır, ancak şartnamenin bu otomatik dönüşümlerden herhangi birini yasakladığı konusunda sizinle aynı fikirdeyim.
Doug Richardson

4

Bunun açık nedeni, bu kodun derlenmemesidir:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

Bir dizinin varsayılan yükseltmesi, nitelenmemiş bir göstericidir.

Ayrıca bu soruya bakın , kullanmak foo(&p)işe yaramalı.


3
Elbette foo (p) çalışmayacak, foo 10 elemanlık bir diziye işaretçi istiyor, bu yüzden dizinizin adresini iletmeniz gerekiyor ...
Brian R. Bondy

9
Bu "bariz sebep" nasıl oluyor? Açıkçası, işlevi çağırmanın doğru yolunun olduğu anlaşılmıştır foo(&p).
AnT

3
Sanırım "apaçık" yanlış kelime. "En açık" demek istedim. Bu durumda p ve & p arasındaki ayrım, ortalama C programcısı için oldukça belirsizdir. Posterin önerdiklerini yapmaya çalışan biri yazdıklarımı yazacak, derleme zamanı hatası alacak ve pes edecek.
Keith Randall

2

Ayrıca daha fazla tür denetimini etkinleştirmek için bu sözdizimini kullanmak istiyorum.

Ama aynı zamanda işaretçi kullanmanın sözdizimi ve zihinsel modelinin daha basit ve hatırlanması daha kolay olduğuna da katılıyorum.

İşte karşılaştığım birkaç engel daha.

  • Diziye erişim (*p)[]şunları kullanmayı gerektirir :

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    Bunun yerine yerel bir karakter için işaretçi kullanmak cazip geliyor:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Ancak bu, doğru türü kullanma amacını kısmen ortadan kaldırır.

  • Bir dizinin adresini bir diziye işaretçi atarken adres operatörünü kullanmayı unutmamak gerekir:

    char a[10];
    char (*p)[10] = &a;

    İşleci adresi işleci, atanacağı &adoğru türle içerideki tüm dizinin adresini alır p. Operatör olmadan a, otomatik olarak &a[0]farklı bir türe sahip olan dizinin ilk öğesinin adresine dönüştürülür .

    Bu otomatik dönüşüm zaten gerçekleştiğinden, &gerekli olduğu konusunda her zaman şaşkınım . &Diğer türlerdeki on değişkenlerinin kullanımıyla tutarlıdır , ancak bir dizinin özel olduğunu &ve adres değeri aynı olsa bile doğru türde adres almam gerektiğini hatırlamalıyım .

    Sorunumun bir nedeni, 80'lerde K&R C'yi öğrendiğim olabilir; bu, &operatörün tüm dizilerde kullanılmasına henüz izin vermedi (bazı derleyiciler bunu görmezden gelse veya sözdizimini tolere etse de). Bu arada, dizilerden diziye işaretçilerin benimsenmekte zorlanmasının bir başka nedeni de olabilir: bunlar yalnızca ANSI C'den beri düzgün çalışıyorlar ve &operatör sınırlaması onları çok garip bulmanın başka bir nedeni olabilir.

  • Ne zaman typedefbir değil (ortak başlık dosyasına) işaretçi-dizi için bir tür oluşturmak için kullanılan, daha sonra küresel bir işaretçi-to-dizi daha karmaşık ihtiyacı externdosyalar arasında paylaşmak beyanı:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];

1

Basitçe söylemek gerekirse, C işleri bu şekilde yapmaz. Bir dizi türü T, dizideki birinciye işaretçi olarak geçirilir Tve tüm elde ettiğiniz budur.

Bu, dizide aşağıdaki gibi ifadelerle döngü yapmak gibi bazı harika ve zarif algoritmalara izin verir.

*dst++ = *src++

Olumsuz yanı, boyutun yönetiminin size kalmış olmasıdır. Ne yazık ki, bunu bilinçli bir şekilde yapmamak, C kodlamada milyonlarca hataya ve / veya kötü niyetli sömürü fırsatlarına da yol açmıştır.

C'de sorduğunuz şeye yaklaşan şey, bir struct(değere göre) veya bir göstericinin (referans olarak) etrafından geçmektir . Bu işlemin her iki tarafında da aynı yapı türü kullanıldığı sürece, hem referansı veren kod hem de onu kullanan kod işlenmekte olan verilerin boyutu konusunda hemfikirdir.

Yapınız istediğiniz verileri içerebilir; iyi tanımlanmış bir boyuttaki dizinizi içerebilir.

Yine de, hiçbir şey sizin veya yetersiz veya kötü niyetli bir kodlayıcının, derleyiciyi yapınızı farklı bir boyutta ele alması için kandırmak için dökümleri kullanmasını engellemez. Bu tür şeyleri yapmak için neredeyse zincirlenmemiş yetenek, C'nin tasarımının bir parçasıdır.


1

Bir karakter dizisini çeşitli şekillerde tanımlayabilirsiniz:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Bir diziyi değere göre alan bir işlevin prototipi şöyledir:

void foo(char* p); //cannot modify p

veya referansla:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

veya dizi sözdizimine göre:

void foo(char p[]); //same as char*

2
Sabit boyutlu bir dizinin dinamik olarak char (*p)[10] = malloc(sizeof *p).
AnT

Burada char dizisi [] ve char * ptr arasındaki farklar arasında daha ayrıntılı bir tartışma için buraya bakın. stackoverflow.com/questions/1807530/…
t0mm13b

1

Bu çözümü tavsiye etmem

typedef int Vector3d[3];

Çünkü Vector3D'nin bilmeniz gereken bir türe sahip olduğu gerçeğini gizler. Programcılar genellikle aynı türdeki değişkenlerin farklı boyutlara sahip olmasını beklemezler. Düşünmek :

void foo(Vector3d a) {
   Vector3D b;
}

burada sizeof a! = sizeof b


Bunu bir çözüm olarak önermiyordu. Bunu sadece bir örnek olarak kullanıyordu.
figurassa

Hm. Neden aynı sizeof(a)değil sizeof(b)?
sherrellbc

0

Belki bir şeyi kaçırıyorum, ama ... diziler sabit işaretçiler olduğu için, temelde bu, onlara işaretçilerden geçmenin bir anlamı olmadığı anlamına gelir.

Sadece kullanamaz void foo(char p[10], int plen);mısın?


4
Diziler sabit işaretçiler DEĞİLDİR. Lütfen dizilerle ilgili bazı SSS'leri okuyun.
AnT

2
Burada önemli olan şey için (parametreler olarak tek boyutlu diziler), gerçek şu ki, sabit işaretleyicilere bozunuyorlar. Nasıl daha az bilgiçlikçi olunacağına dair bir SSS okuyun, lütfen.
fortran

-2

Derleyicimde (vs2008) char (*p)[10], bir C dosyası olarak derlesem bile, parantez yokmuş gibi bir karakter işaretçisi dizisi olarak davranır . Derleyici bu "değişken" için destekliyor mu? Eğer öyleyse, bu onu kullanmamak için önemli bir nedendir.


1
-1 Yanlış. Vs2008, vs2010, gcc'de sorunsuz çalışıyor. Özellikle bu örnek iyi çalışıyor: stackoverflow.com/a/19208364/2333290
kotlomoy
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.