C言語で大規模なプログラムを開発する際、避けて通れないのが「構造体」と「ポインタ」の組み合わせです。
しかし、初心者の多くが「ドット演算子とアロー演算子の使い分けがわからない」「引数にポインタを渡すとどうなるの?」といった疑問を抱え、学習の壁に突き当たります。
また、構造体の中に構造体が入る入れ子構造や、構造体の配列をポインタで操作する手法は、システム開発の現場で頻出する非常に重要なテクニックです。
この記事では、構造体ポインタの基本的な定義や初期化から、関数へのポインタ渡し、配列の操作、そして「構造体の代入ができない」時の対処法まで、実務に即したサンプルコードを交えて詳しく解説します。
![]() 執筆者:マヒロ |
|
- OS:Windows 11 / macOS Sequoia
- IDE:Visual Studio / VS Code / IntelliJ IDEA
- その他:Chrome DevTools / 各言語最新安定版
※本メディアでは、上記環境にてコードの動作と情報の正確性を検証済みです。
構造体ポインタの基本と初期化方法
C言語において、構造体ポインタは「構造体変数がメモリ上のどこにあるか」という情報を保持する変数です。
通常のポインタ変数と同様に宣言し、アドレス演算子を用いて初期化を行います。
構造体そのものを扱うよりも、ポインタを介して操作する方がシステムのリソース効率が良いため、特に組み込み開発やゲーム開発などの分野では多用される手法です。
ポインタ変数の宣言と初期化
構造体ポインタを利用するためには、まず構造体の型を定義し、その後にポインタ変数を宣言する必要があります。
#include <stdio.h>
// 構造体の定義
struct Person {
char name[20];
int age;
};
int main(void) {
// 1. 通常の構造体変数を宣言して初期化
struct Person student = {"田中太郎", 20};
// 2. 構造体ポインタ変数を宣言し、studentのアドレスを代入
struct Person *ptr = &student;
// 結果を表示
printf("名前: %s\n", ptr->name);
printf("年齢: %d\n", ptr->age);
return 0;
}
実行結果
名前: 田中太郎
年齢: 20
このソースコードの内容について詳しく解説していきます。
まず、名前と年齢を保持する struct Person という型を作成しました。
次に、struct Person *ptr という記述でポインタ変数を宣言しています。
ここでのポイントは、型名の後に * を付けることで「Person構造体を指し示すポインタですよ」と明示している点です。
そして、ptr = &student; という代入処理によって、実際にメモリ上に確保された student 変数の場所を ptr に覚えさせています。
これによって、以降は ptr を通じて student の中身を自由に覗いたり書き換えたりできるようになるのです。
アロー演算子(->)によるメンバアクセス
構造体ポインタから中身(メンバ)にアクセスする際、最も頻繁に使用されるのが「アロー演算子(->)」です。
これはハイフンと不等号を組み合わせた記号で、文字通り「ポインタの先にあるもの」を指し示します。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p1 = {10, 20};
struct Point *ptr = &p1;
// アロー演算子を使った書き換え
ptr->x = 100;
ptr->y = 200;
// 従来の書き方(間接参照)
(*ptr).x = 500;
printf("座標: (%d, %d)\n", p1.x, p1.y);
return 0;
}
実行結果
座標: (500, 200)
このプログラムで重要なのは、メンバへのアクセスの仕方が2種類登場している点です。
ptr->x は、ポインタ変数 ptr が指している構造体の x というメンバを操作することを意味します。
一方で、(*ptr).x という書き方も存在します。
これはポインタを一度 * で実体に引き戻してからドット演算子を使うという手順を踏みますが、カッコが必要になり記述が煩雑になります。
そのため、現在の開発現場では、構造体ポインタを扱う際にはアロー演算子を用いるのが標準的なマナーとされています。
可読性が高く、コードの意図が「ポインタを介した操作である」と一目で伝わるため、必ずマスターしておくべき記法と言えるでしょう。
構造体ポインタ渡し(参照渡し)のメリット
関数間で構造体のデータをやり取りする際、値をそのまま渡す(値渡し)か、ポインタを渡す(ポインタ渡し / 参照渡し)かによって、プログラムの性能には大きな差が生まれます。
特にデータ量の多い構造体では、ポインタ渡しを採用することがパフォーマンス向上の鍵となります。
ここでは、具体的な実装方法とその理由を解説します。
引数としての構造体ポインタ渡し
関数の引数に構造体のポインタを渡すことで、関数内部から呼び出し元の変数を直接書き換えたり、無駄なデータコピーを防いだりすることができます。
#include <stdio.h>
#include <string.h>
struct Product {
int id;
int price;
};
// ポインタを受け取る関数
void updatePrice(struct Product *p, int newPrice) {
// ポインタの先にある価格メンバを更新
p->price = newPrice;
}
int main(void) {
struct Product item = {101, 1000};
printf("更新前価格: %d円\n", item.price);
// 関数にアドレス(&)を渡す
updatePrice(&item, 1200);
printf("更新後価格: %d円\n", item.price);
return 0;
}
実行結果
更新前価格: 1000円
更新後価格: 1200円
updatePrice 関数の定義では、第一引数として struct Product *p というポインタを受け取るよう設定しています。
呼び出し側の main 関数では、&item と記述して構造体の場所(アドレス)を渡しています。
もしこれをポインタではなく値渡し(struct Product p)で実装した場合、関数が呼ばれるたびに構造体の中身が丸ごとコピーされるため、メモリを浪費し、処理時間も増えてしまいます。
ポインタ渡しであれば、アドレスという極めて小さなデータ(通常4〜8バイト)を渡すだけで済むため、非常に高速に動作します。
さらに、関数内での変更が元の item 変数に即座に反映されるため、情報の整合性を保つのに最適な手法だと言えます。
構造体メンバにポインタを持つ場合
構造体は、そのメンバ変数としてポインタを持つことも可能です。
これにより、可変長の文字列を扱ったり、動的に確保したメモリ領域を管理したりといった柔軟なデータ構造を作ることができます。
#include <stdio.h>
#include <stdlib.h>
struct DynamicData {
int *numbers; // ポインタ型のメンバ
int size;
};
int main(void) {
struct DynamicData data;
data.size = 3;
// メンバ変数であるポインタにメモリを割り当てる
data.numbers = (int *)malloc(sizeof(int) * data.size);
if (data.numbers != NULL) {
data.numbers[0] = 10;
data.numbers[1] = 20;
data.numbers[2] = 30;
printf("2番目の値: %d\n", data.numbers[1]);
// メモリ解放
free(data.numbers);
}
return 0;
}
実行結果
2番目の値: 20
このコードでは、構造体のメンバ自体がポインタ変数となっています。
numbers メンバに対して malloc 関数を使い、実行時に必要な分だけのメモリを確保しました。
このように構造体メンバにポインタを持たせることで、コンパイル時にはデータの数が決まっていないような「名簿」や「測定ログ」などを効率的に管理できる構造が構築できます。
ただし、動的に確保したメモリは free で解放し忘れるとメモリリークの原因になるため、構造体の破棄と合わせて適切に管理する責任がプログラマには伴います。
構造体ポインタと配列の操作
構造体の配列を扱う際、ポインタ演算を組み合わせることで、データのリストを効率よく走査(スキャン)できるようになります。
ループ処理においてポインタをインクリメント(加算)していく手法は、C言語らしい強力なテクニックの一つです。
ここでは、配列をポインタとして関数に渡し、連続したメモリ領域にアクセスするパターンを見ていきましょう。
構造体配列のポインタ渡し
配列名は、C言語の仕様上「先頭要素のアドレス」として扱われます。
これを利用して、複数の構造体データを一括で処理するロジックを実装します。
#include <stdio.h>
struct Score {
int math;
int english;
};
// 構造体配列のポインタを受け取り、合計を計算する
void printAverage(struct Score *list, int count) {
for (int i = 0; i < count; i++) {
// ポインタ演算または添字アクセスを利用
double avg = (list[i].math + list[i].english) / 2.0;
printf("学生%dの平均点: %.1f\n", i + 1, avg);
}
}
int main(void) {
struct Score classes[2] = {
{80, 90}, // 1人目
{70, 60} // 2人目
};
// 配列名をそのまま渡す(先頭アドレスが渡される)
printAverage(classes, 2);
return 0;
}
実行結果
学生1の平均点: 85.0
学生2の平均点: 65.0
main 関数で定義された classes は、構造体が2つ並んだ「配列」です。
これを printAverage 関数に渡すと、関数側は struct Score *list というポインタとしてこれを受け取ります。
関数内では list[i] という形式で各要素にアクセスしていますが、これは内部的には 「先頭のアドレスから数えてi個分進んだ場所のデータ」 を参照するというポインタ演算が自動で行われています。
このように、複数の構造体データをリストとして管理し、一括で集計や出力を行うのは非常に実務的な実装パターンです。
ポインタのインクリメントによる連続アクセス
インデックス変数(i)を使わずに、ポインタそのものを進めることで次の要素へ移動する手法もあります。
これは「ポインタのインクリメント」と呼ばれます。
#include <stdio.h>
struct Log {
int code;
};
int main(void) {
struct Log logs[3] = {{100}, {200}, {300}};
struct Log *p = logs; // 先頭アドレス
for (int i = 0; i < 3; i++) {
printf("Code: %d\n", p->code);
p++; // ポインタを次の構造体の位置へ進める
}
return 0;
}
このプログラムにおける p++ という命令は、単に数値を1増やしているわけではありません。
「Score構造体1つ分に必要なメモリサイズ」の分だけアドレスを進めています。
C言語はこの型情報を元に移動量を自動計算してくれるため、異なるサイズの構造体であっても、インクリメントするだけで正確に次のデータ位置へ着地できます。
これは非常に効率的な走査方法ですが、配列の範囲を超えてインクリメントしすぎると不正なメモリ領域を叩いてしまうため、ループ回数の管理には細心の注意が必要です。
構造体の代入と注意点
C言語の構造体操作において、意外と知られていないのが「代入」の挙動です。
同じ型の構造体であれば = 演算子で一気にコピーできますが、中身がポインタである場合や、配列そのものを代入しようとする場合には特有の問題が発生します。
ここでは「代入できない」と悩む原因や、入れ子構造での注意点を整理します。
構造体ポインタの代入と「代入できない」時の対処
構造体変数同士であれば代入によって全メンバをコピーできますが、ポインタを代入した場合は「中身のコピー」ではなく「指し示す場所の共有」になります。
#include <stdio.h>
struct Data {
int val;
};
int main(void) {
struct Data d1 = {10};
struct Data d2 = {20};
struct Data *p1 = &d1;
struct Data *p2 = &d2;
// ポインタの代入(指し示す場所を入れ替える)
p1 = p2;
// 中身をコピーしたい場合はアロー演算子が必要
// *p1 = *p2; // これなら内容がコピーされる
printf("p1が指す値: %d\n", p1->val);
return 0;
}
実行結果
p1が指す値: 20
このコードにおいて p1 = p2; を実行すると、p1 も d2 のアドレスを指すようになります。
この状態では、もともと指していた d1 へのアクセス経路が断たれてしまいます。
「C言語で構造体が代入できない」と言われる際、よくある誤解が配列メンバを直接代入しようとしているケースです。
構造体の中にある配列メンバ(例:char name[20])に対して文字列を直接代入(item.name = "Test";)することはできません。
これは配列が「定数ポインタ」としての性質を持つため、変更不能だからです。
このような場合は strcpy 関数などを用いて「中身をコピーする」処理が必要になります。
構造体そのものの代入と、メンバ単位の操作を混同しないように意識しましょう。
構造体の中に構造体(入れ子構造)のポインタ操作
複雑なデータを扱う場合、構造体のメンバとして別の構造体を含める「ネスト(入れ子)」という手法がよく使われます。
この場合のポインタ操作は、アロー演算子とドット演算子を適切に組み合わせるパズル的な面白さがあります。
#include <stdio.h>
struct Birthday {
int year;
int month;
};
struct User {
char name[20];
struct Birthday birth; // 入れ子構造
};
int main(void) {
struct User user1 = {"山田", {1995, 5}};
struct User *uPtr = &user1;
// ポインタを介してネストしたメンバにアクセス
printf("名前: %s\n", uPtr->name);
printf("誕生年: %d年\n", uPtr->birth.year);
return 0;
}
実行結果
名前: 山田
誕生年: 1995年
このソースコードのアクセスの仕方に注目してください。
uPtr は User 構造体のポインタであるため、まずは uPtr-> を使ってメンバにアクセスします。
しかし、メンバである birth はポインタではなく「実体(構造体の値)」です。
そのため、その中にある year にアクセスするにはドット演算子(.)を使います。
つまり uPtr->birth.year という形になります。
もし birth メンバもポインタ(struct Birthday *birth)として定義されていた場合は、uPtr->birth->year と全てアロー演算子で繋ぐことになります。
コードを書く際に、「今アクセスしている場所はポインタなのか、実体なのか」を常に判断することで、複雑な階層構造でも迷わずプログラミングできるようになります。
C言語のスキルを活かして年収を上げる方法
以上、C言語の構造体ポインタについて解説してきました。
C言語を扱えるエンジニアは比較的希少価値が高く、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。
なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。
転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。
| 年収アップにこだわりたい方 (平均アップ額138万円の実績) | テックゴー |
| 未経験・経験者問わず幅広く探したい方 | ユニゾンキャリア |
| 業界に精通した担当者に相談したい方 | キッカケエージェント |
| ゲーム業界への転職を志望する方 | ファミキャリ |
| エンジニア未経験からキャリアを築く方 | イーチキャリア |



