Câu hỏi Lập trình C: Làm thế nào để lập trình cho Unicode?


Điều kiện tiên quyết nào cần thiết để lập trình Unicode chặt chẽ?

Điều này có nghĩa là mã của tôi không nên sử dụng char các loại ở bất kỳ đâu và các chức năng cần được sử dụng có thể xử lý wint_t và wchar_t?

Và vai trò của các chuỗi ký tự nhiều byte trong kịch bản này là gì?


76
2018-02-08 21:22


gốc




Các câu trả lời:


Lưu ý rằng đây không phải là về "lập trình unicode nghiêm ngặt", nhưng một số kinh nghiệm thực tế.

Những gì chúng tôi đã làm ở công ty của tôi là tạo một thư viện bao bọc xung quanh thư viện ICU của IBM. Thư viện trình bao bọc có giao diện UTF-8 và chuyển đổi thành UTF-16 khi cần gọi ICU. Trong trường hợp của chúng tôi, chúng tôi không lo lắng quá nhiều về số lần truy cập hiệu suất. Khi hiệu suất là một vấn đề, chúng tôi cũng cung cấp các giao diện UTF-16 (sử dụng kiểu dữ liệu của chính chúng ta).

Các ứng dụng có thể vẫn chủ yếu là (sử dụng char), mặc dù trong một số trường hợp, chúng cần phải biết một số vấn đề nhất định. Ví dụ, thay vì strncpy () chúng ta sử dụng một trình bao bọc để tránh cắt các chuỗi UTF-8. Trong trường hợp của chúng tôi, điều này là đủ, nhưng người ta cũng có thể xem xét kiểm tra để kết hợp các ký tự. Chúng tôi cũng có trình bao bọc để đếm số lượng điểm mã, số lượng đồ thị, v.v.

Khi giao tiếp với các hệ thống khác, đôi khi chúng ta cần phải làm thành phần ký tự tùy chỉnh, vì vậy bạn có thể cần một số linh hoạt ở đó (tùy thuộc vào ứng dụng của bạn).

Chúng tôi không sử dụng wchar_t. Sử dụng ICU tránh các vấn đề không mong muốn trong tính di động (nhưng không phải các vấn đề không mong muốn khác, tất nhiên :-).


20
2018-02-08 22:44



Một chuỗi byte UTF-8 hợp lệ sẽ không bao giờ bị cắt (cắt ngắn) bởi strncpy. Các chuỗi UTF-8 hợp lệ có thể không chứa bất kỳ byte 0x00 nào (ngoại trừ byte null kết thúc). - Dan Moulding
@Dan Molding: nếu bạn strncpy (), nói, một chuỗi có chứa một ký tự Trung Quốc (có thể là 3 byte) thành một mảng char 2 byte, bạn tạo một chuỗi UTF-8 không hợp lệ. - Hans van Eck
1: Tôi cũng thích UTF-8. Wrappers ftw! - rubenvb
@Hans van Eck: Nếu trình bao bọc của bạn sao chép ký tự 3 byte đơn trung thành một mảng 2 byte, thì bạn sẽ cắt nó và tạo chuỗi không hợp lệ hoặc bạn sẽ có hành vi không xác định. Rõ ràng, nếu bạn đang sao chép dữ liệu xung quanh, mục tiêu cần phải đủ lớn; mà đi mà không nói. Quan điểm của tôi là strncpy sử dụng đúng cách là hoàn toàn an toàn để sử dụng với UTF-8. - Dan Moulding
@DanMoulding: Nếu bạn biết rằng bộ đệm mục tiêu của bạn đủ lớn, bạn chỉ có thể sử dụng strcpy (thực sự an toàn để sử dụng với UTF-8). Những người đang sử dụng strncpy có thể làm như vậy bởi vì họ không biết liệu bộ đệm đích có đủ lớn không, vì vậy chúng muốn truyền một số byte tối đa để sao chép - điều này thực sự có thể tạo ra các chuỗi UTF-8 không hợp lệ. - Frerich Raabe


C99 trở xuống

Tiêu chuẩn C (C99) cung cấp cho các ký tự rộng và các ký tự nhiều byte, nhưng vì không có sự đảm bảo về những gì các ký tự rộng có thể giữ, giá trị của chúng có phần hạn chế. Đối với một triển khai cụ thể, chúng cung cấp hỗ trợ hữu ích, nhưng nếu mã của bạn phải có khả năng di chuyển giữa các lần triển khai, không đủ bảo đảm rằng chúng sẽ hữu ích.

Do đó, cách tiếp cận được đề xuất bởi Hans van Eck (để viết một trình bao bọc xung quanh ICU - Các thành phần quốc tế cho Unicode - thư viện) là âm thanh, IMO.

Mã hóa UTF-8 có nhiều thành tích, một trong số đó là nếu bạn không gây rối với dữ liệu (bằng cách cắt bớt nó), thì nó có thể được sao chép bởi các hàm không nhận thức đầy đủ về sự phức tạp của UTF-8 mã hóa. Đây không phải là trường hợp với wchar_t.

Unicode đầy đủ là một định dạng 21-bit. Tức là, Unicode đặt điểm mã từ U + 0000 lên U + 10FFFF.

Một trong những điều hữu ích về định dạng UTF-8, UTF-16 và UTF-32 (trong đó UTF là viết tắt của Unicode Transformation Format - xem Unicode) là bạn có thể chuyển đổi giữa ba đại diện mà không mất thông tin. Mỗi có thể đại diện cho bất cứ điều gì mà những người khác có thể đại diện. Cả UTF-8 và UTF-16 đều là các định dạng nhiều byte.

UTF-8 được biết đến là một định dạng nhiều byte, với cấu trúc cẩn thận giúp tìm thấy sự khởi đầu của các ký tự trong một chuỗi đáng tin cậy, bắt đầu từ bất kỳ điểm nào trong chuỗi. Ký tự một byte có bit cao được đặt thành 0. Các ký tự nhiều byte có ký tự đầu tiên bắt đầu bằng một trong các mẫu bit 110, 1110 hoặc 11110 (đối với các ký tự 2 byte, 3 byte hoặc 4 byte), với các byte tiếp theo luôn bắt đầu 10. Các ký tự tiếp tục luôn nằm trong phạm vi 0x80 .. 0xBF. Có các quy tắc mà các ký tự UTF-8 phải được thể hiện ở định dạng tối thiểu có thể. Một hệ quả của các quy tắc này là các byte 0xC0 và 0xC1 (cũng 0xF5..0xFF) không thể xuất hiện trong dữ liệu UTF-8 hợp lệ.

 U+0000 ..   U+007F  1 byte   0xxx xxxx
 U+0080 ..   U+07FF  2 bytes  110x xxxx   10xx xxxx
 U+0800 ..   U+FFFF  3 bytes  1110 xxxx   10xx xxxx   10xx xxxx
U+10000 .. U+10FFFF  4 bytes  1111 0xxx   10xx xxxx   10xx xxxx   10xx xxxx

Ban đầu, người ta hy vọng rằng Unicode sẽ là một bộ mã 16 bit và mọi thứ sẽ phù hợp với không gian mã 16 bit. Thật không may, thế giới thực là phức tạp hơn, và nó đã được mở rộng đến mã hóa 21-bit hiện tại.

UTF-16 do đó là một đơn vị đơn vị (16-bit từ) mã cho 'Cơ bản Multilingual Plane', có nghĩa là các ký tự với mã Unicode điểm U + 0000 .. U + FFFF, nhưng sử dụng hai đơn vị (32-bit) cho ký tự bên ngoài phạm vi này. Do đó, mã hoạt động với mã hóa UTF-16 phải có khả năng xử lý mã hóa độ rộng biến đổi, giống như UTF-8 phải. Mã cho các ký tự đơn vị kép được gọi là thay thế.

Người thay thế là các điểm mã từ hai phạm vi giá trị Unicode đặc biệt, được dành riêng để sử dụng làm giá trị hàng đầu và cuối của các đơn vị mã được ghép nối trong UTF-16. Dẫn đầu, cũng được gọi là cao, thay thế là từ U + D800 đến U + DBFF, và trailing, hoặc thấp, thay thế là từ U + DC00 đến U + DFFF. Chúng được gọi là người thay thế, vì chúng không đại diện trực tiếp cho nhân vật, mà chỉ là một cặp.

UTF-32, tất nhiên, có thể mã hóa bất kỳ điểm mã Unicode nào trong một đơn vị lưu trữ. Đó là hiệu quả để tính toán nhưng không phải để lưu trữ.

Bạn có thể tìm thấy nhiều thông tin hơn tại ICU và các trang web Unicode.

C11 và <uchar.h>

Tiêu chuẩn C11 đã thay đổi các quy tắc, nhưng không phải tất cả các triển khai đã bắt kịp với những thay đổi ngay cả bây giờ (giữa năm 2017). Tiêu chuẩn C11 tóm tắt các thay đổi đối với hỗ trợ Unicode như sau:

  • Ký tự Unicode và chuỗi (<uchar.h>) (ban đầu được chỉ định trong   ISO / IEC TR 19769: 2004)

Những gì sau đây là một phác thảo tối thiểu của chức năng. Đặc điểm kỹ thuật bao gồm:

6.4.3 Tên ký tự phổ quát

Cú pháp
universal-character-name:
  \u  hex-quad
  \U  hex-quad hex-quad
hex-quad:
  chữ số thập lục phân thập lục phân   chữ số thập lục phân thập lục phân

7.28 Tiện ích Unicode <uchar.h>

Tiêu đề <uchar.h> khai báo các kiểu và hàm để thao tác các ký tự Unicode.

Các loại được khai báo là mbstate_t (được mô tả trong 7.29.1) và size_t (được mô tả trong 7.19);

char16_t

là một loại số nguyên không dấu được sử dụng cho các ký tự 16 bit và có cùng loại với uint_least16_t (được mô tả trong 7.20.1.2); và

char32_t

là loại số nguyên không dấu được sử dụng cho các ký tự 32 bit và có cùng loại với uint_least32_t (cũng được mô tả trong 7.20.1.2).

(Dịch các tài liệu tham khảo chéo: <stddef.h> định nghĩa size_t, <wchar.h> định nghĩa mbstate_t, và <stdint.h> định nghĩa uint_least16_t và uint_least32_t.) Các <uchar.h> header cũng định nghĩa một tập hợp tối thiểu các hàm chuyển đổi (có thể khởi động lại):

  • mbrtoc16()
  • c16rtomb()
  • mbrtoc32()
  • c32rtomb()

Có các quy tắc về các ký tự Unicode nào có thể được sử dụng trong số nhận dạng bằng cách sử dụng \unnnn hoặc là \U00nnnnnn ký hiệu. Bạn có thể phải tích cực kích hoạt hỗ trợ cho các ký tự đó trong số nhận dạng. Ví dụ, GCC yêu cầu -fextended-identifiers để cho phép chúng trong số nhận dạng.

Lưu ý rằng macOS Sierra (10.12.5), để đặt tên nhưng một nền tảng, không hỗ trợ <uchar.h>.


36
2018-02-09 07:00



Tôi nghĩ bạn đang bán wchar_t và bạn bè hơi ngắn ở đây. Những loại này là cần thiết để cho phép thư viện C xử lý văn bản trong bất kì mã hóa (bao gồm cả mã hóa không phải Unicode). Nếu không có các loại ký tự và chức năng rộng, thư viện C sẽ yêu cầu một bộ hàm xử lý văn bản cho mỗi mã hóa được hỗ trợ: hãy tưởng tượng có koi8len, koi8tok, koi8printf chỉ dành cho văn bản được mã hóa KOI-8 và utf8len, utf8tok, utf8printf cho văn bản UTF-8. Thay vào đó, chúng tôi may mắn có được một thiết lập các hàm này (không tính các hàm ASCII gốc): wcslen, wcstokvà wprintf. - Dan Moulding
Tất cả lập trình viên cần làm là sử dụng các hàm chuyển đổi ký tự của thư viện C (mbstowcs và bạn bè) để chuyển đổi bất kỳ mã hóa được hỗ trợ nào thành wchar_t. Một lần trong wchar_t định dạng, lập trình viên có thể sử dụng một tập hợp các hàm xử lý văn bản rộng mà thư viện C cung cấp. Việc triển khai thư viện C tốt sẽ hỗ trợ hầu như bất kỳ trình mã hóa nào mà hầu hết các lập trình viên sẽ cần (trên một trong các hệ thống của tôi, tôi có quyền truy cập vào 221 mã hóa độc đáo). - Dan Moulding
Theo như việc liệu chúng có đủ rộng để có ích hay không: tiêu chuẩn yêu cầu thực hiện phải đảm bảo rằng wchar_t đủ rộng để chứa bất kỳ ký tự nào được hỗ trợ bởi việc triển khai. Điều này có nghĩa là (có thể có một ngoại lệ đáng chú ý) hầu hết các triển khai sẽ đảm bảo rằng chúng đủ rộng để một chương trình sử dụng wchar_t sẽ xử lý mọi mã hóa được hệ thống hỗ trợ (Microsoft wchar_t chỉ rộng 16 bit có nghĩa là triển khai của chúng không hỗ trợ đầy đủ tất cả các mã hóa, đáng chú ý nhất là các mã hóa UTF khác nhau, nhưng chúng là ngoại lệ không phải là quy tắc). - Dan Moulding


Điều này Câu hỏi thường gặp là một sự giàu có của thông tin. Giữa trang đó và bài viết này của Joel Spolsky, bạn sẽ có một khởi đầu tốt.

Một kết luận tôi đã đi dọc đường:

  • wchar_t là 16 bit trên Windows, nhưng không nhất thiết phải 16 bit trên các nền tảng khác. Tôi nghĩ rằng đó là một điều ác cần thiết trên Windows, nhưng có lẽ có thể tránh được ở nơi khác. Lý do quan trọng trên Windows là bạn cần nó để sử dụng các tệp có ký tự không phải ASCII trong tên (cùng với phiên bản W của hàm).

  • Lưu ý rằng các API Windows sử dụng wchar_t chuỗi mong đợi mã hóa UTF-16. Cũng lưu ý rằng điều này khác với UCS-2. Lưu ý các cặp thay thế. Điều này trang thử nghiệm có các bài kiểm tra khai sáng.

  • Nếu bạn đang lập trình trên Windows, bạn không thể sử dụng fopen(), fread(), fwrite(), v.v. vì họ chỉ mất char * và không hiểu mã hóa UTF-8. Làm cho tính di động đau đớn.


9
2018-02-09 16:34



Lưu ý rằng stdio f* và bạn bè làm việc với char * trên mỗi nền tảng vì tiêu chuẩn nói như vậy - sử dụng wcs* thay vào đó cho wchar_t. - cat


Để thực hiện lập trình Unicode nghiêm ngặt:

  • Chỉ sử dụng các API chuỗi là nhận thức Unicode (KHÔNG PHẢI  strlen, strcpy, ... nhưng đối tác rộng nhất của họ wstrlen, wsstrcpy, ...)
  • Khi xử lý một khối văn bản, hãy sử dụng mã hóa cho phép lưu trữ các ký tự Unicode (utf-7, utf-8, utf-16, ucs-2, ...) mà không bị mất.
  • Kiểm tra xem bộ ký tự mặc định của hệ điều hành của bạn có tương thích với Unicode không (ví dụ: utf-8)
  • Sử dụng phông chữ tương thích với Unicode (ví dụ: arial_unicode)

Chuỗi ký tự nhiều byte là một mã hóa được mã hóa trước mã UTF-16 (mã được sử dụng bình thường với wchar_t) và có vẻ như với tôi nó chỉ là Windows thôi.

Tôi chưa bao giờ nghe nói về wint_t.


7
2018-02-08 21:56



wint_t là một kiểu được định nghĩa trong <wchar.h>, giống như wchar_t. Nó có vai trò tương tự đối với các ký tự rộng mà int có liên quan đến 'char'; nó có thể chứa bất kỳ giá trị ký tự rộng hoặc WEOF. - Jonathan Leffler


Điều quan trọng nhất là luôn phân biệt rõ ràng giữa văn bản và dữ liệu nhị phân. Cố gắng làm theo mô hình Python 3.x str so với bytes hoặc SQL TEXT so với BLOB.

Thật không may, C nhầm lẫn vấn đề bằng cách sử dụng char cho cả "ký tự ASCII" và int_least8_t. Bạn sẽ muốn làm một cái gì đó như:

typedef char UTF8; // for code units of UTF-8 strings
typedef unsigned char BYTE; // for binary data

Bạn cũng có thể muốn typedef cho các đơn vị mã UTF-16 và UTF-32, nhưng điều này phức tạp hơn vì mã hóa wchar_t không được xác định. Bạn sẽ cần chỉ là một bộ tiền xử lý #ifS. Một số macro hữu ích trong C và C ++ 0x là:

  • __STDC_UTF_16__ - Nếu được xác định, loại _Char16_t tồn tại và là UTF-16.
  • __STDC_UTF_32__ - Nếu được xác định, loại _Char32_t tồn tại và là UTF-32.
  • __STDC_ISO_10646__ - Nếu được xác định, thì wchar_t là UTF-32.
  • _WIN32 - Trên Windows, wchar_t là UTF-16, mặc dù điều này phá vỡ tiêu chuẩn.
  • WCHAR_MAX - Có thể được sử dụng để xác định kích thước của wchar_t, nhưng không cho dù hệ điều hành sử dụng nó để đại diện cho Unicode.

Điều này có nghĩa là mã của tôi nên   không sử dụng các loại char ở bất kỳ đâu và   các chức năng cần được sử dụng có thể   đối phó với wint_t và wchar_t?

Xem thêm:

Số UTF-8 là một bảng mã Unicode hoàn toàn hợp lệ sử dụng char* dây. Có lợi thế là nếu chương trình của bạn là minh bạch đối với các byte không phải ASCII (ví dụ: một dòng kết thúc trình biến đổi hoạt động trên \r và \n nhưng vượt qua các nhân vật khác không thay đổi), bạn sẽ không cần phải thay đổi gì cả!

Nếu bạn đi với UTF-8, bạn sẽ cần phải thay đổi tất cả các giả định char= ký tự (ví dụ: không gọi toupper trong một vòng lặp) hoặc char = cột màn hình (ví dụ: để gói văn bản).

Nếu bạn đi với UTF-32, bạn sẽ có sự đơn giản của các ký tự có chiều rộng cố định (nhưng không phải là chiều rộng cố định đồ thị, nhưng sẽ cần phải thay đổi loại của tất cả các chuỗi của bạn).

Nếu bạn đi với UTF-16, bạn sẽ phải loại bỏ cả giả định của các ký tự chiều rộng cố định  giả định các đơn vị mã 8 bit, làm cho con đường nâng cấp khó khăn nhất này từ mã hóa một byte.

Tôi muốn giới thiệu tránh  wchar_t vì nó không phải là đa nền tảng: Đôi khi nó là UTF-32, đôi khi nó là UTF-16, và đôi khi nó là mã hóa Đông Á mã Unicode. Tôi khuyên bạn nên sử dụng typedefs 

Quan trọng hơn, tránh TCHAR.


3
2017-08-18 13:45





Về cơ bản, bạn muốn xử lý các chuỗi trong bộ nhớ như mảng wchar_t thay cho char. Khi bạn thực hiện bất kỳ loại I / O nào (như đọc / ghi tệp), bạn có thể mã hóa / giải mã bằng cách sử dụng UTF-8 (đây có lẽ là mã hóa phổ biến nhất) đủ đơn giản để triển khai. Chỉ cần google các RFC. Vì vậy, trong bộ nhớ không có gì nên được đa byte. Một wchar_t đại diện cho một ký tự. Tuy nhiên, khi bạn đến serializing, đó là khi bạn cần mã hóa thành một cái gì đó như UTF-8, nơi một số ký tự được biểu diễn bằng nhiều byte.

Bạn cũng sẽ phải viết các phiên bản mới của strcmp vv cho các chuỗi ký tự rộng, nhưng đây không phải là một vấn đề lớn. Vấn đề lớn nhất sẽ được interop với các thư viện / mã hiện có mà chỉ chấp nhận mảng char.

Và khi nói đến sizeof (wchar_t) (bạn sẽ cần 4 byte nếu bạn muốn làm điều đó đúng), bạn luôn có thể xác định lại kích thước của nó với kích thước lớn hơn với typedef / macro hacks nếu bạn cần.


2
2018-02-09 06:40





Tôi sẽ không tin tưởng bất kỳ triển khai thư viện chuẩn nào. Chỉ cần cuộn các loại unicode của riêng bạn.

#include <windows.h>

typedef unsigned char utf8_t;
typedef unsigned short utf16_t;
typedef unsigned long utf32_t;

int main ( int argc, char *argv[] )
{
  int msgBoxId;
  utf16_t lpText[] = { 0x03B1, 0x0009, 0x03B2, 0x0009, 0x03B3, 0x0009, 0x03B4, 0x0000 };
  utf16_t lpCaption[] = L"Greek Characters";
  unsigned int uType = MB_OK;
  msgBoxId = MessageBoxW( NULL, lpText, lpCaption, uType );
  return 0;
}

2
2018-03-29 18:45