web-dev-qa-db-ja.com

C ++のポインターの何が悪いのですか?

C++でコーディングするときにポインターが推奨されないのはなぜですか?

ネットワークソケットのように、有効にするために初期化が必要なオブジェクトをカプセル化するクラスがあるとします。

// Blah manages some data and transmits it over a socket
class socket; // forward declaration, so Nice weak linkage.      

class blah
{
  ... stuff 
  TcpSocket *socket;
}

~blah {
   // TcpSocket dtor handles disconnect
   delete socket; // or better, wrap it in a smart pointer
}

Ctorは、socketがNULLとしてマークされていることを確認し、その後、オブジェクトを初期化するための情報があるときにコードの後半でマークします。

// initialising blah
if ( !socket ) {
   // I know socket hasn't been created/connected
   // create it in a known initialised state and handle any errors 
   // RAII is a good thing ! 
   socket = new TcpSocket(ip,port);
}

// and when i actually need to use it
if (socket) {
   // if socket exists then it must be connected and valid 
}

これは、スタックをソケットに配置し、プログラムの開始時に「保留」状態で作成し、使用する前にisOK()またはisConnected()関数を継続的にチェックする必要がある場合よりも優れているようです。
さらに、TcpSocket ctorが例外をスローする場合、プログラムの開始時よりもTcp接続が確立された時点で処理する方がはるかに簡単です。

明らかに、ソケットは単なる例ですが、内部状態のあるカプセル化されたオブジェクトをnewで作成および初期化してはならない場合を考えるのに苦労しています。

5
Martin Beckett

一般に、プログラミングの構造と手法は、特定のタスクに利用できる「より良い」代替手段がある場合、「悪い」と一般的に見なされています。ポインターの使用は多くの場所で技術的に正しい場合がありますが、C++ではrawポインターの使用がより良い代替手段がない状況が発生することはまれです。

ほとんどの場合、参照、スマートポインター、イテレーター、および標準ライブラリコンテナーを使用すると、ポインターを使用する同等のソリューションと比較して、より安全でクリーンな、より慣用的なコードになります。そして、通常、プログラマーに追加のコストはかかりません(実際には、かなり低コストで頻繁に行われます)。

生のポインタが最も賢明なオプションである場合は常にあり、そのような場合、ポインタを回避する「賢い」方法を見つけようとしても誰も得ません。特に、生のポインタを回避することは、他の方法では変更する必要がなかった可能性のある、作業中のレガシーコードに大きな重大な変更を加えるリスクがあることを意味する場合。しかし、新しいコードでは、少なくともこれらの状況は、最新のC++ 11実装を使用している誰にとっても珍しいものです。

19
Ben Cottrell

ポインタ持っているあなたが主張するように、使用することにも十分な理由があります。ただし、ほとんどの場合、C++ 11で導入された標準化されたスマートポインターを使用せずに、安全性を高めながら、ポインターがもたらす柔軟性を維持する必要があります。

ただし、C++にあまり慣れていない人にとっては混乱を招くように見えることもあるので、メンテナンスの観点からは、最も単純で理解しやすい手法と方法論に基づいて実装するのが最善です。

2
zxcdw

ポインタは間違いなく悪いことではありません。最高のパフォーマンスを得るために、メモリに直接アクセスできます。
ポインタをクラスの周りに渡す必要があるときに、ライフタイムを追跡することが難しい場合があります。これは、スマートポインタが役立つ場所です。パフォーマンスへの影響を少しでも犠牲にできる場合は、可能です。利用可能な場合は、スマートポインターを使用してください。

スマートポインターを使用することの悪い点は、クラスのヘッダーファイルの依存関係です。生のポインターを使用すると、クラス/構造体を前方宣言するだけで、コンパイル時に依存関係を明確にすることができます。
また、使用するスマートポインタがマルチスレッドアプリケーションの場合にスレッドセーフであることを確認する必要があります。

あなたのクラスが十分に単純であるならば、私はスマートなものよりも通常のポインタを使い続けるでしょう。

1
albert

私の意見では、ポインタは扱いにくいまたはbadとして記述されています。なぜなら、それらはアプリケーション設計の2つの重要な側面を壊すからです。

  • 意味論
  • 構文

覚えていれば、プログラミングの世界の基本的な概念は、黒板に4と書いた場合、黒板に書いた内容にコンテキストとセマンティクスを与えるまでは意味がないということです。 45と書きました。黒板に何が私の年齢だと言います。45は実際の情報として取得できますが、コンテキスト/セマンティクスがなければ、45は役に立たず、明確な解釈がありません。実際の情報とその表現は異なります。

これはOpenGLのようなテクノロジーではさらに明確です。コンテキストを指定しない場合、ポインターを使用してほとんど何もできません。コンテキストを指定しない場合、C++でも同じことが起こります(target = type)ポインタまたは参照の場合、ポインタだけでは何も意味がなく、役に立たない。

アプリケーションのビジネスロジックとコンポーネントのライフタイムについて境界を設定するため、構文も重要です。ポインターはすべての境界とビジネスロジックを潜在的に壊す可能性があります。それでもなお、ポインターには参照が含まれているため、実際にはありません。状態、それらはアプリケーションの存続期間中に処理できないメモリ内の何かを指すだけです。それらは、明確に定義された状態と明確に定義された存続期間を持つオブジェクトではなく、メモリアドレスを持つポストイットのようなものです。

古いC++とC++ 11の主な違いはすべてデザインに関する違いです。つまり、アプリケーションのロジックを駆動できる構文とセマンティクスです。スマートポインターは単なるポインターにすぎませんが、ポインター自体に含まれる特定のデザインに関連する大きなプラスがあるため、ポインターをより細かく制御できます。

結局、ポインターは状態を持つことができず、コンテキストを処理する必要があります。これは、安全で優れたアプリケーション設計が通常受け入れることができないものです。スマートポインターは、基本的にデザイン指向の世界における一歩です。プログラミングの世界への現代的なアプローチ。

1
user827992

ポインターは、次の理由で不良です。

  1. メモリ領域に関する情報が消えます。 TcpSocketメモリがblahオブジェクトの内部にあるか外部にあるかは明確ではなくなりました。参照と構成で修正できます。 (参照=外側、構成=内側)
  2. ポインターはNULLにすることができます。関数呼び出しの振る舞いが尊重されない場合に、プログラムがどれだけうまく機能することを期待するか。 connect()関数を呼び出すと、接続が向上しますが、一部のptrがNULLであるという理由だけで失敗するのは適切な動作ではありません。一部のprevious呼び出しが失敗したために失敗するのは、良い動作ではありません。
  3. Ptr == NULLのような動的な振る舞いはブールで実行されるべきであり、ptr == NULLであるこの種の隠された振る舞いではありません。次に、ブール値がポインタとは関係なく適切な名前を持っていると、失敗の理由を簡単に理解できます。
  4. 障害に対する最善の選択は、それらが決して起こらないことです。
  5. 2番目に最適な選択肢は、ネットワークが利用できない場合にアプリケーションの一部を無効にすることです。たとえば、新しい情報が表示されず、接続ができるだけ早く復元されます。
  6. ソケット関数が失敗する可能性がある場合は、関数プロトタイプにその情報を含めることをお勧めします。 void connect()は受け入れられません。エラーコードを含むint connect()の方が適しています。失敗ビットを含むオブジェクトを返すことも問題ありません。

どんな種類の選択肢が利用できるかの例:(構成1)

class Blah {
public:
  Blah(ip,port) : s(ip,port) { }
private:
  TcpSocket s;
  bool failbit;
};

または参照付き:

class Blah {
public:
  Blah(TcpSocket &s) : s(s) { }
private:
  TcpSocket &s;
};

接続を後で作成する必要がある場合(コンストラクターでアドレスが十分に早期に知られていない場合など)、これを使用します。

class Blah {
public:
   Blah() : sock(NULL) { }
   TcpSocket &connect(ip, port) {
     delete sock;
     sock = new TcpSocket(ip,port);
     return *sock;
    }
  ~Blah() { delete sock; }
private:
  TcpSocket *sock;
};

しかし、これは通常最良の選択肢ではありません...(connectを2回呼び出し、前の1つは接続を失います-返されたTcpSocketはどこにも格納されてはいけません)次の代替はstd :: vector <TcpSocket *> ...を必要とします.

class Blah {
public:
   Blah() : vec(), error("logfile.txt") { }
   int connect(ip,port) {
      vec.Push_back(new TcpSocket(ip,port));
      return vec.size()-1;
   }
   int write(int conn, char *data) {
      if (vec[conn])
         { vec[conn]->write(data); return OK; }
      else return ERR;
   }
  TcpSocket &connection(int conn) const 
      { if (vec[conn]) return *vec[conn]; else return error; }
  void disconnect(int conn) { delete vec[conn]; vec[conn]=NULL; }
   ~Blah() { for(int i=0;i<vec.size();i++) delete vec[i]; }
private:
   std::vector<TcpSocket*> vec;
   TcpSocket error;
};

インデックスを接続std :: vectorに返すと、接続が削除されないことに注意してください(インデックスが壊れる/ NULLにする必要があります)。最後の1つは、connect()の戻り値とwrite()の戻り値が異なるという問題があるだけです。ある種の型チェックトリックを使用して、異なるintにする必要があります。

struct Err { int error; };
Err write(int conn, char *data);
0
tp1