Tại sao tôi không thể truy cập một con trỏ tới con trỏ cho một mảng ngăn xếp?


35

Xin hãy xem đoạn mã sau. Nó cố gắng truyền một mảng như là char**một hàm:

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

Thực tế là tôi chỉ có thể biên dịch nó bằng cách truyền một cách rõ ràng &test2để char**gợi ý rằng mã này là sai.

Tuy nhiên, tôi tự hỏi chính xác những gì sai về nó. Tôi có thể chuyển một con trỏ tới một con trỏ tới một mảng được phân bổ động nhưng tôi không thể chuyển một con trỏ tới một con trỏ cho một mảng trên ngăn xếp. Tất nhiên, tôi có thể dễ dàng giải quyết vấn đề bằng cách trước tiên gán mảng cho một biến tạm thời, như vậy:

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

Tuy nhiên, có thể ai đó giải thích cho tôi lý do tại sao nó không hoạt động để cast char[256]để char**trực tiếp?

Câu trả lời:


29

Bởi vì testkhông phải là một con trỏ.

&testđưa bạn một con trỏ tới mảng, loại char (*)[256], không tương thích với char**(vì một mảng không phải là con trỏ). Điều này dẫn đến hành vi không xác định.


3
Nhưng tại sao các trình biên dịch C sau đó cho phép đi qua một cái gì đó kiểu char (*)[256]để char**?
ComFalet

@ComFalet Tôi nghi ngờ rằng với các cảnh báo tối đa và -Werror, nó không cho phép điều đó.
PiRocks

@ComFalet: Nó không thực sự cho phép nó. Tôi phải buộc trình biên dịch chấp nhận nó bằng cách chuyển nó thành rõ ràng char**. Không có diễn viên đó, nó không biên dịch.
Andreas

38

testlà một mảng, không phải là một con trỏ và &testlà một con trỏ tới mảng. Nó không phải là một con trỏ đến một con trỏ.

Bạn có thể đã được thông báo rằng một mảng là một con trỏ, nhưng điều này không chính xác. Tên của một mảng là tên của toàn bộ đối tượng, tất cả các phần tử. Nó không phải là một con trỏ đến phần tử đầu tiên. Trong hầu hết các biểu thức, một mảng được tự động chuyển đổi thành một con trỏ thành phần tử đầu tiên của nó. Đó là một sự tiện lợi thường hữu ích. Nhưng có ba trường hợp ngoại lệ cho quy tắc này:

  • Mảng là toán hạng của sizeof.
  • Mảng là toán hạng của &.
  • Mảng là một chuỗi ký tự được sử dụng để khởi tạo một mảng.

Trong &test, mảng là toán hạng của &, vì vậy chuyển đổi tự động không xảy ra. Kết quả &testlà một con trỏ tới một mảng 256 char, có kiểu char (*)[256], không char **.

Để có được một con trỏ đến một con trỏ chartừ test, trước tiên bạn cần tạo một con trỏ tới char. Ví dụ:

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

Một cách khác để suy nghĩ về điều này là nhận ra rằng testđặt tên cho toàn bộ đối tượng là toàn bộ mảng 256 char. Nó không đặt tên cho một con trỏ, vì vậy, trong &testđó, không có con trỏ nào có thể lấy địa chỉ, vì vậy điều này không thể tạo ra a char **. Để tạo một char **, trước tiên bạn phải có một char *.


1
Là danh sách ba ngoại lệ đầy đủ?
Ruslan

8
@Ruslan: Có, mỗi C 2018 6.3.2.1 3.
Eric Postpischil

Ồ, và trong C11 cũng có _Alignoftoán tử được đề cập ngoài sizeof&. Tôi tự hỏi tại sao họ lại gỡ bỏ nó ...
Ruslan

@Ruslan: Điều đó đã bị xóa vì đó là một sai lầm. _Alignofchỉ chấp nhận tên loại dưới dạng toán hạng và không bao giờ chấp nhận một mảng hoặc bất kỳ đối tượng nào khác làm toán hạng. (Tôi không biết tại sao; nó có vẻ như về mặt cú pháp và ngữ pháp có thể giống như vậy sizeof, nhưng không phải vậy.)
Eric Postpischil

6

Các loại test2char *. Vì vậy, loại &test2sẽ được char **đó là tương thích với các loại tham số xcủa printchar().
Các loại testchar [256]. Vì vậy, loại &testsẽ được char (*)[256]đó là không tương thích với các loại tham số xcủa printchar().

Hãy để tôi chỉ cho bạn sự khác biệt về địa chỉ 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;
}

Đầu ra:

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

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

Điểm cần lưu ý ở đây:

Đầu ra (bộ nhớ địa chỉ) của test2&test2[0]số lượng tương tự và loại của họ cũng là cùng đó là char *.
Nhưng test2&test2là địa chỉ khác nhau và loại của họ cũng khác nhau.
Các loại test2char *.
Các loại &test2char **.

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

Đầu ra (bộ nhớ địa chỉ) của test, &test&test[0]số lượng tương tự nhưng kiểu của họ là khác nhau .
Các loại testchar [256].
Các loại &testchar (*) [256].
Các loại &test[0]char *.

Như đầu ra cho thấy &testlà giống như &test[0].

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

Do đó bạn đang nhận được lỗi phân khúc.


3

Bạn không thể truy cập một con trỏ tới một con trỏ bởi vì đó &testkhông phải là một con trỏ mà đó là một mảng.

Nếu bạn lấy địa chỉ của một mảng, truyền mảng và địa chỉ của mảng đó (void *), và so sánh chúng, chúng sẽ (chặn các bước con trỏ có thể) tương đương.

Những gì bạn thực sự đang làm tương tự như thế này (một lần nữa, loại bỏ bí danh nghiêm ngặt):

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

Điều này khá rõ ràng là sai.


3

Mã của bạn mong đợi đối số xcủaprintchar để trỏ đến bộ nhớ có chứa một (char *).

Trong cuộc gọi đầu tiên, nó trỏ đến bộ lưu trữ được sử dụng test2và do đó thực sự là một giá trị trỏ đến một(char *) , cái sau trỏ đến bộ nhớ được phân bổ.

Tuy nhiên, trong cuộc gọi thứ hai, không có nơi nào (char *)có thể lưu trữ bất kỳ giá trị nào như vậy và do đó không thể trỏ đến bộ nhớ đó. Diễn viên (char **)mà bạn đã thêm sẽ xóa một lỗi biên dịch (về việc chuyển đổi (char *)sang (char **)) nhưng nó sẽ không làm cho bộ lưu trữ xuất hiện ngoài không khí mỏng để chứa một(char *) khởi tạo để trỏ đến các ký tự thử nghiệm đầu tiên. Đúc con trỏ trong C không thay đổi giá trị thực của con trỏ.

Để có được những gì bạn muốn, bạn phải thực hiện nó một cách rõ ràng:

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

Tôi giả sử ví dụ của bạn là một chưng cất một đoạn mã lớn hơn nhiều; như một ví dụ, có lẽ bạn muốn printchartăng (char *)giá trị mà xgiá trị đã truyền chỉ ra để trong lần gọi tiếp theo, ký tự tiếp theo được in. Nếu đó không phải là trường hợp, tại sao bạn không chuyển một (char *)điểm đến nhân vật sẽ được in, hoặc thậm chí chỉ vượt qua chính nhân vật đó?


Câu trả lời tốt; Tôi đồng ý cách dễ nhất để giữ điều này là suy nghĩ xem liệu có một đối tượng C giữ địa chỉ của mảng hay không, tức là một đối tượng con trỏ mà bạn có thể lấy địa chỉ để lấy a char **. Các biến / đối tượng mảng chỉ đơn giản mảng, với địa chỉ được ẩn, không được lưu trữ ở bất cứ đâu. Không có thêm mức độ gián tiếp để truy cập chúng, không giống như một biến con trỏ trỏ đến bộ lưu trữ khác.
Peter Cordes

0

Rõ ràng, lấy địa chỉ của testcũng giống như lấy địa chỉ của 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;
}

Biên dịch nó và chạy:

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

Vì vậy, nguyên nhân cuối cùng của lỗi phân đoạn là chương trình này sẽ cố gắng hủy bỏ địa chỉ tuyệt đối 0x42(còn được gọi là'B' ), mà chương trình của bạn không được phép đọc.

Mặc dù với một trình biên dịch / máy khác nhau, các địa chỉ sẽ khác nhau: Hãy thử trực tuyến! , nhưng bạn vẫn sẽ nhận được điều này, vì một số lý do:

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

Nhưng địa chỉ gây ra lỗi phân khúc rất có thể khác nhau:

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

1
Lấy địa chỉ của testkhông giống như lấy địa chỉ của test[0]. Cái trước có loại char (*)[256], và cái sau có loại char *. Chúng không tương thích và tiêu chuẩn C cho phép chúng có các biểu diễn khác nhau.
Eric Postpischil

Khi định dạng một con trỏ với %p, nó nên được chuyển đổi thành void *(một lần nữa vì lý do tương thích và đại diện).
Eric Postpischil

1
printchar(&test);có thể sụp đổ cho bạn, nhưng hành vi không được xác định theo tiêu chuẩn C và mọi người có thể quan sát các hành vi khác trong các trường hợp khác.
Eric Postpischil

Vì vậy, nguyên nhân cuối cùng của lỗi phân đoạn là chương trình này sẽ cố gắng hủy bỏ địa chỉ tuyệt đối 0x42 (còn được gọi là 'B'), có thể bị HĐH chiếm giữ. Tên: Nếu có lỗi phân đoạn đang cố đọc một vị trí, nó có nghĩa là không có gì được ánh xạ ở đó, không phải là nó bị chiếm bởi hệ điều hành. (Ngoại trừ có thể có một cái gì đó được ánh xạ ở đó, như, chỉ thực hiện mà không có quyền đọc, nhưng điều đó là không thể.)
Eric Postpischil

1
&test == &test[0]vi phạm các ràng buộc trong C 2018 6.5.9 2 vì các loại không tương thích. Tiêu chuẩn C yêu cầu triển khai để chẩn đoán vi phạm này và hành vi kết quả không được xác định bởi tiêu chuẩn C. Điều đó có nghĩa là trình biên dịch của bạn có thể tạo mã đánh giá chúng bằng nhau, nhưng trình biên dịch khác có thể không.
Eric Postpischil

-4

Các đại diện char [256]là thực hiện phụ thuộc. Nó không được giống nhưchar * .

Đúc &testcác loại char (*)[256]đểchar ** mang lại hành vi không xác định.

Với một số trình biên dịch, nó có thể làm những gì bạn mong đợi, một trình biên dịch khác thì không.

BIÊN TẬP:

Sau khi thử nghiệm với gcc 9.2.1, có vẻ như printchar((char**)&test)thực tế test là giá trị được truyền tới char**. Như thể là hướng dẫn printchar((char**)test). Trong printcharhàm, xlà một con trỏ tới char đầu tiên của kiểm tra mảng, không phải là một con trỏ kép cho ký tự đầu tiên. Một xkết quả khử tham chiếu kép trong lỗi phân đoạn vì 8 byte đầu tiên của mảng không tương ứng với một địa chỉ hợp lệ.

Tôi nhận được chính xác hành vi và kết quả tương tự khi biên dịch chương trình với clang 9.0.0-2.

Đây có thể được coi là một lỗi trình biên dịch, hoặc kết quả của một hành vi không xác định mà kết quả có thể là trình biên dịch cụ thể.

Một hành vi bất ngờ khác là mã

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

Đầu ra là

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Các hành vi kỳ lạ là vậy x*xcó cùng giá trị.

Đây là một điều biên dịch. Tôi nghi ngờ rằng điều này được xác định bởi ngôn ngữ.


1
Bạn có nghĩa là đại diện của char (*)[256]là phụ thuộc thực hiện? Việc trình bày char [256]không liên quan trong câu hỏi này, nó chỉ là một loạt các bit. Nhưng, ngay cả khi bạn có nghĩa là biểu diễn của một con trỏ tới một mảng khác với biểu diễn của một con trỏ thành một con trỏ, điều đó cũng bỏ lỡ điểm. Ngay cả khi chúng có cùng các biểu diễn, mã của OP sẽ không hoạt động, bởi vì con trỏ tới một con trỏ có thể được hủy đăng ký hai lần, như được thực hiện trong printchar, nhưng con trỏ tới một mảng không thể, bất kể biểu diễn.
Eric Postpischil

@EricPostpischil cast từ char (*)[256]đến char **được trình biên dịch chấp nhận, nhưng không mang lại kết quả như mong đợi vì a char [256]không giống như a char *. Tôi giả sử, mã hóa là khác nhau, nếu không nó sẽ mang lại kết quả mong đợi.
chmike

Tôi không biết ý của bạn về kết quả mong đợi. Đặc điểm kỹ thuật duy nhất trong tiêu chuẩn C về kết quả sẽ là gì, nếu căn chỉnh không phù hợp char **, hành vi không được xác định và nếu không, nếu kết quả được chuyển đổi trở lại char (*)[256], nó sẽ so sánh với con trỏ ban đầu. Theo kết quả mong đợi, bạn có thể có nghĩa là, nếu (char **) &testđược chuyển đổi thành a char *, nó sẽ tương đương với &test[0]. Đó không phải là một kết quả không thể xảy ra trong việc triển khai sử dụng một không gian địa chỉ phẳng, nhưng nó không hoàn toàn là vấn đề đại diện.
Eric Postpischil

2
Ngoài ra, Đúc Đúc & thử nghiệm loại char (*) [256] để char ** mang lại hành vi không xác định. không đúng C 2018 6.3.2.3 7 cho phép một con trỏ đến một loại đối tượng được chuyển đổi thành bất kỳ con trỏ nào khác thành một loại đối tượng. Nếu con trỏ không được căn chỉnh chính xác cho loại được tham chiếu (loại được tham chiếu char **char *), thì hành vi không được xác định. Mặt khác, chuyển đổi được xác định, mặc dù giá trị chỉ được xác định một phần, theo nhận xét của tôi ở trên.
Eric Postpischil

char (*x)[256]không phải là điều tương tự như char **x. Lý do x*xin cùng một giá trị con trỏ xlà đơn giản chỉ là một con trỏ tới mảng. Của bạn *x là mảng và sử dụng nó trong ngữ cảnh con trỏ phân rã trở lại địa chỉ của mảng . Không có lỗi trình biên dịch ở đó (hoặc trong những gì (char **)&testhiện tại), chỉ cần một chút thể dục tinh thần cần thiết để tìm ra điều đó đang xảy ra với các loại. (cdecl giải thích nó là "khai báo x là con trỏ tới mảng 256 của char"). Ngay cả việc sử dụng char*để truy cập đại diện đối tượng của một char**UB không; nó có thể bí danh bất cứ điều gì.
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.