Có thể khởi tạo con trỏ C thành NULL không?


90

Tôi đã viết những thứ như

char *x=NULL;

trên giả định rằng

 char *x=2;

sẽ tạo một charcon trỏ đến địa chỉ 2.

Tuy nhiên, trong Hướng dẫn lập trình GNU C, nó nói rằng int *my_int_ptr = 2;lưu trữ giá trị số nguyên 2vào bất kỳ địa chỉ ngẫu nhiên my_int_ptrnào khi nó được cấp phát.

Điều này dường như ngụ ý rằng của tôi char *x=NULLđang gán bất kỳ giá trị nào của NULLép kiểu cho a charlà một địa chỉ ngẫu nhiên nào đó trong bộ nhớ.

Trong khi

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

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

thực tế là in

là NULL

khi tôi biên dịch và chạy nó, tôi lo ngại rằng tôi đang dựa vào hành vi không xác định hoặc ít nhất là hành vi chưa được chỉ định và rằng tôi nên viết

char *x;
x=NULL;

thay thế.


72
Có một sự khác biệt rất khó hiểu giữa những gì int *x = whatever;hiện và những gì int *x; *x = whatever;không. int *x = whatever;thực sự hành xử giống như int *x; x = whatever;, không phải *x = whatever;.
user2357112 hỗ trợ Monica

78
Hướng dẫn này dường như đã sai sự phân biệt khó hiểu đó.
user2357112 hỗ trợ Monica

51
Rất nhiều hướng dẫn chết tiệt trên web! Ngừng đọc ngay lập tức. Chúng tôi thực sự cần một danh sách đen SO nơi chúng ta có thể công khai xấu hổ sách crappy ...
Lundin

9
@MM Điều đó không làm cho nó bớt trở nên tồi tệ hơn trong năm 2017. Với sự phát triển của trình biên dịch và máy tính từ những năm 80, về cơ bản, việc tôi là bác sĩ và đọc những cuốn sách y học được viết trong thế kỷ 18 giống như vậy.
Lundin

13
Tôi không nghĩ rằng đủ tiêu chuẩn hướng dẫn này là " The GNU C Lập trình Hướng dẫn" ...
marcelm

Câu trả lời:


114

Có thể khởi tạo con trỏ C thành NULL không?

TL; DR Vâng, rất nhiều.


Các tuyên bố thực tế thực hiện trên dẫn đọc như

Mặt khác, nếu bạn chỉ sử dụng một phép gán ban đầu duy nhất int *my_int_ptr = 2;, chương trình sẽ cố gắng lấp đầy nội dung của vị trí bộ nhớ được trỏ tới bằng my_int_ptrgiá trị 2. Vì my_int_ptrchứa đầy rác nên nó có thể là bất kỳ địa chỉ nào. [...]

Vâng, họ sai, bạn là đúng.

Đối với câu lệnh, ( bỏ qua, hiện tại, thực tế là chuyển đổi con trỏ thành số nguyên là một hành vi do triển khai xác định )

int * my_int_ptr = 2;

my_int_ptrlà một biến (kiểu con trỏ tới int), nó có một địa chỉ của riêng nó (kiểu: địa chỉ của con trỏ tới số nguyên), bạn đang lưu trữ một giá trị của 2vào địa chỉ đó .

Bây giờ, my_int_ptrlà một kiểu con trỏ, chúng ta có thể nói, nó trỏ đến giá trị của "kiểu" tại vị trí bộ nhớ được trỏ bởi giá trị được giữ trong đó my_int_ptr. Vì vậy, về cơ bản bạn đang gán giá trị của biến con trỏ, không phải giá trị của vị trí bộ nhớ được trỏ tới bởi con trỏ.

Vì vậy, để kết luận

 char *x=NULL;

khởi tạo biến con trỏ xtới NULL, không phải giá trị tại địa chỉ bộ nhớ mà con trỏ trỏ tới .

Điều này giống như

 char *x;
 x = NULL;    

Sự bành trướng:

Bây giờ, đang tuân thủ nghiêm ngặt, một tuyên bố như

 int * my_int_ptr = 2;

là bất hợp pháp, vì nó liên quan đến vi phạm ràng buộc. Để rõ ràng,

  • my_int_ptr là một biến con trỏ, kiểu int *
  • một hằng số nguyên, 2có kiểu int, theo định nghĩa.

và chúng không phải là kiểu "tương thích", vì vậy việc khởi tạo này không hợp lệ vì nó vi phạm các quy tắc của phép gán đơn giản, được đề cập trong chương §6.5.16.1 / P1, được mô tả trong câu trả lời của Lundin .

Trong trường hợp bất kỳ ai quan tâm đến cách khởi tạo được liên kết với các ràng buộc gán đơn giản, trích dẫn C11, chương §6.7.9, P11

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, tùy chọn được đặt trong dấu ngoặc nhọn. 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 giống 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.


@ Random832n Họ sai. Tôi đã trích dẫn phần liên quan trong câu trả lời của mình, vui lòng sửa cho tôi nếu khác. Ồ, và sự nhấn mạnh trong chủ ý.
Sourav Ghosh

"... là bất hợp pháp, vì nó liên quan đến vi phạm ràng buộc. ... một số nguyên theo nghĩa đen, 2 có kiểu int, theo định nghĩa." là có vấn đề. Nghe có vẻ như bởi vì 2là một int, bài tập là một vấn đề. Nhưng nó là nhiều hơn thế. NULLcũng có thể là một int, một int 0. Nó chỉ char *x = 0;là được xác định rõ và char *x = 2;không. 6.3.2.3 Pointers 3 (BTW: C không xác định một literal số nguyên , chỉ có chuỗi chữđen hợp chất . 0Là một hằng số nguyên )
Chux - Khôi phục Monica

@chux Bạn rất đúng, nhưng không phải vậy char *x = (void *)0;, để phù hợp? hay là nó chỉ với các biểu thức khác mang lại giá trị 0?
Sourav Ghosh

10
@SouravGhosh: các hằng số nguyên có giá trị 0là đặc biệt: chúng chuyển đổi ngầm thành con trỏ null tách biệt với các quy tắc thông thường để truyền rõ ràng các biểu thức số nguyên chung sang kiểu con trỏ.
Steve Jessop

1
Ngôn ngữ được mô tả bởi Sách hướng dẫn tham chiếu C 1974 không cho phép các khai báo chỉ định các biểu thức khởi tạo, và việc thiếu các biểu thức như vậy làm cho "việc sử dụng các bản sao khai báo" trở nên thực tế hơn nhiều. Cú pháp int *p = somePtrExpressionIMHO khá khủng khiếp vì có vẻ như nó đang đặt giá trị của *pnhưng thực tế nó đang đặt giá trị của p.
supercat

53

Hướng dẫn sai. Trong ISO C, int *my_int_ptr = 2;là một lỗi. Trong GNU C, nó có nghĩa giống như int *my_int_ptr = (int *)2;. Điều này chuyển đổi số nguyên 2thành một địa chỉ bộ nhớ, theo một cách nào đó được xác định bởi trình biên dịch.

Nó không cố gắng lưu trữ bất cứ thứ gì ở vị trí mà địa chỉ đó gửi đến (nếu có). Nếu bạn tiếp tục viết *my_int_ptr = 5;, thì nó sẽ cố gắng lưu trữ số 5ở vị trí được chỉ định bởi địa chỉ đó.


1
Tôi không biết rằng chuyển đổi số nguyên thành con trỏ được xác định triển khai. Cảm ơn vì thông tin.
taskinoor

1
@taskinoor Xin lưu ý rằng chỉ có một chuyển đổi trong trường hợp bạn ép buộc nó bằng cách cast, như trong câu trả lời này. Nếu không phải cho diễn viên, mã không nên biên dịch.
Lundin

2
@taskinoor: Vâng, các chuyển đổi khác nhau trong C khá khó hiểu. Câu hỏi này có thông tin thú vị về chuyển đổi: C: Khi nào thì truyền giữa các loại con trỏ không phải là hành vi không xác định? .
sleske

17

Để làm rõ lý do tại sao hướng dẫn sai, int *my_int_ptr = 2; là "vi phạm ràng buộc", đó là mã không được phép biên dịch và trình biên dịch phải cung cấp cho bạn chẩn đoán khi gặp phải nó.

Theo 6.5.16.1 Phép gán đơn giản:

Ràng buộc

Một trong những điều sau đây sẽ giữ:

  • toán hạng bên trái có loại số học nguyên tử, đủ điều kiện hoặc không đủ điều kiện, và bên phải có loại số học;
  • toán hạng bên trái có phiên bản nguyên tử, đủ điều kiện hoặc không đủ tiêu chuẩn của cấu trúc hoặc kiểu liên hợp tương thích với kiểu bên phải;
  • toán hạng bên trái có loại con trỏ nguyên tử, đủ điều kiện hoặc không đủ tiêu chuẩn và (xem xét loại toán hạng bên trái sẽ có sau khi chuyển đổi giá trị) cả hai toán hạng đều là con trỏ đến các phiên bản đủ điều kiện hoặc không đủ tiêu chuẩn của các loại tương thích và loại được trỏ đến bên trái có tất cả các vòng loại của loại được trỏ tới bên phải;
  • toán hạng bên trái có loại con trỏ nguyên tử, đủ điều kiện hoặc không đủ tiêu chuẩn và (xem xét loại toán hạng bên trái sẽ có sau khi chuyển đổi giá trị) một toán hạng là con trỏ đến một loại đối tượng và toán hạng kia là con trỏ tới phiên bản đủ điều kiện hoặc không đủ tiêu chuẩn của void, và kiểu được trỏ đến bên trái có tất cả các định nghĩa của kiểu được trỏ tới bên phải;
  • toán hạng bên trái là một con trỏ nguyên tử, đủ điều kiện hoặc không đủ điều kiện và bên phải là hằng số con trỏ null; hoặc là
  • toán hạng bên trái có kiểu nguyên tử _Bool đủ điều kiện hoặc không đủ tiêu chuẩn và bên phải là một con trỏ.

Trong trường hợp này, toán hạng bên trái là một con trỏ không đủ tiêu chuẩn. Không nơi nào nó đề cập rằng toán hạng bên phải được phép là một số nguyên (kiểu số học). Vì vậy, mã vi phạm tiêu chuẩn C.

GCC được biết là hoạt động kém, trừ khi bạn nói rõ ràng nó là một trình biên dịch C tiêu chuẩn. Nếu bạn biên dịch mã dưới dạng -std=c11 -pedantic-errors, nó sẽ đưa ra một chẩn đoán chính xác như nó phải làm.


4
ủng hộ vì đề xuất -pedantic-error. Mặc dù tôi có thể sẽ sử dụng -Wpedantic liên quan.
fagricipni

2
Một ngoại lệ đối với câu lệnh của bạn rằng toán hạng bên phải không được phép là số nguyên: Phần 6.3.2.3 cho biết, "Một biểu thức hằng số nguyên với giá trị 0 hoặc một biểu thức được ép kiểu như vậy void *, được gọi là hằng số con trỏ null." Lưu ý dấu đầu dòng thứ hai đến cuối cùng trong trích dẫn của bạn. Do đó, int* p = 0;là một cách viết hợp pháp int* p = NULL;. Mặc dù cái sau rõ ràng hơn và thông thường hơn.
Davislor

1
Điều này làm cho sự tắc nghẽn bệnh int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;lý cũng hợp pháp.
Davislor

@Davislor được bao phủ bởi gạch đầu dòng 5 trong trích dẫn tiêu chuẩn trong câu trả lời này (đồng ý rằng bản tóm tắt sau đó có lẽ nên đề cập đến nó)
MM

1
@chux Tôi tin rằng một chương trình được định dạng tốt sẽ cần phải chuyển đổi một intptr_tcách rõ ràng thành một trong những kiểu được phép ở phía bên phải. Nghĩa là, void* a = (void*)(intptr_t)b;là hợp pháp theo điểm 4, nhưng (intptr_t)bkhông phải là kiểu con trỏ tương thích, cũng void*không phải là hằng số con trỏ null và void* acũng không phải là kiểu số học _Bool. Tiêu chuẩn nói rằng việc chuyển đổi là hợp pháp, nhưng không phải là nó là ẩn.
Davislor

15

int *my_int_ptr = 2

lưu trữ giá trị số nguyên 2 vào bất kỳ địa chỉ ngẫu nhiên nào trong my_int_ptr khi nó được cấp phát.

Điều này là hoàn toàn sai lầm. Nếu điều này thực sự được viết thì hãy lấy một cuốn sách hoặc hướng dẫn tốt hơn.

int *my_int_ptr = 2 xác định một con trỏ số nguyên trỏ đến địa chỉ 2. Rất có thể bạn sẽ gặp sự cố nếu cố truy cập địa chỉ 2 .

*my_int_ptr = 2, tức là không có inttrong dòng, lưu trữ giá trị hai vào bất kỳ địa chỉ ngẫu nhiên nào my_int_ptrđang trỏ đến. Nói điều này, bạn có thể gán NULLcho một con trỏ khi nó được xác định.char *x=NULL;là hoàn toàn hợp lệ C.

Chỉnh sửa: Trong khi viết bài này, tôi không biết rằng chuyển đổi số nguyên thành con trỏ là hành vi được xác định thực thi. Vui lòng xem câu trả lời hay của @MM và @SouravGhosh để biết chi tiết.


1
Điều đó hoàn toàn sai vì đó là vi phạm ràng buộc chứ không phải vì bất kỳ lý do nào khác. Đặc biệt, điều này không chính xác: "int * my_int_ptr = 2 xác định một con trỏ số nguyên trỏ đến địa chỉ 2".
Lundin

@Lundin: Bản thân cụm từ "không phải vì lý do nào khác" của bạn đã sai và gây hiểu lầm. Nếu bạn khắc phục được sự cố về khả năng tương thích kiểu, bạn vẫn còn thấy thực tế là tác giả của hướng dẫn đang trình bày sai về cách thức hoạt động của các phép gán và khởi tạo con trỏ.
Lightness Races in Orbit

14

Rất nhiều sự nhầm lẫn về con trỏ C xuất phát từ một lựa chọn rất tệ vốn được đưa ra ban đầu liên quan đến phong cách mã hóa, được chứng thực bởi một lựa chọn nhỏ rất tệ trong cú pháp của ngôn ngữ.

int *x = NULL;C là đúng, nhưng nó rất dễ gây hiểu lầm, tôi thậm chí sẽ nói vô nghĩa, và nó đã cản trở việc hiểu ngôn ngữ của nhiều người mới làm quen. Nó khiến người ta nghĩ rằng sau này chúng ta có thể làm được *x = NULL;, điều tất nhiên là không thể. Bạn thấy đấy, kiểu của biến không phải int, và tên của biến cũng không *x, cũng như *khai báo trong khai báo không đóng bất kỳ vai trò chức năng nào khi phối hợp với =. Nó hoàn toàn là khai báo. Vì vậy, điều có ý nghĩa hơn là:

int* x = NULL;cũng chính xác C, mặc dù nó không tuân theo kiểu mã hóa K&R ban đầu. Nó làm cho nó hoàn toàn rõ ràng rằng kiểu là int*và biến con trỏ x, vì vậy nó trở nên rõ ràng ngay cả với người chưa bắt đầu rằng giá trị NULLđang được lưu trữ vào x, đó là một con trỏ tới int.

Hơn nữa, nó làm cho nó dễ dàng hơn để suy ra một quy tắc: khi ngôi sao ở xa tên biến thì đó là một khai báo, trong khi ngôi sao được gắn với tên là tham chiếu con trỏ.

Vì vậy, bây giờ nó trở nên dễ hiểu hơn rất nhiều rằng chúng ta có thể làm được x = NULL;hoặc *x = 2;nói cách khác, nó giúp người mới làm quen dễ dàng hơn trong việc xem cách variable = expressiondẫn đến pointer-type variable = pointer-expressiondereferenced-pointer-variable = expression. (Đối với khởi tạo, bởi 'biểu thức', tôi có nghĩa là 'rvalue'.)

Sự lựa chọn đáng tiếc trong cú pháp của ngôn ngữ là khi khai báo các biến cục bộ, bạn có thể nói int i, *p;cái nào khai báo một số nguyên và một con trỏ tới một số nguyên, vì vậy nó khiến người ta tin rằng cái tên *là một phần hữu ích. Nhưng nó không phải vậy, và cú pháp này chỉ là một trường hợp đặc biệt kỳ quặc, được thêm vào để thuận tiện, và theo ý kiến ​​của tôi, nó đáng lẽ không bao giờ tồn tại, bởi vì nó làm mất hiệu lực của quy tắc mà tôi đã đề xuất ở trên. Theo như tôi biết, không nơi nào khác trong ngôn ngữ cú pháp này có ý nghĩa, nhưng ngay cả khi đúng như vậy, nó chỉ ra sự khác biệt trong cách các loại con trỏ được định nghĩa trong C. Mọi nơi khác, trong khai báo một biến, trong danh sách tham số, trong cấu trúc thành viên, v.v., bạn có thể khai báo con trỏ của mình type* pointer-variablethay vì type *pointer-variable; nó hoàn toàn hợp pháp và có ý nghĩa hơn.


int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Tôi phải đồng ý không đồng ý. It makes one think.... đừng nghĩ nữa, đọc một cuốn sách C trước, không có xúc phạm.
Sourav Ghosh

^^ điều này sẽ có ý nghĩa hoàn hảo đối với tôi. Vì vậy, tôi cho rằng đó là chủ quan.
Mike Nakis

5
@SouravGhosh Theo ý kiến ​​của tôi, tôi nghĩ rằng C nên được thiết kế để int* somePtr, someotherPtrkhai báo hai con trỏ, trên thực tế, tôi đã từng viết int* somePtrnhưng điều đó dẫn đến lỗi bạn mô tả.
fagricipni

1
@fagricipni Tôi đã ngừng sử dụng cú pháp khai báo nhiều biến vì lý do này. Tôi khai báo từng biến một. Nếu tôi thực sự muốn chúng trên cùng một dòng, tôi phân tách chúng bằng dấu chấm phẩy thay vì dấu phẩy. "Nếu chỗ nào không tốt, đừng đến chỗ đó."
Mike Nakis

2
@fagricipni Chà, nếu tôi có thể thiết kế linux từ đầu, tôi đã sử dụng createnó thay vì creat. :) Vấn đề là, nó là như thế nào và chúng ta cần phải uốn nắn bản thân để thích nghi với điều đó. Tất cả chỉ đơn giản là sự lựa chọn cá nhân vào cuối ngày, đồng ý.
Sourav Ghosh

6

Tôi muốn thêm một cái gì đó trực giao vào nhiều câu trả lời xuất sắc. Trên thực tế, việc khởi tạo thành không phải NULLlà một thực tiễn xấu và có thể hữu ích nếu con trỏ đó có thể được sử dụng để lưu trữ một khối bộ nhớ được cấp phát động.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Vì theo tiêu chuẩn ISO-IEC 9899 freeNULLmã số ở trên (hoặc cái gì đó có ý nghĩa hơn dọc theo các dòng giống nhau) là hợp pháp.


5
Việc truyền kết quả của malloc trong C là dư thừa, trừ khi mã C đó cũng phải được biên dịch thành C ++.
cat

Bạn đúng, void*được chuyển đổi khi cần thiết. Nhưng có mã hoạt động với trình biên dịch C và C ++ có thể có lợi ích.
Luca Citi

1
@LucaCiti C và C ++ là các ngôn ngữ khác nhau. Chỉ có lỗi đang chờ bạn nếu bạn cố gắng biên dịch một tệp nguồn được viết cho một tệp bằng trình biên dịch được thiết kế cho tệp kia. Nó giống như cố gắng viết mã C mà bạn có thể biên dịch bằng các công cụ Pascal.
Evil Dog Pie,

1
Lời khuyên tốt. Tôi (cố gắng) luôn khởi tạo hằng số con trỏ của mình cho một cái gì đó. Trong C hiện đại, đây thường có thể là giá trị cuối cùng của chúng và chúng có thể là constcon trỏ được khai báo trong res trung gian , nhưng ngay cả khi một con trỏ cần có thể thay đổi (như con trỏ được sử dụng trong vòng lặp hoặc bằng cách realloc()), hãy đặt nó để NULLbắt lỗi ở nơi nó được sử dụng trước đó nó được đặt với giá trị thực của nó. Trên hầu hết các hệ thống, tham chiếu NULLgây ra lỗi segfault tại điểm bị lỗi (mặc dù có những ngoại lệ), trong khi một con trỏ chưa được khởi tạo chứa rác và việc ghi vào nó sẽ làm hỏng bộ nhớ tùy ý.
Davislor

1
Ngoài ra, rất dễ thấy trong trình gỡ lỗi có chứa một con trỏ NULL, nhưng có thể rất khó để phân biệt một con trỏ rác từ một con trỏ hợp lệ. Vì vậy, sẽ rất hữu ích khi đảm bảo rằng tất cả các con trỏ luôn hợp lệ hoặc NULLngay từ thời điểm khai báo.
Davislor


1

Chính xác.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Chức năng này đúng cho những gì nó làm. Nó gán địa chỉ của 0 cho con trỏ char x. Tức là nó trỏ con trỏ x đến địa chỉ bộ nhớ 0.

Thay thế:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Tôi đoán những gì bạn muốn là:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.

"Nó gán địa chỉ của 0 cho con trỏ char x." -> Có thể. C không xác định giá trị của con trỏ, chỉ điều đó char* x = 0; if (x == 0)sẽ đúng. Con trỏ không nhất thiết phải là số nguyên.
chux - Phục hồi Monica

Nó không 'trỏ con trỏ x đến địa chỉ bộ nhớ 0'. Nó đặt giá trị con trỏ thành một giá trị không hợp lệ không xác định có thể được kiểm tra bằng cách so sánh nó với 0 hoặc NULL. Hoạt động thực tế được xác định bởi triển khai. Không có gì ở đây trả lời câu hỏi thực tế.
Marquis of Lorne
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.