配列、ポインター、右辺値参照
5.1 配列とポインター
5.1.1 ポインターへの暗黙変換
#include <iostream>
int main(){
int array[] = {0,1,2,3};
std::cout << "先頭のアドレス" << &array[0] << std::endl;
int* ptr = array;
std::cout << "ポインター" << ptr << std::endl;
std::cout << "値" << *ptr << std::endl;
}
先頭のアドレス0x16b0bf1d8
ポインター0x16b0bf1d8
値0
5.1.2 次のアドレス
#include <iostream>
int main(){
int array[] = {0,1,2,3};
int* ptr = array;
std::cout << ptr[0] << std::endl;
std::cout << ptr[1] << std::endl;
std::cout << ptr[2] << std::endl;
std::cout << ptr[3] << std::endl;
}
変数のアドレスは1を足すと次の要素のアドレスを、1を引くと前の要素のアドレスを指す.
#include <iostream>
int main()
{
int array[] = {0, 1, 2, 3};
int* ptr = array;
ptr += 2; // 2番目の要素のアドレス
std::cout << *ptr << std::endl;
++ptr; // 3番目の要素のアドレス
std::cout << *ptr << std::endl;
ptr -= 2; // 1番目の要素のアドレス
std::cout << *ptr << std::endl;
--ptr; // 0番目の要素のアドレス
std::cout << *ptr << std::endl;
}
#include <iostream>
int main(){
int array[] = {0,1,2,3};
int* ptr = array;
ptr += 2;
std::cout << *ptr << std::endl;
ptr += 1;
ptr -= 2;
std::cout << *ptr << std::endl;
}
5.1.3 配列と引数
#include <iostream>
void function(int array[5])
{
std::cout << "function: " << array << std::endl;
}
void function_int(int val){
std::cout << "function_int" << &val << std::endl;
}
int main()
{
int array[] = {0, 1, 2, 3};
std::cout << "main: " << array << std::endl;
function(array);
int val = 3;
std::cout << &val << std::endl;
function_int(val);
}
// 配列のコピーは作らない
main: 0x16b33f1e0
function: 0x16b33f1e0
// int型はコピーを作る
0x16b33f1dc
function_int0x16b33f1bc
これ配列をstd::vectorとかにするとコピーされる.
#include <iostream>
#include <bits/stdc++.h>
void function2(std::vector<int> array)
{
std::cout << "function: " << &array << std::endl;
}
void function_int(int val){
std::cout << "function_int" << &val << std::endl;
}
int main()
{
std::vector<int> array = {0, 1, 2, 3};
std::cout << "main: " << &array << std::endl;
function2(array);
int val = 3;
std::cout << &val << std::endl;
function_int(val);
}
main: 0x16ef8f1a0
function: 0x16ef8f1d0
0x16ef8f19c
function_int0x16ef8f16c
5.1.4 配列の型と別名
#include <iostream>
int main()
{
int array[5] = {0, 1, 2, 3, 4};
// 配列へのポインター
int(*ptr)[5] = &array;
for (int e : *ptr)
{
std::cout << e << std::endl;
}
std::cout << std::endl;
// 配列への参照
int(&ref)[5] = array;
for (int e : ref)
{
std::cout << e << std::endl;
}
}
長くなるからusingを使うべき.
#include <iostream>
int main()
{
using int_array = int[5];
int_array array = {0, 2, 1, 3, 4};
int_array *aptr = &array;
int_array &aref = array;
for (int e : *aptr)
{
std::cout << e << std::endl;
}
std::cout << std::endl;
for (int e : aref)
{
std::cout << e << std::endl;
}
using int_array_pointer = int(*)[5];
int_array_pointer ptr = &array;
using int_array_reference = int(&)[5];
int_array_reference ref = array;
}
章末
void reverse(int array[5], int){
int tmp0 = array[0];
int tmp1 = array[1];
array[0] = array[4];
array[1] = array[3];
array[3] = tmp1;
array[4] = tmp0;
}
とか
void reverse(int *array, int){
int tmp0 = array[0];
int tmp1 = array[1];
array[0] = array[4];
array[1] = array[3];
array[3] = tmp1;
array[4] = tmp0;
}
#include <iostream>
int main()
{
int array[] = {0,1,2,3,4};
for (int *ptr = array; ptr != array + 5; ++ptr){
std::cout << *ptr << std::endl;
}
}
こういうのも可能.
#include <iostream>
int main()
{
int array[] = {0,1,2,3,4};
int *num = array + 3;
std::cout << *num << std::endl;
}
5.2 オブジェクトの配列
5.2.1 オブジェクトの配列と初期化
クラスも配列を作ることがdけいる. コンストラクタ呼び出しを行なってインスタンスを返す記法を使うことができる.
#include <iostream>
class Triangle
{
int m_height;
int m_base_length;
public:
explicit Triangle(int height, int base_length);
int height() const;
int base_length() const;
};
Triangle::Triangle(int height, int base_length) : m_height(height), m_base_length(base_length)
{
}
int Triangle::height() const
{
return m_height;
}
int Triangle::base_length() const
{
return m_base_length;
}
int main()
{
Triangle triangles[] = {
Triangle{10, 20},
Triangle{20, 30},
Triangle{40, 50}
};
for (auto &tri : triangles)
{
std::cout << "面積" << (tri.base_length() * tri.height() / 2)
<< std::endl;
}
}
コンストラクター呼び出しの数よりも配列の方が長い場合、先頭から順番に渡した引数で初期化され、足りない分はデフォルトコンストラクターが呼び出される. そのためデフォルトコンストラクターがない場合はエラーになる.
#include <iostream>
class A{
std::string m_name;
int m_value;
public:
explicit A(std::string name, int value);
explicit A(std::string name);
A();
void show() const;
};
A::A(std::string name, int value): m_name(name), m_value(value){}
A::A(std::string name): A(name, -1){}
A::A(): A("default"){}
void A::show() const {
std::cout << m_name << " " << m_value << std::endl;
}
int main(){
A a[4] = {
A{"first", 42},
A{"second"}
};
a[0].show();
a[1].show();
a[2].show();
a[3].show();
}
first 42
second -1
default -1
default -1
5.2.2 動的配列
#include <iostream>
#include <vector>
int main(){
std::vector<int> list;
list.push_back(42);
list.push_back(0);
for (int e : list){
std::cout << e << std::endl;
}
std::cout << std::endl;
list.pop_back();
list.push_back(-10);
for (int e : list){
std::cout << e << std::endl;
}
}
5.3 newとdeleteの使用
オブジェクトを動的確保すると、スコープの影響を受けないフリーストアからオブジェクトを格納するのに必要な領域を確保する.
C++では動的確保のためにnewとdelete演算子がある.
new演算子はtypeで指定したオブジェクトを1つ動的確保して、そのオブジェクトのアドレスを返す. この時通常はデフォルトコンストラクタによって初期化が行われ、new演算子からポインターを受け取った時にはもうオブジェクトの初期化が終わった状態になっている.
#include <iostream>
int* local(){ # ダングリングポインターになってしまってだめ
int i = 42;
return &i;
}
int* dyn_alloc(){
int* ptr = new int;
*ptr = 42;
return ptr;
}
int main(){
int* d = dyn_alloc();
std::cout << *d << std::endl;
delete d;
}
#include <iostream>
class A
{
int a;
public:
A(int a)
{
this->a = a;
};
~A(){
std::cout << "destructor" << std::endl;
}
int getA(){
return a;
}
};
int main(){
A* a = new A(2);
std::cout << a->getA() << std::endl;
delete a;
}
5.4 newとdeleteの詳細
5.4.1 割り当てたオブジェクトに初期値を与える
{}の方は危険な変換ができないようになっている.
#include <iostream>
int main(){
int* p = new int(42);
std::cout << *p << std::endl;
delete p;
}
#include <iostream>
class Triangle
{
float height;
float width;
public:
explicit Triangle(float height, float width) : height(height), width(width)
{
}
float area() const;
};
float Triangle::area() const {
return height*width/2;
}
int main(){
Triangle* tri = new Triangle(10.0f, 5.0f);
std::cout << tri->area() << std::endl;
delete tri;
}
5.4.2 配列の動的確保
type* variable = new type [array-length](initial-values)
delete []variable;
delete演算子では動的確保した配列につき1回のみ呼び出す. 各要素のデストラクタはdelete演算子が呼び出すのでこの時いつ確保したのかを覚えておく必要はない.
#include <iostream>
#include <string>
class Object{
std::string name;
public:
Object():Object("NO NAME"){} // デフォルトコンストラクタ
explicit Object(std::string name):name(name){};
~Object();
void show_name() const;
};
Object::~Object(){
std::cout << "destructor" << std::endl;
}
void Object::show_name() const{
std::cout << "object name:" << name << std::endl;
}
int main(){
Object* obj = new Object[10]{
Object{"first"},
Object{"second"} // 足りない分はデフォルトコンストラクタ
};
obj[0].show_name();
obj[1].show_name();
obj[2].show_name();
delete [] obj;
}
object name:first
object name:second
object name:NO NAME
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
練習問題5.4
#include <iostream>
#include <string>
class A
{
std::string name;
int age;
public:
A(std::string name, int age) : name(name), age(age) {
std::cout << "1番目" << std::endl;
}
A(std::string name) : A(name, 15) {
std::cout << "2番目" << std::endl;
}
A() : A("default name", 15) {
std::cout << "3番目" << std::endl;
}
~A();
void show_name_age() const;
};
A::~A()
{
std::cout << "deconstructor" << std::endl;
}
void A::show_name_age() const
{
std::cout << "name" << name << "age" << age << std::endl;
}
int main(){
A* arr = new A[4]{
A{"saori", 10},
A{"saori"},
A{}
};
arr[0].show_name_age();
arr[1].show_name_age();
arr[2].show_name_age();
arr[3].show_name_age();
delete[] arr;
}
5.5 {}による初期化とstd::initializer_list
5.5.1 {}による初期化
#include <iostream>
#include <string>
class A
{
int m_v;
std::string m_n;
public:
A(int, std::string);
A(float);
};
A::A(int v, std::string n):m_v(v), m_n{n}{} // メンバー初期化リストに{}使える.
A::A(float):A{-1, "float"}{} // 委譲コンストラクタにもok
int main(){
A ap(42, "0"); // 普通に()使うのok
A ab{42, "0"}; // {}も使える.
A bp = A(42, "0"); // ok. 関数型の明示的な型変換
A bb = A{42, "0"}; // ok.
// A cp = (42, "0"); // これはダメ
A cb = {42, "0"}; // ok.
A dp = (42, 0.0); // これはA(float)が呼ばれる
// A db = {42, 0.0}; // これはerror. doubleからstringにはできない.
double pi= 3.1415926536;
A ep(pi); // okだがdoubleからfloatへの暗黙変換が行われる
A eb{pi}; // error. doubleからfloatにはできない
}
{}使うと暗黙的な型変換が禁止される.
5.6 参照渡し
#include <iostream>
class Marker
{
public:
Marker();
~Marker();
};
Marker::Marker()
{
std::cout << "constructor" << this << std::endl;
}
Marker::~Marker()
{
std::cout << "destructor" << this << std::endl;
}
void copy(Marker m)
{
std::cout << "reference" << &m << std::endl;
}
void reference(const Marker &m)
{
std::cout << "reference: " << &m << std::endl;
};
int main()
{
Marker m;
std::cout << "値渡し前" << std::endl;
copy(m);
std::cout << "値渡し後" << std::endl;
reference(m);
std::cout << "参照渡し後" << std::endl;
}
constructor0x16f83f0d0
値渡し前
reference0x16f83f0d8
destructor0x16f83f0d8
値渡し後
reference: 0x16f83f0d0
参照渡し後
destructor0x16f83f0d0
値渡しをした場合、関数から処理を戻ってくる直前でデストラクター呼び出しが、仮引数のオブジェクトのアドレスで行われている.
参照を受け取る場合には、値渡しで行われたデストラクター呼び出しがない.
5.7 参照を返す関数
#include <iostream>
int x;
int& get_x(){
return x;
}
int main(){
x = 10;
int& y = get_x();
y = 100;
std::cout << x << std::endl;
}
ダングリングポインターに注意.
5.7.1 グローバル変数の参照を返す関数
#include <iostream>
int x;
int & get_x(){
return x;
}
int main(){
x = 10;
int& y = get_x();
y = 100;
std::cout << x << std::endl;
}
int y = get_x()ってすると参照として受け取らないので注意.
5.7.2 メンバー変数の参照を返す関数
#include <iostream>
#include <string>
class Object {
std::string name;
public:
Object(std::string name);
const std::string& get_name() const;
};
Object::Object(std::string name): name{name}{
}
const std::string& Object::get_name() const {
return name;
}
int main(){
Object obj("とても大きなオブジェクト");
const std::string& name = obj.get_name();
std::cout << name << std::endl;
}
5.7.3 参照を返した場合の型推論
#include <iostream>
int& id(int &i){
return i;
}
int main(){
int i = 42;
auto& j = id(i);
j = 0;
std::cout << i << std::endl;
}
練習問題
# include <iostream>
class A
{
int i;
public:
A(int i) : i{i} {
};
int &get_i();
const int& get_i() const;
};
int &A::get_i()
{
std::cout << "not const" << std::endl;
return i;
}
const int& A::get_i() const {
std::cout << "const" << std::endl;
return i;
}
int main()
{
A a(2);
std::cout << a.get_i() << std::endl;
const A b(3); // これはconstなのでconstなメンバ関数の方がよばれる.
std::cout << b.get_i() << std::endl;
}
const int& A::get_i() const {
std::cout << "const" << std::endl;
return i;
}
1つ目のconst→const int&を返すよってこと. 呼び出し元で戻り値を変更できないよ.
2つ目のconst→このメンバ関数はメンバ変数を変更しないよって意味.
5.8 右辺値参照
左辺値→メモリ上の変数
右辺値→数値リテラル
5.8.1 通常の参照と右辺値参照
type&& variable = R-value;
5.8.2 右辺値のアドレス
# include <iostream>
int main(){
int && i = 0; // 右辺値参照型変数は左辺値
int && j = 0; // 右辺値参照型変数は左辺値
std::cout << &i << std::endl;
std::cout << &j << std::endl;
}
0x16f41311c
0x16f413118
5.8.3 右辺値参照のオーバーロード
# include <iostream>
void show(int& v){
std::cout << "reference" << v << std::endl;
}
void show(int && v){
std::cout << "right reference" << v << std::endl;
}
int main(){
int v = 1;
show(v);
show(42);
}
reference1
right reference42
5.8.4 ムーブコンストラクター
# include <iostream>
# include <string>
# include <utility>
class person{
std::string m_name;
int m_age;
person(int age): m_age{age}{};
public:
person():person{-1}{};
person(std::string name, int age):m_name{name}, m_age{age}{};
person(person&& other);
const std::string& name() const {return m_name;}
int age() const {return m_age;}
};
// move constructor
person::person(person&& other):m_name{other.m_name}, m_age{other.m_age}{
std::cout << "move constructor called" << std::endl;
}
int main(){
person alice{"alice", 15};
person move{std::move(alice)};
std::cout << move.name() << std::endl;
std::cout << move.age() << std::endl;
}
std::move()は左辺値を強制的に右辺値にするヘルパー関数.
5.8.5 コピーとムーブ
コピーコンストラクターとムーブコンストラクターの違いは、明確.
コピー: 元のインスタンスとメンバー変数が同じになるようにする. ただししメンバー変数が動的確保したポインターを持っていた場合、多重開放しないように新たにメモリ領域を動的確保して変数をコピーする. ただメモリの動的確保はパフォーマンスに影響を及ぼすことがあるので、コピーを行いすぎると、処理が遅くなる可能性.
ムーブ: コピーとは異なり所有権の移動を行う. 所有している動的確保したメモリ領域を、他のインスタンスに譲渡するみたいなこと.
#include <iostream>
#include <utility>
class home
{
int *m_land;
public:
explicit home(std::size_t size) : m_land{new int[size]} {}
~home() { delete[] m_land; }
home(home &&other);
int *land() const { return m_land; }
};
home::home(home&& other):m_land{other.m_land}{ // ムーズ元のポインターをコピーする
other.m_land = nullptr; // ムーブ元のポインターを空にする. これで所有権の移動完了.
}
int main(){
home A{100};
std::cout << "Aの土地のアドレス" << A.land() << std::endl;
// AからBに所有権を移動
home B{std::move(A)};
std::cout << "Bの土地のアドレス" << B.land() << std::endl;
std::cout << "移動後のAの土地のアドレス" << A.land() << std::endl;
}
Aの土地のアドレス0x14de05fc0
Bの土地のアドレス0x14de05fc0
移動後のAの土地のアドレス0
所有権の移動を含む一連の動作はムーブセマンティクスと呼ばれる.
引数として渡す際にムーブするとコピーしないで関数にインスタンスを渡すことができる. もう使わないならムーブした方がいい.
コピーコンストラクタも足したversion
#include <iostream>
#include <utility>
class home
{
int *m_land;
std::size_t size;
public:
explicit home(std::size_t size) : size{size}, m_land{new int[size]}
{
}
~home() { delete[] m_land; }
home(home &&other);
home(const home &other);
int *land() const { return m_land; }
};
home::home(home &&other) : size{other.size}, m_land{other.m_land}
{ // ムーズ元のポインターをコピーする
other.m_land = nullptr; // ムーブ元のポインターを空にする. これで所有権の移動完了.
}
home::home(const home &other) : size{other.size}, m_land{new int[other.size]}
{
std::cout << other.m_land << std::endl;
std::cout << other.size << std::endl;
std::copy(other.m_land, other.m_land + other.size, m_land);
}
int main()
{
home A{100};
std::cout << "Aの土地のアドレス" << A.land() << std::endl;
// AからBに所有権を移動
home B{std::move(A)};
std::cout << "Bの土地のアドレス" << B.land() << std::endl;
std::cout << "移動後のAの土地のアドレス" << A.land() << std::endl;
// BからCにコピ
home C(B);
std::cout << "Cの土地のアドレス" << C.land() << std::endl;
std::cout << "移動後のBの土地のアドレス" << B.land() << std::endl;
}
Aの土地のアドレス0x11ee06030
Bの土地のアドレス0x11ee06030
移動後のAの土地のアドレス0
0x11ee06030
// BとCの土地のアドレスは異なる. 新規確保してるから.
Cの土地のアドレス0x11ee061c0
移動後のBの土地のアドレス0x11ee06030
5.8 関数ポインターと関数リファレンス
5.9.1 関数へのポインター
int (*func_name)(type,,,)
の形で宣言する.
#include <iostream>
int identity(int v){
return v;
}
int square(int v){
return v*v;
}
int main(){
int (*func)(int) = &identity;
std::cout << "func(4)" << func(4) << std::endl;
func = □
std::cout << "func(4)" << func(4) << std::endl;
}
5.9.2 関数リファレンス
参照型なので、後から参照先を変更することはできない.
int (&func_name)(int)
みたいに関数ポインタと同様に宣言する.
5.9.3 高階関数
関数ポインタや関数リファレンスを引数に持っていたり、戻り値として返す関数を高階関数という.
#include <iostream>
void filtered_show(int (&array)[5], bool (*predicate)(int)){
for(int e: array){
if(predicate(e)){
std::cout << e << std::endl;
}
}
}
bool is_odd(int v){
return (v % 2) == 1;
}
bool is_less_than_5(int v){
return v < 5;
}
int main(){
int array[] = {5,10,3,0,1};
filtered_show(array, &is_odd);
std::cout << std::endl;
filtered_show(array, &is_less_than_5);
}
filtered_show()は高階関数になっている.
5.10 thisのキャプチャ
- とりあえずラムダ式の復習
#include <iostream>
int main(){
auto show = [](int i) -> void {
std::cout << "iの値は" << i << "です" << std::endl;
};
show(42);
};
#include <iostream>
int main(){
int a = 0;
auto lambda = [a](){ // 外部の変数のキャプチャ
std::cout << a << std::endl;
};
lambda();
}
mutableをつけることで、変更可能になる.
#include <iostream>
int main(){
int a = 0;
auto lambda = [a]() mutable{
a = 42;
};
lambda();
}
#include <iostream>
int main(){
int a = 0;
auto lambda = [&a](){ // 参照でキャプチャする. mutable指定不要.
std::cout << "captured variable" << a << std::endl;
++a;
};
lambda();
std::cout << a << std::endl;
a = 42;
lambda();
}
5.10.1 メンバ変数のキャプチャ
以下のようなメンバ変数のキャプチャはできない.
5.10.2 thisポインターのキャプチャ
thisをコピーキャプチャすることは可能.
mutableしていなくても、メンバ変数を変更できちゃう.
#include <iostream>
class C{
int a;
public:
void show_a(){
auto lambda = [this](){
// this->aと同じ意味になる
std::cout << a << std::endl;
};
lambda();
}
void set_a(int value){
// mutableしていなくても、キャプチャしているのはthisポインタだから
// aにvalueを代入するのはできる.
[this, value](){
a = value;
}();
};
};
int main(){
C c;
c.show_a();
}
constをつけたらerrorが起きる. キャプチャしてるのは、constのthisポインターだから.
void set_a(int value) const{
// mutableしていなくても、キャプチャしているのはthisポインタだから
// aにvalueを代入するのはできる.
[this, value](){
a = value;
}();
};
5.10.3 インスタンス全体のコピー
*thisを指定することでインスタンス丸ごとコピーできる.
#include <iostream>
class C
{
int a;
public:
explicit C(int a) : a{a} {};
void copy_and_set(int value){
std::cout << "copy_and_set" << a << std::endl;
[*this, value]() mutable{
std::cout << "lambda:a == " << a << std::endl;
a = value;
std::cout << "lambda:a == " << a << std::endl;
}();
std::cout << "copy_and_set: a == " << a << std::endl;
};
};
int main(){
C c{42};
c.copy_and_set(0);
}
→42, 42, 0, 42になる
5.11 関数ポインターに変換可能なラムダ式
何もキャプチャしないラムダ式は関数ポインターや関数リファレンスに変換できるようになっている.
#include <iostream>
int function(int a, int b, int c){
std::cout << "function(" << a << "," << b << "," << c << ")" << std::endl;
}
int main(){
function(0, 1, 2);
auto lambda = [](int a, int b, int c){
std::cout << "lambda(" << a << "," << b << "," << c << ")" << std::endl;
};
lambda(0,1,2);
}
章末
配列の動的確保
new演算子もstd::vectorクラスもヒープ領域に確保される。
#include <iostream>
int* allocate(int length){
int* arr = new int[length]{};
return arr;
};
int main(){
int* ptr = allocate(10);
std::cout << ptr << std::endl;
}
- 参照と右辺値参照のオーバーロード
#include <iostream>
#include <utility>
void reference(int &v){
std::cout << "reference" << v << std::endl;
}
void reference(int&& v){
std::cout << "right reference" << v << std::endl;
}
int main(){
int v = 21;
reference(v);
reference(42);
}
- enumerateの実装
- 配列はポインタに暗黙変換される
- 関数ポインタ
#include <iostream>
void show(int v){
std::cout << v << std::endl;
}
void enumerate(int* first, int *end, void(*func)(int)){
for (int* s = first; s != end ; s += 1){
func(*s);
}
}
int main(){
int array[] = {1,2,3,5,7,11,13};
std::size_t length = sizeof(array)/sizeof(array[0]);
enumerate(array, array + length, show);
}
5 ラムダ式に書き換え
#include <iostream>
void enumerate(int* first, int *end, void(*func)(int)){
for (int* s = first; s != end ; s += 1){
func(*s);
}
}
int main(){
int array[] = {1,2,3,5,7,11,13};
std::size_t length = sizeof(array)/sizeof(array[0]);
auto show = [](int v){
std::cout << v << std::endl;
};
enumerate(array, array + length, show);
}