Ada tiga bagian dari teka-teki ini.
Bagian pertama adalah bahwa spasi dalam C dan C ++ biasanya tidak signifikan selain memisahkan token yang berdekatan yang tidak dapat dibedakan.
Selama tahap preprocessing, teks sumber dipecah menjadi urutan token - pengidentifikasi, punctuator, literal numerik, literal string, dll. Urutan token tersebut kemudian dianalisis untuk sintaks dan artinya. Tokeniser bersifat "serakah" dan akan membuat token valid terpanjang yang memungkinkan. Jika Anda menulis sesuatu seperti
inttest;
tokenizer hanya melihat dua token - pengenal inttestdiikuti oleh tanda pengenal ;. Itu tidak dikenali intsebagai kata kunci terpisah pada tahap ini (yang terjadi nanti dalam proses). Jadi, agar baris dibaca sebagai deklarasi integer bernama test, kita harus menggunakan spasi untuk memisahkan token pengenal:
int test;
The *karakter bukan bagian dari identifier apapun; itu adalah token terpisah (punctuator) dengan sendirinya. Jadi jika Anda menulis
int*test;
kompilator melihat 4 token terpisah - int, *, test, dan ;. Jadi, spasi putih tidak signifikan dalam deklarasi pointer, dan semua
int *test;
int* test;
int*test;
int * test;
ditafsirkan dengan cara yang sama.
Bagian kedua dari teka-teki adalah bagaimana deklarasi sebenarnya bekerja di C dan C ++ 1 . Deklarasi dipecah menjadi dua bagian utama - urutan penentu deklarasi (penentu kelas penyimpanan, penentu jenis, penentu jenis, dll.) Diikuti dengan daftar deklarator (mungkin diinisialisasi) yang dipisahkan koma . Dalam deklarasi
unsigned long int a[10]={0}, *p=NULL, f(void);
specifiers deklarasi yang unsigned long intdan declarators yang a[10]={0}, *p=NULL, dan f(void). Deklarator memperkenalkan nama hal yang dideklarasikan ( a,, pdan f) bersama dengan informasi tentang array-ness, pointer-ness, dan function-ness. Deklarator mungkin juga memiliki penginisialisasi terkait.
Jenisnya aadalah "larik 10 elemen unsigned long int". Jenis tersebut sepenuhnya ditentukan oleh kombinasi penentu deklarasi dan deklarator, dan nilai awal ditentukan dengan penginisialisasi ={0}. Demikian pula, tipe padalah "pointer to unsigned long int", dan sekali lagi tipe itu ditentukan oleh kombinasi penentu deklarasi dan deklarator, dan diinisialisasi ke NULL. Dan jenisnya fadalah "fungsi yang kembali unsigned long int" dengan alasan yang sama.
Ini adalah kunci - tidak ada penentu tipe "penunjuk-ke" , sama seperti tidak ada penentu jenis "larik-dari", sama seperti tidak ada penentu jenis yang "mengembalikan fungsi". Kami tidak dapat mendeklarasikan sebuah array sebagai
int[10] a;
karena operan []operatornya a, bukan int. Begitu pula di deklarasi
int* p;
operannya *adalah p, bukan int. Tetapi karena operator indirection adalah unary dan whitespace tidak signifikan, compiler tidak akan mengeluh jika kita menuliskannya dengan cara ini. Namun, itu selalu diartikan sebagai int (*p);.
Karena itu, jika Anda menulis
int* p, q;
operan dari *adalah p, jadi itu akan diartikan sebagai
int (*p), q;
Jadi, semuanya
int *test1, test2;
int* test1, test2;
int * test1, test2;
melakukan hal yang sama - dalam ketiga kasus, test1adalah operan *dan karenanya memiliki tipe "pointer to int", sementara test2memiliki tipe int.
Deklarator bisa menjadi rumit secara sewenang-wenang. Anda dapat memiliki array pointer:
T *a[N];
Anda dapat memiliki pointer ke array:
T (*a)[N];
Anda dapat memiliki fungsi yang mengembalikan pointer:
T *f(void);
Anda dapat memiliki petunjuk ke fungsi:
T (*f)(void)
Anda dapat memiliki array pointer ke fungsi:
T (*a[N])(void)
Anda dapat memiliki fungsi yang mengembalikan pointer ke array:
T (*f(void))[N];
Anda dapat memiliki fungsi yang mengembalikan pointer ke array pointer ke fungsi yang mengembalikan pointer ke T:
T *(*(*f(void))[N])(void)
dan kemudian Anda memiliki signal:
void (int)))(int);
yang berbunyi
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int)
dan ini hanya menggores permukaan dari apa yang mungkin. Tetapi perhatikan bahwa array-ness, pointer-ness, dan function-ness selalu menjadi bagian dari deklarator, bukan penentu tipe.
Satu hal yang harus diperhatikan - constdapat memodifikasi tipe pointer dan tipe menunjuk-ke:
const int *p;
int const *p;
Kedua hal di atas mendeklarasikan psebagai pointer ke sebuah const intobjek. Anda dapat menulis nilai baru untuk pmenyetelnya agar mengarah ke objek yang berbeda:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
tetapi Anda tidak dapat menulis ke objek yang diarahkan ke:
*p = 3;
Namun,
int * const p;
mendeklarasikan psebagai constpointer ke non-const int; Anda dapat menulis ke hal yang pditunjukkan
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
tetapi Anda tidak dapat menetapkan puntuk menunjuk ke objek lain:
p = &y
Yang membawa kita ke bagian ketiga dari teka-teki - mengapa deklarasi disusun seperti ini.
Maksudnya adalah bahwa struktur deklarasi harus mencerminkan struktur ekspresi dalam kode ("deklarasi meniru penggunaan"). Sebagai contoh, misalkan kita memiliki array pointer untuk intdinamai ap, dan kita ingin mengakses intnilai yang ditunjukkan oleh ielemen 'th. Kami akan mengakses nilai itu sebagai berikut:
printf( "%d", *ap[i] );
The ekspresi *ap[i] memiliki tipe int; dengan demikian, pernyataan aptertulis sebagai
int *ap[N];
Deklarator *ap[N]memiliki struktur yang sama dengan ekspresi *ap[i]. Operator *dan []berperilaku dengan cara yang sama dalam deklarasi yang mereka lakukan dalam ekspresi - []memiliki prioritas yang lebih tinggi daripada unary *, jadi operand of *adalah ap[N](diurai sebagai *(ap[N])).
Sebagai contoh lain, misalkan kita memiliki pointer ke array intbernama padan kita ingin mengakses nilai ielemen 'th. Kami akan menulisnya sebagai
printf( "%d", (*pa)[i] )
Jenis ekspresi (*pa)[i]adalah int, sehingga deklarasi ditulis sebagai
int (*pa)[N];
Sekali lagi, aturan prioritas dan asosiatif yang sama berlaku. Dalam hal ini, kami tidak ingin mendereferensi ielemen 'th pa, kami ingin mengakses ielemen' th pa poin apa , jadi kami harus secara eksplisit mengelompokkan *operator dengan pa.
Operator *, []dan ()semuanya adalah bagian dari ekspresi dalam kode, jadi semuanya adalah bagian dari deklarator dalam deklarasi. Deklarator memberi tahu Anda cara menggunakan objek dalam ekspresi. Jika Anda memiliki deklarasi seperti int *p;, itu memberi tahu Anda bahwa ekspresi *pdalam kode Anda akan menghasilkan intnilai. Dengan ekstensi, ini memberitahu Anda bahwa ekspresi pmenghasilkan nilai tipe "pointer ke int", atau int *.
Jadi, bagaimana dengan pemeran dan sizeofekspresi, di mana kita menggunakan hal-hal seperti (int *)atau sizeof (int [10])atau seperti itu? Bagaimana cara membaca sesuatu seperti
void foo( int *, int (*)[10] );
Tidak ada deklarator, bukankah operator *and []memodifikasi tipe secara langsung?
Ya, tidak - masih ada deklarator, hanya dengan pengenal kosong (dikenal sebagai deklarator abstrak ). Jika kami mewakili sebuah identifier kosong dengan λ simbol, maka kita dapat membaca hal-hal sebagai (int *λ), sizeof (int λ[10]), dan
void foo( int *λ, int (*λ)[10] );
dan mereka berperilaku persis seperti deklarasi lainnya. int *[10]mewakili larik yang terdiri dari 10 penunjuk, sedangkan int (*)[10]mewakili penunjuk ke larik.
Dan sekarang bagian opini dari jawaban ini. Saya tidak menyukai konvensi C ++ yang menyatakan pointer sederhana sebagai
T* p;
dan menganggapnya sebagai praktik yang buruk karena alasan berikut:
- Ini tidak konsisten dengan sintaks;
- Ini menimbulkan kebingungan (seperti yang dibuktikan oleh pertanyaan ini, semua duplikat dari pertanyaan ini, pertanyaan tentang arti
T* p, q;, semua duplikat dari pertanyaan - pertanyaan itu, dll.);
- Ini tidak konsisten secara internal - mendeklarasikan array pointer sebagai
T* a[N]asimetris dengan penggunaan (kecuali jika Anda terbiasa menulis * a[i]);
- Ini tidak bisa diterapkan ke tipe pointer-to-array atau pointer-to-function (kecuali Anda membuat typedef supaya Anda bisa menerapkan
T* pkonvensi dengan rapi, yang ... tidak );
- Alasan untuk melakukannya - "ini menekankan penunjuk-an objek" - adalah palsu. Itu tidak dapat diterapkan ke tipe array atau fungsi, dan saya akan berpikir kualitas itu sama pentingnya untuk ditekankan.
Pada akhirnya, ini hanya menunjukkan pemikiran yang bingung tentang bagaimana sistem tipe kedua bahasa bekerja.
Ada alasan bagus untuk menyatakan item secara terpisah; mengatasi praktik yang buruk ( T* p, q;) bukanlah salah satunya. Jika Anda menulis deklarator dengan benar ( T *p, q;), kemungkinan kecil Anda akan menimbulkan kebingungan.
Saya menganggapnya mirip dengan sengaja menulis semua forloop sederhana Anda sebagai
i = 0;
for( ; i < N; )
{
...
i++
}
Valid secara sintaksis, tetapi membingungkan, dan maksudnya cenderung disalahartikan. Namun, T* p;konvensi tersebut mengakar dalam komunitas C ++, dan saya menggunakannya dalam kode C ++ saya sendiri karena konsistensi di seluruh basis kode adalah hal yang baik, tetapi itu membuat saya gatal setiap kali melakukannya.
- Saya akan menggunakan terminologi C - terminologi C ++ sedikit berbeda, tetapi konsepnya sebagian besar sama.