C ve C ++ derleyicileri, hiçbir zaman zorlanmadıklarında neden işlev imzalarında dizi uzunluklarına izin veriyor?


132

Öğrenme dönemimde bulduğum şey bu:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

Yani değişkende int dis(char a[1]), [1]hiçbir şey yapmıyor ve hiç çalışmıyor
, çünkü kullanabilirim a[2]. Tıpkı int a[]veya gibi char *a. Dizi adının bir işaretçi olduğunu ve bir dizinin nasıl aktarılacağını biliyorum, bu yüzden bilmecem bu kısımla ilgili değil.

Bilmek istediğim şey, derleyicilerin bu davranışa neden izin verdiğidir ( int a[1]). Yoksa bilmediğim başka anlamları mı var?


6
Bunun nedeni dizileri işlevlere gerçekten geçirememenizdir.
Ed S.

37
Sanırım buradaki soru, C'nin bir parametrenin her halükarda tam olarak bir işaretçi gibi davranacağı halde, bir parametrenin dizi türünde olduğunu bildirmenize neden izin verdiğiydi.
Brian

8
@Brian: Bunun davranış lehine mi yoksa aleyhine bir argüman mı olduğundan emin değilim, ancak argüman türü bir typedefdizi tipiyse de geçerlidir . Argüman türlerinde "işaretçisi çürüme" Yani değiştirilmesi sadece sözdizimsel şeker değil []ile *, gerçekten tip sistemi üzerinden gidiyor. Bunun, va_listdizi veya dizi olmayan türle tanımlanabilen bazı standart türler için gerçek dünya sonuçları vardır .
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

4
@songyuanyao Bir işaretçi kullanılarak C (ve C ++) tamamen farklı değil bir şey başarabilirsiniz: int dis(char (*a)[1]). Daha sonra, bir dizi için bir işaretçi geçmesi: dis(&b). C ++ 'da bulunmayan C özelliklerini kullanmak istiyorsanız, void foo(int data[static 256])ve gibi şeyler de söyleyebilirsiniz int bar(double matrix[*][*]), ancak bu tamamen başka bir solucan kutusu.
Stuart Olsen

1
@StuartOlsen Mesele hangi standardın neyi tanımladığı değil. Mesele şu ki, onu kim tanımladıysa onu bu şekilde tanımladı.
user253751

Yanıtlar:


156

Dizileri işlevlere geçirmek için sözdiziminin bir tuhaflığıdır.

Aslında, C'de bir diziyi geçirmek mümkün değildir. Diziyi geçmesi gerektiği gibi görünen bir sözdizimi yazarsanız, gerçekte olan şey, onun yerine dizinin ilk elemanına bir göstericinin geçirilmesidir.

İşaretçi herhangi bir uzunluk bilgisi içermediğinden [], işlevin biçimsel parametre listesindeki içeriği gerçekte yok sayılır.

Bu sözdizimine izin verme kararı 1970'lerde alındı ​​ve o zamandan beri çok fazla kafa karışıklığına neden oldu ...


21
C olmayan bir programcı olarak, bu cevabı çok erişilebilir buluyorum. +1
asteri

21
"Bu sözdizimine izin verme kararı 1970'lerde alındı ​​ve o zamandan beri çok fazla kafa karışıklığına neden oldu ..."
NoSenseEtAl

8
bu doğrudur, ancak bu boyuttaki bir diziyi kullanarak da geçirmek mümkündür.void foo(int (*somearray)[20]) sözdizimini . bu durumda 20, arayan sitelerde uygulanır.
v.oddou

14
-1 Bir C programcısı olarak bu yanıtı yanlış buluyorum. []pat'ın cevabında gösterildiği gibi çok boyutlu dizilerde göz ardı edilmez. Yani dizi sözdiziminin dahil edilmesi gerekliydi. Buna ek olarak, hiçbir şey derleyicinin tek boyutlu dizilerde bile uyarı vermesini engellemez.
user694733

7
"[] Öğenizin içeriği" derken, özellikle Soru'daki koddan bahsediyorum. Bu sözdizimi tuhaflığı hiç gerekli değildi, aynı şey işaretçi sözdizimi kullanılarak elde edilebilir, yani eğer bir işaretçi geçilirse parametrenin bir işaretçi tanımlayıcısı olmasını gerektir. Örneğin, pat örneğinde, void foo(int (*args)[20]);Ayrıca, kesinlikle C'nin çok boyutlu dizileri yoktur; ancak elemanları başka diziler olabilen dizilere sahiptir. Bu hiçbir şeyi değiştirmez.
MM

143

İlk boyutun uzunluğu göz ardı edilir, ancak derleyicinin ofsetleri doğru bir şekilde hesaplamasına izin vermek için ek boyutların uzunluğu gereklidir. Aşağıdaki örnekte, fooişleve iki boyutlu bir diziye bir işaretçi aktarılır.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

İlk boyutun boyutu [10]yok sayılır; derleyici sondan itibaren indekslemenizi engellemeyecektir (biçimin 10 öğe istediğini, ancak gerçek öğenin yalnızca 2 sağladığını unutmayın). Ancak ikinci boyutun boyutu[20] , her sıranın adımını belirlemek için kullanılır ve burada, biçimin gerçekle eşleşmesi gerekir. Yine, derleyici ikinci boyutun sonunu indekslemenizi de engellemeyecektir.

Dizinin tabanından bir öğeye olan bayt uzaklığı args[row][col]şu şekilde belirlenir:

sizeof(int)*(col + 20*row)

Unutmayın eğer col >= 20 , o zaman aslında sonraki bir satıra (veya tüm dizinin sonundan) indeksleyeceğinizi .

sizeof(args[0]), İadeler 80 makinemde nereyesizeof(int) == 4 . Ancak, almaya çalışırsam sizeof(args), aşağıdaki derleyici uyarısını alıyorum:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

Burada, derleyici, dizinin kendisinin boyutu yerine yalnızca dizinin bozulduğu göstericinin boyutunu vereceği konusunda uyarıda bulunuyor.


Çok kullanışlı - 1-d durumundaki tuhaflığın nedeni olarak bununla tutarlılık da makul.
jwg

1
1-D vakasıyla aynı fikirdir. C ve C ++ 'da 2 boyutlu bir dizi gibi görünen şey, aslında her bir öğesi başka bir 1 boyutlu dizi olan 1 boyutlu bir dizidir. Bu durumda, her bir elemanı "20 ints dizisi" olan 10 elemanlı bir dizimiz var. Yazımda anlatıldığı gibi, aslında işleve geçirilen şey, öğesinin ilk öğesine göstericidir args. Bu durumda, bağımsız değişkenlerin ilk öğesi "20 inçlik bir dizi" dir. İşaretçiler, tür bilgilerini içerir; iletilen şey "20 inçlik bir diziye işaretçi" dir.
MM

9
Evet, int (*)[20]türü bu; "20 inçlik bir diziye işaretçi".
pat

33

Sorun ve C ++ 'da nasıl aşılacağı

Sorun kapsamlı bir şekilde pat ve Matt tarafından açıklandı . Derleyici temelde dizinin boyutunun ilk boyutunu yok sayar ve aktarılan bağımsız değişkenin boyutunu etkin bir şekilde yok sayar.

C ++ 'da ise, bu sınırlamayı iki şekilde kolayca aşabilirsiniz:

  • referanslar kullanarak
  • kullanarak std::array(C ++ 11'den beri)

Referanslar

İşleviniz yalnızca mevcut bir diziyi okumaya veya değiştirmeye çalışıyorsa (kopyalamıyorsa), referansları kolayca kullanabilirsiniz.

Örneğin, inther öğeyi ayarlayan on s'lik bir diziyi sıfırlayan bir işleve sahip olmak istediğinizi varsayalım 0. Aşağıdaki işlev imzasını kullanarak bunu kolayca yapabilirsiniz:

void reset(int (&array)[10]) { ... }

Sadece bu iyi çalışmayacak , aynı zamanda dizinin boyutunu da zorlayacaktır. .

Ayrıca yararlanabilirler şablonları Yukarıdaki kod yapmak jenerik :

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

Ve nihayet constdoğruluktan yararlanabilirsiniz . 10 öğelik bir dizi yazdıran bir işlevi ele alalım:

void show(const int (&array)[10]) { ... }

Uygulayarak constön eleme biz edilir olası değişiklikler önlenmesi .


Diziler için standart kitaplık sınıfı

Yukarıdaki sözdiziminin hem çirkin hem de gereksiz olduğunu düşünürseniz, benim yaptığım gibi, onu kutuya atıp kullanabiliriz std::array (C ++ 11'den beri).

İşte yeniden düzenlenen kod:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

Harika değil mi? Size daha önce öğrettiğim genel kod numarasının hala işe yaradığından bahsetmiyorum bile :

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Sadece bu değil, aynı zamanda ücretsiz olarak anlamsal kopyalama ve taşıma elde edersiniz. :)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Peki ne bekliyorsunuz? Git kullan std::array.


2
@kietz, önerilen düzenlemeniz reddedildiği için üzgünüm, ancak aksi belirtilmedikçe otomatik olarak C ++ 11 kullanıldığını varsayıyoruz .
Ayakkabı

bu doğrudur, ancak verdiğiniz bağlantıya göre herhangi bir çözümün yalnızca C ++ 11 olup olmadığını da belirtmemiz gerekiyor.
trlkly

@trlkly, katılıyorum. Cevabı buna göre düzenledim. Gösterdiğiniz için teşekkürler.
Ayakkabı

9

C'nin eğlenceli bir özelliği, eğer çok eğilimliyseniz, kendinizi ayağınıza etkili bir şekilde vurmanızı sağlar.

Bence sebebi, C'nin assembly dilinin sadece bir adım üzerinde olması. En yüksek performansı sağlamak için boyut kontrolü ve benzeri güvenlik özellikleri kaldırıldı, bu da programcı çok çalışkan ise kötü bir şey değildir.

Ayrıca, işlev bağımsız değişkenine bir boyut atamanın avantajı, işlev başka bir programcı tarafından kullanıldığında, bir boyut kısıtlamasını fark etme olasılıkları vardır. Sadece bir işaretçi kullanmak, bu bilgiyi bir sonraki programlayıcıya aktarmaz.


3
Evet. C, programcıya derleyici üzerinde güvenmek için tasarlanmıştır. Bir dizinin sonunu bu kadar açık bir şekilde indeksliyorsanız, özel ve kasıtlı bir şey yapıyor olmalısınız.
John

7
14 yıl önce C programlamada dişlerimi kestim. Profesörümün söylediği her şey arasında, bana diğerlerinden daha çok takılan tek cümle, "C programcılar tarafından programcılar için yazılmıştır." Dil son derece güçlü. (Klişe için hazırlanın) Ben amcanın öğrettiği gibi, "Büyük güç, büyük sorumluluk getirir."
Andrew Falanga

7

İlk olarak, C asla dizi sınırlarını kontrol etmez. Yerel, global, statik, parametreler, ne olursa olsun farketmez. Dizi sınırlarının kontrol edilmesi daha fazla işlem anlamına gelir ve C'nin çok verimli olması beklenir, bu nedenle dizi sınırları kontrolü gerektiğinde programcı tarafından yapılır.

İkincisi, bir diziyi bir işleve değerle aktarmayı mümkün kılan bir numara vardır. Bir işlevden bir dizinin değerine göre döndürülmesi de mümkündür. Sadece struct kullanarak yeni bir veri türü oluşturmanız gerekir. Örneğin:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Bunun gibi öğelere erişmelisiniz: foo.a [1]. Fazladan ".a" tuhaf görünebilir, ancak bu numara C diline büyük işlevsellik katar.


7
Derleme zamanı tür denetimi ile çalışma zamanı sınırlarını kontrol etmeyi karıştırıyorsunuz.
Ben Voigt

@Ben Voigt: Orijinal soruda olduğu gibi sadece sınır kontrolünden bahsediyorum.
user34814

2
@ user34814 derleme zamanı sınırları denetimi, tür denetimi kapsamındadır. Birkaç üst düzey dil bu özelliği sunar.
Leushenko

5

Derleyiciye myArray'in en az 10 inçlik bir diziyi gösterdiğini söylemek için:

void bar(int myArray[static 10])

İyi bir derleyici, myArray [10] 'a erişirseniz size bir uyarı vermelidir. "Statik" anahtar kelime olmadan, 10 hiçbir şey ifade etmez.


1
11. öğeye erişirseniz ve dizi en az 10 öğe içeriyorsa bir derleyici neden uyarmalı ?
nwellnhof

Muhtemelen bunun nedeni, derleyicinin yalnızca en az 10 öğeye sahip olduğunuzu uygulayabilmesidir . 11. öğeye erişmeye çalışırsanız, var olduğundan emin olamaz (öyle olsa bile).
Dylan Watson

2
Bunun standardın doğru bir okuması olduğunu sanmıyorum. ile ararsanız[static] derleyicinin uyarmasına izin verir . Bu Erişebileceğiniz neyi dikte etmez içinde . Sorum tamamen arayan tarafındadır. barint[5] bar
sekme

3
error: expected primary-expression before 'static'bu sözdizimini hiç görmedim. bunun standart C veya C ++ olması olası değildir.
v.oddou

3
@ v.oddou, 6.7.5.2 ve 6.7.5.3'te C99'da belirtilmiştir.
Samuel Edwin Ward

5

Bu, C ++ 'nın C kodunu doğru bir şekilde derlemesi beklendiği için C ++' ya geçirilen, C'nin iyi bilinen bir "özelliğidir".

Sorun birkaç yönden ortaya çıkar:

  1. Bir dizi adının bir göstericiye tamamen eşdeğer olduğu varsayılır.
  2. C'nin hızlı olması gerekiyordu, başlangıçta bir tür "üst düzey birleştirici" olarak geliştirildi (özellikle ilk "taşınabilir İşletim Sistemi" olan Unix'i yazmak için tasarlandı), bu nedenle değil eklemek gerekiyordu kodu "gizli"; çalışma zamanı aralığı denetimi bu nedenle "yasaktır".
  3. Statik bir diziye veya dinamik diziye (yığın içinde veya ayrılmış) erişmek için oluşturulan makine kodu aslında farklıdır.
  4. Çağrılan işlev, argüman olarak iletilen dizinin "türünü" bilemeyeceğinden, her şeyin bir gösterici olduğu ve bu şekilde işlem gördüğü varsayılır.

Dizilerin C'de gerçekten desteklenmediğini söyleyebilirsin (daha önce söylediğim gibi bu gerçekten doğru değil, ama iyi bir yaklaşımdır); bir dizi gerçekten bir veri bloğuna işaretçi olarak kabul edilir ve işaretçi aritmetiği kullanılarak erişilir. C herhangi bir RTTI biçimine sahip olmadığından, işlev prototipindeki dizi öğesinin boyutunu bildirmeniz gerekir (işaretçi aritmetiğini desteklemek için). Bu, çok boyutlu diziler için daha da "daha doğrudur".

Her neyse, yukarıdakilerin hepsi artık gerçekten doğru değil: p

Çoğu modern C / C ++ derleyiciler yapmak destek sınırları denetleme ama standartları (geriye dönük uyumluluk için) varsayılan olarak kapalı olması gerekir. Örneğin, gcc'nin oldukça yeni sürümleri, "-O3 -Wall -Wextra" ile derleme zamanı aralığı denetimi ve "-fbounds-denetimi" ile tam çalışma zamanı sınırları denetimi yapar.


Belki C ++ edildi 20 yıl önce C kodu derlemek gerekiyordu, ama kesinlikle bir değil, ve (herhangi bir yeni C ++ standart tarafından "sabit" henüz en az C ++ 98? C99) uzun süre değil sahiptir.
hyde

@hyde Bu bana biraz fazla sert geliyor. Stroustrup'tan alıntı yapacak olursak, "Küçük istisnalar dışında, C, C ++ 'nın bir alt kümesidir." (C ++ PL 4. baskı, sn. 1.2.1). Hem C ++ hem de C daha da gelişirken ve en son C sürümünün en son C ++ sürümünde olmayan özellikler mevcut olsa da, genel olarak Stroustrup teklifinin hala geçerli olduğunu düşünüyorum.
mvw

@mvw Bu binyılda yazılan ve uyumsuz özelliklerden kaçınarak kasıtlı olarak C ++ uyumlu tutulmayan çoğu C kodu, yapıları başlatmak için C99 tarafından belirlenmiş başlatıcı sözdizimini ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) kullanacaktır , çünkü bir yapıyı başlatmanın çok daha açık bir yoludur. Sonuç olarak, mevcut C kodlarının çoğu standart C ++ derleyicileri tarafından reddedilecektir, çünkü çoğu C kodu yapıları başlatacaktır.
hyde

@mvw C ++ 'nın C ile uyumlu olması gerektiği söylenebilir, bu nedenle bazı tavizler verilirse hem C hem de C ++ derleyicileriyle derlenecek kod yazılabilir. Ancak bu , yalnızca C ++ alt kümesini değil , hem C hem de C ++ alt kümesini kullanmayı gerektirir .
hyde

@hyde C kodunun ne kadarının C ++ derlenebilir olduğuna şaşıracaksınız. Birkaç yıl önce tüm Linux çekirdeği C ++ derlenebilirdi (hala doğru olup olmadığını bilmiyorum). Üstün bir uyarı kontrolü elde etmek için C ++ derleyicisinde rutin olarak C kodunu derliyorum, yalnızca "üretim" en fazla optimizasyonu sıkıştırmak için C modunda derleniyor.
ZioByte

3

C, sadece tür bir parametre dönüşümü olmaz int[5]içine *int; bildirilmedikçe typedef int intArray5[5];, bu tür bir parametre dönüştürecek intArray5için *intde. Bu davranışın tuhaf olmasına rağmen yararlı olduğu bazı durumlar vardır (özellikle içinde va_listtanımlanmış gibi stdargs.hbazı uygulamaların bir dizi olarak tanımladığı şeylerde ). Bir parametre olarak int[5](boyutu yok sayarak) olarak tanımlanan bir türe izin vermek int[5], ancak doğrudan belirtilmesine izin vermemek mantıksız olacaktır .

C'nin dizi türündeki parametreleri ele almasını saçma buluyorum, ancak bu, büyük bir kısmı özellikle iyi tanımlanmamış veya düşünülmemiş geçici bir dil alma çabalarının bir sonucudur ve davranışsal Mevcut uygulamaların mevcut programlar için yaptıklarıyla tutarlı olan özellikler. C'nin tuhaflıklarının çoğu, bu açıdan bakıldığında mantıklı geliyor, özellikle de çoğu icat edildiğinde, bugün bildiğimiz dilin büyük bölümlerinin henüz var olmadığı düşünülürse. Anladığım kadarıyla, BCPL denen C'den önceki sürümde, derleyiciler değişken türlerini pek iyi takip etmediler. Bir bildirim int arr[5];, bir işaretçi veya diziye eşdeğerdi . Ya veya olarak erişildiğindeint anonymousAllocation[5],*arr = anonymousAllocation; ; tahsis bir kenara bırakıldığında. derleyici ne biliyordu ne de önemsiyorduarrarr[x]*arr, nasıl ilan edildiğine bakılmaksızın bir işaretçi olarak kabul edilecektir.


1

Henüz cevaplanmamış bir şey asıl sorudur.

Daha önce verilen cevaplar, dizilerin C veya C ++ 'daki bir işleve değer olarak aktarılamayacağını açıklar. Ayrıca int[], olarak bildirilen bir parametrenin , türü varmış gibi ele alındığını int *ve int[]böyle bir işleve türden bir değişkenin geçirilebileceğini açıklarlar .

Ancak, bir dizi uzunluğunu açıkça belirtmek için neden hiçbir zaman hata yapılmadığını açıklamıyorlar.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Bunların sonuncusu neden bir hata değil?

Bunun bir nedeni, yazı tiplerinde sorunlara neden olmasıdır.

typedef int myarray[10];
void f(myarray array);

Dizi uzunluğunu işlev parametrelerinde belirtmek bir hata olsaydı myarray, işlev parametresindeki adı kullanamazsınız . Ve bazı uygulamalar gibi standart kitaplık türleri için dizi türleri kullandığından va_listve tüm uygulamalar jmp_bufbir dizi türü oluşturmak için gerekli olduğundan , bu adları kullanarak işlev parametrelerini bildirmenin standart bir yolu olmasaydı çok sorunlu olurdu: bu yetenek olmadan, gibi işlevlerin taşınabilir bir uygulaması değildir vprintf.


0

Derleyicilerin, aktarılan dizinin boyutunun beklenenle aynı olup olmadığını kontrol etmesine izin verilir. Derleyiciler, durum böyle değilse bir sorunu uyarabilir.

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.