Mengapa kompiler C dan C ++ memungkinkan panjang array dalam tanda tangan fungsi ketika mereka tidak pernah ditegakkan?

131

Inilah yang saya temukan selama masa belajar saya:

#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;
}  

Jadi dalam variabel int dis(char a[1]), [1]sepertinya tidak melakukan apa-apa dan tidak berfungsi sama
sekali, karena saya dapat menggunakan a[2]. Hanya suka int a[]atau char *a. Saya tahu nama array adalah pointer dan cara menyampaikan array, jadi teka-teki saya bukan tentang bagian ini.

Yang ingin saya ketahui adalah mengapa kompiler mengizinkan perilaku ini ( int a[1]). Atau apakah ada makna lain yang tidak saya ketahui?

Fanl
sumber
6
Itu karena Anda tidak dapat benar-benar melewati array ke fungsi.
Ed S.
37
Saya pikir pertanyaan di sini adalah mengapa C memungkinkan Anda untuk mendeklarasikan parameter menjadi tipe array saat itu hanya akan berperilaku persis seperti pointer.
Brian
8
@ Brian: Saya tidak yakin apakah ini argumen untuk atau melawan perilaku, tetapi juga berlaku jika tipe argumen adalah typedeftipe array. Jadi "pembusukan untuk pointer" di jenis argumen bukan hanya gula sintaksis mengganti []dengan *, itu benar-benar akan melalui sistem jenis. Ini memiliki konsekuensi dunia nyata untuk beberapa tipe standar seperti va_listyang dapat didefinisikan dengan tipe array atau non-array.
R .. GitHub BERHENTI MEMBANTU ICE
4
@songyuanyao Anda dapat mencapai sesuatu yang tidak sepenuhnya berbeda dalam C (dan C ++) menggunakan pointer: int dis(char (*a)[1]). Kemudian, Anda melewati pointer ke array: dis(&b). Jika Anda bersedia menggunakan fitur C yang tidak ada di C ++, Anda juga bisa mengatakan hal-hal seperti void foo(int data[static 256])dan int bar(double matrix[*][*]), tetapi itu adalah kaleng cacing lainnya.
Stuart Olsen
1
@ StuartOlsen Intinya bukan standar yang mendefinisikan apa. Intinya adalah mengapa siapa pun yang mendefinisikannya mendefinisikannya seperti itu.
user253751

Jawaban:

156

Ini adalah kekhasan dari sintaks untuk meneruskan array ke fungsi.

Sebenarnya tidak mungkin untuk melewatkan array di C. Jika Anda menulis sintaks yang terlihat seperti harus melewati array, yang sebenarnya terjadi adalah pointer ke elemen pertama array dilewatkan.

Karena pointer tidak menyertakan informasi panjang, konten Anda []dalam daftar parameter formal fungsi sebenarnya diabaikan.

Keputusan untuk mengizinkan sintaks ini dibuat pada tahun 1970-an dan telah menyebabkan banyak kebingungan sejak ...

MM
sumber
21
Sebagai seorang programmer non-C, saya menemukan jawaban ini sangat mudah diakses. +1
asteri
21
+1 untuk "Keputusan untuk mengizinkan sintaks ini dibuat pada tahun 1970-an dan telah menyebabkan banyak kebingungan sejak ..."
NoSenseEtAl
8
ini benar tetapi juga dimungkinkan untuk melewatkan array dengan ukuran hanya menggunakan void foo(int (*somearray)[20])sintaks. dalam hal ini 20 diberlakukan di situs penelepon.
v.oddou
14
-1 Sebagai seorang programmer C, saya menemukan jawaban ini salah. []tidak diabaikan dalam array multidimensi seperti yang ditunjukkan pada jawaban pat. Jadi termasuk sintaks array diperlukan. Selain itu, tidak ada yang menghentikan kompiler mengeluarkan peringatan bahkan pada array satu dimensi.
user694733
7
Dengan "konten [] Anda", saya berbicara secara spesifik tentang kode dalam Pertanyaan. Sintaks sintaks ini sama sekali tidak perlu, hal yang sama dapat dicapai dengan menggunakan sintaks pointer, yaitu jika pointer dilewatkan maka memerlukan parameter untuk menjadi pointer deklarator. Misalnya dalam contoh tepuk, void foo(int (*args)[20]);Juga, secara tegas C tidak memiliki array multi-dimensi; tetapi memiliki array yang elemen-elemennya dapat berupa array lain. Ini tidak mengubah apa pun.
MM
143

Panjang dimensi pertama diabaikan, tetapi panjang dimensi tambahan diperlukan untuk memungkinkan kompilator menghitung offset dengan benar. Dalam contoh berikut, foofungsi dilewatkan pointer ke array dua dimensi.

#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;
}

Ukuran dimensi pertama [10]diabaikan; kompiler tidak akan mencegah Anda mengindeks akhir (perhatikan bahwa formal menginginkan 10 elemen, tetapi sebenarnya hanya menyediakan 2). Namun, ukuran dimensi kedua [20]digunakan untuk menentukan langkah setiap baris, dan di sini, formal harus cocok dengan yang sebenarnya. Sekali lagi, kompiler tidak akan mencegah Anda mengindeks dari akhir dimensi kedua.

Byte offset dari basis array ke elemen args[row][col]ditentukan oleh:

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

Perhatikan bahwa jika col >= 20, maka Anda benar-benar akan mengindeks ke baris berikutnya (atau dari akhir seluruh array).

sizeof(args[0]), mengembalikan 80mesin saya di mana sizeof(int) == 4. Namun, jika saya mencoba untuk mengambilsizeof(args) , saya mendapatkan peringatan kompiler berikut:

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.

Di sini, kompiler memperingatkan bahwa ia hanya akan memberikan ukuran pointer ke mana array telah membusuk, bukan ukuran array itu sendiri.

menepuk
sumber
Sangat berguna - konsistensi dengan ini juga masuk akal sebagai alasan untuk kekhasan dalam kasus 1-d.
jwg
1
Ini adalah ide yang sama dengan kasus 1-D. Apa yang tampak seperti array 2-D di C dan C ++ sebenarnya adalah array 1-D, yang masing-masing elemennya adalah array 1-D lainnya. Dalam hal ini kita memiliki sebuah array dengan 10 elemen, yang masing-masing elemennya adalah "array of 20 ints". Seperti yang dijelaskan dalam posting saya, apa yang sebenarnya diteruskan ke fungsi adalah pointer ke elemen pertama args. Dalam hal ini, elemen pertama dari args adalah "array of 20 ints". Pointer termasuk informasi jenis; apa yang dilewatkan adalah "pointer ke array 20 int".
MM
9
Yup, begitulah int (*)[20]tipenya; + msgstr "arahkan ke array 20 int".
pat
33

Masalahnya dan bagaimana cara mengatasinya di C ++

Masalahnya telah dijelaskan secara luas oleh Pat dan Matt . Compiler pada dasarnya mengabaikan dimensi pertama ukuran array secara efektif mengabaikan ukuran argumen yang diteruskan.

Di C ++, di sisi lain, Anda dapat dengan mudah mengatasi batasan ini dengan dua cara:

  • menggunakan referensi
  • menggunakan std::array(sejak C ++ 11)

Referensi

Jika fungsi Anda hanya mencoba membaca atau memodifikasi array yang ada (tidak menyalinnya), Anda dapat dengan mudah menggunakan referensi.

Misalnya, mari kita asumsikan Anda ingin memiliki fungsi yang me-reset array dari sepuluh intpengaturan setiap elemen 0. Anda dapat dengan mudah melakukannya dengan menggunakan tanda tangan fungsi berikut:

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

Tidak hanya ini akan berfungsi dengan baik , tetapi juga akan memperkuat dimensi array .

Anda juga dapat menggunakan templat untuk membuat kode di atas umum :

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

Dan akhirnya Anda bisa memanfaatkan constkebenaran. Mari kita pertimbangkan fungsi yang mencetak array 10 elemen:

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

Dengan menerapkan constkualifikasi kami mencegah kemungkinan modifikasi .


Kelas perpustakaan standar untuk array

Jika Anda menganggap sintaks di atas jelek dan tidak perlu, seperti yang saya lakukan, kita bisa melemparkannya ke dalam kaleng dan menggunakannya std::arraysebagai gantinya (karena C ++ 11).

Berikut kode yang telah di-refactored:

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

Luar biasa bukan? Belum lagi bahwa trik kode generik yang saya ajarkan sebelumnya, masih berfungsi:

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) { ... }

Bukan hanya itu, tetapi Anda mendapatkan salinan dan memindahkan semantik secara gratis. :)

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
}

Jadi tunggu apa lagi? Pergi gunakan std::array.

Sepatu
sumber
2
@kietz, maaf edit yang Anda sarankan ditolak, tetapi kami menganggap secara otomatis C ++ 11 sedang digunakan , kecuali dinyatakan sebaliknya.
Sepatu
ini benar, tetapi kami juga harus menentukan apakah solusi apa pun hanya C ++ 11, berdasarkan tautan yang Anda berikan.
trlkly
@ CtrlKly, saya setuju. Saya telah mengedit jawabannya. Terima kasih telah menunjukkannya.
Sepatu
9

Ini adalah fitur menyenangkan dari C yang memungkinkan Anda untuk secara efektif menembak diri sendiri jika Anda ingin.

Saya pikir alasannya adalah bahwa C hanya satu langkah di atas bahasa assembly. Pengecekan ukuran dan fitur keselamatan serupa telah dihapus untuk memungkinkan kinerja puncak, yang bukan merupakan hal yang buruk jika programmer sedang sangat rajin.

Juga, menetapkan ukuran ke argumen fungsi memiliki keuntungan bahwa ketika fungsi tersebut digunakan oleh programmer lain, ada kemungkinan mereka akan melihat batasan ukuran. Hanya menggunakan pointer tidak menyampaikan informasi itu ke programmer berikutnya.

tagihan
sumber
3
Iya. C dirancang untuk mempercayai pemrogram atas kompiler. Jika Anda dengan sangat terang mengindeks akhir array, Anda harus melakukan sesuatu yang khusus dan disengaja.
John
7
Saya memotong gigi saya dalam pemrograman pada C 14 tahun yang lalu. Dari semua profesor saya berkata, satu kalimat yang lebih cocok dengan saya daripada semua yang lain, "C ditulis oleh programmer, untuk programmer." Bahasanya sangat kuat. (Bersiap untuk klise) Seperti yang diajarkan oleh Ben, "Dengan kekuatan besar, datang tanggung jawab besar."
Andrew Falanga
6

Pertama, C tidak pernah memeriksa batas array. Tidak masalah jika parameternya lokal, global, statis, apa pun. Memeriksa batas array berarti lebih banyak pemrosesan, dan C seharusnya sangat efisien, sehingga pemeriksaan batas array dilakukan oleh programmer ketika dibutuhkan.

Kedua, ada trik yang memungkinkan untuk melewatkan nilai array ke suatu fungsi. Dimungkinkan juga untuk mengembalikan array nilai dari suatu fungsi. Anda hanya perlu membuat tipe data baru menggunakan struct. Sebagai contoh:

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

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Anda harus mengakses elemen-elemen seperti ini: foo.a [1]. Ekstra ".a" mungkin terlihat aneh, tetapi trik ini menambahkan fungsionalitas hebat ke bahasa C.

pengguna34814
sumber
7
Anda membingungkan batas runtime yang memeriksa dengan pemeriksaan tipe waktu kompilasi.
Ben Voigt
@ Ben Voigt: Saya hanya berbicara tentang batas memeriksa, seperti pertanyaan aslinya.
user34814
2
@ user34814 memeriksa batas waktu kompilasi berada dalam lingkup pemeriksaan tipe. Beberapa bahasa tingkat tinggi menawarkan fitur ini.
Leushenko
5

Untuk memberi tahu kompiler bahwa myArray menunjuk ke array setidaknya 10 int:

void bar(int myArray[static 10])

Kompiler yang baik harus memberi Anda peringatan jika Anda mengakses myArray [10]. Tanpa kata kunci "statis", 10 tidak akan berarti apa-apa.

gnasher729
sumber
1
Mengapa kompiler harus memperingatkan jika Anda mengakses elemen ke-11 dan array mengandung setidaknya 10 elemen?
nwellnhof
Agaknya ini karena kompiler hanya dapat menegakkan bahwa Anda memiliki setidaknya 10 elemen. Jika Anda mencoba mengakses elemen ke-11, itu tidak dapat memastikan bahwa itu ada (meskipun mungkin).
Dylan Watson
2
Saya tidak berpikir itu pembacaan yang benar dari standar. [static]memungkinkan kompiler untuk memperingatkan jika Anda memanggil bar dengan int[5]. Itu tidak menentukan apa yang dapat Anda akses di dalam bar . Tanggung jawab sepenuhnya berada di sisi penelepon.
tab
3
error: expected primary-expression before 'static'tidak pernah melihat sintaks ini. ini tidak mungkin standar C atau C ++.
v.oddou
3
@ v.oddou, ditentukan dalam C99, dalam 6.7.5.2 dan 6.7.5.3.
Samuel Edwin Ward
5

Ini adalah "fitur" terkenal dari C, diteruskan ke C ++ karena C ++ seharusnya mengkompilasi kode C dengan benar.

Masalah muncul dari beberapa aspek:

  1. Nama array seharusnya sama dengan pointer.
  2. C seharusnya cepat, awalnya dikembangkan untuk menjadi semacam "Assembler tingkat tinggi" (terutama dirancang untuk menulis "Sistem Operasi portabel" pertama: Unix), sehingga tidak seharusnya memasukkan kode "tersembunyi"; Pengecekan rentang runtime dengan demikian "terlarang".
  3. Kode mesin yang dibuat untuk mengakses array statis atau dinamis (baik di stack atau dialokasikan) sebenarnya berbeda.
  4. Karena fungsi yang dipanggil tidak dapat mengetahui "jenis" array yang dilewatkan sebagai argumen, semuanya dianggap sebagai pointer dan diperlakukan seperti itu.

Bisa dikatakan array tidak benar-benar didukung dalam C (ini tidak benar, seperti yang saya katakan sebelumnya, tetapi ini adalah perkiraan yang baik); sebuah array benar-benar diperlakukan sebagai pointer ke blok data dan diakses menggunakan pointer aritmatika. Karena C TIDAK memiliki bentuk RTTI, Anda harus mendeklarasikan ukuran elemen array dalam prototipe fungsi (untuk mendukung aritmatika pointer). Ini bahkan "lebih benar" untuk array multidimensi.

Pokoknya semua di atas tidak benar lagi: p

Paling modern C / C ++ compiler melakukan batas dukungan memeriksa, tapi standar membutuhkan untuk menjadi off secara default (untuk kompatibilitas). Versi terbaru gcc, misalnya, melakukan pengecekan rentang waktu-kompilasi dengan "-O3 -Wall -Wextra" dan batas waktu run penuh memeriksa dengan "-fbounds-checking".

ZioByte
sumber
Mungkin C ++ itu seharusnya untuk mengkompilasi kode C 20 tahun yang lalu, tapi jelas adalah tidak, dan belum untuk waktu yang lama (C ++ 98? C99 setidaknya, yang belum "tetap" oleh C lebih baru ++ standar).
hyde
@ Hyde Kedengarannya agak terlalu keras bagi saya. Mengutip Stroustrup "Dengan pengecualian kecil, C adalah bagian dari C ++." (C ++ PL edisi ke-4, bagian 1.2.1). Sementara C ++ dan C berkembang lebih jauh, dan fitur dari versi C terbaru ada yang tidak ada dalam versi C ++ terbaru, secara keseluruhan saya berpikir bahwa kutipan Stroustrup masih valid.
mvw
@ mvw Sebagian besar kode C yang ditulis dalam milenium ini, yang tidak sengaja dibuat agar kompatibel dengan C ++ dengan menghindari fitur-fitur yang tidak kompatibel, akan menggunakan sintaks initializers yang ditunjuk C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) untuk menginisialisasi struct, karena ini merupakan cara yang jauh lebih jelas untuk menginisialisasi struct. Akibatnya, sebagian besar kode C saat ini akan ditolak oleh kompiler C ++ standar, karena sebagian besar kode C akan menginisialisasi struct.
hyde
@mvw Bisa dikatakan, bahwa C ++ seharusnya kompatibel dengan C, sehingga dimungkinkan untuk menulis kode yang akan dikompilasi dengan kompiler C dan C ++, jika ada kompromi tertentu. Tapi itu membutuhkan menggunakan subset dari kedua C dan C ++, bukan hanya subset dari C ++.
hyde
@hyde Anda akan terkejut betapa banyak kode C yang dapat dikompilasi C ++. Beberapa tahun yang lalu seluruh kernel Linux dapat dikompilasi dengan C ++ (saya tidak tahu apakah masih benar). Saya secara rutin mengkompilasi kode C di kompiler C ++ untuk mendapatkan pemeriksaan peringatan yang superior, hanya "produksi" yang dikompilasi dalam mode C untuk memeras optimasi yang paling.
ZioByte
3

C tidak hanya akan mengubah parameter tipe int[5]menjadi *int; diberikan deklarasi typedef int intArray5[5];, itu akan mengubah parameter tipe intArray5untuk *intjuga. Ada beberapa situasi di mana perilaku ini, meskipun aneh, berguna (terutama dengan hal-hal seperti yang va_listdidefinisikan dalam stdargs.h, yang didefinisikan oleh beberapa implementasi sebagai array). Akan tidak masuk akal untuk mengizinkan sebagai parameter tipe yang didefinisikan int[5](mengabaikan dimensi) tetapi tidak memungkinkan int[5]untuk ditentukan secara langsung.

Saya menemukan penanganan parameter parameter C menjadi tidak masuk akal, tetapi ini merupakan konsekuensi dari upaya untuk mengambil bahasa ad-hoc, sebagian besar yang tidak terlalu terdefinisi dengan baik atau dipikirkan, dan mencoba untuk datang dengan perilaku spesifikasi yang konsisten dengan apa yang dilakukan oleh implementasi yang ada untuk program yang ada. Banyak keanehan C masuk akal bila dilihat dalam cahaya itu, terutama jika orang menganggap bahwa ketika banyak dari mereka ditemukan, sebagian besar bahasa yang kita kenal sekarang belum ada. Dari apa yang saya mengerti, di pendahulunya ke C, disebut BCPL, kompiler tidak benar-benar melacak tipe variabel dengan sangat baik. Deklarasi int arr[5];setara dengan int anonymousAllocation[5],*arr = anonymousAllocation;; setelah alokasi disisihkan. kompiler tidak tahu atau tidak peduli apakaharradalah pointer atau array. Ketika diakses sebagai salah satu , itu akan dianggap sebagai penunjuk terlepas dari bagaimana itu dinyatakan.arr[x] atau*arr

supercat
sumber
1

Satu hal yang belum dijawab adalah pertanyaan sebenarnya.

Jawaban yang sudah diberikan menjelaskan bahwa array tidak dapat diteruskan oleh nilai ke fungsi dalam C atau C ++. Mereka juga menjelaskan bahwa suatu parameter yang dinyatakan sebagai int[]diperlakukan seolah-olah memiliki tipe int *, dan bahwa variabel tipe int[]dapat diteruskan ke fungsi tersebut.

Tetapi mereka tidak menjelaskan mengapa tidak pernah dibuat kesalahan untuk secara eksplisit memberikan panjang array.

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

Mengapa kesalahan terakhir ini tidak terjadi?

Alasan untuk itu adalah bahwa hal itu menyebabkan masalah dengan typedef.

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

Jika kesalahan menentukan panjang array dalam parameter fungsi, Anda tidak akan dapat menggunakan myarraynama dalam parameter fungsi. Dan karena beberapa implementasi menggunakan tipe array untuk tipe pustaka standar seperti va_list, dan semua implementasi diperlukan untuk membuat jmp_buftipe array, akan sangat bermasalah jika tidak ada cara standar untuk mendeklarasikan parameter fungsi menggunakan nama-nama tersebut: tanpa kemampuan itu, bisa ada bukan menjadi implementasi portabel fungsi seperti vprintf.


sumber
0

Memungkinkan kompiler dapat memeriksa apakah ukuran array yang dilewati sama dengan yang diharapkan. Compiler dapat memperingatkan masalah jika bukan itu masalahnya.

Hamidi
sumber