JUNのブログ

JUNのブログ

活動記録や技術メモ

printfを使わずにメモリアドレスを取得する

はい. こんにちは. 今日は printf を使わずにメモリアドレスを表示する方法について書きます.

大体C言語の教科書などでは変数のメモリアドレスを表示するのに以下のようなコードを書くと思います.

#include <stdio.h>

int main(){
    int a = 10;
    printf("data: %d  address: %p\n", a, &a);
}

これをコンパイルして実行すると以下のようなものが出力されます.

data: 10  address: 0x7ffe894eadd4

このプログラムは printf%p フォーマットを使って変数のアドレスを16進数で表示しています. 簡単ですね.

しかし, これをprintf を使わずにしようとすると結構メモリやポインタについて理解していないと出来ず, 苦労したのでここにやり方を書き残しておこうと思ったわけです.

そもそもポインタとは?

そもそもポインタってなんなんでしょうか? Wikipediaを見るとこんなことが書いてあります.

C言語のポインタは「特定のメモリ領域を指し示す」ものである。

ふむふむ, ポインタがメモリ領域を指すことはわかったけど, 結局ポインタってなんなんだ? 数値としてのメモリアドレスはどこに入っているんだ? って思ったのでもう少し読んでみると面白いことが書いてありました.

C言語の関数では、引数は、値渡しだけをサポートし、参照渡しをサポートしない。これは、アドレスの数値を取得すれば、参照に可能な全てを行えるため、実質的に参照を数値と同一視できるからである。実際、初期のC言語では、アドレス値は、整数型互換するものとして扱われていた。

なんと! つまり, 変数aのポインタ&a は実際にはメモリアドレスを数値として取得しているに過ぎなかったわけだ!

はい. これがわかればもう出来そうですね? じゃあ上記WIkipediaからの情報を信じてコードを書いてみましょう.

#include <stdio.h>

int main(){
    int a = 10;
    unsigned int addr = (unsigned int)&a;

    printf("%p\n", &a);  // 確認用
    printf("%dx\n", addr);  // 取得したメモリアドレスを16進数表示
}

これをコンパイルしようとすると

test.c: In function ‘main’:
test.c:5:16: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
    5 |     int addr = (int)&a;
      |                ^

あれ, コンパイルに警告が出てますね? とりあえず気にせず実行してみましょう.

0x7ffcf5629e90
-178086256x

あれ, メモリアドレスが printf を使ったものと一致しませんね?

warning: cast from pointer to integer of different size とのことですが, 要は ポインタからint型へのキャストはサイズが違うけど大丈夫か?コンパイラちゃんは言っているわけです.

サイズが違う???? 整数型と言っているのだからint型で良いのでは?

では実際にサイズを見てみましょう.

#include <stdio.h>

int main(){
    int a = 10;

    printf("a: %ld\n", sizeof(a));
    printf("&a: %ld\n", sizeof(&a));
}

このプログラムをコンパイルして実行すると以下のようになりました.

a: 4
&a: 8

どうやら int型 が4バイトで, int*型が8バイトみたいですね.

これで, コンパイラが怒っていた「ポインタとint型はサイズが違うからキャスト出来ないよ」というメッセージの解決策がわかりましたね.

そんで, 先程のプログラムをポインタと整数型のサイズが合うように修正するとこうなります

#include <stdio.h>

int main(){
    int a = 10;
    unsigned long long addr = (unsigned long long)&a;

    printf("%p\n", &a);  // 確認用
    printf("%llx\n", addr);  // 取得したメモリアドレスを16進数表示
}

long long型 は8バイトの整数型です.

これでコンパイルして実行してみると

0x7fff1216619c
7fff1216619c

おお! これでメモリアドレスを整数のポインタ型としてではなく, 整数型として取得出来ました!!

もう少し細かいお話

ポインタのアドレスが8バイトとということがわかり, そして8バイトの整数型である long long型 を使うことで整数としてのメモリアドレスを取得することが出来ました.

しかし, なぜ8バイトなのでしょうか?

実は32bitマシンでポインタのサイズを表示するプログラムをコンパイルして実行すると面白いことがわかります.

a: 4
&a: 4

あれれ, ポインタのサイズが4バイトになりましたね?

そう, 実はポインタのサイズは必ず8バイトというわけではなく, 32bitCPUではポインタは4バイトなのです!!

なので, 32bitCPUでは以下のコードが普通にコンパイル出来ます.

#include <stdio.h>

int main(){
    int a = 10;
    int addr = (int)&a;
    
    printf("%p\n", &a);
    printf("%x\n", addr);
}

コンパイルして実行すると

0xbfa1ccc4
bfa1ccc4

はい. ちゃんと表示出来ましたね.

しかし, これだと, 32bit向けのコードと64bit向けのコードで毎回 int型 と long long 型 を書き分けなきゃいけないのでめんどいですね.

そんなあなたに long型〜〜

printf("%d\n", sizeof(long));

このコードを32bitでコンパイルして動かすと以下が出力されます

4

64bitでコンパイルして動かすと以下が出力されます

8

ということで, long型 だとコンパイル対象のシステムによってバイト数が変わるので, int型 や long long型 を気にしなくてもかけるので, もしこういうことを気にしなくてはいけない機会があるのであれば使ってみるといいかもですね.

サイズ (32 ビット) サイズ (64 ビット)
short 2 バイト 2 バイト
int 4 バイト 4 バイト
long 4 バイト 8 バイト
long long 8 バイト 8 バイト

データ型とサイズ

追記 (2021/01/07)

long型の大きさはコンパイラによって例え64bit環境向けにコンパイルしたとしても32bitになる場合があるとの指摘を受けました.

なので, long型が必ずしも対象のアーキテクチャのビット数と等しくなるとは限らないようなので気をつけた方が良いかも知れません.

ja.wikipedia.org