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 inttest
diikuti oleh tanda pengenal ;
. Itu tidak dikenali int
sebagai 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 int
dan declarators yang a[10]={0}
, *p=NULL
, dan f(void)
. Deklarator memperkenalkan nama hal yang dideklarasikan ( a
,, p
dan f
) bersama dengan informasi tentang array-ness, pointer-ness, dan function-ness. Deklarator mungkin juga memiliki penginisialisasi terkait.
Jenisnya a
adalah "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 p
adalah "pointer to unsigned long int
", dan sekali lagi tipe itu ditentukan oleh kombinasi penentu deklarasi dan deklarator, dan diinisialisasi ke NULL
. Dan jenisnya f
adalah "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, test1
adalah operan *
dan karenanya memiliki tipe "pointer to int
", sementara test2
memiliki 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 - const
dapat memodifikasi tipe pointer dan tipe menunjuk-ke:
const int *p;
int const *p;
Kedua hal di atas mendeklarasikan p
sebagai pointer ke sebuah const int
objek. Anda dapat menulis nilai baru untuk p
menyetelnya 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 p
sebagai const
pointer ke non-const int
; Anda dapat menulis ke hal yang p
ditunjukkan
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
tetapi Anda tidak dapat menetapkan p
untuk 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 int
dinamai ap
, dan kita ingin mengakses int
nilai yang ditunjukkan oleh i
elemen 'th. Kami akan mengakses nilai itu sebagai berikut:
printf( "%d", *ap[i] );
The ekspresi *ap[i]
memiliki tipe int
; dengan demikian, pernyataan ap
tertulis 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 int
bernama pa
dan kita ingin mengakses nilai i
elemen '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 i
elemen 'th pa
, kami ingin mengakses i
elemen' 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 *p
dalam kode Anda akan menghasilkan int
nilai. Dengan ekstensi, ini memberitahu Anda bahwa ekspresi p
menghasilkan nilai tipe "pointer ke int
", atau int *
.
Jadi, bagaimana dengan pemeran dan sizeof
ekspresi, 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* p
konvensi 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 for
loop 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.