Tại sao C ++ có 'hành vi không xác định' (UB) và các ngôn ngữ khác như C # hoặc Java thì không?


50

Bài đăng Stack Overflow này liệt kê một danh sách khá đầy đủ các tình huống trong đó đặc tả ngôn ngữ C / C ++ tuyên bố là 'hành vi không xác định'. Tuy nhiên, tôi muốn hiểu tại sao các ngôn ngữ hiện đại khác, như C # hoặc Java, không có khái niệm 'hành vi không xác định'. Điều đó có nghĩa là, trình thiết kế trình biên dịch có thể kiểm soát tất cả các kịch bản có thể (C # và Java) hay không (C và C ++)?




3
và bài viết SO này đề cập đến hành vi không xác định ngay cả trong thông số Java!
gbjbaanb

"Tại sao C ++ có 'Hành vi không xác định'" Thật không may, đây dường như là một trong những câu hỏi khó trả lời khách quan, ngoài tuyên bố "bởi vì, vì lý do X, Y và / hoặc Z (tất cả đều có thể nullptr) người ta bận tâm xác định hành vi bằng cách viết và / hoặc thông qua một đặc tả được đề xuất ". : c
code_dredd

Tôi sẽ thách thức tiền đề. Ít nhất C # có mã "không an toàn". Microsoft viết "Theo một nghĩa nào đó, viết mã không an toàn giống như viết mã C trong chương trình C #" và đưa ra ví dụ lý do tại sao người ta muốn làm như vậy: để truy cập phần cứng hoặc HĐH và cho tốc độ. Đây là những gì C được phát minh ra (địa ngục, họ đã viết HĐH bằng C!), Vì vậy bạn có nó.
Peter - Tái lập lại

Câu trả lời:


72

Hành vi không xác định là một trong những điều được công nhận là một ý tưởng rất xấu chỉ khi nhìn lại.

Các trình biên dịch đầu tiên là những thành tựu to lớn và tưng bừng hoan nghênh những cải tiến so với giải pháp thay thế - ngôn ngữ máy hoặc lập trình ngôn ngữ lắp ráp. Các vấn đề với điều đó đã được biết đến và các ngôn ngữ cấp cao được phát minh đặc biệt để giải quyết những vấn đề đã biết. .

Mãi đến sau này chúng tôi mới nhận ra những vấn đề mới hơn xảy ra với cách tiếp cận mới hơn. Nằm cách xa máy thực tế mà mã chạy trên có nghĩa là có nhiều khả năng mọi thứ âm thầm không làm những gì chúng ta mong đợi chúng làm. Chẳng hạn, việc phân bổ một biến thường sẽ không xác định giá trị ban đầu; đây không được coi là một vấn đề, bởi vì bạn sẽ không phân bổ một biến nếu bạn không muốn giữ một giá trị trong đó, phải không? Chắc chắn sẽ không quá nhiều để mong đợi rằng các lập trình viên chuyên nghiệp sẽ không quên gán giá trị ban đầu, phải không?

Hóa ra, với các cơ sở mã lớn hơn và các cấu trúc phức tạp hơn có thể có với các hệ thống lập trình mạnh hơn, vâng, nhiều lập trình viên thực sự sẽ có những lần giám sát như vậy theo thời gian và hành vi không xác định đã trở thành một vấn đề lớn. Thậm chí ngày nay, phần lớn các rò rỉ bảo mật từ nhỏ đến khủng khiếp là kết quả của hành vi không xác định ở dạng này hay dạng khác. (Lý do là thông thường, trên thực tế, hành vi không xác định được xác định rất nhiều bởi những thứ ở cấp độ thấp hơn tiếp theo về điện toán và những kẻ tấn công hiểu rằng cấp độ đó có thể sử dụng phòng ngọ nguậy đó để tạo ra một chương trình không chỉ là những thứ ngoài ý muốn, mà chính xác là những thứ họ dự định.)

Vì chúng tôi đã nhận ra điều này, đã có một nỗ lực chung để loại bỏ hành vi không xác định khỏi các ngôn ngữ cấp cao và Java đặc biệt kỹ lưỡng về điều này (điều này tương đối dễ dàng vì dù sao nó cũng được thiết kế để chạy trên máy ảo được thiết kế riêng của nó). Các ngôn ngữ cũ hơn như C không thể dễ dàng được trang bị thêm như thế mà không mất khả năng tương thích với số lượng lớn mã hiện có.

Chỉnh sửa: Như đã chỉ ra, hiệu quả là một lý do khác. Hành vi không xác định có nghĩa là người viết trình biên dịch có rất nhiều thời gian để khai thác kiến ​​trúc đích để mỗi lần thực hiện được thực hiện với việc thực hiện nhanh nhất có thể của từng tính năng. Điều này quan trọng hơn đối với các máy thiếu năng lực của ngày hôm qua so với ngày nay, khi lương lập trình viên thường là nút thắt cho phát triển phần mềm.


56
Tôi không nghĩ rằng nhiều người trong cộng đồng C sẽ đồng ý với tuyên bố này. Nếu bạn trang bị thêm C và xác định hành vi không xác định (ví dụ: khởi tạo mặc định mọi thứ, chọn thứ tự đánh giá cho tham số hàm, v.v.), cơ sở lớn của mã hoạt động tốt sẽ tiếp tục hoạt động hoàn hảo. Chỉ có mã không được xác định rõ ngày hôm nay sẽ bị phá vỡ. Mặt khác, nếu bạn không xác định như ngày hôm nay, trình biên dịch sẽ tiếp tục được tự do khai thác những tiến bộ mới trong kiến ​​trúc CPU và tối ưu hóa mã.
Barshe

13
Phần chính của câu trả lời không thực sự thuyết phục đối với tôi. Ý tôi là, về cơ bản không thể viết một hàm an toàn thêm hai số (như trong int32_t add(int32_t x, int32_t y)) trong C ++. Các đối số thông thường xung quanh một đối số có liên quan đến hiệu quả, nhưng thường xen kẽ với một số đối số tính di động (như trong "Viết một lần, chạy ... trên nền tảng nơi bạn đã viết nó ... và không ở đâu khác ;-)"). Do đó, một lý do có thể là: Một số điều không được xác định bởi vì bạn không biết liệu bạn đang sử dụng một vi điều khiển 16 bit hay máy chủ 64 bit (một đối số yếu, nhưng vẫn là một đối số)
Marco13

12
@ Marco13 Đồng ý - và loại bỏ vấn đề "hành vi không xác định" bằng cách thực hiện "hành vi được xác định", nhưng không nhất thiết là những gì người dùng muốn và không có cảnh báo khi xảy ra "thay vì" hành vi không xác định "chỉ là chơi trò chơi luật sư mã IMO .
alephzero

9
"Ngay cả ngày nay, phần lớn các rò rỉ bảo mật từ nhỏ đến khủng khiếp là kết quả của hành vi không xác định ở dạng này hay dạng khác." Cần dẫn nguồn. Tôi nghĩ rằng hầu hết trong số họ đã được tiêm XYZ.
Joshua

34
"Hành vi không xác định là một trong những điều được công nhận là một ý tưởng rất xấu chỉ khi nhìn lại." Đó là quan điểm của bạn. Nhiều người (bao gồm cả tôi) không chia sẻ nó.
Cuộc đua nhẹ nhàng với Monica

103

Về cơ bản vì các nhà thiết kế Java và các ngôn ngữ tương tự không muốn hành vi không xác định trong ngôn ngữ của họ. Đây là một sự đánh đổi - cho phép hành vi không xác định có khả năng cải thiện hiệu suất, nhưng các nhà thiết kế ngôn ngữ ưu tiên an toàn và dự đoán cao hơn.

Ví dụ: nếu bạn phân bổ một mảng trong C, dữ liệu không được xác định. Trong Java, tất cả các byte phải được khởi tạo thành 0 (hoặc một số giá trị được chỉ định khác). Điều này có nghĩa là thời gian chạy phải vượt qua mảng (một hoạt động O (n)), trong khi C có thể thực hiện phân bổ ngay lập tức. Vì vậy, C sẽ luôn luôn nhanh hơn cho các hoạt động như vậy.

Nếu mã sử dụng mảng sẽ cư trú bằng cách nào trước khi đọc, thì về cơ bản, điều này đã lãng phí công sức cho Java. Nhưng trong trường hợp mã được đọc trước, bạn sẽ nhận được kết quả có thể dự đoán được trong Java nhưng kết quả không thể đoán trước được trong C.


19
Trình bày xuất sắc về tình huống khó xử HLL: an toàn và dễ sử dụng so với hiệu suất. Không có viên đạn bạc: có trường hợp sử dụng cho mỗi bên.
Barshe

5
@Christophe Công bằng mà nói, có nhiều cách tiếp cận vấn đề tốt hơn nhiều so với việc để UB hoàn toàn không bị kiểm soát như C và C ++. Bạn có thể có một ngôn ngữ được quản lý an toàn, với lối thoát vào lãnh thổ không an toàn, để bạn áp dụng khi có lợi. TBH, thật tuyệt khi có thể biên dịch chương trình C / C ++ của tôi với một lá cờ có nội dung "chèn bất kỳ máy móc thời gian chạy đắt tiền nào bạn cần, tôi không quan tâm, nhưng chỉ cần cho tôi biết về TẤT CẢ các UB xảy ra . "
Alexander

4
Một ví dụ điển hình về cấu trúc dữ liệu cố tình đọc các vị trí chưa được khởi tạo là biểu diễn tập hợp thưa thớt của Briggs và Torczon (ví dụ: mã hóa playplayground.blogspot.com / 2009/03 / .) n) với khởi tạo bắt buộc của Java.
Arch D. Robison

9
Mặc dù đúng là việc buộc khởi tạo dữ liệu làm cho các chương trình bị hỏng dễ dự đoán hơn nhiều, nhưng nó không đảm bảo hành vi dự định: Nếu thuật toán dự kiến ​​sẽ đọc dữ liệu có ý nghĩa trong khi đọc nhầm số 0 được khởi tạo ngầm, thì đó cũng là một lỗi như thể nó đã xảy ra đọc một số rác. Với chương trình C / C ++, một lỗi như vậy sẽ hiển thị bằng cách chạy quy trình bên dưới valgrind, nó sẽ hiển thị chính xác nơi sử dụng giá trị chưa được khởi tạo. Bạn không thể sử dụng valgrindmã java vì thời gian chạy thực hiện khởi tạo, làm cho valgrinds kiểm tra trở nên vô dụng.
cmaster

5
@cmaster Đó là lý do tại sao trình biên dịch C # không cho phép bạn đọc từ các địa phương chưa được khởi tạo. Không cần kiểm tra thời gian chạy, không cần khởi tạo, chỉ cần phân tích thời gian biên dịch. Tuy nhiên, đây vẫn là một sự đánh đổi - có một số trường hợp bạn không có cách nào tốt để xử lý việc phân nhánh xung quanh những người dân địa phương có khả năng chưa được chỉ định. Trong thực tế, tôi đã không tìm thấy bất kỳ trường hợp nào mà đây không phải là một thiết kế tồi ở nơi đầu tiên và được giải quyết tốt hơn thông qua việc xem xét lại mã để tránh sự phân nhánh phức tạp (rất khó để con người phân tích), nhưng ít nhất là có thể.
Luaan

42

Hành vi không xác định cho phép tối ưu hóa đáng kể, bằng cách cho vĩ độ trình biên dịch thực hiện điều gì đó kỳ lạ hoặc bất ngờ (hoặc thậm chí bình thường) ở ranh giới nhất định hoặc các điều kiện khác.

Xem http://blog.llvm.org/2011/05/what-every-c-programmer-should-ledge.html

Sử dụng một biến chưa được khởi tạo: Đây thường được gọi là nguồn của các vấn đề trong các chương trình C và có nhiều công cụ để nắm bắt những điều này: từ cảnh báo của trình biên dịch đến các máy phân tích tĩnh và động. Điều này cải thiện hiệu suất bằng cách không yêu cầu tất cả các biến được khởi tạo bằng 0 khi chúng đi vào phạm vi (như Java thực hiện). Đối với hầu hết các biến vô hướng, điều này sẽ gây ra ít chi phí, nhưng các mảng ngăn xếp và bộ nhớ malloc'd sẽ phải chịu một bộ nhớ lưu trữ, điều này có thể khá tốn kém, đặc biệt là vì bộ lưu trữ thường bị ghi đè hoàn toàn.


Tràn số nguyên đã ký: Nếu số học trên loại 'int' (ví dụ) tràn, kết quả không được xác định. Một ví dụ là "INT_MAX + 1" không được bảo đảm là INT_MIN. Hành vi này cho phép một số lớp tối ưu hóa quan trọng đối với một số mã. Ví dụ: biết rằng INT_MAX + 1 không xác định cho phép tối ưu hóa "X + 1> X" thành "true". Biết phép nhân tràn "không thể" (vì làm như vậy sẽ không được xác định) cho phép tối ưu hóa "X * 2/2" thành "X". Mặc dù những thứ này có vẻ tầm thường, nhưng những thứ này thường được phơi bày bằng cách mở rộng nội tuyến và vĩ mô. Một tối ưu hóa quan trọng hơn mà điều này cho phép là cho các vòng lặp "<=" như thế này:

for (i = 0; i <= N; ++i) { ... }

Trong vòng lặp này, trình biên dịch có thể giả định rằng vòng lặp sẽ lặp lại chính xác N + 1 lần nếu "i" không được xác định khi tràn, điều này cho phép một phạm vi tối ưu hóa vòng lặp rộng để khởi động. Mặt khác, nếu biến được xác định là quấn quanh tràn, sau đó trình biên dịch phải giả sử rằng vòng lặp có thể là vô hạn (xảy ra nếu N là INT_MAX) - sau đó vô hiệu hóa các tối ưu hóa vòng lặp quan trọng này. Điều này đặc biệt ảnh hưởng đến các nền tảng 64 bit vì rất nhiều mã sử dụng "int" làm biến cảm ứng.


27
Tất nhiên, lý do thực sự tại sao tràn số nguyên đã ký không được xác định là vì khi C được phát triển, có ít nhất ba cách biểu diễn khác nhau của các số nguyên đã ký (bổ sung của một, bổ sung hai, cường độ ký hiệu và có lẽ là bù nhị phân) và mỗi kết quả cho một kết quả khác nhau cho INT_MAX + 1. Làm cho tràn không xác định giấy phép a + bđược biên dịch theo add b ahướng dẫn gốc trong mọi tình huống, thay vì có khả năng yêu cầu trình biên dịch mô phỏng một số dạng số học số nguyên đã ký khác.
Đánh dấu

2
Cho phép số nguyên tràn ra để hành xử theo kiểu lỏng lẻo cho phép tối ưu hóa đáng kể trong trường hợp tất cả các hành vi có thể sẽ đáp ứng yêu cầu ứng dụng . Hầu hết các tối ưu hóa đó sẽ bị mất, tuy nhiên, nếu các lập trình viên được yêu cầu để tránh tràn số nguyên bằng mọi giá.
supercat

5
@supercat Đó là một lý do khác tại sao việc tránh hành vi không xác định phổ biến hơn trong các ngôn ngữ gần đây - thời gian lập trình viên được đánh giá cao hơn nhiều so với thời gian CPU. Loại tối ưu hóa C được phép thực hiện nhờ vào UB về cơ bản là vô nghĩa trên các máy tính để bàn hiện đại và khiến cho việc lập luận về mã trở nên khó khăn hơn nhiều (không đề cập đến các hàm ý bảo mật). Ngay cả trong mã quan trọng về hiệu năng, bạn có thể hưởng lợi từ việc tối ưu hóa ở mức độ cao sẽ khó hơn một chút (hoặc thậm chí khó hơn nhiều) trong C. Tôi có trình kết xuất 3D phần mềm của riêng tôi trong C # và có thể sử dụng ví dụ như một điều HashSettuyệt vời.
Luaan

2
@supercat: Wrt_loosely xác định_, lựa chọn logic cho tràn số nguyên sẽ là yêu cầu Hành vi được xác định thực hiện . Đó là một khái niệm hiện có và nó không phải là gánh nặng quá lớn đối với việc triển khai. Hầu hết mọi người sẽ bỏ đi với "đó là sự bổ sung của 2 với sự bao bọc", tôi nghi ngờ. <<có thể là trường hợp khó khăn
MSalters

@MSalters Có một giải pháp đơn giản và được nghiên cứu kỹ mà không phải là hành vi không xác định hoặc hành vi được xác định thực hiện: hành vi không xác định. Đó là, bạn có thể nói " x << yđánh giá một số giá trị hợp lệ của loại int32_tnhưng chúng tôi sẽ không nói là". Điều này cho phép người triển khai sử dụng giải pháp nhanh, nhưng không hoạt động như một điều kiện tiên quyết sai cho phép tối ưu hóa kiểu du hành thời gian bởi vì tính không điều kiện bị hạn chế ở đầu ra của thao tác này - thông số kỹ thuật đảm bảo rằng bộ nhớ, biến dễ bay hơi, v.v. bằng cách đánh giá biểu thức. ...
Mario Carneiro

20

Trong những ngày đầu của C, có rất nhiều hỗn loạn. Trình biên dịch khác nhau đối xử với ngôn ngữ khác nhau. Khi có hứng thú viết một đặc tả cho ngôn ngữ, đặc tả đó sẽ cần tương thích ngược với C mà các lập trình viên đang dựa vào trình biên dịch của họ. Nhưng một số trong những chi tiết đó là không thể mang theo và nói chung không có ý nghĩa, ví dụ như giả sử một bố cục cụ thể hoặc bố cục dữ liệu. Do đó, tiêu chuẩn C bảo lưu rất nhiều chi tiết dưới dạng hành vi không xác định hoặc được chỉ định thực hiện, điều này để lại rất nhiều tính linh hoạt cho người viết trình biên dịch. C ++ xây dựng dựa trên C và cũng có các hành vi không xác định.

Java đã cố gắng trở thành một ngôn ngữ an toàn và đơn giản hơn nhiều so với C ++. Java định nghĩa ngữ nghĩa ngôn ngữ theo nghĩa của một máy ảo kỹ lưỡng. Điều này để lại không gian nhỏ cho hành vi không xác định, mặt khác, nó tạo ra các yêu cầu có thể khó thực hiện Java (ví dụ: các phép gán tham chiếu phải là nguyên tử hoặc cách các số nguyên hoạt động). Trong đó Java hỗ trợ các hoạt động không an toàn tiềm tàng, chúng thường được máy ảo kiểm tra khi chạy (ví dụ: một số phôi).


Vì vậy, bạn đang nói, khả năng tương thích ngược là lý do duy nhất tại sao C và C ++ không thoát khỏi các hành vi không xác định?
Sisir

3
Đó chắc chắn là một trong những cái lớn hơn, @Sisir. Ngay cả trong số các lập trình viên có kinh nghiệm, bạn sẽ ngạc nhiên bao nhiêu thứ mà không nên phá vỡ không phá vỡ khi một trình biên dịch thay đổi cách nó xử lý hành vi không xác định. (Trường hợp tại điểm, đã có một chút của sự hỗn loạn khi GCC bắt đầu tối ưu hóa ra "được thiskiểm tra trong một thời gian trở lại, với lý do null?" thisHạnh phúc nullptrlà UB, và do đó có thể không bao giờ thực sự xảy ra.)
Justin Time 2 Khôi phục Monica

9
@Sisir, một cái lớn khác là tốc độ. Trong những ngày đầu của C, phần cứng không đồng nhất hơn nhiều so với ngày nay. Bằng cách đơn giản là không chỉ định điều gì xảy ra khi bạn thêm 1 vào INT_MAX, bạn có thể để trình biên dịch làm bất cứ điều gì nhanh nhất cho kiến ​​trúc (ví dụ: hệ thống bổ trợ của một người sẽ tạo ra -INT_MAX, trong khi hệ thống hai phần bổ sung sẽ tạo ra INT_MIN). Tương tự, bằng cách không chỉ định điều gì xảy ra khi bạn đọc hết phần cuối của mảng, bạn có thể có một hệ thống bảo vệ bộ nhớ chấm dứt chương trình, trong khi một hệ thống không cần phải thực hiện kiểm tra giới hạn thời gian chạy đắt tiền.
Đánh dấu

14

Các ngôn ngữ JVM và .NET có thể dễ dàng:

  1. Họ không phải làm việc trực tiếp với phần cứng.
  2. Họ chỉ phải làm việc với các hệ thống máy tính để bàn và máy chủ hiện đại hoặc các thiết bị tương tự hợp lý hoặc ít nhất là các thiết bị được thiết kế cho chúng.
  3. Họ có thể áp đặt bộ sưu tập rác cho tất cả bộ nhớ và bắt buộc khởi tạo, do đó có được sự an toàn của con trỏ.
  4. Họ được chỉ định bởi một diễn viên duy nhất cũng cung cấp việc thực hiện dứt khoát duy nhất.
  5. Họ được chọn để an toàn hơn hiệu suất.

Có những điểm tốt cho các lựa chọn mặc dù:

  1. Lập trình hệ thống là một trò chơi hoàn toàn khác, và tối ưu hóa hoàn toàn cho lập trình ứng dụng thay vào đó là hợp lý.
  2. Phải thừa nhận rằng, có phần cứng kỳ lạ hơn mọi lúc, nhưng các hệ thống nhúng nhỏ vẫn ở đây.
  3. GC không phù hợp với tài nguyên không bị nấm và giao dịch nhiều không gian hơn để có hiệu suất tốt. Và hầu hết (nhưng không phải gần như tất cả) các khởi tạo bắt buộc có thể được tối ưu hóa đi.
  4. Có lợi thế để cạnh tranh nhiều hơn, nhưng các ủy ban có nghĩa là thỏa hiệp.
  5. Tất cả những giới hạn-kiểm tra làm thêm lên, mặc dù hầu hết có thể được tối ưu hóa đi. Kiểm tra con trỏ Null hầu hết có thể được thực hiện bằng cách bẫy truy cập với chi phí không nhờ vào không gian địa chỉ ảo, mặc dù tối ưu hóa vẫn bị ức chế.

Khi các cửa thoát hiểm được cung cấp, những người mời hành vi không xác định đầy đủ trở lại. Nhưng ít nhất chúng thường chỉ được sử dụng trong một số đoạn rất ngắn, do đó dễ xác minh thủ công hơn.


3
Thật. Tôi lập trình trong C # cho công việc của tôi. Cứ sau một thời gian tôi lại chạm tới một trong những cái búa không an toàn ( unsafetừ khóa hoặc thuộc tính trong System.Runtime.InteropServices). Bằng cách giữ những thứ này cho một vài lập trình viên, những người biết cách gỡ lỗi những thứ không được quản lý và một lần nữa, nó ít thực tế, chúng tôi giữ các vấn đề. Đã hơn 10 năm kể từ khi chiếc búa không an toàn liên quan đến hiệu suất cuối cùng nhưng đôi khi bạn phải làm điều đó bởi vì thực sự không có giải pháp nào khác.
Joshua

19
Tôi thường xuyên làm việc trên nền tảng từ các thiết bị tương tự trong đó sizeof (char) == sizeof (short) == sizeof (int) == sizeof (float) == 1. Nó cũng thực hiện bổ sung bão hòa (vì vậy INT_MAX + 1 == INT_MAX) và điều tuyệt vời ở C là tôi có thể có một trình biên dịch tuân thủ tạo mã hợp lý. Nếu ngôn ngữ bắt buộc nói rằng twos bổ sung với sự bao bọc thì mọi bổ sung sẽ kết thúc bằng một bài kiểm tra và một nhánh, một cái gì đó không bắt đầu trong phần tập trung DSP. Đây là một phần sản xuất hiện tại.
Dan Mills

5
@BenVoigt Một số người trong chúng ta sống trong một thế giới nơi một máy tính nhỏ có thể là 4k không gian mã, ngăn xếp cuộc gọi / trả lại 8 cấp cố định, 64 byte RAM, đồng hồ 1 MHz và có giá <0,20 đô la với số lượng 1.000 đô la. Một điện thoại di động hiện đại là một PC nhỏ với dung lượng lưu trữ không giới hạn khá nhiều cho mọi mục đích và mục đích, và có thể được coi là một PC. Không phải tất cả thế giới là đa lõi và thiếu các ràng buộc thời gian thực cứng.
Dan Mills

2
@DanMills: Không nói về điện thoại di động hiện đại ở đây với bộ xử lý Arm Cortex A, nói về "điện thoại tính năng" vào khoảng năm 2002. Có 192kB của SRAM nhiều hơn 64 byte (không phải là "nhỏ" mà là "nhỏ"), nhưng 192kB cũng không được gọi chính xác là máy tính để bàn hoặc máy chủ "hiện đại" trong 30 năm. Ngoài ra, những ngày này, 20 xu sẽ giúp bạn có được MSP430 với hơn 64 byte SRAM.
Ben Voigt

2
@BenVoigt 192kB có thể không phải là máy tính để bàn trong 30 năm qua, nhưng tôi có thể đảm bảo với bạn rằng nó hoàn toàn đủ để phục vụ các trang web, mà tôi sẽ cho rằng một máy chủ như vậy theo định nghĩa của từ này. Thực tế là đó là một lượng ram hoàn toàn hợp lý (hào phóng, thậm chí) cho rất nhiều ứng dụng nhúng thường bao gồm các máy chủ web cấu hình. Chắc chắn, tôi có thể không chạy amazon trên nó, nhưng tôi chỉ có thể đang chạy một tủ lạnh hoàn chỉnh với crapware IOT trên lõi như vậy (Với thời gian và không gian dự phòng). Đừng ai cần giải thích hoặc ngôn ngữ JIT cho điều đó!
Dan Mills

8

Java và C # được đặc trưng bởi một nhà cung cấp vượt trội, ít nhất là trong giai đoạn đầu phát triển. (Sun và Microsoft tương ứng). C và C ++ là khác nhau; họ đã có nhiều triển khai cạnh tranh từ sớm. C đặc biệt chạy trên nền tảng phần cứng kỳ lạ, quá. Kết quả là, có sự khác biệt giữa các triển khai. Các ủy ban ISO đã chuẩn hóa C và C ++ có thể đồng ý về mẫu số chung lớn, nhưng ở các cạnh mà việc triển khai khác nhau, các tiêu chuẩn còn lại để thực hiện.

Điều này cũng là bởi vì việc chọn một hành vi có thể tốn kém đối với các kiến ​​trúc phần cứng thiên về một lựa chọn khác - endian là sự lựa chọn rõ ràng.


Nghĩa đen của mẫu số lớn có nghĩa là gì? Bạn đang nói về tập hợp con hoặc supersets? Bạn có thực sự có nghĩa là đủ các yếu tố chung? Đây giống như là bội số chung nhỏ nhất hay yếu tố chung lớn nhất? Điều này rất khó hiểu đối với chúng ta, những robot không biết nói lingo trên đường phố, chỉ là toán học. :)
tchrist

@tchrist: Hành vi phổ biến là một tập hợp con, nhưng tập hợp con này khá trừu tượng. Trong nhiều lĩnh vực không được chỉ định bởi tiêu chuẩn chung, việc triển khai thực tế phải đưa ra lựa chọn. Bây giờ một số trong những lựa chọn đó là khá rõ ràng và do đó được xác định theo triển khai, nhưng những lựa chọn khác thì mơ hồ hơn. Bố cục bộ nhớ trong thời gian chạy là một ví dụ: phải có một sự lựa chọn, nhưng không rõ bạn sẽ ghi lại nó như thế nào.
MSalters

2
C ban đầu được thực hiện bởi một anh chàng. Nó đã có rất nhiều UB, theo thiết kế. Mọi thứ chắc chắn trở nên tồi tệ hơn khi C trở nên phổ biến, nhưng UB đã ở đó ngay từ đầu. Pascal và Smalltalk có ít UB hơn rất nhiều và được phát triển cùng lúc. Ưu điểm chính của C là nó cực kỳ dễ chuyển - tất cả các vấn đề về tính di động được giao cho lập trình viên ứng dụng: P Tôi thậm chí đã chuyển một trình biên dịch C đơn giản sang CPU (ảo) của mình; làm một cái gì đó như LISP hoặc Smalltalk sẽ là nỗ lực lớn hơn nhiều (mặc dù tôi đã có một nguyên mẫu giới hạn cho thời gian chạy .NET :).
Luaan

@Luaan: Đó sẽ là Kernighan hay Ritchie? Và không, nó không có Hành vi không xác định. Tôi biết, tôi đã có tài liệu trình biên dịch stprinted AT & T ban đầu trên bàn của mình. Việc thực hiện đã làm những gì nó đã làm. Không có sự phân biệt giữa hành vi không xác định và không xác định.
MSalters

4
@MSalters Ritchie là người đầu tiên. Kernighan chỉ tham gia (không nhiều) sau đó. Chà, nó không có "Hành vi không xác định", vì thuật ngữ đó chưa tồn tại. Nhưng nó đã có hành vi tương tự mà ngày nay sẽ được gọi là không xác định. Vì C không có thông số kỹ thuật, thậm chí "không xác định" là một sự kéo dài :) Đó chỉ là điều mà trình biên dịch không quan tâm, và các chi tiết tùy thuộc vào các lập trình viên ứng dụng. Nó không được thiết kế để tạo ra các ứng dụng di động , chỉ có trình biên dịch có nghĩa là dễ dàng chuyển sang cổng.
Luaan

6

Lý do thực sự dẫn đến một sự khác biệt cơ bản về ý định giữa C và C ++ trên một mặt và Java và C # (chỉ cho một vài ví dụ) mặt khác. Vì lý do lịch sử, phần lớn các cuộc thảo luận ở đây nói về C thay vì C ++, nhưng (như bạn có thể đã biết) C ++ là hậu duệ khá trực tiếp của C, vì vậy những gì nó nói về C cũng áp dụng tương tự cho C ++.

Mặc dù chúng hầu như bị lãng quên (và sự tồn tại của chúng đôi khi thậm chí bị từ chối), các phiên bản đầu tiên của UNIX được viết bằng ngôn ngữ lắp ráp. Phần lớn (nếu không chỉ) mục đích ban đầu của C là chuyển UNIX từ ngôn ngữ lắp ráp sang ngôn ngữ cấp cao hơn. Một phần của ý định là viết càng nhiều hệ điều hành càng tốt bằng ngôn ngữ cấp cao hơn - hoặc nhìn nó từ hướng khác, để giảm thiểu số lượng phải viết bằng ngôn ngữ lắp ráp.

Để thực hiện điều đó, C cần cung cấp gần như cùng mức truy cập vào phần cứng như ngôn ngữ lắp ráp đã làm. PDP-11 (ví dụ) lấy các thanh ghi I / O ánh xạ tới các địa chỉ cụ thể. Ví dụ: bạn sẽ đọc một vị trí bộ nhớ để kiểm tra xem phím đã được nhấn trên bảng điều khiển hệ thống chưa. Một bit được đặt ở vị trí đó khi có dữ liệu đang chờ để đọc. Sau đó, bạn sẽ đọc một byte từ một vị trí được chỉ định khác để lấy mã ASCII của khóa đã được nhấn.

Tương tự, nếu bạn muốn in một số dữ liệu, bạn sẽ kiểm tra một vị trí được chỉ định khác và khi thiết bị đầu ra đã sẵn sàng, bạn sẽ ghi dữ liệu của mình vào một vị trí được chỉ định khác.

Để hỗ trợ trình điều khiển ghi cho các thiết bị như vậy, C cho phép bạn chỉ định một vị trí tùy ý bằng cách sử dụng một số loại số nguyên, chuyển đổi nó thành một con trỏ và đọc hoặc ghi vị trí đó trong bộ nhớ.

Tất nhiên, điều này có một vấn đề khá nghiêm trọng: không phải mọi cỗ máy trên trái đất đều có bộ nhớ được đặt giống hệt với PDP-11 từ đầu những năm 1970. Vì vậy, khi bạn lấy số nguyên đó, chuyển đổi nó thành một con trỏ, sau đó đọc hoặc viết thông qua con trỏ đó, không ai có thể cung cấp bất kỳ đảm bảo hợp lý nào về những gì bạn sẽ nhận được. Chỉ cần một ví dụ rõ ràng, đọc và viết có thể ánh xạ tới các thanh ghi riêng biệt trong phần cứng, do đó bạn (trái với bộ nhớ thông thường) nếu bạn viết một cái gì đó, sau đó cố gắng đọc lại, những gì bạn đọc có thể không khớp với những gì bạn đã viết.

Tôi có thể thấy một vài khả năng để lại:

  1. Xác định giao diện cho tất cả các phần cứng có thể - chỉ định địa chỉ tuyệt đối của tất cả các vị trí bạn có thể muốn đọc hoặc ghi để tương tác với phần cứng theo bất kỳ cách nào.
  2. Cấm mức độ truy cập đó và quy định rằng bất kỳ ai muốn làm những việc đó đều cần sử dụng ngôn ngữ lắp ráp.
  3. Cho phép mọi người làm điều đó, nhưng để họ đọc (ví dụ) hướng dẫn sử dụng cho phần cứng họ đang nhắm mục tiêu và viết mã để phù hợp với phần cứng họ đang sử dụng.

Trong số này, 1 dường như đủ kích thích mà hầu như không đáng để thảo luận thêm. 2 về cơ bản là vứt bỏ ý định cơ bản của ngôn ngữ. Điều đó khiến cho lựa chọn thứ ba về cơ bản là lựa chọn duy nhất họ có thể cân nhắc hợp lý.

Một điểm khác xuất hiện khá thường xuyên là kích thước của các loại số nguyên. C lấy "vị trí" intphải là kích thước tự nhiên được đề xuất bởi kiến ​​trúc. Vì vậy, nếu tôi đang lập trình VAX 32 bit, intcó thể là 32 bit, nhưng nếu tôi đang lập trình Univac 36 bit, intcó lẽ nên là 36 bit (v.v.). Có lẽ không hợp lý (và thậm chí có thể không khả thi) để viết một hệ điều hành cho máy tính 36 bit chỉ sử dụng các loại được đảm bảo là bội số của 8 bit. Có lẽ tôi chỉ hời hợt, nhưng dường như với tôi rằng nếu tôi đang viết một hệ điều hành cho máy 36 bit, có lẽ tôi muốn sử dụng ngôn ngữ hỗ trợ loại 36 bit.

Từ quan điểm ngôn ngữ, điều này dẫn đến vẫn còn nhiều hành vi không xác định. Nếu tôi lấy giá trị lớn nhất phù hợp với 32 bit, điều gì sẽ xảy ra khi tôi thêm 1? Trên phần cứng 32 bit thông thường, nó sẽ bị trục trặc (hoặc có thể gây ra một số lỗi phần cứng). Mặt khác, nếu nó chạy trên phần cứng 36 bit, nó sẽ chỉ ... thêm một. Nếu ngôn ngữ sẽ hỗ trợ viết hệ điều hành, bạn không thể đảm bảo một trong hai hành vi - bạn chỉ cần cho phép cả kích cỡ của loại và hành vi tràn thay đổi từ loại này sang loại khác.

Java và C # có thể bỏ qua tất cả điều đó. Họ không có ý định hỗ trợ viết hệ điều hành. Với họ, bạn có một vài lựa chọn. Một là làm cho phần cứng hỗ trợ những gì họ yêu cầu - vì họ yêu cầu các loại 8, 16, 32 và 64 bit, chỉ cần xây dựng phần cứng hỗ trợ các kích thước đó. Khả năng rõ ràng khác là ngôn ngữ chỉ chạy trên phần mềm khác cung cấp môi trường họ muốn, bất kể phần cứng cơ bản có thể muốn gì.

Trong hầu hết các trường hợp, đây không thực sự là một hoặc / hoặc sự lựa chọn. Thay vào đó, nhiều triển khai làm một chút của cả hai. Bạn thường chạy Java trên JVM chạy trên hệ điều hành. Thường xuyên hơn không, HĐH được viết bằng C và JVM bằng C ++. Nếu JVM đang chạy trên CPU ARM, rất có thể CPU sẽ bao gồm các phần mở rộng Jazelle của ARM, để điều chỉnh phần cứng chặt chẽ hơn với nhu cầu của Java, do đó, cần phải thực hiện ít hơn trong phần mềm và mã Java chạy nhanh hơn (hoặc ít hơn từ từ, dù sao đi nữa).

Tóm lược

C và C ++ có hành vi không xác định, bởi vì không ai xác định một giải pháp thay thế chấp nhận được cho phép họ thực hiện những gì họ dự định làm. C # và Java có một cách tiếp cận khác, nhưng cách tiếp cận đó không phù hợp (nếu có) với các mục tiêu của C và C ++. Cụ thể, dường như không cung cấp một cách hợp lý để viết phần mềm hệ thống (như hệ điều hành) trên hầu hết các phần cứng được lựa chọn tùy ý. Cả hai thường phụ thuộc vào các cơ sở được cung cấp bởi phần mềm hệ thống hiện có (thường được viết bằng C hoặc C ++) để thực hiện công việc của họ.


4

Các tác giả của Tiêu chuẩn C mong muốn độc giả của họ nhận ra điều gì đó mà họ cho là hiển nhiên và được ám chỉ trong Cơ sở lý luận đã xuất bản của họ, nhưng không nói thẳng: Ủy ban không cần phải ra lệnh cho các nhà văn biên dịch đáp ứng nhu cầu của khách hàng, vì khách hàng nên biết rõ hơn Ủy ban về nhu cầu của họ. Nếu rõ ràng là các trình biên dịch cho một số loại plaform dự kiến ​​sẽ xử lý một cấu trúc theo một cách nhất định, thì không ai quan tâm liệu Standard có nói rằng cấu trúc đó gọi ra Hành vi không xác định hay không. Tiêu chuẩn không bắt buộc các trình biên dịch tuân thủ xử lý một đoạn mã một cách hữu ích theo cách không ngụ ý rằng các lập trình viên nên sẵn sàng mua các trình biên dịch không.

Cách tiếp cận này để thiết kế ngôn ngữ hoạt động rất tốt trong một thế giới nơi các nhà văn biên dịch cần bán sản phẩm của họ cho khách hàng trả tiền. Nó hoàn toàn sụp đổ trong một thế giới nơi các nhà văn biên dịch bị cô lập khỏi các tác động của thị trường. Người ta nghi ngờ rằng các điều kiện thị trường thích hợp sẽ tồn tại để điều khiển một ngôn ngữ theo cách mà họ đã điều khiển ngôn ngữ trở nên phổ biến vào những năm 1990, và càng nghi ngờ hơn nữa là bất kỳ nhà thiết kế ngôn ngữ lành mạnh nào cũng muốn dựa vào các điều kiện thị trường như vậy.


Tôi cảm thấy rằng bạn đã mô tả một cái gì đó quan trọng ở đây, nhưng nó thoát khỏi tôi. Bạn có thể làm rõ câu trả lời của bạn? Đặc biệt là đoạn thứ hai: nó nói các điều kiện bây giờ và các điều kiện trước đó khác nhau, nhưng tôi không hiểu điều đó; những gì chính xác thay đổi? Ngoài ra, "cách" bây giờ khác với trước đây; có thể giải thích điều này quá?
anatolyg

4
Có vẻ như chiến dịch của bạn để thay thế tất cả các hành vi không xác định bằng hành vi không xác định hoặc một cái gì đó bị hạn chế hơn vẫn đang diễn ra mạnh mẽ.
Ded repeatator

1
@anatolyg: Nếu bạn chưa có, hãy đọc tài liệu Cơ sở lý luận C đã xuất bản (loại C99 Cơ sở lý luận trong Google). Trang 11 dòng 23-29 nói về "thị trường" và trang 13 dòng 5-8 nói về những gì được dự định liên quan đến tính di động. Bạn nghĩ ông chủ của một công ty biên dịch thương mại sẽ phản ứng thế nào nếu một người viết trình biên dịch nói với các lập trình viên phàn nàn rằng trình tối ưu hóa đã phá vỡ mã mà mọi trình biên dịch khác xử lý một cách hữu ích rằng mã của họ bị "hỏng" vì nó thực hiện các hành động không được xác định bởi Tiêu chuẩn và từ chối hỗ trợ vì điều đó sẽ thúc đẩy tiếp tục ...
supercat

1
... sử dụng các cấu trúc như vậy? Một quan điểm như vậy là dễ thấy trên các bảng hỗ trợ của clang và gcc, và đã phục vụ để cản trở sự phát triển của nội tại có thể tạo điều kiện tối ưu hóa dễ dàng và an toàn hơn nhiều so với ngôn ngữ gcc và clang muốn hỗ trợ.
supercat

1
@supercat: Bạn đang lãng phí hơi thở của mình khi phàn nàn với các nhà cung cấp trình biên dịch. Tại sao không hướng mối quan tâm của bạn đến các ủy ban ngôn ngữ? Nếu họ đồng ý với bạn, một lỗi sẽ được phát hành mà bạn có thể sử dụng để đánh bại các nhóm biên dịch qua đầu. Và quá trình đó nhanh hơn nhiều so với việc phát triển một phiên bản mới của ngôn ngữ. Nhưng nếu họ không đồng ý, ít nhất bạn sẽ có được lý do thực tế, trong khi các tác giả biên dịch sẽ lặp lại (lặp đi lặp lại) "Chúng tôi đã không chỉ định mã bị hỏng, quyết định đó được đưa ra bởi ủy ban ngôn ngữ và chúng tôi làm theo quyết định của họ. "
Ben Voigt

3

Cả C ++ và c đều có các tiêu chuẩn mô tả (dù sao cũng là phiên bản ISO).

Mà chỉ tồn tại để giải thích làm thế nào các ngôn ngữ hoạt động, và để cung cấp một tài liệu tham khảo duy nhất về ngôn ngữ là gì. Thông thường, các nhà cung cấp trình biên dịch và các nhà văn thư viện, dẫn đường và một số đề xuất được đưa vào tiêu chuẩn ISO chính.

Java và C # (hoặc Visual C #, mà tôi giả sử bạn muốn nói) có các tiêu chuẩn quy định . Họ cho bạn biết những gì trong ngôn ngữ dứt khoát trước thời hạn, cách thức hoạt động và những gì được coi là hành vi được phép.

Quan trọng hơn thế, Java thực sự có "triển khai tham chiếu" trong Open-JDK. (Tôi nghĩ Roslyn được coi là triển khai tham chiếu Visual C #, nhưng không thể tìm thấy nguồn cho việc đó.)

Trong trường hợp của Java, nếu có bất kỳ sự mơ hồ nào trong tiêu chuẩn và Open-JDK thì đó là một cách nhất định. Cách Open-JDK thực hiện nó là tiêu chuẩn.


Tình hình còn tồi tệ hơn thế: Tôi không nghĩ Ủy ban đã từng đạt được sự đồng thuận về việc liệu nó được cho là mô tả hay kê đơn.
supercat

1

Hành vi không xác định cho phép trình biên dịch tạo mã rất hiệu quả trên nhiều kiến ​​trúc sư. Câu trả lời của Erik đề cập đến tối ưu hóa, nhưng nó vượt xa hơn thế.

Ví dụ, tràn tràn đã ký là hành vi không xác định trong C. Trong thực tế, trình biên dịch dự kiến ​​sẽ tạo ra một mã bổ sung được ký đơn giản để CPU thực thi và hành vi sẽ là bất cứ điều gì mà CPU cụ thể đã làm.

Điều đó cho phép C thực hiện rất tốt và tạo ra mã rất nhỏ gọn trên hầu hết các kiến ​​trúc. Nếu tiêu chuẩn đã chỉ định rằng các số nguyên đã ký phải tràn theo một cách nhất định thì các CPU hoạt động khác nhau sẽ cần nhiều mã hơn để tạo ra một bổ sung được ký đơn giản.

Đó là lý do cho phần lớn hành vi không xác định trong C và tại sao những thứ như kích thước intkhác nhau giữa các hệ thống. Intphụ thuộc vào kiến ​​trúc và thường được chọn là loại dữ liệu nhanh nhất, hiệu quả nhất lớn hơn a char.

Trở lại khi C mới, những cân nhắc này rất quan trọng. Máy tính ít mạnh hơn, thường có tốc độ xử lý và bộ nhớ hạn chế. C đã được sử dụng khi hiệu suất thực sự quan trọng và các nhà phát triển dự kiến ​​sẽ hiểu làm thế nào máy tính hoạt động đủ tốt để biết những hành vi không xác định này thực sự sẽ là gì trên các hệ thống cụ thể của họ.

Các ngôn ngữ sau này như Java và C # ưa thích loại bỏ hành vi không xác định so với hiệu suất thô.


-5

Theo một nghĩa nào đó, Java cũng có nó. Giả sử, bạn đã đưa ra bộ so sánh không chính xác với Arrays.sort. Nó có thể ném ngoại lệ của nó phát hiện ra nó. Nếu không, nó sẽ sắp xếp một mảng theo một cách nào đó không được đảm bảo là bất kỳ cụ thể.

Tương tự như vậy nếu bạn sửa đổi biến từ một số chủ đề kết quả cũng không thể đoán trước.

C ++ chỉ đi xa hơn để tạo ra nhiều tình huống không xác định (hay đúng hơn là java đã quyết định xác định thêm các hoạt động) và để đặt tên cho nó.


4
Đó không phải là hành vi không xác định của loại chúng ta đang nói ở đây. "Bộ so sánh không chính xác" có hai loại: loại so sánh xác định tổng thứ tự và loại không có. Nếu bạn cung cấp một bộ so sánh xác định nhất quán thứ tự tương đối của các mục, thì hành vi được xác định rõ, đó không phải là hành vi mà lập trình viên muốn. Nếu bạn cung cấp một bộ so sánh không nhất quán về thứ tự tương đối, thì hành vi vẫn được xác định rõ: hàm sắp xếp sẽ đưa ra một ngoại lệ (có lẽ cũng không phải là hành vi mà lập trình viên muốn).
Đánh dấu

2
Đối với sửa đổi các biến, điều kiện chủng tộc thường không được coi là hành vi không xác định. Tôi không biết chi tiết về cách Java xử lý các bài tập cho dữ liệu được chia sẻ, nhưng biết triết lý chung của ngôn ngữ, tôi khá chắc chắn rằng nó bắt buộc phải là nguyên tử. Đồng thời gán 53 và 71 asẽ là hành vi không xác định nếu bạn có thể nhận được 51 hoặc 73 trong số đó, nhưng nếu bạn chỉ có thể nhận được 53 hoặc 71, thì điều đó được xác định rõ.
Đánh dấu

@Mark Với khối dữ liệu lớn hơn kích thước từ gốc của hệ thống (ví dụ: biến 32 bit trên hệ thống kích thước từ 16 bit), có thể có một kiến ​​trúc yêu cầu lưu trữ riêng từng phần 16 bit. (SIMD là một tình huống tiềm năng khác.) Trong trường hợp đó, ngay cả một phép gán mức mã nguồn đơn giản không nhất thiết phải là nguyên tử trừ khi trình biên dịch được chăm sóc đặc biệt để đảm bảo rằng nó được thực thi nguyên tử.
một CVn
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.