C++ 基礎編 6日目

C++は、C言語のポインタに加え、参照という概念を導入しました
ポインタと参照を使い分けることで、コードの可読性と安全性を向上させ、より効率的なプログラミングを行うことができます
この章では、ポインタと参照について説明をしていきます

ポインタ

メモリアドレスを扱うことで、メモリ上のデータを直接操作することができます
それにより、以下のような使い方ができます

  • 動的メモリの割り当てを行う
  • 関数内で呼び出し元の変数を直接変更する
  • 配列を効率的に扱う

ポインタを扱う際、以下の注意点があります

  • ポインタの不適切な使用は、バッファオーバーフローやメモリリークなどの問題を招きやすくなる
  • ポインタがNULLの場合、ポインタをデリファレンスするとクラッシュする

ポインタの詳細については、以下を参照ください

  1. C言語 応用編 ~1日目~
  2. C言語 応用編 ~2日目~

動的メモリ割り当て

C言語では、mallocfreeを利用して動的メモリを確保、解放をしていました
C++では、これらの関数の利用は推奨されていません
代わりにnewdeleteを使用して、動的なメモリの確保および解放を行います
newを利用してメモリを確保した場合、必ず、deleteを実行した確保したメモリを解放する必要があります

以下に、new、deleteを利用して配列のサンプルプログラムを示します

#include <iostream>

int main() {
  // サイズが5の int 型配列をヒープ領域に動的に確保
  int* arrPtr = new int[5];

  // 配列の要素に値を代入
  for (int i = 0; i < 5; ++i) {
    arrPtr[i] = i * 10;
  }

  std::cout << "確保した配列の要素: ";
  for (int i = 0; i < 5; ++i) {
    std::cout << arrPtr[i] << " ";
  }
  std::cout << std::endl;

  // 不要になった配列のメモリを解放
  delete[] arrPtr;

  // 解放後のポインタには nullptr を代入
  arrPtr = nullptr;

  return 0;
}

参照

C++の参照は以下のような性質を持っています

  • 変数への別名のようなもので、アドレスを直接扱う必要がない
  • ポインタのように、関数内で呼び出し元の変数を直接変更できる

参照の定義方法

参照の定義方法は以下のようにします

型名& 参照名 = 参照先

参照の宣言時に必ず、参照先を初期化する必要があります
また、初期化後、他の参照先を代入することはできません

以下に、参照を利用したサンプルプログラムを示します

#include <iostream>

int main(){
    int x;
    int &y = x; // yはintの参照型、参照先はx

    x = 10;
    std::cout << "x=" << x << std::endl; // xの値
    std::cout << "y=" << y << std::endl; // yが参照する先の値

    std::cout << "&x=" << &x << std::endl;  // 0x7fffb560c88c
    std::cout << "&y=" << &y << std::endl;  // 0x7fffb560c88c

    return 0;
}
実行結果
x=10
y=10
&x=0x7fffb560c88c
&y=0x7fffb560c88c
int &y = x;

yはxの参照型として定義しています
この宣言により、yはxの別名となり、メモリ上で同じ場所を指すようになります

std::cout << "&x=" << &x << std::endl;  // 0x7fffb560c88c
std::cout << "&y=" << &y << std::endl;  // 0x7fffb560c88c

変数x,yのアドレスを取得しています
&yは変数yのアドレスを取得します
yはxの参照なので、&yはxのアドレスと同じ値を持ちます
つまり、xとyはメモリ上の同じ場所を指していることが確認できます
また、参照変数自身のメモリ上のアドレスを取得することはできません

上記のサンプルプログラムに、aという変数を用意しy=aと実行した場合、どのような結果になるでしょうか?

#include <iostream>

int main(){
    int x;
    int &y = x;

    x = 10;
    std::cout << "x=" << x << std::endl;
    std::cout << "y=" << y << std::endl;

    std::cout << "&x=" << &x << std::endl;
    std::cout << "&y=" << &y << std::endl;

    int a = 20;
    y = a;
    std::cout << "x=" << x << std::endl;
    std::cout << "y=" << y << std::endl;
    std::cout << "&x=" << &x << std::endl;
    std::cout << "&y=" << &y << std::endl;

    return 0;
}
実行結果
x=10
y=10
&x=0x7fffc7e7c378
&y=0x7fffc7e7c378
x=20
y=20
&x=0x7fffc7e7c378
&y=0x7fffc7e7c378
int a = 10;
y = a;

参照に対して代入を行うと、参照先の変数の値が変更されます
y = a; という代入は、「yが参照している変数(つまりx)」に、変数 a の値 (20) を代入する、という意味になります

したがって、この代入の後、xの値は 20 に変更されます
yは依然としてxを参照しているので、yの値も 20 になります
これが、後半の出力で x と y の値がどちらも 20 になる理由です
そして、&xと&yのアドレスは、この後も変わらず同じ値を示します
なぜなら、yが参照している対象が変わったのではなく、yが参照している変数 x の値が変更されただけだからです
yは初期化以降、ずっと x を参照し続けています

関数の引数

C++では、関数を呼び出す際、「値渡し」、「ポインタ渡し」、「参照渡し」の3種類の方法があります
ここでは、「ポインタ渡し」と「参照渡し」について説明をします

ポインタ渡し

ポインタ渡しは、ポインタ変数を関数の引数に渡す方法です
以下にポインタを利用したサンプルプログラムを示します

void swap(int *x,int *y){
    int tmp;

    tmp = *y;
    *y = *x;
    *x = tmp;
}

void show(const int *ary,int cnt){
    if( ary == nullptr ){
        std::cout << "null" << std::endl;
        return;
    }

    for(int i=0;i<cnt;i++){
        std::cout << *(ary+i) << " ";
    }
}

void sort(int *ary,int cnt){
    if( ary == nullptr ){
        std::cout << "null" << std::endl;
        return;
    }

    for(int i=0;i<cnt;i++){
        for(int j=cnt-i;j>i;j--){
            if(ary[j]<ary[j-1]){
                swap(&ary[j],&ary[j-1]);
            }
        }
    }
}

int* makeMemory(int cnt){
    int* ptr = new int[cnt];

    // 乱数の種を設定
    std::srand(static_cast<unsigned int>(std::time(nullptr)));

    for(int i=0;i<cnt;i++){
        ptr[i] = std::rand() % 10 + 1;
    }

    return ptr;
}

int main(){
    int *ary = makeMemory(5);
    show(ary,5);
    std::cout << std::endl;
    sort(ary,5);
    show(ary,5);
    std::cout << std::endl;

    delete [] ary;
    return 0;

}

ポインタ渡しは、関数内で確保したメモリ領域のアドレスを呼び出し元に伝えることができるため、関数の処理後もそのデータを利用したい場合に有効です
例えば、動的にメモリを確保して作成したデータ構造を関数から返す場合などに用いられます

int* makeMemory(int cnt)

この関数は、関数内で確保したメモリ領域のアドレスを呼び出しもとに伝えています

if( ary == nullptr )

ポインタ渡しを使用する場合、ポインタ変数がNULL(何も指していないポインタ)の場合もあるため、関数内でポインタ変数を扱う際には、NULLポインタではないかを確認する必要があります
この行はポインタ変数がNULLかどうかをチェックしています
nullptrはC++でNULLポインタを表すキーワードとして追加されました

void show(const int *ary,int cnt)
void sort(int *ary,int cnt)`

C++では、配列名は先頭要素へのポインタとして扱われます
そのため、配列を関数に渡す際には、暗黙的にポインタ渡しが行われます
これにより、関数内で配列の要素にアクセスしたり、変更したりすることが可能です
これら関数の*aryは配列要素の先頭を指しています
*(ary + i)ary[i]という配列のアクセス方法と同じアドレスを指しています

void show(const int *ary,int cnt)

ポインタ渡しの際に、引数のポインタ型にconst修飾子をつけることで、関数内で指し示す変数の値を変更しないことを保証できます
これにより、関数の意図を明確にし、誤った変更を防ぐことができます
この関数は、内部では変数の値を変更しないため、constを付加することで、誤った変更を防いでいます

`delete [] ary;

new演算子によりメモリを確保した場合は、必ず、delete[]演算子によりメモリの解放をする必要があります
この行はmakeMemory()で作成したメモリ領域を解放しています

参照渡し

参照渡しは、関数の定義で引数が参照型であることを定義するが、呼び出し側は通常の変数と同様の記述をすることができます
参照変数を受け取った関数では、参照変数が指し示す領域の値を取得したり、値を変更することができます

以下に参照を使ったサンプルプログラムを示します

#include <iostream>
#include <cstdlib> 
#include <ctime>   

void swap(int &x,int &y){
    int tmp;
    std::cout << "swap()内のアドレス:" << &x << "," << &y <<std::endl;
    tmp = y;
    y = x;
    x = tmp;
}

void printValue(const int& val) { 
    std::cout << val << " "; 
}

int main(){
    int ary[5];

    // 乱数の種を設定
    std::srand(static_cast<unsigned int>(std::time(nullptr)));

    for(int i=0;i<5;i++){
        ary[i] = std::rand() % 10 + 1;
        printValue(ary[i]);
    }

    std::cout << std::endl;

    for(int i=0;i<5;i++){
        for(int j=5-i;j>i;j--){
            if(ary[j]<ary[j-1]){
                std::cout << "swap呼び出し前のアドレス:" << &ary[j] << "," << &ary[j-1] <<std::endl;
                swap(ary[j],ary[j-1]);
            }
        }
    }

    return 0;
}
実行結果
1 6 5 3 5 
swap呼び出し前のアドレス:0x7ffdf419187c,0x7ffdf4191878
swap()内のアドレス:0x7ffdf419187c,0x7ffdf4191878
swap呼び出し前のアドレス:0x7ffdf4191878,0x7ffdf4191874
swap()内のアドレス:0x7ffdf4191878,0x7ffdf4191874
swap呼び出し前のアドレス:0x7ffdf419187c,0x7ffdf4191878
swap()内のアドレス:0x7ffdf419187c,0x7ffdf4191878

全体的に、参照渡しの場合、関数の定義時には、&をつけていますが、呼び出し時は不要であるため、構文が簡潔になっています
また、NULL参照は存在しないため、参照変数は安全に使用することができます

void swap(int &x,int &y)

参照は、既存の変数に対する別名です
swap()関数の前後で引数のアドレスを表示してみると、swap()関数内の参照は、swap()を呼び出す前の変数と同じアドレスと等しくなっていることが確認できます

void printValue(const int& val)

この関数は、関数内で引数の値を変更しないことを保証するため、const参照を用いています

また、関数の戻り値を参照にしたサンプルプログラムを以下に示します

#include <iostream>
#include <cstdlib> 
#include <ctime>   

void swap(int &x,int &y){
    int tmp;

    tmp = y;
    y = x;
    x = tmp;
}

int& getValue(int *ary,int index){
    return ary[index];
}

void printValue(const int& val) { 
    std::cout << val << " "; 
}

void show(int *ary,int cnt){
    for(int i=0;i<cnt;i++){
        // 関数getValue()から参照を取得しその値を引数にして関数printValue()を呼び出す
        printValue(getValue(ary,i));
    }
}


int main(){
    const int CNT = 5;
    int ary[CNT];

    // 乱数の種を設定
    std::srand(static_cast<unsigned int>(std::time(nullptr)));

    for(int i=0;i<CNT;i++){
        // 関数から参照を取得しその値を変更する
        getValue(ary,i) = std::rand() % 10 + 1;
    }

    show(ary,CNT);
    std::cout << std::endl;

    for(int i=0;i<CNT;i++){
        for(int j=CNT-i;j>i;j--){
            if(ary[j]<ary[j-1]){
                swap(ary[j],ary[j-1]);
            }
        }
    }

    show(ary,CNT);
    std::cout << std::endl;

    return 0;

}
int& getValue(int *ary,int index)

この関数は、はint&型を返す関数として定義しています
それにより、main関数内の getValue(ary,i) = std::rand() % 10 + 1;のように左辺値として使用したり、printValue(getValue(ary,i));のように右辺値として使用することもできます
※変数や引数など値が代入されるものは左辺値と呼ばれます
一方、数値やリテラルなど値を代入することができないものを右辺値と呼びます

一般的に、変数の値を関数内で変更する必要がある場合や、大きなサイズのオブジェクトをコピーせずに扱いたい場合には、ポインタ渡しまたは参照渡しが用いられます
NULLポインタの可能性があり、アドレスを明示的に扱いたい場合はポインタ渡し、より安全で直感的な書き方をしたい場合は参照渡しが推奨されることが多いです

コメント

この記事へのコメントはありません。

関連記事

C++ 基礎編 5日目

C++ 発展編 7日目

C言語 導入編②

PAGE TOP