C言語の復習とデバッガの利用 - 情報処理IIレジュメ(2022-7)

1. はじめに

来週からのテストファーストプログラミングの学習に入る前に、昨年度の配列とポインタの復習を行います。また、私の授業内での表記法についても解説します。

2. 復習用のプログラム

今回は、以下に示すプログラムで配列とポインタの関係について、復習していきます。 まず、arrayTest というフォルダを作成します。

mkdir -p $HOME/Documents/arrayTest

ここに arrayTest.c を作成します。以下の内容をコピーして記載してください。

#include <stdio.h>
#define IN 5
#define DN 3
int calcIntSumAndInt(int *xp, int n, int base);
double calcDoubleSumAndDouble(double *xp, int n, double base);

int main(int argc, const char * argv[]) {
    int a = 3;
    double b = 3.14;
    int c[IN] = {3, 1, 4, 1, 5};
    double d[DN] = {3.14, 1.59, 2.65};
    int e = calcIntSumAndInt(c, IN, a);
    double f = calcDoubleSumAndDouble(d, DN, b);

    printf("e = %d\n", e);
    printf("f = %f\n", f);
    return 0;
}

int calcIntSumAndInt(int *xp, int n, int base) {
    int i;
    int sum = base;
    for (i = 0; i < n; i++) {
        sum += xp[i];
    }
    return sum;
}

double calcDoubleSumAndDouble(double *xp, int n, double base) {
    int i;
    double sum = base;
    for (i = 0; i < n; i++) {
        sum += xp[i];
    }
    return sum;
}

次に、Makefile を作成します。今回はテストがないので、TARGET のみを記載しています。最後の3つはタブが必要です。タブになっていないと、Missing separator のようなエラーになってしまいます。

# makefile for arrayTest

#### 頻繁に変更が必要なもの
# 実装のためのヘッダー(プロトタイム宣言、構造体宣言、定数定義を含む)
HEADER=

# 作成したい関数が入ったファイル
OBJECTS= 

# 関数のテストが記載されたファイル(3年前期では使わない)
TEST_OBJECTS=

# 最終実行ファイル(名前を修正したら .gitignore も修正すること)
TARGET=arrayTest

# テスト実行ファイル(名前を修正したら .gitignore も修正すること)
TEST_TARGET=

#### 以下は変更する必要がないもの

# 最終実行ファイルの実名
TARGET_EXE=$(TARGET)$(EXE)
# ターゲット実行ファイルの実名
TEST_TARGET_EXE=$(TEST_TARGET)$(EXE)
# 実装のメインファイル main 関数を含む
MAIN=$(TARGET).o
# テストのためのヘッダー(プロトタイプ宣言、3年前期では使わない)
#TEST_HEADER=$(TEST_TARGET).h
TEST_HEADER=
# テストのメインファイル main 関数を含む
TEST_MAIN=$(TEST_TARGET).o
# テストに必要なファイル
TEST_COMMON=testCommon.o
# 必要な CFLAGS
CFLAGS=-Wall -g
# 必要なライブラリ
LIBS=-lm

ifeq ($(OS),Windows_NT)
    CC=gcc
    RM=cmd.exe /C del
    EXE=.exe
    CHCP=chcp 65001
else
    RM=rm -f
    EXE=
    CHCP=
endif

exec: $(TARGET_EXE) $(PNGS) $(SVGS)
test: $(TEST_TARGET_EXE)

### ここから下の先頭の空白はタブキー で入力すること
$(TARGET_EXE): $(MAIN) $(OBJECTS) $(HEADER)
  $(CHCP)
  $(CC) -o $@ $(CFLAGS) $(MAIN) $(OBJECTS) $(LIBS)
  ./$(TARGET_EXE)
#↑この字下げ部分はタブ

$(TEST_TARGET_EXE): $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(HEADER) $(TEST_HEADER)
  $(CHCP)
  $(CC) -o $@ $(CFLAGS) $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(LIBS)
  ./${TEST_TARGET_EXE}
#↑この字下げ部分はタブ

clean:
  $(CHCP)
  $(RM) $(TARGET_EXE) $(TEST_TARGET_EXE) $(MAIN) $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON)
#↑この字下げ部分はタブ

make とすると、arrayTest.c をコンパイルして、結果が表示される。Windows の場合の結果は以下のようになる。

make
gcc -Wall -g   -c -o arrayTest.o arrayTest.c
gcc -o arrayTest.exe -Wall -g arrayTest.o  -lm
./arrayTest.exe
e = 17
f = 10.520000

3. 授業での表記方法

3.1 変数の表記方法

私の授業では変数は四角で表現します。 変数名は箱の左側に、変数の型は箱の上に記載します。

int a = 3;
double b = 3.14;

また、配列は縦に並べた箱で表現します。 配列名は一番上の箱の左に記載します。 配列の型は単一の変数の場合と同様に箱の上に記載します。

int c[IN] = {3, 1, 4, 1, 5, 9};
double d[DN] = {3.14, 1.59, 2.65};

3.2 関数の表記方法

私の授業では関数の入出力の関係を四角で表現します。 関数名は箱の上に、返り値の型は箱の右側に、引数は型の中に変数として記載します。

この図はプロトタイプ宣言と一対一に対応するので、互いに変換できるようにしておいてください(試験やレジュメでは図だけが書かれていることが多いです)。

int calcSumAndInt(int *xp, int n, int base);

3.3 関数の呼び出し

関数を呼び出す時には、呼び出し側と関数の引数の対応を意識することが必要です。 また、この時のポインタ変数と配列名との関係はどうなっているのか理解しておいてください。

int e = calcSumAndInt(c, IN, a);

ここで、配列を関数に渡す場合の流れを地主と小作人を例にしたロールプレイで説明してみます。 ここで、地主(main関数)は土地はあるが仕事はしたくありません。 そのため、小作人に収穫を依頼することにします。 一方、小作人は地主から住所(ポインタ)を紙に書いて教えてもらい,その紙を通行手形として地主の土地に入り込みます。 C 言語では変数は各関数のスコープ内のものにしかアクセスできませんが、ポインタという通行手形を使うことで、アクセスを可能とします。

配列とポインタの関係

4. デバッガによる確認

上記の仕組みをデバッガを用いて、確認してみます。 デバッガとはプログラムの動作が不明な時に、プログラムの途中で止めたり、変数を表示したりして、プログラムのバグを取り去る作業(デバッグという)をサポートするツールです。

macOS では clang というコンパイラを用いているので、lldb というデバッガを利用します。 一方、Windows では gcc というコンパイラを用いているので、gdb というデバッガを利用します。 それぞれコマンド体系が異なるので、別々に説明します。ただ、コマンド名が違うくらいなので、表の形で左側に lldb、右側に gdb のコマンドを記載することにします。

4.1 デバッガの起動とブレークポイントの設定

lldb (macOS)、gdb (Windows) 共にコマンドの後ろに実行ファイル名を記載する。

lldb (macOS) gdb(Windows)
lldb arrayTest gdb arrayTest

デバッガが起動したら、プログラムを止めたい場所にブレークポイントを設定します。 今回は、main の先頭で止めることにします。 コマンドは以下の通りです。

lldb (macOS) gdb(Windows)
breakpoint set --name main b main

ブレークポイントが設定できたら、プログラムを起動します。 デバッガのコマンドは基本的に何度もタイプすることになるので、省略形が用意されています。 どちらのデバッガでもコマンド名は「run」ですが、省略形「r」で実行できます。

lldb (macOS) gdb(Windows)
r r

macOS の場合には、「r」の後に以下のようなダイアログが表示されます。 パスワードを入れて、許可してください。 認証画面が出た場合、「r」が一度失敗する場合があります。 その時には再度「r」を実行してください。

確認画面

実行すると変数を確認できます。 ローカル変数を全て表示するには以下のコマンドを利用します。

lldb (macOS) gdb(Windows)
fr v i locals

また、個別の変数の値を表示するには、どちらも p コマンドを利用できます。 例えば変数 a の値を表示するには以下のようにします。

lldb (macOS) gdb(Windows)
p a p a

macOS の場合のここまでの画面を示しておきます。 ちゃんと main の頭の部分で止まっていることが確認できます。 実行前なので変数には全てゴミが入っています。

hkob@hMBP ~/D/arrayTest> lldb arrayTest
(lldb) target create "arrayTest"
Current executable set to '/Users/hkob/Documents/arrayTest/arrayTest' (x86_64).
(lldb) breakpoint set --name main
Breakpoint 1: where = arrayTest`main + 51 at arrayTest.c:8:9, address = 0x0000000100000db3
(lldb) r
error: process exited with status -1 (Error 1)
(lldb) r
Process 63415 launched: '/Users/hkob/Documents/arrayTest/arrayTest' (x86_64)
Process 63415 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000db3 arrayTest`main(argc=1, argv=0x00007ffeefbff688) at arrayTest.c:8:9
   5      double calcDoubleSumAndDouble(double *xp, int n, double base);
   6
   7      int main(int argc, const char * argv[]) {
-> 8         int a = 3;
   9          double b = 3.14;
   10         int c[IN] = {3, 1, 4, 1, 5};
   11         double d[DN] = {3.14, 1.59, 2.65};
Target 0: (arrayTest) stopped.
(lldb) fr v
(int) argc = 1
(const char **) argv = 0x00007ffeefbff688
(int) a = 0
(double) b = 0
(int [5]) c = ([0] = 0, [1] = 0, [2] = 0, [3] = 0, [4] = -272632200)
(double [3]) d = ([0] = 2.1219957909652723E-314, [1] = 0, [2] = 0)
(int) e = 0
(double) f = 0
(lldb) p a
(int) $0 = 0

Windows の db の場合も同様に示しておきます。 こちらも main() 関数の最初で止まっていることがわかります。

PS C:\Users\hkob> cd .\Documents\arrayTest\
PS C:\Users\hkob\Documents\arrayTest> gdb arrayTest
GNU gdb (GDB) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from arrayTest...done.
(gdb) b main
Breakpoint 1 at 0x401564: file arrayTest.c, line 8.
(gdb) run
Starting program: C:\Users\hkob\Documents\arrayTest\arrayTest.exe
[New Thread 10776.0x2a1c]
[New Thread 10776.0x2a20]

Thread 1 hit Breakpoint 1, main (argc=1, argv=0x6f14c0) at arrayTest.c:8
8           int a = 3;
(gdb) i locals
a = 0
b = 3.5967346613216072e-317
c = {0, 0, 0, 0, 8}
d = {3.9525251667299724e-323, 0, 2.075170573137237e-317}
e = 0
f = 3.9525251667299724e-323
(gdb) p a
$1 = 0

4.2 ステップ実行と変数変更の確認

1行だけ実行するには「skip」コマンドの省略形である「s」をタイプします。 その後、「p a」で変数 a が変わったことを確認します。

lldb (macOS) gdb(Windows)
s s
p a p a

同様に処理を続けていき、12行めの関数の呼び出しまで「s」を実行し、変数一覧で全ての変数に値が入っていることを確認してみます。

macOS の最後の画面なこんな感じになりました。

(lldb) s
Process 63415 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x0000000100000dff arrayTest`main(argc=1, argv=0x00007ffeefbff688) at arrayTest.c:12:37
   9           double b = 3.14;
   10          int c[IN] = {3, 1, 4, 1, 5};
   11          double d[DN] = {3.14, 1.59, 2.65};
-> 12         int e = calcIntSumAndInt(c, IN, a);
   13          double f = calcDoubleSumAndDouble(d, DN, b);
   14
   15          printf("e = %d\n", e);
Target 0: (arrayTest) stopped.
(lldb) fr v
(int) argc = 1
(const char **) argv = 0x00007ffeefbff688
(int) a = 3
(double) b = 3.1400000000000001
(int [5]) c = ([0] = 3, [1] = 1, [2] = 4, [3] = 1, [4] = 5)
(double [3]) d = ([0] = 3.1400000000000001, [1] = 1.5900000000000001, [2] = 2.6499999999999999)
(int) e = 0
(double) f = 0

Window の最後の画面はこんな感じになりました。

(gdb) s
12          int e = calcIntSumAndInt(c, IN, a);
(gdb) i locals
a = 3
b = 3.1400000000000001
c = {3, 1, 4, 1, 5}
d = {3.1400000000000001, 1.5900000000000001, 2.6499999999999999}
e = 0
f = 3.9525251667299724e-323

4.3 関数に入る

関数の呼び出しで s を実行すると関数の中に入れます。 ここで変数を表示すると、関数のスコープになるので、変数一覧が変わっていることが確認できます。 gdb では xp の値まで確認できないので p で確認してみます。 また、xp[i] のアクセス,*(xp+i) のアクセスに差がないことを確認します。

lldb (macOS) gdb(Windows)
s s
p xp p xp
p *xp p *xp
p xp[0] p xp[0]
p *(xp+1) p *(xp+1)
p xp[1] p xp[1]

macOS では以下のような感じになります。

(lldb) s
Process 63415 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x0000000100000e8e arrayTest`calcIntSumAndInt(xp=0x00007ffeefbff640, n=5, base=3) at arrayTest.c:22:15
   19
   20      int calcIntSumAndInt(int *xp, int n, int base) {
   21          int i;
-> 22         int sum = base;
   23          for (i = 0; i < n; i++) {
   24              sum += xp[i];
   25          }
Target 0: (arrayTest) stopped.
(lldb) fr v
(int *) xp = 0x00007ffeefbff640
(int) n = 5
(int) base = 3
(int) i = 1
(int) sum = 16384
(lldb) p xp
(int *) $2 = 0x00007ffeefbff640
(lldb) p *xp
(int) $3 = 3
(lldb) p xp[0]
(int) $4 = 3
(lldb) p *(xp+1)
(int) $5 = 1
(lldb) p xp[1]
(int) $6 = 1

Windows で以下のような感じになります。

(gdb) s
calcIntSumAndInt (xp=0x61fde0, n=5, base=3) at arrayTest.c:22
22          int sum = base;
(gdb) i locals
i = 0
sum = 0
(gdb) p xp
$2 = (int *) 0x61fde0
(gdb) p *xp
$3 = 3
(gdb) p xp[0]
$4 = 3
(gdb) p *(xp+1)
$5 = 1
(gdb) p xp[1]
$6 = 1

s を実行すると次の行に移ります。 次にリターンキーやエンターキーだけを押すと、一つ前のコマンドを再度実行します。 このまま継続して実施し、return の後に main の e に sum の値が戻ることを確認する。

macOS の場合の最後の実行はこんな感じになります(13行目に→がきた時で停止)。

(lldb)
Process 63415 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x0000000100000e16 arrayTest`main(argc=1, argv=0x00007ffeefbff688) at arrayTest.c:13:46
   10          int c[IN] = {3, 1, 4, 1, 5};
   11          double d[DN] = {3.14, 1.59, 2.65};
   12          int e = calcIntSumAndInt(c, IN, a);
-> 13         double f = calcDoubleSumAndDouble(d, DN, b);
   14
   15          printf("e = %d\n", e);
   16          printf("f = %f\n", f);
Target 0: (arrayTest) stopped.
(lldb) p e
(int) $7 = 17

Windows の場合にはこんな感じになります。

(gdb)
main (argc=1, argv=0x6f14c0) at arrayTest.c:13
13          double f = calcDoubleSumAndDouble(d, DN, b);
(gdb) p e
$7 = 17

4.4 関数に入らずに値を得る

関数の呼び出しで n を実行すると関数の中に入らずに実行だけをします。 ここで、main の f に値が入ることを確認します。 あとは同様にプログラムの最後まで実行してみてください。

lldb (macOS) gdb(Windows)
n n
p f p f

macOS の場合には、こんな感じになります。

(lldb) n
Process 63415 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100000e2a arrayTest`main(argc=1, argv=0x00007ffeefbff688) at arrayTest.c:15:24
   12          int e = calcIntSumAndInt(c, IN, a);
   13          double f = calcDoubleSumAndDouble(d, DN, b);
   14
-> 15         printf("e = %d\n", e);
   16          printf("f = %f\n", f);
   17          return 0;
   18      }
Target 0: (arrayTest) stopped.
(lldb) p f
(double) $8 = 10.52

Windows の場合には、こんな感じになります。

(gdb) n
15          printf("e = %d\n", e);
(gdb) p f
$8 = 10.52

5. リファクタリング

私が作るとしたら、calcSumAndInt はこんな感じで書くと思う。

int calcIntSumAndInt(int *xp, int n, int base) {
    while (n-- > 0) {
        base += *xp++;
    }
    return base;
}

このようにとりあえず動くものを作ってから,より効率的なコードに書き換えることを、リファクタリングといいます。このリファクタリングで考えたことは以下の通りです。

  • 関数(小作人)に渡された変数値は所詮コピーなので壊してもよい
  • n--は n の値を評価した後に n の値を1つ減ずる。すなわち,n > 0 が評価された後で,n の値が一つ減る。
  • xp++ も同じで,xp が計算された後で,xp が1つ増加される
  • *xp の値を常に確認できるので,デバッグしやすい時もある
  • 余計な変数を用意する必要がないのもメリット

6. テストファーストプログラミング

この授業ではテストファーストプログラミングを実施します。 それでは、なぜ先にテストをするのでしょうか? 以下のようなことが挙げられます。

  • 修正前のコードと修正後のコードが同じ出力を出すのかが心配
  • そもそも元のコードも正しいかは不安
  • 例外値の処理をやっているのか,境界部分で正しく動くのか不安
  • 正しく動くことが保証された関数は道具として使うのに最適

来週、このテストファーストの実施方法を説明します。


hkob.hatenablog.com