ポインタ・配列・構造体

はじめに

前回はマクロと関数の勉強をしました。とくに関数の抽象と具体の部分は重要になります。よく読み返しておいてください。

ポインタ

この章は練習問題が細かく作られています。問題文を読んですぐに何をすればいいかわかった人は飛ばしてしまって構いません。確認のためのものなので。

ポインタとはなんなのか

今回はポインタというものを学んで行きます。ポインタは、C言語初心者がつまづきやすく、難しい分野だと言われています。といっても、「とても難しいものを学ぶんだ」と身構えてしまうとかえって疲れてしまいますから、最初は簡単な話から始めていきましょう。

みなさんが使っているコンピュータには、メモリと言われるものが載っています。「CPUはCore i3、メモリは4GB」とか言われるときのメモリです。ノートパソコンなら最近のものなら4GBから16GB、スマートフォンなら2GBとかぐらいあるのではないでしょうか。ノートパソコンのメモリは多ければ多いほど快適なので、追加できる余裕がある人は追加しておくと幸せになります。

さて、今までプログラム上で使ってきた変数について考えてみましょう。あれってどこに保存されているんでしょうか?答えは「メモリ」です。メモリの上に変数が保存されて、それをCPUが取りに行ったりしてくれます1

ここで、メモリがどんなふうになっているかを見てみましょう。こんなプログラムを考えます2

#include <stdio.h>

int main(void){
    char x = 2500,y = 3204,z = 0,w = 600;
    return 0;
}

そのとき、メモリはこんな感じになります。

メモリ図

メモリ図

メモリは現実の世界と同じで、住所があり、その中に値が(現実で言えば実際の建物が)あります。CPUはそのアドレスを使って値を得て、実際に演算などをすることが可能なわけですね。今回はxの値がアドレス100の場所に、yの値がアドレス101に、zの値がアドレス102に、wの値がアドレス103に入っています3

では、この変数のアドレスをプログラム上で取得するためにはどうすればいいのでしょうか。

&演算子を使うと、その変数のアドレスを取得する事ができます。&演算子をアドレス演算子と言ったりします。scanf関数のとき使いましたね。

int a;
&a; // => aの値が格納されているアドレスを得る
#include <stdio.h>

int main(void){
    char x = 2500,y = 3204,z = 0,w = 600;
    // アドレスを表示する際、printfでは%pを利用する
    printf("%p",&x);  // => xのアドレスが表示される
    return 0;
}

ある変数に-をつけるとその変数の値×-1になるのと同じように、ある変数に&をつけるとその変数のアドレスになるわけです。

このとき&aの型はint * 型となり、「int型へのポイント型」と読みます。また、&aの値を一般に「aへのポインタ」といいます。「変数aのポインタ」と言われたら、「変数aのアドレス」のことを指していると考えてください。この講習資料では、ここから先では&aを「aのポインタ」と呼ぶことにします。

int y;
&y; // => 「yへのポインタ」、「yのポインタ」と表現する

練習問題

ポインタ型

#include <stdio.h>

int main(void){
    int a;
    &a;
    return 0;
}

さきほどの文章の中で、&aはint型へのポインタ型であるというお話をしました。実は、C言語ではポインタ型の変数を宣言することができるのです。t型へのポインタ型のxという変数を宣言する場合、次のように記述します。

int * x; //int型へのポインタ型の変数 x
double * y; //double 型へのポインタ型の変数 y

ちなみにポインタ型の変数を複数個宣言する場合は、それぞれに*をつける必要があります。間違えやすいので注意してください。

int *a,*b; //int型へのポイント型の変数a,b
int *c,d; //int型へのポイント型の変数c,int型の変数d

ポインタ型の変数はこんなふうに使うことができます。

#include <stdio.h>

int main (void) {
  int x;
  double y;
  int *px;
  double *py;
  px = &x;
  py = &y;
  printf ("The address of x is %p.\n", px);
  printf ("The address of y is %p.\n", py);
  return 0;
}

ポインタ型の変数だから、&演算子によって得られたポインタ型の値を代入することができるわけですね。

// ある変数からそのポインタを取得することは`&`演算子を使って可能
int a;
printf("%p\n",&a);

ある変数から、その変数のポインタを取得するためには&演算子を使えばいいということがわかりました。その反対として、あるアドレスからそのアドレスに格納されている値を取得することも可能です。

int a = 0;
int * p;
p = &a;
// ここでアドレス(ポインタ)pに格納されている値を取ることができる
printf("value of %p is %d",p,*p);

演算子*を利用すると、そのアドレスに格納されている値を取得する事ができます。あるアドレスが格納された変数xに対し、そのアドレスの値を取得するためには次のように記述します。

* x;

このプログラムを実行すると、「the value of (なにかアドレス) = 0」と表示されるはずです。pにはaのアドレスが格納されていますから、そのアドレスに対して*をつけると、その中の値0が取得されるわけです。

練習問題

ポインタから値を変更する

アドレスから元の値を変更することもできます。

#include <stdio.h>

int main(void){
    int a = 0;
    int * p;
    p = &a;
    *p = 5;
    printf ("the value of a = %d",a);
    return 0;
}

このプログラムを実行すると、「the value of a = 5」と表示されます。pは現在aのアドレスが格納されているわけですから、*pに新たな数字を代入するということは、アドレスpに格納されている値を変えるということになります。アドレスを使えば変数の値を書き換えることが可能ということです

ポインタを使って値を直接いじる

ポインタを使って値を直接いじる

練習問題

関数でのポインタの利用

ここで、皆さんにscanfのことを思い出してもらいましょう。scanfで値を代入したい変数を渡すときに&演算子をつけて渡していた理由がわかりましたか?

関数の中で、呼び出し元に渡すことができるのは返り値だけです。

#include <stdio.h>

int function(int a,int b){
    return a+b;
}
int main(void){
    int a = 0,b = 3,result;
    result = function(a,b);
    printf("result is %d",result);
    return 0;
}

関数の中で複数の値を返すことはできません。しかし、複数の値を同時に変更したいという場合はあります。そんなときポインタを利用すれば、引数を通じて複数の値を変更することが可能です。

#include <stdio.h>
void add_2_to_all_value(int *a,int *b){
    *a += 2;
    *b += 2;
    return;
}
int main(void){
    int a = 0;
    int b = 3;
    add_2_to_all_value(&a,&b);
    printf("%d,%d",a,b);
    return 0;
}

上のプログラムを実行すると、「2,5」と表示されます。add_2_to_all_value関数の中で、ポインタによって渡された値にそれぞれ2を足しているからです。返り値はないので関数の型はvoidとなっています。

ポインタを使うと複数の値をいじれるだけでなく,場合によって違う型を返す必要がある処理も書くことができます。scanfなどは最初に渡すフォーマット指定子によって値が変わるので、普通の返り値を返すタイプの関数としては実装できないのです。これも、ポインタを使ってあげると実装することが出来ます。今までscanfで値を代入する変数に&aなどと&をつけていたのは、ポインタを渡していたからですね4

練習問題

配列

次に、配列というものを学んでいきます。配列というのは複数のデータを処理するときに便利なものです。

ユーザーに3つの値を入力してもらい、その平均値を出すプログラムを考えてみましょう。

#include <stdio.h>

int main(void){
    int num1,num2,num3
    printf("3つの値を入力してください\n");
    printf("1つ目:");
    scanf("%d",&num1);
    printf("2つ目:");
    scanf("%d",&num2);
    printf("3つ目:");
    scanf("%d",&num3);
    printf("3つの値の平均値は%fです。\n",(num1+num2+num3)/3);
    return 0;
}

ちょっと煩わしいですね。この処理は配列を使うと次のようになります。

#include <stdio.h>
int main(void){
    int nums[3];
    int sum = 0;
    printf("3つの値を入力してください\n");
    for(int i = 0;i<3;i++){
        printf("%dつめ:",i+1);
        scanf("%d",&nums[i]);
    }
    printf("3つの値の平均値は%fです。\n",(nums[0] + nums[1] + nums[2])/3);
    return 0;
}

配列は、複数のデータをまとめて扱うときに使うものです。例えば、クラスメイトの身長・体重の平均を取るとか。コンピュータは大量のデータを処理することに長けています。実際に大量のデータを扱ったプログラムを記述したい場合は、配列を使って記述していきます。

#include <stdio.h>
#include <math.h>
#define N 8

int main(void){
    int data[N] = {10,20,30,40,50,60,70,80};
    int sum=0,sum2=0,i;
    double ave,dev;
    for(i=0;i<N;i++){
        sum += data[i];
        sum2 += data[i] * data[i];
    }
    ave = sum / N;
    dev = sqrt(sum2/N - ave*ave);
    printf("平均:%f,標準偏差:%f\n",ave,dev);
    return;
}

標準偏差と平均を求めるプログラムです5。高校時代にやりましたね。覚えていますか?これを例えば8個の変数で実現してみるとどんな感じになるのでしょうか?

#include <stdio.h>
#include <math.h>
#define N 8

int main(void){
    int data1 = 10,data2 = 20,data3 = 30,data4 = 40,data5 = 50,data6 = 60,data7 = 70,data8 = 80;
    int sum = 0;sum2 = 0;i;
    double ave,dev;
    sum = data1 + data2 + data3 + data4 + data5 + data6 + data7 + data8;
    sum2 = (data1*data1) + (data2*data2) + (data3*data3) + (data4*data4) + (data5*data5) + (data6*data6) + (data7*data7) + (data8*data8);
    ave = sum / N;
    dev = sqrt(sum2/N - ave*ave);
    printf("平均:%f,標準偏差:%f\n",ave,dev);
    return;
}

これはひどい。ひどいですね。読みたくもない。上の例から分かる通り、配列を使うとデータを統一的に処理することが簡単になるわけです。

型名 変数名[配列個数];

配列の基本的な宣言方法です。ただ、配列は様々な表現方法があります。基本形を覚えておけば問題ないですが、全く知らないと他の人が記述したコードを読む際に困ることがあるので一応理解はしておいてください。

int int_data[100]; // 100個分のint型の配列
double double_data[100]; // 100個分のdoublle型の配列
int small_data[3] = {1,2,3};  // 値の指定も可能
int nankano_data[] = {1,2,3}; // 値の指定をする場合は要素数を省略することも可能
int zero[100] = {0}; // 要素数を指定し、それより小さい場合しか初期化しなかった場合、他の要素は0で埋められる

ちょっといろいろあってわかりづらいですね。覚えきれないな、と感じるであれば基本形だけ覚えて使ってください。他のものが出てきたら、調べなおせばよいことです。

データにアクセスするときは添字を使います。

int data[3] = {1,2,3};
data[0]; // => 1
data[1]; // => 2
data[2]; // => 3
data[3]; // => エラーとなる

上の例を見るとわかりますが、配列の添字は0から始まります。これは大変間違えやすいので注意してください。上の例のプログラムを見ればわかりますが、for文を使って配列にアクセスするときも0からアクセスしていますね。配列の要素数が5であれば、実際にアクセスできる要素は0~4ということです。

練習問題

  1. 以下に書かれている配列のなかから、一番大きな値と一番小さな値を探して表示するプログラムを作りなさい
int data[20] = {10,30,20,43,5323,34232,4023,12,432412,11,43,23,12,432,9,22,3,4,5,6};

構造体

構造体構文

いよいよこの講習最後の項目となりました。ここでは、構造体について学んでいきます。

構造体は複数のデータをまとめて1つにしたいとき使います。構造体はどういったデータ構造を持つのかの宣言をしてから実際に使います。

//  - 構造体の宣言
// struct 構造体の名前  {
// ここにメンバを書く
// }

struct people {
    double weight;
    double height;
};


// 構造体の変数宣言
// struct 構造体名 変数名;
// このときこの変数を「構造体名型の変数」と言ったりする
// この場合はpeople型のstudentという名前の変数を宣言してることになる
struct people student;

// 要素にはドット(.)を使ってアクセスできる
// その他の扱い方は他の変数と変わらない
student.height = 172.5;
student.wegith = 58.2;
printf("体重:%f,身長:%f\n",student.weight,student.height);

構造体自体の使い方はなんとなくわかりましたか?ではどんなときに構造体を使うのでしょうか。次のようなプログラムを考えてみましょう。

#include <stdio.h>

int main(void){
    double height[10];
    double weight[10];
    int count = 10;
    printf("今から10人分の身長・体重データを入力してもらいます。\n");
    for(int i = 0;i<count;i++){
        printf("身長:");
        // 配列の値にscanfで値を代入する場合、普通にアクセスするときのものに&をつければよいです
        scanf("%lf",&height[i]); 
        printf("体重:");
        scanf("%lf",&weight[i]);
    }
    for(int i = 0;i<10;i++){
        printf("%d人目 - 体重: %f,身長 %f\n",i,weight[i],height[i]);
    }
    return 0;
}

10人分の身長・体重を入力してもらい、さらにそれを表示していくプログラムです。これを構造体を使って表現するとこうなります。

#include <stdio.h>

struct people {
    double weight;
    double height;
}; // ここのセミコロン忘れがちなので気をつけて

int main(void){
    struct people students[10];
    int count = 10;
    printf("今から10人分の身長・体重データを入力してもらいます。\n");
    for(int i = 0;i<10;i++){
        printf("身長:");
        scanf("%lf",&students[i].height);
        printf("体重:");
        scanf("%lf",&students[i].weight);
    }
    for(int i = 0;i<10;i++){
        printf("%d人目 - 体重: %f,身長 %f\n",i,students[i].weight,students[i].height);
    }
    return 0;
}

少しスッキリしたのではないでしょうか。このように、プログラム中では複数のデータを関連して扱いたいということが多くあります。例えば、キャラクターのデータを1つにまとめたいとか。

struct chara {
    char name[20]; // char型の配列の話ってしましたっけ?
    int hp; // HP
    int mp; // MP
    int atk; // 攻撃力
    double def; // 防御力
}

struct chara hero,enemy;

みたいな。こうやってやると、例えばstruct charaを2つ受け取ってダメージを計算する関数とかが書けます。

void dipslayDamage(struct chara attcker,struct chara receiver){
    printf("ダメージ:%f\n",attcker.atk / receiver.def);
    if(receiver.hp - attcker.atk / receiver.def < 0){
        printf("%sは倒れてしまった!",receiver.name); // %s表記の場所にchar型の配列をそのまま渡してあげるとその文字列が表示されます。詳しい説明はどこかで。
    }
}

なるほど、便利そうですね。ピンと来ましたか?(僕の説明が下手というのはありますが)ピンと来ない人は、とりあえずなんとなくで理解しておいてくれればいいです。この構造体は、マイコンの設定等でも利用します。多く目にしているうちに、理解できるようになれるでしょう。

typedef

ところで、struct charaっていちいち書くの面倒ですよね。長い。そこで便利なのがtypedefです。

struct chara {
    char name[20]; // char型の配列の話ってしましたっけ?
    int hp; // HP
    int mp; // MP
    int atk; // 攻撃力
    double def; // 防御力
}
// こうすると
typedef struct chara character;
// こう使えるようになる
character hero,enemy;

typedefはその名前からなんとなく分かる通り、ある型に別の名前をつけることができる機能です。上のコードの場合であれば、struct chara型にcharacterという別の名前をつけてあげたことになります。typedef自体は構造体以外にも使う事ができます。

typedef int my_int; //なんの意味もないが……
my_int i = 1;

最初からtypedefで別の名前をつけて使いたい場合はこんな書き方もできます。

typedef struct {
    char name[20]; // char型の配列の話ってしましたっけ?
    int hp; // HP
    int mp; // MP
    int atk; // 攻撃力
    double def; // 防御力
} character;

便利~。

練習問題

  1. 以下に示す構造体を引数にする関数を自由に作りなさい。関数内でメンバーにアクセスすることを条件とします。引数はいくつとっても構いません。
typedef struct {
    char name[20]; // char型の配列の話ってしましたっけ?
    int hp; // HP
    int mp; // MP
    int atk; // 攻撃力
    double def; // 防御力
} character;

例えば非常にシンプルな関数を考えると、こんなふうになります。

void displayHPAndMP(character c){
    printf("HP:%d MP:%d\n",c.hp,c.mp);
}

  1. この辺の話が詳しく知りたいのであれば、「コンピュータの構成と設計」という本の上巻を読むといいでしょう。この本さえちゃんと理解していれば組み込みプログラマとして最低限の知識を得ることができます

  2. このプログラム上でint型ではなくchar型を利用しているのは,図をより正確にするためです。char型の変数は1バイトであり、メモリのアドレス1つに対して4bitまでのデータが格納できるため、char型の変数を使うとアドレス1つに対して1つの変数の値を格納できるからです。わからなくても問題はありません

  3. 実際このように保存されるとは限りません。アドレスは環境によって変わります

  4. 実際のscanfの実装を説明するには皆さんにC言語の知識をもっとつけてもらう必要があるのでやめておきます

  5. sqrtは平方根を求める関数です