C pointer: menunjuk ke array dengan ukuran tetap


120

Pertanyaan ini ditujukan kepada guru C di luar sana:

Di C, dimungkinkan untuk mendeklarasikan pointer sebagai berikut:

char (* p)[10];

.. yang pada dasarnya menyatakan bahwa penunjuk ini menunjuk ke larik 10 karakter. Hal yang rapi tentang mendeklarasikan pointer seperti ini adalah Anda akan mendapatkan kesalahan waktu kompilasi jika Anda mencoba menetapkan pointer dari array dengan ukuran berbeda ke p. Ini juga akan memberi Anda kesalahan waktu kompilasi jika Anda mencoba menetapkan nilai pointer char sederhana ke p. Saya mencoba ini dengan gcc dan tampaknya berfungsi dengan ANSI, C89 dan C99.

Menurut saya, mendeklarasikan pointer seperti ini akan sangat berguna - terutama, saat meneruskan pointer ke suatu fungsi. Biasanya, orang akan menulis prototipe dari fungsi seperti ini:

void foo(char * p, int plen);

Jika Anda mengharapkan buffer dengan ukuran tertentu, Anda cukup menguji nilai plen. Namun, Anda tidak dapat dijamin bahwa orang yang meneruskan p kepada Anda akan benar-benar memberi Anda lokasi memori yang valid di buffer itu. Anda harus percaya bahwa orang yang memanggil fungsi ini melakukan hal yang benar. Di samping itu:

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

..akan memaksa pemanggil untuk memberi Anda buffer dengan ukuran yang ditentukan.

Ini tampaknya sangat berguna tetapi saya belum pernah melihat penunjuk yang dinyatakan seperti ini dalam kode apa pun yang pernah saya temui.

Pertanyaan saya adalah: Apakah ada alasan mengapa orang tidak menyatakan petunjuk seperti ini? Apakah saya tidak melihat beberapa jebakan yang jelas?


3
Catatan: Karena C99, array tidak harus berukuran tetap seperti yang disarankan oleh judul, 10dapat diganti dengan variabel apa pun dalam cakupan
MM

Jawaban:


174

Apa yang Anda katakan di postingan Anda benar sekali. Saya akan mengatakan bahwa setiap pengembang C sampai pada penemuan yang persis sama dan pada kesimpulan yang persis sama ketika (jika) mereka mencapai tingkat kemahiran tertentu dengan bahasa C.

Ketika spesifikasi area aplikasi Anda memanggil larik dengan ukuran tetap tertentu (ukuran larik adalah konstanta waktu kompilasi), satu-satunya cara yang tepat untuk meneruskan larik tersebut ke suatu fungsi adalah dengan menggunakan parameter penunjuk-ke-larik

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

(dalam bahasa C ++ ini juga dilakukan dengan referensi

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

).

Ini akan mengaktifkan pemeriksaan tipe tingkat bahasa, yang akan memastikan bahwa larik dengan ukuran yang benar-benar tepat disediakan sebagai argumen. Faktanya, dalam banyak kasus orang menggunakan teknik ini secara implisit, bahkan tanpa menyadarinya, menyembunyikan tipe array di belakang nama typedef.

typedef int Vector3d[3];

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

Perhatikan juga bahwa kode di atas adalah invariant dengan relasi ke Vector3dtipe menjadi array atau a struct. Anda dapat mengganti definisi Vector3dkapan saja dari array ke a structdan kembali, dan Anda tidak perlu mengubah deklarasi fungsi. Dalam kedua kasus, fungsi akan menerima objek gabungan "dengan referensi" (ada pengecualian untuk ini, tetapi dalam konteks diskusi ini hal ini benar).

Namun, Anda tidak akan melihat metode pengoperan array ini terlalu sering digunakan secara eksplisit, hanya karena terlalu banyak orang yang bingung dengan sintaks yang agak berbelit-belit dan tidak cukup nyaman dengan fitur bahasa C seperti itu untuk menggunakannya dengan benar. Karena alasan ini, dalam kehidupan nyata rata-rata, meneruskan array sebagai penunjuk ke elemen pertamanya adalah pendekatan yang lebih populer. Itu hanya terlihat "lebih sederhana".

Tetapi pada kenyataannya, menggunakan pointer ke elemen pertama untuk pengoperan array adalah teknik yang sangat khusus, sebuah trik, yang melayani tujuan yang sangat spesifik: satu-satunya tujuannya adalah untuk memfasilitasi lintasan yang lewat dengan ukuran yang berbeda (yaitu ukuran run-time) . Jika Anda benar-benar harus dapat memproses array ukuran run-time, maka cara yang tepat untuk meneruskan array tersebut adalah dengan pointer ke elemen pertamanya dengan ukuran konkret yang disediakan oleh parameter tambahan.

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

Sebenarnya, dalam banyak kasus, sangat berguna untuk dapat memproses array ukuran run-time, yang juga berkontribusi pada popularitas metode. Banyak pengembang C tidak pernah menemukan (atau tidak pernah mengenali) kebutuhan untuk memproses array ukuran tetap, sehingga tetap tidak menyadari teknik ukuran tetap yang tepat.

Namun demikian, jika ukuran array tetap, meneruskannya sebagai pointer ke elemen

void foo(char p[])

adalah kesalahan tingkat teknik utama, yang sayangnya agak meluas akhir-akhir ini. Teknik pointer-to-array adalah pendekatan yang jauh lebih baik dalam kasus seperti itu.

Alasan lain yang mungkin menghalangi pengadopsian teknik passing larik ukuran tetap adalah dominasi pendekatan naif untuk pengetikan larik yang dialokasikan secara dinamis. Misalnya, jika program memanggil array tipe tetap char[10](seperti dalam contoh Anda), rata-rata pengembang akan mallocseperti array seperti

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

Larik ini tidak dapat diteruskan ke fungsi yang dideklarasikan sebagai

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

yang membingungkan developer rata-rata dan membuat mereka meninggalkan deklarasi parameter ukuran tetap tanpa memikirkannya lebih lanjut. Namun kenyataannya, akar masalahnya terletak pada mallocpendekatan yang naif . The mallocFormat yang ditunjukkan di atas harus disediakan untuk array ukuran run-time. Jika tipe array memiliki ukuran waktu kompilasi, cara yang lebih baik untuk mallocitu akan terlihat sebagai berikut

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

Ini, tentu saja, dapat dengan mudah diteruskan ke pernyataan di atas foo

foo(p);

dan kompilator akan melakukan pemeriksaan jenis yang benar. Tetapi sekali lagi, ini terlalu membingungkan bagi pengembang C yang tidak siap, itulah sebabnya Anda tidak akan melihatnya terlalu sering dalam kode harian rata-rata yang "biasa".


2
Jawabannya memberikan deskripsi yang sangat ringkas dan informatif tentang bagaimana sizeof () berhasil, seberapa sering gagal, dan cara selalu gagal. pengamatan Anda terhadap sebagian besar insinyur C / C ++ tidak memahami, dan oleh karena itu melakukan sesuatu yang menurut mereka benar-benar mereka pahami adalah salah satu hal yang lebih profetik yang pernah saya lihat dalam beberapa waktu, dan tabir tidak seberapa dibandingkan dengan keakuratan yang dijelaskannya. serius, tuan. jawaban yang bagus.
WhozCraig

Saya baru saja merefaktor beberapa kode berdasarkan jawaban ini, bravo dan terima kasih untuk Q dan A.
Perry

1
Saya ingin tahu bagaimana Anda menangani constproperti dengan teknik ini. Sebuah const char (*p)[N]argumen tampaknya tidak kompatibel dengan pointer ke char table[N];Sebaliknya, sederhana char*ptr tetap kompatibel dengan const char*argumen.
Cyan

4
Mungkin berguna untuk dicatat bahwa untuk mengakses elemen array Anda, Anda perlu melakukannya (*p)[i]dan tidak *p[i]. Yang terakhir akan melompat sesuai ukuran array, yang hampir pasti bukan yang Anda inginkan. Setidaknya bagi saya, mempelajari sintaksis ini menyebabkan, bukannya mencegah, kesalahan; Saya akan mendapatkan kode yang benar lebih cepat hanya dengan melewati pelampung *.
Andrew Wagner

1
Ya @mickey, yang Anda sarankan adalah constpenunjuk ke larik elemen yang bisa berubah. Dan ya, ini benar-benar berbeda dari penunjuk ke larik elemen yang tidak dapat diubah.
Cyan

11

Saya ingin menambahkan jawaban AndreyT (jika ada yang menemukan halaman ini mencari info lebih lanjut tentang topik ini):

Ketika saya mulai bermain lebih banyak dengan deklarasi ini, saya menyadari bahwa ada cacat utama yang terkait dengannya di C (ternyata bukan di C ++). Sangat umum untuk memiliki situasi di mana Anda ingin memberikan penelepon sebuah pointer const ke buffer yang telah Anda tulis. Sayangnya, ini tidak mungkin ketika mendeklarasikan pointer seperti ini di C. Dengan kata lain, standar C (6.7.3 - Paragraph 8) bertentangan dengan hal seperti ini:


   int array[9];

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

Batasan ini tampaknya tidak ada di C ++, membuat jenis deklarasi ini jauh lebih berguna. Tetapi dalam kasus C, perlu untuk kembali ke deklarasi pointer biasa setiap kali Anda ingin pointer const ke buffer ukuran tetap (kecuali buffer itu sendiri dideklarasikan sebagai const). Anda dapat menemukan info lebih lanjut di utas email ini: teks tautan

Ini adalah kendala yang parah menurut saya dan ini bisa menjadi salah satu alasan utama mengapa orang biasanya tidak menyatakan petunjuk seperti ini di C. Yang lainnya adalah fakta bahwa kebanyakan orang bahkan tidak tahu bahwa Anda dapat menyatakan penunjuk seperti ini sebagai AndreyT telah menunjukkan.


2
Itu tampaknya menjadi masalah khusus kompilator. Saya dapat menduplikasi menggunakan gcc 4.9.1, tetapi clang 3.4.2 dapat beralih dari versi non-const ke versi const tidak ada masalah. Saya membaca spesifikasi C11 (p 9 dalam versi saya ... bagian yang berbicara tentang dua jenis yang memenuhi syarat yang kompatibel) dan setuju bahwa tampaknya mengatakan konversi ini ilegal. Namun, kami tahu dalam praktiknya bahwa Anda selalu dapat mengonversi secara otomatis dari char * ke char const * tanpa peringatan. IMO, dentang lebih konsisten dalam mengizinkan ini daripada gcc, meskipun saya setuju dengan Anda bahwa spesifikasi tampaknya melarang salah satu konversi otomatis ini.
Doug Richardson

4

Alasan yang jelas adalah bahwa kode ini tidak dapat dikompilasi:

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

Promosi default dari sebuah larik adalah ke penunjuk yang tidak memenuhi syarat.

Juga lihat pertanyaan ini , menggunakan foo(&p)harus bekerja.


3
Tentu saja foo (p) tidak akan berfungsi, foo meminta penunjuk ke larik 10 elemen sehingga Anda harus meneruskan alamat larik Anda ...
Brian R. Bondy

9
Bagaimana "alasan yang jelas" itu? Jelas dipahami bahwa cara yang tepat untuk memanggil fungsi tersebut adalah foo(&p).
AnT

3
Saya kira "jelas" adalah kata yang salah. Maksud saya "paling lugas". Perbedaan antara p dan & p dalam hal ini cukup tidak jelas bagi rata-rata programmer C. Seseorang yang mencoba melakukan apa yang disarankan poster akan menulis apa yang saya tulis, mendapatkan kesalahan waktu kompilasi, dan menyerah.
Keith Randall

2

Saya juga ingin menggunakan sintaks ini untuk mengaktifkan lebih banyak pemeriksaan tipe.

Tapi saya juga setuju bahwa sintaksis dan model mental menggunakan pointer lebih sederhana, dan lebih mudah diingat.

Berikut beberapa kendala yang saya temui.

  • Mengakses larik membutuhkan penggunaan (*p)[]:

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

    Sangat menggoda untuk menggunakan pointer-to-char lokal sebagai gantinya:

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

    Tapi ini sebagian akan menggagalkan tujuan menggunakan tipe yang benar.

  • Kita harus ingat untuk menggunakan operator address-of saat menetapkan alamat array ke pointer-to-array:

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

    Operator address-of mendapatkan alamat dari seluruh array &a, dengan tipe yang benar untuk ditetapkan p. Tanpa operator, asecara otomatis diubah ke alamat elemen pertama dari array, sama seperti in &a[0], yang memiliki tipe berbeda.

    Karena konversi otomatis ini sudah berlangsung, saya selalu bingung apakah &itu perlu. Hal ini konsisten dengan penggunaan &pada variabel jenis lain, tetapi saya harus ingat bahwa sebuah array adalah khusus dan saya membutuhkannya &untuk mendapatkan jenis alamat yang benar, meskipun nilai alamatnya sama.

    Salah satu alasan untuk masalah saya mungkin karena saya belajar K&R C di tahun 80-an, yang belum mengizinkan penggunaan &operator di seluruh larik (meskipun beberapa kompiler mengabaikannya atau mentolerir sintaksis). Ngomong-ngomong, mungkin menjadi alasan lain mengapa pointers-to-array mengalami kesulitan untuk diadopsi: mereka hanya bekerja dengan baik sejak ANSI C, dan &batasan operator mungkin menjadi alasan lain untuk menganggapnya terlalu canggung.

  • Bila typedefini tidak digunakan untuk membuat jenis untuk array pointer-ke-(dalam file header umum), maka array pointer-to-global yang membutuhkan lebih rumit externdeklarasi untuk berbagi di file:

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

1

Sederhananya, C tidak melakukan hal-hal seperti itu. Sebuah array tipe Tditeruskan sebagai penunjuk ke yang pertama Tdalam array, dan hanya itu yang Anda dapatkan.

Hal ini memungkinkan untuk beberapa algoritma yang keren dan elegan, seperti perulangan melalui array dengan ekspresi seperti

*dst++ = *src++

Sisi negatifnya adalah pengelolaan ukurannya terserah Anda. Sayangnya, kegagalan untuk melakukan ini dengan hati-hati juga telah menyebabkan jutaan bug dalam pengkodean C, dan / atau peluang untuk eksploitasi yang jahat.

Apa yang mendekati apa yang Anda tanyakan di C adalah meneruskan a struct(dengan nilai) atau penunjuk ke satu (dengan referensi). Selama tipe struct yang sama digunakan di kedua sisi operasi ini, baik kode yang membagikan referensi dan kode yang menggunakannya sama-sama setuju tentang ukuran data yang ditangani.

Struct Anda dapat berisi data apa pun yang Anda inginkan; itu bisa berisi larik Anda dengan ukuran yang ditentukan dengan baik.

Namun, tidak ada yang mencegah Anda atau pembuat kode yang tidak kompeten atau jahat untuk menggunakan cast untuk mengelabui compiler agar memperlakukan struct Anda sebagai salah satu ukuran yang berbeda. Kemampuan yang hampir tidak terbelenggu untuk melakukan hal semacam ini adalah bagian dari desain C.


1

Anda dapat mendeklarasikan larik karakter dengan beberapa cara:

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

Prototipe ke fungsi yang mengambil array berdasarkan nilai adalah:

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

atau dengan referensi:

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

atau dengan sintaks array:

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

2
Jangan lupa bahwa array berukuran tetap juga bisa dialokasikan secara dinamis sebagai char (*p)[10] = malloc(sizeof *p).
AnT

Lihat di sini untuk pembahasan lebih rinci antara perbedaan char array [] dan char * ptr di sini. stackoverflow.com/questions/1807530/…
t0mm13b

1

Saya tidak akan merekomendasikan solusi ini

typedef int Vector3d[3];

karena mengaburkan fakta bahwa Vector3D memiliki tipe yang harus Anda ketahui. Pemrogram biasanya tidak mengharapkan variabel dengan tipe yang sama memiliki ukuran yang berbeda. Pertimbangkan:

void foo(Vector3d a) {
   Vector3D b;
}

dimana sizeof a! = sizeof b


Dia tidak menyarankan ini sebagai solusi. Dia hanya menggunakan ini sebagai contoh.
figurassa

Hm. Kenapa sizeof(a)tidak sama sizeof(b)?
sherrellbc

0

Mungkin saya melewatkan sesuatu, tapi ... karena array adalah pointer konstan, pada dasarnya itu berarti tidak ada gunanya meneruskan pointer ke mereka.

Tidak bisakah kamu menggunakan saja void foo(char p[10], int plen);?


4
Array BUKAN pointer konstan. Harap baca beberapa FAQ tentang array.
AnT

2
Untuk yang penting di sini (array unidimensi sebagai parameter), faktanya adalah bahwa array tersebut meluruh menjadi pointer yang konstan. Silakan baca FAQ tentang bagaimana menjadi tidak terlalu bertele-tele.
fortran

-2

Pada kompiler saya (vs2008) itu memperlakukan char (*p)[10]sebagai array penunjuk karakter, seolah-olah tidak ada tanda kurung, bahkan jika saya mengkompilasi sebagai file C. Apakah kompilator mendukung "variabel" ini? Jika demikian itu adalah alasan utama untuk tidak menggunakannya.


1
-1 Salah. Ini berfungsi dengan baik pada vs2008, vs2010, gcc. Khususnya contoh ini berfungsi dengan baik: stackoverflow.com/a/19208364/2333290
kotlomoy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.