電脳ミツバチのコンピュータ広報室

銀座の屋上菜園を耕しています。コンピュータ畑も耕します。

②構造体自体をポインタで扱う

構造体をmallocして動的に確保したい。
こんなときも多いだろうがとりあえず書き方として

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "kouzoutai.h"

main()
{
	POINT *p_memallocate;
	p_memallocate=malloc(sizeof(struct p_kouzoutai));
	p_memallocate->a=strdup("testa");
	p_memallocate->b=strdup("testb");
	printf("%s\t%s\n",p_memallocate->a,p_memallocate->b);

}
結果
testa   testb

ここで->(アロー演算子)を使っているが別に使わなくてもよい。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "kouzoutai.h"

main()
{
	POINT *p_memallocate;
	p_memallocate=malloc(sizeof(struct p_kouzoutai));
	(*p_memallocate).a=strdup("testa");
	(*p_memallocate).b=strdup("testb");
	printf("%s\t%s\n",p_memallocate->a,p_memallocate->b);

}

代入時だけ戻してみた。
こんなことも出来る。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "kouzoutai.h"

main()
{
	POINT *p_memallocate;
	p_memallocate=malloc(sizeof(struct p_kouzoutai)*2);
	(p_memallocate+1)->a=strdup("testa");
	(*p_memallocate).b=strdup("testb");
	printf("%s\t%s\n",(p_memallocate+1)->a,p_memallocate->b);

}
結果
testa   teatb

ポインタの加算である。(mallocは二倍分取った)

*(p+1)はp[1]のようにも扱えたことより、配列のようにすることも出来る。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "kouzoutai.h"

main()
{
	POINT *p_memallocate;
	p_memallocate=malloc(sizeof(struct p_kouzoutai)*3);
	p_memallocate[1].a=strdup("testa");
	(p_memallocate+1)->b=strdup("testb");
	printf("%s\t%s\n",p_memallocate[1].a,(p_memallocate+1)->b);

}
	
結果
testa  testb


ポインタ*(p+1)
構造体(p+1)->a
配列p[1]構造体p[1].a
もちろんポインタの構造体で(p+1)->aを(*(p+1)).aとも書ける。(普通書かないが)
ポインタと配列の関係が理解していればおのずと構造体の方もイメージできるはず。
後はこれらを組み合わせた応用である。

最後に構造体同士を比較することは出来ない。

②構造体の初期化

構造体は初期化することが出来る。
例えば一度に全て0を入れたい場合、memset関数を使う。

#ifndef KOUZOUTAI_H
#define KOUZOUTAI_H
typedef struct p_kouzoutai
{
	char *a;
	char *b;
	char *c;
}POINT;
typedef struct a_kouzoutai
{
	char a[10];
	char b[10];
	char c[10];
}ARRAY;

#endif
#include<stdio.h>
#include "kouzoutai.h"
main()
{
	POINT syokika;
	memset(&syokika,'\0',sizeof(syokika));
	printf("syokika.a=%s\tsyokika.b=%s\tsyokika.c=%s\n",syokika.a,syokika.b,syokika.c);
}

結果	
syokika.a=(null)        syokika.b=(null)        syokika.c=(null)

memset関数は

#include <string.h>
void *memset(void *s, int c, size_t n);

と定義され先頭からnバイトまでをint cで埋める関数なのでコンパイラによっては0がnullで無いものもある。
よって移植性重視のプログラムではmemset関数を使うのは避けたほうがよい。

宣言時に初期化するのは感覚で分かるとおりである。

#include<stdio.h>
#include<string.h>
#include "kouzoutai.h"
main()
{
	POINT syokika={"name","住所","03-XXXX-XXXX"};
	printf("syokika.a=%s\tsyokika.b=%s\tsyokika.c=%s\n",syokika.a,syokika.b,syokika.c);
}

結果
syokika.a=name  syokika.b=住所  syokika.c=03-XXXX-XXXX

②関数のポインタと構造体を使ったプログラム

少し難しいが非常に有用なプログラム。

#include<stdio.h>
#include<string.h>
int list(void)
{
	printf("func list\n");
	return 1;
}
int show(void)
{
	printf("fanc show\n");
	
	return 1;
}
int quit(void)
{
	exit(0);
}

struct command{
	char *com_str;
	int (*com_func)(void);
};
struct command coms[]={
	{"ls",list},{"dir",list},
	{"cat",show},{"type",show},
	{"quit",quit},{"exit",quit},
	{NULL,NULL}
};
int do_command(char *command)
{
	struct command *p;
	for(p=coms;p->com_str!=NULL;p++){
		if(strcmp(p->com_str,command)==0){
			return p->com_func();
		}
	}
	printf("command not found\n");
	return 0;
	
}
	
int main()
{
	char command[80];
	char *p;
	while(1)
	{
		printf(">");
		fgets(command,8,stdin);
		if((p=strrchr(command,'\n'))!=NULL)
			*p='\0';			
		do_command(command);
	}
	return 0;
}

結果として>というプロンプトの後に好きな文字を打ち込み、
ls,dirならlist。catならshow・・・という感じでコマンド処理を行える。
mainのargvですんでしまうこともある。

①メンバに文字列をコピー

メンバに文字列をコピーする。なお上で作成したヘッダファイルkouzoutai.hを使う。

#include<stdio.h>
#include<string.h>
#include "kouzoutai.h"

int main(void){
	struct p_kouzoutai copy;
	strcpy(copy.a,"testcopy");	
	printf("%s\n",copy.a);
}
	
結果
異常終了
<||
結果うまくいかない。
今度は配列のほうでやってみる。
>||
#include<stdio.h>
#include<string.h>
#include "kouzoutai.h"

int main(void){
	ARRAY copy;
	strcpy(copy.a,"testcopy");	
	printf("%s\n",copy.a);
}

結果
testcopy

ついでにせっかく宣言したtypedefの方を使った。
それはともかくうまくいった。
文字列操作の項と上で散々やったように配列は配列で取った分の領域を確保しているのに対してポインタはアドレスである。
ポインタのときはわざとtypedefの型を使わなかったのはp_kouzoutaiをローカル変数で宣言しているということを伝えたかった。ローカル変数は初期化しない限り値は不定。そのアドレスに文字列をコピーしていたので結果異常終了を起こした。
ではポインタは使えないかというともちろんそんなことはない。
この場合strdup関数を使う。(まだANSII Cでは標準関数として認められていないが一般的に使われている。)strdup関数は引数に与えられた文字列のコピーをmallocした領域に作成し、そのアドレスを返す。

#include<stdio.h>
#include<string.h>
#include "kouzoutai.h"

int main(void){
	POINT copy;
	copy.a=strdup("testcopy");	
	printf("%s\n",copy.a);
}
結果
testcopy

なるたけ同じコードを流用する。
今度はうまく言った。
strdupの仕様はchar *strdup(const char *s);でmallocもししてくれているので非常に使い勝手がいい。
strdupに別のメンバ名を入れることもよく行われる
exp; copy.a=strdup(copy.b)

ついでにstrdupの中身を記述すると

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char *strdup(const char *src)
{
	char *p;
	if(src==NULL)
		return NULL;
	
	p=malloc(strlen(src)+1);
	if(p)	{strcpy(p,src);}
	return p;
}
	

こんな感じである。

①構造体の使い方

struct p_kouzoutai
{
char *a;
char *b;
char *c;
}

struct a_kouzoutai
{
char a;
char b;
char c[];
}

の違いは何だろうか?関数とポインタの項でやったが大きさ分のメモリを取るのが配列でアドレス分だけを取るのがポインタである。
早速見てみると、

#include<stdio.h>
main(){
typedef struct p_kouzoutai
{
	char *a;
	char *b;
	char *c;
}POINT;
typedef struct a_kouzoutai
{
	char a[10];
	char b[10];
	char c[10];
}ARRAY;

printf("p_kouzoutai=%d\n",sizeof(POINT));

printf("a_kouzoutai=%d\n",sizeof(ARRAY));

}
	
結果
p_kouzoutai=12
a_kouzoutai=30

見て分かるとおりポインタの方は4バイト*3で12バイト。配列は10バイト*3で30バイト。

①プロトタイプ宣言

もし、コードが複数ファイルにまたがっていた時、プロトタイプ宣言はどこでするのか。
答えから言うとヘッダファイルにするべきである。
そして、その関数を使う全てのコードファイルにそのヘッダをインクルードする。そうすれば仮にaというファイルのafanc関数を変更してそれがbに反映されないなんてことがなくなる。

①関数

関数を理解するには何といってもスタック(メモリ)の動作である。
詳しくはスタックの項にあるが、

void B(int b,int c)
{
int d=4;
}
void A(void)
{
int a=1;
B(2,3);
}

このような関数を呼び出したときに

|  |
|  |
|  |
|4 |d
|復帰情報
|2 |b
|3 |c
|1 |a    
|  |
   ̄

このような形でスタックに積まれる。
よく見てみると引数b,cは復帰情報の前に書いてある。
ならばこれらは関数Aから参照できるのではないか?
ということでやってみた。

#include<stdio.h>
void fanc(int b)
{
	printf("b=%d\n",&b);
	b=12;
}
void main()
{
	int a=0;
	printf("a=%d\n",*(&a+1));

	fanc(a);
}
結果
a=1245112
b=1245060

b=12の値をaのアドレスを増やすことで表示させてみたかった。

イメージ図
|  |
|  |
|  |
|  |
|復帰情報
|12|b
|0 |a← これにアドレスを1足して12を表示する。 
|  |
   ̄

だがどうにもうまくいかなかった。
しかしながらアドレスに1を足した「値」がアドレスっぽくなっていることからとりあえずアドレス計算が間違っていると判断する。
では実際a,b,cがこうなっていたらどんなアドレスをとるか見てみる。

#include<stdio.h>
void fanc(int c)
{
	printf("c=%d\n",&c);
	c=12;
}
void main()
{
	int a=0;
	int b;
	printf("a=%d\n",&a);
	printf("b=%d\n",&b);

	fanc(b);
}
結果
a=1245064
b=1245060
c=1245056

すると宣言が行われた順に「アドレスが減っている!」
これはスタックセグメントの特徴でヒープその他のセグメントは増えていき、
スタックセグメントに拡張されたデータが減っていくことでデータの衝突を抑えているからである。因みにローカル変数はスタックセグメントに格納される

スタックセグメント   ←ローカル変数を格納
------------------------ ↓減っていく
) ( )           (
------------------------ ↑増えていく
ヒープセグメント   mallocの動的な値を格納。
------------------------
BSSセグメント  
------------------------
データセグメント  
------------------------
テキストセグメント  

さて、原因が分かったところで先ほどの結果が

イメージ図
|  |
|  |
|  |
|  |
|復帰情報
|12|b
|0 |a ← 12を表示したかった。
|  |← つもりがここのアドレスを指してしまっていた。
   ̄

ということがなんとなく理解だろうか?
従って*(&a+1)ではなく*(&a-1)にすればbの値を得るはずだ。

#include<stdio.h>
void fanc(int b);
void main()
{
	int a=0;
	fanc(a);
	printf("a=%d\n",*(&a-1));

}
void fanc(int b)
{
	printf("b=%d\n",&b);
	b=12;
}
結果
a=12

aの値でbを得ることが出来た。

イメージ図
|  |
|  |
|  |
|  |
|復帰情報
|12|b←この値が表示された。
|0 |a← これにアドレスを1引く。 
|  |
   ̄

戻り値

関数において戻り値は非常に重要な概念である。
関数は戻り値を一つだけ呼び出し元に返すことが出来る。

return 0;
return a;

等は見慣れている。
返さなくても良い。
そして戻り値の型で関数の型が決まる。
return 0;ならint fanc(){}
return a;でaがint型ならint fanc(){},char型ならchar fanc(){}
戻り値がないならvoid fanc(){}
といった具合である。

ここで戻り値がポインタならどうなるのだろうか?

この場合文字列を返すことが挙げられる。
当然関数もchar *fanc(){}のようになる。

#include<stdio.h>
char *str(int a)
{
	char buf[10];
	sprintf(buf,"[%d]",a);
	printf("buf=%s\n",buf);
	return buf;
}
int main(void){
	printf("main str=%s\n",str(100));
	return 0;
}

結果
buf=[100]
main str=

int型を文字列として出力するのはキャストでこちゃこちゃやるよりsprintf関数を覚えたほうが早い。
といってもmainの方に帰ってこなかった。これはbufをローカル変数で宣言していることによる。
変数宣言の項でやったがローカル変数はそのブロックの終了と共に消える。従って、main関数に帰ってきたときには既にbufは消えてしまっているからだ。
ではstaticにするとどうなるか?

#include<stdio.h>
char *str(int a)
{
	static char buf[10];
	sprintf(buf,"[%d]",a);
	printf("buf=%s\n",buf);
	return buf;
}
int main(void){
	printf("main str=%s\n",str(100));
	return 0;
}
結果
buf=[100]
main str=[100]

あっさりうまくいった。だがこれではこんな問題が生じる。

#include<stdio.h>
char *str(int a)
{
	static char buf[10];
	sprintf(buf,"[%d]",a);
	printf("buf=%s\n",buf);
	return buf;
}
int main(void){
	printf("main str1=%s\tmain str2=%s\n",str(230),str(100));
	return 0;
}
buf=[100]
buf=[230]
main str1=[230] main str2=[230]

そう、mallocの項のfreeによる弊害に近い。
バッファが上書きされてしまうのだ。
つまり戻り値が一つしかないとはこういうことで戻り値のアドレスは常に同じなのだ。
別に打開策は単純で引数なら何度も取れるので引数をポインタで指定してやればよい。

#include<stdio.h>
char *str(int a,char *buf)
{
	sprintf(buf,"[%d]",a);
	printf("buf=%s\n",buf);
	return buf;
}
int main(void){
	char *bufa,*bufb;
	printf("main bufa=%s\tmain bufb=%s\n",str(230,bufa),str(100,bufb));
	return 0;
}
結果
buf=[100]
buf=[230]
main bufa=[230] main bufb=[100]

ついでに結果からfanc(a,b)においてスタック上にb,aの順で積まれていることにも注目したい。
関数で文字列を返すのはやや初級から脱する。
変数がローカルなのか、グローバルなのか。
ヒープ上なのかスタック上なのかそれとも別の場所か。
しっかり把握しないと使うのは難しい。

関数をポインタにする

関数のアドレスを調べてみる。

#include<stdio.h>
int fanc(){}
int main(){
	fanc();
	printf("fanc=0x%p\tmain=0x%p\n",fanc,main);
}
結果
fanc=0x00401150 main=0x00401155

mainとfancの違いが5バイト、
上の図を持ってくると

スタックセグメント   ←ローカル変数を格納
------------------------ ↓減っていく
) ( )           (
------------------------ ↑増えていく
ヒープセグメント   mallocの動的な値を格納。
------------------------
BSSセグメント  
------------------------
データセグメント  
------------------------
←main=0x...55
テキストセグメント   ←fanc=0x...50

つまりfancは5バイトということが分かる。
ついでに色々増やしてみる。

#include<stdio.h>
int fanc()
{
puts("");
}
int main(){
	fanc();
	printf("fanc=0x%p\tmain=0x%p\n",fanc,main);
}
結果

fanc=0x00401150 main=0x00401160

putsを入れたら16進なので12バイト増えた。
因みに中に定数を入れても変わらない。
定数はデータセグメントに入れられる。

#include<stdio.h>
int fanc()
{
printf("");
}
int main(){
	fanc();
	printf("fanc=0x%p\tmain=0x%p\n",fanc,main);
}
結果
fanc=0x00401150 main=0x00401160

printf関数も12バイト増えた。

#include<stdio.h>
int fanc()
{
int i;
}
int main(){
	fanc();
	printf("fanc=0x%p\tmain=0x%p\n",fanc,main);
}
結果
fanc=0x00401150 main=0x00401155

宣言しただけでは変わらない。
まぁなんかそんな感じ。
では何がうれしいのか?というとアドレスがあるというのはすなわち「ポインタで言い換えれる!」ということである。
早速利用してみる。

#include<stdio.h>
int fanc(int a)
{
printf("%d\n",a);
}
int main(){
	int (*pfanc)(int);//*宣言*//
	pfanc=fanc;    //*代入*//
	(*pfanc)(100);

}
結果
100

関数も変数と同じように宣言することが出来た。
さらには配列と同じようにも扱える。

このとき必ず括弧でくくらなければ「戻り値の型がポインタ」とみなされてしまう
さてこれを利用することによるメリットは結局何なのかという問題だが、
結果として構造体や配列などと組み合わせなければ利用価値はない。
使える場面としては

  • GUI処理としてイベントがあったら関数を呼び出すというようなイベント駆動型のプログラム
  • 割り込み処理プログラムに使われる。