0.要約
固定長配列[]に可変長データを書き込みたければ次のようにする。
(recv
は引数として渡されたchar
型配列に値を書き込む任意の関数)
#include<iostream>
#include<vector>
#include<string.h>
intmain(){intresponse_size=10,inttimeout=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};//のように宣言されたかのように利用することが出来る。return1;}
これは、vector
内のメモリ配置と固定長配列のメモリ配置が同じであることを利用したトンチである。
関数fの戻り値にしたい場合は、ベクトルv
をグローバル変数にするだけでよい。
具体例は次の通り。
#include<iostream>
#include<vector>
#include<string.h>
std::vector<char> v;char*f(intresponse_size,inttimeout){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(intresponse_size,inttimeout){charbuf[response_size]={0};recv(buf,timeout);returnbuf;}
のようにすることはできない。buf
のサイズをresponse_max_size
という変数で指定すると、CやC++では怒られるのだ。(だから固定長配列という。)
2.解決策とその理論
グローバルなvector
を利用すればよい。
#include<iostream>
#include<vector>
#include<string.h>
std::vector<char>v;//とても大事ですchar*get_response(intresponse_size,inttimeout){v=std::vector<char>(response_size,'0');//vectorで動的確保。char*char_arr=&v[0];// ★vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。recv(char_arr,response_size,timeout);//char配列にレスポンスが流し込まれる。returnchar_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
を支えている。例えるならv
がchar_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」を返すものとする)
#include <iostream>
#include <vector>
#include <string.h>
charresponse_[]="hello world";//今回はrecvは常に「hello world」を返すものとする。charresp_err[]="";//但し、サイズが足りない場合、空文字列を返却する。voidrecv(char*char_arr,intsize,inttimeout){if(strlen(response_)<size)for(inti=0;i<strlen(response_)+1;i++)char_arr[i]=response_[i];elsefor(inti=0;i<strlen(resp_err)+1;i++)char_arr[i]=resp_err[i];}std::vector<char>v;//とても大事ですchar*get_response(intresponse_size,inttimeout){v=std::vector<char>(response_size,'0');//vectorで動的確保。char*char_arr=&v[0];// ★vのアドレス(に入った値のアドレス)をchar配列のアドレスとする。recv(char_arr,response_size,timeout);//char配列にレスポンスが流し込まれる。returnchar_arr;}booltruncated(constchar*response){returnstrlen(response)==0;}intmain(){intsize;intlimit=100;constchar*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.");elseprintf("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
関数を次のように書き換えても、まったく同じ結果を得る。
std::vector<char>v;//とても大事ですchar*get_response(intresponse_size,inttimeout){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
関数を次のように書き換える。
char*get_response(intresponse_size,inttimeout){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にレスポンスが流し込まれる。returnchar_arr;}
結果、次のような表示を得た。
ok: 'タks', where size is 3
これは、メモリをローカル変数として確保したことにより、get_response
関数の処理が終了した後、処理系によって自動的に無関係の値を上書きされてしまったために起きたものである。