Các loại chiều rộng thay đổi đã được thay thế bằng các loại cố định trong C hiện đại?


21

Tôi đã bắt gặp một điểm thú vị ngày hôm nay trong một bài đánh giá về Code Review . @Veedrac khuyến nghị trong câu trả lời này rằng các loại kích thước thay đổi (ví dụ intlong) được thay thế bằng các loại kích thước cố định như uint64_tuint32_t. Trích dẫn từ các bình luận của câu trả lời đó:

Kích thước của int và long (và do đó các giá trị họ có thể giữ) phụ thuộc vào nền tảng. Mặt khác, int32_t luôn dài 32 bit. Sử dụng int chỉ có nghĩa là mã của bạn hoạt động khác nhau trên các nền tảng khác nhau, thường không phải là những gì bạn muốn.

Lý do đằng sau tiêu chuẩn không sửa các loại phổ biến được giải thích một phần ở đây bởi @supercat. C được viết để có thể di động trên các kiến ​​trúc, trái ngược với lắp ráp thường được sử dụng để lập trình hệ thống tại thời điểm đó.

Tôi nghĩ rằng mục đích thiết kế ban đầu là mỗi loại khác với int là thứ nhỏ nhất có thể xử lý các số có kích cỡ khác nhau và đó là kích thước "mục đích chung" thực tế nhất có thể xử lý +/- 32767.

Đối với tôi, tôi đã luôn luôn sử dụng intvà không thực sự lo lắng về các lựa chọn thay thế. Tôi đã luôn nghĩ rằng đó là loại tốt nhất với hiệu suất tốt nhất, kết thúc câu chuyện. Nơi duy nhất tôi nghĩ chiều rộng cố định sẽ hữu ích là khi mã hóa dữ liệu để lưu trữ hoặc để truyền qua mạng. Tôi cũng hiếm khi thấy các loại chiều rộng cố định trong mã được viết bởi người khác.

Tôi có bị mắc kẹt trong những năm 70 hay thực sự có một lý do để sử dụng inttrong thời đại của C99 và hơn thế nữa?


1
Một bộ phận người chỉ bắt chước người khác. Tôi tin rằng phần lớn các loại mã cố định bit đã được thực hiện không giới hạn. Không có lý do để thiết lập kích thước cũng không. Tôi có mã được tạo chủ yếu trên nền tảng 16 bit (MS-DOS và Xenix of 80), chỉ cần biên dịch và chạy ngày hôm nay trên bất kỳ 64 và lợi ích của kích thước từ và địa chỉ mới, chỉ cần biên dịch nó. Điều đó có nghĩa là việc tuần tự hóa để xuất / nhập dữ liệu là một thiết kế kiến ​​trúc rất quan trọng để giữ cho nó di động.
Luciano

Câu trả lời:


7

Có một huyền thoại phổ biến và nguy hiểm là các kiểu như uint32_tcứu lập trình viên khỏi phải lo lắng về kích thước của int. Mặc dù sẽ hữu ích nếu Ủy ban Tiêu chuẩn định nghĩa một phương tiện khai báo số nguyên với ngữ nghĩa độc lập với máy, các loại không dấu như uint32_tcó ngữ nghĩa quá lỏng lẻo để cho phép mã được viết theo kiểu vừa sạch vừa di động; hơn nữa, các kiểu đã ký như int32có ngữ nghĩa dành cho nhiều ứng dụng được xác định một cách chặt chẽ không cần thiết và do đó loại trừ những gì sẽ là tối ưu hóa hữu ích.

Hãy xem xét, ví dụ:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

Trên các máy intkhông thể giữ 4294967295 hoặc có thể giữ 18446744065119617025, chức năng đầu tiên sẽ được xác định cho tất cả các giá trị của nexponent, và hành vi của nó sẽ không bị ảnh hưởng bởi kích thước của int; hơn nữa, tiêu chuẩn sẽ không yêu cầu nó mang lại hành vi khác nhau trên các máy có bất kỳ kích thước nào của int một số giá trị nexponent, tuy nhiên, sẽ khiến nó gọi Hành vi không xác định trên các máy trong đó 4294967295 không thể biểu thị như một intnhưng 18446744065119617025 thì không.

Hàm thứ hai sẽ mang lại Hành vi không xác định cho một số giá trị của nexponenttrên các máy intkhông thể giữ 4611686014132420609, nhưng sẽ mang lại hành vi được xác định cho tất cả các giá trị nexponenttrên tất cả các máy có thể (các thông số kỹ thuật int32_tngụ ý rằng hành vi bao bọc bổ sung của hai máy trên đó nhỏ hơn int).

Trong lịch sử, mặc dù Tiêu chuẩn không nói gì về trình biên dịch nên làm gì khi inttràn vào upow, trình biên dịch sẽ liên tục mang lại hành vi giống như khi intđủ lớn để không tràn. Thật không may, một số trình biên dịch mới hơn có thể tìm cách "tối ưu hóa" các chương trình bằng cách loại bỏ các hành vi không bắt buộc theo Tiêu chuẩn.


3
Bất cứ ai tình cờ muốn thực hiện thủ công pow, hãy nhớ mã này chỉ là một ví dụ và không phục vụ cho exponent=0!
Đánh dấu

1
Tôi nghĩ rằng bạn nên sử dụng toán tử giảm tiền tố chứ không phải hậu tố, hiện tại nó đang thực hiện thêm 1 phép nhân, ví dụ exponent=1sẽ dẫn đến n được nhân với chính nó một lần, vì việc giảm được thực hiện sau kiểm tra, nếu việc tăng được thực hiện trước kiểm tra ( tức là --exponent), không có phép nhân nào được thực hiện và n sẽ được trả về.
ALXGTV

2
@MarkHurd: Hàm này được đặt tên kém, vì những gì nó thực sự tính toán được N^(2^exponent), nhưng các tính toán của biểu mẫu N^(2^exponent)thường được sử dụng để tính toán các hàm lũy thừa và mod-4294967296 lũy thừa là hữu ích cho những thứ như tính toán hàm băm của hai chuỗi băm được biết đến.
supercat

1
@ALXGTV: Hàm này có nghĩa là minh họa cho một cái gì đó tính toán một cái gì đó liên quan đến sức mạnh. Những gì nó thực sự tính toán là N ^ (2 ^ số mũ), là một phần của tính toán hiệu quả số mũ N ^, và có thể thất bại ngay cả khi N nhỏ (phép nhân lặp đi lặp lại của uint32_t31 sẽ không mang lại UB, nhưng hiệu quả cách tính 31 ^ N đòi hỏi phải tính toán 31 ^ (2 ^ N), điều này sẽ xảy ra
supercat

Tôi không nghĩ rằng đây là một cuộc tranh luận tốt. Mục đích không phải là làm cho các chức năng được xác định cho tất cả các đầu vào, hợp lý hay không; đó là để có thể lý do về kích thước và tràn. int32_tđôi khi đã xác định tràn và đôi khi không, đó là những gì bạn dường như đang đề cập, dường như có tầm quan trọng tối thiểu liên quan đến thực tế rằng nó cho phép tôi lý do về việc ngăn chặn tràn ngay từ đầu. Và nếu bạn thực sự muốn tràn được xác định, rất có thể bạn đang muốn kết quả modulo một số giá trị cố định - vì vậy dù sao bạn cũng đang sử dụng các loại chiều rộng cố định.
Veedrac

4

Đối với các giá trị liên quan chặt chẽ với con trỏ (và do đó, với số lượng bộ nhớ có thể định địa chỉ), chẳng hạn như kích thước bộ đệm, chỉ mục mảng và Windows ' lParam, có nghĩa là có một kiểu số nguyên với kích thước phụ thuộc kiến ​​trúc. Vì vậy, các loại kích thước thay đổi vẫn hữu ích. Đây là lý do tại sao chúng ta có typedefs size_t, ptrdiff_t, intptr_t, vv Họ được typedefs vì không ai trong số được xây dựng trong C nguyên loại cần được trỏ kích thước.

Vậy câu hỏi thực sự là liệu char, short, int, long, và long longvẫn còn có ích.

IME, các chương trình C và C ++ vẫn được sử dụng intcho hầu hết mọi thứ. Và hầu hết thời gian (tức là khi số của bạn nằm trong phạm vi ± 32 767 và bạn không có yêu cầu hiệu suất nghiêm ngặt), điều này hoạt động tốt.

Nhưng nếu bạn cần làm việc với các số trong phạm vi 17-32 bit (như dân số của các thành phố lớn) thì sao? Bạn có thể sử dụng int, nhưng điều đó sẽ khó mã hóa một sự phụ thuộc nền tảng. Nếu bạn muốn tuân thủ nghiêm ngặt tiêu chuẩn, bạn có thể sử dụng long, được đảm bảo ít nhất là 32 bit.

Vấn đề là tiêu chuẩn C không chỉ định kích thước tối đa cho loại số nguyên. Có các triển khai longlà 64 bit, tăng gấp đôi mức sử dụng bộ nhớ của bạn. Và nếu những điều này longxảy ra là các yếu tố của một mảng với hàng triệu mục, bạn sẽ làm bộ nhớ như điên.

Vì vậy, không phải loại nào intcũng longphù hợp để sử dụng ở đây nếu bạn muốn chương trình của mình vừa đa nền tảng vừa tiết kiệm bộ nhớ. Nhập int_least32_t.

  • Trình biên dịch I16L32 của bạn cung cấp cho bạn 32 bit long, tránh các vấn đề cắt ngắn củaint
  • Trình biên dịch I32L64 của bạn cung cấp cho bạn 32 bit int, tránh bộ nhớ lãng phí của 64 bit long.
  • Trình biên dịch I36L72 của bạn cung cấp cho bạn 36 bit int

OTOH, giả sử bạn không cần số lượng lớn hoặc mảng lớn nhưng bạn có nhu cầu về tốc độ. Và intcó thể đủ lớn trên tất cả các nền tảng, nhưng nó không nhất thiết phải là loại nhanh nhất: hệ thống 64 bit thường vẫn có 32 bit int. Nhưng bạn có thể sử dụng int_fast16_tvà nhận được “nhanh nhất” loại, cho dù đó là int, longhoặc long long.

Vì vậy, có trường hợp sử dụng thực tế cho các loại từ <stdint.h>. Các loại số nguyên tiêu chuẩn không có nghĩa gì cả. Đặc biệt long, có thể là 32 hoặc 64 bit, và có thể hoặc không đủ lớn để giữ một con trỏ, tùy thuộc vào ý thích của người viết trình biên dịch.


Một vấn đề với các loại như uint_least32_tlà tương tác của chúng với các loại khác thậm chí còn được chỉ định yếu hơn so với các loại uint32_t. IMHO, Tiêu chuẩn nên xác định các loại như uwrap32_tunum32_t, với ngữ nghĩa mà bất kỳ trình biên dịch nào xác định loại uwrap32_t, phải quảng bá là một loại không dấu trong các trường hợp tương tự như nó sẽ được quảng bá nếu intlà 32 bit và bất kỳ trình biên dịch nào xác định loại unum32_tphải đảm bảo rằng các chương trình khuyến mãi số học cơ bản luôn chuyển đổi nó thành một loại đã ký có khả năng giữ giá trị của nó.
supercat

Bên cạnh đó, chỉ số Standard cũng có thể xác định các loại mà lưu trữ và răng cưa là tương thích với intN_tuintN_t, và có được xác định ứng xử này sẽ phù hợp với intN_tuintN_t, nhưng đó sẽ cấp biên dịch một số tự do trong trường hợp đang giá trị giao bên ngoài phạm vi của họ [cho phép ngữ nghĩa tương tự những gì đã có lẽ dự định uint_least32_t, nhưng không có sự không chắc chắn như việc thêm một uint_least16_tvà một int32_tsẽ mang lại một kết quả đã ký hoặc được chỉ định.
supercat
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.