Mengapa saya tidak bisa mengakses pointer ke pointer untuk array stack?

35

Silakan lihat kode berikut. Mencoba untuk melewatkan array sebagai char**ke fungsi:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Fakta bahwa saya hanya bisa mendapatkannya untuk mengkompilasi dengan secara eksplisit pengecoran &test2untuk char**sudah mengisyaratkan bahwa kode ini salah.

Tetap saja, saya bertanya-tanya apa sebenarnya yang salah tentang itu. Saya bisa meneruskan sebuah pointer ke sebuah pointer ke sebuah array yang dialokasikan secara dinamis tetapi saya tidak bisa melewatkan sebuah pointer ke sebuah pointer untuk sebuah array di stack. Tentu saja, saya dapat dengan mudah mengatasi masalah dengan terlebih dahulu menetapkan array ke variabel sementara, seperti:

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

Namun, bisakah seseorang menjelaskan kepada saya mengapa itu tidak berhasil dilemparkan char[256]ke char**langsung?

Andreas
sumber

Jawaban:

29

Karena testbukan pointer.

&testmemberi Anda pointer ke array, tipe char (*)[256], yang tidak kompatibel dengan char**(karena array bukan pointer). Ini menghasilkan perilaku yang tidak terdefinisi.

emlai
sumber
3
Tapi mengapa C compiler kemudian memungkinkan melewati sesuatu jenis char (*)[256]ke char**?
ComFreek
@ComFreek Saya menduga bahwa dengan peringatan maksimal dan -Kesalahan, itu tidak memungkinkan.
PiRocks
@ComFreek: Itu tidak benar-benar mengizinkannya. Saya harus memaksa kompiler untuk menerimanya dengan secara eksplisit melemparkannya ke char**. Tanpa gips itu, ia tidak bisa dikompilasi.
Andreas
38

testadalah array, bukan pointer, dan &testmerupakan pointer ke array. Ini bukan pointer ke pointer.

Anda mungkin telah diberi tahu bahwa array adalah pointer, tetapi ini tidak benar. Nama array adalah nama seluruh objek — semua elemen. Ini bukan penunjuk ke elemen pertama. Dalam kebanyakan ekspresi, array secara otomatis dikonversi ke pointer ke elemen pertamanya. Itu adalah kenyamanan yang sering bermanfaat. Namun ada tiga pengecualian untuk aturan ini:

  • Array adalah operan dari sizeof.
  • Array adalah operan dari &.
  • Array adalah string literal yang digunakan untuk menginisialisasi array.

Dalam &test, array adalah operan &, jadi konversi otomatis tidak terjadi. Hasilnya &testadalah pointer ke array 256 char, yang bertipe char (*)[256], bukan char **.

Untuk mendapatkan pointer ke pointer chardari test, Anda harus terlebih dahulu membuat pointer ke char. Sebagai contoh:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

Cara lain untuk memikirkan hal ini adalah dengan menyadari bahwa testmenamai seluruh objek — seluruh array 256 char. Itu tidak menamai pointer, jadi, di &test, tidak ada pointer yang alamatnya dapat diambil, jadi ini tidak dapat menghasilkan a char **. Untuk membuat char **, Anda harus terlebih dahulu memiliki char *.

Eric Postpischil
sumber
1
Apakah daftar tiga pengecualian ini lengkap?
Ruslan
8
@Ruslan: Ya, per C 2018 6.3.2.1 3.
Eric Postpischil
Oh, dan di C11 ada juga _Alignofoperator yang disebutkan di samping sizeofdan &. Saya ingin tahu mengapa mereka menghapusnya ...
Ruslan
@Ruslan: Itu dihapus karena itu adalah kesalahan. _Alignofhanya menerima nama jenis sebagai operan dan tidak pernah menerima array atau objek lain sebagai operan. (Saya tidak tahu mengapa; sepertinya secara sintaksis dan tata bahasa bisa seperti sizeof, tetapi tidak.)
Eric Postpischil
6

Jenisnya test2adalah char *. Jadi, jenis &test2akan menjadi char **yang kompatibel dengan jenis parameter xdari printchar().
Jenisnya testadalah char [256]. Jadi, jenis &testakan menjadi char (*)[256]yang tidak kompatibel dengan jenis parameter xdari printchar().

Biarkan saya menunjukkan kepada Anda perbedaan dalam hal alamat testdan test2.

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Keluaran:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

Poin yang perlu diperhatikan di sini:

Output (alamat memori) dari test2dan &test2[0]secara numerik sama dan tipenya juga sama yaitu char *.
Tetapi test2dan &test2adalah alamat yang berbeda dan jenis mereka juga berbeda.
Jenisnya test2adalah char *.
Jenisnya &test2adalah char **.

x = &test2
*x = test2
(*x)[0] = test2[0] 

Output (alamat memori) dari test, &testdan &test[0]secara numerik sama tetapi tipenya berbeda .
Jenisnya testadalah char [256].
Jenisnya &testadalah char (*) [256].
Jenisnya &test[0]adalah char *.

Sebagai output menunjukkan &testsama dengan &test[0].

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

Karenanya Anda mendapatkan kesalahan segmentasi.

HS
sumber
3

Anda tidak dapat mengakses pointer ke pointer karena &testbukan pointer — ini adalah array.

Jika Anda mengambil alamat array, melemparkan array dan alamat array ke (void *), dan membandingkannya, mereka akan (membatasi kemungkinan pointer pedantry) setara.

Apa yang sebenarnya Anda lakukan mirip dengan ini (sekali lagi, cegah alias ketat):

putchar(**(char **)test);

yang jelas-jelas salah.

SS Anne
sumber
3

Kode Anda mengharapkan argumen xdari printchartitik ke memori yang berisi (char *).

Dalam panggilan pertama, itu menunjuk ke penyimpanan yang digunakan untuk test2dan dengan demikian memang nilai yang menunjuk ke (char *), yang terakhir menunjuk ke memori yang dialokasikan.

Dalam panggilan kedua, bagaimanapun, tidak ada tempat di mana (char *)nilai tersebut dapat disimpan dan oleh karena itu tidak mungkin untuk menunjuk ke memori tersebut. Para pemain yang (char **)Anda tambahkan akan menghapus kesalahan kompilasi (tentang konversi (char *)ke (char **)) tetapi itu tidak akan membuat penyimpanan muncul dari udara tipis untuk mengandung (char *)inisialisasi yang mengarah ke karakter pertama pengujian. Pointer casting dalam C tidak mengubah nilai aktual dari pointer.

Untuk mendapatkan yang Anda inginkan, Anda harus melakukannya secara eksplisit:

char *tempptr = &temp;
printchar(&tempptr);

Saya menganggap contoh Anda adalah penyulingan dari sepotong kode yang jauh lebih besar; sebagai contoh, mungkin Anda ingin printcharmenambah (char *)nilai yang xditunjukkan nilai yang diteruskan sehingga pada panggilan berikutnya karakter berikutnya dicetak. Jika bukan itu masalahnya, mengapa Anda tidak hanya (char *)menunjuk ke karakter yang akan dicetak, atau bahkan hanya meneruskan karakter itu sendiri?

Kevin Martin
sumber
Jawaban yang bagus; Saya setuju cara termudah untuk menjaga hal ini adalah dengan berpikir apakah ada objek C yang menyimpan alamat array, yaitu objek pointer yang dapat Anda ambil alamatnya untuk mendapatkan char **. Variabel array / benda sederhana yang array, dengan alamat yang implisit, di mana saja tidak disimpan. Tidak ada tingkat tipuan tambahan untuk mengaksesnya, tidak seperti dengan variabel penunjuk yang menunjuk ke penyimpanan lain.
Peter Cordes
0

Rupanya, mengambil alamat testsama dengan mengambil alamat test[0]:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Kompilasi dan jalankan:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

Jadi penyebab utama kesalahan segmentasi adalah bahwa program ini akan mencoba untuk meringkas alamat absolut 0x42(juga dikenal sebagai 'B'), yang program Anda tidak memiliki izin untuk membaca.

Meskipun dengan kompiler / mesin yang berbeda, alamatnya akan berbeda: Coba online! , tetapi Anda masih akan mendapatkan ini, karena beberapa alasan:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

Tetapi alamat yang menyebabkan kesalahan segmentasi mungkin sangat berbeda:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
sumber
1
Mengambil alamat testtidak sama dengan mengambil alamat test[0]. Yang pertama memiliki tipe char (*)[256], dan yang kedua memiliki tipe char *. Mereka tidak kompatibel, dan standar C memungkinkan mereka untuk memiliki representasi yang berbeda.
Eric Postpischil
Ketika memformat pointer %p, itu harus dikonversi ke void *(lagi untuk alasan kompatibilitas dan representasi).
Eric Postpischil
1
printchar(&test);mungkin macet untuk Anda, tetapi perilaku tidak ditentukan oleh standar C, dan orang-orang dapat mengamati perilaku lain dalam keadaan lain.
Eric Postpischil
Re “Jadi penyebab utama kesalahan segmentasi adalah bahwa program ini akan mencoba untuk mereduksi alamat absolut 0x42 (juga dikenal sebagai 'B'), yang mungkin ditempati oleh OS.”: Jika ada kesalahan segmen yang mencoba membaca lokasi, itu berarti tidak ada yang dipetakan di sana, bukan ditempati oleh OS. (Kecuali mungkin ada sesuatu yang dipetakan di sana seperti, katakanlah, jalankan saja tanpa izin baca, tapi itu tidak mungkin.)
Eric Postpischil
1
&test == &test[0]melanggar kendala dalam C 2018 6.5.9 2 karena tipe tidak kompatibel. Standar C membutuhkan implementasi untuk mendiagnosis pelanggaran ini, dan perilaku yang dihasilkan tidak ditentukan oleh standar C. Itu berarti kompiler Anda mungkin menghasilkan kode yang menilai mereka sama, tetapi kompiler lain mungkin tidak.
Eric Postpischil
-4

Representasi char [256]tergantung pada implementasi. Itu tidak harus sama dengan char *.

Casting &testtipe char (*)[256]untuk char **menghasilkan perilaku yang tidak terdefinisi.

Dengan beberapa kompiler, mungkin melakukan apa yang Anda harapkan, dan yang lain tidak.

EDIT:

Setelah pengujian dengan gcc 9.2.1, tampaknya printchar((char**)&test)lolos test sebagai nilai yang diberikan char**. Seolah-olah instruksi itu printchar((char**)test). Dalam printcharfungsinya, xadalah pointer ke char pertama dari tes array, bukan double pointer ke karakter pertama. Hasil de-referensi ganda xdalam kesalahan segmentasi karena 8 byte pertama dari array tidak sesuai dengan alamat yang valid.

Saya mendapatkan perilaku dan hasil yang sama persis ketika mengkompilasi program dengan dentang 9.0.0-2.

Ini dapat dianggap sebagai bug penyusun, atau hasil dari perilaku tidak terdefinisi yang hasilnya mungkin khusus untuk penyusun.

Perilaku tak terduga lainnya adalah kode itu

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

Outputnya adalah

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Perilaku anehnya adalah itu xdan *xmemiliki nilai yang sama.

Ini adalah kompiler. Saya ragu bahwa ini didefinisikan oleh bahasa.

chmike
sumber
1
Apakah yang Anda maksud adalah representasi yang char (*)[256]bergantung pada implementasi? Representasi char [256]tidak relevan dalam pertanyaan ini — itu hanya sekumpulan bit. Tapi, bahkan jika Anda maksud representasi pointer ke array berbeda dari representasi pointer ke pointer, itu juga meleset. Bahkan jika mereka memiliki representasi yang sama, kode OP tidak akan berfungsi, karena penunjuk ke penunjuk dapat ditinjau dua kali, seperti yang dilakukan printchar, tetapi penunjuk ke array tidak dapat, terlepas dari penyajiannya.
Eric Postpischil
@EricPostpischil para pemain dari char (*)[256]ke char **diterima oleh kompiler, tetapi tidak menghasilkan hasil yang diharapkan karena a char [256]tidak sama dengan a char *. Saya berasumsi, pengkodeannya berbeda, jika tidak akan menghasilkan hasil yang diharapkan.
chmike
Saya tidak tahu apa yang Anda maksud dengan "hasil yang diharapkan." Satu-satunya spesifikasi dalam standar C dari apa hasil seharusnya adalah bahwa, jika keberpihakan tidak memadaichar ** , perilaku tidak terdefinisi, dan bahwa, jika hasilnya dikonversi kembali ke char (*)[256], itu sebanding dengan pointer asli. Dengan "hasil yang diharapkan", Anda dapat berarti bahwa, jika (char **) &testdikonversi lebih jauh ke a char *, hasilnya sama dengan &test[0]. Itu bukan hasil yang tidak mungkin dalam implementasi yang menggunakan ruang alamat datar, tapi itu bukan masalah representasi.
Eric Postpischil
2
Juga, "Casting & uji tipe char (*) [256] untuk char ** menghasilkan perilaku yang tidak terdefinisi." tidak benar. C 2018 6.3.2.3 7 memungkinkan pointer ke jenis objek untuk dikonversi ke pointer lain ke jenis objek. Jika pointer tidak benar selaras untuk jenis direferensikan (jenis direferensikan dari char **is char *), maka perilaku tersebut tidak terdefinisi. Kalau tidak, konversi didefinisikan, meskipun nilainya hanya sebagian didefinisikan, per komentar saya di atas.
Eric Postpischil
char (*x)[256]tidak sama dengan char **x. Alasan xdan *xmencetak nilai pointer yang sama adalah itu xhanyalah pointer ke array. Anda *x adalah array , dan menggunakannya dalam konteks pointer meluruh kembali ke alamat array . Tidak ada bug kompiler di sana (atau dalam hal apa (char **)&test), hanya sedikit senam mental yang diperlukan untuk mencari tahu yang terjadi dengan jenis. (cdecl menjelaskannya sebagai "menyatakan x sebagai pointer ke array 256 dari char"). Bahkan menggunakan char*untuk mengakses objek-representasi char**bukan UB; bisa alias apa saja.
Peter Cordes