検証環境
端末: MacBook Air
OS: macOS High Sierra
Swift: 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2)
Clang: Apple LLVM version 9.0.0 (clang-900.0.39.2)
概要
皆様何気なくCの文字列(char*)をSwiftやObjective-Cの文字列(String, NSString)に変換する際に以下のメソッドを使用されているかと思います。
init?(cString: UnsafePointer<CChar>) //typealias CChar = Int8
まずこのメソッドに渡すcString、つまりCの文字列についてご紹介したいと思います。
次に、実はNSString側は上記のメソッドが廃止され、以下のようにencodingを同時に指定するメソッドが用意されています。この時に渡すencoding値について考えたいと思います。
init?(cString: UnsafePointer<CChar>, encoding enc: String.Encoding)
Cの文字について
C言語の文字を表す型としてchar
が用意されています。このchar型の変数には1バイトの値を格納することが出来ます。
Cの文字はシングルクォーテーションで文字を囲みます。そしてシングルクォーテーションで囲まれた値は、文字リテラル(character literal)と呼ばれます。
int main(void) {
char c = '*'; //「'」マークで文字を囲むことで1バイトのASCII値に変換される
printf("%c\n", c); // *
printf("%ld\n", sizeof(c)); // 1
}
コメントにも記載していますが、実は上記は以下の糖衣構文になっています。
int main(void) {
char c = 42;
printf("%c\n", c); // *
printf("%ld\n", sizeof(c)); // 1
}
つまり文字リテラルの実体は、「ただの数字」であるということです。
また以下のようにマルチバイト文字をシングルクォーテーションで囲んだ場合、生成される数字(*1)が、char
のサイズ(1バイト)を超えてしまうためコンパイルエラーになります。
(*1: 生成される値については、下記のセクション「Cの文字列にマルチバイト文字を格納した場合」で説明いたします)
// UTF8でこのソースコードファイルは保存されています
int main(void) {
char c = 'あ'; // error: character too large for enclosing character
}
つまりchar型の変数にはマルチバイト文字をそのまま格納することが出来ません。
また文字リテラルの実体が1バイトの数字であるという検証は以下のようにint型(4バイト)に値を直接格納出来ることでも証明できます。
int main(void) {
int num = 'abcd';
printf("%0x\n", num); // 64656667
}
numの値を16進数で出力した結果が「64656667」となっており、それぞれバイト単位で値を読み取ると、「64,65,66,67」と分解出来ることが分かります。
ここでのまとめ
- Cの文字は
char
型で表される -
char
型は1バイトの値を格納するための箱である - シングルクォーテーションで囲んだ値は、文字リテラルと呼ばれる
- 文字リテラルは
char
型の箱を返し、中には数字(エンコーディング値)を格納する
Cの文字列について
C言語の文字列はchar
型の配列です。つまり1バイトデータを格納するための配列で表現されています。
またCの文字列はダブルクォーテーションで文字を囲みます。またダブルクォーテーションで囲んだ値は、文字列リテラル(string literal)と呼ばれます。
int main(void) {
char str[] = "Hello"; // 同時に初期化を行うことで要素数を省略できます
printf("sizeof(str)/sizeof(char) = %ld\n", sizeof(str)/sizeof(char)); // 6
}
上記にて初期化に利用したHelloという文字列は5文字ですが、要素数が6となっております。
実は、以下の糖衣構文となっております。
int main(void) {
char str[] = {'H','e','l','l','o','\0'};
printf("sizeof(str)/sizeof(char) = %ld\n", sizeof(str)/sizeof(char)); //6
}
つまり "Hello"
という文字列リテラルは、最後にナル文字を格納した要素数6のchar配列を返すということです。
ここでのまとめ
- Cの文字列は
char
型の配列である - ダブルクォーテーションで囲んだ値は、文字列リテラル(String Literal)と呼ばれる
- 文字列リテラルは
char
型の配列を返し、最後の値はナル文字(数字の0)が格納されている
Cの文字列にマルチバイト文字を格納した場合
上記のセクションでは触れませんでしたが、以下のようにマルチバイト文字で初期化した場合のソースコードをそれぞれUTF8でファイルに保存した場合と、Shift-JISでファイルに保存した場合のそれぞれの結果をみてみたいと思います。
int main(void) {
char str[] = "あ"; //初期化と同時に配列を宣言する場合は、要素数を省略できる
int size = sizeof(str);
for (int i = 0; i < size; i++) {
printf("%hhx ", str[i]); // この出力をそれぞれのエンコーディングで検証する
}
}
検証方法:
- エディタを開く
- エディタのエンコード設定をShift-JIS or UTF-8に変更
- ソースコードを貼り付けて保存する
- clangコンパイラでコンパイルする (\$cc ファイル.c)
- 実行する (\$./a.out)
UTF8で保存した際の出力:
e3 81 82 0
Shift-JISで保存した際の出力:
82 a0 0
上記それぞれの値ですが、このサイトで「あ」を入力して、結果を表示してみて下さい。
結果
つまりC言語のマルチバイト文字の文字列リテラルの結果は、テキストエディタのエンコードと一致することが分かります。
というのもコンパイラには「ソースコード」ではなく、そのソースコードが書かれた「ファイル」を渡しているため、至極当然の結果とも言えます。
つまりUTF8の場合、char str[] = "あ"
は以下の糖衣構文であるとも言えます。
int main(void) {
//e3 81 82 0
char str[] = {0xe3, 0x81, 0x82, 0x0};
printf("%s \n", str); //ターミナルのエンコード設定がUTF8ならば「あ」と表示されます
}
上記実行するターミナルのエンコードをUTF-8にして実行した場合は、「あ」と表示されます。
Shift-JISにした場合は文字化けします。
(設定 → Profiles → Advanceタグ)
その結果 上がShift-JISにした際の結果で、下がUTF8にした際の結果
ここでのまとめ
-
char
型の配列は、マルチバイト文字の文字列リテラルで初期化できる - 文字列リテラルで生成される値は、テキストエディタのエンコーディング値である
- コンパイラにはソースコードではなく、それが記述されたファイルが渡される
Swiftの文字列に変換するときに指定するエンコードについて
以下のSwiftのAPIを用いて、CのAPIから渡された文字をSwiftのStringに変換したいと思います。この時指定するencoding値はどうするべきでしょうか?
init?(cString: UnsafePointer<CChar>, encoding enc: String.Encoding) //CChar = Int8
検証するCのプログラムコードは以下です。
char* file_name() {
return "hello.txt";
}
char* new_file_header_str() {
FILE *f = fopen(file_name(), "r");
if (f == NULL) return NULL;
char *str = calloc(256, sizeof(char));
fgets(str, 256, f); //1行だけ
fclose(f);
return str;
}
上記をSwiftから呼び出した場合、Cのchar*
型は、UnsafeMutablePointer<Int8>
型として結果が渡されます。
まず最初にfile_name
関数から取得したCの文字をSwiftの文字に変換することを検証したいと思います。
こちら文字列リテラルがそのまま返されております。
つまり、こちらをSwiftの文字列に変換する際のエンコード値は、libc.cファイルのエンコードと同一にする必要が有ることが分かります。
次に、new_file_header_str
関数から取得したCの文字をSwiftの文字列に変換する際に使用するエンコード値はどうでしょうか?
こちらはhello.txt
ファイルの文字列が返されております。
つまりこちらで指定しなくてはならないエンコード値は、hello.txt
ファイルが保存されているエンコード値と同一でなければならないことが分かります。
以下、lib.cファイルをUTF-8で、hello.txtファイルをShift-JISで保存し、Swiftからそれぞれ関数を呼び出したサンプルソースコードです。
let name = file_name() //Optional<UnsafeMutablePointer<Int8>>
if let name = name,
let converted = String(cString: name, encoding: .utf8) {
print(converted)
}
let header = new_file_header_str() //Optional<UnsafeMutablePointer<Int8>>
if let header = header,
let converted = String(cString: header, encoding: .shiftJIS) {
print(converted)
}
なおCライブラリーの呼び出しは以下を参照して下さい。
https://qiita.com/ysn551/items/83e06cf74ae628cb573c
ここでのまとめ
- Cの文字列をSwiftの文字列に変換する際に指定するエンコード値は、Cから返される文字が文字列リテラルの場合は、Cソースコードファイルのエンコード値を指定する
Python3の文字列リテラル
このようにCの文字列リテラルは、エンコーディング値が直接格納されるため、開発環境に依存してしまいます。
ちなみにSwiftコンパイラの場合は、UTF8ファイル以外はコンパイル出来ないようになっていました。
一方Python3では、文字列リテラルによって生成される値は数字ですが、こちらはユニコード値が生成されます。
従ってファイル間の文字列リテラルのやり取りにエンコードを考慮する必要はありません。
python3での検証結果は以下です。ちなみにPython2はエンコーディング値が使われるのでソースコード間のエンコーディングが異なると駄目です。
以下のshift_jis.pyファイルをShift-JISエンコードで保存する
# ! coding=shift-jis
word = "よろしくです"
以下のutf8.pyファイルをUTF8で保存して実行する。
# ! coding=utf-8
import shift_jis as sh
if sh.word == "よろしくです":
print("true")
else:
print("false")
上記をpython3で実行するとtrueが表示されますが、python2だとfalseが表示されます。
最後のまとめ
2018年もよろしくお願いします。m(__)m