文字列は0で終わるバイトの列
C言語の文字列は、文字のコードを並べて、最後に0のバイトを置いたものです。専用の型はなく、char の配列です。
この終わりの0を忘れたり、置く場所が足りなかったりすると、バッファオーバーフローという危険な誤りになります。配列と文字列の形を知ると、その誤りの理由が見えます。
このレッスンのゴール
メモ
配列をメモリとして、文字列を0で終わるバイトとして説明できる。
重要な言葉
| ことば | いみ | れい |
|---|---|---|
| 配列 | 同じ型の要素が、続けて並んだもの | int a[4] |
| 文字列 | 0で終わる char の並び | |
| ヌル終端 | 文字列の最後に置く0のバイト | \0 |
| 文字コード | 文字に割り当てた数 | 'T' は84 |
| バッファ | データを入れる、決まった大きさの場所 | |
| バッファオーバーフロー | バッファの外に書き込んでしまう誤り |
学生が入力するセルは、このプレビューでは空白です。
配列は同じ型の要素がメモリに並ぶ
int a[4] は、4バイトの要素が4つ、すき間なく並んだものです。全体は16バイトで、a[2] は先頭から8バイト先にあります。
C言語は、配列の長さを覚えていません。範囲の外を読み書きしても、止めてくれません。だから長さは、自分で持ち歩きます。
配列 int a[4] の中身
| 要素 | 先頭からのバイト数 |
|---|---|
| a[0] | 0から3 |
| a[1] | 4から7 |
| a[2] | 8から11 |
| a[3] | 12から15 |
学生が入力するセルは、このプレビューでは空白です。
配列の大きさと要素の数
printf '%s\n' 'int main(void){int a[10]; printf("%zu %zu\n", sizeof(a), sizeof(a) / sizeof(a[0])); return 0;}' | gcc -include stdio.h -x c - -o /tmp/len && /tmp/lenここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
文字列は文字のコードに0を足したもの
"Tokyo" は5文字ですが、メモリでは6バイトです。最後に0のバイトが付くからです。
strlen は、0の手前までの文字数を数えるので、5を返します。sizeof は配列全体の大きさなので、6です。この2つは別のものです。
"Tokyo" の6バイト
| 番号 | 文字 | 文字コード |
|---|---|---|
| 0 | T | 84 |
| 1 | o | 111 |
| 2 | k | 107 |
| 3 | y | 121 |
| 4 | o | 111 |
| 5 | 終わりの印 | 0 |
学生が入力するセルは、このプレビューでは空白です。
sizeof と strlen の違い
printf '%s\n' 'int main(void){char s[] = "Tokyo"; printf("%zu %zu\n", sizeof(s), strlen(s)); return 0;}' | gcc -include stdio.h -include string.h -x c - -o /tmp/str && /tmp/strここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
6バイトの文字コードを並べる
printf '%s\n' 'int main(void){char s[] = "Tokyo"; for (int i = 0; i < 6; i++) printf("%d ", s[i]); printf("\n"); return 0;}' | gcc -include stdio.h -x c - -o /tmp/codes && /tmp/codesここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
バッファの外に書くと、隣のデータが壊れる
4バイトの name に、abcd の4文字と、終わりの0を書くと、5バイト要ります。1バイトがあふれて、隣のメンバーに当たります。
下の Python は、メモリを12バイトのリストで真似ます。本物のC言語では、あふれたときの結果は決まっていません。コンパイラや検査の道具が、見つけて止めることもあります。
隣の変数を壊す様子を真似る
# メモリを12バイトのリストで真似ます。0から3が name、4から7が flag です。
memory = [0] * 12
memory[4] = 1
text = "abcd"
for i in range(len(text) + 1):
if i < len(text):
memory[i] = ord(text[i])
else:
# 5番目に、終わりの0が書かれ、flag の最初のバイトを上書きします。
memory[i] = 0
print("name の4バイト:", memory[0:4])
print("flag の最初のバイト:", memory[4])
# text を "ab" に変えて、もう一度実行しましょう。
大きさを守ってコピーする
printf '%s\n' 'int main(void){char buf[8]; snprintf(buf, sizeof(buf), "%s", "abcd"); printf("%zu %s\n", strlen(buf), buf); return 0;}' | gcc -include stdio.h -include string.h -x c - -o /tmp/safe && /tmp/safeここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
文字列の長さは、sizeof で分かる、と思いがちです。sizeof は配列全体の大きさなので、終わりの0も数え、余った部分も数えます。文字数が知りたいときは strlen を使います。コピーの前には、文字数に1を足した大きさがあるか、確かめます。
さらに挑戦: 文字列をつなげる
課題が通ったら、concat(first, second, size) を書きましょう。0で終わる2つのリストをつなげ、size バイトのバッファに入れます。
入りきらないときは None を返します。そして、"ab" と "cd" を size が5と4のときに試す、自分のテストを書きましょう。