เหตุใดฉันไม่สามารถเข้าถึงตัวชี้ไปยังตัวชี้สำหรับสแต็กอาเรย์


35

โปรดดูรหัสต่อไปนี้ มันพยายามที่จะส่งผ่านอาร์เรย์เป็นchar**ฟังก์ชั่น:

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

ความจริงที่ว่าฉันสามารถรวบรวมมันได้โดยการชี้แนะอย่างชัดเจน&test2เพื่อchar**บอกเป็นนัย ๆ ว่ารหัสนี้ผิด

แต่ถึงกระนั้นฉันสงสัยว่าสิ่งที่ผิดเกี่ยวกับมัน ฉันสามารถส่งตัวชี้ไปยังตัวชี้ไปยังอาร์เรย์ที่จัดสรรแบบไดนามิก แต่ฉันไม่สามารถส่งตัวชี้ไปยังตัวชี้สำหรับอาร์เรย์ในกองซ้อนได้ แน่นอนว่าฉันสามารถแก้ไขปัญหาได้อย่างง่ายดายโดยกำหนดอาร์เรย์ให้กับตัวแปรชั่วคราวเช่น:

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

แต่ถึงกระนั้นคนที่สามารถอธิบายให้ฉันทำไมมันไม่ทำงานเพื่อโยนchar[256]ไปchar**โดยตรง

คำตอบ:


29

เพราะtestไม่ใช่ตัวชี้

&testทำให้คุณได้ตัวชี้ไปยังอาร์เรย์ชนิดchar (*)[256]ซึ่งไม่เข้ากันได้กับchar**(เนื่องจากอาร์เรย์ไม่ใช่ตัวชี้) ซึ่งส่งผลในลักษณะการทำงานที่ไม่ได้กำหนด


3
แต่ทำไมคอมไพเลอร์ C จึงอนุญาตให้ส่งบางสิ่งบางอย่างchar (*)[256]ไปchar**?
ComFreek

@ComFreek ฉันสงสัยว่าด้วยคำเตือนสูงสุดและ -Werror มันไม่อนุญาต
PiRocks

@ComFreek: มันไม่อนุญาตจริงๆ char**ฉันต้องบังคับให้คอมไพเลอร์ที่จะยอมรับมันอย่างชัดเจนโดยเหวี่ยงลงสู่ หากไม่มีตัวละครนั้นมันก็ไม่ได้คอมไพล์
Andreas

38

testเป็นอาร์เรย์ไม่ใช่ตัวชี้และ&testเป็นตัวชี้ไปยังอาร์เรย์ มันไม่ได้เป็นตัวชี้ไปยังตัวชี้

คุณอาจได้รับแจ้งว่าอาร์เรย์เป็นตัวชี้ แต่สิ่งนี้ไม่ถูกต้อง ชื่อของอาร์เรย์เป็นชื่อของวัตถุทั้งหมด - องค์ประกอบทั้งหมด มันไม่ได้เป็นตัวชี้ไปยังองค์ประกอบแรก ในนิพจน์ส่วนใหญ่อาร์เรย์จะถูกแปลงเป็นตัวชี้ไปยังองค์ประกอบแรกโดยอัตโนมัติ นั่นคือความสะดวกสบายที่มักจะมีประโยชน์ แต่มีข้อยกเว้นสามข้อสำหรับกฎนี้:

  • sizeofอาร์เรย์เป็นตัวถูกดำเนินการของ
  • &อาร์เรย์เป็นตัวถูกดำเนินการของ
  • อาเรย์เป็นสตริงตัวอักษรที่ใช้ในการเริ่มต้นอาร์เรย์

ใน&testอาร์เรย์เป็นตัวถูกดำเนินการ&ดังนั้นการแปลงอัตโนมัติจะไม่เกิดขึ้น ผลมาจากการ&testเป็นตัวชี้ไปยังอาร์เรย์ของ 256 charซึ่งมีประเภทไม่char (*)[256]char **

ที่จะได้รับตัวชี้ไปยังตัวชี้ไปcharจากขั้นแรกคุณจะต้องทำให้ตัวชี้ไปยังtest charตัวอย่างเช่น:

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

วิธีการที่จะคิดเกี่ยวกับเรื่องนี้ก็คือการตระหนักว่าtestชื่อทั้งวัตถุอาร์เรย์ทั้ง char256 มันไม่ได้ตั้งชื่อตัวชี้ดังนั้นในมีตัวชี้ไม่มีที่มีอยู่สามารถนำมาดังนั้นนี้ไม่สามารถผลิต&test char **ในการสร้าง a char **คุณต้องมี a char *ก่อน


1
รายการข้อยกเว้นสามข้อนี้ครบถ้วนสมบูรณ์หรือไม่
Ruslan

8
@Ruslan: ใช่ต่อ C 2018 6.3.2.1 3.
Eric Postpischil

Oh, และใน C11 นอกจากนี้ยังมี_Alignofผู้ประกอบการกล่าวถึงในนอกเหนือไปและsizeof &ฉันสงสัยว่าทำไมพวกเขาลบมันออก ...
Ruslan

@Ruslan: นั่นถูกลบออกเพราะมันเป็นความผิดพลาด _Alignofยอมรับเฉพาะชื่อประเภทเป็นตัวถูกดำเนินการและไม่เคยยอมรับอาร์เรย์หรือวัตถุอื่น ๆ เป็นตัวถูกดำเนินการ (ฉันไม่ทราบว่าทำไมดูเหมือนว่า syntactically และไวยากรณ์มันอาจจะเป็นเหมือนsizeofแต่มันไม่ได้)
Eric Postpischil

6

ประเภทของการมีtest2 char *ดังนั้นประเภทของการ&test2จะเป็นchar **ที่เข้ากันได้กับชนิดของพารามิเตอร์ของx ประเภทของการมี ดังนั้นประเภทของจะได้รับซึ่งเป็นไม่ได้เข้ากันได้กับชนิดของพารามิเตอร์ของprintchar()
testchar [256]&testchar (*)[256]xprintchar()

ผมขอแสดงความแตกต่างในแง่ของการที่อยู่ของและtesttest2

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

เอาท์พุท:

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

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

ชี้ไปที่บันทึกที่นี่:

เอาท์พุท (ที่อยู่หน่วยความจำ) ของtest2และ&test2[0]เป็นตัวเลขchar *เดียวกันและประเภทของพวกเขายังเป็นเหมือนกันซึ่งเป็น
แต่ที่อยู่test2และ&test2แตกต่างกันและประเภทก็แตกต่างกัน
ประเภทของการมีtest2 ประเภทของการมี char *
&test2char **

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

เอาท์พุท (ที่อยู่หน่วยความจำ) ของtest, &testและ&test[0]เป็นตัวเลขเดียวกันแต่ประเภทที่แตกต่างกันของพวกเขาคือ
ประเภทของการมีtest ประเภทของการมี ประเภทของการมี char [256]
&testchar (*) [256]
&test[0]char *

แสดงให้เห็นว่าการส่งออกเป็นเช่นเดียวกับ&test&test[0]

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

ดังนั้นคุณจะได้รับการแบ่งส่วนความผิด


3

คุณไม่สามารถเข้าถึงตัวชี้ไปยังตัวชี้ได้เนื่องจาก&testไม่ใช่ตัวชี้ซึ่งเป็นอาร์เรย์

หากคุณใช้ที่อยู่ของอาเรย์ให้โยนอาเรย์และที่อยู่ของอาเร(void *)ย์แล้วเปรียบเทียบพวกมันจะเทียบเท่ากัน

สิ่งที่คุณทำจริงๆคล้ายกับสิ่งนี้ (อีกครั้งยกเว้นการกำหนดนามแฝงที่เข้มงวด):

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

ซึ่งค่อนข้างผิดอย่างเห็นได้ชัด


3

รหัสของคุณคาดว่าข้อโต้แย้งxของที่จะชี้ไปที่หน่วยความจำที่มีprintchar(char *)

ในการโทรครั้งแรกมันจะชี้ไปยังหน่วยเก็บข้อมูลที่ใช้สำหรับการทำtest2เช่นนั้นและเป็นค่าที่ชี้ไปที่ a (char *)ซึ่งชี้ไปยังหน่วยความจำที่จัดสรรไว้

อย่างไรก็ตามในการโทรครั้งที่สองไม่มีสถานที่ที่(char *)ค่าดังกล่าวอาจถูกจัดเก็บและดังนั้นจึงเป็นไปไม่ได้ที่จะชี้ไปยังหน่วยความจำดังกล่าว การส่งไปยังที่(char **)คุณเพิ่มจะลบข้อผิดพลาดในการคอมไพล์ (เกี่ยวกับการแปลง(char *)เป็น(char **)) แต่จะไม่ทำให้ที่เก็บข้อมูลปรากฏออกมาจากอากาศบาง ๆ เพื่อให้มีการ(char *)เริ่มต้นให้ชี้ไปที่อักขระแรกของการทดสอบ ตัวชี้การชี้ใน C ไม่เปลี่ยนค่าจริงของตัวชี้

เพื่อให้ได้สิ่งที่คุณต้องการคุณต้องทำอย่างชัดเจน:

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

ฉันสมมติว่าตัวอย่างของคุณเป็นการกลั่นโค้ดที่มีขนาดใหญ่กว่ามาก ตัวอย่างเช่นคุณอาจต้องการprintcharเพิ่ม(char *)ค่าที่ค่าที่ส่งผ่านxชี้ไปเพื่อให้มีการพิมพ์อักขระถัดไปในการโทรครั้งถัดไป หากไม่เป็นเช่นนั้นทำไมคุณไม่ส่งต่อตัว(char *)ชี้ไปยังตัวละครที่จะพิมพ์หรือแม้แต่แค่ผ่านตัวละครเอง?


คำตอบที่ดี; ผมเห็นวิธีที่ง่ายที่สุดที่จะทำให้ตรงนี้คือการคิดเกี่ยวกับหรือไม่ว่ามีวัตถุ C char **ที่มีอยู่ของอาร์เรย์คือวัตถุชี้ว่าคุณสามารถใช้ที่อยู่ของผู้ที่จะได้รับ ตัวแปร / วัตถุของอาเรย์เป็นเพียงอาเรย์เท่านั้นโดยที่อยู่นั้นไม่ได้เก็บไว้ที่ใด ไม่มีระดับทางอ้อมเพิ่มเติมในการเข้าถึงพวกเขาซึ่งแตกต่างจากตัวแปรตัวชี้ที่ชี้ไปยังที่เก็บข้อมูลอื่น
Peter Cordes

0

เห็นได้ชัดว่าการใช้ที่อยู่ของtestจะเหมือนกับที่อยู่ของ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;
}

รวบรวมและเรียกใช้:

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

ดังนั้นสาเหตุที่ดีที่สุดของความผิดพลาดในการแบ่งส่วนคือโปรแกรมนี้จะพยายามอ้างอิงที่อยู่แบบสัมบูรณ์0x42(หรือที่รู้จักกันในชื่อ'B') ซึ่งโปรแกรมของคุณไม่ได้รับอนุญาตให้อ่าน

แม้ว่าจะมีคอมไพเลอร์ / เครื่องอื่น ๆ ที่อยู่จะแตกต่างกัน: ลองออนไลน์! แต่คุณจะยังได้รับสิ่งนี้ด้วยเหตุผลบางอย่าง:

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

แต่ที่อยู่ที่ทำให้เกิดความผิดพลาดในการแบ่งกลุ่มอาจแตกต่างกันมาก:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]

1
การที่อยู่ของไม่ได้เช่นเดียวกับการที่อยู่ของtest test[0]อดีตมีประเภทและหลังมีประเภทchar (*)[256] char *พวกเขาไม่เข้ากันและมาตรฐาน C อนุญาตให้พวกเขามีตัวแทนที่แตกต่างกัน
Eric Postpischil

เมื่อทำการฟอร์แมตตัวชี้ด้วย%pควรแปลงเป็นvoid *(อีกครั้งเพื่อเหตุผลด้านความเข้ากันได้และการแสดง)
Eric Postpischil

1
printchar(&test);อาจมีปัญหาสำหรับคุณ แต่พฤติกรรมไม่ได้ถูกกำหนดโดยมาตรฐาน C และผู้คนอาจสังเกตเห็นพฤติกรรมอื่น ๆ ในสถานการณ์อื่น ๆ
Eric Postpischil

Re“ ดังนั้นสาเหตุที่ดีที่สุดของความผิดพลาดในการแบ่งเซ็กเมนต์คือโปรแกรมนี้จะพยายามตรวจสอบที่อยู่สัมบูรณ์ 0x42 (หรือที่รู้จักกันว่า 'B') ซึ่งน่าจะอยู่ในระบบปฏิบัติการ”: หากมีข้อผิดพลาดส่วนที่พยายามอ่าน ตำแหน่งหมายถึงไม่มีสิ่งใดถูกแมปตรงนั้นไม่ใช่ว่าครอบครองโดยระบบปฏิบัติการ (ยกเว้นอาจมีบางสิ่งที่แมปที่นั่นเช่นพูดดำเนินการเท่านั้นโดยไม่มีสิทธิ์อ่าน แต่ไม่น่าเป็นไปได้)
Eric Postpischil

1
&test == &test[0]ละเมิดข้อ จำกัด ใน C 2018 6.5.9 2 เนื่องจากประเภทไม่รองรับ มาตรฐาน C ต้องการการนำไปใช้เพื่อวินิจฉัยการละเมิดนี้และพฤติกรรมที่เป็นผลลัพธ์ไม่ได้ถูกกำหนดโดยมาตรฐาน C นั่นหมายความว่าคอมไพเลอร์ของคุณอาจสร้างโค้ดที่ประเมินว่ามันเท่ากัน แต่คอมไพเลอร์ตัวอื่นอาจไม่ได้
Eric Postpischil

-4

การเป็นตัวแทนของchar [256]คือการดำเนินการขึ้นอยู่กับ char *มันเป็นจะต้องไม่เป็นเช่นเดียวกับ

การคัดเลือก&testประเภทchar (*)[256]เพื่อchar **ให้ได้พฤติกรรมที่ไม่ได้กำหนด

สำหรับคอมไพเลอร์บางตัวอาจทำในสิ่งที่คุณคาดหวัง

แก้ไข:

หลังจากการทดสอบกับ GCC 9.2.1 ก็ปรากฏว่าprintchar((char**)&test)ผ่านในความเป็นจริงเป็นค่าหล่อไปtest มันเป็นเหมือนการเรียนการสอนเป็นchar** printchar((char**)test)ในprintcharฟังก์ชั่นxนี้จะเป็นตัวชี้ไปยังอักขระตัวแรกของการทดสอบอาร์เรย์ไม่ใช่ตัวชี้คู่กับตัวอักษรตัวแรก xผลลัพธ์การยกเลิกการอ้างอิงสองครั้งทำให้เกิดความผิดพลาดในการแบ่งส่วนเนื่องจาก 8 ไบต์แรกของอาร์เรย์ไม่สอดคล้องกับที่อยู่ที่ถูกต้อง

ฉันได้รับพฤติกรรมที่เหมือนกันและผลลัพธ์เมื่อรวบรวมโปรแกรมด้วยเสียงดังกราว 9.0.0-2

สิ่งนี้อาจถือได้ว่าเป็นข้อผิดพลาดของคอมไพเลอร์หรือผลลัพธ์ของพฤติกรรมที่ไม่ได้กำหนดซึ่งผลลัพธ์อาจเป็นเฉพาะของคอมไพเลอร์

พฤติกรรมที่ไม่คาดคิดก็คือรหัส

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

ผลลัพธ์คือ

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

พฤติกรรมแปลก ๆ นั้นคือxและ*xมีค่าเท่ากัน

นี่คือสิ่งที่รวบรวม ฉันสงสัยว่านี่ถูกกำหนดโดยภาษา


1
คุณหมายถึงการเป็นตัวแทนของchar (*)[256]การใช้งานขึ้นอยู่กับ? การเป็นตัวแทนของchar [256]ไม่เกี่ยวข้องในคำถามนี้ - มันเป็นเพียงเศษบิต แต่แม้ว่าคุณหมายถึงการเป็นตัวแทนของตัวชี้ไปยังอาร์เรย์ที่แตกต่างจากการเป็นตัวแทนของตัวชี้ไปยังตัวชี้ที่ยังขาดจุด แม้ว่าพวกเขาจะมีการเป็นตัวแทนเดียวกันรหัสของ OP จะไม่ทำงานเนื่องจากตัวชี้ไปยังตัวชี้สามารถถูกยกเลิกการลงทะเบียนสองครั้งตามที่ทำในprintcharแต่ตัวชี้ไปยังอาร์เรย์ไม่สามารถทำได้โดยไม่คำนึงถึงการเป็นตัวแทน
Eric Postpischil

@EricPostpischil โยนจากchar (*)[256]การchar **เป็นที่ยอมรับโดยคอมไพเลอร์ แต่ไม่ได้ให้ผลที่คาดหวังเพราะไม่ได้เช่นเดียวกับchar [256] char *ฉันสันนิษฐานว่าการเข้ารหัสแตกต่างกันมิฉะนั้นจะให้ผลลัพธ์ที่คาดหวัง
chmike

ฉันไม่รู้ว่าคุณหมายถึงอะไรโดย“ ผลลัพธ์ที่คาดหวัง” สเปคเพียงอย่างเดียวในมาตรฐาน C ของผลลัพธ์ที่ควรจะเป็นหากการจัดตำแหน่งไม่เพียงพอchar **พฤติกรรมจะไม่ได้กำหนดและมิฉะนั้นหากผลลัพธ์ถูกแปลงกลับเป็นchar (*)[256]จะเปรียบเทียบเท่ากับตัวชี้ดั้งเดิม โดย“ผลที่คาดหวัง” คุณอาจหมายถึงว่าถ้า(char **) &testจะถูกแปลงต่อไปจะเปรียบเทียบเท่ากับchar * &test[0]นั่นไม่ใช่ผลลัพธ์ที่ไม่น่าเป็นไปได้ในการนำไปใช้งานซึ่งใช้พื้นที่ที่อยู่แบบแฟลต แต่มันไม่ใช่เรื่องของการเป็นตัวแทนเพียงอย่างเดียว
Eric Postpischil

2
นอกจากนี้“ การส่ง & ทดสอบประเภท char (*) [256] ไปยังถ่าน ** ให้ผลลัพธ์ที่ไม่ได้กำหนด " ไม่ถูกต้อง. C 2018 6.3.2.3 7 อนุญาตให้ตัวชี้ไปยังชนิดวัตถุถูกแปลงเป็นตัวชี้อื่นเป็นชนิดวัตถุ หากตัวชี้ไม่ได้รับการจัดตำแหน่งอย่างถูกต้องสำหรับประเภทที่อ้างอิง (ประเภทที่อ้างอิงของchar **คือchar *) แสดงว่าพฤติกรรมนั้นไม่ได้กำหนดไว้ มิฉะนั้นการแปลงจะถูกกำหนดแม้ว่าค่าจะถูกกำหนดเพียงบางส่วนเท่านั้นตามความคิดเห็นของฉันด้านบน
Eric Postpischil

char (*x)[256]char **xไม่ได้เป็นสิ่งเดียวกับ เหตุผลxและ*xพิมพ์ค่าพอยน์เตอร์เดียวกันคือxเป็นตัวชี้ไปยังอาเรย์ คุณ*x เป็นอาร์เรย์และใช้มันในบริบทตัวชี้สลายตัวกลับไปยังที่อยู่ของอาร์เรย์ ไม่มีข้อผิดพลาดของคอมไพเลอร์ (หรือในสิ่งที่(char **)&testทำ) เพียงยิมนาสติกจิตเล็ก ๆ น้อย ๆ ที่จำเป็นในการคิดออกที่เกิดขึ้นกับประเภท (cdecl อธิบายว่ามันเป็น "ประกาศ x เป็นตัวชี้ไปยังอาร์เรย์ 256 ของถ่าน") แม้แต่การใช้char*เพื่อเข้าถึงการแทนค่าวัตถุของchar**UB ก็ไม่ใช่ มันสามารถนามแฝงอะไรก็ได้
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.