ryuuga89
@ryuuga89

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

C++とPythonの違いについての質問

解決したいこと

プログラミングを勉強中の初心者です。C++にて実装した二分探索を、同じ発想でPythonにも実装しようとしたところ、エラーが発生しました。その理由がどうしてもわからず、質問させていただきます。

発生している問題・エラー

以前書いたC++での二分探索のコードが以下です。

二分探索
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> date(n);
    for (int i=0;i<n;i++) {
        cin >> date[i];
    }

    int target;
    cin >> target;

    int hi=n,lo=0,mid;

    while (lo<=hi) {
        mid=((lo+hi)/2);
        if (target<=date[mid]) hi=(mid-1);
        if (target>=date[mid]) lo=(mid+1);

        //一致しなかった場合は範囲を半分に。
        //一致した場合は、lo>hiとなりwhileが終了
    }

    int pos=(lo-1);//posは見つかった場合はそのレコードの場所を、見つからなかった場合は直前の場所を指し示す。
    bool found=(lo==hi+2);//foundは見つかったかどうかのbool値

    if (found) {
        cout << "見つかりました" << endl;
        cout << "場所は" << pos << "です" << endl;
    } else {
        cout << "見つかりませんでした" << endl;
        cout << "targetの直前の場所は" << pos << "です" << endl;
    }
}

このコードを、同じようにpythonで実装しようとして、次のコードを書きました。(こちらは関数であり、出力の様式が少し異なりますが、二分探索自体の挙動は同じであると思います)

二分探索
def search(ordered, item):
    hi=len(ordered)
    lo=0

    while lo <= hi:
      mid=((lo+hi)//2)
      if item<=ordered[mid]:
        hi=(mid-1)
      if item>=ordered[mid]:
        lo=(mid+1)

    if lo==hi+2:
      return lo-1
    else:
      return None

問題点

探索する元のリストが[1,2,3,4,5,6,7,8,9,10]で、探索する対象が11の時、C++では正しく動作したのに対して、pythonでは配列外を参照したことによるエラーが発生しました。そこで2行目のhi=len(ordered)を、hi=len(ordered)-1としたところ、正しく動作しました。

質問

なぜこのようなことが発生するのでしょうか。pythonとC++で配列の扱いに違いがあるのでしょうか。
また、hi=len(ordered)-1とすれば、このpythonのコードは適切に二分探索を行うことができますか。

終わりに

拙い文章とコードですが、ご教授いただけたら幸いです。

0

4Answer

探索する元のリストが[1,2,3,4,5,6,7,8,9,10]で、探索する対象が11の時、C++では正しく動作したのに対して、

正しく動作しません。 範囲外アクセスの結果は未定義です。 C++ で言う未定義とは、何が起こるかわからない (コンパイラは何をしてもかまわない) という意味です。

偶然にもアクセスした場所が他の用途に使われていない場合に一見して問題として現れないこともそれなりにありますが、エラーが検出されないのであって正しく動作しているわけではありません。

2Like

C++では正しく動作したのに対して、pythonでは配列外を参照したことによるエラーが発生しました。

正しく動作したわけではなくて、C++ は配列外を参照しないというのはプログラマの責任ということで配列外を参照したかどうかのチェックはしてないので、エラーを出さないです。

なので、もし配列外を参照するコードがあるのに期待通り動いたということですと、運よくたまたま動いたに過ぎないです。

書き込むこともできるので、昔は OS が使っているメモリに書き込んで内容を壊してしまって、ハングアップしたという失敗が自分にもありました。

2Like

len(ordered)はordered[0]も含めた要素数を返しますから、orderedの最大のインデックス+1の値がlen(ordered)です。(わざわざ言うことでもないと思いますが、念のため)
配列の扱いはまあ小さなところでは色々違うでしょうけど、大枠は同じです。
コンパイラやインタプリタの厳密性の違いですかね。

1Like

C++では配列は単なる連続したメモリ領域ですが、Pythonのそれはオブジェクトです
中身が同一でも、その扱いは異なります

Pythonでは、メモリを確保する他に、そこに格納されるデータの保護、及び操作方法をオブジェクトの機能として提供します
言わばオブジェクトで扱うデータと機能がパッケージングされたものであり、クラスの考え方の基礎でもあります
そのため領域外へのアクセスは事前に検知されます

対するC++の配列は純粋なデータの集まりであり、それを操作する手段は提供されません
ですのでそのままでは配列内にアクセスすることすらできません
(vectorもクラスではないのか、という疑問に対しては後述)

ではどうやってアクセスするかというと、その回答が変数から返される値にあります
C++では変数からポインタが返されます
個々の要素のアドレスを用いることで、ポインタの機能として内部へのアクセスを可能とします
ただポインタは本来メモリ内の好きな場所を指し示せるので、そこが配列の内外だろうが関係ありません
なので配列の領域外への参照を阻止する手段が用意できません

対するPythonでは、変数がオブジェクトへの参照を返します
配列の領域がオブジェクトのデータ型として認識されるため、そのオブジェクトの機能を使用することが可能であり、その一端として内部へのアクセスに常にチェック機構が働きます
この機構のおかげで、領域外アクセスに対してエラーを返すことができます

では何故同じクラスであるvectorにはこれがないのでしょうか
その答えは以下のリファレンスにあります

std::
vector::
operator[]

この関数は、 at() メンバ関数とちがって境界チェックを行うことが規定されない。標準ライブラリの実装によっては assert(n < size()) による境界チェックが行われる場合がある

つまりvectorのチェック機構はライブラリ依存であり、標準では実装されないということです
オブジェクトの仕様として境界チェックがサポートされないために、領域外にもアクセス可能ということになります

このように、C++とPythonではオブジェクト内での配列の管理手法に相違があります
クラスを用いることで、同じデータ構造でも、その扱いを差別化することができます

反対にC++でも、チェック機構を採用しているクラスが存在することになります
配列を扱う際には、目的に適したクラスを探してみるのも良いでしょう

1Like

Your answer might help someone💌