Văn bản từ người dùng, từ tệp và từ dòng lệnh đều đến dưới dạng ký tự. "42" là ba byte, gồm '4', '2', '\0', và chưa có gì trong đó là con số 42 cho đến khi bạn chuyển đổi nó. C có hai thế hệ hàm chuyển đổi: nhóm cũ, ngắn gọn nhưng không báo được lỗi, và nhóm dài hơn nhưng báo được. Trang này trình bày cả hai và giải thích vì sao chương trình thực tế dùng nhóm thứ hai.
Nhóm hàm cũ: atoi, atol, atof
Tên hàm nói lên công dụng: ASCII sang int, sang long, sang double. Một đối số (argument), một kết quả.
Khoảng trắng ở đầu bị bỏ qua, dấu tùy chọn được đọc, các chữ số được tiêu thụ, và mọi thứ phía sau bị bỏ qua. Như vậy rất tiện, cho đến khi đầu vào sai:
Cả ba dòng đều in ra 0. Không có mã trả về, không có cờ báo, không có cách nào hỏi "chuyển đổi có thành công không?", và một giá trị vượt quá phạm vi int là undefined behavior chứ không phải một lỗi được báo. Với một hằng số viết sẵn trong chương trình đồ chơi thì còn chấp nhận được. Với bất cứ thứ gì do người dùng hay tệp cung cấp, đây là nguồn sinh bug âm thầm.
Công cụ đúng: strtol
strtol (string to long) làm cùng phép chuyển đổi nhưng có hai kênh để báo chuyện gì đã xảy ra.
long strtol(const char *text, char **endptr, int base);
endptr được gán bằng ký tự đầu tiên mà hàm không tiêu thụ. base là cơ số: dùng 10, hoặc 0 để tiền tố của chính số đó quyết định (0x cho hệ thập lục phân, 0 ở đầu cho hệ bát phân).
Đọc endptr sẽ trả lời những câu hỏi mà atoi không trả lời được:
end == inputnghĩa là không có gì được chuyển đổi: văn bản không bắt đầu bằng một số.*end != '\0'nghĩa là còn nội dung phía sau. Đó có phải lỗi hay không là do bạn quyết định: không sao khi phân tích"42 apples", nhưng là vấn đề khi cả dòng lẽ ra phải là một số.
Tràn số được báo riêng qua errno. Cách viết đầy đủ và đúng đáng để viết một lần rồi dùng lại:
Ba chi tiết làm cho đoạn code này đúng. Gán errno = 0 trước khi gọi là cần thiết vì thư viện chỉ gán errno chứ không bao giờ xóa nó, nên một giá trị cũ từ lần gọi trước sẽ bị hiểu nhầm là lỗi của lần gọi này. strtol trả về long, có thể rộng hơn int, nên việc kiểm tra phạm vi với INT_MIN/INT_MAX tách biệt với ERANGE. Và trả về trạng thái thành công trong khi đưa giá trị ra qua con trỏ là cách các hàm C báo "hoặc có giá trị, hoặc thất bại": giá trị trả về không thể làm cả hai việc.
strtoul là phiên bản không dấu, còn strtoll/strtoull xử lý long long.
Các cơ số khác
Cơ số 16 đọc số thập lục phân; cơ số 0 xem tiền tố rồi tự quyết định. Để kiểm tra một chuỗi có nghĩa là gì ở cơ số khác, hãy dán nó vào công cụ chuyển đổi cơ số.
Cơ số 0 là một cái bẫy trong một trường hợp cụ thể: số thập phân có số 0 đứng đầu như "012" bị đọc thành bát phân và cho ra 10. Nếu đầu vào là số thập phân, hãy ghi rõ 10.
Số thực: strtod
strtod có cùng cách dùng nhưng cho double, và atof có cùng khuyết điểm như atoi.
strtod cũng nhận ký hiệu khoa học ("1.5e3"), số thực dạng hex, và các từ "inf" và "nan". Locale ảnh hưởng tới dấu phân cách thập phân, điều này hay làm người ta bất ngờ khi đọc tệp được tạo ở nơi khác. Với locale "C" mặc định, dấu phân cách luôn là dấu chấm.
Số thành chuỗi: snprintf
Theo chiều ngược lại, snprintf là lựa chọn chuẩn. Hàm này ghi tối đa đúng kích thước bạn đưa, luôn thêm ký tự kết thúc, và trả về độ dài mà kết quả đầy đủ lẽ ra cần.
Giá trị trả về chính là cách kiểm tra bị cắt. Nếu nó lớn hơn hoặc bằng kích thước buffer, văn bản đã không vừa:
Hai thứ không nên dùng: itoa không thuộc chuẩn C, nên code dùng nó sẽ không biên dịch được ở mọi nơi; còn sprintf là snprintf không có giới hạn kích thước, nghĩa là nó sẽ ghi tràn qua cuối buffer mà không hề báo gì.
Ký tự đơn lẻ và '0'
Các ký tự chữ số từ '0' đến '9' được đảm bảo nằm liên tiếp nhau, nên một phép trừ đơn giản là đủ để chuyển qua lại giữa ký tự chữ số và giá trị của nó.
Chỉ chữ số mới có tính chất này: chữ cái không bắt buộc phải liên tiếp, nên c - 'a' không khả chuyển khi tính toán trên bảng chữ cái. Hãy kiểm tra bằng isdigit trước khi tin vào phép trừ:
Ép kiểu sang unsigned char là bắt buộc vì cùng lý do như với chuỗi: các hàm trong <ctype.h> có hành vi không xác định với đối số âm, và char thường có thể là kiểu có dấu.
Nên dùng hàm nào
- Văn bản từ người dùng, từ tệp hoặc
argv→strtol/strtodvới đầy đủ bước kiểm tra. - Một hằng số bạn tự viết và kiểm soát →
atoichấp nhận được, dù không có gì nổi bật. - Số vào buffer →
snprintf, và kiểm tra giá trị trả về. - Một ký tự chữ số → trừ
'0', sau khi kiểm traisdigit. - Không bao giờ dùng →
gets,sprintf,itoa.
Câu hỏi thường gặp
Làm sao chuyển chuỗi sang int trong C?
Cách đúng là strtol(text, &end, 10): hàm này trả về giá trị, gán end bằng ký tự đầu tiên nó không đọc tới, và báo tràn số qua errno. atoi(text) ngắn hơn nhưng trả về 0 cho cả "0" lẫn "hello", và bạn không có cách nào phân biệt hai trường hợp này.
atoi trong C có vấn đề gì?
Hàm này không báo được thất bại. atoi("abc") trả về 0, không phân biệt được với số 0 thật, còn tràn số là undefined behavior chứ không phải một lỗi được báo. Hãy dùng strtol mỗi khi văn bản đến từ người dùng, từ tệp hay từ đối số dòng lệnh.
Làm sao chuyển int sang chuỗi trong C?
snprintf(buf, sizeof buf, "%d", n). Hàm này có trong chuẩn C, bị giới hạn bởi kích thước buffer và trả về số ký tự mà kết quả đầy đủ cần, nên giá trị trả về lớn hơn hoặc bằng sizeof buf cho biết văn bản đã bị cắt. itoa không thuộc chuẩn C.
Làm sao đổi ký tự '7' thành số 7 trong C?
Trừ đi '0': int digit = c - '0';. Các ký tự chữ số luôn nằm liên tiếp nhau trong mọi bộ ký tự của C, nên phép trừ cho ra từ 0 đến 9. Hãy kiểm tra bằng isdigit((unsigned char)c) trước, vì phép tính này vô nghĩa với mọi ký tự khác.