C言語入門 第11週

1
C言語入門
第11週
プログラミング言語Ⅰ(実習を含む。),
計算機言語Ⅰ・計算機言語演習Ⅰ,
情報処理言語Ⅰ(実習を含む。)
2
コンパイル時のエラーのパターン
復習
3
gcc のエラーメッセージ
• 環境変数LANGを設定すると言語が変わる
mintty + bash + GNU C
$ LANG=C gcc error1.c
error1.c: In function 'main':
error1.c:7:3: error: expected ';' before 'printf'
printf("world¥n");
^
mintty + bash + GNU C
$ LANG=ja_JP.UTF-8 gcc error1.c
error1.c: 関数 ‘main’ 内:
error1.c:7:3: エラー: expected ‘;’ before ‘printf’
printf("world¥n");
^
ロケール(locale)と呼ばれる
多言語化の仕組み
JM: locale (7)
4
mintty の locale の設定
• mintty左上のアイコンからoption→Text
ここに
Locale: ja_JP
Cahaacter set: UTF-8
を設定して「OK」しておく
5
文末の「;」忘れ
• 文末に「;」を忘れると次の文字でエラーに
4
5
6
7
8
9
10
error1.c
mintty + bash + GNU C
int main()
{
printf("hello, ")
printf("world¥n");
$ gcc error1.c
error1.c: 関数 ‘main’ 内:
error1.c:7:3: エラー: expected ‘;’ before ‘printf’
printf("world¥n");
^
return EXIT_SUCCESS;
}
エラーが生じたのは7行目の3文字目だが、エラーの原因は6行目の行末。
C言語では、スペースや改行は人間に読み易くするための位置調整に過ぎないので
printf("hello, ")
ここに「;」があるべきだが
printf("world¥n");
「;」の前に「p」が現れたことが
は
printf("hello, ")printf("world¥n"); エラーの原因
と同じ意味。
6
ブロック開始終端の不整合
• 「{」が1つ多い
4
5
6
7
8
9
10
error2_1.c
mintty + bash + GNU C
int main()
{
{
printf("hello, world¥n");
$ gcc error2_1.c
error2_1.c: 関数 ‘main’ 内:
error2_1.c:10:1: エラー: expected declaration or statement at end of input
}
^
return EXIT_SUCCESS;
}
「}」が1つ足らない状態で、ファイルの終端(入力の終端)に達している。
7
ブロック開始終端の不整合
• 「}」が1つ多い
4
5
6
7
8
9
error2_2.c
mintty + bash + GNU C
int main()
{
printf("hello, world¥n");
}
return EXIT_SUCCESS;
}
$ gcc error2_2.c
error2_2.c:8:3: エラー: expected identifier or ‘(’ before ‘return’
return EXIT_SUCCESS;
^
error2_2.c:9:1: エラー: expected identifier or ‘(’ before ‘}’ token
}
^
エラーが生じたのは8行目の3文字目だが、エラーの原因は7行目。
「}」が1つ多いので、main関数の定義が7行目で終わっている。
つまり、8~9行目は、何もないところにいきなり以下のように書いたのと同じ。
8
9
return EXIT_SUCCESS;
}
8
ブロック開始終端の不整合
• 関数名のミススペル
4
5
6
7
8
9
error3.c
mintty + bash + GNU C
int main()
{
print("hello, world¥n");
$ gcc error3.c
/tmp/ccopiKHp.o:error3.c:(.text+0x15): `print' に対する定義されていない参照で
す
/tmp/ccopiKHp.o:error3.c:(.text+0x15): 再配置がオーバーフローしないように切り詰
められました: R_X86_64_PC32 (未定義シンボル `print' に対して)
/usr/lib/gcc/x86_64-pc-cygwin/4.8.2/../../../../x86_64-pc-cygwin/bin/ld:
/tmp/ccopiKHp.o: 誤った再配置アドレス 0x0 がセクション `.pdata' 内にあります
/usr/lib/gcc/x86_64-pc-cygwin/4.8.2/../../../../x86_64-pc-cygwin/bin/ld: 最
終リンクに失敗しました: 無効な操作です
collect2: エラー: ld はステータス 1 で終了しました
return EXIT_SUCCESS;
}
コンパイルではなくリンクに失敗したと言われている。
分割コンパイルによる、外部関数のリンクの際、print が見つからなかった。
printf のつもりが print とミススペルしている。
標準ライブラリに print 関数は用意されていない。
9
ポインタ
講義資料 第4週pp.42-44., 第9週p.6., 教科書p.171.
call by pointer (ポインタ渡し)
• 呼び出し元の変数の内容を変更したい場合
訂正2014-07-11
誤:swap
正:swapi
swapi.c
swapi_test.c
void swapi(int *a, int *b)
{
int c;
c = *a;
*a = *b;
*b = c;
}
int a, b;
fprintf(stderr, "a = ? ");
scanf("%d", &a);
fprintf(stderr, "b = ? ");
scanf("%d", &b);
swapi(&a, &b);
void swapi(int *a, int *b); 関数
*a と *b の値を入れ替える
printf("a = %d¥n", a);
printf("b = %d¥n", b);
10
11
講義資料 第4週pp.42-44., 第9週p.6., 教科書p.171.
call by pointer (ポインタ渡し)
• 2つ以上の値を返したい場合
• 戻り値は1つしかないので、関数の引数にポイン
タを渡して、値を返す
modf.c
double modf(double x, double *iptr)
{
*iptr = x < 0 ? ceil(x) : floor(x);
return x < 0 ? *iptr - x : x - *iptr;
}
double modf(double x, double *iptr); 関数
戻り値:
x の小数部を戻り値に
x の整数部を *iptr に返す
戻り値は共に x と同じ符号を持つ
JM: modf (3)
12
講義資料 第4週pp.42-44., 第9週p.6., 教科書p.171.
call by pointer (ポインタ渡し)
• 任意の長さの配列を渡したい場合
• 例えば文字列等
strlen.c
strlen_test.c
size_t strlen(const char *s)
{
size_t len = 0;
while(s[len] != '¥0') {
len++;
}
return len;
}
char s[1024] = "";
fprintf(stderr, "s = ? ");
scanf("%1023[^¥n]", &s);
printf("strlen(s) = %d¥n", strlen(s));
size_t strlen(const char *s); 関数
'¥0'で終端された文字列の長さを返す
値が書き変えられては困る場合
const char への * (ポインタ)
にしておくと、この関数を使っても
与えた内容が変更されないことを
ある程度保証することが出来る
13
教科書p.308,310,314., [1] pp.240,257,261-262.
const 修飾子
• const 型: 変数の値を変更出来なくなる
const_test1.c
6
7
8
9
const int i = 0; // 初期化は出来る
int const j = 0; // const int も int const も同じ意味
i = 1;
// const を付けた変数には代入出来ない
j = 1;
// const を付けた変数には代入出来ない
const 修飾子は型修飾子(type-qualifier)の一種
型修飾子は型名の前後どちらにつけても良い
mintty + bash + GNU C
$ gcc const_test1.c
const_test1.c: 関数 ‘main’ 内:
const_test1.c:8:3: エラー: 読み取り専用変数 ‘i’ への代入です
i = 1; // Error: i is const
^
const_test1.c:9:3: エラー: 読み取り専用変数 ‘j’ への代入です
j = 1; // Error: j is const
^
変更しようとするとコンパイル時に
エラーになるので
本来書き変えてはいけない値を
書き変えてしまうことで生じるバグを
未然に防げる
14
教科書p.308,310,314., [1] pp.240,257,261-262.
const 修飾子
• const char 型へのポインタ
char const * 型
const char * 型
const_test2.c
6
7
8
9
10
11
char s[] = "hello, world";
const char *p; // ポインタの場合 *p が const になる
p = s;
// ポインタへは代入出来る
*p = 'H';
// ポインタ指し示す先の変数には代入出来ない
p[7] = 'W';
// ポインタ指し示す先の変数には代入出来ない
*(char *) p = 'H'; // const のない型へ cast してやると代入出来る
mintty + bash + GNU C
ただし、わざわざ const を付けているということは
変更してはいけない、
または変更しないことを前提としているのであるから
普通は、特別に理由がない限り無理矢理書き変えてはいけない
$ gcc const_test2.c
const_test2.c: 関数 ‘main’ 内:
const_test2.c:9:3: エラー: 読み取り専用位置 ‘*p’ への代入です
*p = 'H'; // Errpr: *p is const
^
const_test2.c:10:3: エラー: 読み取り専用位置 ‘*(p + 7u)’ への代入です
p[7] = 'W'; // Errpr: p[x] is const
^
15
教科書p.308,310,314., [1] pp.240,257,261-262.
const 修飾子
• char 型への const ポインタ
char * const 型
const_test3.c
6
7
8
9
10
char s[] = "hello,
char *const p = s;
p = s;
*p = 'H';
p[7] = 'W';
world";
// p が const で *p が char になる
// p が const なのでポインタへは代入出来ない
// ポインタ指し示す先の変数には代入出来る
// ポインタ指し示す先の変数には代入出来る
mintty + bash + GNU C
$ gcc const_test3.c
const_test3.c: 関数 ‘main’ 内:
const_test3.c:8:3: エラー: 読み取り専用変数 ‘p’ への代入です
p = s; // Error: p is const
^
const char *p と
char const *p は同じだが
char * const p は意味が異なる
前者は *p が const char
つまり p は変数で *p が定数
後者は *const p が char
つまり p は定数で *p は変数
である
* がどこに係っているか
よく考えるましょう
16
標準ライブラリ関数を例にした実例
文字列操作とポインタ操作
17
ポインタを用いた文字列操作の例
• strlen 関数の大まかな仕組み
strlen_with_idx.c
size_t strlen(const char *s)
{
size_t len = 0;
while (s[len] != '¥0')
len++;
return len;
}
文字列の長さは
先頭から終端文字('¥0')の手前までの
文字数
strlen_with_ptr1.c
strlen_with_ptr2.c
size_t strlen(const char *s)
{
const char *s0 = s;
while (*s != '¥0')
s++;
return s - s0;
}
size_t strlen(const char *s)
{
const char *s0 = s;
while (*(s++) != '¥0')
;
return s - s0 - 1;
}
18
ポインタを用いた文字列のコピーの例
• strcpy 関数の大まかな仕組み
strcpy_with_idx.c
char *strcpy(char *dst, const char *src)
{
int i;
for (i = 0; (dst[i] = src[i]) != '¥0'; i++)
;
return dst;
}
文字列のコピーは
先頭から終端文字('¥0')までを
コピーすれば良い
strcpy_with_ptr.c
char *strcpy(char *dst, const char *src)
{
char *dst0 = dst;
while ((*(dst++) = *(src++)) != '¥0')
;
return dst0;
}
訂正2014-07-04
誤:'0'
正:'¥0'
19
ポインタを用いた文字列のコピーの例
• strncpy 関数の大まかな仕組み
strncpy_with_idx.c
char *strncpy(char *dst, const char *src, size_t n)
{
size_t i = 0;
for (; i < n && (dst[i] = src[i]) != '¥0'; i++)
;
for (; i < n; i++)
dst[i] = '¥0';
return dst;
}
strncpy_with_ptr.c
char *strncpy(char *dst, const char *src, size_t n)
{
char *dst0 = dst;
while(0 < n-- && (*(dst++) = *(src++)) != '¥0')
;
while(0 < n--)
*(dst++) = '¥0';
return dst0;
}
strncpy は strcpy に加えて
終端文字('¥0')以降を'¥0'で埋める
論理演算は左から右に評価され、
真偽値が確定すると評価を終了する。
つまり
i < n や
dst < dst0 + n が偽なら、
そこで真偽値が確定するので
それより右にある
(dst[i] = src[i]) != '¥0' や
(*(dst++) = *(src++)) != '¥0' は
実行されない。
20
ポインタを用いた文字列の比較の例
• strcmp 関数の大まかな仕組み
strcmp_with_idx.c
どちらかが終端文字('¥0')になるか
異なる値が出てくるまで比較し
終了位置を比較すれば良い
int strcmp(const char *s1, const char *s2)
{
size_t i;
for (i = 0; s1[i] != '¥0' && s2[i] != '¥0' && s1[i] == s2[i]; i++)
;
return s1[i] - s2[i];
}
strcmp_with_ptr.c
int strcmp(const char *s1, const char *s2)
{
while (*s1 != '¥0' && *s2 != '¥0' && *s1 == *s2) {
s1++;
s2++;
}
return *s1 - *s2;
}
21
ポインタを用いた文字列の比較の例
• strncmp 関数の大まかな仕組み
strncmp_with_idx.c
strncmp は strcmp の
比較文字数を最大n文字に限定する
int strncmp(const char *s1, const char *s2, size_t n)
{
size_t i;
if (n <= 0) return 0;
for (i = 0; i < n - 1 && s1[i] != '¥0' && s2[i] != '¥0' && s1[i] == s2[i]; i++)
;
return s1[i] - s2[i];
}
strncmp_with_ptr.c
int strncmp(const char *s1, const char *s2, size_t n)
{
if (n <= 0) return 0;
while (0 < --n && *s1 != '¥0' && *s2 != '¥0' && *s1 == *s2) {
s1++;
s2++;
}
return *s1 - *s2;
}
22
ポインタを用いた文字列の連結の例
• strcat 関数の大まかな仕組み
strcat_with_idx.c
char *strcat(char *dst, const char *src)
{
int i, len = strlen(dst);
for (i = 0; (dst[len + i] = src[i]) != '¥0'; i++)
;
return dst;
}
strcat_with_ptr.c
char *strcat(char *dst, const char *src)
{
char *dst0 = dst;
dst += strlen(dst);
while ((*(dst++) = *(src++)) != '¥0')
;
return dst0;
}
dst の終端位置に
src をコピーする
23
ポインタを用いた文字列の連結の例
• strncat 関数の大まかな仕組み
strncat_with_idx.c
char *strncat(char *dst, const char *src, size_t n)
{
int i, len = strlen(dst);
for (i = 0; i < n && src[i] != '¥0'; i++)
dst[len + i] = src[i];
dst[len + i] = '¥0';
return dst;
}
strncat_with_ptr.c
char *strncat(char *dst, const char *src, size_t n)
{
char *dst0 = dst;
dst += strlen(dst);
while (0 < n-- && *src != '¥0')
*(dst++) = *(src++);
*dst = '¥0';
return dst0;
}
strncat は strcat の
連結文字を最大n文字に限定する
ただしsrcがn文字以上の場合
終端文字が+1文字され
合計n+1バイト追記される
24
教科書.pp.235-239., [1]pp.148-153.
複雑なポインタの宣言
• *を付けると何になるか?
pointertest5.c
mintty + bash + GNU C
int *(p1[7]); // int *p1[7]; と同義
int (*p2)[7];
printf("sizeof( p1)=%2d¥n", sizeof( p1));
printf("sizeof( p2)=%2d¥n", sizeof( p2));
printf("sizeof(*p1)=%2d¥n", sizeof(*p1));
printf("sizeof(*p2)=%2d¥n", sizeof(*p2));
$ gcc pointertest5.c && ./a
sizeof( p1)=56
sizeof( p2)= 8
sizeof(*p1)= 8
sizeof(*p2)=28
[] は * よりも優先順位の高い演算子
p1[x]に*が付く、つまり*p1[x]がint型になる
従ってp1[x]はint*型、つまりp1は要素数7のint*型配列
p2に*が付く、つまり*p2が要素数7のint型配列になる
従ってp2は「「要素数7のint型配列」へのポインタ」
宣言の読み方
int *(p1[7]);
→ *(p1[x])がint
int (*p2)[7];
→ *p2がint[7]
要注意
25
教科書.pp.235-239., [1]pp.148-153.
ポインタ配列の初期化
• char * の配列の初期化
char *s[] = {"one", "two", "three"};
s
は char* 型で要素数3の配列 s[0]
s[x] は char* 型
*s[x] は char 型
s[0] は "one"
s[1] は "two"
s[0][0]
s[0][1]
s[0][2]
s[0][3]
は
は
は
は
'o'
'n'
'e'
'¥0'
s[1]
s[2]
メモリ上のどこかに配置された
文字列
0x~00
00
0x~00
o
0x~01
~
0x~01
n
0x~02
~
0x~02
e
0x~03
~
0x~03
¥0
0x~04
04
0x~04
t
0x~05
~
0x~05
w
0x~06
~
0x~06
o
0x~07
~
0x~07
¥0
0x~08
08
0x~08
t
0x~09
~
0x~09
h
0x~0a
~
0x~0a
r
0x~0b
~
0x~0b
e
0x~0c
e
0x~0d
¥0
26
教科書.pp.235-239., [1]pp.148-153.
配列とポインタの初期値と文字列
• 配列とポインタで扱いが異なることに注意
pointertest6.c
void sub()
{
char s[] = "hello";
char *p = "world";
...
objdump の結果
一般にポインタに初期値として与えた
文字列定数は書き変えてはいけない
.rdataセクションに用意されたデータは
書き変えてはいけない
sへは"hello"の文字コード
68,65,6c,6c,6fが代入されているが
pへは.rdataセクションに予め
用意してある文字列"world"の
アドレス403060が代入されている
セクション .rdata の内容:
...
403060 776f726c 64007320 3d202225 73220a00 world.s = "%s"..
...
void sub(void)
{
...
char s[] = "hello";
401196:
c7 45 ee 68 65 6c 6c
movl
$0x6c6c6568,-0x12(%ebp)
40119d:
66 c7 45 f2 6f 00
movw
$0x6f,-0xe(%ebp)
char *p = "world";
4011a3:
c7 45 f4 60 30 40 00
movl
$0x403060,-0xc(%ebp)
...
27
教科書.pp.235-239., [1]pp.148-153.
配列とポインタの初期値と文字列
• 配列とポインタで扱いが異なることに注意
pointertest6.c
Cygwin + GNU C
void sub(void)
{
char s[] = "hello";
char *p = "world";
printf("s = ¥"%s¥"¥n", s);
printf("p = ¥"%s¥"¥n", p);
s[0] = 'H';
p[0] = 'W';
}
$ gcc pointertest6.c && ./a
s = "hello"
p = "world"
Segmentation fault (コアダンプ)
int main()
{
sub();
sub();
return EXIT_SUCCESS;
}
Borland C++
>bcc32 pointertest6.c && pointertest6
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
pointertest6.c:
Turbo Incremental Link 5.00 Copyright (c) 1997, 2000 Borland
s = "hello"
p = "world"
s = "hello"
p = "World"
sub()が実行された際、
sは毎回"hello"だが
pは2回目以降"World"になってしまう
もしくは.rdataへの不正な書き込みで
異常終了してしまう
28
演習: print_str_with_ptr.c
• char型へのポインタ変数pを用いて、char型配
列sに保存された文字列を表示せよ
• 文字列末尾で改行すること
• 用いて良い変数はpのみとする
• 表示にはputchar関数を使用すること。printf
関数は使用しないこと
• 用いて良いループ構造はwhile文のみとする
• print_str_with_ptr_tmp.c を元に指定箇
所に作成せよ。 mintty + bash + GNU C
$ gcc print_str_with_ptr.c && ./a
s = ? hello, world
JM: putchar (3)
hello, world
29
演習: print_str.c
• 文字列を表示する関数print_str(s)を実装せよ
• 与えられた文字列末尾で改行すること
• 関数のプロトタイプ宣言はmyfunc_week11.hに作成
せよ
• print_str_test.c と共にコンパイルして動作を確
認すること
• 引数
• const char *s : 文字列へのポインタ
• 戻り値
• なし(void)
mintty + bash + GNU C
ヒント:
print_str_with_ptr.c から
切り出して関数化すれば良い
$ gcc print_str_test.c print_str.c && ./a
s = ? hello, world
hello, world
30
演習: base36toint.c
• 36進数で用いられる「0~9,A~Z,a~z」までの文字をint型の数
値0~35に変換する関数base36toint(c)を実装せよ
• 関数のプロトタイプ宣言はmyfunc_week11.hに作成せよ
• エラーの際、DEBUGマクロが定義されていたら、標準エラー出力に
警告メッセージを表示せよ
• base36toint_test.cと共にコンパイルして動作を確認する事
• 引数
• int c : 「0~9,A~Z,a~z」までの文字コード
(0x30~0x39,0x41~0x5a,0x61~0x7a)
• 戻り値
• cで与えられた文字コードに対応する数値0~35をint型で返す
• エラーの場合は-1を返す
mintty + bash + GNU C
$ gcc base36toint_test.c base36toint.c && ./a
c = ? z
第13週へ移動+改訂
35
31
演習: basetoint.c
• 「0~9,A~Z,a~z」の文字をN進数表現の1桁としてint型の数値に変
換する関数basetoint(c,base)を実装せよ
• 関数のプロトタイプ宣言はmyfunc_week11.hに作成せよ
• エラーの際、DEBUGマクロが定義されていたら、標準エラー出力に警告
メッセージを表示せよ
• basetoint_test.cと共にコンパイルして動作を確認する事
• 引数
• int c : N進数表現の1桁を表す文字「0~9,A~Z,a~z」の文字コード
(0x30~0x39,0x41~0x5a,0x61~0x7a)
• int base : N進数表現の基数(つまりbase進数)、最大36
• 戻り値
• cで与えられた文字コードに対応する数値0~最大35をint型で返す
• エラーの場合は-1を返す
mintty + bash + GNU C
$ gcc basetoint_test.c basetoint.c && ./a
c = ? z
base = ? 10
第13週へ移動+改訂
-1
32
教科書 pp.243-250.
ポインタへのポインタ
• 関数の引数でポインタを返したい場合はポイ
ンタ変数へのポインタを用いる
strtoui.c
unsigned int strtoui(const char *s, char **endp, int base)
{
int v;
unsigned int r = 0;
この例だと
while (0 <= (v = basetoint(*(s++), base))) {
endp に "z" へのポインタ
r = r * base + v;
つまり&s[2]が返ってくる
}
main.c
if (endp != NULL) *endp = (char *) s;
return r;
char s[] = "ffz";
}
char *endp;
printf("strtoui(s, &endp, 16);
33
参考文献
• [1] B.W.カーニハン/D.M.リッチー著 石田晴久
訳、プログラミング言語C 第2版 ANSI 規格準
拠、共立出版(1989)