プログラミング及び演習
第8回 ポインタ2 / 標準入出力 /デバッガ(教科書第10,9章) (2017/06/16)
大学院情報学研究科知能システム学専攻
教授 森健策
大学院情報学研究科知能システム学専攻
助教 小田昌宏

本日の講義・演習の内容


ポインタ
第10章
 おさらい 多重ポインタ


標準入出力 デバッガ

講義・演習ホームページ


http://www.newves.org/~mori/17Programming

ところで，

ポインタは理解できましたか?

出席

NUCTにて出席アンケートに回答
 回答1




回答2




今日の合言葉
理解度を5段階で記述

回答3


前回の演習課題について一言復習

ポインタとは?
変数や関数などがメモリの中で格納されている場所
 メモリ空間上の「アドレス」を指し示すもの
 ポインタ変数


ポインタを取り扱うことのできる変数

一般的にポインタを使うプログラムは作成や理解が難しい ポインタが常にどこを指すかを考える ポインタ変数を初期化せずにそれ指し示す内容をアクセスするとSegmentation Faultが発生
0x00000019

0x00000018

0x00000017

0x00000016

0x00000015

0x00000014

0x00000013

値

0 0 0 05 0 0 00x00000012

0x00000011

0x00000010

0x0000000F

0x0000000E

0x0000000D

0x0000000C

0x0000000B

0x0000000A

0x00000009

0x00000008

0x00000007

0x00000006

0x00000005

0x00000004

0x00000003

0x00000002

0x00000001

0x00000000

番地

ポインタを理解するうえで

変数の内容はメモリ空間のどこかに記録される
 コンピュータのメモリ

番地と値(大抵の場合1つの番地に1バイト)
 書き込み動作 "0x00001234番地に値0x26を書け" 読み込み動作 "0x00001234番地の値を読め" 複数バイトからなる変数は複数の番地を使って記録

メモリ空間

復習

変数はどのように記憶されるか

int i=5;とした場合iの中身を記憶する場所がどこかに確保される
 そこに0x00000005が書き込まれる (intなので4byte)char c=6;とすればcの中身を記憶する場所がどこかに確保される
 そこに0x06が書き込まれる (charなので1byte)


0x06

0x08047abf

0x05

0x08047ac00x00

0x08047ac1

0x00

0x08047ac2

0x00

0x08047ac3

復習

ポインタとはメモリ空間での「番地」を表す
 それでは：

ある変数の中身が格納されている番地を知るにはどうするの?
 番地はわかっているのだけどそこに格納されている値は何?
'*' と '&'が鍵となる記号*pと書くとpが指し示すアドレス(番地)の内容を表す
 &iと書くとiの中身が格納されているアドレス(番地)を表す


0x06

0x08047abf

0x05

0x08047ac00x00

0x08047ac1

0x00

0x08047ac2

0x00

0x08047ac3

復習

ポインタ変数の宣言
'*'を変数名の前につける
int i, *p; int *p, q; int *p, *q;

pがint型データへのポインタ変数
pはポインタ変数 qは単なるint変数
pもqもポインタ変数

型はその番地にある値が表しているデータの型を示す
int *p; pが指し示す番地にはint型データがある float *q; qが指し示す番地にはfloat型データがある char *r; rが指し示す番地にはchar型データがある 型によって何バイト分の値を使うかが異なる
復習

番地を得るには?
& 演算子を使う &iと記述すればiが格納される番地を表す
 得られた番地はポインタ型変数に代入
 例

int i=5; char c=6;
int *p1; char *p2;
p1 = &i;p2 = &c;
 p1には0x08047ac0
p2には0x08047abf が代入

0x06

0x08047abf

0x05

0x08047ac00x00

0x08047ac1

0x00

0x08047ac2

0x00

0x08047ac3



実際の番地は計算機毎に異なる
ここはあくまでも例!!

復習

番地の内容を得るには?
*演算子を使いその番地に書かれている内容を得る
 注意: ポインタ変数の型で値を得る


charなら1byte分をアクセス int なら4byte分をアクセス int i=5; char c=6;
int *p1; char *p2;
p1 = &i;p2 = &c;
printf("%d¥n",(*p1)+(*p2));
 *p1は5*p2は6 を表す


0x06

0x08047abf

0x05

0x08047ac00x00

0x08047ac1

0x00

0x08047ac2

0x00

0x08047ac3

実際の番地は計算機毎に異なる
ここはあくまでも例!!

復習

ポインタとメモリ空間
0x06

0x08047abf

p2が指す番地

iの中身が格納されている場所

int i=5;cの中身
char c=6;
int *p1, *p2;
p1=&i; p2=&c;

0x05

0x08047ac0p1が指す番地

0x00

0x08047ac1

0x00

0x08047ac2

0x00

0x08047ac3

p2の中身が格納
p1の中身 されている場所

…

…
bf

どこかの番地

7a04
08
c07a04

実際の番地は計算機毎に異なる

08

ここはあくまでも例!!

復習

間接参照
ポインタを仲介して別の変数の値を読み書きすること
 例




int i, j, *p, *q;
p=q=&i;
*p=1;
p=&j
*p=*q

ポインタ変数自身に代入を行うこと，それが指し示している先に代入を行うことの違いに注意 ポインタ変数が何も指していない状態
NULLポインタを用いる;
 int *p;p = (int *) NULL;
復習

ポインタと配列 0x08047ac10x08047ac0a[0]a, &(a[0])a[1]a+1, &(a[1])a[2]a+2, &(a[1])a[3]a+3, &(a[1])a[4]a+4, &(a[1])0x08047ac20x08047ac3
int a[10]；と宣言0x08047ac4
 aは配列先頭へのポインタ *(a+2) で a[2]にアクセス可能

*(a+2) と a[2]は同じ(a+2)と記述したとき単に番地に2を足すのではなく1要素の(バイト数*2)を足した番地となる
配列の2番目を正しくアクセス可能 右の例だと
a→ 0x08047ac0
a+1→ 0x08047ac4(int型=4byteのため)復習

関数ポインタ関数のエントリポイントのアドレスを保持する変数
 プログラムをコンパイルするとプログラムの各関数についてエントリポイントを作成
 関数が呼び出されると実行制御はエントリポイントに移行
 エントリポイント=アドレス


関数へのポインタが得られればポインタを使って関数を呼び出すことが可能

復習

関数ポインタの作成
ポインタをその関数の戻り値の型と同じ型を持つポインタ変数と宣言
 仮引数があれば続けて宣言
 宣言の例int (*p)(int x, int y); /* *pは括弧でくくる */関数ポインタ取得例
int sum(int a, int b);という関数がある場合
 p = sum; とすれば関数ポインタ取得可能
 呼び出しは result = (*p)(d,e); のように行う
 result=p(d,e);でもOK復習

関数ポインタと呼び出し例#include <stdio.h>
int add(int x, int y){return(x+y);}
int mult(int x, int y){return(x*y);}
int main(){int(*fp)(int,int);
fp=add;printf("add %d¥n",(*fp)(2,3));
fp=mult;
printf("mul %d¥n",(*fp)(2,3));}

多重間接参照
ポインタを使って別のポインタを指すこと
 文字列の配列（文字の配列ではない）などを表すのに用いられる


ポインタ

変数

ポインタ

ポインタ

変数

ポインタ

ポインタ

ポインタ変数多重間接参照

宣言時には*を2つつける
char **str;
 strは文字ポインタへのポインタを表す


ポインタのポインタが示す値にアクセスするには*演算子を２つつけるchar **mp, *p, ch;p = &ch;mp = &p;**mp = 'A';

ポインタ配列
配列の要素としてポインタが並んだもの
 例えば


文字列の配列 = 各配列の要素は文字列の先頭へのポインタ
 配列の要素の順番を替えるのみで文字列の順番を変更可能defghidefghi

jklnm

jklnm

fdfsdfs

fdfsdfs

文字列の配列char day1[][10]={"Sunday", "Monday","Tuesday", "Wednesday", "Thursday","Friday", "Saturday"}; char *day2[]={"Sunday", "Monday","Tuesday", "Wednesday", "Thursday","Friday", "Saturday"}; printf( "%s %s¥n", day[1], day2[1]);で月曜日が表示される
 2つの違いは一体何?
(ポインタを学習する必要あり)char day1[][10]タイプ
day1
S u n d a ｙ¥0

M o n d a y

¥

¥0

¥

W e d n e s d a y 0 T h u r s d a y 0¥

S a t u r d a y 0

T u e s d a y¥0

F r i d a y¥0char *day2[]タイプ
day24byte

S u n d a y¥
07byteM o n d a y¥
0

7byte

4byte
4byte
4byte
4byte

T u e s d a y¥
0W e d n e s d a y
T h u r s d a y

4byte
4byte

F ri d a y

¥
0

¥
0

S a t u r d a y

曜日名文字列の先頭位置を7個記憶

8byte
¥
0

10byte
9byte
7byte

¥
0

9byte

7つの曜日名文字列はメモリ空間では連続していない入力されたテキストをアルファベット順に分類 (KR p.131を改変)

全体の流れ
 入力全行を読み込む
 それを分類する
 順番に印字する
 各行への先頭を表すポインタ配列を確保し，配列内に格納されているポインタの順番を変更することで行を並び替え並べ替えプログラム (1)#include <stdio.h>#include <string.h>#define MAXLINES 5000char *lineptr[MAXLINES];
int readlines(char **lineptr, int nlines);
int writelines(char **lineptr, int nlines);
int getline(char *line,int maxlen);void bubble(char *data[], int num);
int main(){
int nlines;
if((nlines=readlines(lineptr,MAXLINES))>=0){
bubble(lineptr,nlines);
writelines(lineptr,nlines);
return 0;}else{
printf("error: input too big to start!¥n");return 1;}}

並べ替えプログラム (2)#define MAXLEN 1000
int readlines(char **lineptr, int maxlines){
int len, nlines;
char *p, line[MAXLEN];
nlines = 0;
while((len=getline(line,MAXLEN))>0){
if(nlines>=maxlines||(p=(char*)malloc(len*sizeof(char)))==NULL){
return -1;}else{
line[len-1] = '¥0';
strcpy(p,line);
lineptr[nlines++]=p;}}return nlines;}並べ替えプログラム (3)int writelines(char **lineptr, int nlines){
int i;
for(i=0;i<nlines;i++){printf("%s¥n", lineptr[i]);}}int getline(char *line,int maxlen){
if(fgets(line,maxlen,stdin)!=NULL)
return(strlen(line));else
return -1;)}

並べ替えプログラム (4)void bubble(char *data[], int num){int flag,i;char *tmp;
do{flag=0;for(i=0;i<num-1;i++){if(strcmp(data[i],data[i+1])>0){
tmp=data[i]; data[i]=data[i+1]; data[i+1]=tmp;
flag=1;}}}while(flag!=0);}

配列を動的に割り当てる
malloc配列の要素分だけ記憶域を確保すればよい #include <stdlib.h>
int *p, num;
scanf("%d",&num);p = (int *)malloc(sizeof(int)*num);p[2] = 1;…
…
free(p); 任意の大きさの配列をプログラム実行中作成可能

2次元配列以上は?

1次元配列で多次元配列を実現


p x q x r の配列を確保 (3次元)

int *data;
data = (int *)malloc(p*q*r*sizeof(int));

アクセス方法(i,j,k)における値を取り出す (data[k][j][i]) a=data[i+j*p+k*p*q];(もしくはa=data[i+(j+k*q)*p];)同様のことを行えばn次元配列を1次元配列で代用可能

標準入出力UNIXにおける3つの標準テキストストリーム
標準入力
 通常はキーボード

FILE型ポインタ → stdin

標準出力
通常は画面
 FILE型ポインタ → stdout




標準エラー
通常は画面
 FILE型ポインタ → stderr
 プログラムの正常出力とエラーメッセージ出力を区別するために設けられている


標準入出力例 fscanf(stdin "%d", &a);
 fprintf(stdout, "%d", i); fprintf(stderr, "Error: Code = %d",code);リダイレクション


標準入力・出力・エラーの結合先は変更可能
 通常
 入力:キーボード
出力:画面、エラー:画面
 それぞれの入出力先をファイルに変更可 「リダイレクト」と呼ばれる

リダイレクションの例


lec8-out > abc.txt
progの標準出力をすべてファイルabc.txtに出力
 標準エラーは画面に出力される
 多量のデータを出力するプログラムのデバッグに便利

lec8-out >& abc.txt
progの標準出力・エラー共にファイルabc.txtに出力
 多量のデータを出力するプログラムのデバッグに便利

lec8-in < def.txt


progの入力をファイルから入力

lec8-in <abc.def >def.txt


abc.defの内容を標準入力とし標準出力をdef.txtとする

リダイレクトの例#include <stdio.h>main(){
int i;
for(i=0;i<100;i++){fprintf( stdout, "STDOUT %d¥n", i);fprintf( stderr, "STDERR %d¥n", i);}}taka{mori}20: cat lec7-in.c#include <stdio.h>main(){
int i;
char c;while((c=fgetc(stdin))!=EOF){
fprintf(stdout,"%c",c);}}taka{mori}21: cat test.txt
fsdf
fsdfdsfsdf
fsdtaka{mori}24: ./a.out < test.txt
fsdf
fsdfdsfsdf
fsdtaka{mori}25:

パイプあるプログラムの標準出力を別のプログラムの標準出力とすることが可能 例lec8-pipe1 | lec8-pipe2

cat myprog.c | morelec8-pipe1の標準出力をlec8-pipe2の標準入力として用いる
myprog.cの内容を画面に書き出し、１ページ毎表示

tail -1000 abc.txt | grep dictionary | more

abc.txtの終わり1000行分の中で'dictionary'が含まれる行を見つけ、1ページ毎表示gdbとは1 (centos: man gdbより)


GDB をはじめとするデバッガは、プログラムが実行中もしくはクラッシュした時にそのプログラムの ‘‘内部’’ で何が行なわれているか/行われていたかを調べるのに使用されます。

gdbとは2 (centos: man gdbより)
GDB は、4 つの機能 (加えてこれらをサポートする機能) によって実行中にバグを見つけることを手助けします。
プログラムの動作を詳細に指定してプログラムを実行させる。
 指定した条件でプログラムを停止させる。
 プログラムが止まった時に、何が起こったか調べる。
 バグによる副作用を修正し、別のバグを調べるためプログラムの状態を 変更する。
Segmentation faultが発生するプログラム lec8-seg1.c#include <stdio.h>
static int func1();
static int *myAlloc();main(){
int val;val = func1();
printf("val = %d¥n",val);}
static int func1(){
int retVal;
int *retPtr;
retPtr = myAlloc();*retPtr = 10;
retVal = *retPtr;
return retVal;}

static int *myAlloc(){
return 0;}

myAllocでint型変数を格納するメ
モリ領域を確保するはずなのに，確保せずに0ポインタを返しているデバッガの起動とコマンド (1/2)


起動方法




gdb 実行ファイル名 (coreファイル)

コマンド
run




backtrace

現在のフレームを表示

frame n

関数フレームを１(n)段up

frame




変数名の内容を表示

up (n)


行番号のソースファイルを表示

print 変数名

現在止まっている位置にどのように到達したかを示す

list 行番号


実行ファイルを実行

quit


n番目のフレームを選択
gdbを終了デバッガの起動とコマンド (2/2)
コマンド

break 行番号

delete (or clear) ブレークポイント番号

次の行を実行

continue

ブレークポイントの解除

next (or step)

breakポイントの設定

次のブレークポイントまで実行

examine 変数
変数のアドレスで示されるメモリ内容を表示
 x/12 buf
x/12b など

list表示と変数表示


デバッグ情報を含めてコンパイル
gcc -g lec8-seg1.c
 -gオプションをつけることで実行ファイルにデバッグ情報が含まれる

異常終了した部分のリストを表示可能
 任意の変数の値を調査可能

ssh.ice.nuie.nagoya-u.ac.jp{mori}50: ./lec8-seg1
Segmentation fault
ssh.ice.nuie.nagoya-u.ac.jp{mori}51: gdb lec8-seg1
途中略(gdb) runStarting program: /home0/mori/lec8-seg1
Program received signal SIGSEGV, Segmentation fault.
0x080483ea in func1 () at lec8-seg1.c:17
17*retPtr = 10;(gdb) list 17
12
static int func1()13{14int retVal;
15
int *retPtr;
16
retPtr = myAlloc();17*retPtr = 10;18
retVal = *retPtr;
19
return retVal;
20}
21(gdb) print retPtr$1 = (int(gdb)Segmentation faultを発生するプログラム lec8-seg2.c#include <stdio.h>
static char * func1();
static char *myAlloc();main(){
char *retPtr;
retPtr = func1();
printf("buffer %s¥n",retPtr);}
staticchar *func1(){
char *retPtr;
retPtr = myAlloc();
sprintf(retPtr,"%s","ABC");
return retPtr;}

staticchar *myAlloc(){
char *retPtr;
retPtr = 0;
return retPtr;}

myAllocで文字列を格納するメモ
リ領域を確保するはずなのに，確保せずに0ポインタを返しているgdbによるbacktracessh.ice.nuie.nagoya-u.ac.jp{mori}54: gcc -g lec8-seg2.c -o lec8-seg2
ssh.ice.nuie.nagoya-u.ac.jp{mori}55: gdb ./lec8-seg2
..
..
途中略
..
(gdb) runStarting program: /home0/mori/lec8-seg2
Program received signal SIGSEGV, Segmentation fault.
0x080483ea in func1 () at lec8-seg2.c:17
17
sprintf(retPtr,"%s","ABC");(gdb) bt
#0 0x080483ea in func1 () at lec8-seg2.c:17
backtraceで関数の呼び#1 0x080483ba in main () at lec8-seg2.c:8
出し状況をチェック(gdb)
フレーム番号

１７行目でエラーが発生