2016-09-03 26 views
18

Tôi đã trải qua ví dụ này có chức năng xuất ra một mẫu bit hex để biểu diễn một phao tùy ý.Tại sao truyền đến một con trỏ, sau đó dereference?

void ExamineFloat(float fValue) 
{ 
    printf("%08lx\n", *(unsigned long *)&fValue); 
} 

Tại sao lấy địa chỉ của fValue, truyền đến con trỏ dài chưa ký, sau đó dereference? Không phải tất cả các công việc chỉ tương đương với một diễn viên trực tiếp để unsigned dài?

printf("%08lx\n", (unsigned long)fValue); 

Tôi đã thử nó và câu trả lời là không giống nhau, vì vậy bối rối.

+9

Đây là hành vi không xác định. Đó là điều mà mọi người đã làm trước khi C được chuẩn hóa vào năm 1989 và một vài người không theo kịp thời điểm –

Trả lời

27
(unsigned long)fValue 

này chuyển giá trị float đến một giá trị unsigned long, theo "chuyển đổi số học bình thường".

*(unsigned long *)&fValue 

Mục đích ở đây là để có những địa chỉ mà fValue được lưu trữ, giả vờ rằng không có một float nhưng một unsigned long tại địa chỉ này, và sau đó đọc mà unsigned long. Mục đích là để kiểm tra mẫu bit được sử dụng để lưu trữ float trong bộ nhớ.

Như được hiển thị, điều này gây ra hành vi không xác định.

Lý do: Bạn không thể truy cập đối tượng thông qua con trỏ đến loại không tương thích với loại đối tượng.Các kiểu "Tương thích" là ví dụ (unsigned) char và mọi loại khác hoặc cấu trúc có cùng thành viên ban đầu (nói về C ở đây). Xem §6.5/7 N1570 để xem danh sách chi tiết (C11)

Solution (Lưu ý rằng việc sử dụng của tôi về "tương thích" là khác nhau - - rộng hơn trong văn bản tham chiếu.): Cast để unsigned char *, truy cập cá nhân byte của đối tượng và lắp ráp một unsigned long ra trong số họ:

unsigned long pattern = 0; 
unsigned char * access = (unsigned char *)&fValue; 
for (size_t i = 0; i < sizeof(float); ++i) { 
    pattern |= *access; 
    pattern <<= CHAR_BIT; 
    ++access; 
} 

lưu ý rằng (như @CodesInChaos chỉ ra) các xử lý trên các giá trị dấu chấm động như đang được lưu trữ với byte quan trọng nhất của nó đầu tiên ("big endian") . Nếu hệ thống của bạn sử dụng thứ tự byte khác nhau cho các giá trị dấu phẩy động bạn cần điều chỉnh (hoặc sắp xếp lại các byte ở trên unsigned long, bất kỳ điều gì thực tế hơn cho bạn).

+2

Liệu 'reinterpret_cast (fValue)' có được cho phép/định nghĩa trong C++ (giả sử kích thước kiểu phù hợp, tất nhiên)? – celtschk

+5

Mã gốc hoạt động miễn là tính cuối của float và số nguyên giống nhau (bỏ qua UB). Mã của bạn giả định người dùng lớn. Tôi muốn sử dụng 'memcpy' vào' uint32_t' (và xác nhận cho các kích thước phù hợp). – CodesInChaos

+2

@celtschk Tôi sẽ rất ngạc nhiên nếu thực sự sử dụng tham chiếu đó sẽ không được tính là vi phạm bí danh nghiêm ngặt. - "Một biểu thức giá trị của loại T1 có thể được chuyển đổi thành tham chiếu đến loại T2 khác. Kết quả là một giá trị hoặc xvalue đề cập đến cùng một đối tượng như giá trị ban đầu, nhưng với một kiểu khác. Không tạo tạm thời, không có bản sao nào thực hiện, không có hàm tạo hoặc hàm chuyển đổi nào được gọi. Tham chiếu kết quả chỉ có thể được truy cập một cách an toàn nếu được cho phép bởi các quy tắc bí danh loại "[(src)] (http://en.cppreference.com/w/cpp/language/reinterpret_cast) – CodesInChaos

3

Việc tạo kiểu trong C thực hiện cả chuyển đổi loại và chuyển đổi giá trị. Điểm nổi → chuyển đổi dài chưa ký được cắt bớt phần phân số của số dấu phẩy động và hạn chế giá trị thành phạm vi có thể có của một số không dấu dài. Việc chuyển đổi từ một kiểu con trỏ sang kiểu con trỏ khác không cần thay đổi về giá trị, do đó, sử dụng kiểu con trỏ là một cách để giữ cùng một biểu tượng trong bộ nhớ trong khi thay đổi loại được liên kết với đại diện đó.

Trong trường hợp này, đó là cách để có thể xuất biểu diễn nhị phân của giá trị dấu phẩy động.

+1

"Chuyển đổi từ loại con trỏ này sang loại khác không có thay đổi về giá trị", có thể có hoặc không thay đổi đại diện giá trị. Trên máy tính cá nhân hiện đại và thường thì không có. Nhưng điều này không liên quan đến vấn đề của OP anyway, tôi nghĩ rằng bạn có nghĩa là để nói một cái gì đó như thế, chuyển đổi loại con trỏ không thay đổi nội dung của bộ nhớ mà con trỏ trỏ đến. –

4

Giá trị dấu phẩy động có biểu diễn bộ nhớ: ví dụ: byte có thể biểu thị giá trị dấu phẩy động bằng cách sử dụng IEEE 754.

Biểu thức đầu tiên *(unsigned long *)&fValue sẽ diễn giải các byte này như thể đó là đại diện giá trị unsigned long. Trong thực tế trong tiêu chuẩn C nó kết quả trong một hành vi không xác định (theo cái gọi là "quy tắc bí danh nghiêm ngặt"). Trong thực tế, có những vấn đề như endianness phải được đưa vào tài khoản.

Biểu thức thứ hai (unsigned long)fValue là tuân thủ tiêu chuẩn C. Nó có một ý nghĩa chính xác:

C11 (n1570), § 6.3.1.4 nổi Real và số nguyên

Khi một giá trị hữu hạn các loại nổi thực được chuyển đổi sang một kiểu số nguyên khác hơn _Bool, các phần phân đoạn được loại bỏ (nghĩa là giá trị được cắt ngắn về 0). Nếu giá trị của phần tích phân không thể được biểu diễn bằng kiểu số nguyên, hành vi là không xác định.

4

*(unsigned long *)&fValue không tương đương với việc truyền trực tiếp tới unsigned long.

Việc quy đổi để (unsigned long)fValue chuyển đổi giá trị của fValue thành một unsigned long, sử dụng các quy tắc bình thường đối với chuyển đổi của một giá trị float đến một giá trị unsigned long. Việc biểu diễn giá trị đó trong một unsigned long (ví dụ, về các bit) có thể hoàn toàn khác với cách mà cùng một giá trị được biểu diễn trong một float.

Chuyển đổi *(unsigned long *)&fValue chính thức có hành vi không xác định. Nó diễn giải bộ nhớ bị chiếm bởi fValue như thể nó là một unsigned long. Thực tế (tức là đây là những gì thường xảy ra, mặc dù hành vi không xác định) điều này thường mang lại giá trị khá khác với fValue.

1

Như những người khác đã lưu ý, hãy đưa con trỏ đến loại không phải char để trỏ đến một loại không phải char khác và sau đó dereferencing là hành vi không xác định.

printf("%08lx\n", *(unsigned long *)&fValue) gọi hành vi không xác định này không nhất thiết có nghĩa là chạy chương trình cố gắng thực hiện một sự cố như vậy sẽ dẫn đến xóa ổ cứng hoặc làm cho các mũi mũi phun ra khỏi mũi (hai dấu hiệu của hành vi không xác định). Trên máy tính trong đó sizeof(unsigned long)==sizeof(float) và trên cả hai loại có cùng yêu cầu căn chỉnh, printf hầu như chắc chắn sẽ làm những gì người ta mong đợi, đó là in biểu diễn hex của giá trị dấu chấm động đang được đề cập.

Điều này không đáng ngạc nhiên. Tiêu chuẩn C công khai mời các triển khai để mở rộng ngôn ngữ. Nhiều người trong số các phần mở rộng là trong các lĩnh vực được, nói đúng, hành vi không xác định. Ví dụ, hàm POSIX dlsym trả về một void*, nhưng hàm này thường được sử dụng để tìm địa chỉ của một hàm thay vì một biến toàn cầu. Điều này có nghĩa là con trỏ void được trả về bởi dlsym cần phải được truyền đến một con trỏ hàm và sau đó dereferenced để gọi hàm. Đây rõ ràng là hành vi không xác định, nhưng nó vẫn hoạt động trên bất kỳ nền tảng tuân thủ POSIX nào. Điều này sẽ không hoạt động trên một máy kiến ​​trúc Harvard mà trên đó các con trỏ tới các hàm có các kích thước khác với các con trỏ tới dữ liệu.

Tương tự, hãy đưa con trỏ đến số float đến con trỏ đến số nguyên chưa dấu và sau đó dereferencing sẽ hoạt động trên hầu hết mọi máy tính. a float.

Điều đó nói rằng, sử dụng unsigned long cũng có thể khiến bạn gặp rắc rối. Trên máy tính của tôi, một unsigned long dài 64 bit và có yêu cầu căn chỉnh 64 bit. Điều này không tương thích với phao. Sẽ tốt hơn nếu sử dụng uint32_t - trên máy tính của tôi.


Các công đoàn Hack là một trong những con đường xung quanh đống lộn xộn này:

typedef struct { 
    float fval; 
    uint32_t ival; 
} float_uint32_t; 

Gán một float_uint32_t.fval và truy cập từ một `` float_uint32_t.ival` từng là hành vi không xác định. Đó không còn là trường hợp trong C. Không có trình biên dịch mà tôi biết thổi mũi quỷ cho công đoàn hack. Đây không phải là UB trong C++. Nó là bất hợp pháp. Cho đến C++ 11, một trình biên dịch C++ tuân thủ phải phàn nàn là tuân thủ.


Bất kỳ cách nào tốt hơn xung quanh đống lộn xộn này là sử dụng các định dạng %a, mà đã là một phần của chuẩn C từ năm 1999:

printf ("%a\n", fValue); 

Đây là đơn giản, dễ dàng, di động, và không có cơ hội của hành vi không xác định. Điều này in đại diện hệ thập lục phân/nhị phân của giá trị điểm nổi chính xác kép trong câu hỏi. Vì printf là một chức năng cổ, tất cả các đối số float được chuyển đổi thành double trước cuộc gọi đến printf. Chuyển đổi này phải chính xác theo phiên bản 1999 của tiêu chuẩn C. Người ta có thể nhận giá trị chính xác đó thông qua một cuộc gọi đến scanf hoặc các chị em của nó.

+0

Cảm ơn bạn đã thêm câu trả lời này, nó giúp làm rõ mọi thứ hơn nữa! cổ vũ. – bobbay

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