Làm thế nào để các biến trong C ++ lưu trữ loại của họ?


42

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ì?


8
Ai / bạn đang đề cập đến cái gì bởi " " trong " làm thế nào để nó theo dõi "? Trình biên dịch hoặc CPU hoặc một cái gì đó / một cái khác như ngôn ngữ hoặc chương trình?
Erik Eidt


8
@ErikEidt IMO OP rõ ràng có nghĩa là "chính biến" bởi "nó." Tất nhiên câu trả lời hai từ cho câu hỏi là "nó không".
alephzero

2
câu hỏi tuyệt vời! đặc biệt có liên quan ngày hôm nay với tất cả các ngôn ngữ ưa thích lưu trữ loại của họ.
Trevor Boyd Smith

@alephzero Đó rõ ràng là một câu hỏi hàng đầu.
Luaan

Câu trả lời:


105

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 đó flà một đối tượng của kiểu Foovà 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.


3
Rất toàn diện.
Ded repeatator

9
Lưu ý rằng để truy cập loại đối tượng đa hình, trình biên dịch vẫn phải biết rằng đối tượng thuộc về một họ thừa kế cụ thể (nghĩa là có một tham chiếu / con trỏ được gõ vào đối tượng, không phải void*).
Ruslan

5
+0 vì câu đầu tiên không đúng trong hai đoạn cuối sửa nó.
Marcin

3
Nói chung, những gì được lưu trữ khi bắt đầu một đối tượng đa hình là một con trỏ tới bảng phương thức ảo, không phải chính bảng đó.
Peter Green

3
@ v.oddou Trong đoạn văn của tôi, tôi đã bỏ qua một số chi tiết. 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 đó.
amon

51

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:

máy ép cam tự động

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à:

  • đúc kiểu không chính xác: các biểu mẫu rõ ràng được coi là chính xác và nó được lập trình viên để đảm bảo rằng anh ta không truyền void*đến orange*khi có một quả táo ở đầu kia của con trỏ,
  • các vấn đề quản lý bộ nhớ như con trỏ null, con trỏ lơ lửng hoặc phạm vi sử dụng; trình biên dịch không thể tìm thấy hầu hết trong số họ,
  • Tôi chắc chắn có một cái gì đó khác tôi đang thiếu.

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.


4
Trình biên dịch cố gắng kiểm tra xem hàm có được truyền một đối tượng đúng loại không, nhưng cả C và C ++ đều quá phức tạp để trình biên dịch chứng minh nó trong mọi trường hợp. Vì vậy, so sánh táo và cam của bạn với máy ép trái cây là khá hướng dẫn.
Calchas

@Calchas Cảm ơn bình luận của bạn! Câu này thực sự là một sự đơn giản hóa. Tôi đã xây dựng một chút về các vấn đề có thể xảy ra, chúng thực sự khá liên quan đến câu hỏi.
Frax

5
ẩn dụ tuyệt vời cho mã máy! ẩn dụ của bạn được thực hiện tốt hơn gấp 10 lần bởi hình ảnh quá!
Trevor Boyd Smith

2
"Tôi chắc chắn có một cái gì đó khác tôi đang thiếu." - Tất nhiên! C void*cưỡng chế foo*, các chương trình khuyến mãi số học thông thường, unionloại xảo quyệt, NULLso với nullptr, thậm chí chỉ một con trỏ xấu là UB, v.v. nó là như nó là
Kevin

@Kevin Tôi không nghĩ cần thêm C ở đây, vì câu hỏi chỉ được gắn thẻ là C ++. Và trong C ++ void*không hoàn toàn chuyển đổi thành foo*unionloại pucky không được hỗ trợ (có UB).
Ruslan

3

Một biến có một số thuộc tính cơ bản trong một ngôn ngữ như C:

  1. Một cái tên
  2. Một loại
  3. Phạm vi
  4. Một đời
  5. Một địa điểm
  6. Một giá trị

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.


1
Cẩn thận với "khi dịch xong, tên bị quên" ... việc liên kết được thực hiện thông qua tên ("ký hiệu không xác định xy") và có thể xảy ra trong thời gian chạy với liên kết động. Xem blog.fesnel.com/blog/2009/08/19/ . Không có biểu tượng gỡ lỗi, thậm chí bị tước: Bạn cần tên hàm (và, tôi giả sử là biến toàn cục) cho liên kết động. Vì vậy, chỉ có tên của các đối tượng nội bộ có thể bị lãng quên. Nhân tiện, danh sách tốt các thuộc tính biến.
Peter - Phục hồi Monica

@ PeterA.Schneider, bạn hoàn toàn đúng, trong bức tranh lớn, các trình liên kết và trình tải cũng tham gia và sử dụng tên của các hàm và biến (toàn cầu) xuất phát từ mã nguồn.
Erik Eidt

Một điều phức tạp nữa là một số trình biên dịch diễn giải các quy tắc, theo Tiêu chuẩn, nhằm để các trình biên dịch giả định một số điều sẽ không bí danh khi cho phép chúng coi các hoạt động liên quan đến các loại khác nhau là không có kết quả, ngay cả trong các trường hợp không liên quan đến răng cưa như được viết . Đưa ra một cái gì đó 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].member2không thể truy cập unionArray[i].member1mặc dù cả hai đều có nguồn gốc từ cùng một unionArray[].
supercat

Cho dù trình biên dịch có diễn giải chính xác ngôn ngữ hay không, công việc của nó là tạo ra các chuỗi lệnh máy mã thực hiện chương trình. Điều này có nghĩa là (tối ưu hóa modulo và nhiều yếu tố khác) cho mỗi lần truy cập biến trong mã nguồn, nó phải tạo ra một số hướng dẫn mã máy cho bộ xử lý biết kích thước và giải thích dữ liệu sẽ sử dụng cho vị trí lưu trữ. Bộ xử lý không nhớ bất cứ điều gì về biến, vì vậy mỗi lần cần truy cập vào biến, nó phải được hướng dẫn chính xác cách thực hiện.
Erik Eidt

2

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:

  • Thời gian biên dịch

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).

  • Thời gian chạy

Đầ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.


Nếu trong bất kỳ trường hợp nào bạn đang đề cập đến C # hoặc Java với "ngôn ngữ hướng mã byte" thì con trỏ không có nghĩa là bỏ qua chúng; hoàn toàn ngược lại: Con trỏ phổ biến hơn nhiều trong C # và Java (và do đó, một trong những lỗi phổ biến nhất trong Java là "NullPulumException"). Rằng họ được đặt tên là "tài liệu tham khảo" chỉ là một vấn đề về thuật ngữ.
Peter - Phục hồi Monica

@ PeterA.Schneider, chắc chắn, có NullPOINTERException, nhưng có một sự phân biệt rất rõ ràng giữa một tham chiếu và một con trỏ trong các ngôn ngữ tôi đã đề cập (như Java, ruby, có thể là C #, thậm chí là Perl ở một mức độ nào đó) với hệ thống loại của họ, bộ sưu tập rác, quản lý bộ nhớ tự động, v.v.; thường không thể nói rõ ràng một vị trí bộ nhớ (như char *ptr = 0x123trong 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.
AnoE

con trỏ "đi cùng với hệ thống loại" trong C ++ ;-). .
Peter - Phục hồi Monica

OK, @ PeterA.Schneider, tôi thực sự không nghĩ rằng chúng ta đang đạt được cấp độ ở đây. Tôi đã xóa đoạn mà tôi đã đề cập đến con trỏ, nó không làm gì cho câu trả lời.
AnoE

1

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 classcó một virtualhà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 classkhi 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 virtualhà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 ofstreamchứa một con trỏ tới ofstreambảng ảo, mỗi ifstreamcái cho ifstreambả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_casttypeofhoạt động.


"Tiêu chuẩn ngôn ngữ không nói với các lập trình viên", có lẽ bạn nên nhấn mạnh rằng "các lập trình viên" trong câu hỏi là những người viết gcc, clang, msvc, v.v., chứ không phải những người sử dụng chúng để biên dịch C ++ của họ.
Caleth

@Caleth Gợi ý tốt!
Davislor
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.