C exercises(d) Scope and Storage class

変数の通用範囲

自動変数のことを局所変数、外部変数のことをグローバル変数(大域変数)ともいいます。

変数のスコープの範囲を図で表してみます。

c_09_03

赤色で囲った部分がグローバル変数の有効範囲です。
青色で囲った部分がローカル変数の有効範囲です。

この図ではローカル変数の寿命を関数内と説明しましたが、正確にはブロック内です。
ブロックとは、{}で囲まれている範囲のことを指しています。

記憶クラス

Cで扱う記憶領域は一般に、プログラム領域、静的領域、スタック領域、ヒープ領域の 4つに大別されます。

記憶クラスには、4つあり、自動、静的、外部、レジスタがあります。

記憶クラス 記憶領域 スコープ 記憶クラス指定子
自動変数 スタック { } の内側 auto( 不要 )
静的変数 静的領域 { } の内側* static
外部変数 静的領域 全域 extern**
レジスタ変数 レジスタまたはスタック { } の内側 register
  • * { }の外側で宣言された時は、その行以降
  • ** 他のファイルにある時、externを付け、その変数を宣言(メモリを確保)しているファイルでは何も付けない。

自動変数は使う前に何らかの値を代入します。このことを、初期化するといいます。

静的変数は初期化の式がなくても、コンパイル時に0に初期化されます。

変数の名前

変数は名前を付けなければ、使えません。C言語では、名前(識別子)は、英字または’_’(アンダースコア)で始まり、英数字または’_’が0個以上続くという決まりになっています。’_’一つまたは二つで始まる変数は、システムに密着したデータに使います。例えば、コンパイラのプログラム等で使われます。

変数に名前を付ける時、外部変数は定義されている所から遠い所でも使われるので、その変数の用途を想像できるような名にすることが推奨されています。

    例:timetable, itemindex, thisyear

自動変数は通用範囲が狭いので、何の変数かがすぐわかるので、タイピングの手間を省く意味でも、短くて良いとされています。

    例:i, j, m, x, c

その場合でも、その変数がなんであるかを、瞬時に連想できるものが良いとされています。例えば、整数ならば、i、j、k、n、浮動小数点数ならば、x、y、z、文字ならば、cなどです。

#include <stdio.h>

void Func(void);        /* プロトタイプ宣言 */
void main(void);

/* 静的変数 i と自動変数 j の値を表示する */
void Func(void)
{                       /* i の値を読み書きできるのはこの関数の中でだけ */
                        /* i はずっと存在し続け、 */
        static int i;   /* func(  ) が呼ばれる度に1増える */

        auto int j;     /* 自動変数は初期化しないと */
                        /* ゴミの値が入っている */
                        /* j はゴミの値が表示される */
                        /* 幾つになるかはわからない */
                        /* auto は付けなくてもよい */
                        /* というより、普通は付けない */

        printf("i = %d  j = %d\n", i, j);        /* i, j の値を表示 */
        i++;        /* 値を1増やす */
        j++;        /* 値を1増やす */
}                   /* 関数を抜けたこの時点で、j は破壊される */

void main(void)
{

        Func(  );        /* 呼び出す度に i の値は 1 増える */
        Func(  );
        Func(  );
}

 演習

渡された引数の最大値を返す関数を作成してください。(教科書p299)

ヒント:

 

C exercises(c) Command line arg.

今まで、main 関数へ引数なしを意味する

int main(void) と記述、

実は main関数にも引数を渡すことができます。 この main関数に渡す引数のことを「コマンドライン引数」といいます。

コマンドライン引数

main関数へ渡せる引数は、

  1. 引数の総個数
  2. 引数の文字列を指すポインタの配列

の 2つです。

一般に

  • int main(int argc, char *argv[]) と記述し、
  • int argc: 引数の総個数(プログラム名も含む)
  • char *argv[]: 引数の文字列を指すポインタの配列を表します。

とりあえず、下の例を見てみましょう。

(例)

#include <stdio.h>

int main(int argc,char *argv[])
{
	int i;
	
	printf("引数の総個数 = %d\n", argc);
	for (i = 0; i < argc; i++) {
		printf("%d番目の引数 = %s\n", i, argv[i]);
	}
	return 0;
}

例題は、command_line.c と保存して、コンパイルだけしてください。

dir コマンドで、command.exe ファイル生成されたと確認。

2016-07-04 (1)

実行結果

Ctrl-Qでコマンドプロンプトを起動、 下記のコマンドを入れてください。

> command_line Kitty on your lap

 

E:\chen\My Documents\C>command_line Kitty on your lap
引数の総個数 = 5
0番目の引数 = E:\chen\My Documents\C\command_line.exe
1番目の引数 = Kitty
2番目の引数 = on
3番目の引数 = your
4番目の引数 = lap

E:\chen\My Documents\C>

演習

演習1

教科書 P291
コマンドラインで数値を入力し、その合計値を求めるプログラムを作成してください。

プログラムソースファイルは、a8-5-1.c とする。

実行結果

> a8-5-1 10 15 22 45 9 66 71 4 37 82
合計は 361です

演習2

http://chenlab.net/2016/06/07/c-exercises8-pointer-summary/

 

を改造し、整数は配列ではなく、コマンドライン引数として引き渡し、引数を順に調べ、奇数の値のみ、別の大きさ10の整数型配列に代入しなさい。また、配列の中身と、何個格納したかを画面表示しなさい。

実行結果

> find_odd_number 10 15 22 45 9 66 71 4 37 82
実行結果

15
45
9
71
37
格納個数 = 5

 

C exercises(b) Recursive call

東京魅力PRサークル会員募集中

http://svn.mki.biz/pukiwiki/index.php?u-tokyo

興味があれば、ぜひコメントください。

階乗の計算

皆さんは「階乗の計算」を覚えていますか? 5の階乗なら「5!」と表記し,

5! = 5 × 4 × 3 × 2 × 1

と計算します。つまり答えは120です。

0 の階乗:1
1 の階乗:1
2 の階乗:2 × 1
3 の階乗:3 × 2 × 1
– – – – – – –
n の階乗: n × (n – 1) × (n – 2) – – – 3 × 2 × 1
– – – – – – –

方法1)掛け算を続けることで階乗を求め

n! = n × (n-1) × (n-2) .. × 1

#include <stdio.h>

long Factorial1(int n) ;
long Factorial2(int n) ;
void main(void);

  /* 階乗を求める(非再帰版) */
long Factorial1(int n)
 {
    long p = 1L;

    if (n < 2)       /* nが0か1なら */
        return (1L); /* 1を返す */
    else if (n > 1) {
        for ( ; n > 0; n--)
            p = p * n;
            return (p);
    }
}	


void main(void)
{
    int n;
    long f;

    printf("整数を入力して下さい\t");
    scanf("%d", &n);

    f = Factorial1(n);
    printf("Factorial1(  ) = %ld\n", f);

}

 

方法2)関数の中から自分自身を呼び出す

再帰呼び出し (recursive call)とは,関数の中から自分自身を呼び出すプログラミングのテクニックです。再帰呼び出しで階乗の計算式を考えてみると,

n! = n × (n-1)!

と定義できることがわかるでしょうか。

#include <stdio.h>

long Factorial1(int n) ;
long Factorial2(int n) ;
void main(void);

  /* 階乗を求める(再帰版) */
long Factorial2(int n)
 {
    if (n < 2)        /* nが0か1なら */
        return (1L);  /* 1を返す */
    else              /* nに自分より1小さい自分を掛ける */
        return (n * Factorial2(n -1));
}

void main(void)
{
    int n;
    long f;

    printf("整数を入力して下さい\t");
    scanf("%d", &n);

    f = Factorial2(n);
    printf("Factorial2(  ) = %ld\n", f);
}

再帰呼び出しを使う上で、大切なポイントが2つあります。

 

 1.終着点があること

呼び出しが無限に繰り返されてはなりません。再帰とは再び帰ってくるということ。そのためには終着点が必要になります。nの階乗はn=1が終着点です。

 

 2.関数を抜けるときは元に戻す

グローバルな変数を用いて状態を把握している場合、関数を抜けるときは関数に入ったときの状態に必ず戻すようにして下さい。そうしないと探索の整合性が失われてしまいます。

 再帰は使うべきか

一般に次のことが言えます。

  • 再帰プログラムは計算機に負荷をかけるプログラムである。
  • 時によっては,膨大な負荷をかけることもある。
  • 簡単に非再帰プログラムとして書けるものは再帰プログラムを使うべきではない。

それでも,再帰プログラムが基本的であると言われるのは何故でしょうか。それは再帰プログラムが大きな力を秘めているからです。つまり

  • 再帰プログラムでは簡単に書けるが,非再帰プログラムはかなり複雑なプログラムになってしまうようなものがある。

ということです。

このような問題が意外とあるのです。再帰プログラム技法を,身につけたら,プログラミングを行う際,次のような視点で考えるのが良いかもしれません。

  • まずは,非再帰プログラムで問題を考えてみる。
  • 難しいと判断した場合,再帰プログラムで考えてみる。

再帰が有効な例

  • ハノイの塔
  • データの木構造, 入れ子構造
  • XML文書
  • ファイルシステム

演習

再帰呼び出しの方法で、整数を二進数で表示するプログラムを作成

/*
実行例

12345678
101111000110000101001110

*/

ヒント

void print_binary(unsigned int n)
{
    if (n >= 2) print_binary(n/2);
    printf("%d", n%2);
}

 

C exercises(a) Return Values

ある関数が別の関数を呼び出して、呼び出した関数に制御が戻って来た時に、関数によっては値が返ってくるものがあります。

関数から値を返すには、関数の戻り値の他に、ポインタを使って、関数から呼び出し側の領域を書き換えるも可能です。

値を関数戻り値で返す

関数の定義

戻り値の型 関数名(パラメータリスト) {
  文
  ...
}

関数の戻り値で返す。

この方法では、常に1つの情報しか返すことが出来ません。
2つ以上の情報を返したい時などは不便です。

値を引数の配列に返す

配列の先頭要素のアドレスを引数にして、上位関数側と下位関数側で配列を共有し、見かけ上、複数のデータを返したようにします。

#include <stdio.h>

void waru2(int *p);

int main(void)
{
  int i;
  int dt[] = { 20, 10, 4, 35, 66, 78, -1 };
  
  waru2(dt);		/* 配列の先頭要素のアドレスを渡す */
  
  for(i = 0; dt[i] != -1; i++) {
    printf("%d ", dt[i]);
  }
  printf("\n");
  
  return 0;
}

void waru2(int *p)		/* 配列dt のアドレスをポインタp に入れる */
{
  while(*p != -1) {
    *p = *p / 2;	/* ポインタの中身を 2 で割る */
    ++p;
  }
}

 

値を引数の変数に返す

複数の変数のアドレスを引数にして、上位関数側と下位関数側でデータを共有し、見かけ上、複数のデータを返したようにします。

#include <stdio.h>

void swap(int *x, int *y);

int main(void)
{
  int a = 123, b = 456;
  
  printf("呼出し前a = %d b = %d\n", a, b);
  swap(&a,&b);		/* 変数a と変数b のアドレスを渡す */
  printf("呼出し後a = %d b = %d\n", a, b);
  
  return 0;
}

/* 変数a のアドレスをポインタx に、変数b のアドレスをポインタy に入れる */
void swap(int *x, int *y)
{
  int wk;
  
  wk = *x;
  *x = *y;		/* ポインタを使って中身を入換える。*/
  *y = wk;
}

 

引数のconst型修飾子

ポインタを使って、関数から呼び出し側の領域を書き換えるも可能です。しかし、書き換えて困る場合もある。

関数の引数を const として宣言すると、その関数が引数の値を変更しないことを約束するということを意味する。

関数の引数が const 修飾されている場合、ポインタによって参照される値を変更しようとするとコンパイラが致命的エラーを出す。

void foo(const int *x) {
  if (x != NULL) {
    *x = 3; /* コンパイルエラーを出す */
  }
  /* ... */
}

 

演習

ミニクイズ

http://lmspress.net/ (登録は : 学籍番号 / 学校e-mailアドレス)

関数の作成演習

ABクラス:a-8-4-1-1 (p283) 文字列を大文字に変換する関数を作成してください。

CDクラス:a-8-4-2-2 (p287) 三つの整数の中身を入れ替える関数を作成してください。

 

C exercises(9) Basics of Function

関数の基本

C言語において関数を使うことには、以下のような利点があります。

関数化しておくとそれを1つのブラックボックスとして扱うことができます。 つまり、使用する側が知っておくべきことは、以下の通りです。

  • 入力として、どのような引数を与えるか
  • 出力として、関数がどんな値を返すのか
  • 関数を使ったことによって、どんな副作用があるか
  • 一度、関数として作成しておけば、後で何度でも再利用することができる
  • 関数として各機能をまとめていくと、一般にプログラムが見やすく、デバッグの作業もしやすい

関数の呼び出し

#include <stdio.h>

void func();   //関数のプロトタイプ宣言

int main()
{
 func();    //関数呼び出し
 printf(" ここがメイン関数内\n");
 return 0;
}

void func()    //自作関数
{
 printf("ここは自作関数内\n");
 return;    //「return;」は省略可
}

 

関数定義

関数の定義

戻り値の型 関数名(パラメータリスト) {
  文
  ...
}

関数プロトタイプ宣言

戻り値型 関数名(パラメータリスト);

戻り値の型、関数名、引数のルールは関数を作る際とほとんど同じです。但し、プロトタイプ宣言する行の最後には「;」が必要。

Void Type関数

void型は、今までの数値型や文字型とは異なり、戻り値が無い関数のデータ型のことです。処理の結果を戻す必要が無い場合は、わざわざint型にして「return 0;」としなくても、void型で関数を定義すれば、最後にreturn文を書く必要はありません。

 

  1. 値を返さない return型に用いる
  2. 引数が無い場合に用いる

関数へ値を渡す

C言語では、関数へ情報を渡す場合、必ず元の変数の値のコピーを渡します。
この様な方法を値渡しと呼び、元の変数の値が変更されないことが特徴です。

「値渡し」, ex : printf()

#include <stdio.h>
 
void plus1(int x)
{
    x = x + 1;
}
 
int main(int argc, const char * argv[])
{
     
    // insert code here...
    int a;
     
    a = 1;
    plus1(a);
     
    printf("a = %d\n", a);
     
    return 0;
}

 

アドレス

「参照渡し」, ex : scanf()

ポインタ型の引数であっても、値のコピーが渡される原則に違いはありません。

それでもポインタ型を使うのは、ポインタ型はアドレスを受け取ることが出来るからです。

#include <stdio.h>
 
void plus1(int *x)
{
    *x = *x + 1;
}
 
int main(int argc, const char * argv[])
{
     
    // insert code here...
    int a;
     
    a = 1;
    plus1(&a);
     
    printf("a = %d\n", a);
     
    return 0;
}

配列

関数に配列を丸ごと渡すことができません。あたかも配列を丸ごと渡すように書くことができます。

#include <stdio.h>

void func(int [10]);

int main(void){
  
  int figure[10]={1,2,3,4,5,6,7,8,9,10};

  func(figure);

  return 0;

}

void func(int temp[10]){

  for(int i=0;i<10;++i){
    printf("%d\n",temp[i]);
  }
}

void func2(int temp[]){

  for(int i=0;i<10;++i){
    printf("%d\n",temp[i]);
  }
}

void func3(int *temp){

  for(int i=0;i<10;++i){
    printf("%d\n",*(temp+i));
  }
}

func, func2, func3は全く同じ機能する。

配列を丸ごと渡すように書くだが、実質参照渡しなので、下記のおかしな現象が発生する。

  1. 配列の要素数は無視される
  2. 関数内で配列の値を変えると呼び出し側まで変化する

演習

ABクラス

a-8-1-2 (p266)

曜日を求まる関数を作成してください。

CDクラス

a-8-3-2-1 (p276)

平均値を求まる関数を作成してください。

C exercises(8) Pointer Summary

ポインタのまとめ

  1. アドレス
  2. ポインタと使用手順
  3. ポインタ配列

過去のページを参照

演習

ポインタを使ったプログラムをグループで作成する。

7-3-4-3(P245)

大きさ10の整数型配列を用意し、下図のように初期設定しなさい。

この配列を順に調べ、奇数の値のみ、別の大きさ10の整数型配列に代入しなさい。

また、配列の中身と、何個格納したかを画面表示しなさい。

zu10-12

実行結果

15
45
9
71
37
格納個数 = 5

ヒント:

  1. 大きさ10の整数型配列を用意し、初期設定する。
  2. 別の大きさ10の整数型配列を用意。
  3. この配列を順に調べ、奇数の値のみ、別の大きさ10の整数型配列に代入。
  4. 配列の中身と、何個格納したかを画面表示。
#include <stdio.h>

int main( void )
{
  int data1[10] = { 10, 15, 22, 45, 9, 66, 71, 4, 37, 82 };
  int data2[10], i, cnt;
  int *p1, *p2;

  cnt = 0;
  p1 = data1;          /* 配列data1のアドレスをp1に設定 */
  p2 = data2;          /* 配列data2のアドレスをp2に設定 */
  for ( i = 0; i < 10; i++ ) {
    if ( ( ( *p1 )%2 ) == 1 ) { /* p1の指す内容が奇数なら */
      ;                /* p2の指す中身に代入 */
      ;
      ;                /* ポインタp2の更新 */
      ;
    }
    p1++;	/* ポインタp1の更新 */
  }
  printf( "格納個数 = %d\n", cnt );

  return 0;
}

 

C exercises(7) Pointer Arrays

ポインタの配列

複数の文字列をchar型の2次元配列で宣言しました。それを下記に示します。

char kw[3][7] = {"double", "extern", "switch"};

同様なものをポインタを使って宣言すると、下記のようにポインタの配列となります。

const static char *wday[  ] = {
    "Sunday", 
    "Monday", 
    "Tuesday",
    "Wednesday", 
    "Thursday",
    "Friday", 
    "Saturday", 
    NULL
};

[ ]の中には8が入るのですが、コンパイラが数えてくれるので省略しています。

wday[0]は最初は”Sunday”の先頭アドレスを指します。
wday[1]は最初は”Monday”の先頭アドレスを指します。
wday[0]++とすると、
wday[0]は”Sunday”の1番目の要素を指します。
wday[1]++とすると、
wday[1]は”Monday の2番目の要素を指します。

以下、同様です。

配列の最後の要素は、’NULL’です。このようにしてあるのは、ポインタの配列が幾つあるか計るためです。

‘NULL’を使わない場合は、ポインタの配列の数を保持している変数が必要になります。

#include <stdio.h>

  /* ポインタwday[  ]の指している文字列の配列は、
     const:書き変え禁止で
     static:他のファイルから参照禁止 */

const static char *wday[  ] = {
    "Sunday", 
    "Monday", 
    "Tuesday",
    "Wednesday", 
    "Thursday",
    "Friday", 
    "Saturday", 
    NULL
};

void MyPrint(const char **p);  /* 引数はポインタを指すポインタ */
void main(void);

void MyPrint(const char **p)
{
    while (*p) {               /* pの指すポインタがNULLでない間 */
        printf("%s\n", *p);    /* pの指す中身を表示 */
        p++;
    }
    printf("\n");
}

void main(void)
{
    const char **p;      /* ポインタを指すポインタ */

    p = wday;            /* ポインタの配列の先頭を指すようにする */
    MyPrint(p);

    while (*wday[0] != '\0' )        /* 指す中身がNULLでない間 */
        printf("%c ", *wday[0]++ );  /* 表示する */
    printf("\n");
}

出力結果

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

S u n d a y

演習

A7-4-2 (P253)

疑似乱数を発生させ、「誰がいつとこで何をした」と表示してください。

(Who) が (When)  (Where) で (What) をした。

ヒント

  printf("%s が %s %s で %s をした", 
    pwho[rand() % 3], 
    pwhen[rand() % 3],
    pwhere[rand() % 2],
    pwhat[rand() % 2]
  );

出力結果

田中が夜家で勉強をした。

 

C exercises(6) Pointers and Variables

ポインタの仕組み:ポインタで変数を指す

ポインタとは

ポインタ (pointer)とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照するものである。有名な例としてはC/C++でのメモリアドレスを表すポインタが挙げられる。(ja.wikipedia.org)

ポインタ=メモリアドレス

間接演算子 * と アドレス演算子 &

ポインタの使用手順

  1. 宣言
  2. アドレスの設定
  3. 使用

ポインタで変数

間接演算子を使って、ポインタが指すメモリの値を取得することを間接参照するといいます。

間接演算子を用いれば、アドレスを間接参照するだけではなく、ポインタが表すアドレスに値を間接代入することもできます。

#include <stdio.h>

int main() {
  int iVar = 0;
  int *iPo = &iVar;   // 宣言&設定

  printf("*iPo = %d\n" , *iPo);  // 使用

  *iPo = 100; // 間接代入
  printf("iVar = %d\n" , iVar);
  return 0;
}

 

ポインタで配列

配列とポインタは全く別物

配列とは、多数の変数を順番つけでまとめて扱う方法であり、
ポインタとは、変数のショートカットを作る方法です。

文字列を1文字ずつ表示するプログラム

/* 文字列を1文字ずつ表示するプログラム */
#include <stdio.h>

int main(void)
{
  char str[] = "sun";
  char *p;  // 宣言

  p = str;  // 設定
  
  while (*p != '\0') {
    printf("%c ", *p);
    p++;
  }

  return 0;
}

 

演習(P245-2)

str1 に、’ABCDEFGHIJKLMNOPQRSTUVWXYZ’と初期化
str2 に文字列を逆順に格納

コード: a7-2-3.c

実行結果例: a7-2-3.exe

str1 = ABCDEFGHIJKLMNOPQRSTUVWXYZ
str2 = ZYXWVUTSRQPONMLKJIHGFEDCBA