そろそろC++やるぞパート24 演算子 「sizeof」「後置++, --」
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
- 今回は「sizeof演算子」と「後置インクリメント/デクリメント」を行っていきます。
sizeof 演算子 sizeof
構文
sizeof unary-expression sizeof ( type-name )
char
のサイズに対する相対的になサイズが返されます。char
のサイズは基本的に1となっています。
演算結果としてsize_t
が返されます。
int i = 100; size_t si = sizeof(i); cout << si << endl; long l = 100; size_t sl = sizeof(l); cout << sl << endl;
Output
4 8
sizeof
を重ねて、こういうこともできます。
int i = 100; size_t si = sizeof(i); size_t s = sizeof(sizeof(sizeof(sizeof(sizeof(si)))));
配列の要素数の取得
sizeof array / sizeof array[0]
sizeof array は配列に使用しているメモリ全て、 sizeof array[0] は配列の型の大きさを表します。
int ar[2] = {13, 17}; size_t arSize = sizeof(ar) / sizeof(ar[0]); cout << arSize << endl;
Output
2
前置インクリメント/デクリメント演算子: ++
. --
構文
++ unary-expression -- unary-expression
後置インクリメント/デクリメントと同様に、1単位分増減させます。
int i = 100; cout << i << endl; ++i; cout << i << endl; --i; cout << i << endl;
Output
100 101 100
前置と後置の違い
同じ「1単位分増減させる」演算子ですが、それぞれ定義されていることには理由があります。それが、評価と使用の順番です。それぞれこのようになっています。
前置 インクリメント/デクリメント
- 評価
- 使用
後置インクリメント/デクリメント
- 使用
- 評価
何を意味しているのかソースコードで見てみます。
int i = 100; int j = i++; cout << i << endl; cout << j << endl; int i2 = 100; int j2 = ++i2; cout << i2 << endl; cout << j2 << endl;
Output
101 100 101 101
int j = i++;
のところは先にint j = i;
として代入されて、使用された後にi++
が評価されて101に変わります。
一方で、int j2 = ++i2;
は先に++i2
が評価されて101になって、int j2 = 101;
となり、j2
は101になります
アセンブリ
前置と後置でコンパイルした結果を見てみます。以下の2つのソースコードで試します。
main.cpp
#include <iostream> using namespace std; int main() { int i = 100; int j = i++; }
main2.cpp
#include <iostream> using namespace std; int main() { int i = 100; int j = ++i; }
コンパイルには-S
をつけてアセンブリに、ファイルの比較にはdiff
コマンドを使用します。
$ diff main.cpp main2.cpp 7c7 < int j = i++; --- > int j = ++i; $ g++ -S main.cpp -o a.s $ g++ -S main2.cpp -o b.s $ diff a.s b.s 13,14c13,14 < add w9, w8, #1 < str w9, [sp, #12] --- > add w8, w8, #1 > str w8, [sp, #12]
アセンブリにした時に差分は2行現れました。これだけでは分かりにくいので、前後を少し取り出してみます。
a.s
mov w8, #100 str w8, [sp, #12] ldr w8, [sp, #12] add w9, w8, #1 str w9, [sp, #12] str w8, [sp, #8]
- 100を
w8
レジスタに入れる w8
レジスタの内容を#12
に保存#12
の内容をw8
レジスタに読み込みw8
と 1 を足した値をw9
に入れるw9
レジスタの内容を#12
に保存w8
レジスタの内容を#8
に保存
b.s
mov w8, #100 str w8, [sp, #12] ldr w8, [sp, #12] add w8, w8, #1 str w8, [sp, #12] str w8, [sp, #8]
- 100を
w8
レジスタに入れる w8
レジスタの内容を#12
に保存#12
の内容をw8
レジスタに読み込みw8
と 1 を足した値をw8
に入れるw8
レジスタの内容を#12
に保存#8
の内容をw8
レジスタに読み込み。
後置インクリメントの場合は、w9
レジスタに値に計算した値を一度入れています。#12
にはインクリメントされた101、#8
にはインクリメント前の100が入っている状態になります。
このように少し違いがあるので使用する際には注意が必要です。
参考リスト
感想
インクリメントとデクリメントが間違えずに使わないとエラーが出そうです。sizeof演算子はあまり使わないと思います。配列の要素数もこれを用いて求めるよりは初期化した値を取っておく方がよいかなと思いました。
次回も引き続き演算子を行おうと思います。
そろそろC++やるぞパート23 演算子 「キャスト演算子」
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
- 今回は「4種類のキャスト演算子」を行っていきます。
const_cast 演算子
構文
const_cast <type-id> (expression)
const_cast
は型変換をするのでなく、const
やvolatile
の追加や削除を行います。
const int *a = new int(13); int *b = const_cast<int *>(a); volatile int *c = new int(123); int *d = const_cast<int *>(c);
継承された型について
class Base { }; class Derived : public Base { };
const_cast
の型指定に親の型を書いてキャストすることはできません。キャストしてから親の型の変数に代入します。
const Derived *a = new Derived{}; Derived *b1 = const_cast<Derived *>(a); // OK Base *b2 = const_cast<Derived *>(a); // OK Base *b3 = const_cast<Base *>(a); // Error
これは、あまり使わないと思います。
dynamic_cast 演算子
構文
dynamic_cast < type-id > ( expression )
小クラスから親クラスへキャストする時に使用します。正確にはポリモーフィッククラスにキャスト可能だそうです。
Derived *a = new Derived(); Derived *b = dynamic_cast<Derived *>(a); Base *c = dynamic_cast<Base *>(a); Derived *d = dynamic_cast<Derived *>(c); // Error
これは、使いそうです。
reinterpret_cast 演算子
構文
reinterpret_cast < type-id > ( expression )
他のポイント型に、強制的に型変換します。
char
やint
、long
など実質整数型の時に相性が良く使えます。
char *e = new char('a'); int *f = reinterpret_cast<int *>(e); // OK
これは必要に応じて使いそうです。
static_cast 演算子
構文
static_cast <type-id> ( expression )
暗黙の型変換を明示的に行う時に使います。省略できる場合がほとんどです。
long a = 100; int b = a; int c = static_cast<int>(a);
あまり明示的にしたい時はないような気がしますが、必要になったら使います。
参考リスト
- const_cast 演算子 | Microsoft Learn
- const_cast 演算子 (C++ のみ)
- dynamic_cast 演算子 | Microsoft Learn
- dynamic_cast 演算子 (C++ のみ)
- C++と 4 つのキャスト演算 | yunabe.jp
- static_cast 演算子 | Microsoft Learn
感想
今回も演算子の続きを行いました。ポインタのキャストや、明示的なキャストと4種類やりましたが、dynamic_cast
をよく使うのかなという印象です。
次回も、演算子の続きを行なっていきます。
そろそろC++やるぞパート22 演算子 「( )」「++」「--」「[typeid」
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
- 今回は演算子
( )
,++
,--
,typeid
を行っていきます。
関数呼び出し演算子 ()
構文
postfix-expression: postfix-expression ( argument-expression-list opt )
引数は、式のコンマ区切りの一覧であるargument-expression-list
からなります。argument-expression-list
は空の場合もあります。
引数式は、不定の順序で評価されます。
とは書いてありますが、こちらのコードで100回ループしても引数順に呼び出されました。
int Write(string text, int val) { cout << text << endl; return val; } void ReceiveFunc(int a, int b, int c) { cout << a << ":" << b << ":" << c << endl; } int main() { for (int i = 0; i < 100; i++) { ReceiveFunc(Write("one", 1), Write("two", 2), Write("three", 3)); } }
- 値を返す関数
int AAA() { int a = 123; return a; }
- ポインタを返す関数
int *BBB() { int *b = new int(123); return b; }
- 参照を返す関数
int &CCC() { int *c = new int(123); int &d = *c; return d; }
後置 インクリメント/デクリメント 演算子 ++
, --
構文
postfix-expression ++ postfix-expression --
C++には前置インクリメントと後置インクリメントが共に実装されています。
後置インクリメントは、オペランドの値が適切な型の 1 単位分、増加させます。
int
型やlong
型はインクリメントすると1増えます。
int i = 12; i++; cout << i << endl; long l = 34; l++; cout << l << endl;
Output
13 35
ポインタや配列の時に増え方が、1ではなく1単位分になります。
int *a = new int[10]; for (int i = 0; i < 10; i++) { a[i] = 3 * i; } int *ai = &a[5]; cout << ai << endl; cout << *ai << endl; ai++; cout << ai << endl; cout << *ai << endl;
Output
0x121e06904 15 0x121e06908 18
intポインタの場合は1単位分(4)インクメントされていることがわかります。
これがshort
になると、1単位分の2がインクリメントされるようになります。
short *a = new short[10]; for (int i = 0; i < 10; i++) { a[i] = 100 + i; } short *ai = &a[5]; cout << ai << endl; cout << *ai << endl; ai++; cout << ai << endl; cout << *ai << endl;
Output
0x121e0685a 105 0x121e0685c 106
typeid演算子 typeid
構文
typeid(type-id) typeid(expression)
typeidの結果はconst type_info&
です。
int a = 123; cout << typeid(a).name() << endl; string s = "Hello"; cout << typeid(s).name() << endl; char c = 'a'; cout << typeid(c).name() << endl; HogeClass hogeClass; cout << typeid(hogeClass).name() << endl;
Output
i NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE c 9HogeClass
このように、直接の型名ではなく、C++側の出力しやすい形で出力されているように思います。
typeid
はデバッグ出力用に使うのかなと思います。
参考リスト
- 関数呼び出し演算子: () | Microsoft Learn
- typeid 演算子 | Microsoft Learn
- 後置インクリメント演算子と後置デクリメント演算子: ++ および -- | Microsoft Learn
感想
typeidはあまり使わないかな、という感じです。 関数呼び出し演算子やインクメント/デクリメントも他の言語と同じような一般的な使い方しかしなさそうです。
次回も引き続き、演算子を行います。
そろそろC++やるぞパート21 演算子 「::」「.」「->」「[ ]」
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
スコープの識別や、曖昧さを解決するために使われます。
例えば namespace ごとに同名の変数が定義されていた時に、{namespace名}::{変数名}
とすることでアクセス先を明示的に変えることができます。
namespace NamespaceA { int x; } namespace NamespaceB { int x; } int main() { NamespaceA::x = 1; NamespaceB::x = 2; int x = 3; cout << NamespaceA::x << endl; cout << NamespaceB::x << endl; cout << x << endl; return 0; }
Output
1 2 3
グローバルスペースは::x
のようにアクセスすることができます。
int x; int main(){ int x = 1; ::x = 1; }
namespaceはusingディレクティブによって、変数もアクセスできるようになります。
namespace NamespaceA { int x; } namespace NamespaceB { using namespace NamespaceA; } int main() { NamespaceB::x = 4; // NamespaceBはNamespaceAのxを表しているので、4を出力する cout << NamespaceB::x << endl; }
静的メンバの参照
::
を用いて静的メンバ(staticメンバ)にアクセスすることができます。
class MyClass { public: static int X; }; int main() { MyClass::X = 34; }
メンバアクセス修飾子.
, ->
これらは、struct
やunion
、class
のメンバーを参照するために使用します。
- ポインタではない場合は
.
- ポインタの場合は
->
を使用します。
class MyClass { public: MyClass(int xVal) { x = xVal; }; int x; }; int main() { MyClass mc = MyClass(32); cout << mc.x << endl; MyClass *mcPtr = new MyClass(42); cout << mcPtr->x << endl; }
添字演算子 [ ]
添字演算子[ ]
は、[配列](の要素指定とアクセスに用います。
int intAr[5] = {3, 4, 5, 6, 7}; cout << intAr[3] << endl;
class MyIntArray { public: MyIntArray(int size) { intArray = new int[size]; for (int i = 0; i < size; i++) { intArray[i] = 11 * i; } }; inline int operator[](std::size_t index) const { cout << "aain" << endl; return intArray[index % size]; } inline int &operator[](size_t index) { cout << "iiiiin" << endl; return intArray[index % size]; } private: int size; int *intArray; };
参考リスト
- スコープ解決演算子: `::` | Microsoft Learn
- スコープ解決演算子 :: (C++ のみ)
- メンバー アクセス演算子: '.' および '->' | Microsoft Learn
感想
スコープ解決演算子(::
)だけ、初めての内容でしたが、他は復習みたいな形でした!前にやったことが出てくると楽しくなってきます。
次回も演算子の続きを行います。
そろそろC++やるぞパート20 演算子
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
演算子
加減乗除(+
-
*
/
)や配列の添字演算子([ ]
)など、いろいろな演算をするための記号や文字です。
C++の演算子は、C言語の全ての演算子にいくつかの新しい演算子が加えられてできています。
優先順位と結合規則
優先順位
複数の演算子が含む式で、どの演算子を先に演算するかの規則
結合規則
同じ優先順位の演算子を含む式ではどちらを先に演算するかの規則
例えばこの文の場合は1 + 2 * 3 / 2
は式になります。
int a = 1 + 2 * 3 / 2;
優先順位は、乗算(*
)と除算(/
)が同じで、その下に加算(+
)があります。
`*` = `/` > `+`
*
と/
の順番は優先順位が同じなので、結合規則で決まります。これらの結合規則は左結合なので、*
を先に計算します。
当たり前のようにやっていますが、このような仕組みになっています。
より複雑になったりしたら、明示的に書く場合もあります。
int a = 1 + ((2 * 3) / 2);
代替スペル/代替表現
一部の演算子の代替スペルが<iso646.h> ヘッダーにマクロとして定義されています。
例えばこのように書き換えられます。
// 変更前 if (a > 0 && a < 10) // 変更後 if (a > 0 and a < 10)
他にもこれらが<iso646.h>に定義されています。
#define and && #define and_eq &= #define bitand & #define bitor | #define compl ~ #define not ! #define not_eq != #define or || #define or_eq |= #define xor ^ #define xor_eq ^=
こちらにも、書いてある通り、使わずに普通に書いた方が良さそうです。
優先順位と結合規則リスト
それぞれの演算子の詳細はのちのち行います。
[ ]
の中が低い方優先されます。`[3]`のみ右から左への結合なので注意が必要です。
[ 1 ]: 結合規則なし
- スコープ演算子(
::
)
[ 2 ]: 左から右へ結合
[ 3 ]: 右から左へ結合
- sizeof 演算子(
size_of
) - 前置インクリメント/デクリメント演算子(
++
,--
) - 1 の補数演算子(
~
) - 論理否定演算子(
!
) - 単項加算演算子(
+
) - 単項否定演算子(
-
) - アドレス取得演算子(
&
) - 間接演算子(
*
) - new 演算子(
new
) - delete 演算子(
delete
) - キャスト演算子(
( )
)
[ 4 ]: 左から右へ結合
[ 5 ]: 左から右へ結合
[ 6 ]: 左から右へ結合
[ 7 ]: 左から右へ結合
[ 8 ]: 左から右へ結合
- 関係演算子(
>
,<
,<=
,>=
)
[ 9 ]: 左から右へ結合
- 等値演算子(
==
,!=
)
[ 10 ]: 左から右へ結合
- ビットごとの AND 演算子(
&
)
[ 11 ]: 左から右へ結合
- ビット処理排他的 OR 演算子(
^
)
[ 12 ]: 左から右へ結合
- ビット処理包括的 OR 演算子(
|
)
[ 13 ]: 左から右へ結合
[ 14 ]: 左から右へ結合
[ 15 ]: 左から右へ結合
[ 16 ]: 左から右へ結合
- コンマ演算子(
,
)
参考リスト
感想
基本に立ち返って演算子を行いました。基本の基本。だいたいわかりますが、メンバーへのポインター演算子(.*
, ->*
)なんてあるのか、と思いました。
次回からは、それぞれの演算子について一つずつ使用例を確認します。
そろそろC++やるぞパート19 ポインター ~ 簡易まとめ ~
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
- 今回は、今まで触れてきたポインタをまとめます。
ポインタ
「ポインター」とも呼びますが、この記事では「ポインタ」の表記を使用します。
アドレス(メモリの位置)を持った変数のことを、ポインタ(またはポインタ変数)と呼びます。
生ポインタとスマートポインタ
C++では、いわゆる一般的なポインタである生ポインタと、リソースの管理を容易にしたスマートポインタが存在します。
生ポインタ
型名 *
を用いて宣言します。
int *intPtr; string *strPtr;
new
などで、メモリを割り当てます。
int *intPtr = new int(123);
ヒープ領域にメモリの確保するので、メモリの解放が必要です。
delete intPtr;
スマートポインタ
スマートポインタは、生ポインタと違いメモリの解放を自動で行ってくれます。スマートポインタは、3種類存在します。
unique_ptr
shared_ptr
weak_ptr
それぞれが別々の特徴を持っているクラスです。
unique_ptr
とshared_ptr
は、ヘルパー関数で初期化します。
auto up = make_unique<int>(234); auto sp = make_shared<string>("Hello World");
weak_ptr
はshared_ptr
から生成します。
weak_ptr wp = sp;
所有権
スマートポインタには、所有権という考えがあります。
所有権を普通のクラスを例にして考えてみます。以下の例では、unknown
という名前のMyClassを、変数mc
とmc2
が参照しています。
class MyClass { public: MyClass(string _name) : name(_name) { } string name; }; int main() { MyClass *mc = new MyClass("unknown"); MyClass *mc2 = mc; return 0; }
この参照を、所有として考えます。
つまり、いくつの変数が所有(参照)しているかです。
unique_ptr
unique_ptr
は、所有権を一つしか持てません。
以下は変数up
に最初に代入して、所有権(参照)はup
が持っています。所有権は一つしかないので、他の変数にも代入するとエラーになります。
auto up = make_unique<int>(124); auto up2 = up; // コンパイルエラー
コードブロックを抜けて使われていないとメモリが解放されます。
shared_ptr
shared_ptr
は、所有権を複数持つことが可能です。
unique_ptr
と異なり、他の変数に代入することができます。
auto up = make_shared<int>(124); auto up2 = up; auto up3 = up; // 実行可能
所有権の数をカウントする、参照カウントが存在します。参照が増えた場合に増やして、コードブロックを抜けると減らします。
auto up = make_shared<int>(124); cout << up.use_count() << endl; // 1 auto up2 = up; cout << up.use_count() << endl; // 2 cout << up2.use_count() << endl; // 2 { auto up3 = up; cout << up.use_count() << endl; // 3 } // コードブロックを抜けるので一つ解放 cout << up.use_count() << endl; // 2
参照カウントが0になった時に、メモリの解放を行います。
weak_ptr
weak_ptr
は弱いため、弱い所有権を持つことしかできません。shared_ptr
から生成できますが、その時に参照カウントは増えません。
auto up = make_shared<int>(124); cout << up.use_count() << endl; // 1 weak_ptr wp = up; cout << up.use_count() << endl; // 1
これは、shared_ptr
を使用して、循環参照が起こってしまう場合に主に使用します。
感想
今まで複数回にわたり触れてきて、これさえ覚えておけば、簡単に使用する分には問題なさそうなところをまとめました。(実戦ではもっと知識が必要そうですが)そこまで複雑ではなさそうというイメージです!
次回から、演算子について1から見ていこうと思います!
そろそろC++やるぞパート18 ポインター ~ スマートポインタ/weak_ptr ~
そろそろC++やります
そろそろC++をやります。そろそろC++をやりたいからです。
何回やるかはわかりません。基礎を理解するまではやろうと思います。
という感じでやっています。
※ 初心者がメモレベルで記録するので、技術記事としてはお力になれないかもしれません。
内容
weak_ptrの宣言
weak_ptr
は shared_ptrのリソースと同じクラステンプレートを持ちいて、shared_ptrから生成されます。
auto sp = make_shared<int>(145); weak_ptr<int> wp = sp;
weak_ptrとは
shared_ptrが持つリソースへの弱参照を保持するクラスです。
忘れていたのですが、unique_ptrもshared_ptrもweak_ptrも全てクラスなんですよね。
そして、weak_ptrはshared_ptrへの所有権を持つのでなく、そのリソースを覗き見、監視します。そのため、shared_ptrの参照カウントは増えません。
auto sp = make_shared<int>(145); cout << sp.use_count() << endl; weak_ptr<int> wp = sp; cout << sp.use_count() << endl;
Output
1 1
リソースの使用
リソースを使用する場合は、メンバー関数lock
を呼び出すことによって作成された、shared_ptr
オブジェクトを介して、使用することができます。
auto sp = make_shared<int>(145); weak_ptr<int> wp = sp; if (auto p = wp.lock()) { cout << *p << endl; }
Output
145
lock
関数で返されるshared_ptrは所有権を持ちます。
cout << sp.use_count() << endl; if (auto p = wp.lock()) { cout << sp.use_count() << endl; }
Output
1 2
なぜ、「lock」という命名なのかは、分かりませんでした。
if (auto p = wp.lock()) { cout << sp.use_count() << endl; cout << *sp << endl; auto sp2 = p; if (auto p = wp.lock()) { cout << sp.use_count() << endl; cout << *sp << endl; } }
以下のようなことは問題なくできました。
- lockの中で再度lock
- lockの中で元のshared_ptrの使用
- lockで得られたshared_ptrからshared_ptrの作成
get() みたいな名前でも良かったような気もしなくもないですが、lockでやって行きましょう。
循環参照 なぜweak_ptrが必要か
「weak_ptr使わないで、全てshared_ptrでやれば良い」という意見が出そうですが、循環参照に落ちいった時にweak_ptrが必要となります。
お互いをshare_ptr
型のメンバ変数として持つPairClass
を考えてみます。
class PairClass { public: PairClass(string _name) : name(_name) { } string name; shared_ptr<PairClass> pair; }; int main() { PairClass p1 = PairClass("pair1"); PairClass p2 = PairClass("pair2"); auto p1_sp = make_shared<PairClass>(p1); auto p2_sp = make_shared<PairClass>(p2); p2.pair = p1_sp; p1.pair = p2_sp; return 0; }
こちらのp2.pair = p1_sp; p1.pair = p2_sp;
の部分で、お互いに相手をshared_ptr
として保持しています。
本来であればスコープ{ }
を抜けた時に、p1
, p2
が解放され同時に変数であるshared_ptr
も解放されるはずです。しかし、p1
が解放されるタイミングでp1
のメンバ変数pair
は使用されているため、解放されません。同様な現象がp2
でも生じます。
よって、お互いに参照する循環参照が生じた時は適切にメモリが解放されないという現象が起きてしまいます。
これを対策するために、weak_ptr
を使用します。
参考リスト
感想
今回はweak_ptr
について触れて行きましたが、shared_ptr
の循環参照救済処置用のポインタだと分かり、使う場面も限定しそうです。
ポインタが一通り終わったので次回は一度、ポインタのまとめに入りたいと思います。