“Int * nums = {5, 2, 1, 4}” gây ra lỗi phân đoạn


81
int *nums = {5, 2, 1, 4};
printf("%d\n", nums[0]);

gây ra một segfault, ngược lại

int nums[] = {5, 2, 1, 4};
printf("%d\n", nums[0]);

không. Hiện nay:

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);

bản in 5.

Dựa trên điều này, tôi đã phỏng đoán rằng ký hiệu khởi tạo mảng, {}, tải dữ liệu này một cách mù quáng vào bất kỳ biến nào ở bên trái. Khi nó là int [], mảng được lấp đầy như mong muốn. Khi nó là int *, con trỏ được lấp đầy bởi 5 và các vị trí bộ nhớ sau nơi con trỏ được lưu trữ sẽ được lấp đầy bởi 2, 1 và 4. Vì vậy, nums [0] cố gắng loại bỏ 5, gây ra lỗi segfault.

Nếu tôi sai, xin vui lòng sửa cho tôi. Và nếu tôi đúng, xin vui lòng giải thích thêm, vì tôi không hiểu tại sao các trình khởi tạo mảng hoạt động theo cách chúng làm.



3
Biên dịch với tất cả các cảnh báo được bật và trình biên dịch của bạn sẽ cho bạn biết điều gì sẽ xảy ra.
Jabberwocky

1
@GSerg Đó không phải là bất kỳ nơi nào gần bản sao. Không có con trỏ mảng trong câu hỏi này. Mặc dù một số câu trả lời trong bài đăng đó tương tự như ở đây.
Lundin

2
@Lundin Mình đã chắc chắn 30% nên không vote chốt, chỉ đăng link thôi.
GSerg

3
Tạo thói quen chạy GCC với -pedantic-errorscờ và xem chẩn đoán. int *nums = {5, 2, 1, 4};không hợp lệ C.
AnT

Câu trả lời:


113

Có một quy tắc (ngu ngốc) trong C nói rằng bất kỳ biến đơn giản nào cũng có thể được khởi tạo bằng danh sách bộ khởi tạo có dấu ngoặc nhọn, giống như thể nó là một mảng.

Ví dụ bạn có thể viết int x = {0};, hoàn toàn tương đương với int x = 0;.

Vì vậy, khi bạn viết, int *nums = {5, 2, 1, 4};bạn thực sự đang đưa một danh sách trình khởi tạo cho một biến con trỏ duy nhất. Tuy nhiên, nó chỉ là một biến duy nhất nên nó sẽ chỉ được gán giá trị đầu tiên là 5, phần còn lại của danh sách bị bỏ qua (thực sự tôi không nghĩ rằng mã có bộ khởi tạo dư thừa thậm chí nên biên dịch bằng một trình biên dịch nghiêm ngặt) - nó không được ghi vào bộ nhớ. Mã tương đương với int *nums = 5;. Có nghĩa là, numsnên chỉ vào địa chỉ 5 .

Tại thời điểm này, bạn hẳn đã nhận được hai cảnh báo / lỗi trình biên dịch:

  • Gán số nguyên cho con trỏ mà không cần ép kiểu.
  • Các phần tử thừa trong danh sách trình khởi tạo.

Và sau đó tất nhiên mã sẽ bị hỏng và cháy vì 5rất có thể đây không phải là địa chỉ hợp lệ mà bạn được phép tham khảo nums[0].

Một lưu ý nhỏ là bạn nên printftrỏ các địa chỉ với trình %pxác định hoặc nếu không thì bạn đang gọi hành vi không xác định.


Tôi không chắc bạn đang cố gắng làm gì ở đây, nhưng nếu bạn muốn đặt con trỏ trỏ vào một mảng, bạn nên làm:

int nums[] = {5, 2, 1, 4};
int* ptr = nums;

// or equivalent:
int* ptr = (int[]){5, 2, 1, 4};

Hoặc nếu bạn muốn tạo một mảng con trỏ:

int* ptr[] = { /* whatever makes sense here */ };

BIÊN TẬP

Sau một số nghiên cứu, tôi có thể nói rằng "danh sách bộ khởi tạo phần tử dư thừa" thực sự không phải là C hợp lệ - nó là một phần mở rộng GCC .

Khởi tạo tiêu chuẩn 6.7.9 cho biết (tôi nhấn mạnh):

2 Không có trình khởi tạo nào cố gắng cung cấp giá trị cho một đối tượng không có trong thực thể đang được khởi tạo.

/ - /

11 Bộ khởi tạo cho một đại lượng vô hướng phải là một biểu thức duy nhất, được đặt trong dấu ngoặc nhọn tùy ý. Giá trị ban đầu của đối tượng là giá trị của biểu thức (sau khi chuyển đổi); áp dụng các ràng buộc và chuyển đổi kiểu tương tự như đối với phép gán đơn giản, lấy kiểu của đại lượng vô hướng là phiên bản không đủ tiêu chuẩn của kiểu đã khai báo của nó.

"Kiểu vô hướng" là một thuật ngữ tiêu chuẩn đề cập đến các biến đơn không thuộc kiểu mảng, cấu trúc hoặc liên hợp (những biến đó được gọi là "kiểu tổng hợp").

Vì vậy, trong tiếng Anh đơn giản, tiêu chuẩn nói: "khi bạn khởi tạo một biến, hãy thoải mái thêm một số dấu ngoặc nhọn xung quanh biểu thức khởi tạo, chỉ vì bạn có thể."


11
Không có gì là "ngu ngốc" về khả năng khởi tạo một đối tượng vô hướng với một giá trị duy nhất kèm theo {}. Ngược lại, nó tạo điều kiện cho một trong những thành ngữ quan trọng và thuận tiện nhất của ngôn ngữ C - { 0 }như là bộ khởi tạo số 0 phổ quát. Mọi thứ trong C đều có thể được khởi tạo bằng 0 thông qua = { 0 }. Điều này rất quan trọng để viết mã không phụ thuộc vào kiểu.
AnT

3
@AnT Không có cái gọi là "bộ khởi tạo số 0 phổ biến". Trong trường hợp tổng hợp, {0}chỉ có nghĩa là khởi tạo đối tượng đầu tiên bằng 0 và khởi tạo phần còn lại của các đối tượng như thể chúng có thời lượng lưu trữ tĩnh. Tôi sẽ nói điều này là bởi sự trùng hợp chứ không phải là ngôn ngữ thiết kế có chủ ý của một số "initializer phổ quát", vì {1}không khởi tạo tất cả các đối tượng 1.
Lundin

3
@Lundin C11 6.5.16.1/1 bao gồm p = 5;(không có trường hợp nào được liệt kê được đáp ứng để gán số nguyên cho con trỏ); và 6.7.9 / 11 nói rằng các ràng buộc để gán cũng được sử dụng để khởi tạo.
MM

4
@Lundin: Vâng, có. Nó hoàn toàn không liên quan đến cơ chế khởi tạo phần nào của đối tượng. Cũng hoàn toàn không liên quan liệu việc {}vô hướng hóa các đại lượng vô hướng có được cho phép vào mục đích cụ thể hay không. Điều quan trọng duy nhất là trình = { 0 }khởi tạo được đảm bảo không khởi tạo toàn bộ đối tượng , đó chính xác là điều đã khiến nó trở thành một trong những thành ngữ cổ điển và thanh lịch nhất của ngôn ngữ C.
AnT

2
@Lundin: Tôi cũng hoàn toàn không rõ nhận xét của bạn {1}có liên quan gì đến chủ đề này. Không ai từng tuyên bố rằng {0}giải thích rằng đó 0là một bộ đa khởi tạo cho mỗi và mọi thành viên của tập hợp.
AnT

28

CẢNH 1

int *nums = {5, 2, 1, 4};    // <-- assign multiple values to a pointer variable
printf("%d\n", nums[0]);    // segfault

Tại sao điều này lại mặc định?

Bạn đã khai báo numslà một con trỏ tới int - numsđược cho là giữ địa chỉ của một số nguyên trong bộ nhớ.

Sau đó, bạn đã cố gắng khởi tạo numsthành một mảng nhiều giá trị. Vì vậy, nếu không đi sâu vào chi tiết, điều này là không chính xác về mặt khái niệm - không có ý nghĩa gì khi gán nhiều giá trị cho một biến được cho là chứa một giá trị. Về vấn đề này, bạn sẽ thấy chính xác hiệu quả tương tự nếu bạn làm điều này:

int nums = {5, 2, 1, 4};    // <-- assign multiple values to an int variable
printf("%d\n", nums);    // also print 5

Trong cả hai trường hợp (gán nhiều giá trị cho một con trỏ hoặc một biến int), điều xảy ra sau đó là biến sẽ nhận giá trị đầu tiên 5, trong khi các giá trị còn lại bị bỏ qua. Mã này tuân thủ nhưng bạn sẽ nhận được cảnh báo cho từng giá trị bổ sung không được phép có trong nhiệm vụ:

warning: excess elements in scalar initializer.

Đối với trường hợp gán nhiều giá trị cho biến con trỏ, chương trình sẽ mặc định khi bạn truy cập nums[0], có nghĩa là bạn đang tham chiếu đến bất cứ thứ gì được lưu trữ trong địa chỉ 5 theo nghĩa đen. Bạn đã không cấp phát bất kỳ bộ nhớ hợp lệ nào cho con trỏ numstrong trường hợp này.

Cần lưu ý rằng không có mặc định nào cho trường hợp gán nhiều giá trị cho biến int (bạn không tham chiếu đến bất kỳ con trỏ không hợp lệ nào ở đây).


KỊCH BẢN 2

int nums[] = {5, 2, 1, 4};

Điều này không mặc định, bởi vì bạn đang phân bổ hợp pháp một mảng 4 int trong ngăn xếp.


KỊCH BẢN 3

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);   // print 5

Điều này không mặc định như mong đợi, bởi vì bạn đang in giá trị của chính con trỏ - KHÔNG phải thứ mà nó đang tham chiếu (là truy cập bộ nhớ không hợp lệ).


Khác

Nó hầu như luôn phải chịu đựng segfault bất cứ khi nào bạn mã hóa giá trị của một con trỏ như thế này (bởi vì nhiệm vụ của hệ điều hành là xác định quá trình nào có thể truy cập vị trí bộ nhớ nào).

int *nums = 5;    // <-- segfault

Vì vậy, một quy tắc chung là luôn khởi tạo một con trỏ đến địa chỉ của một số biến được phân bổ , chẳng hạn như:

int a;
int *nums = &a;

hoặc là,

int a[] = {5, 2, 1, 4};
int *nums = a; 

2
+1 Đó là lời khuyên tốt, nhưng "không bao giờ" thực sự quá mạnh, với các địa chỉ kỳ diệu trong nhiều nền tảng. (Việc sử dụng các bảng không đổi cho những địa chỉ cố định đó không chỉ vào các biến còn tồn tại và do đó vi phạm quy tắc của bạn như đã nêu.) Những thứ cấp thấp như phát triển trình điều khiển giải quyết vấn đề đó khá thường xuyên.
The Nate

3
"Điều này là hợp lệ" - bỏ qua các bộ khởi tạo dư thừa là một phần mở rộng GCC; trong Tiêu chuẩn C không được phép
MM

1
@TheNate - vâng bạn đã chính xác. Tôi đã chỉnh sửa dựa trên nhận xét của bạn - cảm ơn.
artm

@MM - cảm ơn bạn đã chỉ ra điều đó. Tôi đã chỉnh sửa để loại bỏ điều đó.
artm

25

int *nums = {5, 2, 1, 4};là mã không hợp lệ. Có một tiện ích mở rộng GCC xử lý mã này giống như:

int *nums = (int *)5;

cố gắng tạo một con trỏ đến địa chỉ bộ nhớ 5. (Đây có vẻ không phải là một tiện ích mở rộng hữu ích đối với tôi, nhưng tôi đoán cơ sở nhà phát triển muốn nó).

Để tránh hiện tượng này (hoặc ít nhất, có được một cảnh báo), bạn có thể biên dịch trong chế độ tiêu chuẩn, ví dụ -std=c11 -pedantic.

Một dạng mã hợp lệ thay thế sẽ là:

int *nums = (int[]){5, 2, 1, 4};

trỏ đến một ký tự có thể thay đổi có cùng thời lượng lưu trữ với nums. Tuy nhiên, int nums[]phiên bản này nói chung tốt hơn vì nó sử dụng ít dung lượng hơn và bạn có thể sử dụng sizeofđể phát hiện thời lượng của mảng.


Mảng ở dạng phức hợp chữ có được đảm bảo sẽ có thời gian lưu trữ ít nhất là lâu như thời gian tồn tại numskhông?
supercat

@supercat vâng, nó tự động nếu nums là tự động và tĩnh nếu nums tĩnh
MM

@MM: Điều đó có áp dụng ngay cả khi numsmột biến tĩnh được khai báo trong một hàm hay trình biên dịch có quyền giới hạn thời gian tồn tại của mảng đối với thời gian tồn tại của khối bao quanh ngay cả khi nó được gán cho một biến tĩnh?
supercat

@supercat có (bit đầu tiên). Tùy chọn thứ hai có nghĩa là UB vào lần thứ hai hàm được gọi (vì các biến tĩnh chỉ được khởi tạo trong lần gọi đầu tiên)
MM

12
int *nums = {5, 2, 1, 4};

numslà một con trỏ kiểu int. Vì vậy, bạn nên thực hiện điều này đến một số vị trí bộ nhớ hợp lệ. num[0]bạn đang cố gắng bỏ qua một số vị trí bộ nhớ ngẫu nhiên và do đó lỗi phân đoạn.

Có, con trỏ đang giữ giá trị 5 và bạn đang cố gắng bỏ qua nó, đó là hành vi không xác định trên hệ thống của bạn. (Có vẻ như 5không phải là vị trí bộ nhớ hợp lệ trên hệ thống của bạn)

Trong khi

int nums[] = {1,2,3,4};

là một khai báo hợp lệ mà bạn đang nói numslà một mảng kiểu intvà bộ nhớ được cấp phát dựa trên số phần tử được truyền trong quá trình khởi tạo.


1
"Có, con trỏ đang giữ giá trị 5 và bạn đang cố tham khảo nó là hành vi không xác định." Không hề, đó là hành vi hoàn toàn tốt và được xác định rõ ràng. Nhưng trên hệ thống mà OP đang sử dụng, nó không phải là địa chỉ bộ nhớ hợp lệ, do đó xảy ra sự cố.
Lundin

@Lundin Đồng ý. Nhưng tôi nghĩ rằng OP không bao giờ biết 5 là một vị trí bộ nhớ hợp lệ vì vậy tôi đã nói về những dòng đó. Hy vọng bản chỉnh sửa giúp ích
Gopi

Nên như thế này? int *nums = (int[]){5, 2, 1, 4};
Islam Azab,

10

Bằng cách chỉ định {5, 2, 1, 4}

int *nums = {5, 2, 1, 4};

bạn đang gán 5 cho nums(sau một typecast ngầm định từ int đến con trỏ đến int). Bỏ qua nó thực hiện một cuộc gọi truy cập đến vị trí bộ nhớ tại 0x5. Điều đó có thể không được phép cho chương trình của bạn truy cập.

Thử

printf("%p", (void *)nums);
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.