C言語の復習

ここでは,C/C++言語を用いた実習に先立ち,重要な復習ポイントを列挙する.
本実習を通してプログラミングへの理解をより深めることができるが,その準備として前提となる基礎事項について必ず復習をしておくこと.

実習課題はページの最後にある.

目次

配列,二次元配列

C言語において,配列とは要素が連続的に並ぶデータ形式であり,「一次元配列」のことを指す.
一次元データの代表的な例としては,温度や電圧などの時系列データが挙げられる.

これに対して,「画像」データや、「データベース」などは,二次元的にデータが並ぶ形式である.
例えば下に示すような成績表は,出席番号と科目という 2つの要素(行,列)から成り立っている.

出席番号 語学点数 力学点数
1 65 71
2 84 83
3 74 74
4 90 90
5 87 78
6 93 95

この表に示す様なデータをプログラム中で効率よく扱うためには,二次元配列が用いられる.

int seiseki[6][2]; 二次元配列の宣言
(行6人分,列2科目分の記憶場所がメモリー上に確保される)
int seiseki[6][2] = {
{65, 71},
{84, 83},
{74, 74},
{90, 90},
{87, 78},
{93, 95}
};
宣言と同時に初期化
(括弧の対応,カンマの区切り方に注意)
seiseki[0][1] = 0; 1つの配列要素に代入.
出席番号 1 番の力学の点数を 0 にする.
printf("%d\n", seiseki[1][0]); 1つの配列要素を参照.
出席番号 2 番の語学の点数が画面に表示される.

二次元配列の宣言,使用法は基本的には一次元配列と同じく

  1. 配列の添え字はそれぞれ 0 から始まる.
  2. 添え字は(配列の要素数 - 1)まで使用可能.seiseki[0][0] から seiseki[5][1] まで参照,代入が可能.
  3. 配列名の命名規則は予約語を除く a-z, A-Z, 0-9, _ の組み合わせ.一般の変数と同じ.

である.



構造体

同じ型のデータをまとめて扱う場合は配列を用いるが,以下のように文字列型や整数型が混在するようなデータ構造ある場合は,構造体を用いる.

出席番号  名前   語学   力学   平均点
1 佐藤 65 71
2 鈴木 84 83
3 高橋 74 74
4 田中 90 90
5 渡辺 87 78
6 伊藤 93 95

構造体は,新しい変数の「型」を自分で作り出す役割をする.

構造体の定義

構造体を利用するには,まず既存の変数を組み合わせて,新しい「型」の構造体変数を宣言する.

/* 構造体の定義 */
struct seiseki {
    char simei[25];
    int ten_1;
    int ten_2;
};

構造体の中身の変数(ここではsimeiten_1など)の要素を,構造体のメンバと呼ぶ.
これで,新しい変数の「型」であるseiseki型が定義できた.

定義した構造体型の変数を宣言し利用する

定義した構造体型の変数を「使いますよ」という宣言する.
(構造体に限らず,C言語では,変数の使用には必ず宣言が必要である)

struct seiseki list;    /*  seiseki 型の変数,list */

これで,成績データを格納する変数 list が,使えるようになる.

代入

次に,構造体のメンバに値を代入する方法と,値を参照する方法をマスターしよう.
基本的には,構造体変数名.メンバ変数名 のように,構造体変数の名前と,メンバの変数の名前を「.」(ドット)で結ぶ.

変数の名前がlistで,メンバ名がsimei, ten_1, ...なので,

strcpy(list.simei, "Meiji"); /* 文字列の代入.下の注を参照!       */

list.ten_1 = 90;
list.ten_2 = 80;

また,変数の定義と同時に初期化するときは,配列の場合と同様に,

struct seiseki list = {"メイジ", 90,  80};

と,変数宣言順に初期値を並べて書くことができる.

次に,構造体変数の各メンバの参照.

printf("name = %s 点数1 = %d \n", list.simei, list.ten_1);

とすることで,値を取り出すことができる.

注:文字列の代入

文字列は,「文字型」の「配列」なので,

char name[25]; 
name = "meiji"

これは,正しくない.では,どのように文字列に代入するかというと,

char name[20];

name[0] = 'm';
name[1] = 'e';
name[2] = 'i';
name[3] = 'j';
name[4] = 'i';
name[5] = '\0';   /* 文字列は最後にヌル文字が必要! */

が,配列の扱い方として正しい手順である.
しかし,文字数が増えるとこれは大変である.

そこで,C言語では文字列の代入を簡単に行うライブラリ関数が用意されている.
(厳密に言えば,「ライブラリ関数」はC言語の文法規格とは独立のものである.)

#include <stdio.h>
#include <string.h> /* 文字列操作関数用のヘッダファイル  */ 

int main()
{
    char name[20];
    strcpy(name,  "meiji");  /* string copy関数 */
    return 0;
}



ポインタ変数

アドレスとは?

プログラム中の変数や配列などの値は,コンピュータのメインメモリ(Random Access Memory,RAM,ラムと読む)上に記憶されている.
このメモリには,場所を表す「アドレス」という連続した通し番号(符号なし整数)がついており,変数ごとのアドレスがOS(Operating System, ここではWindows)により管理されている.

たとえば,C言語プログラム中で int a; と整数の変数を1つ定義すると,整数の値1個を格納する場所が sizeof(int) = 4バイト分確保され,a という名前を使ってこの場所に値を書き込んだり参照したりすることができる.
下の図では,int型の変数a が,4 バイト分のメモリを割り当てられていることを示す.(箱1つを1バイトとする.)
機械語ではこの「アドレス」を直接操作してプログラミングするが,このような数値(アドレス)をいちいち人間が管理するのは大変なので,C言語ではわかりやすい名前(=変数名)により,変数の値を操作することができる。

では,ある変数に割り当てられた場所(メモリのアドレス,上の図では0x0000E008)を調べるには,どうしたらよいか.
また,アドレスを知ることができるという事にどのような利点があるのか.

C言語では,変数の名前の直前に「&」演算子を付けると,変数のアドレスを取り出すことができる.

#include <stdio.h>
 
int main(void)
{
    int a = 10;
    
    printf("aの値は,       %d です\n",  a); /*  値を表示  */
    printf("aのアドレスは,0x%p です\n", &a); /*  %p はアドレスを16進数で表示する */
    
    return 0;
} 


ポインタ変数

前述のとおり,すべての変数は「メモリー中のどこに存在するか」を「アドレス」により管理されている.
C言語では,この変数の置かれているメモリの位置(=アドレス値)を格納するための変数が用意されており,これをポインタ変数と呼ぶ.

「アドレス」自身は符合なし整数で,32bitOSでは,0 から 232 までの値,64bitOSでは,0 から 264 までの値である. 32bitの場合,16進数で表すと,0x00000000から,0xFFFFFFFFまでである.(1文字が0?Fで,4ビット分.)

復習:

「ポインタ変数」と「(unsigned intなど)普通の符号なし整数型」は,整数値を格納する点では同じであるが,その数値の解釈に差異がある

ポインタ変数の基本的な書式

int a=10, b;

これは普通の整数型変数a,bの宣言.

int *p;

ポインタ変数pの宣言.ポインタの指す相手の型を指定し,変数名の前に(アスタリスク)をつける.

p = &a;

アドレスを格納するポインタ変数pに,int型変数aアドレスを代入.
aのアドレスを取り出すには,変数名の前に&をつける.

b = *p;

ポインタ変数に格納されているアドレスの示す先に格納されているを取り出すには,ポインタ変数の前に*をつける.
ここではポインタ変数 p の指す変数の値を,別の変数 b に代入.

プログラム例:

#include <stdio.h>
 
int main(void)
{
    int a; /* 普通の変数の宣言 */ 
    int *p;   /* ポインタ変数の宣言 */
 
    a = 3;
    printf("aのアドレスは,0x%p です\n", &a);  
 
    p = &a;
    printf("ポインタ変数pの値(=aのアドレス)は,0x%p です\n", p);  
    printf("ポインタ変数pの指しているアドレスに格納されている値は,%d です\n", *p);  
 
    p++;
    printf("ポインタ変数を一つ増やすと,0x%p です\n", p);

    return 0;
}



配列とポインタの関係(重要!)

C言語において,配列はメモリ中の連続した区間に確保されることが保証されている.
このことから,「ポインタに加算・減算」,「ポインタ同士の引き算」をすることで,配列の値にアクセスできる.
また,C言語では処理効率を優先して関数に配列を渡す際は,配列のコピーではなく,配列の先頭アドレスが渡される決まりになっている.

メモリー中での配列のイメージ

例:

#include <stdio.h>
 
int main()
{
    int a[5] = {10, 20, 30, 40, 50};   /* 配列 */
    int b;
    int *p;   /* ポインタ変数 */
 
    p = &a[0];            /* 配列の先頭のアドレスを代入する */ 
 
    printf("配列 a の先頭のアドレスは,0x%p です\n", p);
 
    printf("はじめに:p の指している値は,%d です\n", *p);
 
    p += 2;    /* ポインタ変数に2を足すと,pは配列の先頭から2つ先を指す=a[2]のアドレス! */
 
    printf("p+=2 の後:pの指している値は,%d です\n", *p);
 
    b = *(p-1); /* ポインタの指しているアドレスのひとつ前隣の値をbに代入 */
    printf("b = *(p-1) は,%d です\n", b);  

    return 0;
}

ポインタに1を足したときに,何バイト先に進むかは,ポインタ変数を宣言する時の型指定から自動的に計算される.


いろいろな型の変数のアドレスを調べよう.

#include <stdio.h>
 
int main()
{
    int e[3];
    double f[3];
    char g[3];
 
    printf("int配列    :0x%p 0x%p 0x%p\n", &e[0], &e[1], &e[2]);
    printf("double配列 :0x%p 0x%p 0x%p\n", &f[0], &f[1], &f[2]);
    printf("char配列   :0x%p 0x%p 0x%p\n", &g[0], &g[1], &g[2]);

    return 0;
}

実行結果の例(アドレスの値はプログラム実行時にOSが決めるため,必ずしも値がこうなるとは限らない)

0xbffffab4 0xbffffab8 0xbffffabc
0xbffffac0 0xbffffac8 0xbffffad0
0xbffffad8 0xbffffad9 0xbffffada

この結果から,int, double, char, 配列などの各型がメモリ上でどれくらいの大きさかがわかる.

関数

変数のスコープ(有効範囲)

「関数には引数として様々な値を渡すことができる」ことを学んだ. なぜ関数間において変数の受け渡しが必要なのだろうか.
その理由は,変数は「宣言する場所によってその有効範囲が異なる」ためである.

一般に変数はmain()をはじめ,関数のブロックの中で宣言してきた. このように関数の中で宣言する変数のことをローカル変数という.
ローカル変数を利用できる範囲は,変数を宣言した関数の中,即ち  から  までのブロック内に限られる. この変数の有効範囲のことを変数のスコープと呼ぶ. したがって,異なる関数内においては「同じ名前」の「異なる変数」を用いる事が可能である. (つまり,関数が異なれば,同じ名前でも別の変数である.)

実際に以下のプログラムで,別の関数内の同名のローカル変数の値が変化しないことを確認してみよう.

#include <stdio.h>

int y=100; /* グローバル変数.これ以降,どこでもアクセスできる */

int func(void)
{
    int x;      /*この変数はローカル変数で, main 関数内の x とは別物*/
    x = 10;

        y=200;
    printf("x,y の値( in func() ):%d , %d\n", x, y);   /* func内のxの値の表示 */
                
    return 0;
}
                
int main()
{
    int x;      /*  この変数はローカル変数で, func 関数内の x とは別物  */
    x = 3;
                
    printf("x, yの値( in main() ):%d , %d\n", x, y);  /*  main内の x,y の値の表示  */
                
    func();    /* 関数呼び出し */
                
    printf("x,y の値( in main() ) 関数 func() 呼び出し後:%d , %d\n", x, y);  /*  func関数実行後の x,y の値の表示  */

    return 0;
}
                

同じ名前の変数であっても,関数が違えば影響しないことが実行結果からわかる. これに対して,関数のスコープの外側に宣言された変数は「グローバル変数」と呼ばれ,変数の定義位置以降のすべての関数内から参照可能である.

グローバル変数は便利ではあるが,関数が増えて処理が込み入ってくるとどの場所でどの変数が変更されるかがわかりにくくなるため,可能な限りローカル変数を使用するほうが良い.


値参照とアドレス参照

変数のスコープのため,関数に引数として値を渡さなければ,他の関数の値を用いた計算が出来ない. 引数として用いる変数には,前回の実習で学んだ通常の変数と,ポインタ変数がある.
値で渡す値参照(値渡し)と,アドレスを渡すアドレス参照(ポインタ渡し)とをうまく使い分けることで,様々な関数の作成,利用が可能となる.
(これ以外に,C++固有の機能として,「参照渡し」がある)

値参照(Call by value)

関数の呼び出しでは,関数へ変数の値がコピーされて渡される.
従って,普通の変数を関数に渡し,関数内で変更しても,関数を呼びだした側の元の変数は変化しない.

これを値参照(Call by value),値渡しと呼ぶ.

呼び出し元の関数に計算結果などの数値を戻したい場合は,return 文を用いるが,戻り値の個数は 0個,または 1個 のいずれかであった.

簡単なプログラムで確認してみよう.

#include <stdio.h>
                
int test(int a)
{
    a += 10;  /* この関数内で値を変更 */
    return a;
}

int main()
{
    int a, b;

    printf("aの値を入力:");
    scanf("%d", &a);

    printf("関数へ渡す前の a の値は,%dです\n", a);

    b = test(a);

    printf("関数から戻ると,a の値は %d , 戻り値 b の値は %d \n", a, b);

    return 0;
}

例えば a に 2 を入力すると,

結果はどのように表示されるだろうか.


アドレス参照(Call by reference)

値参照では,最大でも 1 つの値しか関数から返すことができなかった.
また,呼び出した関数内で値を変更した場合も,呼び出し元(mainなど)では値は変化しない.
これは,関数には変数の中身が複製され,渡されることを考えると,当然である.

では,上記の値参照では実現できない

を実現するにはどうしたらよいか.

関数に変数の格納されている「場所」を伝え,その「場所」のデータを関数内で操作するようにすれば,実現できる.

変数の格納されている場所,即ちアドレスを扱うためには, ポインタ変数を利用する.

ポインタを用いて,変数の場所を関数に渡したプログラムの例.

#include <stdio.h>
                
void test2(int *p)        /*  変数のアドレスを受け取る(ポインタ変数)   */
{
    *p += 10;             /*  p の指す値に変更を加える.main関数でも変化する  */

    return;  /*  戻り値は無し  */
}
                
int main()
{
    int a;

    printf("aの値を入力してください:");
    scanf("%d", &a);

    printf("関数へ渡す前の a の値は,%dです\n", a);

    test2(&a);        /* 変数aのアドレスを関数に渡す */

    printf("関数から戻ると,a の値は %d\n", a);

    return 0;
}

上の例と同様,a に 2 を入力すると,今度はどのようになったか.


配列の受け渡し

ポインタを用いて,関数に変数のアドレスを渡せることがわかった.同じ方法を使って,関数に配列を渡してみよう.

以下は,配列を受け渡しする典型的な例である.配列名=配列の先頭アドレス,であるので,ポインタで受け取ることができる.

一般に,配列を関数渡す場合は,配列の先頭アドレス+配列の要素数を同時に渡す.
(C言語では,仮引数の配列のサイズを知る方法が無い.)

#include <stdio.h>
                
void zero_init1(int a[], int n)    /*  要素数 n 個,配列 a を引数とする関数 */
{
    int i;

    for(i = 0; i < n; i++) {
        a[i] = 0;
    }

    return;
}
                
void zero_init2(int *p, int n)    /*  要素数 n 個,配列の先頭のアドレス p を引数とする関数 */
{
    int i;

    for(i = 0; i < n; i++) {
        *(p+i) = 0;     /*  p[i] = 0; という書き方も可 */
    }

    return;
}
                
int main()
{
    int i;
    int  ary[] = {10, 20, 30, 40, 50};
                
    zero_init1(ary, 5);
    zero_init2(ary, 5);
                
    for(i =0; i < 5; i++) {
        printf("ary[%d] = %d\n", i, ary[i]);
    }
    return 0;
}

このプログラム中の関数zero_initは,配列 ary の先頭から n 個の要素の値を 0 に設定する関数で,配列の初期化などの際に用いられる.
注:Cでは,配列を宣言しただけでは中身は「不定」である.
関数には,配列の先頭要素のアドレス(ary)が渡されるので,関数側ではポインタ(int *p)で受け取っている.
この例では関数に戻り値が無いが,関数でのデータ変更は,main 関数においても反映されている.

関数のまとめ