Làm thế nào để so sánh con trỏ làm việc trong C? Bạn có thể so sánh các con trỏ không trỏ đến cùng một mảng không?


33

Trong K & R (Ngôn ngữ lập trình C phiên bản 2) chương 5 tôi đọc phần sau:

Đầu tiên, con trỏ có thể được so sánh trong các trường hợp nhất định. Nếu pqđiểm cho các thành viên của cùng một mảng, quan hệ sau đó thích ==, !=, <, >=vv làm việc đúng cách.

Điều này dường như ngụ ý rằng chỉ con trỏ trỏ đến cùng một mảng có thể được so sánh.

Tuy nhiên khi tôi thử mã này

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 được in ra màn hình.

Trước hết, tôi nghĩ rằng tôi sẽ không xác định được hoặc một số loại hoặc lỗi, bởi vì ptpx không chỉ đến cùng một mảng (ít nhất là theo cách hiểu của tôi).

Cũng pt > pxbởi vì cả hai con trỏ đều trỏ đến các biến được lưu trữ trên ngăn xếp và ngăn xếp phát triển xuống, vì vậy địa chỉ bộ nhớ của tlớn hơn x? Đó là lý do tại sao pt > pxlà đúng?

Tôi càng bối rối hơn khi malloc được đưa vào. Ngoài ra trong K & R ở chương 8.7, phần sau đây được viết:

Tuy nhiên, vẫn còn một giả định rằng con trỏ tới các khối khác nhau được trả về sbrkcó thể được so sánh một cách có ý nghĩa. Điều này không được đảm bảo bởi tiêu chuẩn cho phép so sánh con trỏ chỉ trong một mảng. Do đó, phiên bản mallocnày chỉ có thể mang theo trong số các máy mà việc so sánh con trỏ chung có ý nghĩa.

Tôi không có vấn đề gì khi so sánh các con trỏ chỉ vào không gian được đặt trên heap với các con trỏ trỏ đến các biến stack.

Ví dụ, đoạn mã sau hoạt động tốt, 1được in:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Dựa trên các thử nghiệm của tôi với trình biên dịch của mình, tôi được dẫn dắt để nghĩ rằng bất kỳ con trỏ nào cũng có thể được so sánh với bất kỳ con trỏ nào khác, bất kể chúng trỏ vào đâu. Hơn nữa, tôi nghĩ rằng số học con trỏ giữa hai con trỏ là tốt, bất kể chúng chỉ ở đâu bởi vì số học chỉ sử dụng bộ nhớ địa chỉ lưu trữ con trỏ.

Tuy nhiên, tôi bối rối bởi những gì tôi đang đọc trong K & R.

Lý do tôi hỏi là vì prof của tôi. thực sự làm cho nó một câu hỏi thi. Anh ta đưa ra đoạn mã sau:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

Những đánh giá này để làm gì:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

Câu trả lời là 0, 10 .

(Giáo sư của tôi không bao gồm tuyên bố từ chối trong bài kiểm tra rằng các câu hỏi dành cho môi trường lập trình phiên bản Ubuntu Linux 16.04, 64 bit)

(lưu ý của biên tập viên: nếu SO cho phép nhiều thẻ hơn, phần cuối đó sẽ đảm bảo , và có thể . Nếu điểm của câu hỏi / lớp cụ thể là chi tiết triển khai hệ điều hành cấp thấp, thay vì di động C.)


17
Bạn đang có lẽ khó hiểu những gì là hợp lệ trong Cvới những gì là an toàn trong C. Tuy nhiên, việc so sánh hai con trỏ với cùng loại có thể được thực hiện (ví dụ: kiểm tra sự bằng nhau), bằng cách sử dụng số học con trỏ và so sánh ><chỉ an toàn khi được sử dụng trong một mảng nhất định (hoặc khối bộ nhớ).
Adrian Mole

13
Bên cạnh đó, bạn không nên học C từ K & R. Để bắt đầu, ngôn ngữ đã trải qua rất nhiều thay đổi kể từ đó. Và, thành thật mà nói, mã ví dụ trong đó là từ thời điểm mà sự căng thẳng thay vì dễ đọc được coi trọng.
paxdiablo

5
Không, nó không được đảm bảo để làm việc. Nó có thể thất bại trong thực tế trên các máy có mô hình bộ nhớ được phân đoạn. Xem C có tương đương với std :: less từ C ++ không? Trên hầu hết các máy hiện đại, nó sẽ hoạt động bất chấp UB.
Peter Cordes

6
@Adam: Đóng, nhưng đây thực sự là UB (trừ khi trình biên dịch mà OP đang sử dụng, GCC, không chọn định nghĩa nó. Nó có thể). Nhưng UB không có nghĩa là "chắc chắn bùng nổ"; một trong những hành vi có thể có đối với UB là hoạt động theo cách bạn mong đợi !! Đây là điều làm cho UB rất khó chịu; nó có thể hoạt động ngay trong bản dựng gỡ lỗi và không bật được tối ưu hóa hoặc ngược lại hoặc ngắt tùy thuộc vào mã xung quanh. So sánh các con trỏ khác sẽ vẫn cho bạn một câu trả lời, nhưng ngôn ngữ không xác định câu trả lời đó có nghĩa gì (nếu có gì). Không, sự cố được cho phép. Đó thực sự là UB.
Peter Cordes

3
@Adam: Ồ vâng, không bao giờ phần đầu tiên trong nhận xét của tôi, tôi đã đọc sai của bạn. Nhưng bạn tuyên bố So sánh các con trỏ khác vẫn sẽ cho bạn một câu trả lời . Đo không phải sự thật. Đó sẽ là một kết quả không xác định , không đầy đủ UB. UB tệ hơn nhiều và có nghĩa là chương trình của bạn có thể segfault hoặc SIGILL nếu việc thực thi đạt được câu lệnh đó với các đầu vào đó (tại bất kỳ thời điểm nào trước hoặc sau khi điều đó thực sự xảy ra). .
Peter Cordes

Câu trả lời:


33

Theo tiêu chuẩn C11 , các nhà khai thác quan hệ <, <=, >, và >=chỉ có thể được sử dụng trên các con trỏ tới phần tử của mảng tương tự hoặc đối tượng struct. Điều này được đánh vần trong phần 6.5.8p5:

Khi hai con trỏ được so sánh, kết quả phụ thuộc vào vị trí tương đối trong không gian địa chỉ của các đối tượng được trỏ đến. Nếu hai con trỏ tới các loại đối tượng cả hai đều trỏ đến cùng một đối tượng hoặc cả hai điểm một đi qua phần tử cuối cùng của cùng một đối tượng mảng, chúng sẽ so sánh bằng nhau. Nếu các đối tượng được chỉ ra là thành viên của cùng một đối tượng tổng hợp, các con trỏ tới các thành viên cấu trúc được khai báo sau đó so sánh lớn hơn các con trỏ với các thành viên được khai báo trước đó trong cấu trúc và các con trỏ tới các phần tử mảng có giá trị con lớn hơn so với con trỏ với các phần tử của cùng một mảng với các giá trị đăng ký thấp hơn. Tất cả các con trỏ đến các thành viên của cùng một đối tượng công đoàn so sánh bằng nhau.

Lưu ý rằng mọi so sánh không thỏa mãn yêu cầu này đều gọi hành vi không xác định , nghĩa là (trong số những điều khác) mà bạn không thể phụ thuộc vào kết quả có thể lặp lại.

Trong trường hợp cụ thể của bạn, đối với cả so sánh giữa địa chỉ của hai biến cục bộ và giữa địa chỉ của địa chỉ động và địa chỉ động, thao tác dường như "hoạt động", tuy nhiên kết quả có thể thay đổi bằng cách thay đổi dường như không liên quan đến mã của bạn hoặc thậm chí biên dịch cùng một mã với các cài đặt tối ưu hóa khác nhau. Với hành vi không xác định, chỉ vì mã có thể bị sập hoặc phát sinh lỗi không có nghĩa là nó sẽ .

Ví dụ, bộ xử lý x86 chạy ở chế độ thực 8086 có mô hình bộ nhớ được phân đoạn bằng cách sử dụng phân đoạn 16 bit và bù 16 bit để tạo địa chỉ 20 bit. Vì vậy, trong trường hợp này, một địa chỉ không chuyển đổi chính xác thành một số nguyên.

Các toán tử đẳng thức ==!=tuy nhiên không có hạn chế này. Chúng có thể được sử dụng giữa bất kỳ hai con trỏ đến các loại tương thích hoặc con trỏ NULL. Vì vậy, sử dụng ==hoặc !=trong cả hai ví dụ của bạn sẽ tạo ra mã C hợp lệ.

Tuy nhiên, ngay cả với ==!=bạn có thể nhận được một số kết quả bất ngờ nhưng vẫn được xác định rõ. Xem một so sánh bình đẳng của con trỏ không liên quan đánh giá là đúng? để biết thêm chi tiết về điều này.

Liên quan đến câu hỏi thi do giáo sư của bạn đưa ra, nó đưa ra một số giả định thiếu sót:

  • Một mô hình bộ nhớ phẳng tồn tại trong đó có sự tương ứng 1-1 giữa một địa chỉ và giá trị nguyên.
  • Các giá trị con trỏ được chuyển đổi phù hợp với một kiểu số nguyên.
  • Rằng việc thực hiện chỉ đơn giản coi con trỏ là số nguyên khi thực hiện so sánh mà không khai thác sự tự do được đưa ra bởi hành vi không xác định.
  • Đó là một ngăn xếp được sử dụng và các biến cục bộ được lưu trữ ở đó.
  • Đó là một đống được sử dụng để kéo bộ nhớ được phân bổ từ.
  • Rằng ngăn xếp (và do đó biến cục bộ) xuất hiện ở địa chỉ cao hơn heap (và do đó các đối tượng được phân bổ).
  • Các hằng chuỗi đó xuất hiện ở một địa chỉ thấp hơn sau đó là heap.

Nếu bạn chạy mã này trên một kiến ​​trúc và / hoặc với trình biên dịch không thỏa mãn các giả định này thì bạn có thể nhận được các kết quả rất khác nhau.

Ngoài ra, cả hai ví dụ cũng thể hiện hành vi không xác định khi chúng gọi strcpy, vì toán hạng bên phải (trong một số trường hợp) trỏ đến một ký tự duy nhất và không phải là một chuỗi kết thúc null, dẫn đến hàm đọc qua giới hạn của biến đã cho.


3
@Shisui Ngay cả khi đã cho rằng, bạn vẫn không nên phụ thuộc vào kết quả. Trình biên dịch có thể trở nên rất tích cực khi tối ưu hóa và sẽ sử dụng hành vi không xác định làm cơ hội để làm điều đó. Có thể sử dụng một trình biên dịch khác nhau và / hoặc các cài đặt tối ưu hóa khác nhau có thể tạo ra đầu ra khác nhau.
dbush

2
@Shisui: Nói chung sẽ xảy ra để làm việc trên các máy có mô hình bộ nhớ phẳng, chẳng hạn như x86-64. Một số trình biên dịch cho các hệ thống như vậy thậm chí có thể xác định hành vi trong tài liệu của chúng. Nhưng nếu không, thì hành vi "điên rồ" có thể xảy ra do UB biên dịch theo thời gian biên dịch. (Trong thực tế tôi không nghĩ có ai muốn điều đó vì vậy đó không phải là thứ mà các trình biên dịch chính tìm kiếm và "cố gắng phá vỡ".)
Peter Cordes

1
Giống như nếu trình biên dịch thấy rằng một đường dẫn thực thi sẽ dẫn đến <giữa mallockết quả và biến cục bộ (lưu trữ tự động, tức là ngăn xếp), nó có thể giả định rằng đường dẫn thực thi không bao giờ được thực hiện và chỉ biên dịch toàn bộ hàm thành một ud2lệnh (tăng bất hợp pháp -Xây dựng ngoại lệ mà kernel sẽ xử lý bằng cách cung cấp SIGILL cho tiến trình). GCC / clang thực hiện điều này trong thực tế cho các loại UB khác, như rơi ra khỏi sự kết thúc của một voidchức năng. godbolt.org có vẻ không ổn ngay bây giờ, nhưng hãy thử sao chép / dán int foo(){int x=2;}và lưu ý việc thiếu mộtret
Peter Cordes

4
@Shisui: TL: DR: nó không phải là C di động, mặc dù thực tế là nó hoạt động tốt trên x86-64 Linux. Tuy nhiên, việc đưa ra các giả định về kết quả so sánh chỉ là điên rồ. Nếu bạn không ở trong luồng chính, ngăn xếp luồng của bạn sẽ được phân bổ động bằng cách sử dụng cùng một cơ chế mallocsử dụng để lấy thêm bộ nhớ từ HĐH, vì vậy không có lý do gì để cho rằng các vars cục bộ (ngăn xếp luồng) của bạn được mallocphân bổ động lưu trữ.
Peter Cordes

2
@PeterCordes: Điều cần thiết là nhận ra các khía cạnh khác nhau của hành vi là "được xác định tùy chọn", sao cho việc triển khai có thể định nghĩa chúng hoặc không, trong lúc rảnh rỗi, nhưng phải chỉ ra theo kiểu có thể kiểm tra (ví dụ: macro được xác định trước) nếu chúng không làm như vậy. Ngoài ra, thay vì mô tả rằng bất kỳ tình huống nào mà hiệu ứng của tối ưu hóa có thể quan sát được là "Hành vi không xác định", sẽ hữu ích hơn nhiều khi nói rằng các trình tối ưu hóa có thể coi các khía cạnh nhất định của hành vi là "không thể quan sát được" nếu chúng chỉ ra rằng chúng làm như vậy. Ví dụ, được đưa ra int x,y;, một triển khai ...
supercat

12

Vấn đề chính với việc so sánh các con trỏ với hai mảng riêng biệt cùng loại là các mảng không cần phải được đặt ở một vị trí tương đối cụ thể - một mảng có thể kết thúc trước và sau mảng kia.

Trước hết, tôi nghĩ rằng tôi sẽ không xác định được hoặc một số loại hoặc lỗi, bởi vì một px không trỏ đến cùng một mảng (ít nhất là theo cách hiểu của tôi).

Không, kết quả phụ thuộc vào việc thực hiện và các yếu tố không thể đoán trước khác.

Cũng là pt> px vì cả hai con trỏ đều trỏ đến các biến được lưu trữ trên ngăn xếp và ngăn xếp phát triển xuống, vì vậy địa chỉ bộ nhớ của t lớn hơn x? Đó là lý do tại sao pt> px là đúng?

Không nhất thiết phải là một chồng . Khi nó tồn tại, nó không cần phải phát triển xuống. Nó có thể lớn lên. Nó có thể không tiếp giáp theo một cách kỳ quái nào đó.

Hơn nữa, tôi nghĩ rằng số học con trỏ giữa hai con trỏ là tốt, bất kể chúng chỉ ở đâu bởi vì số học chỉ sử dụng bộ nhớ địa chỉ lưu trữ con trỏ.

Chúng ta hãy xem đặc tả C , §6.5.8 trên trang 85, thảo luận về các toán tử quan hệ (tức là các toán tử so sánh bạn đang sử dụng). Lưu ý rằng điều này không áp dụng cho trực tiếp !=hoặc ==so sánh.

Khi hai con trỏ được so sánh, kết quả phụ thuộc vào vị trí tương đối trong không gian địa chỉ của các đối tượng được trỏ đến. ... Nếu các đối tượng được trỏ đến là thành viên của cùng một đối tượng tổng hợp, ... con trỏ tới các phần tử mảng có giá trị chỉ số lớn hơn so với con trỏ với các phần tử của cùng một mảng có giá trị chỉ số thấp hơn.

Trong tất cả các trường hợp khác, hành vi là không xác định.

Câu cuối cùng là quan trọng. Trong khi tôi cắt giảm một số trường hợp không liên quan để tiết kiệm không gian, có một trường hợp quan trọng đối với chúng tôi: hai mảng, không phải là một phần của cùng một đối tượng cấu trúc / tổng hợp 1 và chúng tôi đang so sánh các con trỏ với hai mảng đó. Đây là hành vi không xác định .

Trong khi trình biên dịch của bạn chỉ chèn một số loại lệnh máy CMP (so sánh) để so sánh số lượng con trỏ và bạn đã gặp may mắn ở đây, thì UB là một con thú khá nguy hiểm. Nghĩa đen là bất cứ điều gì có thể xảy ra - trình biên dịch của bạn có thể tối ưu hóa toàn bộ chức năng bao gồm các tác dụng phụ có thể nhìn thấy. Nó có thể sinh ra quỷ mũi.

1 Con trỏ vào hai mảng khác nhau là một phần của cùng một cấu trúc có thể được so sánh, vì điều này nằm trong mệnh đề trong đó hai mảng là một phần của cùng một đối tượng tổng hợp (cấu trúc).


1
Quan trọng hơn, với txđược xác định trong cùng một hàm, không có lý do nào để giả sử bất cứ điều gì về cách trình biên dịch nhắm mục tiêu x86-64 sẽ bố trí các địa phương trong khung ngăn xếp cho hàm này. Ngăn xếp tăng dần xuống không liên quan gì đến thứ tự khai báo các biến trong một hàm. Ngay cả trong các chức năng riêng biệt, nếu người ta có thể nội tuyến vào người khác thì người địa phương của chức năng "con" vẫn có thể kết hợp với cha mẹ.
Peter Cordes

1
trình biên dịch của bạn có thể tối ưu hóa toàn bộ chức năng bao gồm các tác dụng phụ có thể nhìn thấy Không phải là nói quá: đối với các loại UB khác (như rơi ra khỏi phần cuối của voidhàm không ) g ++ và clang ++ thực sự làm điều đó trong thực tế: godbolt.org/z/g5vesB chúng giả định rằng đường dẫn thực thi không được thực hiện vì nó dẫn đến UB và biên dịch bất kỳ khối cơ bản nào như vậy thành một lệnh bất hợp pháp. Hoặc không có hướng dẫn nào cả, chỉ cần âm thầm rơi vào bất cứ thứ gì tiếp theo nếu chức năng đó được gọi. (Vì một số lý do gcckhông làm điều này, chỉ g++).
Peter Cordes

6

Rồi hỏi cái gì

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Đánh giá để. Câu trả lời là 0, 1 và 0.

Những câu hỏi này giảm xuống:

  1. Là đống trên hoặc dưới ngăn xếp.
  2. Là heap trên hoặc dưới phần chuỗi ký tự của chương trình.
  3. giống như [1].

Và câu trả lời cho cả ba là "thực hiện được xác định". Câu hỏi của bạn là không có thật; họ đã dựa trên bố cục unix truyền thống:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

nhưng một số thống nhất hiện đại (và các hệ thống thay thế) không phù hợp với những truyền thống đó. Trừ khi họ mở đầu câu hỏi bằng "kể từ năm 1992"; đảm bảo cho -1 trên eval.


3
Không xác định thực hiện, không xác định! Hãy nghĩ về nó theo cách này, trước đây có thể khác nhau giữa các lần thực hiện nhưng việc triển khai nên ghi lại cách quyết định hành vi. Điều thứ hai có nghĩa là hành vi có thể thay đổi theo bất kỳ cách nào và việc triển khai không phải nói với bạn ngồi xổm :-)
paxdiablo

1
@paxdiablo: Theo Rationale của các tác giả của Tiêu chuẩn, "Hành vi không xác định ... cũng xác định các khu vực có thể mở rộng ngôn ngữ phù hợp: người triển khai có thể tăng ngôn ngữ bằng cách cung cấp định nghĩa về hành vi không xác định chính thức." Cơ sở lý luận cho biết thêm "Mục tiêu là mang đến cho lập trình viên cơ hội chiến đấu để tạo ra các chương trình C mạnh mẽ cũng có tính di động cao, mà dường như không hạ thấp các chương trình C hoàn toàn hữu ích mà không thể mang theo được, do đó, trạng từ nghiêm ngặt." Các nhà văn trình biên dịch thương mại hiểu điều này, nhưng một số nhà văn trình biên dịch khác thì không.
supercat

Có một khía cạnh thực hiện khác được xác định; so sánh con trỏ được , do đó tùy thuộc vào máy / os / trình biên dịch, một số địa chỉ có thể được hiểu là âm. Ví dụ: một máy 32 bit đặt ngăn xếp ở 0xc << 28, có thể sẽ hiển thị các biến tự động ở địa chỉ bên cho thuê hơn heap hoặc Rodata.
mevets

1
@mevets: Tiêu chuẩn có chỉ định bất kỳ tình huống nào trong đó việc ký kết các con trỏ trong so sánh sẽ có thể quan sát được không? Tôi hy vọng rằng nếu một nền tảng 16 bit cho phép các đối tượng lớn hơn 32768 byte và arr[]là một đối tượng như vậy ,, thì Tiêu chuẩn sẽ bắt buộc arr+32768so sánh lớn hơn arrngay cả khi so sánh con trỏ đã ký sẽ báo cáo khác.
supercat

Tôi không biết; tiêu chuẩn C đang quay quanh vòng tròn thứ chín của Dante, cầu nguyện cho cái chết êm dịu. OP đặc biệt tham khảo K & R và một câu hỏi thi. #UB là những mảnh vỡ từ một nhóm làm việc lười biếng.
mevets

1

Trên hầu hết mọi nền tảng hiện đại từ xa, con trỏ và số nguyên có mối quan hệ trật tự đẳng cấu và con trỏ để phân biệt các đối tượng không được xen kẽ. Hầu hết các trình biên dịch hiển thị thứ tự này cho các lập trình viên khi tối ưu hóa bị vô hiệu hóa, nhưng Tiêu chuẩn không phân biệt giữa các nền tảng có thứ tự như vậy và những thứ không và không yêu cầu bất kỳ triển khai nào đưa ra thứ tự như vậy cho lập trình viên ngay cả trên các nền tảng sẽ định nghĩa nó Do đó, một số người viết trình biên dịch thực hiện các loại tối ưu hóa và "tối ưu hóa" khác nhau dựa trên giả định rằng mã sẽ không bao giờ so sánh sử dụng các toán tử quan hệ trên các con trỏ với các đối tượng khác nhau.

Theo Cơ sở lý luận được công bố, các tác giả của Tiêu chuẩn dự định rằng việc triển khai sẽ mở rộng ngôn ngữ bằng cách chỉ định cách họ sẽ hành xử trong các tình huống mà Tiêu chuẩn mô tả là "Hành vi không xác định" (nghĩa là Tiêu chuẩn không áp dụng yêu cầu nào ) khi làm như vậy sẽ hữu ích và thiết thực , nhưng một số người viết trình biên dịch thà cho rằng các chương trình sẽ không bao giờ cố gắng hưởng lợi từ bất cứ điều gì ngoài những gì Tiêu chuẩn bắt buộc, hơn là cho phép các chương trình khai thác hữu ích các hành vi mà nền tảng có thể hỗ trợ mà không phải trả thêm phí.

Tôi không biết bất kỳ trình biên dịch được thiết kế thương mại nào làm bất kỳ điều gì kỳ lạ với so sánh con trỏ, nhưng khi trình biên dịch chuyển sang LLVM phi thương mại cho phần cuối của chúng, chúng ngày càng có khả năng xử lý mã vô nghĩa mà hành vi của nó đã được chỉ định trước đó trình biên dịch cho nền tảng của họ. Hành vi như vậy không giới hạn ở các nhà khai thác quan hệ, nhưng thậm chí có thể ảnh hưởng đến sự bình đẳng / bất bình đẳng. Ví dụ, mặc dù Tiêu chuẩn chỉ định rằng so sánh giữa một con trỏ với một đối tượng và một con trỏ "vừa qua" với một đối tượng có trước ngay lập tức sẽ so sánh các trình biên dịch dựa trên gcc và LLVM có xu hướng tạo mã vô nghĩa nếu các chương trình thực hiện như vậy so sánh.

Để làm ví dụ về một tình huống trong đó so sánh bình đẳng hành xử vô nghĩa trong gcc và clang, hãy xem xét:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Cả clang và gcc sẽ tạo mã sẽ luôn trả về 4 ngay cả khi xlà mười phần tử, yngay lập tức theo sau nó và ibằng không dẫn đến sự so sánh là đúng và p[0]được viết với giá trị 1. Tôi nghĩ điều gì xảy ra là một lần viết lại tối ưu hóa chức năng như thể *p = 1;được thay thế bằng x[10] = 1;. Mã sau sẽ tương đương nếu trình biên dịch diễn giải *(x+10)tương đương *(y+i), nhưng không may, giai đoạn tối ưu hóa xuôi dòng nhận ra rằng quyền truy cập x[10]sẽ chỉ được xác định nếu xcó ít nhất 11 phần tử, khiến cho quyền truy cập đó không thể ảnh hưởng y.

Nếu trình biên dịch có thể có được "sáng tạo" đó với kịch bản bình đẳng con trỏ được Tiêu chuẩn mô tả, tôi sẽ không tin họ sẽ không sáng tạo hơn nữa trong trường hợp Tiêu chuẩn không áp đặt các yêu cầu.


0

Thật đơn giản: So sánh các con trỏ không có ý nghĩa vì các vị trí bộ nhớ cho các đối tượng không bao giờ được đảm bảo theo cùng thứ tự như bạn đã khai báo chúng. Ngoại lệ là mảng. & mảng [0] thấp hơn & mảng [1]. Đó là những gì K & R chỉ ra. Trong thực tế cấu trúc địa chỉ thành viên cũng theo thứ tự bạn khai báo chúng theo kinh nghiệm của tôi. Không có gì đảm bảo về điều đó .... Một ngoại lệ khác là nếu bạn so sánh một con trỏ bằng nhau. Khi một con trỏ bằng với một con trỏ khác, bạn biết nó trỏ đến cùng một đối tượng. Cái gì cũng được. Đề thi xấu nếu bạn hỏi tôi Tùy thuộc vào Ubuntu Linux 16.04, môi trường lập trình phiên bản 64 bit cho một câu hỏi thi? Có thật không ?


Về mặt kỹ thuật, các mảng không phải là thực sự là một ngoại lệ vì bạn không khai báo arr[0], arr[1], vv riêng. Bạn tuyên bố arrmột cách tổng thể vì vậy việc sắp xếp các phần tử mảng riêng lẻ là một vấn đề khác với vấn đề được mô tả trong câu hỏi này.
paxdiablo

1
Các yếu tố cấu trúc được đảm bảo theo thứ tự, đảm bảo rằng người ta có thể sử dụng memcpyđể sao chép một phần liền kề của cấu trúc và ảnh hưởng đến tất cả các yếu tố trong đó và không ảnh hưởng đến bất kỳ điều gì khác. Tiêu chuẩn cẩu thả về thuật ngữ về loại số học con trỏ có thể được thực hiện với các cấu trúc hoặc malloc()lưu trữ được phân bổ. Các offsetofvĩ mô sẽ là khá vô dụng nếu người ta có thể không cùng một loại con trỏ số học với các byte của một struct như với một char[], nhưng tiêu chuẩn không rõ ràng nói rằng các byte của một struct là (hoặc có thể được sử dụng như là) một đối tượng mảng.
supercat

-4

Thật là một câu hỏi khiêu khích!

Ngay cả chức năng quét lướt qua các câu trả lời và ý kiến trong chủ đề này sẽ tiết lộ cách dễ gây xúc động truy vấn dường như đơn giản và thẳng về phía trước của bạn hóa ra là.

Nó không đáng ngạc nhiên.

Inarguably, hiểu lầm xung quanh khái niệm và sử dụng của con trỏ đại diện cho một chủ yếu nguyên nhân nghiêm trọng thất bại trong lập trình nói chung.

Nhận thức về thực tế này là dễ thấy trong tính phổ biến của các ngôn ngữ được thiết kế đặc biệt để giải quyết, và tốt nhất là để tránh các thách thức con trỏ giới thiệu hoàn toàn. Hãy nghĩ về C ++ và các dẫn xuất khác của C, Java và các mối quan hệ của nó, Python và các tập lệnh khác - chỉ đơn thuần là những thứ nổi bật và phổ biến hơn, và ít nhiều ra lệnh xử lý vấn đề nghiêm trọng.

Phát triển sự hiểu biết sâu sắc hơn về các nguyên tắc cơ bản, do đó phải phù hợp với mọi cá nhân khao khát sự xuất sắc trong lập trình - đặc biệt là ở cấp độ hệ thống .

Tôi tưởng tượng đây là chính xác những gì giáo viên của bạn có nghĩa là để chứng minh.

Và bản chất của C làm cho nó trở thành một phương tiện thuận tiện cho việc khám phá này. Ít rõ ràng hơn lắp ráp - mặc dù có lẽ dễ hiểu hơn - và vẫn rõ ràng hơn nhiều so với các ngôn ngữ dựa trên sự trừu tượng hóa sâu hơn của môi trường thực thi.

Được thiết kế để tạo điều kiện dịch thuật xác định ý định của lập trình viên thành các hướng dẫn mà máy móc có thể hiểu, C là ngôn ngữ cấp hệ thống . Mặc dù được phân loại là cấp cao, nó thực sự thuộc về loại 'trung bình'; nhưng vì không tồn tại như vậy, nên chỉ định 'hệ thống' phải đủ.

Đặc tính này phần lớn chịu trách nhiệm biến nó thành ngôn ngữ được lựa chọn cho trình điều khiển thiết bị , mã hệ điều hành và triển khai nhúng . Hơn nữa, một sự thay thế xứng đáng được ưa chuộng trong các ứng dụng mà hiệu quả tối ưu là tối quan trọng; trong đó điều đó có nghĩa là sự khác biệt giữa sự sống còn và sự tuyệt chủng, và do đó là một điều cần thiết trái ngược với sự xa xỉ. Trong những trường hợp như vậy, sự tiện lợi hấp dẫn của tính di động sẽ mất hết sức hấp dẫn và việc chọn hiệu suất thiếu ánh sáng của mẫu số ít phổ biến nhất trở thành một lựa chọn bất lợi không thể tưởng tượng được .

Điều làm cho C - và một số dẫn xuất của nó - khá đặc biệt, là nó cho phép người dùng kiểm soát hoàn toàn - khi đó là điều họ mong muốn - mà không áp đặt các trách nhiệm liên quan lên họ khi họ không làm. Tuy nhiên, nó không bao giờ cung cấp nhiều hơn các cách điện mỏng nhất từ máy , trong đó sử dụng đúng cách đòi hỏi sự hiểu biết chính xác về khái niệm con trỏ .

Về bản chất, câu trả lời cho câu hỏi của bạn rất đơn giản và thỏa mãn ngọt ngào - để xác nhận những nghi ngờ của bạn. Cung cấp , tuy nhiên, một gắn các điều kiện tiên quyết quan trọng để mỗi khái niệm trong bản Tuyên Bố này:

  • Các hành vi kiểm tra, so sánh và thao tác con trỏ luôn luôn và nhất thiết có giá trị, trong khi các kết luận rút ra từ kết quả phụ thuộc vào tính hợp lệ của các giá trị được chứa, và do đó không cần phải có.

Cái trước luôn an toàncó khả năng thích hợp , trong khi cái trước chỉ có thể là đúng khi nó được thiết lậpan toàn . Đáng ngạc nhiên - với một số người - vì vậy việc thiết lập tính hợp lệ của cái sau phụ thuộcđòi hỏi cái trước.

Tất nhiên, một phần của sự nhầm lẫn xuất phát từ hiệu ứng của đệ quy vốn có trong nguyên tắc của một con trỏ - và những thách thức đặt ra trong nội dung khác biệt với địa chỉ.

Bạn đã phỏng đoán khá chính xác ,

Tôi đang bị dẫn đến việc nghĩ rằng bất kỳ con trỏ nào cũng có thể được so sánh với bất kỳ con trỏ nào khác, bất kể chúng trỏ vào đâu. Hơn nữa, tôi nghĩ rằng số học con trỏ giữa hai con trỏ là tốt, bất kể chúng chỉ ở đâu bởi vì số học chỉ sử dụng bộ nhớ địa chỉ lưu trữ con trỏ.

Và một số người đóng góp đã khẳng định: con trỏ chỉ là con số. Đôi khi một cái gì đó gần hơn với số phức , nhưng vẫn không nhiều hơn số.

Sự thú vị thú vị trong đó sự tranh chấp này đã được nhận ở đây cho thấy nhiều hơn về bản chất con người hơn là lập trình, nhưng vẫn đáng lưu ý và công phu. Có lẽ chúng ta sẽ làm như vậy sau ...

Khi một bình luận bắt đầu gợi ý; tất cả sự nhầm lẫn và cấu thành này xuất phát từ nhu cầu phân biệt những gì hợp lệ từ những gì an toàn , nhưng đó là một sự đơn giản hóa. Chúng ta cũng phải phân biệt đâu là chức năng và đâu là đáng tin cậy , đâu là thực tế và những gì có thể phù hợp và hơn thế nữa: những gì phù hợp trong một hoàn cảnh cụ thể với những gì có thể phù hợp theo nghĩa chung hơn . Chưa kể; sự khác biệt giữa sự phù hợpquyền sở hữu .

Để đạt được điều đó, trước tiên chúng ta cần đánh giá chính xác con trỏ là gì .

  • Bạn đã chứng minh một khái niệm vững chắc về khái niệm này, và giống như một số người khác có thể thấy những minh họa này đơn giản hóa, nhưng mức độ nhầm lẫn rõ ràng ở đây đòi hỏi sự đơn giản như vậy trong việc làm rõ.

Như nhiều người đã chỉ ra: con trỏ thuật ngữ chỉ là một tên đặc biệt cho những gì đơn giản là một chỉ mục , và do đó không có gì nhiều hơn bất kỳ số nào khác .

Điều này nên đã được tự hiển nhiên trong việc xem xét thực tế là tất cả các máy tính chủ đạo hiện đại là máy nhị phânnhất thiết phải làm việc độc quyền với và số . Điện toán lượng tử có thể thay đổi điều đó, nhưng điều đó rất khó xảy ra, và nó chưa đến tuổi.

Về mặt kỹ thuật, như bạn đã lưu ý, con trỏđịa chỉ chính xác hơn ; một cái nhìn sâu sắc rõ ràng giới thiệu một cách tự nhiên sự tương đồng bổ ích của việc tương quan chúng với 'địa chỉ' của những ngôi nhà, hoặc những mảnh đất trên đường phố.

  • Trong mô hình bộ nhớ phẳng : toàn bộ bộ nhớ hệ thống được sắp xếp theo một chuỗi tuyến tính duy nhất: tất cả các ngôi nhà trong thành phố nằm trên cùng một con đường và mỗi ngôi nhà chỉ được xác định duy nhất bởi số lượng của nó. Rất đơn giản.

  • Trong các sơ đồ được phân đoạn : một tổ chức phân cấp của các con đường được đánh số được giới thiệu ở trên các ngôi nhà được đánh số để các địa chỉ tổng hợp được yêu cầu.

    • Một số triển khai vẫn còn phức tạp hơn và tổng số 'đường' riêng biệt không cần phải kết hợp với một chuỗi liền kề, nhưng không có gì thay đổi bất cứ điều gì về bên dưới.
    • Chúng tôi nhất thiết có thể phân tách mọi liên kết phân cấp như vậy trở lại thành một tổ chức phẳng. Tổ chức càng phức tạp, chúng ta sẽ càng phải nhảy qua nhiều vòng để làm như vậy, nhưng nó phải có thể. Thật vậy, điều này cũng áp dụng cho 'chế độ thực' trên x86.
    • Mặt khác, việc ánh xạ các liên kết đến các vị trí sẽ không mang tính phỏng đoán , vì việc thực thi đáng tin cậy - ở cấp độ hệ thống - đòi hỏi nó PHẢI .
      • nhiều địa chỉ không được ánh xạ tới các vị trí bộ nhớ số ít và
      • địa chỉ số ít phải không bao giờ ánh xạ tới nhiều vị trí bộ nhớ.

Đưa chúng ta đến bước ngoặt xa hơn biến câu hỏi hóc búa thành một mớ phức tạp hấp dẫn như vậy . Ở trên, nó đã được thiết kế để gợi ý rằng con trỏ địa chỉ, vì mục đích đơn giản và rõ ràng. Tất nhiên, điều này là không chính xác. Một con trỏ không một địa chỉ; một con trỏ là một tham chiếu đến một địa chỉ , nó chứa một địa chỉ . Giống như phong bì thể thao một tài liệu tham khảo cho ngôi nhà. Suy ngẫm về điều này có thể khiến bạn nhìn thoáng qua ý nghĩa của gợi ý đệ quy có trong khái niệm này. Vẫn; chúng tôi chỉ có rất nhiều từ và nói về địa chỉ của các tham chiếu đến địa chỉvà như vậy, sớm ngăn chặn hầu hết các bộ não ở một ngoại lệ mã không hợp lệ . Và phần lớn, ý định dễ dàng được thu thập từ bối cảnh, vì vậy chúng ta hãy trở lại đường phố.

Nhân viên bưu điện ở thành phố tưởng tượng này của chúng ta rất giống với những người chúng ta tìm thấy trong thế giới 'thực'. Không ai có khả năng bị đột quỵ khi bạn nói chuyện hoặc hỏi thăm về một địa chỉ không hợp lệ , nhưng mỗi người cuối cùng sẽ chùn bước khi bạn yêu cầu họ hành động dựa trên thông tin đó.

Giả sử chỉ có 20 ngôi nhà trên đường phố số ít của chúng tôi. Giả vờ thêm rằng một số linh hồn sai lầm, hoặc mắc chứng khó đọc đã gửi một lá thư, một điều rất quan trọng, đến số 71. Bây giờ, chúng ta có thể hỏi người vận chuyển Frank của chúng tôi, liệu có địa chỉ như vậy không, và anh ta sẽ báo cáo một cách đơn giản và bình tĩnh: không . Chúng tôi thậm chí có thể hy vọng anh ta ước tính có bao xa bên ngoài đường phố vị trí này sẽ nằm nếu nó đã tồn tại: xấp xỉ 2,5 lần xa hơn cuối cùng. Không ai trong số này sẽ gây ra cho anh ta bất kỳ sự bực tức. Tuy nhiên, nếu chúng tôi yêu cầu anh ta chuyển bức thư này, hoặc nhặt một món đồ từ nơi đó, anh ta có thể sẽ khá thẳng thắn về sự không hài lòng của anh ta , và từ chối tuân thủ.

Con trỏ là chỉ địa chỉ và địa chỉ là chỉ số.

Xác nhận đầu ra của các mục sau:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

Gọi nó trên bao nhiêu con trỏ tùy thích, hợp lệ hay không. Xin vui lòng làm gửi phát hiện của bạn nếu nó không thành công trên nền tảng của bạn, hoặc bạn (hiện đại) biên dịch phàn nàn.

Bây giờ, vì con trỏ chỉ đơn giản là con số, nó là chắc chắn có giá trị để so sánh chúng. Theo một nghĩa nào đó thì đây chính xác là những gì giáo viên của bạn đang thể hiện. Tất cả các tuyên bố sau là hoàn toàn hợp lệ - và đúng! - C và khi được biên dịch sẽ chạy mà không gặp phải sự cố , mặc dù không phải con trỏ nào cũng cần được khởi tạo và do đó các giá trị chúng chứa có thể không được xác định :

  • Chúng tôi chỉ tính toán result rõ ràng vì mục đích rõ ràngin nó để buộc trình biên dịch tính toán những gì sẽ là dự phòng, mã chết.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Tất nhiên, chương trình không được định dạng khi a hoặc b không được xác định (đọc: không được khởi tạo đúng cách ) tại điểm kiểm tra, nhưng điều đó hoàn toàn không liên quan đến phần thảo luận này của chúng tôi. Các đoạn mã này, cũng như các tuyên bố sau, được đảm bảo - theo 'tiêu chuẩn' - để biên dịchchạy hoàn hảo, bất chấp tính hiệu lực IN của bất kỳ con trỏ nào có liên quan.

Các vấn đề chỉ phát sinh khi một con trỏ không hợp lệ bị hủy đăng ký . Khi chúng tôi yêu cầu Frank nhận hoặc giao hàng tại địa chỉ không hợp lệ, không tồn tại.

Cho bất kỳ con trỏ tùy ý:

int *p;

Trong khi tuyên bố này phải biên dịch và chạy:

printf(“%p”, p);

... như phải thế này:

size_t foo( int *p ) { return (size_t)p; }

... sau hai, hoàn toàn trái ngược, sẽ vẫn dễ dàng biên dịch, nhưng thất bại trong thực hiện trừ khi con trỏ hợp lệ - mà chúng ta ở đây chỉ có nghĩa là nó tham chiếu một địa chỉ để mà ứng dụng hiện nay đã được cấp quyền truy cập :

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Làm thế nào tinh tế thay đổi? Sự khác biệt nằm ở sự khác biệt giữa giá trị của con trỏ - đó địa chỉ và giá trị của nội dung: của ngôi nhà ở số đó. Không có vấn đề phát sinh cho đến khi con trỏ là dereferenced ; cho đến khi một nỗ lực được thực hiện để truy cập vào địa chỉ mà nó liên kết đến. Trong khi cố gắng giao hàng hoặc nhận gói hàng vượt ra khỏi đoạn đường ...

Bằng cách mở rộng, cùng một nguyên tắc nhất thiết phải áp dụng cho các ví dụ phức tạp hơn, bao gồm cả nhu cầu đã nói ở trên để thiết lập tính hợp lệ cần thiết:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

So sánh quan hệ và số học cung cấp tiện ích giống hệt nhau để kiểm tra tính tương đương và có giá trị tương đương - về nguyên tắc. Tuy nhiên , những gì kết quả của tính toán như vậy sẽ biểu thị , là một vấn đề hoàn toàn khác - và chính xác là vấn đề được giải quyết bằng các trích dẫn bạn đưa vào.

Trong C, một mảng là một bộ đệm liền kề, một chuỗi các vị trí bộ nhớ tuyến tính không bị gián đoạn. So sánh và số học áp dụng cho các con trỏ mà các vị trí tham chiếu trong một chuỗi số ít như vậy là tự nhiên và rõ ràng có ý nghĩa liên quan đến cả hai và với 'mảng' này (được xác định đơn giản bởi cơ sở). Chính xác như vậy áp dụng cho mọi khối được phân bổ thông qua malloc, hoặc sbrk. các mối quan hệ này là ẩn , trình biên dịch có thể thiết lập các mối quan hệ hợp lệ giữa chúng và do đó có thể tin tưởng rằng các tính toán sẽ cung cấp các câu trả lời dự đoán.

Thực hiện các bài thể dục tương tự trên các con trỏ tham chiếu các khối hoặc mảng riêng biệt không cung cấp bất kỳ tiện ích vốn córõ ràng như vậy . Hơn nữa vì bất cứ mối quan hệ nào tồn tại tại một thời điểm có thể bị vô hiệu hóa bởi sự phân bổ lại theo sau, trong đó có khả năng thay đổi cao, thậm chí bị đảo ngược. Trong các trường hợp như vậy, trình biên dịch không thể có được thông tin cần thiết để thiết lập độ tin cậy của nó trong tình huống trước đó.

Bạn , tuy nhiên, là lập trình viên, có thể có kiến ​​thức như vậy! Và trong một số trường hợp có nghĩa vụ khai thác điều đó.

Do đó, trường hợp trong đó ngay cả điều này là hoàn toàn hợp lệ và hoàn hảo PROPER.

Trên thực tế, đó chính xác là những gì mallocbản thân phải làm trong nội bộ khi đến lúc thử hợp nhất các khối khai hoang - trên phần lớn các kiến ​​trúc. Điều tương tự cũng đúng với bộ cấp phát hệ điều hành, như thế đằng sau sbrk; nếu rõ ràng hơn , thường xuyên hơn , trên các thực thể khác nhau hơn , quan trọng hơn - và cũng có liên quan trên các nền tảng nơi điều này malloccó thể không. Và có bao nhiêu trong số đó không được viết bằng C?

Tính hợp lệ, bảo mật và thành công của một hành động chắc chắn là hệ quả của mức độ hiểu biết mà nó được đặt ra và áp dụng.

Trong các trích dẫn bạn đã đưa ra, Kernighan và Ritchie đang giải quyết một vấn đề riêng biệt, nhưng dù sao cũng có liên quan. Họ đang xác định các giới hạn của ngôn ngữ và giải thích cách bạn có thể khai thác các khả năng của trình biên dịch để bảo vệ bạn bằng cách ít nhất phát hiện các cấu trúc có khả năng bị lỗi. Họ đang mô tả độ dài mà cơ chế có thể - được thiết kế - để đi đến để hỗ trợ bạn trong công việc lập trình. Trình biên dịch là đầy tớ của bạn, bạnchủ. Tuy nhiên, một bậc thầy thông thái là một người quen thuộc với khả năng của những người hầu khác nhau.

Trong bối cảnh này, hành vi không xác định phục vụ để chỉ ra nguy cơ tiềm ẩn và khả năng gây hại; không ngụ ý sắp xảy ra, cam chịu không thể đảo ngược, hoặc kết thúc của thế giới như chúng ta biết. Nó đơn giản có nghĩa là chúng tôi - 'có nghĩa là trình biên dịch' - không thể đưa ra bất kỳ phỏng đoán nào về vấn đề này có thể là gì, hoặc đại diện và vì lý do này, chúng tôi chọn rửa tay về vấn đề này. Chúng tôi sẽ không chịu trách nhiệm cho bất kỳ sai sót nào có thể xảy ra do việc sử dụng hoặc sử dụng sai mục đích của cơ sở này .

Trong thực tế, nó chỉ đơn giản nói: 'Ngoài thời điểm này, cao bồi : bạn đang ở một mình ...'

Giáo sư của bạn đang tìm cách thể hiện các sắc thái tốt hơn cho bạn.

Lưu ý những gì họ đã quan tâm rất nhiều trong việc xây dựng ví dụ của họ; và làm thế nào giònvẫn là. Bằng cách lấy địa chỉ của a, trong

p[0].p0 = &a;

trình biên dịch bị ép buộc phân bổ lưu trữ thực tế cho biến, thay vì đặt nó trong một thanh ghi. Nó là một biến tự động, tuy nhiên, lập trình viên không có quyền kiểm soát nơi được gán và do đó không thể đưa ra bất kỳ phỏng đoán hợp lệ nào về những gì sẽ theo nó. Đó là lý do tại sao a phải được đặt bằng 0 để mã hoạt động như mong đợi.

Chỉ thay đổi dòng này:

char a = 0;

đến đây:

char a = 1;  // or ANY other value than 0

làm cho hành vi của chương trình trở nên không xác định . Tối thiểu, câu trả lời đầu tiên bây giờ sẽ là 1; nhưng vấn đề còn độc ác hơn nhiều.

Bây giờ mã đang mời gọi của thảm họa.

Mặc dù vẫn hoàn toàn hợp lệ và thậm chí tuân thủ tiêu chuẩn , nhưng hiện tại nó không được định dạng và mặc dù chắc chắn để biên dịch, có thể thất bại trong việc thực thi trên nhiều lý do khác nhau. Còn bây giờ có rất nhiều vấn đề - không trong đó trình biên dịchkhả năng để nhận ra.

strcpysẽ bắt đầu tại địa chỉ của avà tiến hành vượt quá mức này để tiêu thụ - và chuyển - byte sau byte, cho đến khi nó gặp null.

Con p1trỏ đã được khởi tạo thành một khối chính xác là 10 byte.

  • Nếu atình cờ được đặt ở cuối một khối và quá trình không có quyền truy cập vào phần tiếp theo, thì lần đọc tiếp theo - của p0 [1] - sẽ gợi ra một segfault. Kịch bản này không thể xảy ra trên kiến ​​trúc x86, nhưng có thể.

  • Nếu khu vực nằm ngoài địa chỉ a thể truy cập, sẽ không xảy ra lỗi đọc, nhưng chương trình vẫn không được lưu khỏi điều không may.

  • Nếu một byte 0 xảy ra trong mười bắt đầu tại địa chỉ của anó, thì nó vẫn có thể tồn tại, sau đóstrcpy sẽ dừng lại và ít nhất chúng ta sẽ không bị vi phạm ghi.

  • Nếu nó không bị lỗi khi đọc amiss, nhưng không có byte 0 nào xảy ra trong khoảng 10 này, strcpysẽ tiếp tục và cố gắng ghi vượt ra ngoài khối được phân bổ bởi malloc.

    • Nếu khu vực này không thuộc sở hữu của quy trình, segfault sẽ được kích hoạt ngay lập tức.

    • Vẫn tai hại hơn - và tinh tế --- tình huống phát sinh khi khối sau đây thuộc sở hữu của quá trình này, cho thì lỗi không thể được phát hiện, không có tín hiệu có thể được nâng lên, và vì vậy nó có thể 'xuất hiện' vẫn 'làm việc' , trong khi nó thực sự sẽ ghi đè lên dữ liệu khác, cấu trúc quản lý của người cấp phát hoặc thậm chí mã (trong các môi trường hoạt động nhất định).

Đây là lý do tại sao con trỏ liên quan lỗi có thể rất khó để theo dõi . Hãy tưởng tượng những dòng này được chôn sâu trong hàng ngàn dòng mã liên quan phức tạp, mà người khác đã viết và bạn được hướng dẫn để tìm hiểu kỹ.

Tuy nhiên , chương trìnhvẫn phải biên dịch, vì nó vẫn hoàn toàn hợp lệ tuân thủ tiêu chuẩn C.

Những loại lỗi, không có tiêu chuẩnkhông có trình biên dịch có thể bảo vệ sự không sẵn sàng chống lại. Tôi tưởng tượng đó chính xác là những gì họ đang có ý định dạy bạn.

Người hoang tưởng liên tục tìm cách thay đổi các tính chất của C để xử lý những khả năng có vấn đề và do đó cứu chúng ta khỏi chính mình; nhưng đó là không lịch sự . Đây là trách nhiệm chúng tôi có nghĩa vụ phải chấp nhận khi chúng tôi chọn theo đuổi quyền lực và có được sự tự do mà sự kiểm soát trực tiếp và toàn diện hơn của máy mang lại cho chúng tôi. Những người quảng bá và theo đuổi sự hoàn hảo trong hiệu suất sẽ không bao giờ chấp nhận bất cứ điều gì ít hơn.

Tính di động và tính tổng quát mà nó thể hiện là một sự xem xét cơ bản riêng biệt và tất cả những tiêu chuẩn tìm cách giải quyết:

Tài liệu này chỉ định biểu mẫu và thiết lập việc giải thích các chương trình được thể hiện bằng ngôn ngữ lập trình C. Mục đích của nó là thúc đẩy tính di động , độ tin cậy, khả năng bảo trì và thực thi hiệu quả các chương trình ngôn ngữ C trên nhiều hệ thống máy tính .

Đó là lý do tại sao nó hoàn toàn đúng đắn để giữ cho nó khác biệt với định nghĩađặc điểm kỹ thuật của chính ngôn ngữ. Trái ngược với những gì nhiều người dường như tin rằng tính tổng quátphản đối với ngoại lệmẫu mực .

Để kết luận:

  • Tự kiểm tra và thao tác con trỏ là hợp lệthường có kết quả . Giải thích các kết quả, có thể, hoặc có thể không có ý nghĩa, nhưng tai họa không bao giờ được mời đến khi con trỏ được dereferenced ; cho đến khi một nỗ lực được thực hiện để truy cập vào địa chỉ được liên kết đến.

Điều này không đúng, lập trình như chúng ta biết - và yêu nó - sẽ không thể thực hiện được.


3
Câu trả lời này không may là không hợp lệ. Bạn không thể suy luận bất cứ điều gì về hành vi không xác định. Việc so sánh không cần phải được thực hiện ở cấp độ máy.
Antti Haapala

6
Ghii, thực tế là không. Nếu bạn xem C11 Phụ lục J và 6.5.8, hành động so sánh chính nó là UB. Dereferences là một vấn đề riêng biệt.
paxdiablo

6
Không, UB vẫn có thể gây hại ngay cả trước khi con trỏ bị hủy đăng ký. Một trình biên dịch có thể tự do tối ưu hóa hoàn toàn một chức năng với UB thành một NOP duy nhất, mặc dù điều này rõ ràng thay đổi hành vi có thể nhìn thấy.
nanofarad

2
@Ghii, Phụ lục J (bit tôi đã đề cập) là danh sách những điều không xác định hành vi, vì vậy tôi không chắc điều đó hỗ trợ cho đối số của bạn như thế nào :-) 6.5.8 gọi một cách rõ ràng là so sánh như UB. Đối với nhận xét của bạn về supercat, không có so sánh nào xảy ra khi bạn in một con trỏ để bạn có thể đúng rằng nó sẽ không bị sập. Nhưng đó không phải là những gì OP đang hỏi. 3.4.3cũng là một phần bạn nên xem xét: nó định nghĩa UB là hành vi "mà Tiêu chuẩn quốc tế này áp đặt không có yêu cầu".
paxdiablo

3
@GhiiVelte, bạn cứ nói những điều hoàn toàn sai, mặc dù điều đó được chỉ ra cho bạn. Có, đoạn mã bạn đã đăng phải biên dịch nhưng sự tranh chấp của bạn rằng nó chạy mà không gặp trở ngại là không chính xác. Tôi đề nghị bạn thực sự đọc tiêu chuẩn, đặc biệt (trong trường hợp này) C11 6.5.6/9, hãy nhớ rằng từ "sẽ" chỉ ra một yêu cầuL "Khi hai con trỏ bị trừ, cả hai sẽ trỏ đến các phần tử của cùng một đối tượng mảng, hoặc một quá khứ cuối cùng phần tử của đối tượng mảng ".
paxdiablo

-5

Con trỏ chỉ là số nguyên, giống như mọi thứ khác trong máy tính. Bạn hoàn toàn có thể so sánh chúng với <>và kết quả sản phẩm mà không gây ra một chương trình để sụp đổ. Điều đó nói rằng, tiêu chuẩn không đảm bảo rằng những kết quả đó có bất kỳ ý nghĩa nào ngoài các so sánh mảng.

Trong ví dụ của bạn về các biến được phân bổ ngăn xếp, trình biên dịch có thể tự do phân bổ các biến đó cho các thanh ghi hoặc ngăn xếp địa chỉ bộ nhớ và theo bất kỳ thứ tự nào nó chọn. So sánh như <>do đó sẽ không nhất quán giữa các trình biên dịch hoặc kiến ​​trúc. Tuy nhiên, ==!=không quá hạn chế, so sánh bình đẳng con trỏ là một hoạt động hợp lệ và hữu ích.


2
Ngăn xếp từ xuất hiện chính xác 0 lần trong tiêu chuẩn C11. Và hành vi không xác định có nghĩa là bất cứ điều gì có thể xảy ra (bao gồm cả sự cố chương trình).
paxdiablo

1
@paxdiablo Tôi có nói là đã làm không?
nickelpro

2
Bạn đã đề cập đến các biến được phân bổ ngăn xếp. Không có ngăn xếp trong tiêu chuẩn, đó chỉ là một chi tiết thực hiện. Vấn đề nghiêm trọng hơn với câu trả lời này là sự tranh chấp mà bạn có thể so sánh các con trỏ không có khả năng xảy ra sự cố - điều đó thật sai lầm.
paxdiablo

1
@nickelpro: Nếu một người muốn viết mã tương thích với các trình tối ưu hóa trong gcc và clang, thì cần phải nhảy qua rất nhiều vòng lặp ngớ ngẩn. Cả hai trình tối ưu hóa sẽ tích cực tìm kiếm cơ hội để đưa ra những suy luận về những thứ mà con trỏ sẽ truy cập bất cứ khi nào có bất kỳ cách nào mà Tiêu chuẩn có thể được xoắn để biện minh cho chúng (và thậm chí đôi khi không có). Được đưa ra int x[10],y[10],*p;, nếu mã đánh giá y[0], sau đó đánh giá p>(x+5)và viết *pmà không sửa đổi ptrong thời gian tạm thời, và cuối cùng đánh giá y[0]lại, ...
supercat

1
nickelpro, đồng ý đồng ý không đồng ý nhưng câu trả lời của bạn vẫn sai về cơ bản. Tôi thích cách tiếp cận của bạn với những người sử dụng (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')thay isalpha()vì bởi vì việc triển khai lành mạnh nào sẽ khiến những nhân vật đó không liên tục? Điểm mấu chốt là, ngay cả khi không có triển khai nào bạn biết có vấn đề, bạn nên mã hóa theo tiêu chuẩn càng nhiều càng tốt nếu bạn coi trọng tính di động. Mặc dù vậy, tôi đánh giá cao nhãn "tiêu chuẩn maven", cảm ơn vì điều đó. Tôi có thể đưa vào CV của mình :-)
paxdiablo
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.