C++ with Windows API講習/C言語配列

概要

同じ型の変数を複数管理するのに便利な機能の配列を解説します。
今回の前提となるアドレスやポインタの知識がとても重要です。

重要語

配列

同型の変数をメモリ上に連続して確保するもの

要素

配列に存在する1つ1つの変数

添え字

配列の各要素の番号

添え字演算子

*(配列名 + 添え字)の糖衣構文

std::size関数

配列のサイズを取得する関数

不完全型

変数として宣言できない型

const

変数の値などが不変である事を示すキーワード

必要語

定数

実行中に変化しない値

変数

実行中に値を保存しているもの

宣言

変数の型と名前を定義すること

初期化

変数が最初に持つ値を定義すること

アドレス

メモリ内の位置を表す数値

ポインタ

アドレスを保存する変数

間接参照

アドレスを変数のように扱うこと

糖衣構文/シンタックスシュガー

長い、又は複雑な構文の簡単な書き方

関数

結果を返す、処理のまとまり

仮引数

関数に渡す値

実引数

関数が受け取る値又は変数

input_int.hpp

講習担当者が作ったint値入力ライブラリ

1次元配列

はじめに、最も基本的である1次元配列について解説します。
1次元配列
#include <string>
#include <Windows.h>

int wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
    std::wstring s = L"";

    //配列の宣言には、変数の宣言の変数名の後に角括弧を書く
    //0ではない要素数を記述する。初期化値の数が要素数に満たない場合、不足分は0で埋められる
    int array1[5] = {};
    int array2[] = { 5,6,7,8,9 };

    //配列名は先頭の要素のアドレスに変換されるので、間接参照してアクセスできる
    *(array1) = 5;

    //配列はメモリ上に連続して要素が配置されている
    //また、アドレスに対する演算は、アドレスが指す型の大きさごとに行われる
    //従って、配列名に添え字を足すことで、添え字に対応する要素のアドレスが得られる
    *(array1 + 1) = 10;

    //上記のアクセス方法は記法が複雑になりがちであるので、添え字演算子を利用する
    array1[2] = 15;

    //インデックスループならば「0から始め、全ての要素にアクセスする」ことが一般的
    for (int i = 0; i < 5; i++) {
        s += std::to_wstring(*(array1 + i)) + L" ";
    }
    MessageBoxW(NULL, s.c_str(), L"array1", MB_OK);

    //std::sizeは、配列の長さを取得できる
    //要素数を指定しなかった配列は、初期化に応じて要素数が決定されている
    s = L"";
    for (int i = 0; i < std::size(array2); i++) {
        s += std::to_wstring(array2[i]) + L" ";
    }
    MessageBoxW(NULL, s.c_str(), L"array2", MB_OK);

    return 0;
}

C言語配列とは

今回扱う範囲は、C言語から受け継いだ、長さを変えることが出来ない配列です。
これは「C言語配列」や「生配列」と呼ばれることが多いです。
ただ、いずれは標準ライブラリのvectorに含まれるstd::vectorを使う方が便利でしょう。

1次元配列の宣言

配列は同じ型の変数をメモリ上に連続して確保するもので、広義の変数です。
配列も変数同様に宣言が必要で、型名 変数名[定数]という構文を取ります。
この定数は自然数で、コード上の値や後述するconst変数などの定数が指定できます。
なお、配列の型は型名 [定数]で、要素数が違うと型は違うことになります。

配列の構造、要素、添え字

配列を宣言すると定数の個数分だけ変数がメモリ上に連続して確保されます。
メモリ上に確保されたそれぞれの変数のことを要素、要素を一意に指す番号が添え字です。
ここで注意したいのが、添え字は0から始まるということです。
すなわち、添え字は0から要素数-1まで存在するということになります。

配列の初期化

配列の初期化には、= {}もしくは{}を用います。
波括弧の中に要素数まで、コンマ区切りで初期化値を記述することが出来ます。
そして要素数に初期化値の数が満たない場合には、残りの要素については0で埋められます。
なお、1次元配列の定数を指定しない場合は、初期化値の数が配列の要素数になります。

配列のアクセス - ポインタ演算

配列は複数の変数を指すので、各々の要素には配列名だけではアクセス出来ません。
そこで配列の各々の要素には、配列名と添え字を使ってアクセスします。
実は、配列名は先頭の要素へのアドレスへと暗黙のうちに変換されます。
そのため、アドレス演算を用いることで、*(配列名 + 添え字)で要素にアクセスします。

配列のアクセス - 添え字演算子

先述の通り、アドレス演算を用いて*(配列名 + 添え字)で配列の各要素にアクセス出来ます。
ですが、決まり切ったポインタの加算や括弧、間接参照は複数重なるとかなり複雑になります。
そのため、添え字演算子と呼ばれるシンタックスシュガーが存在します。
添え字演算子を用いると、配列名[添え字]という構文で記述することが出来ます。
また、添え字演算子の方が、間接参照演算子や四則演算、代入よりも優先されます。

配列の外へのアクセス

大原則として、配列の外へアクセスすること、すなわち、範囲外アクセスは許されません。
これは、範囲外アクセスが関係の無いメモリを変更し、意図しない動作の原因になるからです。
よって私達は、負数や要素数以上の値を指定しないようにコードを書かなくてはなりません。
ただし、基本WindowsなどのOS環境下では範囲外アクセスはブロックされます。

std::size関数

std::size関数は、配列を渡すとその配列の要素数を返却する関数です。
この関数は、#include <string>があれば使うことが出来ます。
もちろん、当然ですが、ただのポインタを渡した場合はエラーとなります。

配列の全要素にアクセスする

配列の全要素にアクセスする時には、インデックスループを使用するのが一般的です。
今回は1次元配列なので、1回だけインデックスループを回せばよいことになります。
添え字は0から要素数 - 1なので、for(int i = 0; i < size; i++)などになります。
sizeが配列の要素数のことを示します。これはstd::size関数で得てもよいでしょう。
このfor文を使用すると、変数iがそのまま添え字として使用できます。

コード「1次元配列」解説

コード「1次元配列」の解説です。

array1、array2宣言

配列の宣言は、型名 変数名[定数]という構文を取るのでした。
つまり、array1array2は、int型の変数が5個並んだ配列ということになります。
なお、array2は要素数を指定していないので、初期化値の個数で要素数が決定しています。

array1、array2初期化

array1={}であるので、要素数に対しての不足分、すなわち全てが0で埋められます。
array2={5,6,7,8,9}であるので、前から5,6,7,8,9で初期化されます。

array1、array2へのアクセス

配列の名前は先頭要素のアドレスになるので、*(配列名)で先頭要素にアクセス出来ました。
その他の要素に対しては、アドレス演算を用いて、*(配列名 + 添え字)とアクセスできます。
しかし、それは複雑になりやすいため、シンタックスシュガーの添え字演算子を用いましょう。
添え字演算子を用いると、配列名[添え字]という構文でアクセスできるのでした。
また、アドレス演算から分かる通り、添え字は0から要素数-1であることに注意しましょう。

アクセス例 : array1:添え字

0:先頭

*(array1) *(array1 + 0) array1[0]

n:nは先頭と終端との間の添え字

*(array1 + n) array1[n]

4:終端

*(array1 + 4) array1[4] array1[std::size(array1) - 1]

2次元配列

それでは、配列を要素として持つ配列の、2次元配列を解説していきます。
2次元配列も使えると便利ですが、1次元配列では出来ない事では無いので安心してください。
2次元配列
#include <string>
#include <Windows.h>
#include "input_int.hpp"

void push_int_and_tab(std::wstring* s, int a) {
    *s += std::to_wstring(a) + L"\t";
    return;
}

int wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
    int input[5];
    int table[][5] = { {},{},{},{},{} };

    for (int i = 0; i < std::size(input); i++) {
        input[i] = winput::input(std::to_wstring(i + 1) + L"個目");
    }

    for (int i = 0; i < std::size(table); i++) {
        for (int j = 0; j < std::size(table[i]); j++) {
            table[i][j] = input[i] * input[j];
        }
    }

    std::wstring s = L"\t";
    for (int i = 0; i < std::size(input); i++) {
        push_int_and_tab(&s, input[i]);
    }
    s += L"\n";
    for (int i = 0; i < std::size(table); i++) {
        push_int_and_tab(&s, input[i]);
        for (int j = 0; j < std::size(table[i]); j++) {
            push_int_and_tab(&s, table[i][j]);
        }
        s += L"\n";
    }

    MessageBoxW(NULL, s.c_str(), L"結果", MB_OK);

    return 0;
}

不完全型

不完全型は、変数として宣言できない、すなわち大きさが分からない型のことを指します。
宣言のみされた構造体や要素数が決定しない配列、void型などがこれに属します。
ただし、初期化で要素数が決定する1次元配列や、void*は不完全型には分類されません。

void型

voidは英語で「空」などの意味を持ち、型の情報が失われているという文脈で使用されます。
既に解説した範囲であると、関数宣言でのvoidがあります。
なお、void*は、「ポインタである」のでサイズが決定できるのです。

2次元配列の宣言

2次元配列の宣言は、型名 変数名[定数2][定数1]という構文を取ります。
定数2は1次元配列の定数と同様で、定数1も概ね同様ですが、省略出来ません。
これは、配列の要素に不完全型を持つことが許されていないことが原因です。

2次元配列での次元

2次元配列では、配列を持つ配列を1次元、変数を要素として持つ配列を2次元と考えます。
これによって、宣言の定数1を1次元の要素数、定数2を2次元の要素数と言います。
またこのように、2次元配列を平面として見做して考えることができます。

2次元配列のアクセス

配列を要素として持つ配列なので、*(*(配列名+添え字2)+添え字1)でもいいでしょう。
ただし、添え字演算子を用いて、配列名[添え字2][添え字1]の方がよいでしょう。
添え字2で要素の配列を選択し、添え字1で選択した配列の要素へとアクセスしています。

std::size関数

std::size関数は、配列の要素数を返す関数なので、2次元配列でも特別なことはありません。
2次元配列は、1次元配列を幾つか持っている1次元配列と捉えられます。
すなわち、1次元の要素数、宣言での定数1が要素数として返却されることになります。

全要素へのアクセス

今回は要素の配列を選択し、更にその配列の要素へとアクセスする必要があります。
なので、2重でインデックスループを回せばよいでしょう。
すると、外側は0から2次元の要素数-1、内側は0から1次元の要素数-1などになります。

発展 - 多次元配列

2次元配列があったのですから、3次元、4次元など、n次元配列も作ることが出来ます。
なお、2次元以上の配列はまとめて、多次元配列と呼びます。
宣言の角括弧を増やして宣言しますが、やはり1次元の要素数のみ省略できます。
アクセスも同様ですが、こちらもやはり、添え字演算子を使うことが賢明でしょう。
全要素にアクセスするときは、次元数ごとにインデックスループを回せばよいでしょう。

コード「2次元配列」解説

コード「2次元配列」の解説です。

概要

配列inputにint値をそれぞれ入力させ、総当たり的に表を作り、表示するプログラムです。
配列tableに計算結果を全て保存した後に、文字列に加工して表示させます。

table宣言/初期化

tableは、定数1を省略し、初期化で要素数を決定しています。
したがって、int型の変数が5個並ぶ配列を、5個並ぶ配列を持つ2次元配列です。
今回、tableに存在する25個の要素は、全て0で初期化されることになります。

tableへのアクセス

やはり、添え字演算子を使うのがよいでしょう。

アクセス例:table:2次元/1次元添え字

0/0:先頭/先頭

**table *table[0] (*table)[0] table[0][0]

n/m

*(*(table+n)+m) *(table[n]+m) (*(table+n))[m] table[n][m]

4/4:終端/終端

*(*(table+4)+4) *(table[4]+4) (*(table+4))[4] table[4][4]
*(*(table+std::size(table)-1)+std::size(table[0])-1)
*(table[std::size(table)-1]+std::size(table[0])-1)
(*(table+std::size(table)-1))[std::size(table[0])-1]
table[std::size(table)-1][std::size(table[0])-1]

1次元配列を関数に渡す

最後に、配列を関数に渡すことについて解説します。
多次元配列を関数に渡すのは複雑なので割愛します。
1次元配列を関数に渡す
#include <string>
#include <Windows.h>
#include "input_int.hpp"

void copy(const int from[], int to[], const int size) {
    for (int i = 0; i < size; i++) {
        to[i] = from[i];
    }
    return;
}

using ca_obj_type = int(const int, const int);
using ca_obj_ptr = ca_obj_type*;
void ca(const int lhs[], const int rhs[], int to[], const int size, ca_obj_ptr ca_obj) {
    for (int i = 0; i < size; i++) {
        to[i] = ca_obj(lhs[i], rhs[i]);
    }
    return;
}

int add(const int lhs, const int rhs) {
    return lhs + rhs;
}

int sub(const int lhs, const int rhs) {
    return lhs - rhs;
}

int mul(const int lhs, const int rhs) {
    return lhs * rhs;
}

//divはstdlib.hにも定義されているのでdivi
int divi(const int lhs, const int rhs) {
    return lhs / rhs;
}

void array_to_wstring(const int array[], std::wstring* const str, const int size) {
    for (int i = 0; i < size; i++) {
        *str += std::to_wstring(array[i]) + L" ";
    }
    return;
}

void show_array(const int array[], const int size, std::wstring str) {
    std::wstring out;
    array_to_wstring(array, &out, size);
    MessageBoxW(NULL, out.c_str(), str.c_str(), MB_OK);
    return;
}

int wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
    const int sizes = 5;

    int result[sizes] = {};
    int in1[sizes] = {}, num[sizes] = { 1,2,3,4,5 };

    for (int i = 0; i < sizes; i++) {
        in1[i] = winput::input(std::to_wstring(i) + L"番目を入力");
    }

    copy(in1, result, sizes);
    show_array(result, sizes, L"結果:copy");

    ca(in1, in1, result, sizes, add);
    show_array(result, sizes, L"結果:ca-add / in1+in1");

    ca(in1, in1, result, sizes, sub);
    show_array(result, sizes, L"結果:ca-sub / in1-in1");

    ca(in1, num, result, sizes, mul);
    show_array(result, sizes, L"結果:ca-mul / in1*num");

    ca(in1, num, result, sizes, divi);
    show_array(result, sizes, L"結果:ca-divi / in1/num ただしint値");

    return 0;
}

const変数

constは、変数の値などが不変であることを示すキーワードです。
これと変数を用いることで、定数を表現することが出来ます。
変数の宣言で、const 型名型名 constなどと用います。
const変数は初期化でのみ値を代入でき、なおかつ初期化が必須となります。
なお、以後const 型名型名 constは等価であることから、前者を積極的に扱います。

constポインタ

ポインタでは、位置によって2種類の意味に分れています。
const 型名 *は、ポインタが指す変数がconst 型名であることを意味します。
従って、この場合のconstポインタを間接参照した時に、代入をすることは出来ません。
対して型名 * constは、ポインタが持つアドレスが定数であることを意味します。
従って、この場合のconstポインタはアドレスを再代入出来ず、参照先を変更できません。
もちろん、双方のconstを同時に指定することも可能です。

ポインタのconstパターン

型名 *

参照先の値の書き換え、参照先の変更が出来るポインタ

const 型名 * 型名 const *

参照先の値の書き換えは出来ないが、参照先の変更は出来るポインタ

型名 * const

参照先の値の書き換えは出来るが、参照先の変更は出来ないポインタ

const 型名 * const 型名 const * const

参照先の値の書き換えも、参照先の変更もできないポインタ

関数の仮引数のconst

一般に、ポインタ渡しかつ、それを変更しない場合、仮引数にconstを指定するべきです。
これは、関数側でポインタ渡しされた引数を変更しないことを保証出来るからです。
また今回では、値渡しの場合にもconstを指定しているようにしています。
更に、array_to_wstring関数では、参照先を変更しない意味のconstを指定しています。
これらは、関数を利用する側にとっては重要でないので、通常では過剰と言えるでしょう。

1次元配列のポインタへの降格

C++において、配列は関数の引数として渡したり、関数の返り値として返却できません。
そこで、配列はポインタへと降格され、ポインタとして引数に渡せるようになっています。
すなわち、配列はポインタ渡しでのみ、関数へと渡すことが出来るのです。

1次元配列の仮引数

仮引数での1次元配列型名 []は、要素数が無視され、型名 *と等価になります。
すなわち、ポインタに降格された「元配列」を、通常のポインタで受け取っているのです。
そのため、引数で受け取った配列は「ただのポインタ」であり、std::size関数に渡せません。
したがって、配列の要素数を同時に引数で受け、それを用いるのが良いでしょう。
なお、通常のポインタはアドレス演算を適用できるので、添え字演算子を使用できます。

補足 - 構造体で配列を隠す

配列は、関数の引数に渡したり、返り値として返却できないと言いました。
しかし、構造体のメンバに配列を持たせた場合は、それらは出来るようになります。
もちろん、配列の要素数こそ自由に変えることは出来ませんが、価値はあるかもしれません。
ただ、標準ライブラリのstd::vectorは要素数が変更できるので、そちらの方が良いでしょう。

発展 - 多次元配列のポインタへの降格

配列がポインタへ降格されるのは、最も高い次元のみです。
すなわち、int[5][10]を関数に渡す際は、int(*)[10]int[][10] になります。
したがって、多次元配列はそれぞれの要素数ごとに、関数が必要になってしまいます。
これを解決するには、構造体等で配列を隠すか、今後学ぶテンプレートを使いましょう。
なお、前者の場合にはstd::vectorでも良いでしょう。

コード「1次元配列を関数に渡す」解説

コード「1次元配列を関数に渡す」の解説です。

概要

要素数5のint配列in1に入力を受け、1,2,3,4,5のint型配列numを用意します。
まず、入力された配列を、同じく要素数5のint配列resultにコピーして出力します。
そして、in1+in1in1-in1in1*numin1/numを計算、出力するコードです。
const変数すなわち定数である、sizesの値を変更すると、扱う要素数が変化します。

copy関数

第1引数のint配列fromの先頭からsize個を、第2引数のint配列toの先頭からコピーします。
sizeが有効な値かは、関数側では検証できないので、呼び出し側が注意する必要があります。
なお、fromは、受け取る配列を変更する必要がないため、constを指定しています。
sizeは、関数実行中に変更してはいけない変数なので、constを指定しています。

ca関数

「C++ with Windows API講習 - 入門 - 関数基本」のように関数のポインタを取っています。
この関数のポインタの処理を、int配列lhsrhsの各要素について行います。
その結果は、int配列toへ、それぞれの要素について代入されます。
lhsrhsは受け取った配列を変更しないのでconstを指定します。
sizeは、関数実行中に変更してはいけない変数なので、constを指定しています。
対して、toは、受け取った配列を変更する必要があるので、constを指定していません。

add/sub/mul/divi関数

それぞれ、2つのint値を取り、それらを足し/引き/掛け/割りした結果を返します。
これらは、ca関数に渡し、ca関数の動作を決定するのに使います。
なお、割り算はdivであるとC言語の標準ライブラリと被るので、diviにしています。
ちなみに、div関数はstdlib.hに含まれていて、int値を2つ取り、div_t構造体を返します。

array_to_wstring/show_array関数

array_to_wstring関数は、int配列とその要素数、std::wstringへのポインタを取ります。
そして、int配列を先頭から、std::wstringへと空白区切りで追加していきます。
一方、show_array関数は、array_to_wstring関数を用いて、配列を表示します。

練習問題

あるRの、1番から40番までの出席番号が付けられた生徒40人が数学のテストを受けました。
出席番号順にテストの点が入力されるので、平均点を計算、出力してください。
ただし、テストの点は0から100点で、平均点は小数で出力するようにして下さい。
また、平均点を出力した後、任意の出席番号の点数を出力するようにしてください。
詳しくは、コード「解答の一部」を参照してください。

なお、入力はC++標準ライブラリのsstreamに含まれる、std::wstringstreamを用いています。
今回のコードでは、input >> int型の変数名で、40回入力することが出来るようになっています。
std::wstringstreamに関しては、いづれ執筆します。今すぐ知りたい人は検索してみてください。
解答の一部
#include <string>
#include <sstream>
#include <random>
#include <Windows.h>
#include "input_int.hpp"

int r_random() {
    std::random_device seed_gen;
    std::default_random_engine engine(seed_gen());
    return std::uniform_int_distribution<>(0, 100)(engine);
}

std::wstringstream input;

void init(std::wstringstream* ss, int size) {
    for (int i = 0; i < size; i++) {
        (*ss) << r_random() << L" ";
    }
    return;
}

int wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
    init(&input, 40);
    //入力:input >> int型の変数名
    //例 :int x; input >> x;

    std::wstring out;

    //全員の平均点を出力
    MessageBoxW(NULL, out.c_str(), L"平均", MB_OK);

    while (int index = winput::input(L"0で終了")) {
        //入力された出席番号の点数を出力
        MessageBoxW(NULL, out.c_str(),
            (std::to_wstring(index) + L"番の点数は").c_str(), MB_OK);
    }

    return 0;
}
解答
練習問題解答
#include <string>
#include <sstream>
#include <random>
#include <Windows.h>
#include "input_int.hpp"

int r_random() {
    std::random_device seed_gen;
    std::default_random_engine engine(seed_gen());
    return std::uniform_int_distribution<>(0, 100)(engine);
}

std::wstringstream input;

void init(std::wstringstream* ss, int size) {
    for (int i = 0; i < size; i++) {
        (*ss) << r_random() << L" ";
    }
    return;
}

int wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
    init(&input, 40);
    //入力:input >> int型の変数名
    //例 :int x; input >> x;

    std::wstring out;

    int students[40];
    int sum = 0;
    for (int i = 0; i < 40; i++) {
        input >> students[i];
        sum += students[i];
    }

    out = std::to_wstring(sum / 40.0);

    //全員の平均点を出力
    MessageBoxW(NULL, out.c_str(), L"平均", MB_OK);

    while (int index = winput::input(L"0で終了")) {
        //入力される出席番号の1-40は、添え字の0-39に対応する
        //そのため、配列にアクセスするときには1引く必要がある
        out = std::to_wstring(students[index - 1]);

        //入力された出席番号の点数を出力
        MessageBoxW(NULL, out.c_str(),
            (std::to_wstring(index) + L"番の点数は").c_str(), MB_OK);
    }

    return 0;
}