● 先週の授業のキーポイント
【文字列】
• 文字列とは「文字が0個以上並んだデータ」のことである. Cにおいては,文字列はchar型の 要素を持つ配列として扱われる. しかし, 単なる「char型の配列」ではなく, 「文字列終端記
号」(値0)を見つけると,文字列の終りとみなす.
• Cにおいて,二重引用記号(")で囲まれた文字のならびを「文字列リテラル」と呼び, “定数文字 列”の役割を果たす. したがって,
char str[]="abc" ;
によって配列strを文字列abcで初期化できる. この場合, 文字列終端記号を含むため, 定義 される配列strの要素数は4となる. (文字列strの長さは3 である.)
• 同様に
char str[10]="abc" ;
と定義すると,strの要素数は10であるが, 文字列としてのstrの長さは3 となる.
• 文字列をコピーしようとして, char s[]="abc", t[10] ;
と定義されているとき,t = sによって文字列のコピーを実現することはできない.
【日本語文字】
• ASCIIコードに含まれる文字は,コード値0x00から0x7Fの範囲に含まれ,char型のオブジェ
クトに格納可能である. つまり, 文字列"abc"の長さは3 であり, 各文字は1バイトの領域を 占める.
• 1バイト(8ビット)で表現可能な文字種は256文字であるため,日本語文字を表現するために は2バイトを用いる. すなわち, 2バイトで1文字をあらわす.
• 日本語文字を表すことができるコード体系には以下の4種類がある.
– Shift JIS
「Microsoft Windows」で日本語を含むテキストファイルで用いられているコード体系. 日 本語文字の第一バイトは0x80以上の値を取る8ビット体系のコード体系.
– EUC-JP
Unix 系のシステムで多く用いられている日本語文字コード体系. 日本語文字の各バイトは 0x80以上の値を取る8ビット体系のコード体系. 今回利用しているgccは, EUC-JPであ れば,日本語文字をコメント内や文字列に記入することが可能.
– JIS (ISO 2022-JP)
日本語文字の各バイトが0x7F以下の値を取る7ビット体系のコード体系. 日本語文字か
ASCII文字かの判別には「ここから日本語」を示すバイト列と「ここまでが日本語」を示
すバイト列(各3バイト)を利用する.
– Unicode
Unicodeとは,日本語のみならず,世界中で用いられている種々の文字を一つの文字コード
体系の中で表現するために考えられたものである. 上の3種類の文字コード体系(Shift JIS,
EUC-JP, JIS)は,「日本語文字コード体系」であるので,その中に他の言語の文字(例えば ハングルなど)を挿入することはできない. Unicodeを用いると,一つのファイル中に複数 の言語の文字を書くことが可能である.
Unicodeとは具体的な文字コード体系ではなく,「世界中で用いられている種々の文字を一
つの文字コード体系の中で表現する」というポリシの元に定められた,複数の文字コード体 系を包括する用語である. Unicodeの文字コード体系のうちUTF-8と呼ばれる, 8ビット体 系のコードがMacOS Xの内部コードとして用いられている. UTF-8で日本語文字1文字 は2バイトまたは3バイトで表されている. なお,この他にも Unicodeの文字コード体系 としてUCS-2,UCS-4,UTF-7,UTF-16,UTF-32などがある.
• したがって,char[]="あいうえお"としたとき, EUC-JPまたは Shift JISの場合には, 文字列 の長さは10となる.
【配列を関数の引数に利用する】
• 配列を関数の引数に利用することが可能である. その場合, 関数の仮引数定義には int foo(int a[])
にように「配列の要素数」は必要ない. 仮に要素数を指定しても無視されるだけである. なお, プロトタイプ宣言で引数の名前を省略する場合は
extern int foo(int []);
のようになる.
• 配列を関数引数として関数に渡し,関数内で配列要素を変更すると,呼び出し側の関数に戻った とき,その変更が反映されている. これは,配列以外のint型オブジェクト等を関数に渡した場 合と挙動が異なる.
● 実習内容
【サンプルプログラム】
★ ex12-1.c 最も単純かつ基本的なポインタの例.
• “int *pn, *pm”でpnと pmという2つのint型オブジェクトへのポインタを作成. (以
後簡単のため,「int型へのポインタ」呼ぶ.)
• オブジェクトに対して&をつけると,そのオブジェクトのアドレスを取り出すことができ る. この&を「アドレス演算子」と呼ぶ.
• ポインタに対して*をつけると, ポインタの指し示す先の値を参照できる. この *を「間 接演算子」と呼ぶ.
• “pn = &n”でpnがnを指し示すように,pnに nのアドレスを代入.
• “*pn = 2”によって,pnの指し示す先の値に 2を代入する.
この時点では,pnは nを指し示しているので,nに 2を代入したことになる.
• “pm = pn”によって,pmにはpnが指し示すもののアドレスが代入される.
この時点では, pnは nを指し示しているので, pmにはn のアドレスが代入され, pmは n を指し示している.
• printf 関数で“%p”を指定すると, 対応するポインタのアドレスが表示される. (ポイン タの値とポインタの値を比較することはオブジェクト間の相対的な位置関係を知る上で意 味を持つが,各々のポインタの具体的な値は処理系や動作環境により大きく異なるのでほと んど意味は無い.)
★ ex12-2.c (1次元)配列とポインタとの対応.
C言語の書籍には「Cでは配列とポインタは同じもの」と書いてあることが多いが,その意味を 考えてみよう.
• “int a[10]”, “char s[11]”で,それぞれint型,char型の要素を持つ配列を作成. また,
“int *pa”, “char *ps”で,それぞれint型,char型へのポインタを作成.
• “ps = s”によって psに配列sの先頭のアドレスを代入.
ex12-1.cでは,ポインタにアドレスを代入するためにアドレス演算子を利用したが,配列
の場合には状況が異る.
「オブジェクトが配列の場合には,識別子はその配列の先頭アドレスをあらわす」ので, “s”
そのものには配列sの先頭のアドレスが入っている. それは,s[0]のアドレスそのもので ある.
したがって, “ps = s”と “ps = &s[0]”は等価な操作である.
• char型のポインタ psに対してps+1は「次のアドレス」をあらわす.
したがって, psが s[0]を指し示しているとき, ps+1は s[1]のアドレスをあらわす. 特
に “*(ps+i)”は “s[i]”の値を参照している.
逆にchar型へのポインタpsを用いて, psが char型の配列sを指し示しているときに は,*(ps+i)の代わりにps[i]と書いてもよい.
• int型のポインタpaに対してpa+1は「次のアドレス」をあらわす.
と,ここまではchar型と同じであるが,ポインタが示す型が異ると「次のアドレス」の中 身が異ってくる. すなわち,「次のアドレス」とは, そのポインタが示す型のオブジェクト の意味で「次」を意味する.
したがって, paが a[0]を指し示しているとき, pa+1は a[1]のアドレスをあらわす. 特
に “*(pa+i)”は “a[i]”の値を参照している.
つまり,pa+1はそれが示す型のバイト数先のアドレスである.
• 実際には配列の要素参照a[i]は内部では*(a+i)として実現される. この意味で「配列と ポインタは同じもの」である.
★ ex12-3.c ポインタを引数にする関数
• 最初に定義している変数は,大域変数aと main関数内の局所変数bである.
• 通常“int foo_0(int a)”によって関数内部で実引数の値を変更しても,呼び出し側に戻っ
たとき,その変更は反映されない.
実際, 呼び出し側(main)と foo_0での変数のアドレスをみると,全く異ったものである ことがわかる.
• ところが“int foo_1(int *a)”によって関数内部で仮引数の値を変更すると,呼び出し側
に戻ったとき,その変更が反映されている.
実際, 呼び出し側(main)と foo_1での変数のアドレスをみると, 呼び出し側での実引数 のアドレスと関数内部での仮引数のアドレスが一致している. (当たり前のことである.)
このことを「関数副作用」と呼ぶ.
• なお,大域変数と局所変数は,それらが格納されるアドレス空間が全く異っていることに注 意しよう.
★ ex12-4.c 配列を関数に渡す(配列要素(文字列)のコピー)
• ここでは,文字列aをコピーする数種類の方法を考える.
• 単純に“for(i=0;i<=len;i++) *(b+i) = *(a+i) ;” によって文字列長lenの文字列a
を bにコピーすることができる.
• 文字列は“Null Terminate”であることを用いれば, “while((*(c+i) = *(a+i))) i += 1 ;”
によっても文字列aをcにコピーすることができる.
• “char *_strcpy(char *t, char *s)” で定義した関数には, その実引数としてchar 型
の配列を渡すことができる. なぜなら, char型の配列aを実引数に用いることにより, 関 数にはaの先頭アドレスが渡されるため,関数側からは,実引数が配列であってもポインタ であっても,実際には「アドレスが入っている」という意味では区別はない.
• “char *_strcpy(char *t, char *s)”で定義した関数内部で,
“while((*(t+i) = *(s+i))) i += 1 ;”とすれば,文字列sをtにコピーすることがで
きる.
• 次に, “char *strcpy(char *t, const char *s)”で定義した関数内部では,
“while((*t++ = *s++)) ;” によって文字列sを tにコピーすることができる.
ここで“t++”とはtの示すアドレスをインクリメントすることであり,実際には,tの「次
のアドレス」をtに代入することとなる. (これが文字列コピーの最も標準的なコードで ある)
• これらの関数は「コピーした文字列の先頭アドレス」というchar型へのポインタを戻り 値にしている.
• “const char *s”の“const”とは,「定数」を意味し,これがついたオブジェクトは変更
が不可能になる. この場合は指し示すポインタの中身の変更ができなくなる. しかし,ポイ ンタ自身の変更は可能である.
• ところが, “char a[N], e[N]”と定義した関数内で
“while((*e++ = *a++)) ;” とすると「文法エラー」が発生する. これがなぜかを次の例
で考えてみよう.
★ ex12-5.c ポインタと配列の違い
• ここでは,
char a[] = "message" ; char *p = "message" ;
という2つの方法で文字列を定義している. これらの定義の違いを調べる.
• pに関しては,
for(i=0;*(p+i);i++) printf("%s\n", p+i) ; for(;*p;p++) printf("%s\n", p) ;
のいずれの操作も可能である.
• ところがaに関しては,
for(i=0;*(a+i);i++) printf("%s\n", a+i) ; は可能であるが,
for(;*a;a++) printf("%s\n", a) ;
は “a++”で「文法エラー」となる.
• また,
a[0] = ’M’ ;
は「配列の要素の変更」であるため,当然許されるが, p[0] = ’M’ ;
は許されない.
• これらの2つの定義の違いは以下の通り.
char a[] = "message" ;
は「要素数 8の char型の配列」を定義しているので, aで示されるアドレスから8 バイ トを使って文字列“message”が格納される. このaで示されたアドレスは「固定された場 所」である.
一方,pは
char *p = "message" ;
と定義されているが,これは,char型へのポインタpを定義して,「ある場所に格納された 文字列リテラル(この場合は“message”)のアドレス」をpに代入する(このアドレスで 初期化する)という意味を持つ文である.
• これらの定義の違いにより,pのインクリメントは可能であるが, aのインクリメントが許 されない理由は明らかであろう.
• また,pはmessageという,どこかに静的に保存された定数オブジェクトへのポインタであ
るため, p[0] = ’M’という代入は「定数オブジェクト」への代入となり,処理系によって
は実行時エラーとなることがある.
• この意味で「Cでは配列とポインタは同じ」という言葉を真に受けるとハマってしまうこ とになる.
★ ex12-6.c 値の入れ替えを行う関数
ここでは,ポインタを使った有名な例として,「値の入れ替えを行う関数」“swap”を作ってみ よう.
• 変数に格納された値を入れ替えるためには, 次のようなコードを用いることが多い.
c = a ; a = b ; b = c ;
(a,b,cともに同じ型のオブジェクトと仮定している.)
• この例を単純に関数に入れたものが
void not_swap_char(char a, char b)
である. このままでは, ex12-3.cで見たように,変数の値の入れ替えは実現できない.
• char型の変数の値を入れ替えるには,仮引数としてchar型へのポインタを用いなければ ならない.
void swap_char(char *a, char *b)
• 次にint型の変数の値を入れ替えるためには,仮引数としてint型へのポインタを用いな ければならない.
void swap_int(int *a, int *b)
• この例で, “swap_char”を用いてint型の変数の値を入れ替えてみよう. その呼び出し方 法は
swap_char((char *)&n, (char *)&m) ;
となる. ここで,n,mはint型のオブジェクトであるので,単純に&n,&mとすると,これら は int型のオブジェクトのアドレスであるため,値の型としてはint型へのポインタとな る. そのため,仮引数定義との型の不整合が生じる. これを回避するため, 明示的に(char
*)によってchar型へのポインタに型変換している.
なお,どのような型へのポインタであっても,そのオブジェクトはアドレスを代入するため に必要かつ十分な長さの領域となるため,ポインタの型変換は自由に行うことができる.
しかし,インクリメントやポインタ演算を行うため,型を指定しなければいけない.
• さて, “swap_char” を用いてint型の変数の値を入れ替えると, 正しい結果を得ることが できない.
これは, “swap_char”内部で *aがchar型へのポインタとなっているため, c = *aの代 入では「右辺値」としてaの示すアドレスの先頭バイトのみが参照されるためである.
• 逆に “swap_int” を用いて char 型の変数の値を入れ替えると, 「実行時エラー」 (Bus
Error)が発生する可能性がある. (いつでもそうなるかどうかはわからない.)
これは,(難解な話だが)ワード境界以外の境界から複数バイトを読み出そうとする場合に 生じるエラーである.
• しかし,「型が違っても変数の値の入れ替えくらいはできなきゃ大変!」と思うのが自然で あり,それを解決するための方法が次の例である.
★ ex12-7.c 汎用データポインタ
• 「型が違っても変数の値の入れ替えくらいはできなきゃ大変!」ということで,これを解決 するためには,関数に対して入れ替えたい変数へのポインタだけでなく,その変数のサイズ も渡してしまえばよい.
• 具体的には
void swap(void *a, void *b, size_t size) という形で関数を作成する.
ここで出てきた「void *」を汎用データポインタと呼び,「どんな型へのポインタも受け 取ることができるポインタ」である.
• 汎用データポインタを使う際には,関数側で「インクリメント」がどのような意味があるか を知るために,char型へのポインタ(最もサイズの小さなオブジェクトへのポインタ)に 変換しなければならない.
★ ex12-8.c ポインタの配列とポインタへのポインタ
• “char *str[3]” と定義すると, 「char 型へのポインタ」型の要素数 3 の配列が定義で
きる.
• “char **pstr”と定義すると,「char型のポインタ」へのポインタが定義できる.
• 上で定義したchar *str[3]のstr[0]はchar型へのポインタであるので,strは「char 型へのポインタ」へのポインタとなる. すなわち,char **pstrは「文字列への列」と理解 することができる.
• “char *str_array[] = {"abc", "defg", "hijkl"}”と定義すると,「char型のポイン タ」型の配列が定義できる. この時, “str_array[i]”はこの配列のi番目の要素である文 字列へのポインタとなる.
• main関数の仮引数定義にあるargvは「コマンドライン引数」へのポインタであり,その 要素数がargcに格納されている.
★ ex12-9.c 文字列に含まれる文字の最初の位置を見つける
• 関数 “char *strchr(const char *s, int c)” は標準関数に含まれるもので, 文字列s
の中からcに一致する最初の文字のポインタを返す.
• もし,一致するものが無いときにはNULLを返している. “NULL”とは「何も指し示さない」
という特別なポインタである.
• 関数の戻り値が“char *”となっているので, “p = strchr(s,’i’)”として得られたpを
そのままprintf関数に使うことができる.
• “p-s”という「謎」の「ポインタの減算」は,pの位置がsの先頭から何文字目かを表してい
る. 「ポインタの減算」は「それらのポインタが同じオブジェクト内を指し示しているとき に限り」意味を持つ. また,p-sの結果は「ポインタ」ではなく「符号なし整数」unsigned intまたはunsigned longのいずれかにtypedefされたsize_t型の値となる.
電子メールで「今日の講義の感想や意見」を送ってください.
★
ex12-1.c
の内容
/* $Id: ex12-1.c,v 1.3 2004-06-30 08:01:11+09 naito Exp $ */
#include <stdio.h>
int main(int argc, char **argv) {
int n = 1, m = 3 ; int *pn, *pm ; pn = &n ;
/* printf("*pn = %d, *pm = %d, n = %d, m = %d\n", *pn, *pm, n, m) ; */
printf("*pn = %d, n = %d, m = %d\n", *pn, n, m) ;
*pn = 2 ; /* ここで n の値はどうなっているか? */
printf("*pn = %d, n = %d, m = %d\n", *pn, n, m) ;
/* printf("*pn = %d, *pm = %d, n = %d, m = %d\n", *pn, *pm, n, m) ; */
pm = pn ; /* この時点で pm の指し示すものは何か? */
printf("*pn = %d, *pm = %d, n = %d, m = %d\n", *pn, *pm, n, m) ; pn = &m ; /* この時点で pn, pm の指し示すものは何か? */
printf("*pn = %d, *pm = %d, n = %d, m = %d\n", *pn, *pm, n, m) ;
*pm = 5 ; /* これは n, m のいずれの(それとも両方の?)値を変更したことになるか? */
printf("*pn = %d, *pm = %d, n = %d, m = %d\n", *pn, *pm, n, m) ;
printf("pn = %p\n", pn) ; printf("pm = %p\n", pm) ; printf("address of n = %p\n", &n) ; printf("address of m = %p\n", &m) ; return 0 ;
}
★
ex12-2.c
の内容
/* $Id: ex12-2.c,v 1.4 2004-06-25 11:29:32+09 naito Exp $ */
/* 配列とポインタ */
#include <stdio.h>
int main(int argc, char **argv) {
int a[10] = {0,1,2,3,4,5,6,7,8,9} ; char s[11] = "ABCDEFGHIJ" ;
int *pa ; char *ps ; int i ; ps = s ;
printf("%c %c %p %p\n", *ps, s[0], ps, &s[0]) ; printf("%c %c %p %p\n", *(ps+1), s[1], ps+1, &s[1]) ; ps = s+1 ;
printf("%c %c %p %p\n", *ps, s[1], ps, &s[1]) ; printf("%c %c %p %p\n", *(ps+1), s[2], ps+1, &s[2]) ; ps = &s[2] ;
printf("%c %c %p %p\n", *ps, s[2], ps, &s[2]) ; printf("%c %c %p %p\n", *(ps+1), s[3], ps+1, &s[3]) ; printf("%c %c %p %p\n", s[0], *s, &s[0], s) ;
printf("%c %c %p %p\n", s[2], *(s+2), &s[2], s+2) ; ps = s+1 ;
printf("%s\n", ps) ; ps = s ;
for(i=0;i<10;i++)
printf("%d %c %c %c %p %p %p\n", i, s[i], *(ps+i), ps[i], &s[i], s+i, ps+i) ; pa = a ;
printf("%d %d %p %p\n", *pa, a[0], pa, &a[0]) ; printf("%d %d %p %p\n", *(pa+1), a[1], pa+1, &a[1]) ; pa = a+1 ;
printf("%d %d %p %p\n", *pa, a[1], pa, &a[1]) ; printf("%d %d %p %p\n", *(pa+1), a[2], pa+1, &a[2]) ; pa = &a[2] ;
printf("%d %d %p %p\n", *pa, a[2], pa, &a[2]) ; printf("%d %d %p %p\n", *(pa+1), a[3], pa+1, &a[3]) ; printf("%d %d %p %p\n", a[0], *a, &a[0], a) ;
printf("%d %d %p %p\n", a[2], *(a+2), &a[2], a+2) ; pa = a ;
for(i=0;i<10;i++)
printf("%d %d %d %d %p %p %p\n", i, a[i], *(pa+i), pa[i], &a[i], a+i, pa+i) ; return 0 ;
}
★
ex12-3.c
の内容
/* $Id: ex12-3.c,v 1.2 2004-06-25 10:17:10+09 naito Exp $ */
/* ポインタを引数にする関数 */
#include <stdio.h>
int foo_0(int) ; int foo_1(int *) ;
int a=1 ;
int main(int argc, char **argv) {
int b=2 ;
printf("(main)\taddress of a = %p\n", &a) ; foo_0(a) ; printf("%d\n", a) ;
foo_1(&a) ; printf("%d\n", a) ;
printf("(main)\taddress of b = %p\n", &b) ; foo_0(b) ; printf("%d\n", b) ;
foo_1(&b) ; printf("%d\n", b) ; return 0 ;
}
int foo_0(int a) {
printf("(foo_0)\taddress of a = %p\n", &a) ; a += 1 ;
return 0 ; }
int foo_1(int *a) {
printf("(foo_1)\taddress of a = %p\n", a) ;
*a += 1 ; return 0 ; }
★
ex12-4.c
の内容
/* $Id: ex12-4.c,v 1.6 2004-06-30 08:00:35+09 naito Exp $ */
/* 配列要素のコピー */
#include <stdio.h>
#include <strings.h>
#define N 11
char *_strcpy(char *, char *) ; char *strcpy(char *, const char *) ; int main(int argc, char **argv) {
char a[N] = "0123456789" ; char b[N], c[N], d[N], e[N] ; int i, len ;
len = strlen(a) ;
for(i=0;i<=len;i++) *(b+i) = *(a+i) ; i = 0 ;
while((*(c+i) = *(a+i))) i += 1 ; _strcpy(d,a) ;
strcpy(e,a) ;
printf("a = %s\n", a) ; printf("b = %s\n", b) ; printf("c = %s\n", c) ; printf("d = %s\n", d) ; printf("e = %s\n", e) ;
/* ところが, 以下はエラーになる */
/* while((*e++ = *a++)) ; */
return 0 ; }
char *_strcpy(char *t, char *s) {
int i = 0 ;
while((*(t+i) = *(s+i))) i += 1 ; return t ;
}
char *strcpy(char *t, const char *s) {
char *save=t ;
while((*t++ = *s++)) ; return save ;
}
★
ex12-5.c
の内容
/* $Id: ex12-5.c,v 1.2 2005-07-04 17:34:21+09 naito Exp $ */
/* ポインタと配列の違い */
#include <stdio.h>
int main(int argc, char **argv) {
char a[] = "message" ; char *p = "message" ; int i ;
printf("p = %s\n", p) ;
for(i=0;*(p+i);i++) printf("%s\n", p+i) ; printf("p = %s\n", p) ;
for(;*p;p++) printf("%s\n", p) ; printf("a = %s\n", a) ;
for(i=0;*(a+i);i++) printf("%s\n", a+i) ; /* ところが, 次はエラーになる */
/* for(;*a;a++) printf("%s\n", a) ; */
a[0] = ’M’ ; /* これは許される */
printf("a = %s\n", a) ;
/* p[0] = ’M’ ; これは許されない */
return 0 ; }
★
ex12-6.c
の内容
/* $Id: ex12-6.c,v 1.2 2004-06-25 11:28:34+09 naito Exp $ */
/* ポインタを引数にする関数 */
#include <stdio.h>
void swap_char(char *, char *) ; void not_swap_char(char, char) ; void swap_int(int *, int *) ; int main(int argc, char **argv) {
char a = ’a’, b = ’b’ ;
int n = 0x01020304, m = 0x05060708 ; not_swap_char(a,b) ;
printf("a = %c, b = %c\n", a, b) ; swap_char(&a,&b) ;
printf("a = %c, b = %c\n", a, b) ; swap_int(&n, &m) ;
printf("n = %08x, m = %08x\n", n, m) ; /* 次は BUG が生じる */
swap_char((char *)&n, (char *)&m) ; printf("n = %08x, m = %08x\n", n, m) ; /* 次は実行時エラーが生じる可能性がある */
/* swap_int((int *)&a, (int *)&b) ; */
return 0 ; }
void swap_char(char *a, char *b) {
char c ;
c = *a ; *a = *b ; *b = c ; return ;
}
void not_swap_char(char a, char b) {
char c ;
c = a ; a = b ; b = c ; return ;
}
void swap_int(int *a, int *b) {
int c ;
c = *a ; *a = *b ; *b = c ; return ;
}
★
ex12-7.c
の内容
/* $Id: ex12-7.c,v 1.2 2004-06-25 13:39:57+09 naito Exp $ */
/* 一般の swap 関数(汎用データポインタ) */
#include <stdio.h>
void swap(void *, void *, size_t size) ; int main(int argc, char **argv)
{
char a = ’a’, b = ’b’ ;
int n = 0x01020304, m = 0x05060708 ; swap(&a,&b,sizeof(char)) ;
printf("a = %c, b = %c\n", a, b) ; swap(&n,&m,sizeof(int)) ;
printf("n = %08x, m = %08x\n", n, m) ; return 0 ;
}
void swap(void *a, void *b, size_t size) {
char c ; int i=0 ; while(i<size) {
c = *(char *)a ;
*(char *)a++ = *(char *)b ;
*(char *)b++ = c ; i += 1 ;
}
return ; }
★
ex12-8.c
の内容
/* $Id: ex12-8.c,v 1.3 2005-07-03 16:37:19+09 naito Exp $ */
/* ポインタの配列とポインタへのポインタ */
#include <stdio.h>
int main(int argc, char **argv) {
char **pstr ; char *str[3] ;
char s0[]="string 0", s1[]="string 1", s2[]="string 2" ; char *str_array[] = {"abc", "defg", "hijkl"} ;
int i ;
str[0] = s0 ; str[1] = s1 ; str[2] = s2 ; pstr = str ;
printf("%s\n", str[0]) ; printf("%s\n", str[1]) ; printf("%s\n", str[2]) ;
for(i=0;i<3;i++) printf("%s\n", *(pstr+i)) ; for(i=0;i<3;i++) printf("%s\n", str_array[i]) ; for(i=0;i<argc;i++) printf("%s\n", argv[i]) ; return 0 ;
}
★
ex12-9.c
の内容
/* $Id: ex12-9.c,v 1.4 2005-07-03 16:37:43+09 naito Exp $ */
/* 文字列に含まれる文字の最初の位置を見つける */
#include <stdio.h>
char *strchr(const char *, int) ; int main(int argc, char **argv) {
char s[] = "This is a test." ; char *p ;
if ((p = strchr(s,’i’)) != NULL) { printf("%s\n", p) ;
printf("%lu\n", p-s) ; }
else
printf("Not Found\n") ;
if ((p = strchr(s,’x’)) != NULL) { printf("%s\n", p) ;
printf("%lu\n", p-s) ; }
else
printf("Not Found\n") ; return 0 ;
}
char *strchr(const char *s, int c) {
while((*s)&&(*s++ != c)) ; if (!*s) return NULL ; return (char *)(s-1) ; }
【課題】 以下の課題では,必要に応じて関数を作ってプログラムを書くこと. また,「標準関数」は自由に 利用してかまわないが,当然,その問題自身の目的になっている標準関数は利用してはならない. (必 要なものはstdio.h,ctype.h,strings.hに含まれるものだけである.)
なお,exercise-12-2からexercies-12-4は「閏秒」については考慮しなくて良い.
★ exercise-12-1 次の仕様をみたす関数をつくりなさい.
【形式】
int ext_gcd(int a, int b, int *x, int *y)
【機能説明】
2つの正の整数a,bに対して,拡張されたユークリッドの互除法を用いて ax+by= gcd(a, b)
をみたす x,y を, xy= 0 をみたすもので,一組求めます. 戻り値はa とb の最大公約数 です.
★ exercise-12-2 year年 month月day日hour時minute分 second秒を与えて, 1970年1 月 1日 午前0時 0分0秒からの累積秒数をunsigned long型の値で返す関数を書きなさい. た だし,hourは24時間表示とします.
【ヒント】 累積日数を計算すればよい. 閏年の日数処理は,
static int normal_year[]={31,28,31,30,31,30,31,31,30,31,30,31} ; static int leap_year[] ={31,29,31,30,31,30,31,31,30,31,30,31} ; int *days_of_month=normal_year ;
として,閏年の場合にはポインタをつけかえればよい.
★ exercise-12-3 unsigned long型の値 nを 1970 年1 月 1 日 午前0 時0 分 0 秒からの累積 秒数と考えて,nを与えたときに,それが year年month月day日hour時minute分 second 秒となるyear,month,day,hour,minute,secondを求める関数を書きなさい. ただし,hour は 24時間表示とします.
☆ exercise-12-4 unsigned long型の値 nを 1970 年1 月 1 日 午前0 時0 分 0 秒からの累積 秒数と考えて, nを与えたときに,次の形式の「日付文字列」へのポインタを返す関数を書きな
さい. 形式は“WWWMMMDDHH:MM:SSYYY”ただし,
• DDは日付を示す2桁の数値. (01などとします. 以下同様)
• HHは時間を示す2桁の数値.
• MMは分を示す2桁の数値.
• SSは秒を示す2桁の数値.
• YYYYは年を示す4桁の数値.
• MMMは月を示す3桁の文字. 1月からそれぞれJan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Decとします.
• WWWは曜日を示す3桁の文字. 日曜日からそれぞれSun Mon Tue Wed Thr Fri Satとし ます.
曜日を計算するために1970年1 月1 日は木曜日であることを利用してかまいません.
【ヒント1】 「日付文字列」へのポインタを返すためには,このフォーマットに従う文字列の 長さが24文字であることを利用して,関数内部で定義する静的変数
static char str[25] ; へのポインタを返せばよい.
【ヒント2】 「日付文字列」を作成するためには,標準関数sprintfを使うのがよい.
★ exercise-12-5 以下の標準関数を書きなさい.
★ exercise-12-5-1 strcat
★ exercise-12-5-2 strncat
★ exercise-12-5-3 strncpy
★ exercise-12-5-4 strcmp
★ exercise-12-5-5 strncmp
★ exercise-12-5-6 strspn
★ exercise-12-5-7 strcspn
★ exercise-12-5-8 strstr
★ exercise-12-5-9 strpbrk
【注意1】 多分「易しい順」で「依存関係のある順」に並んでいるはず.
【注意2】 これらを書くためには, 前のものを利用すると簡単になるはず.
【注意3】 それぞれの標準関数の仕様は「オンラインマニュアル」を参照のこと.
☆ exercise-12-6 次の仕様をみたす関数をつくりなさい.
【形式】
char *strrstr(const char *s1, const char *s2)
【機能説明】
文字列 s1の中で文字列s2と最後に一致する部分の先頭アドレスを返します. もし,一致 する部分が無ければNULLを返します.
【注意】 この関数は標準関数の中には含まれていません.
★ exercise-12-7 次の仕様をみたす,文字列をトークン分解する関数を作りなさい.
【形式】
char *_strtok(char *t, const char *s, const char *d)
【機能説明】
文字列sを「区切り文字の集合」dによってトークン分解した,最初のトークンをtに返 し,戻り値として,sの中でトークンtの「次の文字」へのポインタを返します. もし,トー クンが一つも見つからなければ NULLを返します. そのときtの内容は保証されません.
さらに,この関数を利用して,文字列をトークン分解し,各トークンを順に出力するプログラム を書きなさい.
【注意1】 この関数は標準関数の中には含まれていません. また, 標準関数にあるstrtok と は仕様が異ります.
【注意2】 「トークン」(token)とは,指定された「区切り文字」によって区切った文字列の ことをいいます. また,与えられた文字列を「トークン分解」(token decomposition)す るとは,その文字列をトークンに分解することを言います.
【例】
区切り文字が「英数字以外」であるとき,文字列“Thisisatest.” をトークン分解 すると, “This”, “is”, “a”, “test”の4つのトークンに分解されます.
【ヒント】
ここまでに作成した文字列に関する関数をうまく組み合わせればよい.
☆ exercise-12-8 与えられた文字列中に含まれる「単語」の長さに関する頻度表を出力するプログ
ラムを書きなさい. すなわちn文字長の単語がいくつ含まれるかを表示するプログラムを書き なさいということ. ただし,文字列中に含まれる単語の最大長が10以上となるものは「10文字 以上」で一括して扱います. また,単語とは「英文字以外の文字」を区切り文字とした「トーク ン」のことと定義します.
★ exercise-12-9 最大1024文字の文字列中に「10進整数」をあらわす文字列が,最大100個入っ ているとする. この時,その文字列を受け取って,文字列中に含まれる「10進整数」をint型 の配列に格納するプログラムを書きなさい.
ただし,文字列中の区切り文字は「1つ以上の空白文字(標準関数isspaceが非零の値を返す 文字)」とし,「10進整数」をあらわす文字列とは,「先頭に+または-の「符号文字」が0個 以上1個以下あり,その後に「10進数字」(標準関数isdigitが非零の値を返す文字)が1個 以上続く」ものとします. また,文字列中には「空白文字」, 「符号文字」,「10進数字」以外 の文字は含まれないものとします.
また,「10進整数をあらわす文字列」からそれがあらわす整数への変換は,標準関数atoiを利 用してください. また, この問題では「桁あふれ」は考慮しないこととします.
★ exercise-12-10 「非負の10進整数」をあらわす文字列を受け取って,ex-11-10で定義した64 ビット整数として格納するプログラムを書きなさい.
逆に, 64ビット整数を文字列として「10進整数」に変換するプログラムを書きなさい.
「☆」のついた問題を7月18日深夜までに提出してください. 7月20日の講義の際に, 一部の問題の解答 例を配布します.
▼ 課題の提出について
• 「教育実習期間の課題」については,提出しなくても「単位認定」には関係ないものとします. でも, 可能なら提出してください.
• 今後の予定は以下の通りです.
– 学部の受講生及び大学院の受講生で教職免許に関わる授業として受講している学生に対しては, 7月20日の最終回の授業の前までに,出席状況及び課題の提出状況の個人別一覧をメールで送 付します.
– 7月13日及び7月20日の授業の課題の提出の期限は8月2日頃とする予定です. したがって,
「出席および課題の提出」により単位取得を希望する受講生の単位取得の結果は8月初旬頃にわ かります.
– 7月20日の授業では,「夏休みのレポート問題」を配布します. 「出席および課題の提出」によ り単位取得ができなかった受講生,それに該当しない大学院の受講生,「可」より良い評価を希 望する受講生の方は,「夏休みのレポート問題」に解答してください. その提出締切は8月中旬 頃とする予定です.