Help us understand the problem. What is going on with this article?

c++で固定長配列[]に長さのわからないデータを入れ、さらに関数の戻り値にしてみよう

0.要約

固定長配列[]に可変長データを書き込みたければ次のようにする。
(recvは引数として渡されたchar型配列に値を書き込む任意の関数)

#include<iostream>
#include<vector>
#include<string.h>

int main()
{
    int response_size = 10, int timeout=500;
    std::vector<char> v = std::vector<char>(response_size, '0');//vectorで動的確保。
    char* char_arr = &v[0];// vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。
    recv(char_arr, response_size, timeout);//char配列にレスポンスが流し込まれる。

    //以降、char_arrは、あたかも
    //char char_arr[response_size] = {0};
    //のように宣言されたかのように利用することが出来る。

    return 1;
}

これは、vector内のメモリ配置と固定長配列のメモリ配置が同じであることを利用したトンチである。

関数fの戻り値にしたい場合は、ベクトルvをグローバル変数にするだけでよい。
具体例は次の通り。

#include<iostream>
#include<vector>
#include<string.h>

std::vector<char> v;
char* f(int response_size, int timeout)
{
    v = std::vector<char>(response_size, '0');//vectorで動的確保。
    recv(&v[0], response_size, timeout);//レスポンスが流し込まれる。

    //以降、vは、あたかも
    //char型固定長配列であるかのように利用することが出来る。

    return &v[0];
}

1.どんな時に役立つか(想定例)

vectorを使って処理したいのに、何らかの理由で配列しか使えない場合など。

例えばwifi通信モジュールをマイコンから操作するプログラムをマイコンへ書き込んでいるとする。
通信モジュールのライブラリには、HTTPレスポンスを受け取る関数
void recv(char* buf, int len, int timeout)
が用意されているとする。

この関数はlen[byte]未満のデータを受け取った場合、
それをchar配列bufに入れてくれるが、それ以上のデータを受け取った場合、データは途中で途切れてしまう。

今、関数recvを使って、HTTPレスポンスを返却する
char* get_response(int response_size, int timeout)
を作るとする。(リクエストは別の関数で既に送信済みであるものとする)

#include<iostream>

char* get_response(int response_size, int timeout)
{
    char buf[response_size] = {0};
    recv(buf, timeout);
    return buf;
}

のようにすることはできない。bufのサイズをresponse_max_sizeという変数で指定すると、CやC++では怒られるのだ。(だから固定長配列という。)

2.解決策とその理論

グローバルなvectorを利用すればよい。

#include<iostream>
#include<vector>
#include<string.h>

std::vector<char> v;//とても大事です
char* get_response(int response_size, int timeout)
{
    v = std::vector<char>(response_size, '0');//vectorで動的確保。
    char* char_arr = &v[0];// ★vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。
    recv(char_arr, response_size, timeout);//char配列にレスポンスが流し込まれる。
    return char_arr;
}

このようにすると、配列のサイズを指定することなく、char*型のレスポンスを得ることが出来る。

2-1. 全体説明

やっていることとしては、まずvectorによって動的にメモリを確保し、
v[0]からv[response_size]まですべてに'0'を代入しておく。
次にそのメモリをあたかも固定長配列の物であるかのように偽装工作する(★)
char* char_arr = &v[0];とすることで、
「配列char_arrのアドレスを『vのアドレスに入った値v[0]』のアドレスにせよ」ということになる※(2-4節「蛇足」も参考になるかも)。
vのアドレスに入った値』はchar型なので、そのアドレスはchar*となり、辻褄が合うのである。
以上により、
char_arr[0] == v[0]が、
char_arr[1] == v[1]が、
...
char_arr[response_size] == v[response_size]
約束される。

2-2. vectorの利点

さて、char_arr[i]は固定長配列であって、v[i]はベクトルである。
char_arr[0] == v[0]が成立するのはよいにしても、
なぜchar_arr[1以降] == v[1以降]までもが成立するのだろうか。
この謎は、固定長配列やベクトルが、どのようにしてメモリを確保するかを知ることで解決する。
まず、固定長配列において、&char_arr[i]&char_arr[0]+iと等価である。
つまり、char_arr[0], char_arr[1], char_arr[2]のアドレスは
char_arr+0, char_arr+1, char_arr+2のように隣り合っている
これと全く同じことが、実はベクトルにおいても言える
すなわちv[0], v[1], v[2]のアドレスは
&v[0]+0, &v[0]+1, &v[0]+2のようになっている。
char_arr = &v[0];とすることで、
char_arr+i == &v[0]+i;が約束されることはすぐにお分かりいただけるだろう。
そしてその意味は、
char_arr[i]のアドレス == v[i]のアドレス
なのである。

2-3. グローバルにする利点

ソースコードにおいて、ベクトルvはグローバル変数となっている。
グローバル変数とは、関数の外で宣言されているがために、関数終了後にも引き続き有効である変数のことである。
この性質が、実はchar_arrを支えている。例えるならvchar_arrを延命治療しているのである。
char_arrは関数get_response(int, int)の内部で定義されている。
つまり、本来、返却値は、返却後ただちに死ぬのである。
(そんなことが起きた場合の)事態の深刻さが分かるだろうか。get_response関数をペットショップに例えるのなら、
このショップは、販売後ただちに死ぬような動物を、顧客に売りつけるのである。
ペットショップ内でしか生きられない動物を治療し、顧客の家でも元気に走り回れるようにしてくれているのが、グローバル変数vなのである。

char_arrの各要素は、vの各要素と同じアドレスを持つのであった。
つまり、関数がchar_arrを返却し、「char型の配列(の先頭要素のアドレス)です」と言い張っているのは、
実はベクトルvの先頭要素のアドレスを返却しているのである。
そしてこのvはグローバルであるからこそ、関数外に出ても生き続けるため、全く同じ実体であるchar_arrも生き続けるのである。
実際3-1節にて、「関数内でchar_arrを定義せず、すべて&v[0]で代用しても、振る舞いは全く同じである」ことを検証している。

また3-2節では、「vをグローバルでない変数(ローカル変数)に置き換えた場合どうなってしまうのか」という検証も行っている。

2-4.蛇足

ベクトルvでは、配列と違い、v&v[0]と等価であるとは保証されていないため、
char* char_arr = v;としてはいけない。
一方、固定長配列char_arrではchar_arr&char_arr[0]と等価であることが保証されているので、
char_arr = &v[0];
*char_arr = *(&v[0]);つまり
*(&char_arr[0]) = *(&v[0]);とみることができるから、
char_arr[0] = v[0];のように考えることが出来るのである。

さらに蛇足して、
char_arr = &v[0];なんて捻くれた書き方せずとも、
&char_arr[0] = &v[0];//Aとか
char_arr[0] = v[0];//Bとか
char_arr = v;//Cでいいじゃないか」
という疑問に答えよう。
Aは「演算子&が左辺に来るなんてダメだから」である。
Bではv[0]の値がchar_arr[0]へコピーされるだけで終わってしまう。
v[1]以降がchar_arr[1]以降へコピーされるという反応が起こらないのである。
Cでは抽象的過ぎる。ベクトルのアドレスを可変長配列のアドレスへ代入しようとしても、そもそも型が異なるため、おそらくコンパイルも通らないだろう。

3.実験

実際に動作するソースコード全体は次の通り。
(recv関数は、常に「hello world」を返すものとする)

test.cpp
#include <iostream>
#include <vector>
#include <string.h>

char response_[] = "hello world";//今回はrecvは常に「hello world」を返すものとする。
char resp_err[] = "";//但し、サイズが足りない場合、空文字列を返却する。
void recv(char* char_arr, int size, int timeout)
{
    if(strlen(response_) < size)
        for(int i=0; i < strlen(response_)+1; i++)
            char_arr[i] = response_[i];
    else
        for(int i=0; i < strlen(resp_err)+1; i++)
            char_arr[i] = resp_err[i];
}

std::vector<char> v;//とても大事です
char* get_response(int response_size, int timeout)
{
    v = std::vector<char>(response_size, '0');//vectorで動的確保。
    char* char_arr = &v[0];// ★vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。
    recv(char_arr, response_size, timeout);//char配列にレスポンスが流し込まれる。
    return char_arr;
}


bool truncated(const char* response){return strlen(response)==0;}

int main()
{
    int size;
    int limit = 100;
    const char* response;
    for(size = 1; size < limit; size++)
    {
        response = get_response(size, 100);
        if(!truncated(response))break;
    }
    if(size >= limit)
        printf("error: response size too long.");
    else
        printf("ok: '%s', where size is %d", response, strlen(response));

}

main関数内の変数limitが13以上である場合、

ok: 'hello world', where size is 11

のように表示される。
limitを12以下にすると、次のようになる。

error: response size too long.

3-1. 追加実験1

get_response関数を次のように書き換えても、まったく同じ結果を得る。

get_response関数の別のパターン
std::vector<char> v;//とても大事です
char* get_response(int response_size, int timeout)
{
    v = std::vector<char>(response_size, '0');//vectorで動的確保。
    recv(&v[0], response_size, timeout);//vectorにレスポンスが流し込まれる。
    return &v[0];
}

この書き換えでは、char_arrを定義するのをやめ、すべて&v[0]で代用するものである。
両者の実体は全く同じなのだから、結果に影響を及ぼさないのは当然である。

3-2. 追加実験2

グローバルなvectorをローカルに変えた場合にどのような結果を得るか、実験する。
そのために、get_response関数を次のように書き換える。

不良なget_response関数
char* get_response(int response_size, int timeout)
{
    std::vector<char> v = std::vector<char>(response_size, '0');//vectorで動的確保。
    char* char_arr = &v[0];// ★vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。
    recv(char_arr, response_size, timeout);//vectorにレスポンスが流し込まれる。
    return char_arr;
}

結果、次のような表示を得た。

ok: 'タks', where size is 3

これは、メモリをローカル変数として確保したことにより、get_response関数の処理が終了した後、処理系によって自動的に無関係の値を上書きされてしまったために起きたものである。

17ec084
Googleの同級生です※1。実験または自然科学的手法で正当性を示せない知識体系は学問でも科学でもないと思っています。 したがって、すべての人間カガク・社会カガク・イデオロギーは統計的事実に基づくべきだと思います。 だから、統計・データサイエンスに関わるソフトウェアやプログラムを開発するのが夢です。 ※1 僕の生まれた年もGoogleの設立された年も1998年
http://rights-for.men/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away