Cプログラミング
 Computer >> コンピューター >  >> プログラミング >> Cプログラミング

C/C++ポインターパズル:多次元配列のサイズとポインタ演算の謎


ここに、サイズが4バイト(32ビット)の整数型変数と、サイズが8バイトのポインタ変数があると仮定します。このとき、以下のC++コードを実行すると、どのような出力になるでしょうか?

サンプルコード

#include<iostream>
using namespace std;
main() {
   int a[4][5][6];
   int x = 0;
   int* a1 = &x;
   int** a2 = &a1;
   int*** a3 = &a2;
   cout << sizeof(a) << " " << sizeof(a1) << " " << sizeof(a2) << " " << sizeof(a3) << endl;
   cout << (char*)(&a1 + 1) - (char*)&a1 << " ";
   cout << (char*)(&a2 + 1) - (char*)&a2 << " ";
   cout << (char*)(&a3 + 1) - (char*)&a3 << " ";
   cout << (char*)(&a + 1) - (char*)&a << endl;
   cout << (char*)(a1 + 1) - (char*)a1 << " ";
   cout << (char*)(a2 + 1) - (char*)a2 << " ";
   cout << (char*)(a3 + 1) - (char*)a3 << " ";
   cout << (char*)(a + 1) - (char*)a << endl;
   cout << (char*)(&a[0][0][0] + 1) - (char*)&a[0][0][0] << " ";
   cout << (char*)(&a[0][0] + 1) - (char*)&a[0][0] << " ";
   cout << (char*)(&a[0] + 1) - (char*)&a[0] << " ";
   cout << (char*)(&a + 1) - (char*)&a << endl;
   cout << (a[0][0][0] + 1) - a[0][0][0] << " ";
   cout << (char*)(a[0][0] + 1) - (char*)a[0][0] << " ";
   cout << (char*)(a[0] + 1) - (char*)a[0] << " ";
   cout << (char*)(a + 1) - (char*)a;
}

この問題を解くには、あらかじめ以下の重要なポイントを理解しておく必要があります。

  • 整数型のサイズは4バイト(32ビット)、ポインタのサイズは8バイトです。ポインタに1を加算すると、そのポインタが指す型の「直後の要素」を指すことになります。

  • &a1 の型は int**、&a2 の型は int***、&a3 の型は int**** です。いずれも「ポインタを指すポインタ」であるため、1を加算すると8バイト進みます。

  • a[0][0][0] は単なる整数値、&a[0][0][0] は int* 型、a[0][0] も int* 型です。しかし &a[0][0] は int(*)[6] 型、&a[0] は int(*)[5][6] 型、そして &a は int(*)[4][5][6] 型になります。つまり、配列名から参照をどの段階で外すかによって、ポインタ演算時に進むバイト数が大きく変わるのです。

出力結果

480 8 8 8
8 8 8 480
4 8 8 120
4 24 120 480
1 4 24 120

出力結果の解説

1行目:sizeofによる各変数のサイズ

sizeof(a) は配列全体のサイズ、すなわち 4 × 5 × 6 × 4バイト = 480バイト です。一方、a1、a2、a3 はすべてポインタ変数なので、それぞれ 8バイト となります。

2行目:アドレス(&演算子の結果)への加算

&a1 + 1、&a2 + 1、&a3 + 1 はいずれもポインタへのポインタとして1つ先へ進むため、8バイト ずつの差になります。しかし &a + 1 は配列全体(int(*)[4][5][6] 型)として1つ先へ進むため、480バイト 進みます。

3行目:ポインタ変数自体への加算

a1 は int* 型なので +1 で 4バイト、a2(int** 型)と a3(int*** 型)はポインタを指すため、+1 で 8バイト ずつ進みます。一方、配列名 a に +1 すると、一面分(5 × 6 個の int = 120バイト)、つまり 120バイト 進みます。

4行目:配列の部分要素のアドレスへの加算

&a[0][0][0] + 1 は次の int 要素までの 4バイト、&a[0][0] + 1 は int[6] 一列分の 24バイト、&a[0] + 1 は int[5][6] 一面分の 120バイト、そして &a + 1 は配列全体分の 480バイト となります。

5行目:値そのものを使った演算

a[0][0][0] + 1 - a[0][0][0] は単なる整数同士の計算なので、結果は 1 です。続く a[0][0]、a[0]、a の各ポインタに +1 した場合の差分は、それぞれ 4バイト、24バイト、120バイト になります。

このように、ポインタや配列の「型」を正確に把握することで、ポインタ演算で進むバイト数を予測できるようになります。多次元配列とポインタの関係は混乱しやすいテーマですが、型の階層を意識すれば確実に理解できます。

  1. C/C++のmemcpy()関数の使い方と実例解説

    この記事では、C++ STLにおけるmemcpy()関数の動作、構文、および具体的な使用例について詳しく解説します。 memcpy()とは? memcpy()関数は、C++ STLに組み込まれた標準関数の一つで、<cstring>ヘッダーファイルで定義されています。この関数はメモリブロックをコピーするために使用され、あるメモリ位置から別のメモリ位置へ、指定されたバイト数分のデータを転送します。 関数の実行結果は、データのバイナリコピーとなります。重要な点として、memcpy()は終端のNULL文字やその他の終端記号をチェックしません。単純にソースから指定されたバイト数(num)をそ

  2. C/C++で学ぶAA木(AA Tree)とは?基本概念と平衡化操作を徹底解説

    AA木(AA Tree)とはコンピュータサイエンスにおいて、AA木(AA Tree)とは、順序付きデータを効率的に格納・検索するために実装された平衡木(バランスドツリー)の一種です。AA木は、赤黒木(Red-Black Tree)の変種として扱われます。赤黒木は二分探索木の一形態であり、要素の追加や削除を効率的にサポートします。赤黒木と大きく異なる点は、AA木では赤いノードを右の子としてのみ追加でき、左の子としては配置できないという制約があることです。この制約により、2-3-4木ではなく2-3木をシミュレートすることになり、その結果、木の保守操作が大幅に簡素化されます。赤黒木の平衡化アルゴリズ