LoginSignup
245
248

More than 3 years have passed since last update.

それ、ポインタ使わなくてもできるよ:C言語のポインタとC++の流儀

Last updated at Posted at 2015-12-23

はじめに

これは、初心者 C++er Advent Calendar 2015 の 23日目の記事です。
昨日は、ネタがないと困っていたyumechi0525さんが、悩んだ末にC++の文字列処理関係と正規探索についての記事を書いてくださいました。

今日私が書くのは、ポインタについてです。本記事の対象は、

  • C++に興味はあるが、手を出していないC言語ユーザー
  • C言語スタイルでポインタや配列を使い続けているC++ユーザー

です。参照、スマポ、STL、std::functionと聞いて、何のことか分かる方は、流し読みしてください。


C/C++において初心者が学習過程でぶつかる最大の壁。それがポインタです。概念的な難しさもさることながら、ポインタが出てくる文脈の幅が広いことが、ポインタの理解を困難にしているように思われます。

この記事では、まずポインタの理解を難しくしているポインタが使われる複数の文脈について解説します。そのうえで、それぞれの文脈でのポインタが、C++の流儀ではどのように書き直せるのかについて書こうと思います。

ポインタを使う場面

C言語では、効率よくプログラムを書くためにはポインタは必要不可欠な存在です。様々な場面でポインタを使いますが、代表的なのは以下のようなケースではないでしょうか。

  1. スコープ外の変数にアクセスするための参照としてのポインタ
  2. 要素や配列の動的確保のためのポインタ
  3. 配列としてのポインタ
  4. 関数を保持するためのポインタ

これだけ幅広い機能を「ポインタ」という単一の機能で実現していれば、そりゃ「ポインタって結局いつ使うんだ?」と混乱もしますよね。

一方、C++ではこれらの機能は多くの場合ポインタ以外の代替の(そしてより高機能な!)手段が与えられています。C++を使っている以上、これらの機能を利用しない手はありません。

以下では、一つ一つ利用例についてみていきましょう。

1. 参照としてのポインタ

C/C++では、複数の値を関数の外に戻したい時など、スコープの外の変数を書き換える場合には、ポインタが使われます。

c言語
//二次元空間上の座標を、極座標に変換する
//第3,第4引数で、動径と偏角を書き戻す
void to_polar(double x, double y, double* r, double* d){
   *r = sqrt(x*x+y*y);
   *d = atan2(y,x);
}

int main(void){
   double r;
   double d;

   to_polar(1.0, sqrt(3.0)/2, &r,&d);

   //r == 1
   //d == pi/60

   return 0;
}

また、関数内部で関数外の変数を書き換えるつもりがなくても、その変数が非常に大きなサイズを持つ構造体の場合、不必要なコピーを避ける目的でポインタが使われることもあります。

c言語
//大きなサイズの構造体
//コピーが非常に高コスト!
struct data_type{
   double d1;
   double d2;
   //中略
   double d100;
};

//値渡しの関数
void func_copy(struct data_type Data){
   //Dataを使った処理
}

//ポインタ渡しの関数
void func_ptr(const struct data_type* Ptr){
   //*Ptrを使った処理
}

int main(void){
   struct data_type Data;

   //値渡しでは、d1~d100をコピーする処理が発生して重い!
   func_copy(Data);

   //ポインタ渡しでは、コピーが発生しない
   func_ptr(&Data);
}

これらはいずれも、「値をコピーせず、既存の変数のアドレスに直接アクセスしたい」ために使われています。

C++では、これらはいずれも参照の機能を使うことで記述できます。参照は、ポインタのようにアドレスを共有する機能ですが、以下のような特徴があります。

  • 宣言は、Type*ではなくType&を使う。
  • 実体を受け取り、実体のように使える。
struct data{
   int a;
   int b;
};
int main(){
   data A;
   data* Ptr=&A;  //ポインタは、Type*で宣言し、アドレスを受け取る。
   data& Ref=A;   //参照は、Type&で宣言し、値型として受け取る。

   //RefはAと同じアドレスを指す変数
   Ref.a = 4;     //Aのメンバーにアクセスもできる

   //同じアドレスなので、当然 A.a == 4

   return 0;
}
  • 初期化時に代入する必要がある(空の状態にできない)
  • 指し示す対象を書き換えられない
int a;
int b;

int* p;  //ポインタは、初期化なしで宣言できる
p = &a;  //代入も可能 aを参照
p = &b;  //ここからbを参照

//int& r; 
//=ERROR= 参照は、初期化なしで宣言できない

int& r = a;
//rの参照先をaからbに変える方法はない

簡単に言えば、変数の別名をつける機能だ、と考えると、わかりやすいかもしれません。もちろん、constvolatileによる修飾も可能です。

先ほどのポインタの例を参照で書くと、以下のように書き直すことができます。

C++
//二次元空間上の座標を、極座標に変換する
//第3,第4引数で、動径と偏角を書き戻す
void to_polar(double x, double y, double& r, double& d){
   r = std::sqrt(x*x+y*y);
   d = std::atan2(y,x);
}

int main(void){
   double r;
   double d;

   to_polar(1.0, std::sqrt(3.0)/2, r, d);

   //r == 1
   //d == pi/60

   return 0;
}
C++
//参照渡しの関数
void func_ref(const data_type& Data){
   //Dataを使った処理
}

int main(void){
   data_type Data;

   //値渡しのように見えるが、参照渡しなのでコピーが発生しない!
   func_ref(Data);
}

2. 動的確保

C言語で実行時に要素を確保する必要がある場合には、mallocfreeを使ってメモリの動的確保を行い、そのアドレスをポインタで受け取ります。C++の場合は、クラスのコンストラクタ/ディストラクタを考慮したnewdelete演算子を使いますが、こちらも同じくポインタで受け取ります。

使用目的としては、例えばメモリを圧迫するような大規模なデータを、必要になってから確保したい場合でしょうか。

c言語
//非常に大きなサイズの構造体
struct data{
   double d1;
   double d2;
   //中略
   double d100;
};

int main(){
   struct data* ptr=NULL;

   //条件を満たしたときしか struct dataは使わない
   //最初から確保するのはもったいないので、条件を満たしたときだけ用意したい
   if(/*条件*/){
      ptr = malloc(sizeof(struct data));

      //ptrの使用
   }

   free(ptr);
}

また、配列の長さを実行時に決めたい場合にも動的確保を行います。

c言語
int DayNum = 0;
int* DayArray = 0;

//ひと月に含まれる日数を計算
if(Month = 2){
   DayNum = 28;
}else if(Month == 4 || Month == 6 || Month == 9 || Month = 11){
   DayNum = 30;
}else{
   DayNum = 31;
}

//日数分だけ領域を確保
DayArray = malloc(DayNum*sizeof(int));

//DayArrayの使用

free(DayArray);

C++ではさらに用途が増えていて、基底クラスのポインタを使って派生クラスを動的に確保して受け取ったりするのにも使います。

C++
//基底クラス 動物なら、鳴くことができるはず
struct animal{
   virtual void cry()=0;
};
//犬用派生クラス 当然、鳴き声はワンワン
struct dog: public animal{
   void cry()override{std::cout<<"bow wow";}
};
//猫用派生クラス 当然、鳴き声はニャー
struct cat: public animal{
   void cry()override{std::cout<<"meow";}
};

int main(){
   bool LikeDog;
   animal* Animal;

   //犬好きかどうかのフラグに応じて、Animalを作り分ける。
   if(LikeDog){
      Animal = new dog();
   }else{
      Animal = new cat();
   }

   //鳴いてもらう
   Animal->cry();

   delete Animal;
}

しかし、こうしたメモリの動的確保は、freedeleteの実行を忘れるとメモリリークが起きるなど、多くの問題をはらんだ方法です。

C++では、メモリの動的確保にはスマートポインタを使います。スマートポインタは、自動的に動的確保したメモリを解放してくれるオブジェクトで、それ以外はほとんど普通のポインタと同じように使えます。以前、スマートポインタの解説記事を書いたので、詳細はこちらをご覧ください。

例えば、一つ目の例はこんな感じで、newdeleteも使わずに書けます。

C++
int main(){
   std::unique_ptr<data> ptr;

   //条件を満たしたときしか struct dataは使わない
   //最初から確保するのはもったいないので、条件を満たしたときだけ用意したい
   if(/*条件*/){
      ptr = std::make_unique<data>();

      //ptrの使用
   }

   //ここで勝手に解放される
}

三つ目の例でも同じです。

C++
int main(){
   bool LikeDog;
   std::unique_ptr<animal> Animal;

   //犬好きかどうかのフラグに応じて、Animalを作り分ける。
   if(LikeDog){
      Animal = std::make_unique<dog>();
   }else{
      Animal = std::make_unique<cat>();
   }

   //鳴いてもらう
   Animal->cry();

   //ここで勝手に解放される
}

さて、二つ目の例では、配列を動的確保していました。スマートポインタでも配列を動的確保できます。ただ、C++では配列の動的確保はstd::vectorクラスを利用するのが便利です。このクラスは、任意のサイズの配列を動的に確保&自動的に解放してくれるだけでなく、配列の末尾にデータを追加・削除したり、配列のサイズを取得したりできる優れものです。

C++
std::vector<int> DayArray;

//ひと月に含まれる日数を計算
if(Month = 2){
   DayArray.assign(28);
}else if(Month == 4 || Month == 6 || Month == 9 || Month = 11){
   DayArray.assign(30);
}else{
   DayArray.assign(31);
}

//配列のサイズも分かる
DataArray.size();

//任意の要素にも、配列同様アクセスできる。
std::cout<< DataArray[5] <<std::endl;

//あとからデータを追加することも可能
//例えば末尾に一日追加
DataArray.push_back(0);

//解放も勝手にされる

3. 配列とポインタ

まず、最初に断っておきますが、配列はポインタではありません。ただ、どちらもアドレスを指している変数ですし、operator*operator[]も使えます。また、配列を参照するのにポインタが使われることから、混同されることが多いようです。

例えば、関数の引数として配列を受け取る場合、ポインタを使うことがあります。

c言語
//配列を受け取るために、ポインタを使う
int sum(const int* Array, int Size){
   int Value = 0;
   for(i=0;i<Size;++i) Value +=Array[i];
   return Value;
}
int main(void){
   int Sum;
   int Array[5] = {1,3,4,6,7};

   //配列とサイズを渡すと、合計値が返ってくる
   Sum = sum(Array, 5);
}

また、配列中の位置を指し示すためにも、ポインタが使われることがあります。例えば、下記の例では、Ptr1,Ptr2が「今どこまで配列を読んだか」を覚えておく変数として使われています。

c言語
//ソート済みの二つの配列
int Array1[100]={1,3,4,6,...251,256,258};
int Array2[100]={2,3,4,5,...236,243,251};

int Count = 0;

//先頭と末尾を覚える
int* Ptr1 = Array1;
int* Ptr2 = Array2;
int* End1 = Array1+100;
int* End2 = Array2+100;

//二つの配列の間に、同じ数字がいくつあるか数える
while(Ptr1!=End1 && Ptr2!=End2){
   if(*Ptr1 < *Ptr2){
      ++Ptr1;
   }else if(*Ptr1 > *Ptr2){
      ++Ptr2
   }else{
      ++Count;
      ++Ptr1;
      ++Ptr2;
   }
}

C++では、配列はコンテナと呼ばれる機能を使います。これは、STL (Standard Template Library) と呼ばれるC++の標準ライブラリの一部です。STLは、大きく分けて、コンテナイテレータアルゴリズムの三つの部品に分かれて構成されています。

コンテナは、複数のオブジェクトを管理するためのオブジェクトです。先ほど出てきたstd::vectorクラスも、コンテナの一種です。複数のデータの管理方法は、配列以外にも用途に応じてリスト構造や木構造など幾つか方法があります。コンテナは、そういった異なる方法で管理された複数のデータを、統一的に扱うためのオブジェクトです。

標準ライブラリにすでにいくつものコンテナが用意されています。いくつか例を挙げると、以下のようなものがあります。

  • std::array:C言語の配列の上位互換の固定長配列。
  • std::vector:末尾へのデータの挿入・削除が高速な可変長(動的に長さを変えられる)配列。
  • std::deque:末尾に加えて、先頭のデータの挿入・削除も高速な可変長配列。
  • std::list:中間位置のデータの挿入・削除が高速なデータ構造。
  • std::unordered_set:検索が非常に高速なデータ構造。
  • std::unordered_map:「(リンゴ, apple), (みかん, orange), (バナナ, banana)...」のように、二つのデータがペアになっているような辞書型データ構造。

まあ、分からない間は、「動的に配列の長さを変えたければstd::vector、その必要がなければstd::array」と覚えておくといいと思います。

コンテナを使うと、中のデータ構造が分からないのでポインタを使って配列を関数の引数にすることはできません。C++の流儀では、コンテナを使う場合には、イテレータを介してデータにアクセスするのが行儀がよいとされています。

イテレータが何かを説明する前に、まず一つ目の例を、c言語のまま書き直してみましょう。

c言語
//配列の先頭とサイズの代わりに、先頭と末尾のアドレスを受け取っている
int sum(const int* Begin, const int* End){
   int Value = 0;

   //ポインタなので、当然インクリメントすることもできる
   //*で実際の値を見に行くこともできる
   for(;Begin!=End; ++Begin) Value += *Begin;
   return Value;
}
int main(void){
   int Sum;
   int Array[5] = {1,3,4,6,7};

   //使うときは、Arrayの先頭と末尾のアドレスを渡す
   Sum = sum(Array, Array+5);
}

サイズの代わりに、末尾のアドレスを受け取っています。ちょっと変な気がするかもしれませんが、ちゃんとこれでも同じ処理ができていることが分かるでしょうか。ミソは、ポインタが指す対象を、配列の末尾まで順にインクリメントしなから読み出すことで、全体を走査している点です。いわば、ポインタを配列中の位置を指し示す道具として使っている、ということです。

イテレータとは、まさにこの配列中の位置を指し示す道具です。コンテナのデータ構造が配列でない場合があるので、ポインタ自体ではありませんが、実質的はポインタとほぼ同等に使うことができます。すべてのコンテナには、データ構造の先頭と末尾のイテレータを受け取るためのbegin/end関数が用意されており、これを介して配列全体を走査します。例えば、上記の例は、iteratorを使う場合には以下のように書き替えられます。

C++
//先頭と末尾のイテレータを受け取っている
//型が特定できないので、templateで宣言する必要がある
template<typename iterator>
int sum(iterator Begin, iterator End){
   //中身は、まったく上記例と同じ
   int Value = 0;

   for(;Begin!=End; ++Begin) Value += *Begin;
   return Value;
}
int main(void){
   int Sum;
   //固定長配列なのでarrayを使ってみる。
   //<>の中に、配列の要素の型と要素数を指定する。
   std::array<int,5> Array = {1,3,4,6,7};

   //使うときは、Arrayの先頭と末尾のイテレータを渡す
   Sum = sum(Array.begin(), Array.end());
}

ここで使われているtemplateとは、型を特定せず任意の型に対して宣言した関数を使えるようにする機能です。わざわざtemplateを使って、iteratorを介した関数を書くのは面倒な気もするかもしれませんが、コンテナの種類が変わっても使い続けることができるメリットがあるので、できるだけ template + iterator で書く癖をつけるとよいと思います。

二つ目の例も、iteratorを使って同じように書けます。こちらは、もともと「位置を覚えるためのポインタ」として使っていたので、イテレータで書き換えるのも簡単ですね。

C++
//ソート済みの二つの配列
//vectorは可変長配列なので、要素の型のみを<>の中に指定する
std::vector<int> Array1={1,3,4,6,...251,256,258};
std::vector<int> Array2={2,3,4,5,...236,243,251};

int Count = 0;

//先頭と末尾を覚える。autoで型推論機能(右辺の値から型を推定する機能)を使っている。
auto Ptr1 = Array1.begin();
auto Ptr2 = Array2.begin();
auto End1 = Array1.end();
auto End2 = Array2.end();

//ここ以下は、一切処理は書き替えていない
while(Ptr1!=End1 && Ptr2!=End2){
   if(*Ptr1 < *Ptr2){
      ++Ptr1;
   }else if(*Ptr1 > *Ptr2){
      ++Ptr2
   }else{
      ++Count;
      ++Ptr1;
      ++Ptr2;
   }
}

なお、余談ですがSTLの機能の三つめ、アルゴリズムは、イテレータを介したコンテナへの操作用関数をまとめた機能です。例えば、配列中の合計を求めるだけなら、numericヘッダの中にあるaccumurate関数が使えます。

C++
std::array<int, 100> Array;

int Sum = std::accumurate(Array.begin(), Array.end(), 0);

アルゴリズムに用意された関数を使えば、こんな複雑な処理も簡単にかけてしまいます。

C++
//サイズ100の配列を用意
st::vector<int> v(100,0);

//フィボナッチ数列を100個作成
int n0 = 0;
int n1 = 1;
std::generate(v.begin(),v.end(),[&]->int{std::swap(n0,n1);n1+=n0; return n1;});

//奇数だけ抜き出す
auto End = std::remove_if(v.begin(),v.end(),[](int i)->bool{return i%2==0;});

//13で割った剰余を求める
std::transform(v.begin(), End, v.begin(), [](int i)->int{return i%13;});

//重複を取り除く
End = std::unique(v.begin(),End);

//乗算を計算する
int Ans = std::accumulate(v.begin(), End, 1, [](int i, int j)->int{return i*j;});

コンテナに手を出すなら、ぜひアルゴリズムもセットで覚えてみてください。

4. 関数ポインタ

最後は、関数ポインタです。実は、変数と同じように関数もポインタを持っています。関数ポインタを使うと、あらかじめ覚えておいた関数を、好きなタイミングで実行できたりします。

c言語
void dog(){printf("bowwow");}
void cat(){printf("meow");}

int main(void){
   //ここで、戻り値void、引数voidの関数ポインタを用意
   void (*CryFuncPtr)(void);

   if(LikeDog){
      CryFuncPtr = dog;
   }else{
      CryFuncPtr = cat;
   }

   printf("My pet is crying!");

   //これで関数を呼び出せる
   CryFuncPtr(); 
}

知っていると便利なテクニックではありますが、関数ポインタは当然ですが関数のアドレスしか利用できないため、後述するようなC++で追加された関数オブジェクトやラムダ式といった類似の「関数のように扱える」機能と混用することができませんでした。

C++では、関数ポインタより汎用的に扱える std::functionクラスが用意されています。std::functionはテンプレートクラスであり、std::function< ReturnType (ArgType1, ArgType2, ...) >のような形で宣言します。

例えば、以下のような感じです。

  • 戻り値がint、引数がvoidなら、std::function<int(void)>
  • 戻り値がdouble、引数がint二つなら、std::function<double(int,int)>

今回の例の場合は、こんな感じでしょうか。

C++
int main(void){
   //voidが戻り値、voidが引数の関数を持つことができる変数
   std::function<void(void)> CryFunc;

   if(LikeDog){
      CryFunc = dog;
   }else{
      CryFunc = cat;
   }

   std::cout<<"My pet is crying!";
   CryFunc();
}

std::functionは、関数ポインタだけでなく関数オブジェクトと呼ばれる operator()(...)をメンバ関数として持っている関数のようなオブジェクトも入れることができます。

C++
//double型一つを引数、double型を戻り値にする関数オブジェクト
struct pow_functor{
   double Base;
   pow_functor(double Base_):Base(Base_){}
   double operator()(double val){return std::pow(val,Base);}
};
std::function<double(double)> Func = pow_functor(3.0);
double Ans = Func(2.0);
//Ans==8.0

むろん、ラムダ式(無名の関数オブジェクト)も受け取ることができます。

C++
std::function<double(double,double)> Func = [](double val1, double val2)->double{return std::max(val1*val1,val2*val2);};

double Ans = Func(10,-12);
//Ans==144

ただし、std::functionは通常のポインタサイズよりも必要なメモリ量が多く、関数の呼び出しにも若干のオーバーヘッドが存在します。実際には、多くの場合さほど気にするほどでもありませんが、大量に用意したり、非常に小さな処理の関数を何度も繰り返し呼び出す場合には、他の方法とも比較検討したうえで使うべきでしょう。

まとめ

以上のように、C言語で複数の文脈で使われるポインタは、C++では何かしらの代替手段が用意されており、多くの場合はポインタを扱う必要がないことが分かります。

  • スコープ外のアドレスにアクセスするための参照としてのポインタ ⇒ 参照を使う
  • 要素や配列の動的確保のためのポインタ ⇒ スマートポインタ(またはコンテナ)を使う
  • 配列としてのポインタ ⇒ コンテナを使う
  • 配列中の要素を指し示すポインタ ⇒ イテレータを使う
  • 関数を保持するためのポインタ ⇒ std::functionを使う

もちろん、これらの機能があればポインタが不要になるわけではありません。しかし、C++の便利な機能を使うことで、確実に「ややこしいポインタ」の使用頻度は下がることになります。

今回の記事では、各機能の使い方については深く触れませんでしたが、実際に「使ってみよう」と思うきっかけとなれば幸いです。

以上、初心者 C++er Advent Calendar 2015 の 23日目の参加記事でした。
明日24日の記事は、stefafafanさんの「プログラミング初心者でも必ず絶対にわかるC++超入門」です!

245
248
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
245
248