2009-04-03 36 views
30

Trong ANSI C, offsetof được định nghĩa như sau.Tại sao việc thực hiện offsetof() này hoạt động?

#define offsetof(st, m) \ 
    ((size_t) ((char *)&((st *)(0))->m - (char *)0)) 

Tại sao điều này sẽ không xảy ra lỗi phân đoạn vì chúng tôi đang dereferencing con trỏ NULL? Hoặc là một số loại trình biên dịch hack, nơi nó thấy rằng chỉ có địa chỉ của bù đắp được đưa ra, do đó, nó tĩnh tính toán địa chỉ mà không thực sự dereferencing nó? Ngoài ra là mã này di động?

+2

Đây có phải là câu hỏi đầu tiên tôi thấy trên SO phàn nàn về mã hoạt động không? :-) – paxdiablo

+2

Có người đó với nếu (0) {asm (nop)} nơi để nó ra làm một cái gì đó thất bại ... – RBerteig

+3

ANSI C (thực sự ISO C) không xác định định nghĩa này cho 'offsetof'. Nó chỉ đơn thuần chỉ rõ cách nó phải hành xử như thế nào. Định nghĩa thực tế là tùy thuộc vào từng triển khai và có thể thay đổi triển khai thực hiện này sang cài đặt khác. –

Trả lời

32

Không có điểm nào trong mã ở trên là bất kỳ điều gì bị hủy bỏ. Một tham số xảy ra khi * hoặc -> được sử dụng trên một giá trị địa chỉ để tìm giá trị được tham chiếu. Việc sử dụng duy nhất của * ở trên là trong một tuyên bố loại cho mục đích đúc.

Toán tử -> được sử dụng ở trên nhưng không được sử dụng để truy cập giá trị. Thay vào đó nó được sử dụng để lấy địa chỉ của giá trị. Dưới đây là mẫu mã không phải là macro nên làm cho nó rõ ràng hơn một chút

SomeType *pSomeType = GetTheValue(); 
int* pMember = &(pSomeType->SomeIntMember); 

Dòng thứ hai không thực sự gây ra sự thiếu cân nhắc (phụ thuộc thực hiện). Nó chỉ trả về địa chỉ của SomeIntMember trong giá trị pSomeType.

Những gì bạn thấy là rất nhiều sự lựa chọn giữa các loại tùy ý và con trỏ char. Lý do cho char là nó là một loại duy nhất (có lẽ là duy nhất) trong tiêu chuẩn C89 có kích thước rõ ràng. Kích thước là 1. Bằng cách đảm bảo kích thước là một, mã ở trên có thể làm phép thuật tà ác để tính toán độ lệch thực của giá trị.

+0

Tôi không có sẵn một tiêu chuẩn C, nhưng tôi nghĩ rằng tôi đã nhớ một điều gì đó trong C90 về việc không nhất thiết phải có khả năng sử dụng (không chỉ dereference) các địa chỉ tùy ý. Lý do cơ bản là các máy như 8086 và IBM 370 sử dụng thanh ghi phân đoạn và không thể tham chiếu đến toàn bộ không gian địa chỉ của chúng. –

+0

Trong tiêu chuẩn C, '->' trong '& (pSomeType-> SomeIntMember)' gây ra một tham số. Có lẽ bạn có thể làm rõ những gì bạn có nghĩa là khi bạn tuyên bố rằng nó không. –

2

Nó không segfault bởi vì bạn không dereferencing nó. Địa chỉ con trỏ đang được sử dụng như một con số được trừ từ một số khác, không được sử dụng để giải quyết các hoạt động bộ nhớ.

2

Nó tính toán độ lệch của thành viên m so với địa chỉ xuất phát của đại diện cho một đối tượng thuộc loại st.

((st *)(0)) dùng để chỉ một con trỏloại st *. &((st *)(0))->m đề cập đến địa chỉ của thành viên m trong đối tượng này. Vì địa chỉ xuất phát của đối tượng này là 0 (NULL), địa chỉ của thành viên m chính xác là giá trị bù trừ.

char * chuyển đổi và chênh lệch tính toán độ lệch theo byte. Theo thao tác con trỏ, khi bạn tạo sự khác biệt giữa hai con trỏ loại T *, kết quả là số đối tượng thuộc loại T được biểu thị giữa hai địa chỉ chứa bởi toán hạng.

+0

Sean, Tại sao phép trừ là cần thiết? chúng ta không thể quay lại (char *) & ((st *) (0)) -> m? – chappar

+0

Tôi nghĩ rằng trừ là không thực sự cần thiết, nhưng tôi không chắc chắn 100% ... –

+0

Có những triển khai C mà một con trỏ null không được đại diện bởi giá trị 0 trong nội bộ. Trên thực hiện như vậy, tôi giả sử rằng mã C này sẽ hoàn toàn thất bại vì trình biên dịch sẽ không biết cách xử lý con trỏ null trong số học con trỏ, hoặc nó có thể hoạt động nhờ phép trừ (vì biểu diễn con trỏ null cần Bị hủy bỏ). – vinc17

8

Trong ANSI C, offsetof KHÔNG được xác định như vậy. Một trong những lý do nó không được định nghĩa như vậy là một số môi trường thực sự sẽ ném ngoại lệ con trỏ null, hoặc sụp đổ theo những cách khác. Do đó, ANSI C để việc triển khai thực hiện offsetof() mở đối với các nhà xây dựng trình biên dịch.

Mã được hiển thị ở trên là điển hình cho trình biên dịch/môi trường không chủ động kiểm tra con trỏ NULL, nhưng không chỉ khi byte được đọc từ con trỏ NULL.

+0

Chỉ cần rõ ràng, macro 'offsetof()' rất phổ biến và được triển khai rộng rãi như được hiển thị trong câu hỏi, hoặc thậm chí đơn giản hơn nếu không có phép trừ, trên phần lớn các nền tảng mà con trỏ là các số nguyên có hiệu quả. Hầu hết các trình biên dịch C không chủ động kiểm tra con trỏ NULL. Các biểu thức được sử dụng không ** KHÔNG ** dereference * bất cứ điều gì * --- nó chỉ đơn giản là tính toán bù đắp bằng cách sử dụng một địa chỉ (mà sẽ xảy ra là số không) với một số học đơn giản bổ sung của offset nội bộ của thành viên. Khi tối ưu hóa, thậm chí không có bất kỳ bổ sung thời gian chạy nào được thực hiện. –

6

Để trả lời phần cuối của câu hỏi, mã không thể di chuyển được.

Kết quả trừ hai con trỏ được xác định và chỉ di chuyển nếu hai con trỏ trỏ đến các đối tượng trong cùng một mảng hoặc trỏ đến một đối tượng cuối cùng của mảng (7.6.2 Phụ gia khai thác, H & S Fifth Edition)

7

Mặc dù đó là một việc thực hiện đặc trưng của offsetof, nó không ra lệnh tiêu chuẩn, mà chỉ nói:

Các loại và macro sau được định nghĩa trong header chuẩn <stddef.h> [...]

offsetof(type,member-designator)

mà mở rộng để một biểu thức hằng số nguyên mà có kiểu size_t, giá trị của được bù đắp bằng byte, để các thành viên kết cấu (do member-designator), từ đầu cấu trúc của nó (do type). Loại và thành viên vấn thiết kế phải đảm bảo để cho

statictypet;

thì biểu thức &(t.member-designator) kết quả là một hằng số địa chỉ. (Nếu thành viên quy định là một trường bit, hành vi này là không xác định.)

đọc PJ Plauger của "Tiêu chuẩn C Thư viện" cho một cuộc thảo luận về nó và các mặt hàng khác trong <stddef.h> đó là tất cả các tính năng biên giới-line có thể (nên?) được trong ngôn ngữ thích hợp, và có thể yêu cầu hỗ trợ trình biên dịch đặc biệt.

Đó chỉ là sự quan tâm lịch sử, nhưng tôi đã sử dụng trình biên dịch ANSI C ban đầu trên 386/IX (xem, tôi đã nói với bạn về lãi suất lịch sử, vào khoảng năm 1990) đã bị lỗi trên phiên bản offsetof nhưng làm việc khi tôi sửa đổi nó thành:

#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024)) 

Đó là lỗi trình biên dịch các loại, ít nhất vì tiêu đề được phân phối với trình biên dịch và không hoạt động.

1

Liệt kê 1: Một tập hợp đại diện của offsetof() định nghĩa vĩ mô

// Keil 8051 compiler 
#define offsetof(s,m) (size_t)&(((s *)0)->m) 

// Microsoft x86 compiler (version 7) 
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m) 

// Diab Coldfire compiler 
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0)) 

typedef struct 
{ 
    int  i; 
    float f; 
    char c; 
} SFOO; 

int main(void) 
{ 
    printf("Offset of 'f' is %zu\n", offsetof(SFOO, f)); 
} 

Các nhà khai thác khác nhau trong vĩ mô được đánh giá trong một trật tự như vậy mà các bước sau đây được thực hiện:

  1. ((s *)0) mất số nguyên không và chuyển nó thành con trỏ đến s.
  2. ((s *)0)->m các tham chiếu mà con trỏ trỏ tới thành viên cấu trúc m.
  3. &(((s *)0)->m) tính địa chỉ của m.
  4. (size_t)&(((s *)0)->m) đưa kết quả vào loại dữ liệu thích hợp.

Theo định nghĩa, bản thân cấu trúc nằm tại địa chỉ 0.Sau đó địa chỉ của trường được trỏ đến (Bước 3 ở trên) phải là giá trị offset, tính theo byte, từ đầu của cấu trúc.

Các vấn đề liên quan