RS232Cシリアル通信を行うアプリケーションを作成しています。
データを送信するのにwritefileを使用しようと思うのですが
第2引数のconst Bufferの意味がわかりません。
送信データのアドレスを渡せば良いのでしょうか?
~~~~~~~~
WriteFile(hCom,'1234',4,Len,nil);
とすると0x31,0x32,0x33,0x34が送られます。(思惑通りです)
※'1234'は0x31,0x32,0x33,0x34が格納されているバッファへのアドレスですよね?
しかし
str:string;
str:='1234';
WriteFile(hCom,str,Length(str),Len,nil);
とすると0x80,0xFF,0x48,0x00と送信されました。
ヘルプにはstring型(長い文字列)は、文字列を指すポインタで
あると記載されているように思うのですが、どうなのでしょうか?
ちなみに
WriteFIle(hCom,PChar(str)^,Length(str),Len,nil);
と記述すると0x31,0x32,0x33,0x34が送信されました。
(strは文字列へのポインタのポインタ?)
const Bufferの意味、string型の構造がわかっていないのだと思います。
ご教授下さい。宜しくお願いします。
環境:WindowsXPPRO Delphi7
> 送信データのアドレスを渡せば良いのでしょうか?
いいえ。値を渡せばよいのです。
たとえば、ファイルの場合ですが、こんな感じになります。
var
hFile: THandle;
bytValue: Byte; //8bit
chValue: Char; //8bit
intValue: Integer; //32bit
lngValue: Longint; //32bit
int64Value: Int64; //64bit
strValue: String;
intLen: Integer;
dwDummy: DWORD;
begin
hFile := CreateFile('C:\test.dat', GENERIC_READ or GENERIC_WRITE, 0, nil, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0);
bytValue := 1;
chValue := 'A';
intValue := $1234;
lngValue := $7FDCBA98;
int64Value := $FFDDCCBBAA998877;
strValue := 'abcde';
intLen := Length(strValue);
WriteFile(hFile, bytValue, 1, dwDummy, nil);
WriteFile(hFile, chValue, 1, dwDummy, nil);
WriteFile(hFile, intValue, 4, dwDummy, nil);
WriteFile(hFile, lngValue, 4, dwDummy, nil);
WriteFile(hFile, int64Value, 8, dwDummy, nil);
WriteFile(hFile, PCHAR(strValue)^, intLen, dwDummy, nil);
//こっちでも可能
//WriteFile(hFile, strValue[1], intLen, dwDummy, nil);
CloseHandle(hFile);
end;
> ヘルプにはstring型(長い文字列)は、文字列を指すポインタで
> あると記載されているように思うのですが、どうなのでしょうか?
いいえ、ちがいます。
長い文字列=AnsiStringは、文字列を指すポインタの前に、文字数および参照カウンタが存在します。
ただし、PCHARでキャストすると、文字列を指すポインタとなります。
> WriteFIle(hCom,PChar(str)^,Length(str),Len,nil);
> と記述すると0x31,0x32,0x33,0x34が送信されました。
これが正解です(正解は1つとは限りませんが)。
他の型に関しては、上述の通りです。
こんばんは。
>> ヘルプにはstring型(長い文字列)は、文字列を指すポインタで
>> あると記載されているように思うのですが、どうなのでしょうか?
>いいえ、ちがいます。
>長い文字列=AnsiStringは、文字列を指すポインタの前に、文字数および参照カウンタが存在します。
そうなんですが、string 型に変数は何を保持しているのか、というと
procedure TForm1.Button1Click(Sender: TObject);
var
s: string;
begin
s := '1234';
Label1.Caption := IntToHex(Cardinal(PChar(s)), 8);
Label2.Caption := IntToHex(Cardinal(s), 8);
end;
が同じ値をしめすことから、やはり、文字列の先頭へのアドレスを保持しているんですよね。
だから
>ただし、PCHARでキャストすると、文字列を指すポインタとなります。
PChar にキャストできるわけなんですが。今回の const Buffer については、VCL の
他のメソッドでも多用されていて、わたしにはちょっとうまく説明できないのですが、
該当メモリの先頭の変数をそのまま指定するといいように思います。にしのさんの
しめされた多くの例がそれを支持しているように思います。
ただ、文字列のリテラルがどうしてうまくいくのか、よく分かりません。
あと、今回の場合は API なので、もともとのプロトタイプは
BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure needed for overlapped I/O
);
なんですよね。第二引数は、型なしポインタなので PChar(s) ではなく PChar(s)^
であることが不思議です。
すみません、わたし自身は慣れきっているのでまずコーディングに失敗することは
ないのですが、あらためて考えると不思議な気もします。
> Label1.Caption := IntToHex(Cardinal(PChar(s)), 8);
> Label2.Caption := IntToHex(Cardinal(s), 8);
> が同じ値をしめすことから、やはり、文字列の先頭へのアドレスを保持しているんですよね。
違います。ブレイクポイントを仕掛けて、sのアドレスを確認してみてください。
また、
Label1.Caption := IntToHex(Cardinal(PChar(s)), 8);
Label2.Caption := IntToHex(Cardinal(Addr(s)), 8);
こちらに入れ替えて、同じようにブレイクポイントを仕掛けて確認してみてください。
ポインタへキャストする際に、すでに純粋なsへのポインタではなくなっています。
Stringに値を入れて、CPUウィンドウで確認すると動きがよくわかります。
例として次のようなプログラム。
var
s: String;
p: PCHAR;
begin
s := '';
s := 'abc';
p := PCHAR(s);
...
end;
s := 'abc'にブレイクポイントをしかけ、実行してみましょう。
ブレイクポイントでとまったら、sのアドレスをデバッグインスペクタで確認してください。
sのアドレスが12F588だとします。
このアドレスの値は、00000000です。
次に、
s := 'abc';
を1行分実行させます。
私の環境では004BDB80でした(毎回変わるので、CPUウィンドウで確認してください)。
このアドレスをデータペインで見てみると、
61 62 63 00 ....(ASCII部では、a b c . . .)
となり、ようやく実体にたどり着けます。
もう1行、
p := PCHAR(s);
を通してみてください。
このpの値は、004BDB80となっているはずです。
p := PCHAR(s);
と、
p := PCHAR(PBYTE(s));
に、違いがあることに注意してください。Stringのキャストは、そのままの形でキャストされるのでなく、文字列の実体へのポインタへ変換されます。
最後の記述に間違いがありました。
> 誤:p := PCHAR(PBYTE(s));
> 正:p := PCHAR(ADDR(s));
うーむ、よく分かりません。s のアドレスは関係なくて s が保持してる値を
議論してるのですが。
どうも誤解があるようですが、型キャストは、その変数のアドレスではなく、
保持している値をキャストするのだと思います。
> s のアドレスは関係なくて s が保持してる値を議論してるのですが。
ああ、なるほど。
そういう意味では、sの内容をポインタと見立てたときの、そのポインタが指す実体は文字列であっています。
> string 型に変数は何を保持しているのか
こちらの意味としては、文字列のポインタとしてでなく、文字列の長さも持っているし、参照カウンタも持っています。
# 参照カウンタまで確認できませんでしたが
りおりおさん、そこまで解っておられるなら、
> ただ、文字列のリテラルがどうしてうまくいくのか、よく分かりません。
ここも理解していると思います。
文字列は、先頭文字へのポインタですから。
>> string 型に変数は何を保持しているのか
>こちらの意味としては、文字列のポインタとしてでなく、文字列の長さも持っているし、参照カウンタも持っています。
これは、コンパイラが長い文字列型だと判断しているのであって、string 型の変数が保持しているのは、メモリのどこかに確保されている文字列への先頭アドレスです。文字列の長さと参照カウントもさかのぼって見ることができます。以前、興味があっていろいろ調べたことがありました。
ですから PChar(s) のような型キャストは、純粋な型キャストであって、なんら特別扱いをしている訳ではないんですね。
今回疑問に思ったのは、WriteFile() が API であって、しかも、本来、型なしポインタを設定すべきところが PChar(s) ではなく、PChar(s)^ または s[1] を設定しなければならないのはなぜか、ということです。
同様に文字列リテラルが PChar 互換であることは知っていますが、この場合は、そのまま設定できるのはどうしてだろうか? ということだったのです。
まず、const Bufferという引数は、「型なしパラメータ」です。
たとえば関数に、
function Test(const Buffer): DWORD; stdcall;
とあると、Bufferは、ポインタとしてスタックにPUSHされます。
また、
function Test(var Buffer): DWORD; stdcall;
という記述もできます。この場合も、BufferはポインタとしてスタックにPUSHされます。
つまり、型なしパラメータ=型なしポインタ(LPVOID)です。
WriteFile APIのBufferは、constでなければなりません。
その理由は、文字列を直接指定する場合には、varだと構文エラーになるためです。
varの場合、変数に入れてからでないと、値を返せないためエラーになります。
constの場合、その引数を変更しないと言うことになり、定数も渡せるようになります。
ちなみに、ポインタとしてAPIを定義することも出来ます。
function WriteFileTest(hFile: THandle; Buffer: Pointer; nNumberOfBytesToWrite: DWORD;
var lpNumberOfBytesWritten: DWORD; lpOverlapped: POverlapped): BOOL; stdcall; external kernel32 name 'WriteFile';
使い方は、
WriteFileTest(hFile, addr(bytValue), 1, dwDummy, nil);
WriteFileTest(hFile, addr(chValue), 1, dwDummy, nil);
WriteFileTest(hFile, addr(intValue), 4, dwDummy, nil);
WriteFileTest(hFile, addr(lngValue), 4, dwDummy, nil);
WriteFileTest(hFile, addr(int64Value), 8, dwDummy, nil);
WriteFileTest(hFile, PCHAR(strValue), intLen, dwDummy, nil);
こんな感じになります。
にしのさん、りおりおさん早速のアドバイスありがとうございます。
とてもわかりやすい説明でした。
まとめてみました。すると新たな疑問が浮上してしまいました・・・
【string型について】
・string型(長い文字列)変数(4byte)には文字列へのポインタが格納されている。
(デバッグインスペクタで見える変数のところ)
・文字列へのポインタが指し示すアドレス-4byteの位置に文字列長が格納されている。
(-8byteのところが参照カウンタ???)
なのでstring型変数をそのままPChar型にキャストするということは、
もともとその変数には文字列へのポインタが入っているので、
string型だからポインタをずらす等の特別なことはやっていない(本当に単なるキャスト)。
【'1234'と言う記述について】
わたくしの質問で'1234'という記述は0x31,0x32,0x33,0x34が格納されている
バッファへのポインタではないかと申しましたが、
正解は文字列の実体そのものをあらわすのですね。
C言語だと"abc"でabcが格納されているバッファへのポインタなので混乱しました。
【新たな疑問】
りおりおさんと同じ疑問が浮上しました。
> ただ、文字列のリテラルがどうしてうまくいくのか、よく分かりません。
これです。
例えばにしのさんのコードでは
> strValu:String;
> strValue := 'abcde';
> //こっちでも可能
> //WriteFile(hFile, strValue[1], intLen, dwDummy, nil);
でも大丈夫とありましたが、strValue[1]は'a'の実体、つまり0x61が
引数として渡されるだけではないのでしょうか?
つまり
WriteFile(hFile,'a',5,dwDummy,nil);
と同じに思えてしまいます。
strValue[1]と指定しても、'abcde'と指定できるのが不思議です。
>function Test(const Buffer): DWORD; stdcall;
>とあると、Bufferは、ポインタとしてスタックにPUSHされます。
この辺りもう少し教えていただけますでしょうか?
やはり引数のconst Bufferはポインタを指定する?という
始めの疑問に戻ってきてしまいました。
宜しくお願いします。
にしのさん、説明ありがとうございます。
> BufferはポインタとしてスタックにPUSHされます。
つまり、設定されたもののアドレスをスタックに積む、変数のアドレスを実際の引数としている、ってことですね。だから、PChar(s) ではなく、PChar(s)^ または s[1] である必要があるのですね。そして、実際の引数としては PChar(s) か @s[1] (両者は同じ値)であると。
このような「型なしパラメータ」はメモリを操作する
procedure Move(const Source; var Dest; Count: Integer);
TMemoryStream.Write
function Write(const Buffer; Count: Longint): Longint; override;
などにもあって、いままでなんとなく上記のように理解してきました。今回は API だったので、もともとの引数の型が分かっているのではっきりと認識できました。
もやもやが解消できました。ありがとうございました。
ゆうこさん
>(-8byteのところが参照カウンタ???)
そうです。Delphi の長い文字列についての解説をみつけました。
http://kakinotane.s7.xrea.com/delphi/faq/f006.html#i1
にしのさん、りおりおさんありがとうございます。
わかった気がします。
プログラマは値渡しでWriteFileに文字列の実体を指定しても
実際に実行する時は、その実体のアドレスが渡されている(プログラマの意識しないところで)。
意味的には「送信したい(書き込みたい)文字列の実体はここにありますよ」
"ここにあります"は、WriteFileで指定した文字列のポインタになるので、
一文字しか指定しなくてもWriteFileはその後に続く文字列までわかる。
要するに、関数をコールする側が引数に指定した変数と
コールされた関数の引数は同じアドレスの変数になる。
varに似ているでしょうか。
function Test(var Value:Integer):BOOL;
とすると、関数呼び出し側は値渡しなのに、
関数内でその値を変更できます。
ということはプログラマの見えないところで
アドレスが渡っているということになりますよね。
型無パラメータの場合も同様の仕組みとなっている。
又、にしのさんのおっしゃるとおりconstの場合は、
メリットとして「値を変更しません」と宣言しているため
定数も引数に指定することが出来る!。
通常の
function Test(Value:Integer):BOOL;
の場合は引数に型があり、又varでもないので
今回のアドレス渡しには当てはまらない!
にしのさん、りおりおさん丁寧に説明してくださりありがとうございました。
const Bufferの場合、内部ではポインタが渡されています。
要点はそこだけです。
たとえ、引数がBYTE型の変数でも、その変数を格納しているアドレスが渡されることになります。
strValue[1]と、PCHAR(strValue)^の違いは、内部でUniqueStringを使うか、LStrToPCharを使うかの違いです。戻り値は(ほぼ)同じです。
結果として、eaxに文字列へのポインタが返り、それをPUSHしています。
CPUウィンドウで確認すると、
-----------------------------------------
WriteFile(hFile, strValue[1], intLen, dwDummy, nil);
//nilをpush
push $00
//dwDummyをpush
lea eax, [ebp-$20]
push eax
//intLenをpush
push esi
//strValue[1]をpush
lea eax, [ebp-$1c]
call @UniqueStringA
push eax
//hFileをpush
push ebx
//WriteFileを呼び出す
call WriteFile
-----------------------------------------
WriteFile(hFile, PCHAR(strValue)^, intLen, dwDummy, nil);
//nilをpush
push $00
//dwDummyをpush
lea eax, [ebp-$20]
push eax
//intLenをpush
push esi
//strValue[1]をpush
mov eax,[ebp-$1c]
call @LStrToPChar
push eax
//hFileをpush
push ebx
//WriteFileを呼び出す
call WriteFile
-----------------------------------------
となります。
それでは、文字列を直接した場合はどうなるかというと、
WriteFile(hFile, 'abcde', intLen, dwDummy, nil);
//nilをpush
push $00
//dwDummyをpush
lea eax, [ebp-$20]
push eax
//intLenをpush
push esi
//strValue[1]をpush
push $004bdda4 ('abcde'の'a'へのポインタ。ここのアドレスからabcdeと格納されている)
//hFileをpush
push ebx
//WriteFileを呼び出す
call WriteFile
となり、さらにシンプルになります。
にしのさんありがとうざいます。
>const Bufferの場合、内部ではポインタが渡されています。
>要点はそこだけです。
はい。今回の疑問はこれでクリアになりました。
・WriteFileの引数は「値渡し」でよい。
・内部ではポインタが渡されているのでWriteFile関数内でもその後のアドレスに格納されている値がわかる。
(主に文字列の場合)
ですね。
この度は本当にありがとうございました。
>・WriteFileの引数は「値渡し」でよい。
「値渡し」というのは引数の前に何もつけないデフォルトのものを言います。
引数の前に constやvarを付けたものは、「参照渡し」と言います。
ツイート | ![]() |