VC++再独習中で配列とポインタが・・・(改定)(’14/10)
(一度アップしましたが、大間違いをしてましたので、1日で削除し、後半を全面的
書き換えしましたm(__)m)
無料のVisialStudio2013Express(マイクロソフトアカント登録済)を入手して以来、
元々、齧っては挫折していたwin32API+(VC++)プログラミングの再々独習をしてい
ます。と言っても、投稿したりするようなソフト制作を目的にしているわけではな
く、アセンブラに一番近いC言語に関する知的興味だけですけどね)。
で、以前の記憶、過去に購入していた手持ちの書籍、ネット記事、息子の示唆など
が独習の頼りなんですが、以前、それで挫折したポインタについてはやっと今回、
かなりの点で理解が進んできています。これもネットの恩恵ですね(^◇^)。
今回、それを理解できる道筋になったのは、文字列操作関数で色々と戸惑い、
テストしたり調べたことでした。
元々、ちょっと行方不明になってしまっている以前、私のバイブルみたく読んでいた
10年ほど前、本屋で見つけたMicrosoft出版の「Windowsプログラミング」という本の
中で、世の中にはANSIとUNICODEという体系があって、特に文字列に関する関数
の扱いは互換性で注意が必要であることが述べられており、そのうち文字リテラル
についてはTEXTマクロを使うべきとあって、すでに私的には、TEXT("xxxxx")という
表記は言わば、自然になっていました。勿論、charでなくTCHAR型を用いることも含
めてです。
ただ当時はUNICODEと言われても、まだ何か特別なシステムだけのものという勝手
な思いが私の中にあり(Xp時代に所有していたVC++2002.NETはTEXTマクロは使わ
なくてもよく、charも使えたからですが)、その本の中の1項を割いて述べられ
ていたUNICODEは飛ばし読みしていました(^^;。
しかるに、今回のVisualStudio2013はコンパイラがデフォではUNICODE用になって
しまったんですね。とりあえず、TEXTマクロとTCHARは違和感ありませんが、その他
で戸惑うことが沢山でてきました。
最初に躓いたのは、最初UNICODE対応と知らず、ネットにある参照コード例に出て
くる文字列数獲得関数のstrlenが使えなかったことでした。
で、ネット調べてみて、やっと今のVisualStudioはUNICODEをデフォにしていることを
知り、合わせて、UNICODE用はwcslenであることを知りました。で、これが今回の再
独習で新たに覚えた最初の文字列操作関数でした。
ちなみに、最初はWindows8.1がUNICODE対応かと思い違いをしてました(^^;。
尚、文字列を画面に出すために必要な制御文字を含めて文字バッファにセットする
のに必要なwsprintf関数は数少ない共用関数であるので当初あまり困りませんでし
たけど、興味に釣られて他の文字列操作関数を試している中で、再び右往左往して
しまいました(^^;。
まず、ちょっとネット上で触れられていた話題に釣られてwcscpyという関数を試して
みたのですが、ここで述べましたように嵌ってしまいました。
で、ま、こんなものはもうこれ以上試すのやめたとしていました。
ところが、再び、問題にぶちあたってしまいました。それは、たまたま、累乗の関数
powを使ったときに出てきた問題点ゆえでした。そこまでしっかりとMDSNライブラリ
を見ていなかったために全然、知らなかったんですが、wsprintfって'%f'というdouble
やfloat型に対応する制御文字をサポートしていなんですねぇ。ネット見ましたら、結
構一つの欠点として有名な話のようでした。
で、さあ困った・・・息子に聞いたら、ANSIならsprintfが使えるけど、UNICODEなら
swprintfだろうということで、早速試してみたところ、これも前のwcscpyと同様、
2013Expressのコンパイラはエラー表示。やはり、代わりに、swprintf_sを使えって・・・
しかし、ここで触れたwcscpy_s同様、これも一つパラメータが多い・・・
ところが、swprintf_sのMSDNライブラリよく読んでましたら、なにやら「テンプレート」
ということに関して詳細は貼ってあるリンクを見ろとありました。で、そこを見て・・・
(⇒(セキュリティ保護されたテンプレート オーバーロード))
な〜んだぁ、オーバーロードされている「テンプレート」使えばこの余分なサイズ明示
のパラメータ不要という話。
ただ、そこで、最後まで読まずにおまぬけな早とちりをしてしまいました(^^;。
前の方で、
_CRT_SECURE_CPP_OVERLOAD_SECURITY_NAMESを1とすると
このテンプレートが有効になる
とあり、コード例で
#define _CRT_SECURE_CPP_OVERLOAD_SECURITY_NAMES 1
と書かれていました。ところが、この#difine文なくてもテンプレート使えてしまい、
「えっ?!」(キョロキョロ)になった私。しかし、よく読んでみたら、下の方に、
規定では、_CRT_SECURE_CPP_OVERLOAD_SECURITY_NAMESは1
と・・・。私のおまぬけな早とちりでした(^^;。
要するに、結局は、swprintf_s、wcscpy_s、wcscat_sのパラメータはこのテンプレート
により、swprint、wcscpy、wcscatと同じでよいということがわかりました。
Microsoftの説明では、Cの標準関数であるsprintf,swprintfなどの文字列操作関数
は"no safe"だとして、独自の"_s"付の関数を作り出したようですけど、テンプレート
を用意したのはユーザの使用を促進させようとしているんでしょうねぇ。パラメータ
数が増えるのはと反発もあると思いますから。
ちなみに、ことのついでと"wcscat_s"を試してみて、また、初歩的なおまぬけをして
しまいました。ランタイムエラーが出てしまったのです。
結論は、文字列初期化のときに、当然ながら追加される文字列バッファシズを十分
とっておかなければならないのに、初期化時に[]内のサイズを省略したままでした。
あと、ネット記事でよく出てくるTEXTマクロの代わりとしての"_T"ですが、前述の
swprint_s,wcscpy_s,wcscat_sの両対応版関数ともどもtchar.hをincludeしないと使え
ないことも試してエラーになって調べて知りました。
'_T'は前の2002.NETのときは確か、winnt.h(違うかもしれません)にTEXTマクロと一
緒に定義されてたのを記憶しているんですけど、なぜか今のwinnt.hにはTEXTしか
定義されていません。
以上、タイトルとは程遠い長い前置きになりましたけど、実はこのことと、swprintf_s
のMSDNライブラリに示されたテンプレートの第一パラメータの表記がよくわからな
かったことから、ポインタと配列についてじっくり調べなおしてみようという気になっ
たのです。
その結果、「なぜポインタで躓いていたのか」に関して、私の場合は、決して高度の
ところでなく、ずっと初歩的なところでの誤解が私にとっての「諸悪の根源」であった
ことがわかり、その誤解がとけたことでかなりポインタへの理解度が私レベルとして
は進んだ気になっています。もっとも、頭でわかったつもりでわかっていなかったと
いうのを何度も繰り返しての結果ですけどね(^^;
私の一つの大きな誤解は(ネット上での解説でもそういう誤解がポインタで躓く原因
として指摘されていますから、私一人だけではないということですねぇ)
配列やポインタ配列は宣言時(合わせての初期化時)と参照時で
異なっている(※)
ということに気が付かなかったということです。また、配列とポインタの相違、名前
が意味するところなどについてもよくわかっていなかったというのを実感しました。
(※)ですけど、たとえば、
TCHAR *a[3] = {TEXT("xxx"),・・・・}; //@
の読み方を完全に間違えていたんです。
やっと、左辺の
TCHAR *a[3]
は
TCHAR* a[3]または(TCHAR *)a[3]
というものであり、宣言で、
[型] *<変数>
とあると、
・この<変数>は、([型] *)という型のポインタ変数である
・この[型]の部分はポインタ変数が指し示す値の[型]である
ということが理解できました。
私は、参照時に*[ポインタ変数]とあるのとすっかり混同していまっていたのです(^^;。
そして、もう一つ・・・実際にプログラムを書いて使用されるメモリアドレスの状態を調
べてみました。やっぱりまだ理解不十分だったことをそれで知りました。
下記に結果例を示します(アドレス値自体は当然ながらタイミングで異なります)。
ちなみに、これは、
TCHAR pA[3][5]={L"ab",L"abc",L"abcd"};
TCHAR *pB[3]={L"ab",L"abc",L"abcd"};
(LはUNICODE対応です。個人だけの楽しみですから、面倒なのでTEXTマクロ使うの
をやめてます(^^;。_Tも<tchar.h>をincludeしなくちゃ使えないし(^^;)。)
として配列pA[3][5]、ポインタ配列pB[3}の宣言と初期化をしたもののアドレスを調べ
たものです。興味深い結果です。
まずは、説明されているように、配列宣言(二次元配列)では、宣言時に
3×5×sizeof(TCHAR)(VisualStudio2013Expressはデフォでは
UNICODE対応のため、sizeof(TCHAR)は2(byte)になっています)
分の領域が全部が連続して確保されます。
そして、初期化で
それに右辺の文字列がそれぞれ、pA[0][0],pA[1][0],pA[2][0]の
アドレスから順にコピーされています。
そして、興味深いのは、
pA=&pA=pA[0]=&pA[0]=&pA[0][0]
とうことでした。これについては後述します。
一方、ポインタ配列pB[3]の方は、宣言時に
pB[0],pB[1],pB[2]が1個ずつ別々に確保され、また、このポインタ
変数pB[0],pB[1],pB[2]を収める領域(&pB[0],&pB[1],&pB[2}でアク
セスできる)も確保しています。
そして、初期化では
pB[0],pB[1],pB[2]の箱には、右辺のそれぞれの文字列群の先頭
文字のアドレスが入っている。
ことがわかります。こちらも興味深いのは、
pB=&pB=&pB[0]≠pB[0]
ということです。
かつてQCの独習(途中で挫折(^^;)していたとき、その本には確か、<配列名>は
「ポインタ」になるという説明がありました。ところが、ネット見てますと複数のサイト
の説明として、<配列名>はポインタではないという・・・
その理由として、ポインタをポインタ変数とし、変数は左辺値になれるが<配列名>
はなれないからと。じゃ、<配列名>は何かというとアドレスであり、ポインタとアド
レスは異なるというような説明がありました。おやおやって感じでした。
更には、間接参照演算子「&」は「変数(左辺になれるもの)」の前につけられて、ア
ドレスを求めるもので「アドレス演算子」と称しているという説明がありました。
ですから、「それじゃ、どうして<配列名>に&を付けてもコンパイラでエラーになら
ないの?」という疑問が出てきたわけです。どうやら、<配列名>だけはCの一般
ルールからはずれた特別扱いらしいですね。ただし、「ポインタ変数」ではないゆえ
に&をつけても別のポインタ変数格納アドレスを示さない(存在しない)んですね。
ま、そういいながらも、参照時において、例えば配列要素a{i](i:要素番号)は
a[i]=*(a+i)
であり、a[i]というコードを書くと、コンパイラは自動的に*(a+i)と読み替えるそうで、
この形では<配列名>がポインタと同じように使われているんですよね。
ふと思ったんですが、ポインタ変数はどうのこうのと言っても、コード段階の話であり
実行時は必ず「アドレス」が入るわけですから、そこでは区別はないのではないかな
と考えるのです。そういうわけで、私はポインタ=アドレスというQC時代の所有して
いた本通りで差し支えないのではないかと勝手に理解しました。
戻る