Comments
Description
Transcript
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)