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:
- 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.
- 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.
- 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í" int
phả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, int
có thể là 32 bit, nhưng nếu tôi đang lập trình Univac 36 bit, int
có 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ọ.