c++の学習メモ|基礎編
c++の勉強を進めています。
メモ代わりに学習メモをどんどん書いていきます。
c++をこれから勉強する人やすでに勉強を始めた方に参考にもなりそうな内容となっているので、よかったら参考にしてみてください。
void main() とint main()の違い
学習コンテンツで使われているvoid main() を真似して書いてみたら、エラーがでました。そのエラーがこちら。
hello.cpp:3:11: error: ‘::main’ must return ‘int’
void main()
^
何も知らずに、普段はint main()をatcoderで使っていたのですが、void main()にしたらエラーが出ました。そこでググってみた。
osに出力する終了コードを出すか出さないかの違い。voidは出さない。正しくプログラムを終了させるためにも出すことが必須らしいので、int main()の方を使うのが良いみたいです。
そもそも僕の環境ではエラー出るのでint main() を使うしかないですが、少しvoid mainとの違いが気になっていたので、スッキリした気がします。
※int main() およびvoid main()の部分をメイン関数と呼ぶ。
c言語では文字列のデータ型は存在しない
int型の整数のように、数値のデータ型は存在するけど、最近の言語なら存在しているstring型という文字列のデータ型はc言語では存在しない。
c言語で文字列をデータ型として扱うためには、文字列をそのまま使う代わりに、配列に文字列を入れて使う。
→文字列はchar型の配列として扱う。
char s1[10]; // 最大10文字まで入る文字列の配列
scanf("%s", s1); // 文字列の入力。文字列の入力をs1という配列の形で扱う。
文字列がデータ型として存在しない理由は、C言語が古い言語だから。
配列の終わりを\n(¥0)で宣言しないといけない
\nをnull文字という。
配列の終わりを宣言しないといけない。C言語では配列の終わりがどこなのか、C言語が分からないようにできているので、宣言して終わりを示す必要がある。
char s1[4] = {'a', 'b', 'c', '\0'};
4個の配列を入れたい場合、容量として、\0の分を確保(4+1)しないといけない。
\n(¥0)を自分で書かなくても大丈夫
文字列"Hello c++" の終わりには\0が書いてないけど、ちゃんと処理される。
理由は、自動的に文字列の終わりに\0が挿入されるようになっているから。
char s2[ ] = "Hello c++.";
多重配列
多重配列の2次元配列は、エクセルシートのような表になる。
→展開したらX軸、Y軸方向の格子状になる。
#include <iostream>
int main() {
int a[3][4];
int j,k;
// 2次元配列に値を代入。jが最初の配列、kが2番目の配列に入る。
for (j = 0; j < 3; j++) {
for (k = 0; k < 4; k++) {
a[j][k] = j + k;
}
}
(k = 0; k < 4; k++) for文で展開していくと
j = 0、1、2のループそれぞれでkのループは4回実施される。
j = 0のときのループを見てみる。
a[j][k] = j + k;
上記の式は一見すると、左辺を右辺に代入すると思うけど、そうではなくて、
a[j][k]のjにはfor文の(j = 0; j < 3; j++)の中のj の値が入る。kもfor文の(k = 0; k < 4; k++) 中のk が入ることに注意。
a[0][0] = 0 + 0 = 0 // この配列a[0][0]の値は(0, 0) = 0 となる。
a[0][1] = 0 + 1 = 1 // この配列a[0][1]の値は(0, 1) = 1 となる。
a[0][2] = 0 + 2 = 2 // この配列a[0][2]の値は(0, 2) = 2 となる。
a[0][3] = 0 + 3 = 3 // この配列a[0][3]の値は(0, 3) = 3 となる。
以上で、j = 0のループでは、0123が出力される。
j = 1のときは
a[1][0] = 1 + 0 = 1 // この配列a[1][0]の値は(1, 0) = 1 となる。
a[1][1] = 1 + 1 = 2 // この配列a[1][1]の値は(1, 1) = 2 となる。
a[1][2] = 1 + 2 = 3 // この配列a[1][2]の値は(1, 2) = 3 となる。
a[1][3] = 1 + 3 = 4 // この配列a[1][3]の値は(1, 3) = 4 となる。
以上で、j = 1のループでは、1234が出力される。
j = 2のときは
a[2][0] = 2 + 0 = 2 // この配列a[2][0]の値は(2, 0) = 2 となる。
a[2][1] = 2 + 1 = 3 // この配列a[2][1]の値は(2, 1) = 3 となる。
a[2][2] = 2 + 2 = 4 // この配列a[2][2]の値は(2, 2) = 4 となる。
a[2][3] = 2 + 3 = 5 // この配列a[2][3]の値は(2, 3) = 5 となる。
以上で、j = 2のループでは、2345が出力される。
以上ですべてのループが終わり、結果として、
0123
1234
2345
と出力される。
voidとは
void main()など、関数の前に付けるvoidは、戻り値を戻さないという意味。
実はvoidって何なんだとずっと思ってたので、意味を知ってすっきりしました。
global変数の種類は無闇に増やさず最小限にする
ローカル変数は使用が終わったらメモリから消去されてメモリが開放されるのに対して、global変数はずっとメモリに保持され続ける。
メモリの消費は少ない方がいいので、global 変数を使いすぎないようにしたほうがいい。
また、セキュリティ上も、誰かに書き換えられたらいろんな箇所に影響が及ぶかもしれないので、global変数はあまり使わず最低限の使用に控えたほうがいい。
ローカル変数
関数の内部で定義され、その関数内でだけ有効なもの。
→異なる関数の中で同じ変数名を使っても、関数内しかスコープがないので、また、関数が違うので別物として扱われる。
グローバル変数
プログラムの先頭で定義され、どこで呼び出しても同じもの。→名前の重複は避けないといけない。
ファイル分割
ファイル分割は、メイン関数、プロトタイプ宣言、プロトタイプ宣言した関数の実装部分の3つに分けるのが基本。
統合開発環境を使わずにファイル分割をして実行するのは、慣れないと少々難しかったです。
ファイル分割〜実行までの具体的な手順を書いた記事はこちらを参照。
乱数
・rand関数を使うために#include <stdlib.h>でヘッダーファイルをインクルード。
・srand((unsigned)time(NULL));で乱数を初期化。srand関数は乱数を初期化する関数。初期化はこのようにすることが多い。
srand関数の引数にtime関数(1970年からの経過秒を出力する関数)を渡すことで、rand関数を使ったときに値が変化するようにする。
randを疑似関数という。もしsrandに固定値srand(0) のように渡すと、rand関数を使ったとき、出力される値がランダムにならず、同じになってしまう。
rand関数は、0〜RAND_MAXの間の擬似乱数を返す。RAND_MAXの値はいろんな条件、処理系で異なってくる。
乱数のコントロール例
ただ単にrand()とすると、デカい数も返す。
乱数を0〜9の値に絞りたいとき;
rand()%10 と10の余りを計算することで、0〜9の値を返せる。
乱数を0〜10の値にしたいとき;
rand()%10 + 1でできる。
1〜nまでの乱数を得る方法;
rand()%n + 1
0〜nまでの乱数を得る方法;
rand()%(n + 1)
・time関数は1970年〜現在までの経過を秒単位で取得する関数。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 int main() {
6 int x, y;
7 // 乱数の初期化
8 srand((unsigned)time(NULL));
9 // 1から10までの乱数を発生させる
10 x = rand() % 10 + 1;
11 y = rand() % 10 + 1;
12 // 計算結果を出力
13 printf("%d + %d = %d\n", x, y, x + y);
14 return 0;
15 }
// 結果は 3 + 5 = 8 のように出力される
数学の関数を使う
・#include <math.h>をインクルードすると数学関連の関数が利用できる。
・#defineはマクロを定義するためのもの。PIと入力したら3.14だよとなる。定数を定義したいときに使うのが慣習となっている。
・math.hのライブラリで使えるsin(),cos(),tan()の引数はラジアンなので、度数を引数に渡せない。なので、度数の角度(0〜360)をラジアンに変換して渡す。
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PI 3.14
5
6 int main() {
7 int angle;
8 double rad;
9 printf("角度を入力してください(0〜360):");
10 scanf("%d",&angle);
11 // 角度をラジアンに変換
12 rad = PI * (double)angle / 180.0;
13 // 三角関数での計算
14 printf("sin(%d)=%f\n", angle, sin(rad));
15 printf("cos(%d)=%f\n", angle, cos(rad));
16 printf("tan(%d)=%f\n", angle, tan(rad));
17 return 0;
18
19 }
存在しないtan90°の値も無理やり変な値で出力されるので、表示されないように上記コードを修正が必要する場合は、angleの値が90かどうかで条件分岐したらよさそう。
ビット演算
16進数の代入は%x を使う。
OR演算:論理和の計算。0と1のどちらかであれば、1。
00001111 と
11111111 をOR演算すると
11111111となる。
AND演算:論理積。どちらも1じゃないと1にならない
00001111 と
11111111 をAND演算すると
00001111となる。
NOT演算:否定演算。0と1をひっくり返す演算
00001111 をNOT演算すると
11110000となる。
#include <stdio.h>
int main() {
// 16進数
unsigned char i = 0xf; // 2進数:00001111
unsigned char j = 0xff; // 2進数:11111111
printf("%x << 1 = %x\n", i, i << 1); // 1ビット左シフト: 2進数:00001110 = 0x1e0
printf("%x >> 1 = %x\n", i, i >> 1); // 1ビット右シフト:2進数:00000111 = 0x7
printf("%x | %x = %x\n", i, j, i | j ); OR演算 : 2進数:11111111 = 0xff
printf("%x & %x = %x\n", i, j, i & j ); AND演算 : 2進数:00001111 = 0xf
printf("%x = %x\n", i, (unsigned char)~i); // NOT演算 : 2進数:11110000 = 0xf0
}
doubleはbyteが2倍
sizeof(double); // 8byte
sizeof(float); // 4byte
などと表示される。
doubleはfloatに対して2倍大きい。
sizeof()演算子は変数や型の大きさをバイト単位で取得する。
変数は値だけじゃなくアドレスとサイズもある
変数をメモリ上に保存していて、保存先のアドレスがある。
大きさは、コンパイラなど実行する環境で異なってくる。
#include <stdio.h>
int main() {
int a = 100; // int型の変数
double b = 123.7; //double型の変数
float c = 123.7f; // float型の変数(数値の後ろにfをつける)
char d = 'a'; // char型の変数
printf("aの値は%d、大きさは%dbyte、アドレスは0x%x\n", a, sizeof(int), &a);
printf("bの値は%f、大きさは%dbyte、アドレスは0x%x\n", b, sizeof(double), &b);
printf("cの値は%f、大きさは%dbyte、アドレスは0x%x\n", c, sizeof(float), &c);
printf("dの値は%d、大きさは%dbyte、アドレスは0x%x\n", d, sizeof(char), &d);
return 0;
}
ポインタはなりすましができる変数
ポインタ変数に変数のアドレス(メモリ上の保管場所)を保存できる。
ポインタって勉強したあと、何だったか忘れがちなので、
ポインタ=なりすまし という本質だけちゃんと覚えておくとよさげ。
int *p = NULL; // int型のポインタ変数pを定義
初期値が決まってないときはNULLで初期化する。
→NULLはC言語で標準で使われる定数。
ポインタ変数はNULLで初期化するのがルールみたいになっている。
「*」でint型の他の変数のアドレスを受け取る準備をしている。
p = &a; // pにaのアドレスを代入することで、pは変数aになりすましができる。
ポインタから変数の値を取り出すには、アスタリスクを付けると取り出せる。→*p
単にpとすると、アドレスを代入することを意味する。
例えば、
ポインタ変数に値を代入するとき
*p = 30;
ポインタ変数にアドレスを代入するとき
p = &a
と書く。この辺のルールは混乱しそうなので、触れる機会を増やして慣れるしかなさそう。
#include <stdio.h>
void show(int, int, int);
int main() {
int a = 10; // 整数型の変数a
int b = 20; // 整数型の変数b
int *p = NULL; // 整数型のポインタ変数p
p = &a; // pにaのアドレスを代入
show(a, b, *p);
*p = 30; // *pに値を代入
show(a, b, *p);
p = &b; // pにbのアドレスを代入
show(a, b, *p);
*p = 40; // *pに値を代入
show(a, b, *p);
return 0;
}
void show(int n1, int n2, int n3) {
printf("a = %d b =%d *p = %d\n", n1, n2, n3);
}
// 実行結果
a = 10 b = 20 *p = 10
a = 30 b = 20 *p = 30
a = 40 b = 20 *p = 40
実行結果:*pのアドレスはaのアドレスなので、*pの値を変えるとaの値も*pの値に変わる。
ポインタで注意したいこと
ポインタの型を変数の型に合わせて使わないとやばいエラーにつながるので、ポインタの型と変数の型は合わせること。
ポインタはNULLのまま何かの変数のアドレスを設定してなかったら、実行時エラーが出てプログラムは停止するので、ポインタを使うときは注意。
ポインタを使った関数の例
プロトタイプ宣言void swap(int*, int*); の「int*」は、この関数の括弧の中にはポインタ型の変数を入れて下さいねという意味。
*num1: ポインタ型の変数num1。*を付けることで他の変数のアドレスを受け取ることができる。
*num2: 同上。
#include <stdio.h>
// 変数の値入れ替えを行う関数のプロトタイプ宣言
void swap(int*, int*);
int main() {
int a = 1, b = 2;
printf("a = %d b = %d\n", a, b);
swap(&a, &b);
printf("a = %d b = %d\n", a, b);
return 0;
}
void swap(int* num1, int* num2) {
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
関数はvoid型になっているので戻り値を戻さないけど、ポインタ型変数のnum1がmain関数の中の&aからaのアドレスを受け取っているので、num1はaになりすましている状態かつ、swap(&a, &b)の&a,&bで関数の実行結果を参照している状態。なので、戻り値は返さなくてもよい。
main関数の中でswap(&a,&b)が実行されると、実装した関数が以下のように実行される。
・ポインタにアドレスが代入される。
num1 = &a,
num2 = &b
これによってnum1が変数aのアドレスを受け取り、num2が変数bのアドレスを受け取り、num1が変数a、num2が変数2になりすました状態。
・int tempにnum1の値を代入
int temp = *num1
によって変数tempにnum1の値が代入される。num1の値はa = 10なのでtemp = 10
・num1にnum2の値を代入
*num1 = *num2 によってnum1はアドレスはaのアドレスを維持しつつ、値だけnum2の値が代入され書き換えされる。num2の値は20なので、num1 = 20となる。
・num2にtempの値を代入
*num2 = temp によってnum2はアドレスはbのアドレスを維持しつつ、値だけtempの値が代入され書き換えされる。tempの値は10なので、num2 = 10となる。
以上から、swap関数で実行された結果はnum1 = 20, num2 = 10となる。
アドレス&aの値は*num1 = 20なので20、
アドレス&bの値は*num2 = 10なので10
main関数のswap(&a, &b); の&a, &bから実装した関数で実行された上記の値が参照されている状態なので、実装した関数からリターンで戻り値を返さなくても、main関数の中でswap(&a, &b); を実行するだけで値が渡っている。
追記:下記はあくまで値の代入なので、アドレスは変わらない。
*num1 = 1, *num2 = 2
temp = 1
*num1 = 2(アドレスはaのアドレスのまま、2になる)
*num2 = 1(アドレスはbのアドレスのまま、1になる)
ポインタと配列|ポインタと配列は相性がいい
ポインタは配列にもなりすましができる。
char 型の配列を定義してfor分でAからインクリメントすると、ASCIIコードがインクリメントされて、A,B,C,D,...と配列の要素が作られる。→これは知らなかったです。
*(p1 + i)のように、*(ポインタ + i)のように表現すると、ポインタの0番目のアドレスにiをインクリメント。つまりポインタの1番めのアドレス、ポインタの2番めのアドレス、...のようにインクリメントされ、該当のアドレスの値が取得できる。
ポインタで値を出力する方法は他にもある。表現の幅が広いので混乱しそうだけど、少しずつ慣らしていく。ポインタをp1とすると、
・配列みたいに、p1[ i ] と表現しても値が取得できる。
・for分の中でp1++のようにインクリメントすれば、*p1と表現しても値が取得できる。
・*(d + i)としても配列dの値が取得できる。配列dをまるでポインタの様な表現で使える。配列とポインタの似ている点。
p1 = dのようにすると、ポインタp1に配列dのd[ 0 ]のアドレスを代入するのと同じ処理となる。この状態でp1++;とインクリメントすると、アドレスが変わり、p1は&p1[ 1 ]と同じ値、&p1[ 2 ]と同じ値、...という感じになる。
#include <stdio.h>
#define SIZE 5
int main() {
int ar1[SIZE];
char ar2[SIZE];
int i;
// ポインタ変数の初期化
int* p1 = NULL;
char* p2 = NULL;
// 配列に値を代入
for (i = 0; i < SIZE; i++) {
ar1[i] = i;
ar2[i] = 'A' + i;
}
// ポインタ変数に配列のアドレスを代入
p1 = &ar1[0];
p2 = &ar2[0];
for (i = 0; i < SIZE; i++) {
printf("ar1[%d]=%d *(p1+%d)=%d ", i, ar1[i], i, *(p1 + i));
printf("ar2[%d]=%c *(p2+%d)=%c\n", i, ar2[i], i, *(p2 + i));
}
return 0;
}
ポインタで直接配列をつくる
malloc関数は指定したバイトだけメモリを確保する。
メモリはヒープ領域に確保される。
mallocで確保したメモリは、あとで消すことで開放してあげないといけない。開放させる関数がfree関数。mallocとfreeはセットで使う。
開放しないとメモリが残ったままとなり、システムにダメージを与える可能性がある。
ヒープ領域へのメモリの確保はいつでも好きなタイミングでできる。
ローカル変数はスタック領域に保存され、プログラムの終了とともに、自動で値が消されるようになっている。
ポインタや配列を扱うときは、配列の範囲外を発生させないように注意。
3個しかない配列に4個めを参照したりすると、変な値だけど値を持つので、メモリを破壊する可能性がある。
#include <stdio.h>
#include <stdlib.h>
#define SIZE 3
int main() {
int* p1 = NULL;
double *p2 = NULL;
int i;
// ポインタ配列の生成
p1 = (int*)malloc(sizeof(int)*SIZE);
p2 = (double*)malloc(sizeof(double)*SIZE);
// 値の代入
for (i = 0; i < SIZE; i++) {
p1[i] = i;
p2[i] = i / 10.0;
}
// 結果の出力
for (i = 0; i < SIZE; i++) {
printf("p1[%d]=%d p2[%d]=%f\n", i, p1[i], i, p2[i]);
}
// メモリの開放
free(p1);
free(p2);
return 0;
}
4つのメモリ領域
さっきのヒープ領域、スタック領域の他にも、プログラム領域、静的領域もあり、メモリの領域は4つある。
・プログラム領域
プログラム(マシン語)が置かれるところ
・静的領域
グローバル変数やstatic変数が置かれるところ
・ヒープ領域
malloc関数などで動的に確保されたメモリを置くところ
・スタック領域
ローカル変数などが置かれるところ
※ヒープ領域のヒープは山を意味していて、ヒープソートのようなデータ構造のアルゴリズムとはまた別みたいです。
文字列とポインタ
文字列は char型の配列
strcpyは文字列をコピーする関数。文字列は=で代入ができないので、この関数を使う。文字列の配列s1, s2がある場合、s1 = s2のように代入できない。
用法:strcpy(char* s1, char* s2);
左辺のchar型の文字列変数に右辺の文字列を格納する。
コード例ではchar型の10個の配列sに文字列ABCを入れる。
s[0]にA、s[1]にB、s[2]にCが入っている。
Cが配列の終わりであることを示すために、Cの次に\nが入っている。
strcat関数は、文字列を連結する関数。
用法:strcat(char* s1, char* s2);
左辺のchar型の変数に右辺の文字列を追加するかたちで格納する。
かっこの中がポインタになっていることに注目。実際に使うときにはポインタじゃなくても、内部的にはコピーのためにポインタが使われていることを意味する。
コード例ではABCの入っている配列sにDEFが入る。
s[3]にD、s[4]にE、s[5]にFが入っている。
strlen関数は、配列の0番めから\0に到達するまで数をカウントする。
使い方 int l = strlen("HelloWorld"); 戻り値は整数なのでint型の変数に格納。
#include <stdio.h>
#include <string.h>
int main() {
char s[10];
int len;
// 文字列のコピー
strcpy(s, "ABC");
printf("s=%s\n", s);
// 文字列の結合
strcat(s, "DEF");
printf("s=%s\n", s);
// 文字列の長さ
len = strlen(s);
printf("文字列の長さ:%d\n", len);
}
配列がポインタに似ていることを表す例
配列自体がポインタのようなものなので、scanf関数で第2引数に配列を渡すときは、&を付けてアドレス参照させなくても良い。
strcmp関数は文字列を比較して同じであれば0を返す関数
#include <stdio.h>
#include <string.h>
int main() {
char s1[256], s2[256];
printf("s1=");
scanf("%s", s1);
printf("s2=");
scanf("%s", s2);
if (strcmp(s1, s2) == 0) {
printf("s1とs2は等しい\n");
}
else {
printf("s1とs2は等しくない");
}
return 0;
}
文字列を数字に変換する関数
atoi(); ASCII to Integer の略
atof(); ASCII to float の略
前者は文字列を整数に変換する関数、後者は文字列をフロートに変換する関数
復習になるけど、c++では文字列はchar型の配列として扱う。
#include <stdio.h>
#include <stdlib.h>
int main() {
char s1[] = "1000";
char s2[] = "12.345";
int a;
double b;
a = atoi(s1);
b = atof(s2);
printf("a=%d b=%f\n", a, b);
}
数値を文字列に変換する
sprintf();関数
文章を文字列にしてくれる関数。printfのように画面に出力はしないので勘違いに注意。
sprintf(s1, "%d", a);とすることで、int aの値を%dに代入し文章化し、char型の配列s1に代入している。
コードの例では、配列s1には、s[0] = '1', s[1] = '0', s[2] = '0',s[3] = \0と文字列が入る。
puts();関数
改行付きのprintf。引数をprintfと同様に出力してくれる。出力したら自動的に改行も入れてくれるので、printfのように\nを入れなくていいので便利。
#include <stdio.h>
#include <stdlib.h>
int main() {
char s1[256], s2[256];
int a = 100, b = 200;
sprintf(s1, "%d", a);
sprintf(s2, "bの値は%dです。", b);
puts(s1);
puts(s2);
return 0;
}
配列の注意点;長さを超えた配列を作らないようにする。
サイズ10の配列に11個入れるみたいなこと。コンパイル時にエラーはでないけど、実行時に異常終了するかもしれない。osでもセキュリティーホールとして狙われる。
構造体
構造体とは、いろんな種類の変数をひとまとめにしたもの。
構造体の定義を構造体テンプレートという。
いろんな複数の変数があって概念としてはひとくくりにしたほうがいいデータがある。例えば会員のデータとか。メールアドレス、名前、年齢など。
構造体はstruct [構造体につける名前] で宣言し、内容を定義する。
構造体を使うときは、struct student data;のように、定義した構造体にさらに名前をつける。この例ではstudentという構造体にdataという名前をつけている。
#include <stdio.h>
#include <string.h>
// 学生のデータを入れる構造体
struct student {
int id; //学生番号
char name[256]; // 名前
int age; // 年齢
};
int main() {
struct student data;
data.id = 1; // 番号を設定
strcpy(data.name, "山田太郎"); // 名前を設定
data.age = 18; // 年齢を設定
// データの内訳を表示
printf("学生番号:%d 名前:%s 年齢:%d\n", data.id, data.name, data.age);
return 0;
}
構造体と配列
typedefで構造体の名前を再定義できる。
既存の構造体の名前を変更する際に使う。
typedefで再定義した構造体はstructを付けずに使える。下記の例だと、student_dataとするだけで使える。
構造体は配列と組み合わせるとデータを扱いやすい。
#include <stdio.h>
#include <stdlib.h>
// 学生のデータを入れる構造体
struct student {
int id; // 学生番号
char name[256]; // 名前
int age; // 年齢
};
// 構造体の名前をtypeofで定義
typedef struct student student_data;
int main() {
int i;
student_data data[] = {
{1, "山田太郎", 18},
{2, "佐藤良子", 19},
{3, "太田隆", 18},
{4, "中田優子", 18},
};
// データの内訳を表示
for (i = 0; i < 4; i++) {
printf("学生番号:%d 名前:%s 年齢:%d\n", data[i].id, data[i].name, data[i].age);
}
return 0;
}
構造体とポインタ
構造体もポインタとして使える。
構造体のポインタを使う場合、アロー演算子を使ってデータにアクセスする。
例)data->id
setDataのプロトタイプ宣言でchar型だけポインタ型でchar*となっているのに少し疑問を感じた。→setData関数の定義でnameはstrcpy関数をつかうため。strcpy関数は引数にポインタ型の変数を渡すため。
用法:strcpy(char* s1, char* s2);
char name[ ] [ 256 ] = { "要素1", "要素2", ... }のように初期化しており、2次元配列になっている。
データ型 配列名 [ 行数 ] [ 要素数 ]という意味になる。初期化のときは行数を省略できる。
#include <stdio.h>
#include <string.h>
// 学生のデータを入れる構造体
typedef struct {
int id; // 学生番号
char name[256]; // 名前
int age; // 年齢
}student_data;
// 構造体のデータを表示する関数
void setData(student_data*, int, char*, int);
void showData(student_data*);
int main() {
student_data data[4];
int i;
int id[] = { 1,2,3,4 };
char name[][256] = { "山田太郎", "佐藤良子", "太田隆", "中田優子 "};
int age[] = { 18, 19, 18, 18 };
// データの設定
for (i = 0; i < 4; i++) {
setData(&data[i], id[i], name[i], age[i]);
}
// データの内訳を表示
for (i = 0; i < 4; i++) {
showData(&data[i]);
}
return 0;
}
// データのセット
void setData(student_data* data, int id, char* name, int age ){
data->id = id; // idのコピー
strcpy(data->name, name); // 名前のコピー
data->age = age; // 年齢のコピー
}
// データの表示
void showData(student_data* data) {
printf("学生番号:%d 名前:%s 年齢:%d\n",data->id, data->name, data->age);
}
構造体の値渡しとポインタ渡し
構造体の値を参照するときはポインタ渡しをしたほうがいい。
値渡しだとちゃんと値が渡せない。
値渡しをすることの弊害1個め
・値渡しで引数としてデータを渡すと、スタック領域のメモリにデータが保持されるけど、扱うデータが大きいとスタック領域を壊してしまう。
・値渡しだと変数データのコピーが発生するので、プログラムの効率が悪い。アドレスが違うけど値は同じという変数が2つできることになる。ポインタ渡しだとコピーの必要がない。
1 #include <stdio.h>
2
3 // データを入れる構造体
4 typedef struct {
5 int a;
6 double d;
7 }num_data;
8
9 // 2種類の値設定関数
10 void dealData1(num_data data); // 値渡し
11 void dealData2(num_data* pData); // ポインタ渡し
12
13 int main() {
14 num_data n1 = {1, 1.2}, n2 = {1, 1.2};
15 printf("n1のアドレス:0x%x n2のアドレス:0x%x\n", (unsigned int)&n1, (unsigned int)&n2);
16 dealData1(n1);
17 dealData2(&n2);
18 printf("n1.a = %d n1.d = %f\n", n1.a, n1.d);
19 printf("n2.a = %d n2.d = %f\n", n2.a, n2.d);
20
21 return 0;
22 }
23
24 void dealData1(num_data data) {
25 printf("a=%d d=%f\n", data.a, data.d);
26 printf("dealdata1に渡ってきたデータのアドレス:0x%x\n" , (unsigned int)&data);
27 // 値の変更
28 data.a = 2;
29 data.d = 2.4;
30 }
31
32 void dealData2(num_data* pData) {
33 printf("a=%d d=%f\n", pData->a, pData->d);
34 printf("dealData2に渡ってきたデータのアドレス:0x%x\n" , (unsigned int)pData);
35 // 値の変更
36 pData->a = 2;
37 pData->d = 2.4;
38 }
1,1 先頭
別の問題として、上記コードのコンパイルを実行すると、下記のようなエラーが表示されますが、そのまま実行しても実行されます。
num_data*からunsigned int への型変換が厳密にできないみたいなエラーメッセージですが、c++は型変換に関して厳格ということもあり、解決はどうもややこしいエラーのようです。
他にもヘッダーファイルをインクルードしたりもしましたが、エラーは変わりませんでした。
-fpermissiveオプションをコンパイル時につけると、エラーの表示をerrorからwarningにダウングレードはできるみたいです。下記エラーはダウングレードさせてコンパイルしたときのメッセージです。
hello.cpp: In function ‘int main()’:
hello.cpp:15:79: warning: cast from ‘num_data*’ to ‘unsigned int’ loses precision [-fpermissive]
15 | x n2のアドレス:0x%x\n", (unsigned int)&n1, (unsigned int)&n2);
| ^~
hello.cpp:15:98: warning: cast from ‘num_data*’ to ‘unsigned int’ loses precision [-fpermissive]
15 | :0x%x\n", (unsigned int)&n1, (unsigned int)&n2);
| ^~
hello.cpp: In function ‘void dealData1(num_data)’:
hello.cpp:26:86: warning: cast from ‘num_data*’ to ‘unsigned int’ loses precision [-fpermissive]
26 | ータのアドレス:0x%x\n", (unsigned int)&data);
| ^~~~
hello.cpp: In function ‘void dealData2(num_data*)’:
hello.cpp:34:85: warning: cast from ‘num_data*’ to ‘unsigned int’ loses precision [-fpermissive]
34 | �ータのアドレス:0x%x\n", (unsigned int)pData);
| ^~~~~
続けるにはENTERを押すかコマンドを入力してください
C++でファイル操作をする
fileを扱うときはstdlib.hをインクルードする。
FILEという構造体を使う。
fopenの引数の"w"は書き込み専用で開くオプション
fileがopenできないなど、ファイルにアクセスできない場合、c言語ではfileにNULLが返されるので、その場合は処理を終了させる。プログラムを異常終了させる場合はexit(1);を使う。
fprintfでファイルに内容を書き込むことができる。ファイルへのprintfを行える。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
file = fopen("/home/usr/sample.txt", "w");
if (file == NULL) {
printf("do not open file.\n");
exit(1);
}
fprintf(file, "Hello World.\r\n");
fprintf(file, "ABCDEF\r\n");
return 0;
}
c++でファイルの読み込みをする
マクロで文字配列のサイズを決める。サンプルでは256文字とする。
char line[SIZE]で読み込む文字列の行の配列を定義し、配列を空文字で初期化しておく。
”r”で読み取り専用でファイルを開く。
読み取り専用で開いたファイルからlineの配列に最大256文字ずつを代入、都度printfで表示する。ファイルからlineに入れる文字列がなくなるまでwhileで処理を続ける(文字列がなくなったらNULLが返されるので、NULLになるまで継続)。
#include <stdio.h>
#include <stdlib.h>
#define SIZE 256
int main() {
FILE *file;
char line[SIZE];
line[0] = '\n';
file = fopen("/home/usr/sample.txt", "r");
if (file == NULL) {
printf("do not open file.\n");
exit(1);
}
while (fgets(line, SIZE, file) != NULL) {
printf("%s", line);
}
fclose(file);
return 0;
}
ファイルの読み込み:上記と違う方法
上記は1行単位で文字列の配列にして読み込んだけど、
今度は1文字ずつ読み込む方法。fgetcを使う。
読み込む文字がなくなるとEOFというマクロを返す。EOFは−1を返す。
1#include <stdio.h>
2#include <stdlib.h>
3
4int main() {
5 FILE *file;
6 int c;
7 file = fopen("/home/katsuo/memo.txt", "r");
8 if (file == NULL) {
9 printf("do not open file.\n");
10 exit(1);
11 }
12
13 while ((c = fgetc(file)) != EOF) {
14 printf("%c",(char)c);
15 }
16 fclose(file);
17 return 0;
18 }
バイナリファイルにバイナリを書き込んで読み込み表示する方法
バイナリとは、人間が判読できる文字とは違って、画像とかコンパイル済の実行ファイルのデータなど、人間が何か操作できるものではないデータをバイナリという。バイナリファイルは専用の表示ツールなどがないと、普通に開いても中身がちゃんと表示されない。
char型の文字列書き込み用の配列と読み込み用の配列を準備。
バイナリデータを書き込むときは、fopenの引数に"wb"を渡す。bはバイナリデータの意味。
fwriteの引数のsizeof(char)の意味は、1個のデータのサイズはchar型だよという意味。sizeof(wdata)は、wdata配列全体のサイズの意味。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE* file;
int i;
char wdata[] = {0x10, 0x1a, 0x1e, 0x1f};
char rdata[4];
// バイナリデータの書き込み
file = fopen("/home/katsuo/test.bin", "wb");
if (file == NULL) {
printf("do not open file.\n");
exit(1);
}
fwrite(wdata, sizeof(char), sizeof(wdata), file);
fclose(file);
// バイナリデータの読み込み
file = fopen("/home/katsuo/test.bin", "rb");
if (file == NULL) {
printf("do not open file.\n");
exit(1);
}
fread(rdata, sizeof(char), sizeof(rdata), file);
fclose(file);
for (i = 0; i < sizeof(rdata); i++) {
printf("%x ", rdata[i]);
}
printf("\n");
return 0;
}
サイズが分からないバイナリデータを書き込んで読み込む方法
fseekで開いたfileに対して、SEEK_ENDでファイルの最後まで移動する。
char* rdata・・・配列のサイズを動的に生成したいので、配列を準備するんじゃなくて、配列にする予定のポインタ型の変数rdataを準備しておく。
fseek・・・SEEK_ENDでファイルの最後を取得
fseek(file, 0, SEEK_END)でfileポインタに対して、ファイルの最後までシークすることで、ファイルを最後まで読み込んであげていることになる。
そして、SEEK_ENDまできた状態でftellを使うことで、fileのサイズをバイト数で取得できる。
mallocでfileのsize分のメモリを確保する。
fseek(file, 0, SEEK_SET)でポインタをファイルの最初まで戻す。
fseekの第二引数は、第三引数からのオフセット値を指定する。第3引数からどれくらいズラすかを指定。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE* file;
int i, size;
char* rdata;
file = fopen("/home/katsuo/test.bin", "rb");
if (file == NULL) {
printf("do not open file.\n");
exit(1);
}
fseek(file, 0, SEEK_END);
size = ftell(file);
rdata = (char*)malloc(sizeof(char)*size);
fseek(file, 0, SEEK_SET);
fread(rdata, sizeof(char), size, file);
fclose(file);
for (i = 0; i < size; i++) {
printf("%x ", rdata[i]);
}
printf("\n");
free(rdata);
return 0;
}
c++らしいプログラミング
c++とcは互換性があり、c++はcの発展版なので、cのプログラムの書き方はc++でも使える。
ここからは、c++に独特の書き方でプログラミングを勉強していくので、それについてメモを書いていきます。
#include <iostream>
using namespace std;
int main() {
cout << "Hello Wordl!" << endl;
return 0;
}
cout は関数とは違って、ストリームという。
coutの後ろで「<<」記号を使うことで、コンソールに文字列などを流して表示するという操作をすることができる。
coutは関数ではなくて、オブジェクトで、標準入出力の出力を行うためのもの。
cin、coutはprintfやscanfのような関数とは違って、ストリームとよばれるものに対してデータを流したり、受け取ったりするもの
コンソールだけでなく、ファイルもストリームの扱う対象にできるので便利。
c++ではヘッダーファイルを読み込まなくてもok。
#include <iostream>は標準入出力をインクルードするという意味。
上記をインクルードすることで、c++でもともと用意されている標準名前空間であるnamespace stdがusing namespace std;と書くことで使えるようになる。この表記を書かないと、stream(cout)が使えず、cout書いてもエラーになる。
namespace 名前空間の意味。c++では大規模開発にも対応できるけど、大規模開発だと単語がダブってしまうことが発生してしまう。
例えば、「歩く」という単語が、味方が歩く、敵が歩く、誰々が歩くなど、「歩く」がいっぱいかぶってしまう。
名前空間を作ることで、同じ「歩く」の単語の意味合いをその他と分けることができる。
もっとわかりやすい例では、名前空間がないと、名前を変数名などに誰かが宣言してしまっていると、ダブってしまうことになり使えなくなる。違う名前を考えたりしなくてはならず、不便になる。
→名前空間を用意して、名前空間が違えば同じ名前を使用できるようにした。
using namespace std;をなしでcoutを使う方法
#include <iostream>
int main() {
std::cout << "HelloWorld!" << std::endl;
return 0;
}
std::coutとすることで、using namespace std;を宣言しなくてもcoutが使える。
std::coutで、名前空間stdの中でcoutを使うという意味。
(名前空間):: (変数名、クラス名など) のようにして使う。
c++では文字列のstringを使える
1 #include <iostream>
2 #include <string>
3
4 using namespace std;
5
6 int main() {
7 string s, t;
8 t = "入力文字は、";
9 cout << "文字列を入力してください:";
10 cin >> s;
11 cout << t + s << "です。" << endl;
12 return 0;
13 }
cでは文字列をchar型の配列にして扱っていたけど、c++では便利なstringを使える。stringはデータ型のように見えて勘違いしがちだけど、データ型ではくクラス。
stringを扱うときは、<string>をインクルードしないと使えないので注意。c++ではヘッダーファイルのインクルードに「.h」は要らないのでここも注意。
理由:
c言語のヘッダーファイルとc++のヘッダーを区別するためにこうなっている。※c++では単にヘッダーと呼ばれる。
c++ではヘッダーを読み込むことでクラスを使えるようになる。
c++のクラスとインスタンス
ファイルの構成は次のようにする
sourceフォルダ - main.cpp, sample.cpp
headerフォルダ - sample.h
まずはヘッダーファイルから
// sample.h
1 #ifndef _SAMPLE_H_
2 #define _SAMPLE_H_
3
4 // クラス宣言
5 class CSample
6 {
7 public:
8 void set(int num); // m_numに値を設定
9 int get(); // m_numの値を取得
10 private:
11 int m_num;
12 };
13
14 #endif // _SAMPLE_H_
クラスの宣言はヘッダーファイル内に書く。
クラス名はヘッダーファイルの名前に合わせるようにしたほうがよい。
1クラス1ヘッダーファイルにするのが基本。ファイルが増えちゃうけど、c++はこれが原則。
クラスの中で宣言した関数をメンバ関数、クラスの中で宣言した変数をメンバ変数とよぶ。
privateやpublicはアクセス修飾子という。
つぎは、ヘッダーファイルで宣言したクラスの中で使う関数の定義を書く。
// sample.cpp
1 #include "/home/amaenbo/c++/header/sample.h"
2
3 void CSample::set(int num)
4 {
5 m_num = num;
6 }
7
8 int CSample::get()
9 {
10 return m_num;
11 }
クラスのメンバ関数の中身を書くときは、クラス名とメンバ関数を:: で合体する(クラス名::メンバ関数)。
メンバ関数はメソッドと呼ばれることの方が多い。
main.cppにメイン関数を書く。
// main.cpp
1 #include <iostream>
2 #include "/home/amaenbo/c++/header/sample.h"
3
4 using namespace std;
5
6 int main()
7 {
8 CSample obj; // CSampleをインスタンス化
9 int num;
10
11 cout << "整数を入力してください:";
12 cin >> num;
13
14 obj.set(num); // CSampleのメンバ変数をセット
15 cout << obj.get() << endl; // メンバ変数の値を出力
16
17 return 0;
18 }
構造体と同じように、 C Sample obj; のように書くことで、CSampleクラスのインスタンスとしてobjを使えるようになる。
c言語の構造体は複数の変数をひとつにまとめるためのものだけど、クラスはメンバ変数のほかにメンバ関数(=メソッド)も入れられる。
生成したインスタンスはそれぞれ別物になる。車の設計図からできた実際の車がそれぞれ異なる状態であるのと同じこと。インスタンスによって、同じ構造で様々な状態を扱える。これがオブジェクト指向の便利なところ。
インスタンスは実体という意味で、クラスの実体を作ることをインスタンスの生成またはインスタンス化と呼ぶ。
クラスは使うまでは、単なる型にすぎないので、使うためには実体化(インスタンス化)する必要がある。
メンバ変数やメンバ関数にアクセスする(使う)ときは、「.」ドット演算子を使う。
インスタンスへのポインタを経由するときは、「->」アロー演算子を使う。
アクセス指定子
private、publicをアクセス指定子と呼ぶ。
public すべての範囲から呼び出しや読み出しができる。
private 同一のクラスまたはインスタンス内からのみアクセスできる。
protected 同一クラスまたはインスタンス、サブクラスとそのインスタンスからのみアクセスできる。
動作を確認。
ファイル構成
・ヘッダーファイルにクラス宣言: sample.h
・クラス内のメンバ関数の定義: sample.cpp
・クラスのインスタンスを使ったメインの処理: main.cpp
sample.h
クラス宣言を行う
ここでメンバ変数とメンバ関数のアクセス指定子を使用。
privateとpublic
1 #ifndef _SAMPLE_H_
2 #define _SAMPLE_H_
3
4 // クラス宣言
5 class Sample
6 {
7 public:
8 int a;
9 private:
10 int b;
11 public:
12 void func1();
13 private:
14 void func2();
15 };
16
17 #endif // _SAMPLE_H_
sample.cpp
クラスのメンバ関数の定義
1 #include "/home/amaenbo/c++/header/sample.h"
2 #include <iostream>
3
4 using namespace std;
5
6 void Sample::func1()
7 {
8 cout << "func1" << endl;
9 a = 1;
10 b = 1;
11 func2();
12 }
13
14 void Sample::func2()
15 {
16 a = 2;
17 b = 2;
18 cout << "a=" << a << "," << "b=" << b << endl;
19 }
main.cpp
main関数内でprivateにしているクラスのメンバ変数とメンバ関数にアクセスしようとしたらエラーとなる(下記コードのコメント部分)。
1 #include <iostream>
2 #include "/home/amaenbo/c++/header/sample.h"
3
4 using namespace std;
5
6 int main()
7 {
8 Sample s;
9 s.a = 1;
10 // s.b = 2;
11 s.func1();
12 // s.func2();
13 return 0;
14 }
publicの場合は、どこからでもアクセスできる。
カプセル化
オブジェクト指向の原則として、メンバ変数はprivateにして直接アクセスできないように隠蔽する。これをカプセル化という。
privateのメンバ変数の値を引っ張ったり値を変更するためのメンバ変数が必要になってくる。これをアクセスメソッドという。
アクセスメソッドで、プライベートなメンバ変数の値を変えるための関数をセッターといい、値を取得するための関数をゲッターという。
sample.h
1 class CSample
2 {
3 public:
4 void setNum(int num);
5 int getNum();
6 private:
7 int m_num;
8 };
ここでは、メンバ変数であるm_numをprivateにすることで、外部からアクセスできないようにしている。
この変数にアクセスするために用意したのがsetterとしてsetNum関数、getterとしてgetNum関数。setterとgetterはこのようにメンバ変数の名前を入れることでわかりやすくする。
sample.cpp
setterとgetterの定義
1 #include "/home/amaenbo/c++/header/sample.h"
2
3 void CSample::setNum(int num)
4 {
5 m_num = num;
6 }
7
8 int CSample::getNum()
9 {
10 return m_num;
11 }
main.cpp
setterでprivateのメンバ変数m_numに値をセットし、getterでm_numの値を取得してcoutで表示。
1 #include <iostream>
2 #include "/home/amaenbo/c++/header/sample.h"
3
4 using namespace std;
5
6 int main()
7 {
8 CSample s;
9 s.setNum(5);
10 cout << s.getNum() << endl;
11 return 0;
12 }
変数の性質によって、setterだけを用意したり、getterだけを用意したりといった運用をする。
stringクラス
stringは型ではなくてクラス。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s;
s = "this is a";
s.append("pen");
cout << s << endl;
cout << "文字列の長さ:" << s.length() << endl;
printf("char*:%s\n", s.c_str());
return 0;
}
string s; はstringクラスのインスタンス s を生成している。
クラスなので、クラスに定義されているメソッドが使える。
append()は文字列を追加する関数。
length()は文字列の長さを取得する関数
c_str()はstringをchar型の文字列に変換する関数。
stringとc言語のchar型の配列は互換性がないので、この関数を使うことでc言語のchar型の配列に型を変換できる。
stringクラスもユーザー定義クラスと同じように、メンバ変数とメンバ関数が存在する。
メンバ変数は、オブジェクト指向の原則として普通はカプセル化によって隠蔽されているのでユーザーは利用できない。
コンストラクタとデストラクタ
main.cpp
CCarクラスのインスタンスを生成しメンバ関数を実行。
1 #include "car.h"
2
3 int main()
4 {
5 CCar c;
6 c.supply(10);
7 c.move();
8 c.move();
9 return 0;
10 }
ヘッダーファイルcar.h
1 #ifndef _CAR_H_
2 #define _CAR_H_
3
4 // 自動車クラス
5 class CCar {
6 public:
7 // コンストラクタ
8 CCar();
9 // デストラクタ
10 ~CCar();
11 // 移動メソッド
12 void move();
13 // 燃料補給メソッド
14 void supply(int fuel);
15 private:
16 int m_fuel; // 燃料
17 int m_migration; // 移動距離
18
19 };
20 #endif // _CAR_H_
各メンバ関数、変数の実装部分 car.cpp
1 #include "car.h"
2 #include <iostream>
3
4 using namespace std;
5
6 // コンストラクタ
7 CCar::CCar() : m_fuel(0), m_migration(0) {
8 cout << "CCarオブジェクト生成" << endl;
9 }
10 // デストラクタ
11 CCar::~CCar() {
12 cout << "CCarオブジェクト破棄" << endl;
13 }
14 void CCar::move() {
15 // 燃料があるなら移動
16 if (m_fuel > 0) {
17 m_migration++; // 距離移動
18 m_fuel--; // 燃料消費
19 }
20 cout << "移動距離:" << m_migration << endl;
21 cout << "燃料" << m_fuel << endl;
22 }
23 // 燃料補給メソッド
24 void CCar::supply(int fuel) {
25 if (fuel >= 0) {
26 m_fuel += fuel; // 燃料補給
27 }
以上を実行すると、下記のように出力される。
CCarオブジェクト生成
燃料10
移動距離:1
燃料9
移動距離:2
燃料8
CCarオブジェクト破棄
コンストラクタ実行、main.cppの内容を実行、デストラクタ実行の順に出力されていることがわかる。
main.cppでCCar c;によってインスタンスが生成されるときにコンストラクタが実行され、return 0;が実行されインスタンスが破棄されるときにデストラクタが実行されている。
まとめ
コンストラクタとデストラクタは特殊なメンバ関数。
コンストラクタを使うと、クラスのインスタンスが生成されるときに自動的に1回だけコンストラクタが呼ばれる。
コンストラクタの名前はクラスの名前と同じ。戻り値がない。
初期化方法;
クラス名::クラス名(): メンバ変数1(初期値1),メンバ変数2(初期値2),...
上記のメンバ変数の並び順は、ヘッダーファイルでの定義順に書くことが推奨されている。
デストラクタを使うと、クラスのインスタンスが破棄(解放)されるときに、自動的に1回だけデストラクタが呼ばれる。
開放のタイミングは、インスタンスのスコープを抜けるとき。
→関数内でインスタンス化した場合、その関数を抜けるタイミングで解放され、デストラクタが呼ばれる。
デストラクタはクラス名の先頭にチルダ(~)を付ける。
デストラクタは終了処理を行うのが一般的。
終了処理とは、動的に確保されたメモリの解放や、開いたままのファイルを閉じたりすること。
デストラクタの処理の実行を終えた時点で、そのクラスのインスタンスはメモリから解放されてなくなる。
コンストラクタとデストラクタは省略できる
必要がないのならコードとしてコンストラクタやデストラクタは書く必要はないけど、省略した場合でもコンパイラによって自動的にコンストラクタとデストラクタは生成される仕組みになっている。
newとdelete
newとdeleteを使うことでインスタンスを好きなタイミングで生成し削除できる。main.cppだけ下記のように書き換える。
main.cpp
1 #include "/home/amaenbo/c++/header/car.h"
2 #include <iostream>
3
4 using namespace std;
5
6 int main()
7 {
8 CCar* pC = 0;
9 pC = new CCar(); // インスタンス生成
10 pC->supply(10);
11 pC->move();
12 pC->move();
13 delete pC; // インスタンスの消去
14 cout << "インスタンスの消去終了" << endl;
15 return 0;
16 }
CCarクラスのポインタをpCとして作成。pCがCCarクラスになりすましている状態。ポインタなので初期化しないといけないので0で初期化。
new コンストラクタ名();でインスタンスを生成
new CCar();でCCarクラスのインスタンスを生成
生成したインスタンスをpCポインタ変数に代入。
構造体のポインタと同じように、クラスのポインタを使う場合もアロー演算子でメンバにアクセスする。
newで作成したインスタンスは勝手に削除されないので、じぶんで消去。
※勝手にインスタンスを消去してくれる仕組みをガーベジコレクターという。c#など他の言語では用意されてるけど、c++については用意されていないので、newで作成したインスタンスは自分で削除する必要がある。
削除を行うのがdelete。
delete インスタンス名;で削除
コードを実行結果。
CCarオブジェクト生成
燃料10
移動距離:1
燃料9
移動距離:2
燃料8
CCarオブジェクト破棄
インスタンスの消去終了
newでコンストラクタのインスタンス生成され、コンストラクタが実行され、「CCarオブジェクト生成」と出力。
deleteでインスタンスを削除したらデストラクタが呼ばれ、「CCarオブジェクト破棄」と出力。
インスタンス削除後に書いているcoutが実行され、「インスタンスの消去終了」と出力。
以上から、newとdeleteを使うことで、好きなタイミングでインスタンスの生成と消去ができていることがわかる。
newとdeleteはどちらか一方だけにしないこと。deleteせずにnewだけにすると確保したメモリをどんどん圧迫してプログラムが遅くなったり、落ちたりする。
逆にnewせずにdeleteだけすると異常終了する。
c++でmallocとfreeを使わない理由は、mallocとfreeではコンストラクタとデストラクタが呼び出すことができないため。なので、c++ではメモリの生成と消去はnewとdeleteが使われる。
newとdeleteはデータ型にも使える
一般のデータ型にもnewとdeleteが使える。
1 #include <iostream>
2
3 using namespace std;
4
5 int main() {
6 int *p = 0;
7 p = new int(); // int型の領域を動的確保
8 *p = 123;
9 cout << *p << endl;
10 delete p; // 動的に確保した領域を開放
11 return 0;
12 }
ポインタ型の変数pを定義し初期化
p = new int();とすることでpのアドレスにint型の領域を確保。
*p = 123;によってポインタ型の変数pに値を代入
*pによってポインタ変数pの値を出力
newで確保したint型のアドレス領域を開放
配列にも使用可;newとdelete
配列でnewでメモリを確保した時の注意として、deleteするときは配列であることを明示するために、[]をつけることを忘れないようにする。
1 #include <iostream>
2
3 using namespace std;
4
5 int main() {
6 int *p = 0;
7 int i;
8 p = new int[10]; // int型10こ分のメモリ領域を動的に確保
9 for (i = 0; i < 10; i++) {
10 p[i] = i;
11 cout << p[i] << endl;
12 }
13 delete[] p; // 動的に確保したメモリ領域を開放
14 return 0;
15 }
静的メンバ
クラス全体に共通する項目でインスタンスの生成を必要としないメンバ変数、メンバ関数を静的メンバという。静的メンバを準備することで、別途でクラス以外で準備するよりも簡単になる。例えば、車の車種名など。
逆に、インスタンスの生成を必要とするメンバをインスタンスメンバという。
main.cpp
メインの処理の流れ
#include "/home/amaenbo/c++/header/rat.h"
#include <iostream>
using namespace std;
int main() {
CRat *r1, *r2, *r3;
r1 = new CRat(); // 1匹めのネズミを生成
r1->squeak();
CRat::showNum(); // ネズミの数を表示
r2 = new CRat(); // 2匹めのネズミを生成
r3 = new CRat(); // 3匹めのネズミを生成
r2->squeak();
r3->squeak();
delete r1; // 1匹めのネズミを消去
delete r2; // 2匹めのネズミを消去
CRat::showNum(); // ネズミの数を表示
delete r3; // 3匹めのネズミを消去
CRat::showNum(); // ネズミの数を表示
return 0;
}
静的なメンバ関数として、ネズミの数を表示するshowNum()関数を準備。
rat.h
クラスの定義
#ifndef _RAT_H_
#define _RAT_H_
class CRat {
public:
// コンストラクタ
CRat();
// デストラクタ
~CRat();
// ネズミの数の出力
static void showNum();
// ネズミが鳴く
void squeak();
private:
// ネズミの番号
int m_id;
// ネズミの数
static int m_count;
};
#endif // _RAT_H_
静的メンバ関数としたい関数をstatic宣言することで、静的なメンバ関数にできる。
staticにしていない状態だと、生成したインスタンスのそれぞれについてshowNum()が生成されるので、例えばインスタンスAとインスタンスBのshowNum()は別物となる。
→インスタンスAとインスタンスBなど、複数のインスタンスで共通して1個のshowNum()を使いたい場合にstaticを付ける。
インスタンスじゃなく普通の関数なので、インスタンスとは関係なく使うことができる。
使い方は、インスタンスのようにアロー演算子じゃなく、CRat::showNum();のように、クラス名::関数名();とし、普通のメンバ関数のようにして呼び出して使う。
注意:
同じクラス内で定義されているにもかかわらず、静的メンバ関数はインスタンスとは切り離されているので、静的メンバ関数の中ではインスタンスの関数や変数は使えない。使うとエラーになる。逆に、インスタンスの中で静的メンバ関数は使える。
イメージとしては、インスタンスの変数m_idはインスタンスによって異なる値をもっている。静的メンバ関数の中で、インスタンスの変数m_idを呼び出そうとすると、静的メンバ関数内ではインスタンスの変数はスコープ外になるので、呼び出せない、定義されてないものとなる感じかも。
rat.cpp
クラスの実装
#include "/home/amaenbo/c++/header/rat.h"
#include <iostream>
using namespace std;
// ネズミの数の初期値を0に設定
int CRat::m_count = 0;
// コンストラクタ
CRat::CRat() : m_id(0) {
m_id = m_count; // ネズミの数をidとする。
m_count++; // ネズミの数を1つ増やす
}
// デストラクタ
CRat::~CRat() {
cout << "ネズミ:" << m_id << "消去" << endl;
m_count--; // ネズミの数を1つ減らす
}
// ネズミの数の出力
void CRat::showNum() {
cout << "現在のネズミの数は、" << m_count << "匹です。" << endl;
}
// ネズミが鳴く
void CRat::squeak() {
cout << m_id << ":" << "チューチュー" << endl;
}
静的メンバ変数は、呼び出したときに最初の1回だけ実行されるので、処理の最初の方に書いて、初期値を設定しておく。
int CRat::m_count = 0;
静的メンバ関数や変数はクラス全体の状態を管理したいときに便利なので、そういったときに使う。
継承
あるクラスがあって、同じような機能のクラスを複数つくるとき、またいちいち定義していたのでは効率が悪い。そこで、クラスの継承を利用すると、クラスの同じ機能については、いちいち定義せずに使えて、更に追加で機能を追加できたりするので便利。
→あるクラスの性質を受け継いだクラスを作成できる。
親クラスを一つしか持たない継承を単一継承といい、
親クラスを複数持つ継承を多重継承という。
継承の方法
単一継承の書き方
class <子クラス名> : public <親クラス名>
多重継承の書き方
class <子クラス名> : public <親クラス名A>,public <親クラス名B>...
main.cpp
#include "/home/amaenbo/c++/header/car.h"
#include "/home/amaenbo/c++/header/ambulance.h"
int main() {
CCar c;
c.supply(10); // 燃料補給
c.move(); // 移動
c.move(); // 移動
CAmbulance a;
a.supply(10);
a.move();
a.savePeople();
return 0;
}
CCarクラスを継承したクラスがCAmbulanceクラス。
このとき、CCarクラスのことをsuper classまたは親クラスという。
CAmbulanceにことをsub classまたは子クラスという。
スーパークラスを継承したサブクラスでスーパークラスのpublicのメンバを使う場合は、サブクラスの中で定義しなくてもそのまま使えるので、サブクラスで新規で使うメンバだけ定義を書けばよい。
car.h
CCarクラスのヘッダーファイルcar.h
#ifndef _CAR_H_
#define _CAR_H_
// 自動車クラス
class CCar {
public:
// コンストラクタ
CCar();
// デストラクタ
virtual ~CCar();
// 移動メソッド
void move();
// 燃料補給メソッド
void supply(int fuel);
private:
int m_fuel; // 燃料
int m_mgration; // 移動距離
};
#endif // _CAR_H_
継承を使うときは、デストラクタにvirtualを付けること。
virtualは仮想関数。
ambulance.h
CAmbulanceクラスのヘッダーファイルambulance.h
#ifndef _AMBULANCE_H_
#define _AMBULANCE_H_
#include "car.h"
class CAmbulance : public CCar {
public:
// コンストラクタ
CAmbulance();
// デストラクタ
virtual ~CAmbulance();
// 救急救命活動
void savePeople();
private:
int m_number;
};
#endif // _AMBULANCE_H_
CCarクラスをCAmbulanceクラスに継承させるので、car.hのincludeが必要。
継承していても、コンストラクタとデストラクタもCAmbulanceクラスで定義が必要。
car.cpp
#include "/home/amaenbo/c++/header/car.h"
#include <iostream>
using namespace std;
// コンストラクタ
CCar::CCar() : m_fuel(0), m_migration(0) {
cout << "CCarオブジェクト生成" << endl;
}
// デストラクタ
CCar::~CCar() {
cout << "CCarオブジェクト破棄" << endl;
}
void CCar::move() {
// 燃料があるなら移動
if (m_fuel >= 0) {
m_migration++;
}
cout << "移動距離:" << m_migration << endl;
cout << "燃料" << m_fuel << endl;
}
void CCar::supply(int fuel) {
if (fuel >= 0) {
m_fuel += fuel;
}
}
ambulance.cpp
#include "/home/amaenbo/c++/header/ambulance.h"
#include <iostream>
using namespace std;
// コンストラクタ
CAmbulance::CAmbulance() : m_number(119) {
cout << "CAmbulanceオブジェクト生成" << endl;
}
// デストラクタ
CAmbulance::~CAmbulance() {
cout << "CAmbulanceオブジェクト破棄" << endl;
}
// 救急救命活動
void CAmbulance::savePeople() {
cout << "救急救命活動" << endl << "呼び出しは" << m_number << "番号" << endl;
}
実行結果
CCarオブジェクト生成
移動距離:1
燃料9
移動距離:2
燃料8
CCarオブジェクト生成
CAmbulanceオブジェクト生成
移動距離:1
燃料9
救急救命活動
呼び出しは119番号
CAmbulanceオブジェクト破棄
CCarオブジェクト破棄
CCarオブジェクト破棄
コンストラクタとデストラクタの挙動に注目。
CAmbulanceクラスはCCarクラスを継承しているので、CAmbulanceクラスのインスタンスが生成されると、CCarクラスのコンストラクタも実行される。その直後にCAmbulanceクラスのコンストラクタが実行されている。
アクセス修飾子protected
protectedをメンバにつけると、サブクラスでも使えるという意味。
親クラスでprotectedがついたメンバはサブクラスではpublicと同じとなり使えるけど、サブクラスの外からはprivateと同じでアクセスできない。protectedはpublicとprivateの中間的な存在。
サブクラスのみにアクセスを許可するメンバにはprotectedをつける。
main.cpp
1 #include "/home/amaenbo/c++/header/position2d.h"
2 #include <iostream>
3
4 using namespace std;
5
6 int main(int argc, char** args) {
7 Position2D p;
8 p.setValue(1, 1);
9 p.move(2, 3);
10 cout << "p:(" << p.getX() << "," << p.getY() << ")" << endl;
11 p.resetPosition();
12 cout << "p:(" << p.getX() << "," << p.getY() << ")" << endl;
13 return 0;
14 }
vector2d.h
1 #ifndef _VECTOR2D_H_
2 #define _VECTOR2D_H_
3
4 // 2次元ベクトルクラス
5 class Vector2D {
6 protected:
7 int m_x;
8 int m_y;
9 public:
10 // コンストラクタ
11 Vector2D();
12 // 値の設定
13 void setValue(int x, int y);
14 // x座標の取得
15 int getX();
16 // y座標の取得
17 int getY();
18 protected:
19 // 初期化
20 void init();
21 };
22 #endif // _VECTOR2D_H_
potition2d.h
1 #ifndef _POSITION2D_H_
2 #define _POSITION2D_H_
3
4 #include "/home/amaenbo/c++/header/vector2d.h"
5
6 class Position2D : public Vector2D {
7 public:
8 // 位置のリセット
9 void resetPosition();
10 // 移動
11 void move(int dx, int dy);
12 };
13
14 #endif // _POSITION2D_H_
position2d.cpp
1 #include "/home/amaenbo/c++/header/position2d.h"
2
3 void Position2D::resetPosition(){
4 init();
5 }
6 void Position2D::move(int dx, int dy){
7 m_x += dx;
8 m_y += dy;
9 }
vector2d.cpp
1 #include "/home/amaenbo/c++/header/vector2d.h"
2
3 // コンストラクタ
4 Vector2D::Vector2D(){
5 init();
6 }
7 // 値の設定
8 void Vector2D::setValue(int x, int y){
9 m_x = x; m_y = y;
10 }
11 // x座標の取得
12 int Vector2D::getX(){
13 return m_x;
14 }
15 // y座標の取得
16 int Vector2D::getY(){
17 return m_y;
18 }
19 // 値の初期化
20 void Vector2D::init(){
21 m_x = 0; m_y = 0;
22 }
ポリモーフィズム
多様性や多態性という意味。
オーバーロード、オーバーライドを総称してポリモーフィズムと呼ぶ。
ポリモーフィズムを使うことで、メンバ関数の名前を増やすことなくシンプルにでき記述ミスも減らせる。
オーバーロードとは、同じクラスの中に同じ名前のメンバ関数を複数持っていていいこと。
→どうやって同じ関数なのに区別をするかというと、引数とか戻り値の型によって区別をする。
メンバ関数だけでなく、コンストラクタもオーバーロードできる。
引数のないコンストラクタをデフォルトコンストラクタと呼ぶ。
デフォルトコンストラクタという単語は、他の言語では意味合いが違ってくる。
main.cpp
#include "/home/amaenbo/c++/header/calc.h"
#include <iostream>
using namespace std;
int main() {
CCalc *pC1, *pC2;
pC1 = new CCalc(); // デフォルトコンストラクタ
pC2 = new CCalc(1, 2); // コンストラクタ(引数あり)
cout << 3 << " + " << 4 << " = " << pC1->add(3, 4) << endl;
cout << pC2->getA() << " + " << pC2->getB() << " = " << pC2->add() << endl;
delete pC1;
delete pC2;
return 0;
}
calc.h
#ifndef _CALC_H_
#define _CALC_H_
class CCalc {
private:
int m_a, m_b;
public:
// デフォルトコンストラクタ
CCalc();
// コンストラクタ(引数付き)
CCalc(int a, int b);
// 足し算処理その1
int add();
// 足し算処理その2
int add(int a, int b);
// 値の設定
void setValue(int a, int b);
// 値の取得(m_a)
int getA();
// 値の取得(m_b)
int getB();
};
#endif // _CALC_H_
calc.cpp
#include "/home/amaenbo/c++/header/calc.h"
// デフォルトコンストラクタ
CCalc::CCalc() : m_a(0), m_b(0) {
}
// コンストラクタ(引数付き)
CCalc::CCalc(int a, int b) : m_a(a), m_b(b) {
}
// 足し算処理その1
int CCalc::add() {
return m_a + m_b;
}
// 足し算処理その2
int CCalc::add() {
return a + b;
}
// 値の設定
void CCalc::setValue(int a, int b) {
m_a = a; m_b = b;
}
// 値の取得(m_a)
int CCalc::getA() {
return m_a;
}
// 値の取得(m_b)
int CCalc::getB() {
return m_b;
}
この例では、関数のオーバーロード、デフォルトコンストラクタと引数付きのコンストラクタを示している。
デフォルトコンストラクタの注意点
デフォルトコンストラクタ、引数付きのコンストラクタを使う場合、
定義して使わないとエラーになるので注意。
例)引数付きのコンストラクタを定義して使っていて、デフォルトコンストラクタは定義せずに使う。この場合、デフォルトコンストラクタを定義していないのでエラーとなる。
どちらか一つのコンストラクタを使っていてもう1つのコンストラクタを使うときは、ちゃんと定義しておかないといけない。
オーバーライド
スーパークラスを継承したサブクラスでスーパークラスと全く同じメンバを使える。使ったとき、サブクラスのメンバの方が実行されるので、スーパークラスが上書きされる結果となる。これをオーバーラードという。
jij
この記事が気に入ったらサポートをしてみませんか?