【サンプルコード付き】C++ std::shared_ptrを初心者にもわかりやすく解説

[記事内には広告が含まれています]

こんにちは、現役エンジニアの inno_tech です。

shared_ptrスマートポインタの1種で、確保されたメモリ(リソース)は、どこからも参照されなくなったタイミングで自動的に解放されます。

つまり、new や malloc で確保したメモリと異なり、プログラマがメモリを解放する必要がありません。
したがって、メモリリークを防止する目的でよく利用されます。

中でもshared_ptrは、指定されたリソースへの所有権を複数で共有(share)できることが特徴です。

今回は、そんな便利なスマートポインタ std::shared_ptr の使い方をサンプルコードを交えてわかりやすく解説します。(リファレンスサイトを見ても良くわからない…という方は必見)

もくじ

std::shared_ptr | 基本編

インクルード

std::shared_ptrを使用するためには、#include <memory>  を記述します。

std::shared_ptr の宣言

std::shared_ptrは型名がテンプレート化されており、〈〉の中に任意の型を指定してメモリを動的に確保します。
宣言の基本形は以下の通りです。

std::shared_ptr<型名> 変数名 = std::shared_ptr<型名>(new 型名(constructor) );

// 例: 100byteのメモリを確保
std::shared_ptr<char> buf = std::shared_ptr<char>(new char(100) );

サンプルコード

説明の便宜上、Human class を定義しておきます。
以降のサンプルでは、Human class をshared_ptr化する例を示します。

#include <iostream>
#include <string>

class Human
{
public:
    /* constructor */
    Human(const std::string &name, int age ){
        name_ = name;
        age_ = age;
    };

    /* denstructor */
    ~Human(){};

    /* method */
    void PrintMySelf(){
        std::cout << "名前:" << name_ << std::endl;
        std::cout << "年齢:" << age_ << std::endl << std::endl;
    };

    /* member */
    std::string name_;      // 名前
    int age_;               // 年齢
};

続いて、shared_ptrを用いたサンプルコードです。

  • test1 | shared_ptrを宣言し、メソッドをコールする例
  • test2 | shared_ptrから生のポインタを取得する例
  • test3 | shared_ptrの参照先(実態)を取得する例
#include <memory>
#include "human.hpp"

void TestSharedPtr_001()
{
    printf("***** %s ******\n", __func__ );

    /* 宣言 */
    std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( new Human( "tanaka", 20) ); 

    /* メソッドを呼び出し */
    printf("** test1: メソッドを呼び出し **\n" );
    sp_tanaka->PrintMySelf();

    /* 生のポインタを取得 */
    printf("** test2: 生のポインタを取得 **\n" );
    Human *p_tanaka = sp_tanaka.get();
    p_tanaka->PrintMySelf();

    /* 実体を取得 */
    printf("** test3: 実体を取得 **\n" );
    (*sp_tanaka).PrintMySelf();

    return;
}

結果はこのようになります。
これで基本的なshared_ptrの使い方はマスターできました。

***** TestSharedPtr_001 ******
** test1: メソッドを呼び出し **
名前:tanaka
年齢:20

** test2: 生のポインタを取得 **
名前:tanaka
年齢:20

** test3: 実体を取得 **
名前:tanaka
年齢:20

std::shared_ptr | 応用編

shared_ptr を別のshared_ptrにコピーした時の動作

続いて、shared_ptrをコピーした場合の動作を確認します。

  • sp_tanakasp_copyにコピー
  • sp_tanakaage の値を変更
  • sp_copy age の値はどうなっているか?

サンプルコードはこちらです。

void TestSharedPtr_002()
{
    printf("***** %s ******\n", __func__ );

    /* 宣言 */
    std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( new Human( "tanaka", 20) ); 
    std::shared_ptr<Human> sp_copy   = sp_tanaka;

    /* 変更前 */
    printf("** test1: コピー先ポインタ(sp_copy) **\n" );
    sp_copy->PrintMySelf();

    /* 値を変更 */
    printf("** test2: コピー元のポインタ age=30に変更(sp_tanaka) **\n" );
    sp_tanaka->age_ = 30;
    sp_tanaka->PrintMySelf();

    /* 結果を確認 */
    printf("** test3: コピー先ポインタの結果(sp_copy)  **\n" );
    sp_copy->PrintMySelf();

    return;
}

結果はこのようになりました。

***** TestSharedPtr_002 ******
** test1: コピー先ポインタ(sp_copy) **
名前:tanaka
年齢:20

** test2: コピー元のポインタ age=30に変更(sp_tanaka) **
名前:tanaka
年齢:30

** test3: コピー先ポインタの結果(sp_copy)  **
名前:tanaka
年齢:30

コピー元のsp_tanaka の値を書き換えると、コピー先のsp_copyも書き換わりました

これは通常のポインタの考え方と同じで、std::shared_ptrが同じメモリを参照するためです。

ローカル変数をshared_ptr化するには?

ここまではshared_ptr宣言時にnewで新たなメモリ確保し、shared_ptrを生成しました。
しかし、すでに宣言してあるローカル変数をshared_ptrに格納するにはどうすればよいでしょうか?

例えば、自分のプログラムから使用したい3rdPartyライブラリがあり、そのインターフェースがshared_ptrのみで設計されている場面で、本テクニックが必要になる場合があります。

それではサンプルコードです。

void TestSharedPtr_003()
{
    printf("***** %s ******\n", __func__ );

    /* ローカル変数として宣言 */
    Human local_tanaka("tanaka", 20);    

    /* ローカル変数を shared_ptrに格納 */
    // OK
    std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( &local_tanaka, [](Human*){} ); 

    // NG: メモリの二重解放 になる
    // std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( &local_tanaka ); 

    /* メソッドを呼び出し */
    printf("** test1: ローカル変数を shared_ptr化 **\n" );
    sp_tanaka->PrintMySelf();

    return;
}

NG | custom deleter(カスタムデリーター)を使用しない場合

よくある間違いが、ローカル変数のアドレスだけを渡す方法です。

 std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( &local_tanaka );

この場合、「double free or corruption (out)」となり、アプリケーションが強制終了します。

***** TestSharedPtr_003 ******
** test1: ローカル変数を shared_ptr化 **
名前:tanaka
年齢:20
double free or corruption (out)

これはメモリの二重解放が原因で、
まず、shared_ptrのリソースが参照されなくなった時点で、 ローカル変数のメモリが解放(1回目)
さらに、関数が終了した時点で、通常のローカル変数と同じようにメモリの解放が実行(2回目)
という動作となるためです。

Answer | custom deleter(カスタムデリーター)を使用する

正しい方法は、メモリの解放を行わないcustom deleterを渡すことです。
ポイントはこの部分です。

std::shared_ptr<Human> sp_tanaka = std::shared_ptr<Human>( &local_tanaka, [](Human*){} );

std::shared_ptr第2引数は、custom deleterの指定を意味します。

custom deleterには、shared_ptrを生成した型(Human)のポインタを引数とした任意の関数を指定します。
今回は、 C++ 11 のラムダ式何もしない関数を定義しました。

custom deleterの効果

shared_ptrは通常、リソースが参照されなくなったタイミングでメモリを解放しますが、
custom deleterを指定した場合、custom deleterが実行されます

このサンプルでは、custom deleterに 何も処理しないdeleterを渡す ことで、メモリの二重解放を防止し、ローカル変数のshared_ptr化を実現しています。

さいごに

今回はstd::shared_ptrの使い方を説明しました。
リファレンスを見ただけでは使用方法がわかりづらい方も、サンプルコードを見ればイメージできたのではないでしょうか。

最後に、少し難易度が高めの本ですが、C++スキルのレベルアップを目指す方におすすめの本の紹介です。(中級者以上向け)

話は変わりますが、プログラミングを習得したい, 仲間が欲しい人は、プログラミングスクールに通うのがおすすめです
自己学習でかかるコストを考えると十分元が取れると思います。

少しでも興味があれば、まずは無料のオリエンテーションを受講し、自分に合うか試してみるのがよいでしょう。

まずは一歩行動して、将来の自分に楽をさせてあげましょう。

参考になったでしょうか。
ほかにも役立つ情報が他のページにもあるかもしれません。
ご覧いただいても、ご覧いただかなくてもどちらでもどうぞ!

この記事がイイねと思ったら、Twitterフォロー か  にほんブログ村のどちらかしてくれたら嬉しいよ!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

コメント / ご要望

コメントする

goto
もくじ
閉じる