Nếu tôi xác định một biến của một loại nhất định (theo như tôi biết, chỉ phân bổ dữ liệu cho nội dung của biến), làm thế nào để theo dõi loại biến đó là gì?
Nếu tôi xác định một biến của một loại nhất định (theo như tôi biết, chỉ phân bổ dữ liệu cho nội dung của biến), làm thế nào để theo dõi loại biến đó là gì?
Câu trả lời:
Các biến (hay nói chung hơn: các đối tượng của người khác theo nghĩa của C) không lưu trữ kiểu của chúng khi chạy. Theo như mã máy, chỉ có bộ nhớ được giải mã. Thay vào đó, các thao tác trên dữ liệu này diễn giải dữ liệu dưới dạng một loại cụ thể (ví dụ như dấu phẩy hoặc là con trỏ). Các loại chỉ được sử dụng bởi trình biên dịch.
Ví dụ, chúng ta có thể có một cấu trúc hoặc lớp struct Foo { int x; float y; };
và một biến Foo f {}
. Làm thế nào một truy cập trường auto result = f.y;
có thể được biên dịch? Trình biên dịch biết rằng đó f
là một đối tượng của kiểu Foo
và biết cách bố trí của Foo
-objects. Tùy thuộc vào chi tiết cụ thể của nền tảng, điều này có thể được biên dịch dưới dạng Đưa con trỏ đến điểm bắt đầu f
, thêm 4 byte, sau đó tải 4 byte và diễn giải dữ liệu này dưới dạng float. Trong các tập lệnh mã máy (bao gồm x86-64 ) có các hướng dẫn bộ xử lý khác nhau để tải float hoặc ints.
Một ví dụ trong đó hệ thống loại C ++ không thể theo dõi loại đối với chúng tôi là một liên minh như thế nào union Bar { int as_int; float as_float; }
. Một liên minh chứa tối đa một đối tượng thuộc nhiều loại khác nhau. Nếu chúng ta lưu trữ một đối tượng trong một liên minh, thì đây là loại hoạt động của liên minh. Chúng tôi chỉ phải cố gắng đưa loại đó ra khỏi liên minh, bất cứ điều gì khác sẽ là hành vi không xác định. Hoặc là chúng tôi biết rõ trong khi lập trình loại hoạt động là gì, hoặc chúng tôi có thể tạo một liên minh được gắn thẻ nơi chúng tôi lưu trữ một loại thẻ (thường là enum). Đây là một kỹ thuật phổ biến trong C, nhưng vì chúng ta phải giữ liên kết và thẻ loại đồng bộ nên điều này khá dễ bị lỗi. Một void*
con trỏ tương tự như một liên minh nhưng chỉ có thể giữ các đối tượng con trỏ, ngoại trừ các con trỏ hàm.
C ++ cung cấp hai cơ chế tốt hơn để xử lý các đối tượng thuộc loại không xác định: Chúng ta có thể sử dụng các kỹ thuật hướng đối tượng để thực hiện xóa kiểu (chỉ tương tác với đối tượng thông qua các phương thức ảo để chúng ta không cần biết loại thực tế) hoặc chúng ta có thể sử dụng std::variant
, một loại công đoàn an toàn.
Có một trường hợp trong đó C ++ lưu trữ loại đối tượng: nếu lớp của đối tượng đó có bất kỳ phương thức ảo nào (giao diện loại đa hình hình chữ nhật, giao diện aka). Mục tiêu của một cuộc gọi phương thức ảo là không xác định tại thời gian biên dịch và được giải quyết trong thời gian chạy dựa trên loại động của đối tượng (động động điều động). Hầu hết các trình biên dịch thực hiện điều này bằng cách lưu trữ một bảng chức năng ảo (v vableable) khi bắt đầu đối tượng. Vtable cũng có thể được sử dụng để lấy loại đối tượng khi chạy. Sau đó chúng ta có thể rút ra sự phân biệt giữa kiểu tĩnh đã biết của thời gian biên dịch của kiểu biểu thức và kiểu động của một đối tượng khi chạy.
C ++ cho phép chúng ta kiểm tra loại động của một đối tượng với typeid()
toán tử cung cấp cho chúng ta một std::type_info
đối tượng. Trình biên dịch biết loại đối tượng tại thời điểm biên dịch hoặc trình biên dịch đã lưu trữ thông tin loại cần thiết bên trong đối tượng và có thể truy xuất nó khi chạy.
void*
).
typeid(e)
hướng nội kiểu tĩnh của biểu thức e
. Nếu loại tĩnh là loại đa hình, biểu thức sẽ được ước tính và loại động của đối tượng đó được lấy. Bạn không thể trỏ typeid vào bộ nhớ của loại không xác định và nhận thông tin hữu ích. Ví dụ typeid của một công đoàn mô tả công đoàn, không phải là đối tượng trong công đoàn. Kiểu chữ của a void*
chỉ là một con trỏ rỗng. Và nó không thể được quy định void*
để có được nội dung của nó. Trong C ++ không có quyền anh trừ khi được lập trình rõ ràng theo cách đó.
Câu trả lời khác giải thích tốt về khía cạnh kỹ thuật, nhưng tôi muốn thêm một số "cách nghĩ về mã máy" chung chung.
Mã máy sau khi biên dịch khá ngu ngốc, và nó thực sự chỉ giả định rằng mọi thứ hoạt động như dự định. Nói rằng bạn có một chức năng đơn giản như
bool isEven(int i) { return i % 2 == 0; }
Nó lấy một int và phun ra một bool.
Sau khi bạn biên dịch nó, bạn có thể nghĩ về nó như một cái gì đó giống như máy ép cam tự động này:
Nó có trong cam, và trả lại nước trái cây. Nó có nhận ra loại đối tượng mà nó nhận được không? Không, chúng chỉ được coi là cam. Điều gì xảy ra nếu nó nhận được một quả táo thay vì một quả cam? Có lẽ nó sẽ vỡ. Không thành vấn đề, vì chủ sở hữu có trách nhiệm sẽ không thử sử dụng theo cách này.
Hàm trên cũng tương tự: nó được thiết kế để lấy ints, và nó có thể phá vỡ hoặc làm một cái gì đó không liên quan khi được cho ăn thứ khác. Nó (thường) không thành vấn đề, bởi vì trình biên dịch (nói chung) kiểm tra rằng nó không bao giờ xảy ra - và thực sự nó không bao giờ xảy ra trong mã được định dạng tốt. Nếu trình biên dịch phát hiện khả năng một hàm sẽ nhận giá trị gõ sai, nó sẽ từ chối biên dịch mã và trả về lỗi loại thay thế.
Thông báo trước là có một số trường hợp mã không đúng mà trình biên dịch sẽ vượt qua. Ví dụ là:
void*
đến orange*
khi có một quả táo ở đầu kia của con trỏ,Như đã nói, mã được biên dịch giống như máy ép trái cây - nó không biết nó xử lý cái gì, nó chỉ thực hiện các hướng dẫn. Và nếu hướng dẫn sai, nó sẽ phá vỡ. Đó là lý do tại sao các vấn đề trên trong C ++ dẫn đến sự cố không kiểm soát được.
void*
cưỡng chế foo*
, các chương trình khuyến mãi số học thông thường, union
loại xảo quyệt, NULL
so với nullptr
, thậm chí chỉ có một con trỏ xấu là UB, v.v. nó là như nó là
void*
không hoàn toàn chuyển đổi thành foo*
và union
loại pucky không được hỗ trợ (có UB).
Một biến có một số thuộc tính cơ bản trong một ngôn ngữ như C:
Trong mã nguồn của bạn , vị trí, (5), là khái niệm và vị trí này được gọi bằng tên của nó, (1). Vì vậy, một khai báo biến được sử dụng để tạo vị trí và không gian cho giá trị, (6) và trong các dòng nguồn khác, chúng tôi đề cập đến vị trí đó và giá trị mà nó giữ bằng cách đặt tên biến trong một số biểu thức.
Đơn giản hóa phần nào, một khi chương trình của bạn được dịch sang mã máy bởi trình biên dịch, vị trí, (5), là một số vị trí đăng ký bộ nhớ hoặc CPU và bất kỳ biểu thức mã nguồn nào tham chiếu biến được dịch thành chuỗi mã máy tham chiếu bộ nhớ đó hoặc vị trí đăng ký CPU.
Do đó, khi quá trình dịch hoàn thành và chương trình đang chạy trên bộ xử lý, tên của các biến bị lãng quên một cách hiệu quả trong mã máy và, các hướng dẫn được tạo bởi trình biên dịch chỉ tham chiếu đến các vị trí được gán của biến (chứ không phải là tên). Nếu bạn đang gỡ lỗi và yêu cầu gỡ lỗi, vị trí của biến được liên kết với tên, sẽ được thêm vào siêu dữ liệu cho chương trình, mặc dù bộ xử lý vẫn thấy các hướng dẫn mã máy sử dụng vị trí (không phải siêu dữ liệu đó). . đã được chuyển đổi thành địa điểm.)
Điều này cũng đúng với loại, phạm vi và thời gian tồn tại. Trình biên dịch tạo hướng dẫn mã máy biết phiên bản máy của vị trí, nơi lưu trữ giá trị. Các thuộc tính khác, như kiểu, được biên dịch thành mã nguồn được dịch dưới dạng các hướng dẫn cụ thể truy cập vị trí của biến. Ví dụ: nếu biến trong câu hỏi là byte 8 bit đã ký so với byte 8 bit không dấu, thì các biểu thức trong mã nguồn tham chiếu biến sẽ được dịch thành, giả sử, tải byte đã ký so với tải byte không dấu, khi cần thiết để đáp ứng các quy tắc của ngôn ngữ (C). Do đó, loại biến được mã hóa thành bản dịch mã nguồn thành các lệnh máy, hướng dẫn CPU cách diễn giải bộ nhớ hoặc vị trí thanh ghi CPU mỗi lần sử dụng vị trí của biến.
Điều cốt lõi là chúng ta phải nói cho CPU biết phải làm gì thông qua các hướng dẫn (và nhiều hướng dẫn hơn) trong bộ hướng dẫn mã máy của bộ xử lý. Bộ xử lý nhớ rất ít về những gì nó vừa làm hoặc được nói - nó chỉ thực hiện các hướng dẫn được đưa ra, và đó là công việc của trình biên dịch hoặc trình biên dịch lắp ráp để cung cấp cho nó một tập hợp đầy đủ các chuỗi lệnh để thao tác đúng các biến.
Bộ xử lý hỗ trợ trực tiếp một số loại dữ liệu cơ bản, như byte / word / int / long đã ký / unsign, float, double, v.v. Bộ xử lý thường sẽ không phàn nàn hoặc phản đối nếu bạn thay thế xử lý cùng một vị trí bộ nhớ như đã ký hoặc không dấu ví dụ, mặc dù đó thường là lỗi logic trong chương trình. Nhiệm vụ của lập trình là hướng dẫn bộ xử lý ở mọi tương tác với một biến.
Ngoài các kiểu nguyên thủy cơ bản đó, chúng ta phải mã hóa mọi thứ trong các cấu trúc dữ liệu và sử dụng các thuật toán để thao tác chúng theo các nguyên tắc đó.
Trong C ++, các đối tượng liên quan đến hệ thống phân cấp lớp cho đa hình có một con trỏ, thường ở đầu đối tượng, đề cập đến cấu trúc dữ liệu dành riêng cho lớp, giúp gửi đi ảo, truyền, v.v.
Tóm lại, bộ xử lý không biết hoặc nhớ mục đích sử dụng của các vị trí lưu trữ - nó thực thi các hướng dẫn mã máy của chương trình cho nó biết cách thao tác lưu trữ trong các thanh ghi CPU và bộ nhớ chính. Lập trình, sau đó, là công việc của phần mềm (và lập trình viên) sử dụng lưu trữ một cách có ý nghĩa và để trình bày một bộ hướng dẫn mã máy nhất quán cho bộ xử lý thực hiện toàn bộ chương trình.
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, clang và gcc có xu hướng cho rằng con trỏ unionArray[j].member2
không thể truy cập unionArray[i].member1
mặc dù cả hai đều có nguồn gốc từ cùng một unionArray[]
.
nếu tôi định nghĩa một biến của một loại nhất định thì làm thế nào để theo dõi loại biến đó.
Có hai giai đoạn có liên quan ở đây:
Trình biên dịch C biên dịch mã C thành ngôn ngữ máy. Trình biên dịch có tất cả thông tin mà nó có thể nhận được từ tệp nguồn của bạn (và các thư viện và bất kỳ thứ gì khác mà nó cần để thực hiện công việc của nó). Trình biên dịch C theo dõi những gì có nghĩa là gì. Trình biên dịch C biết rằng nếu bạn khai báo một biến là char
, nó là char.
Nó thực hiện điều này bằng cách sử dụng cái gọi là "bảng biểu tượng" liệt kê tên của các biến, loại của chúng và thông tin khác. Nó là một cấu trúc dữ liệu khá phức tạp, nhưng bạn có thể nghĩ về nó như chỉ theo dõi ý nghĩa của những cái tên dễ đọc của con người. Trong đầu ra nhị phân từ trình biên dịch, không có tên biến như thế này xuất hiện nữa (nếu chúng ta bỏ qua thông tin gỡ lỗi tùy chọn có thể được yêu cầu bởi lập trình viên).
Đầu ra của trình biên dịch - tệp thực thi được biên dịch - là ngôn ngữ máy, được hệ điều hành của bạn nạp vào RAM và được CPU thực thi trực tiếp. Trong ngôn ngữ máy, hoàn toàn không có khái niệm "loại" - nó chỉ có các lệnh hoạt động trên một số vị trí trong RAM. Các lệnh thực sự có một loại cố định mà chúng hoạt động (nghĩa là có thể có lệnh ngôn ngữ máy "thêm hai số nguyên 16 bit này được lưu trữ tại các vị trí RAM 0x100 và 0x521"), nhưng không có thông tin nào trong hệ thống byte tại các vị trí đó thực sự là đại diện cho số nguyên. Không có sự bảo vệ khỏi các lỗi loại ở đây.
char *ptr = 0x123
trong C). Tôi tin rằng cách sử dụng từ "con trỏ" của tôi sẽ khá rõ ràng trong ngữ cảnh này. Nếu không, vui lòng cho tôi một cái đầu và tôi sẽ thêm một câu vào câu trả lời.
Có một vài trường hợp đặc biệt quan trọng trong đó C ++ lưu trữ một kiểu khi chạy.
Giải pháp cổ điển là một liên minh phân biệt: một cấu trúc dữ liệu có chứa một trong một số loại đối tượng, cộng với một trường cho biết loại hiện đang chứa. Một phiên bản templated nằm trong thư viện chuẩn C ++ như std::variant
. Thông thường, thẻ sẽ là một enum
, nhưng nếu bạn không cần tất cả các bit lưu trữ cho dữ liệu của mình, thì đó có thể là một bitfield.
Trường hợp phổ biến khác của điều này là gõ động. Khi bạn class
có một virtual
hàm, chương trình sẽ lưu trữ một con trỏ tới hàm đó trong một bảng chức năng ảo , nó sẽ khởi tạo cho từng phiên bản của class
khi nó được xây dựng. Thông thường, điều đó có nghĩa là một bảng chức năng ảo cho tất cả các thể hiện của lớp và mỗi thể hiện giữ một con trỏ tới bảng thích hợp. (Điều này giúp tiết kiệm thời gian và bộ nhớ vì bảng sẽ lớn hơn nhiều so với một con trỏ.) Khi bạn gọi virtual
hàm đó thông qua một con trỏ hoặc tham chiếu, chương trình sẽ tìm kiếm con trỏ hàm trong bảng ảo. (Nếu nó biết loại chính xác tại thời gian biên dịch, nó có thể bỏ qua bước này.) Điều này cho phép mã gọi thực hiện loại dẫn xuất thay vì lớp cơ sở.
Điều làm cho điều này có liên quan ở đây là: mỗi cái ofstream
chứa một con trỏ tới ofstream
bảng ảo, mỗi ifstream
cái cho ifstream
bảng ảo, v.v. Đối với hệ thống phân cấp lớp, con trỏ bảng ảo có thể đóng vai trò là thẻ cho chương trình biết loại đối tượng lớp có!
Mặc dù tiêu chuẩn ngôn ngữ không nói cho những người thiết kế trình biên dịch cách họ phải triển khai thời gian chạy dưới mui xe, đây là cách bạn có thể mong đợi dynamic_cast
và typeof
hoạt động.