「C言語のデータ構造ってなんだか難しそう…」
「配列とかリストって、どうやってプログラムで使うの?」
そんな風に感じているC言語ビギナーの皆さん、こんにちは!
プログラミングの世界では、データをうまく整理整頓するテクニック、それがデータ構造です。特にC言語では、自分でデータ構造を作ることがスキルアップの近道だったりします。
この記事では、C言語のデータ構造の中でも基本となる「配列」と「連結リスト」を取り上げ、その仕組みから実際の作り方、注意点まで、サンプルコードを交えてトコトン分かりやすく解説していきます。
この記事で学べること
- データ構造がなぜプログラムに必要なのか
- C言語でよく使われるデータ構造の種類
- 配列の基本的な使い方と実装方法
- 連結リストの仕組みと実装方法
- 配列と連結リストの使い分け
- 実装する上での注意点
C言語におけるデータ構造とは?なぜ必要になるのか?
データ構造って、一言でいうと「データを効率よく扱うための入れ物や仕組み」のことです。
プログラムは、たくさんのデータを使って計算したり、何かを表示したりしますよね。そのデータを、ただバラバラに置いておくのではなく、目的に合わせて整理整頓してあげる必要があります。
例えば、本棚を想像してみてください。小説、漫画、雑誌…種類ごとに分けて整理しておけば、読みたい本をすぐに見つけられます。データ構造も同じで、データを適切に整理することで、プログラムがデータをすばやく見つけたり、追加したり、削除したりできるようになるのです。
特にC言語では、コンピュータのメモリ(データを記憶する場所)を意識しながらプログラムを書くことが多いです。データ構造を理解していると、メモリを無駄なく使ったり、プログラムの処理速度を速くしたりすることに繋がります。
だから、C言語を学ぶ上でデータ構造の知識は、とっても役に立つ武器になるんです!
+-----------------+ | メモリ領域 | | +---+---+---+---+ | | | A | B | C | D | <-- データが整理されている例 | +---+---+---+---+ | | | | +---+ +---+ | | | X | | Z | <-- バラバラなデータの例 | +---+ +---+ | | +---+ | | | Y | | | +---+ | +-----------------+
C言語でよく使う基本的なデータ構造の種類
C言語の世界には、色々なデータ構造があります。ここでは、特によく使われる代表選手たちを簡単に紹介します。
- 配列 (Array)
同じ種類のデータを、連続したメモリ領域に順番に並べて格納するシンプルな構造です。番号(インデックス)を指定して、すぐにデータを取り出せるのが得意技。 - 連結リスト (Linked List)
データを格納する「ノード」と呼ばれる箱を、数珠つなぎのように繋げていく構造です。データの追加や削除が柔軟に行えるのが特徴。 - スタック (Stack)
後から入れたデータを先に取り出す「後入れ先出し (LIFO)」のルールを持つ構造です。積み重なったお皿のイメージ。 - キュー (Queue)
先に入れたデータを先に取り出す「先入れ先出し (FIFO)」のルールを持つ構造です。お店のレジ待ち行列のイメージ。 - 木構造 (Tree)
データが階層的に繋がっている構造です。家系図やフォルダ構成のようなイメージ。 - ハッシュテーブル (Hash Table)
キーとなる値からデータの格納場所を計算して、高速にデータを探し出す構造です。
たくさんの種類がありますが、全部を一気に覚える必要はありません。
この記事では、基本中の基本である「配列」と「連結リスト」に注目して、じっくり見ていきましょう!
【C言語 データ構造 実装①】配列の基本と実装方法
まずは、一番シンプルで身近なデータ構造、「配列」から見ていきましょう。
配列は、同じ種類のデータを、メモリ上で連続した場所に順番に並べて保管する箱のようなものです。
特徴はこんな感じです。
- データがメモリ上で隣り合って並んでいる
- 最初に決めた数しかデータを入れられない(固定長)
- 「0番目、1番目、2番目…」という番号(インデックス)を使って、直接データにアクセスできる
身近な例だと、クラスの名簿なんかが配列に近いかもしれません。生徒番号が決まっていて、その番号で特定の生徒の情報が分かりますよね。
配列の宣言と初期化の書き方
C言語で配列を使うには、まず「こんな配列を使いますよー」と宣言する必要があります。
書き方はシンプルです。
// 書き方の基本形 データ型 配列名[要素数]; // 例:整数(int)型のデータを5個格納できる配列 score を宣言 int score[5]; // 宣言と同時に初期化もできる // 例:整数型の配列 data を宣言し、初期値を設定 int data[3] = {10, 20, 30}; // 要素数を省略して初期化することも可能 // この場合、初期値の数に合わせて要素数が自動で決まる (この例だと要素数は4) char message[] = "Hello"; // 文字列は文字の配列として扱われる(最後にヌル文字'\0'が入る)
データ型
には、int
(整数)やchar
(文字)、float
(小数)などを指定します。配列名
は好きな名前をつけられます(変数名と同じルールです)。[要素数]
で、いくつのデータを格納するかを指定します。
宣言時に要素数を決めるのが配列のポイントです。後からサイズを変えるのは基本的にはできません。
配列の要素へのアクセスと操作(サンプルプログラム)
配列にデータを入れたり、取り出したりするには、インデックス(番号)を使います。
重要な注意点として、配列のインデックスは0から始まることを覚えておきましょう! 要素数が5の配列なら、インデックスは0, 1, 2, 3, 4 となります。
実際に配列を使って、合計点を計算する簡単なプログラムを見てみましょう。
書き方
#include <stdio.h> int main(void) { // 5教科の点数を格納する配列を宣言し、初期化 int score[5] = {80, 95, 70, 88, 60}; int sum = 0; // 合計点を保存する変数 int i; // ループ用カウンター // 配列の要素にアクセスして合計点を計算 // score[0]からscore[4]まで順番に見ていく for (i = 0; i < 5; i++) { printf("科目%d: %d点\n", i + 1, score[i]); // i番目の要素にアクセスして表示 sum = sum + score[i]; // i番目の要素を合計に加える } printf("----------\n"); printf("合計点: %d点\n", sum); // 配列の要素の値を変更することもできる printf("科目3の点数を変更します。\n"); score[2] = 75; // インデックス2 (3番目の要素) の値を75に変更 printf("変更後の科目3: %d点\n", score[2]); return 0; }
ソースコードの表示結果
科目1: 80点 科目2: 95点 科目3: 70点 科目4: 88点 科目5: 60点 ---------- 合計点: 393点 科目3の点数を変更します。 変更後の科目3: 75点
プログラムの解説
int score[5] = {80, 95, 70, 88, 60};
で、5つの整数を入れる配列 `score` を作り、同時に点数を設定しています。for
ループを使って、配列の要素を順番に見ていきます。i
が0から4まで変化します。score[i]
のように書くことで、配列のi
番目の要素にアクセスできます。ループの中で、各科目の点数を表示し、合計点 `sum` に加えています。- 最後に合計点を表示し、
score[2] = 75;
で3番目(インデックスは2)の要素の値を変更しています。
このように、インデックスを使えば、配列の好きな場所にアクセスしたり、値を書き換えたりできるわけです。
配列を使う上での注意点
配列は便利ですが、いくつか気をつけるべき点があります。特に初心者がやりがちなのが、配列の範囲外アクセスです。
int data[3] = {10, 20, 30}; // これはOK (インデックスは 0, 1, 2) int x = data[0]; int y = data[1]; int z = data[2]; // これはダメ!範囲外アクセス! // data[3] という要素は存在しない int error_val = data[3]; // 未定義の動作を引き起こす可能性大
上の例で、data
配列の要素数は3なので、有効なインデックスは0, 1, 2です。data[3]
のように、存在しないインデックスにアクセスしようとすると、プログラムが予期せぬ動作をしたり、エラーで止まってしまったりする原因になります。
これは非常に危険なバグにつながりやすいので、配列のインデックスは必ず0から(要素数-1)の範囲内に収まっているか確認する習慣をつけましょう!
もう一つの注意点は、配列は基本的に固定長であることです。一度宣言したら、プログラムの実行中にサイズを大きくしたり小さくしたりするのは簡単ではありません。「データがどれくらいの数になるか分からない」という場合には、配列は少し使いにくいかもしれません。
【C言語 データ構造 実装②】連結リストの基本と実装方法
次に紹介するのは「連結リスト」です。配列とはちょっと違った特徴を持つデータ構造です。
連結リストは、データを格納する「ノード」と呼ばれる箱と、次のノードがどこにあるかを示す「ポインタ(矢印のようなもの)」を組み合わせて、データ同士を鎖のようにつなげていく構造です。
[ここに連結リストのイメージ図]
[データA | ポインタ]--->[データB | ポインタ]--->[データC | NULL] ↑ 先頭ポインタ
特徴はこんな感じです。
- データがメモリ上でバラバラの場所に配置されていてもOK
- データの追加や削除が、配列よりも柔軟に行える(特に途中への挿入や削除)
- データを探すときは、先頭から順番にたどっていく必要がある
- ポインタを扱うので、少しだけ複雑に感じるかも
配列が最初からきっちり場所を決めて並べるのに対し、連結リストは必要なときに新しい箱(ノード)を用意して、既存の鎖につなげていくイメージです。そのため、データ数が変動する場合に便利です。
連結リストのノード構造とポインタの役割
連結リストを作るには、まず「ノード」の設計図が必要です。C言語では、構造体 (struct) を使ってノードを定義するのが一般的です。
ノードは主に2つの部分から構成されます。
- データ部: 実際に格納したいデータ(数値、文字、他の構造体など)
- ポインタ部: 次のノードがメモリ上のどこにあるかを示すアドレス情報(次のノードへのポインタ)
書き方(ノードの定義例)
#include <stdio.h> #include <stdlib.h> // malloc, free を使うために必要 // ノードの構造を定義 struct Node { int data; // データを格納する部分 (ここでは整数) struct Node *next; // 次のノードを指すポインタ };
この定義では、struct Node
という名前の構造体を作っています。中には、整数を入れるための `data` と、自分自身と同じ `struct Node` 型へのポインタ `next` があります。
この `next` ポインタが、次のノードを指し示すことで、ノード同士が繋がっていくわけです。
リストの最後のノードの `next` ポインタは、通常「どこも指していない」ことを示す特別な値 NULL を入れておきます。
+-------------+ | data | <-- データ部 +-------------+ | next ----|-----> 次のノード or NULL +-------------+ <-- ポインタ部 ノード
単方向連結リストの実装(サンプルプログラム)
では、実際に簡単な連結リスト(単方向リスト:ポインタが次のノードだけを指す)を作ってみましょう。
ここでは、リストの先頭にデータを追加する機能と、リストの内容を表示する機能を持ったプログラムを紹介します。
連結リストでは、新しいノードを作るためにメモリを動的に確保する必要があります。C言語では `malloc` という関数を使います。使い終わったメモリは `free` 関数で解放するのを忘れずに!
書き方(単方向連結リストの例)
#include <stdio.h> #include <stdlib.h> // malloc, free を使うために必要 // ノードの構造体定義 (上と同じ) struct Node { int data; struct Node *next; }; // リストの先頭にノードを追加する関数 // head はリストの先頭ノードを指すポインタへのポインタ (ポインタのポインタ) // なぜなら、先頭ノード自体が変わる可能性があるから void insertAtBeginning(struct Node **head, int newData) { // 1. 新しいノードのためのメモリを確保 struct Node *newNode = (struct Node *)malloc(sizeof(struct Node)); if (newNode == NULL) { printf("メモリ確保失敗\n"); return; // メモリ確保失敗時は何もしない } // 2. 新しいノードにデータを設定 newNode->data = newData; // 3. 新しいノードの next を、現在の先頭ノードに向ける newNode->next = *head; // 4. リストの先頭ポインタを、新しいノードに向ける *head = newNode; } // リストの内容を表示する関数 void printList(struct Node *node) { printf("リストの内容: "); while (node != NULL) { printf("%d -> ", node->data); node = node->next; // 次のノードへ移動 } printf("NULL\n"); } // リストのメモリを解放する関数 (使い終わったら必ず呼ぶ) void freeList(struct Node *head) { struct Node *tmp; while (head != NULL) { tmp = head; // 現在のノードを一時保存 head = head->next; // 次のノードへ移動 free(tmp); // 一時保存したノードのメモリを解放 } printf("リストのメモリを解放しました。\n"); } int main(void) { // 最初はリストは空なので、先頭ポインタは NULL struct Node *head = NULL; // リストの先頭にデータを追加していく insertAtBeginning(&head, 30); // headのアドレスを渡す insertAtBeginning(&head, 20); insertAtBeginning(&head, 10); // リストの内容を表示 printList(head); // リストのメモリを解放 freeList(head); return 0; }
ソースコードの表示結果
リストの内容: 10 -> 20 -> 30 -> NULL リストのメモリを解放しました。
プログラムの解説
- `struct Node` でノードの形を定義します。
- `insertAtBeginning` 関数は、新しいデータをリストの先頭に追加します。
- `malloc` で新しいノード `newNode` の場所を確保します。
- `newNode->data = newData;` でデータを設定。
- `newNode->next = *head;` で、新しいノードの次を、元々先頭だったノードにします。(空リストの場合は `*head` は `NULL` なので、`newNode->next` も `NULL` になります)
- `*head = newNode;` で、リスト全体の先頭ポインタを、今作った新しいノードに向けます。これで先頭への追加が完了。
- `printList` 関数は、先頭ノードから `next` ポインタをたどっていき、各ノードの `data` を表示します。ノードが `NULL` になったら終わりです。
- `freeList` 関数は、リストのノードを先頭から順番にたどりながら、`free` を使って確保したメモリを一つずつ解放します。これをしないとメモリリークという問題が起きます!
- `main` 関数で、最初に `head` を `NULL` で初期化(空のリスト)、`insertAtBeginning` を呼び出してデータを追加し、`printList` で表示、最後に `freeList` で後片付けをしています。
ポインタや `malloc`, `free` が出てきて少し難しく感じるかもしれませんが、一つ一つの手順を図で追いながら理解していくのがコツです。
連結リストを使う上での注意点
連結リストは柔軟性が高い反面、注意点もあります。
まず、ポインタの操作は慎重に行う必要があります。ポインタの繋ぎ方を間違えると、リストの一部が失われたり、意図しない場所を指してしまったりするバグの原因になります。
特に、存在しない場所を指す `NULL` ポインタにアクセスしようとすると、プログラムがクラッシュすることが多いです(これをヌルポインタアクセス、通称ぬるぽ、と言ったりします)。
次に、メモリ管理です。`malloc` で確保したメモリは、不要になったら必ず `free` で解放する必要があります。これを忘れると、使わないメモリがどんどん溜まっていき、最終的にメモリ不足でプログラムが動かなくなる「メモリリーク」を引き起こします。配列ではあまり意識しなかったメモリ管理が、連結リストでは必須になります。
また、配列と違って、特定の位置のデータに直接アクセスするのは苦手です。
例えば、「リストの5番目のデータが欲しい」という場合、先頭から順番にポインタをたどって5番目まで行かないといけません。データ数が多くなると、アクセスに時間がかかることがあります。
配列と連結リスト、どっちを使う?使い分けのポイント
さて、配列と連結リスト、どちらもデータを扱うための仕組みですが、得意なことと苦手なことがあります。どちらを使えば良いかは、プログラムで何をしたいかによって変わってきます。
簡単な使い分けの目安です。
- 配列が向いている場合
- 扱うデータ数が最初から決まっている、または最大数が予測できる
- データの途中への追加や削除があまり頻繁にない
- 特定の番号(インデックス)のデータに素早くアクセスしたい
- 連結リストが向いている場合
- 扱うデータ数が実行中に変動する可能性がある
- データの途中への追加や削除が頻繁にある
- メモリを柔軟に使いたい(必要な分だけ確保したい)
例えば、
- ゲームのスコアランキング(上位10名など数が決まっている) -> 配列が扱いやすいかも
- 音楽プレイヤーのプレイリスト(曲の追加、削除、順番変更が多い) -> 連結リストの方が柔軟に対応できるかも
- テキストエディタの文字データ(編集中に文字数が増減する) -> 連結リスト的な考え方が使われることも
どちらが絶対的に優れているというわけではなく、状況に応じて適切な方を選ぶのが、良いプログラマーへの道です!
【まとめ】C言語のデータ構造実装をマスターしてステップアップ!
お疲れ様でした!この記事では、C言語におけるデータ構造の基本、特に「配列」と「連結リスト」について、その仕組みから実装方法、注意点までを見てきました。
最初はちょっと難しく感じたかもしれませんが、データ構造はプログラムの効率や柔軟性を大きく左右する、まさに縁の下の力持ちです。今回学んだ配列と連結リストは、その第一歩。
実際に手を動かしてサンプルコードを試してみることで、理解がグッと深まるはずです。エラーが出ても大丈夫、それも学習の一部です!
今回紹介したデータ構造は基本中の基本。ここからさらに、スタック、キュー、木構造…と、もっと色々なデータ構造の世界が広がっています。また、データ構造と「アルゴリズム」(問題を解く手順)は密接に関係しています。
今回の内容をしっかりマスターすれば、より複雑で面白いプログラムを作れるようになりますよ。
【関連記事】
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。