Bộ nhớ của một biến cục bộ có thể được truy cập bên ngoài phạm vi của nó không?


1029

Tôi có mã sau đây.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

Và mã chỉ đang chạy mà không có ngoại lệ thời gian chạy!

Đầu ra là 58

Làm thế nào nó có thể được? Không phải bộ nhớ của một biến cục bộ không thể truy cập bên ngoài chức năng của nó?


14
điều này thậm chí sẽ không được biên dịch như là; nếu bạn sửa lỗi doanh nghiệp không định dạng, gcc vẫn sẽ cảnh báo address of local variable ‘a’ returned; chương trình valgrindInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe

76
@Serge: Hồi còn trẻ, tôi đã từng làm việc với một số mã số không phức tạp chạy trên hệ điều hành Netware có liên quan đến việc di chuyển khéo léo xung quanh con trỏ ngăn xếp theo cách không được hệ điều hành xử phạt chính xác. Tôi biết khi nào tôi mắc lỗi vì thường thì ngăn xếp sẽ chồng lấp bộ nhớ màn hình và tôi chỉ có thể xem các byte được ghi ngay trên màn hình. Bạn không thể thoát khỏi điều đó những ngày này.
Eric Lippert

23
cười lớn. Tôi cần phải đọc câu hỏi và một số câu trả lời trước khi tôi hiểu vấn đề đang ở đâu. Đó thực sự là một câu hỏi về phạm vi truy cập của biến? Bạn thậm chí không sử dụng 'a' bên ngoài chức năng của mình. và đấy là tất cả của nó. Xoay quanh một số tài liệu tham khảo bộ nhớ là một chủ đề hoàn toàn khác với phạm vi biến.
erikbwork

10
Câu trả lời Dupe không có nghĩa là câu hỏi dupe. Rất nhiều câu hỏi bị lừa mà mọi người đề xuất ở đây là những câu hỏi hoàn toàn khác nhau xảy ra để đề cập đến cùng một triệu chứng cơ bản ... nhưng người hỏi có cách biết rằng vì vậy họ vẫn nên mở. Tôi đã đóng một bản dupe cũ hơn và hợp nhất nó vào câu hỏi này nên được mở vì nó có một câu trả lời rất hay.
Joel Spolsky

16
@Joel: Nếu câu trả lời ở đây là tốt, nó nên được hợp nhất thành các câu hỏi cũ hơn , trong đó đây là một bản dupe, không phải là cách khác. Và câu hỏi này thực sự là một bản sao của các câu hỏi khác được đề xuất ở đây và sau đó một số (mặc dù một số đề xuất là phù hợp hơn so với những câu hỏi khác). Lưu ý rằng tôi nghĩ câu trả lời của Eric là tốt. (Trên thực tế, tôi đã gắn cờ câu hỏi này để hợp nhất các câu trả lời thành một trong những câu hỏi cũ hơn để cứu vãn các câu hỏi cũ hơn.)
sbi

Câu trả lời:


4801

Làm thế nào nó có thể được? Không phải bộ nhớ của một biến cục bộ không thể truy cập bên ngoài chức năng của nó?

Bạn thuê một phòng khách sạn. Bạn đặt một cuốn sách vào ngăn kéo trên cùng của bàn cạnh giường ngủ và đi ngủ. Bạn kiểm tra vào sáng hôm sau, nhưng "quên" để trả lại chìa khóa của bạn. Bạn ăn cắp chìa khóa!

Một tuần sau, bạn trở về khách sạn, không nhận phòng, lẻn vào phòng cũ bằng chìa khóa bị đánh cắp và tìm trong ngăn kéo. Cuốn sách của bạn vẫn còn đó. Kinh ngạc!

Làm thế nào mà có thể được? Không phải nội dung của ngăn kéo phòng khách sạn không thể truy cập nếu bạn chưa thuê phòng?

Chà, rõ ràng kịch bản đó có thể xảy ra trong thế giới thực không có vấn đề gì. Không có thế lực bí ẩn nào khiến cuốn sách của bạn biến mất khi bạn không còn được phép ở trong phòng. Cũng không có một thế lực bí ẩn nào ngăn bạn vào một căn phòng có chìa khóa bị đánh cắp.

Quản lý khách sạn không bắt buộc phải xóa sách của bạn. Bạn đã không ký hợp đồng với họ nói rằng nếu bạn để lại đồ đạc, họ sẽ hủy nó cho bạn. Nếu bạn vào lại phòng của bạn một cách bất hợp pháp với chìa khóa bị đánh cắp để lấy lại, nhân viên an ninh khách sạn không bắt buộc bạn phải lẻn vào. Bạn đã không ký hợp đồng với họ rằng "nếu tôi cố gắng lẻn vào phòng sau, bạn được yêu cầu ngăn chặn tôi. " Thay vào đó, bạn đã ký hợp đồng với họ với nội dung "Tôi hứa sẽ không lẻn vào phòng tôi sau", một hợp đồng mà bạn đã phá vỡ .

Trong tình huống này bất cứ điều gì cũng có thể xảy ra . Cuốn sách có thể ở đó - bạn đã gặp may mắn. Cuốn sách của người khác có thể ở đó và cuốn sách của bạn có thể ở trong lò của khách sạn. Ai đó có thể ở đó ngay khi bạn bước vào, xé sách của bạn thành từng mảnh. Khách sạn có thể đã loại bỏ bàn và đặt hoàn toàn và thay thế nó bằng một tủ quần áo. Toàn bộ khách sạn có thể sắp bị phá hủy và thay thế bằng một sân bóng đá, và bạn sẽ chết trong một vụ nổ trong khi bạn đang lén lút xung quanh.

Bạn không biết điều gì sẽ xảy ra; Khi bạn rời khỏi khách sạn và lấy trộm chìa khóa để sử dụng bất hợp pháp sau đó, bạn đã từ bỏ quyền sống trong một thế giới an toàn, có thể dự đoán được vì bạn đã chọn phá vỡ các quy tắc của hệ thống.

C ++ không phải là một ngôn ngữ an toàn . Nó sẽ vui vẻ cho phép bạn phá vỡ các quy tắc của hệ thống. Nếu bạn cố làm điều gì đó bất hợp pháp và dại dột như quay trở lại phòng mà bạn không được phép ở trong và lục lọi bàn làm việc thậm chí không còn ở đó nữa, C ++ sẽ không ngăn bạn. Các ngôn ngữ an toàn hơn C ++ giải quyết vấn đề này bằng cách hạn chế sức mạnh của bạn - bằng cách kiểm soát các phím chặt chẽ hơn nhiều, chẳng hạn.

CẬP NHẬT

Chúa ơi, câu trả lời này đang được chú ý rất nhiều. (Tôi không chắc tại sao - Tôi coi đó chỉ là một sự tương tự nhỏ "vui vẻ", nhưng bất cứ điều gì.)

Tôi nghĩ rằng nó có thể là nguyên bản để cập nhật điều này một chút với một vài suy nghĩ kỹ thuật.

Trình biên dịch đang kinh doanh trong việc tạo mã quản lý việc lưu trữ dữ liệu được thao tác bởi chương trình đó. Có rất nhiều cách khác nhau để tạo mã để quản lý bộ nhớ, nhưng theo thời gian, hai kỹ thuật cơ bản đã trở nên cố thủ.

Đầu tiên là có một vùng lưu trữ "tồn tại lâu" trong đó "tuổi thọ" của mỗi byte trong bộ lưu trữ - nghĩa là khoảng thời gian khi nó được liên kết hợp lệ với một số biến chương trình - không thể dự đoán dễ dàng trước của thời gian Trình biên dịch tạo các cuộc gọi vào một "trình quản lý heap" biết cách phân bổ động lưu trữ khi cần thiết và lấy lại khi không cần thiết nữa.

Phương pháp thứ hai là có một vùng lưu trữ thời gian ngắn có thời gian ngắn, trong đó tuổi thọ của mỗi byte được biết đến. Ở đây, các kiếp sống theo mô hình lồng nhau của hoàng tử. Thời gian tồn tại lâu nhất của các biến có thời gian tồn tại ngắn này sẽ được phân bổ trước bất kỳ biến có thời gian tồn tại ngắn nào khác và sẽ được giải phóng sau cùng. Các biến có thời gian tồn tại ngắn hơn sẽ được phân bổ sau các biến có thời gian tồn tại lâu nhất và sẽ được giải phóng trước chúng. Thời gian tồn tại của các biến có thời gian tồn tại ngắn hơn này là lồng nhau lồng nhau trong vòng đời của các biến có thời gian tồn tại lâu hơn.

Các biến cục bộ theo mô hình sau; khi một phương thức được nhập vào, các biến cục bộ của nó trở nên sống động. Khi phương thức đó gọi một phương thức khác, các biến cục bộ của phương thức mới trở nên sống động. Họ sẽ chết trước khi các biến cục bộ của phương thức đầu tiên bị chết. Thứ tự tương đối của sự khởi đầu và kết thúc của thời gian sống của kho lưu trữ liên quan đến các biến cục bộ có thể được thực hiện trước thời hạn.

Vì lý do này, các biến cục bộ thường được tạo dưới dạng lưu trữ trên cấu trúc dữ liệu "ngăn xếp", bởi vì một ngăn xếp có thuộc tính mà điều đầu tiên được đẩy vào nó sẽ là điều cuối cùng xuất hiện.

Giống như khách sạn quyết định chỉ thuê phòng theo tuần tự và bạn không thể trả phòng cho đến khi mọi người có số phòng cao hơn bạn đã trả phòng.

Vì vậy, hãy nghĩ về ngăn xếp. Trong nhiều hệ điều hành, bạn nhận được một ngăn xếp trên mỗi luồng và ngăn xếp được phân bổ theo một kích thước cố định nhất định. Khi bạn gọi một phương thức, công cụ sẽ được đẩy lên ngăn xếp. Nếu sau đó bạn chuyển một con trỏ tới ngăn xếp ra khỏi phương thức của bạn, như áp phích ban đầu thực hiện ở đây, thì đó chỉ là một con trỏ ở giữa một khối bộ nhớ triệu byte hoàn toàn hợp lệ. Tương tự như vậy, bạn trả phòng khách sạn; Khi bạn làm, bạn chỉ cần kiểm tra ra khỏi phòng chiếm số lượng cao nhất. Nếu không có ai khác đăng ký sau bạn và bạn quay trở lại phòng của bạn một cách bất hợp pháp, tất cả đồ đạc của bạn được đảm bảo vẫn còn ở đó trong khách sạn đặc biệt này .

Chúng tôi sử dụng ngăn xếp cho các cửa hàng tạm thời bởi vì chúng thực sự rẻ và dễ dàng. Không cần phải thực hiện C ++ để sử dụng ngăn xếp để lưu trữ cục bộ; nó có thể sử dụng đống. Nó không, bởi vì điều đó sẽ làm cho chương trình chậm hơn.

Việc triển khai C ++ là không bắt buộc để lại rác mà bạn để lại trên ngăn xếp không bị ảnh hưởng để bạn có thể quay lại để lấy nó bất hợp pháp sau này; việc trình biên dịch tạo mã trở về 0 mọi thứ trong "phòng" mà bạn vừa bỏ trống là hoàn toàn hợp pháp. Nó không phải bởi vì một lần nữa, đó sẽ là đắt tiền.

Việc triển khai C ++ là không bắt buộc để đảm bảo rằng khi ngăn xếp co lại một cách hợp lý, các địa chỉ được sử dụng là hợp lệ vẫn được ánh xạ vào bộ nhớ. Việc triển khai được phép nói với hệ điều hành "chúng ta đã hoàn thành việc sử dụng trang stack này ngay bây giờ. Cho đến khi tôi nói khác, hãy đưa ra một ngoại lệ phá hủy quy trình nếu có ai chạm vào trang stack hợp lệ trước đó". Một lần nữa, việc triển khai không thực sự làm điều đó bởi vì nó chậm và không cần thiết.

Thay vào đó, việc triển khai cho phép bạn phạm sai lầm và thoát khỏi nó. Hầu hết thời gian. Cho đến một ngày một cái gì đó thực sự khủng khiếp đi sai và quá trình bùng nổ.

Đây là vấn đề. Có rất nhiều quy tắc và rất dễ dàng để phá vỡ chúng một cách vô tình. Tôi chắc chắn có nhiều lần. Và tệ hơn, vấn đề thường chỉ xuất hiện khi bộ nhớ được phát hiện là hỏng hàng tỷ nano giây sau khi tham nhũng xảy ra, khi rất khó để tìm ra ai đã làm nó rối tung lên.

Nhiều ngôn ngữ an toàn bộ nhớ giải quyết vấn đề này bằng cách hạn chế sức mạnh của bạn. Trong C # "bình thường" đơn giản là không có cách nào để lấy địa chỉ của một địa phương và trả lại hoặc lưu trữ nó sau này. Bạn có thể lấy địa chỉ của một địa phương, nhưng ngôn ngữ được thiết kế khéo léo để không thể sử dụng nó sau khi vòng đời của địa phương kết thúc. Để lấy địa chỉ của một địa phương và chuyển lại, bạn phải đặt trình biên dịch ở chế độ "không an toàn" đặc biệt đặt từ "không an toàn" trong chương trình của bạn, để chú ý đến thực tế rằng bạn có thể đang làm một cái gì đó nguy hiểm có thể phá vỡ các quy tắc.

Để đọc thêm:

  • Điều gì nếu C # đã cho phép trả lại tài liệu tham khảo? Thật trùng hợp, đó là chủ đề của bài viết blog ngày hôm nay:

    https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/

  • Tại sao chúng ta sử dụng ngăn xếp để quản lý bộ nhớ? Các loại giá trị trong C # luôn được lưu trữ trên ngăn xếp? Bộ nhớ ảo hoạt động như thế nào? Và nhiều chủ đề khác trong cách trình quản lý bộ nhớ C # hoạt động. Nhiều bài viết trong số này cũng là nguyên nhân cho các lập trình viên C ++:

    https://ericlippert.com/tag/memory-man Management /


56
@muntoo: Thật không may, nó không giống như hệ điều hành phát ra tiếng còi cảnh báo trước khi nó ngừng hoạt động hoặc giải phóng một trang của bộ nhớ ảo. Nếu bạn đang lẩn quẩn với bộ nhớ đó khi bạn không sở hữu nó nữa thì hệ điều hành hoàn toàn nằm trong quyền của nó để gỡ bỏ toàn bộ quá trình khi bạn chạm vào một trang bị hủy. Bùng nổ!
Eric Lippert

83
@Kyle: Chỉ những khách sạn an toàn mới làm được điều đó. Các khách sạn không an toàn có được lợi nhuận có thể đo được từ việc không phải lãng phí thời gian vào các khóa lập trình.
Alexander Torstling

498
@cyberguijarro: C ++ không an toàn cho bộ nhớ chỉ đơn giản là một sự thật. Nó không "bash" bất cứ điều gì. Tôi đã nói, chẳng hạn, "C ++ là một mớ hỗn độn kinh khủng của các tính năng quá phức tạp, quá phức tạp được đặt chồng lên trên một mô hình bộ nhớ nguy hiểm, dễ vỡ và tôi rất biết ơn mỗi ngày tôi không còn làm việc trong đó vì sự tỉnh táo của mình", đó sẽ là bashing C ++. Chỉ ra rằng nó không an toàn cho bộ nhớ đang giải thích lý do tại sao người đăng ban đầu thấy vấn đề này; đó là trả lời câu hỏi, không phải biên tập.
Eric Lippert

50
Nói một cách chính xác sự tương tự nên đề cập rằng nhân viên tiếp tân tại khách sạn khá vui khi bạn mang chìa khóa theo. "Ồ, bạn có phiền nếu tôi mang chìa khóa này với tôi không?" "Đi tiếp. Tại sao tôi quan tâm? Tôi chỉ làm việc ở đây". Nó không trở thành bất hợp pháp cho đến khi bạn cố gắng sử dụng nó.
philsquared

140
Xin vui lòng, ít nhất hãy xem xét viết một cuốn sách một ngày. Tôi sẽ mua nó ngay cả khi nó chỉ là một tập hợp các bài đăng blog được sửa đổi và mở rộng, và tôi chắc chắn sẽ có rất nhiều người. Nhưng một cuốn sách với những suy nghĩ ban đầu của bạn về các vấn đề liên quan đến lập trình khác nhau sẽ là một cuốn sách tuyệt vời để đọc. Tôi biết rằng thật khó để tìm ra thời gian cho nó, nhưng xin vui lòng xem xét việc viết một.
Dyppl

276

Những gì bạn đang làm ở đây chỉ đơn giản là đọc và ghi vào bộ nhớ từng là địa chỉ của a. Bây giờ bạn đang ở ngoài foo, nó chỉ là một con trỏ đến một vùng nhớ ngẫu nhiên. Nó chỉ xảy ra rằng trong ví dụ của bạn, vùng nhớ đó tồn tại và không có gì khác đang sử dụng nó vào lúc này. Bạn không phá vỡ bất cứ điều gì bằng cách tiếp tục sử dụng nó và chưa có gì khác ghi đè lên nó. Do đó, 5vẫn còn đó. Trong một chương trình thực tế, bộ nhớ đó sẽ được sử dụng lại gần như ngay lập tức và bạn sẽ phá vỡ thứ gì đó bằng cách thực hiện điều này (mặc dù các triệu chứng có thể không xuất hiện cho đến sau này!)

Khi bạn trở về foo, bạn nói với HĐH rằng bạn không còn sử dụng bộ nhớ đó nữa và nó có thể được gán lại cho thứ khác. Nếu bạn may mắn và nó không bao giờ được chỉ định lại, và HĐH không bắt bạn sử dụng lại, thì bạn sẽ thoát khỏi sự dối trá. Có thể mặc dù bạn sẽ kết thúc bằng văn bản về bất cứ điều gì khác kết thúc với địa chỉ đó.

Bây giờ nếu bạn đang tự hỏi tại sao trình biên dịch không phàn nàn, có lẽ vì foođã bị loại bỏ bởi tối ưu hóa. Nó thường sẽ cảnh báo bạn về loại điều này. C giả định rằng bạn biết những gì bạn đang làm và về mặt kỹ thuật bạn không vi phạm phạm vi ở đây (không có tham chiếu nào abên ngoài foo), chỉ có các quy tắc truy cập bộ nhớ, chỉ kích hoạt cảnh báo thay vì lỗi.

Nói tóm lại: điều này thường không hoạt động, nhưng đôi khi sẽ tình cờ.


152

Bởi vì không gian lưu trữ chưa được dậm chân. Đừng tin vào hành vi đó.


1
Man, đó là sự chờ đợi lâu nhất cho một bình luận kể từ, "Sự thật là gì? Nói Philatô nói." Có lẽ đó là cuốn Kinh thánh của Gideon trong ngăn kéo khách sạn đó. Và những gì đã xảy ra với họ, dù sao? Lưu ý rằng họ không còn hiện diện, ở London ít nhất. Tôi đoán rằng theo luật Bình đẳng, bạn sẽ cần một thư viện các vùng tôn giáo.
Rob Kent

Tôi có thể đã thề rằng tôi đã viết nó từ lâu, nhưng nó đã xuất hiện gần đây và thấy phản hồi của tôi không có ở đó. Bây giờ tôi phải tìm ra những ám chỉ của bạn ở trên vì tôi hy vọng tôi sẽ thích thú khi tôi làm>. <
msw

1
Haha. Francis Bacon, một trong những nhà tiểu luận vĩ đại nhất nước Anh, người mà một số người nghi ngờ đã viết các vở kịch của Shakespeare, bởi vì họ không thể chấp nhận rằng một đứa trẻ học ngữ pháp từ đất nước, con trai của một kẻ hả hê, có thể là một thiên tài. Đó là hệ thống lớp học tiếng Anh. Chúa Giêsu đã nói: 'Tôi là Sự thật'. oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent

84

Một bổ sung nhỏ cho tất cả các câu trả lời:

nếu bạn làm một cái gì đó như thế:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

đầu ra có thể sẽ là: 7

Đó là bởi vì sau khi trở về từ foo (), ngăn xếp được giải phóng và sau đó được sử dụng lại bởi boo (). Nếu bạn lắp ráp lại tệp thực thi, bạn sẽ thấy nó rõ ràng.


2
Đơn giản, nhưng ví dụ tuyệt vời để hiểu lý thuyết ngăn xếp cơ bản. Chỉ cần thêm một phép thử, khai báo "int a = 5;" trong foo () là "static int a = 5;" có thể được sử dụng để hiểu phạm vi và thời gian sống của một biến tĩnh.
kiểm soát

15
-1 " có thể sẽ là 7 ". Trình biên dịch có thể liệt kê một trong boo. Nó có thể loại bỏ nó vì nó không cần thiết. Có một cơ hội tốt rằng * p sẽ không phải là 5 , nhưng điều đó không có nghĩa là có bất kỳ lý do đặc biệt tốt nào tại sao nó có thể là 7 .
Matt

2
Nó được gọi là hành vi không xác định!
Francis Cugler

Tại sao và làm thế nào để bootái sử dụng foongăn xếp? không ngăn xếp các chức năng tách biệt với nhau, tôi cũng nhận được rác khi chạy mã này trên Visual Studio 2015
ampawd

1
@ampawd đã gần một năm tuổi, nhưng không, "ngăn xếp chức năng" không tách rời nhau. Một TIẾP THEO có một ngăn xếp. Bối cảnh đó sử dụng ngăn xếp của nó để nhập chính, sau đó đi vào foo(), tồn tại, sau đó đi xuống boo(). Foo()Boo()cả hai nhập với con trỏ ngăn xếp tại cùng một vị trí. Tuy nhiên, đây không phải là hành vi nên được dựa vào. Các 'nội dung' khác (như ngắt hoặc HĐH) có thể sử dụng ngăn xếp giữa lệnh gọi boo()foo(), sửa đổi nội dung của nó ...
Russ Schultz

72

Trong C ++, bạn có thể truy cập bất kỳ địa chỉ nào, nhưng điều đó không có nghĩa là bạn nên . Địa chỉ bạn đang truy cập không còn hợp lệ. Nó hoạt động vì không có gì khác làm xáo trộn bộ nhớ sau khi foo trở lại, nhưng nó có thể bị sập trong nhiều trường hợp. Hãy thử phân tích chương trình của bạn với Valgrind hoặc thậm chí chỉ biên dịch chương trình được tối ưu hóa và xem ...


5
Bạn có thể có nghĩa là bạn có thể cố gắng truy cập bất kỳ địa chỉ. Bởi vì hầu hết các hệ điều hành ngày nay sẽ không cho phép bất kỳ chương trình nào truy cập vào bất kỳ địa chỉ nào; Có hàng tấn biện pháp bảo vệ không gian địa chỉ. Đây là lý do tại sao sẽ không có LOADLINE.EXE khác ngoài đó.
v010dya

67

Bạn không bao giờ ném ngoại lệ C ++ bằng cách truy cập bộ nhớ không hợp lệ. Bạn chỉ đưa ra một ví dụ về ý tưởng chung về việc tham chiếu một vị trí bộ nhớ tùy ý. Tôi có thể làm như thế này:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Ở đây tôi chỉ đơn giản coi 123456 là địa chỉ của một đôi và viết cho nó. Bất kỳ số lượng điều có thể xảy ra:

  1. qtrong thực tế có thể là một địa chỉ hợp lệ của một đôi, ví dụ double p; q = &p;.
  2. q có thể chỉ ra một nơi nào đó bên trong bộ nhớ được phân bổ và tôi chỉ ghi đè lên 8 byte trong đó.
  3. q các điểm bên ngoài bộ nhớ được phân bổ và trình quản lý bộ nhớ của hệ điều hành sẽ gửi tín hiệu lỗi phân đoạn đến chương trình của tôi, khiến cho bộ thực thi chấm dứt nó.
  4. Bạn trúng xổ số.

Cách bạn thiết lập nó hợp lý hơn một chút là địa chỉ được trả về chỉ vào một vùng bộ nhớ hợp lệ, vì nó có thể sẽ nằm sâu hơn một chút trong ngăn xếp, nhưng nó vẫn là một vị trí không hợp lệ mà bạn không thể truy cập trong một thời trang quyết định.

Không ai sẽ tự động kiểm tra tính hợp lệ ngữ nghĩa của các địa chỉ bộ nhớ như thế đối với bạn trong quá trình thực thi chương trình bình thường. Tuy nhiên, một trình gỡ lỗi bộ nhớ như valgrindsẽ vui vẻ làm điều này, vì vậy bạn nên chạy chương trình của mình thông qua nó và chứng kiến ​​các lỗi.


9
Tôi chỉ sẽ viết một chương trình bây giờ mà vẫn tiếp tục chạy chương trình này để4) I win the lottery
Aidiakapi

29

Bạn đã biên dịch chương trình của mình với trình tối ưu hóa được kích hoạt chưa? Các foo()chức năng khá đơn giản và có thể đã bị inlined hoặc thay thế trong những mã kết quả.

Nhưng tôi đồng ý với Mark B rằng hành vi kết quả là không xác định.


Đó là sự đánh cược của tôi. Trình tối ưu hóa kết xuất cuộc gọi chức năng.
Erik Aronesty

9
Đó là điều không cần thiết. Vì không có hàm mới nào được gọi sau foo (), nên khung ngăn xếp cục bộ của hàm đơn giản là chưa được ghi đè. Thêm một lời gọi hàm khác sau foo () và 5sẽ được thay đổi ...
Tomas

Tôi đã chạy chương trình với GCC 4.8, thay thế cout bằng printf (và bao gồm cả stdio). Cảnh báo chính xác "cảnh báo: địa chỉ của biến cục bộ 'a' được trả về [-Wreturn-local-addr]". Đầu ra 58 không tối ưu hóa và 08 với -O3. Thật lạ là P không có địa chỉ, mặc dù giá trị của nó là 0. Tôi mong đợi NULL (0) là địa chỉ.
kevinf

23

Vấn đề của bạn không có gì để làm với phạm vi . Trong mã bạn hiển thị, hàm mainkhông nhìn thấy tên trong hàm foo, vì vậy bạn không thể truy cập atrực tiếp vào foo với tên này bên ngoài foo.

Vấn đề bạn gặp phải là tại sao chương trình không báo hiệu lỗi khi tham chiếu bộ nhớ bất hợp pháp. Điều này là do các tiêu chuẩn C ++ không chỉ định ranh giới rất rõ ràng giữa bộ nhớ bất hợp pháp và bộ nhớ hợp pháp. Tham chiếu một cái gì đó trong ngăn xếp bật ra đôi khi gây ra lỗi và đôi khi không. Nó phụ thuộc. Đừng tin vào hành vi này. Giả sử nó sẽ luôn dẫn đến lỗi khi bạn lập trình, nhưng giả sử nó sẽ không bao giờ báo hiệu lỗi khi bạn gỡ lỗi.


Tôi nhớ lại từ một bản sao cũ của Lập trình Turbo C cho IBM , thứ mà tôi đã từng chơi xung quanh khi nào đó, cách thao tác trực tiếp bộ nhớ đồ họa và bố cục của bộ nhớ video chế độ văn bản của IBM, được mô tả rất chi tiết. Tất nhiên sau đó, hệ thống mà mã chạy trên đã xác định rõ việc viết vào các địa chỉ đó có nghĩa là gì, miễn là bạn không lo lắng về tính di động đối với các hệ thống khác, mọi thứ đều ổn. IIRC, con trỏ đến void là một chủ đề phổ biến trong cuốn sách đó.
một CVn

@Michael Kjorling: Chắc chắn rồi!
Chang Peng

18

Bạn chỉ trả về một địa chỉ bộ nhớ, nó được phép nhưng có thể là một lỗi.

Có nếu bạn cố gắng xác nhận lại rằng địa chỉ bộ nhớ bạn sẽ có hành vi không xác định.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}

Tôi không đồng ý: Có một vấn đề trước cout. *achỉ vào bộ nhớ chưa được phân bổ (giải phóng). Ngay cả khi bạn không hủy đăng ký, nó vẫn nguy hiểm (và có khả năng là không có thật).
vào

@ereOn: Tôi đã làm rõ hơn những gì tôi có nghĩa là vấn đề, nhưng không, nó không nguy hiểm về mặt mã c ++ hợp lệ. Nhưng nó nguy hiểm về khả năng người dùng mắc lỗi và sẽ làm điều gì đó xấu. Có thể ví dụ bạn đang cố gắng xem ngăn xếp phát triển như thế nào và bạn chỉ quan tâm đến giá trị địa chỉ và sẽ không bao giờ bỏ qua nó.
Brian R. Bondy

18

Đó là hành vi không xác định cổ điển đã được thảo luận ở đây không phải hai ngày trước - tìm kiếm trên trang web một chút. Tóm lại, bạn đã may mắn, nhưng bất cứ điều gì cũng có thể xảy ra và mã của bạn đang truy cập bộ nhớ không hợp lệ.


18

Hành vi này không được xác định, như Alex chỉ ra - trên thực tế, hầu hết các trình biên dịch sẽ cảnh báo không làm điều này, bởi vì đó là một cách dễ dàng để gặp sự cố.

Để biết ví dụ về loại hành vi ma quái mà bạn có thể gặp phải, hãy thử mẫu này:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Điều này in ra "y = 123", nhưng kết quả của bạn có thể thay đổi (thực sự!). Con trỏ của bạn đang ghi đè các biến cục bộ khác, không liên quan.


18

Hãy chú ý đến tất cả các cảnh báo. Không chỉ giải quyết lỗi.
GCC hiển thị Cảnh báo này

cảnh báo: địa chỉ của biến cục bộ 'a' được trả về

Đây là sức mạnh của C ++. Bạn nên quan tâm đến trí nhớ. Với -Werrorcờ, cảnh báo này có lỗi và bây giờ bạn phải gỡ lỗi.


17

Nó hoạt động vì ngăn xếp chưa được thay đổi (chưa) kể từ khi được đặt ở đó. Gọi một vài chức năng khác (cũng đang gọi các chức năng khác) trước khi truy cập alại và bạn có thể sẽ không còn may mắn nữa ... ;-)


16

Bạn thực sự viện dẫn hành vi không xác định.

Trả lại địa chỉ của một công trình tạm thời, nhưng vì tạm thời bị phá hủy ở cuối chức năng, kết quả truy cập chúng sẽ không được xác định.

Vì vậy, bạn đã không sửa đổi amà thay vào đó là vị trí bộ nhớ a. Sự khác biệt này rất giống với sự khác biệt giữa sự cố và không bị rơi.


14

Trong các triển khai trình biên dịch điển hình, bạn có thể nghĩ mã là "in ra giá trị của khối bộ nhớ với địa chỉ đã từng bị chiếm bởi". Ngoài ra, nếu bạn thêm một lời gọi hàm mới vào một hàm chứa một cục bộ intthì rất có thể giá trị của a(hoặc địa chỉ bộ nhớ ađược sử dụng để trỏ đến) thay đổi. Điều này xảy ra vì ngăn xếp sẽ được ghi đè bằng một khung mới chứa dữ liệu khác nhau.

Tuy nhiên, đây là hành vi không xác định và bạn không nên dựa vào nó để làm việc!


3
"In ra giá trị của khối bộ nhớ có địa chỉ từng bị chiếm bởi" không hoàn toàn đúng. Điều này làm cho nó có vẻ như mã của anh ta có một số ý nghĩa được xác định rõ, đó không phải là trường hợp. Bạn đúng rằng đây có lẽ là cách mà hầu hết các trình biên dịch sẽ thực hiện nó, mặc dù.
Brennan Vincent

@BrennanVincent: Trong khi bộ nhớ bị chiếm dụng a, con trỏ giữ địa chỉ của a. Mặc dù Tiêu chuẩn không yêu cầu việc triển khai xác định hành vi của các địa chỉ sau khi thời hạn của mục tiêu kết thúc, nhưng nó cũng nhận ra rằng trên một số nền tảng, UB được xử lý theo cách đặc trưng của môi trường. Mặc dù địa chỉ của một biến cục bộ thường không được sử dụng nhiều sau khi nó vượt quá phạm vi, một số loại địa chỉ khác vẫn có thể có ý nghĩa sau thời gian tồn tại của các mục tiêu tương ứng.
supercat

@BrennanVincent: Ví dụ: trong khi Tiêu chuẩn có thể không yêu cầu việc triển khai cho phép một con trỏ reallocđược so sánh với giá trị trả về, cũng không cho phép các con trỏ tới các địa chỉ trong khối cũ được điều chỉnh để trỏ đến khối mới, một số triển khai thực hiện như vậy và mã khai thác một tính năng như vậy có thể hiệu quả hơn mã phải tránh mọi hành động - thậm chí so sánh - liên quan đến con trỏ đến phân bổ đã được trao realloc.
supercat

14

Nó có thể, bởi vì alà một biến được phân bổ tạm thời cho vòng đời của phạm vi ( foohàm) của nó. Sau khi bạn trở về từ foobộ nhớ là miễn phí và có thể được ghi đè.

Những gì bạn đang làm được mô tả là hành vi không xác định . Kết quả không thể dự đoán.


12

Những thứ có đầu ra giao diện điều khiển chính xác (?) Có thể thay đổi đáng kể nếu bạn sử dụng :: printf nhưng không phải cout. Bạn có thể chơi xung quanh với trình gỡ lỗi trong mã bên dưới (được thử nghiệm trên x86, 32-bit, MSVisual Studio):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}

5

Sau khi trở về từ một hàm, tất cả các mã định danh bị hủy thay vì giữ các giá trị trong một vị trí bộ nhớ và chúng ta không thể định vị các giá trị mà không có mã định danh. Nhưng vị trí đó vẫn chứa giá trị được lưu bởi hàm trước đó.

Vì vậy, ở đây hàm foo()đang trả về địa chỉ của aabị hủy sau khi trả lại địa chỉ của nó. Và bạn có thể truy cập giá trị sửa đổi thông qua địa chỉ trả lại.

Hãy để tôi lấy một ví dụ thực tế:

Giả sử một người đàn ông giấu tiền tại một địa điểm và cho bạn biết địa điểm. Sau một thời gian, người đàn ông đã nói với bạn vị trí tiền chết. Nhưng bạn vẫn có quyền truy cập vào số tiền ẩn đó.


4

Đó là cách 'bẩn' sử dụng địa chỉ bộ nhớ. Khi bạn trả về một địa chỉ (con trỏ), bạn không biết liệu nó có thuộc phạm vi cục bộ của hàm không. Nó chỉ là một địa chỉ. Bây giờ bạn đã gọi hàm 'foo', địa chỉ đó (vị trí bộ nhớ) của 'a' đã được phân bổ ở đó trong bộ nhớ (quy trình) ít nhất là an toàn cho ứng dụng của bạn (quá trình). Sau khi hàm 'foo' được trả về, địa chỉ của 'a' có thể được coi là 'bẩn' nhưng nó ở đó, không được dọn dẹp, cũng không bị làm phiền / sửa đổi bởi các biểu thức trong phần khác của chương trình (ít nhất là trong trường hợp cụ thể này). Trình biên dịch AC / C ++ không ngăn bạn truy cập 'bẩn' như vậy (có thể cảnh báo bạn, nếu bạn quan tâm).


1

Mã của bạn rất rủi ro. Bạn đang tạo một biến cục bộ (wich được coi là bị hủy sau khi hàm kết thúc) và bạn trả về địa chỉ bộ nhớ của biến đó sau khi nó bị hủy.

Điều đó có nghĩa là địa chỉ bộ nhớ có thể hợp lệ hoặc không và mã của bạn sẽ dễ bị ảnh hưởng bởi các sự cố địa chỉ bộ nhớ có thể xảy ra (ví dụ như lỗi phân đoạn).

Điều này có nghĩa là bạn đang làm một điều rất tồi tệ, vì bạn đang truyền một địa chỉ bộ nhớ cho một con trỏ mà không đáng tin chút nào.

Thay vào đó, hãy xem xét ví dụ này và kiểm tra nó:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

Không giống như ví dụ của bạn, với ví dụ này, bạn là:

  • cấp phát bộ nhớ cho int vào một hàm cục bộ
  • địa chỉ bộ nhớ đó vẫn còn hiệu lực khi hết hạn chức năng, (nó không bị xóa bởi bất kỳ ai)
  • địa chỉ bộ nhớ là đáng tin cậy (khối bộ nhớ đó không được coi là miễn phí, vì vậy nó sẽ không bị ghi đè cho đến khi bị xóa)
  • địa chỉ bộ nhớ sẽ bị xóa khi không sử dụng. (xem phần xóa ở cuối chương trình)

Bạn đã thêm một cái gì đó chưa được bao phủ bởi các câu trả lời hiện có? Và xin vui lòng không sử dụng con trỏ thô / new.
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
Người hỏi sử dụng con trỏ thô. Tôi đã làm một ví dụ phản ánh chính xác ví dụ anh ấy đã làm để cho phép anh ấy thấy sự khác biệt giữa con trỏ không đáng tin cậy và con trỏ đáng tin cậy. Trên thực tế có một câu trả lời tương tự như của tôi, nhưng nó sử dụng strcpy wich, IMHO, có thể ít rõ ràng hơn đối với một lập trình viên mới làm quen so với ví dụ của tôi sử dụng mới.
Nobun

Họ đã không sử dụng new. Bạn đang dạy họ sử dụng new. Nhưng bạn không nên sử dụng new.
Các cuộc đua nhẹ nhàng trong quỹ đạo

Vì vậy, theo ý kiến ​​của bạn, tốt hơn là chuyển một địa chỉ cho một biến cục bộ mà bị phá hủy trong một hàm hơn là phân bổ bộ nhớ thực sự? Điều này không có ý nghĩa. Hiểu khái niệm phân bổ bộ nhớ xử lý điện tử là rất quan trọng, imho, chủ yếu nếu bạn hỏi về con trỏ (người hỏi không sử dụng con trỏ mới, nhưng đã sử dụng).
Nobun

Khi nào tôi nói vậy? Không, tốt hơn là sử dụng các con trỏ thông minh để biểu thị đúng quyền sở hữu tài nguyên được tham chiếu. Không sử dụng newvào năm 2019 (trừ khi bạn viết mã thư viện) và không dạy người mới làm như vậy! Chúc mừng.
Các cuộc đua nhẹ nhàng trong quỹ đạo
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.