ポインタの高度な利用

編集

ダブルポインタとポインタのポインタ

編集

ポインタは、変数のアドレスを格納する特殊な変数です。C言語では、ポインタのポインタやダブルポインタのような高度なポインタの利用方法があります。これらの概念は、複雑なデータ構造や関数へのポインタの渡し方など、より高度なプログラミングタスクを実現するために役立ちます。

ポインタのポインタ(ポインタの参照)

編集

ポインタのポインタは、ポインタを指すポインタです。つまり、変数のアドレスを格納する変数をさらに指すポインタです。これにより、関数内でポインタを変更することが可能になります。

例えば、以下のようなコードでは、ポインタのポインタを使用して関数内でポインタの値を変更しています。

#include <stdio.h>

void changeValue(int **ptr) {
    **ptr = 20;
}

int main() {
    int num = 10;
    int *ptr = &num;
    
    printf("Before: %d\n", *ptr);
    changeValue(&ptr);
    printf("After: %d\n", *ptr);
    
    return 0;
}

このプログラムでは、changeValue関数でポインタのポインタを受け取り、そのポインタが指す変数の値を変更しています。main関数内でポインタを定義し、そのアドレスをchangeValue関数に渡しています。結果として、main関数内のポインタが指す変数の値が変更されます。

ダブルポインタ

編集

ダブルポインタは、ポインタのポインタと同様に、ポインタを指すポインタですが、更に1段階ポインタが追加されたものです。主に多次元配列や動的メモリ割り当てなどの高度な操作で使用されます。

#include <stdio.h>
#include <stdlib.h>

void allocateMemory(int ***ptr) {
    *ptr = (int **)malloc(sizeof(int *));
    **ptr = (int *)malloc(sizeof(int));
    ***ptr = 10;
}

int main() {
    int **ptr;
    allocateMemory(&ptr);
    
    printf("Value: %d\n", ***ptr);
    
    free(*ptr);
    free(ptr);
    
    return 0;
}

このプログラムでは、allocateMemory関数でダブルポインタを使って動的にメモリを割り当てています。そして、そのメモリに値を代入し、main関数内でその値を出力しています。

ダブルポインタやポインタのポインタなどの概念を理解することで、より複雑なデータ構造や関数を効果的に扱うことができます。

ポインタと配列の関係

編集

ポインタと配列は、C言語において密接な関係があります。実際、配列名自体は、その最初の要素へのポインタとして解釈されます。このため、ポインタと配列はしばしば互換的に使用されます。

配列とポインタの関係

編集
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 配列の先頭要素へのポインタを取得

    // 配列要素へのアクセス
    printf("arr[0]: %d\n", arr[0]);
    printf("*(arr + 1): %d\n", *(arr + 1)); // ポインタ演算によるアクセス
    printf("ptr[2]: %d\n", ptr[2]); // ポインタを介したアクセス

    return 0;
}

このプログラムでは、配列arrの要素にアクセスするために、通常のインデックス表記とポインタを介した表記の両方が使用されています。配列名arrは、その最初の要素へのポインタとして解釈され、ポインタptrに格納されます。その後、ポインタptrを介して配列の要素にアクセスしています。

ポインタと構造体

編集

構造体は、複数の異なる型のデータをまとめて管理するために使用されます。ポインタを使用すると、構造体のメンバに対する効率的なアクセスや動的なメモリの確保が可能になります。

構造体とポインタの関係

編集
#include <stdio.h>

struct Person {
    char name[20];
    int age;
};

int main() {
    struct Person person1 = {"John", 25};
    struct Person *ptr = &person1;

    // ポインタを使用して構造体のメンバにアクセス
    printf("Name: %s\n", ptr->name);
    printf("Age: %d\n", ptr->age);

    return 0;
}

このプログラムでは、struct Person型の構造体person1を定義し、そのメンバにアクセスするためのポインタptrを取得しています。->演算子を使用してポインタを介して構造体のメンバにアクセスしています。

メモリ管理と動的メモリ割り当て

編集

メモリ管理は、プログラムが実行中にメモリを効率的に使用するための重要な概念です。動的メモリ割り当ては、実行時に必要なだけのメモリを動的に確保するための手法であり、C言語においては主に malloc()calloc()realloc() などの関数が使用されます。

malloc() 関数

編集

malloc() 関数は、指定されたバイト数のメモリ領域を動的に割り当てます。割り当てられたメモリ領域の先頭アドレス(ポインタ)を返します。以下は malloc() の基本的な使用例です。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(5 * sizeof(int)); // int型の要素5つ分のメモリを割り当てる

    if (ptr == NULL) {
        printf("メモリの割り当てに失敗しました。\n");
        return 1;
    }

    // 割り当てられたメモリに値を代入
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }

    // 値を出力
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // メモリを解放
    free(ptr);
    ptr = NULL;

    return 0;
}

calloc() 関数

編集

calloc() 関数は、指定された要素数と要素のサイズに基づいてメモリを割り当て、割り当てられたメモリ領域をゼロで初期化します。以下は calloc() の基本的な使用例です。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)calloc(5, sizeof(int)); // int型の要素5つ分のメモリを割り当て、ゼロで初期化する

    if (ptr == NULL) {
        printf("メモリの割り当てに失敗しました。\n");
        return 1;
    }

    // ゼロで初期化されたメモリに値を代入
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }

    // 値を出力
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // メモリを解放
    free(ptr);
    ptr = NULL;

    return 0;
}

realloc() 関数

編集

realloc() 関数は、既存のメモリ領域のサイズを変更し、新しいサイズに再割り当てします。新しいサイズのメモリ領域には以前の内容が保持されます。以下は realloc() の基本的な使用例です。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(5 * sizeof(int)); // int型の要素5つ分のメモリを割り当てる

    if (ptr == NULL) {
        printf("メモリの割り当てに失敗しました。\n");
        return 1;
    }

    // 割り当てられたメモリに値を代入
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }

    // 新しいサイズのメモリ領域に再割り当て
    ptr = (int *)realloc(ptr, 10 * sizeof(int)); // int型の要素10つ分に再割り当てする
    if (ptr == NULL) {
        printf("メモリの再割り当てに失敗しました。\n");
        return 1;
    }

    // 新しい領域に値を追加
    for (int i = 5; i < 10; i++) {
        ptr[i] = i + 1;
    }

    // 値を出力
    for (int i = 0; i < 10; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // メモリを解放
    free(ptr);
    ptr = NULL;

    return 0;
}

以上が、C言語における動的メモリ割り当ての基本的な方法とその関数の使い方です。malloc()calloc()realloc() を使用して、実行時に必要なだけのメモリを効率的に管理しましょう。

free()関数とメモリリークの防止

編集

動的メモリ割り当てを行った後は、そのメモリを不要になった時点で解放することが重要です。解放されなかったメモリは、メモリリークとしてプログラムのパフォーマンスや安定性に影響を与える可能性があります。C言語において、メモリの解放は free() 関数を使用して行います。

free() 関数の使用方法

編集

free() 関数は、malloc()calloc()realloc() などの関数によって割り当てられたメモリ領域を解放します。解放されたメモリ領域は、以降のプログラム実行中に再利用される可能性があります。以下は、free() 関数の基本的な使用例です。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    ptr = (int *)malloc(5 * sizeof(int)); // int型の要素5つ分のメモリを割り当てる
    if (ptr == NULL) {
        printf("メモリの割り当てに失敗しました。\n");
        return 1;
    }

    // 割り当てられたメモリに値を代入
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }

    // メモリを解放
    free(ptr);
    ptr = NULL;

    return 0;
}

メモリリークの防止

編集

メモリリークは、解放されなかったメモリがプログラムの実行中に増加し続ける問題です。メモリリークを防ぐためには、確保されたメモリが不要になったら適切なタイミングで解放する必要があります。以下は、メモリリークを防ぐための基本的な手順です。

  1. メモリが不要になったら、即座に free() 関数を使用して解放する。
  2. 解放済みのメモリを参照しているポインタを誤って使わないよう、NULLを代入する。
  3. メモリを割り当てる前に、十分なエラーチェックを行い、malloc()calloc() が NULL を返した場合にはエラーメッセージを表示し、プログラムの実行を終了する。
  4. 関数やブロックの終了時に、そのスコープ内で割り当てられたメモリを解放する。
#include <stdio.h>
#include <stdlib.h>

void function() {
    int *ptr;
    ptr = (int *)malloc(5 * sizeof(int)); // int型の要素5つ分のメモリを割り当てる
    if (ptr == NULL) {
        printf("メモリの割り当てに失敗しました。\n");
        exit(1); // プログラムの実行を終了
    }

    // 割り当てられたメモリに値を代入
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }

    // 関数の終了時にメモリを解放
    free(ptr);
    ptr = NULL;
}

int main() {
    function();

    return 0;
}

以上が、free() 関数とメモリリークの防止に関する基本的な解説です。メモリの解放を適切に行うことで、プログラムの安定性とパフォーマンスを向上させることができます。

高度なデータ構造とアルゴリズム

編集

高度なデータ構造とアルゴリズムは、複雑な問題を効率的に解決するために使用されます。これらのデータ構造には、木、グラフ、ハッシュテーブルなどが含まれます。それぞれのデータ構造は、異なる問題に対処するために設計されています。

木は、階層的なデータ構造であり、1つの親ノードから複数の子ノードへと展開される構造を持ちます。木は、ファイルシステム、データベースの索引構造、再帰的なデータ構造の表現など、さまざまな分野で使用されます。代表的な木の種類には、二分木、二分探索木、平衡木などがあります。

以下は、C言語での木(二分探索木)の実装例です。

#include <stdio.h>
#include <stdlib.h>

// 二分探索木のノードを表す構造体
typedef struct TreeNode {
    int data;
    struct TreeNode* left;
    struct TreeNode* right;
} TreeNode;

// 新しいノードを作成する関数
TreeNode* createNode(int data) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    if (newNode == NULL) {
        fprintf(stderr, "メモリの割り当てに失敗しました。\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 二分探索木に要素を挿入する関数
TreeNode* insert(TreeNode* root, int data) {
    // 空の木の場合、新しいノードを作成して返す
    if (root == NULL) {
        return createNode(data);
    }

    // 挿入するデータが現在のノードのデータより小さい場合、左部分木に挿入
    if (data < root->data) {
        root->left = insert(root->left, data);
    }
    // 挿入するデータが現在のノードのデータより大きい場合、右部分木に挿入
    else if (data > root->data) {
        root->right = insert(root->right, data);
    }

    return root;
}

// 二分探索木を中順で表示する関数
void inorderTraversal(TreeNode* root) {
    if (root != NULL) {
        inorderTraversal(root->left);
        printf("%d ", root->data);
        inorderTraversal(root->right);
    }
}

// メモリを解放する関数
void freeTree(TreeNode* root) {
    if (root != NULL) {
        freeTree(root->left);
        freeTree(root->right);
        free(root);
    }
}

int main() {
    TreeNode* root = NULL;

    // 二分木に要素を挿入
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 20);
    insert(root, 40);
    insert(root, 70);
    insert(root, 60);
    insert(root, 80);

    // 中順で木を表示
    printf("二分木の中順表示: ");
    inorderTraversal(root);
    printf("\n");

    // メモリの解放
    freeTree(root);

    return 0;
}

この例では、二分木を中順で表示する方法を示しています。insert() 関数を使用して要素を挿入し、inorderTraversal() 関数を使用して中順で木を表示します。また、freeTree() 関数を使用して、動的に割り当てられたメモリを解放します。

グラフ

編集

グラフは、ノード(頂点)とエッジ(辺)の組み合わせで表されるデータ構造です。ノードは、実体やオブジェクトを表し、エッジはノード間の関係を表します。グラフは、ネットワーク、ルーティング、ソーシャルネットワーク解析など、さまざまな領域で使用されます。グラフには、有向グラフと無向グラフの2種類があります。

以下は、C言語でのグラフの実装例です。ここでは、隣接リストを使用して無向グラフを表現します。

#include <stdio.h>
#include <stdlib.h>

// グラフのノードを表す構造体
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// グラフを表す構造体
typedef struct Graph {
    int numVertices;
    Node** adjLists;
} Graph;

// 新しいノードを作成する関数
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        fprintf(stderr, "メモリの割り当てに失敗しました。\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// グラフを初期化する関数
Graph* createGraph(int numVertices) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    if (graph == NULL) {
        fprintf(stderr, "メモリの割り当てに失敗しました。\n");
        exit(1);
    }
    graph->numVertices = numVertices;

    // 隣接リストの配列を割り当てる
    graph->adjLists = (Node**)malloc(numVertices * sizeof(Node*));
    if (graph->adjLists == NULL) {
        fprintf(stderr, "メモリの割り当てに失敗しました。\n");
        exit(1);
    }

    // 各リストを初期化する
    for (int i = 0; i < numVertices; i++) {
        graph->adjLists[i] = NULL;
    }

    return graph;
}

// エッジを追加する関数
void addEdge(Graph* graph, int src, int dest) {
    // src -> dest のエッジを追加する
    Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;

    // dest -> src のエッジも追加する(無向グラフの場合)
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

// グラフを表示する関数
void printGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        printf("頂点 %d: ", i);
        Node* temp = graph->adjLists[i];
        while (temp) {
            printf("%d -> ", temp->data);
            temp = temp->next;
        }
        printf("NULL\n");
    }
}

// メモリを解放する関数
void freeGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        Node* temp = graph->adjLists[i];
        while (temp) {
            Node* next = temp->next;
            free(temp);
            temp = next;
        }
    }
    free(graph->adjLists);
    free(graph);
}

int main() {
    // グラフの作成
    int numVertices = 5;
    Graph* graph = createGraph(numVertices);

    // エッジの追加
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    // グラフの表示
    printf("グラフの隣接リスト表現:\n");
    printGraph(graph);

    // メモリの解放
    freeGraph(graph);

    return 0;
}

この例では、グラフを隣接リストで表現しています。各頂点にはそれに隣接する頂点のリストが格納されます。addEdge() 関数を使用して、エッジを追加し、printGraph() 関数を使用してグラフを表示します。また、freeGraph() 関数を使用して、動的に割り当てられたメモリを解放します。

ハッシュテーブル

編集

ハッシュテーブルは、効率的なデータの挿入、検索、削除を行うためのデータ構造です。ハッシュテーブルは、キーと値のペアを格納し、キーをハッシュ関数によってハッシュ値に変換し、それに基づいてデータを格納・検索します。ハッシュテーブルは、高速な検索が必要な場合や、大量のデータを管理する場合に使用されます。


以下は、C言語でのハッシュテーブルの実装例です。ここでは、簡単なチェイン法によるハッシュテーブルを実装します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE 10

// ハッシュテーブルのエントリを表す構造体
typedef struct HashNode {
    int key;
    int value;
    struct HashNode* next;
} HashNode;

// ハッシュテーブルを表す構造体
typedef struct HashTable {
    HashNode* table[SIZE];
} HashTable;

// 新しいノードを作成する関数
HashNode* createNode(int key, int value) {
    HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
    if (newNode == NULL) {
        fprintf(stderr, "メモリの割り当てに失敗しました。\n");
        exit(1);
    }
    newNode->key = key;
    newNode->value = value;
    newNode->next = NULL;
    return newNode;
}

// ハッシュ関数
int hashCode(int key) {
    return key % SIZE;
}

// ハッシュテーブルに要素を挿入する関数
void insert(HashTable* ht, int key, int value) {
    int index = hashCode(key);
    HashNode* newNode = createNode(key, value);
    if (ht->table[index] == NULL) {
        ht->table[index] = newNode;
    } else {
        HashNode* temp = ht->table[index];
        while (temp->next != NULL) {
            temp = temp->next;
        }
        temp->next = newNode;
    }
}

// キーに対応する値を検索する関数
int search(HashTable* ht, int key) {
    int index = hashCode(key);
    HashNode* temp = ht->table[index];
    while (temp != NULL) {
        if (temp->key == key) {
            return temp->value;
        }
        temp = temp->next;
    }
    return -1; // キーが見つからない場合
}

int main() {
    HashTable ht;

    // ハッシュテーブルの初期化
    for (int i = 0; i < SIZE; i++) {
        ht.table[i] = NULL;
    }

    // 要素の挿入
    insert(&ht, 1, 10);
    insert(&ht, 2, 20);
    insert(&ht, 11, 30); // 同じハッシュ値を持つキー

    // 検索
    printf("キー1に対応する値: %d\n", search(&ht, 1)); // 10
    printf("キー2に対応する値: %d\n", search(&ht, 2)); // 20
    printf("キー11に対応する値: %d\n", search(&ht, 11)); // 30

    return 0;
}

この例では、チェイン法によるハッシュテーブルを実装しています。insert() 関数を使用して要素を挿入し、search() 関数を使用して指定されたキーに対応する値を検索します。ハッシュ関数は単純な剰余を用いて実装されていますが、実際のアプリケーションではより複雑なハッシュ関数が使用される場合もあります。

アルゴリズム

これらのデータ構造には、それぞれ対応するアルゴリズムがあります。例えば、グラフの最短経路問題にはダイクストラ法やベルマンフォード法、木の探索には深さ優先探索(DFS)や幅優先探索(BFS)、ハッシュテーブルの衝突解決にはチェイン法やオープンアドレス法などがあります。これらのアルゴリズムは、それぞれのデータ構造の特性を活かして問題を解決します。

高度なデータ構造とアルゴリズムは、プログラミングやアルゴリズムの理解を深める上で重要な要素です。これらを理解し、適切に活用することで、効率的なプログラムの設計や問題解決が可能になります。

ソートアルゴリズム(クイックソート、マージソートなど)

編集

探索アルゴリズム(二分探索、深さ優先探索、幅優先探索)

編集

ファイル入出力とストリーム

編集

標準入力・標準出力・標準エラー出力のストリーム

編集

ファイルの読み書き(fopen()、fclose()、fread()、fwrite()など)

編集

バッファリングとファイルポインタの制御

編集

マルチスレッドプログラミングと同期

編集

スレッドの作成と制御(pthreadライブラリの使用)

編集

ミューテックスとセマフォの利用

編集

スレッド間通信とデータの共有

編集

ネットワークプログラミング

編集

ソケットの作成と利用

編集

TCP/IPとUDPプロトコルの実装

編集

クライアント・サーバーモデルの構築

編集

配列は一級市民ではない

編集

C言語において、配列は一級市民ではありません。一級市民とは、変数や関数のように、その値を変数に代入したり、関数の引数として渡したり、関数の戻り値として受け取ったりすることができるプログラム要素のことです。

配列は、一般的な変数とは異なり、ポインタとして解釈される場合があります。つまり、配列名自体は、配列の先頭要素へのポインタとして扱われます。しかし、配列そのものを変数に代入することはできませんし、関数の引数として配列を直接渡すこともできません。

例えば、以下のコードは無効です。

int main() {
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5];

    arr2 = arr1; // 配列を配列に代入することはできない

    return 0;
}

また、関数の引数として直接配列を渡すこともできません。代わりに、ポインタを使用して配列の先頭要素への参照を渡す必要があります。

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5); // 配列を関数の引数として渡す際には、ポインタを使用する

    return 0;
}

このように、C言語において配列は一級市民ではなく、ポインタとして扱われることが多いです。

配列同士の代入

編集

C言語において、配列同士の代入や演算はできません。配列は、単なるメモリ領域を連続して確保したものであり、配列名自体は配列の先頭要素へのポインタとして解釈されます。

配列同士の代入を行うことはできませんが、配列の内容をコピーするためには、ループを使用して要素ごとに値をコピーする必要があります。

#include <stdio.h>

int main() {
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5];

    // 配列の内容をコピー
    for (int i = 0; i < 5; i++) {
        arr2[i] = arr1[i];
    }

    // コピーされた配列の内容を出力
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr2[i]);
    }
    printf("\n");

    return 0;
}

配列同士の演算

編集

また、配列同士の演算も直接行うことはできません。配列の各要素に対して個別に演算を行う場合は、ループを使用して各要素を処理する必要があります。

#include <stdio.h>

int main() {
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5] = {6, 7, 8, 9, 10};
    int result[5];

    // 配列同士の要素ごとの加算
    for (int i = 0; i < 5; i++) {
        result[i] = arr1[i] + arr2[i];
    }

    // 加算結果を出力
    for (int i = 0; i < 5; i++) {
        printf("%d ", result[i]);
    }
    printf("\n");

    return 0;
}

したがって、C言語では配列同士の代入や演算を行うことはできませんが、配列の要素を個別に処理することで目的の操作を実現することができます。

配列の一括代入
この節で触れたように、C言語では配列同士の代入は出来ません。が、構造体同士の代入はできます。

この事と、キャストを組み合わせると。

強引な配列の一括代入
#include <stdio.h>

int main() {
  int a[] = {2, 3, 5, 7, 11},
      b[sizeof a / sizeof *a];
      
  struct Dummy { int dmy[sizeof a / sizeof *a] };
  
  *(struct Dummy *)b = *(struct Dummy *)a;
  printf("b[3] = %d\n", b[3]);
}
の様に要素1つの構造体の代入として実現できます。

main関数

編集

プログラム起動時に呼び出される関数はmainと名付けられています。実装では,この関数のプロトタイプを宣言しない。この関数は,戻り値の型がintで,パラメータがない状態で定義されなければならない。

int main(void) { /* ... */ }

または2つのパラメータ(ここではargcとargvとしていますが、宣言された関数のローカルなものなので、どのような名前を使っても構いません)を持つものです。

int main(int argc, char *argv[]) { /* ... */ }

またはそれと同等の方法で、またはその他の実装で定義された方法による[1]

  • argc の値は非負でなければならない。
  • argv[argc]はヌルポインターとする。
  • argcの値が0より大きい場合、配列メンバargv[0]~argv[argc-1]には、プログラムの起動前にホスト環境によって実装定義された値を持つ文字列へのポインタが格納される。この目的は、ホスト環境の他の場所からプログラム起動前に決定された情報をプログラムに供給することである。ホスト環境が大文字と小文字の両方の文字を持つ文字列を供給できない場合、実装は文字列が小文字で受信されるようにしなければならない。
  • argc の値が 0 より大きい場合、argv[0] が指す文字列はプログラム名を表す。argv[0][0]には、ホスト環境からプログラム名が得られない場合は、ヌル文字を指定する。argv[0][0]はヌル文字とする。argc の値が 1 より大きい場合は、argv[1]~argv[argc-1]が指す文字列がプログラム仮引数(program parameters; 一般にコマンドライン引数)を表す。
  • パラメータ argc、argv および argv 配列が指す文字列は、プログラムによって変更可能であり、プログラムの起動から終了までの間、最後に保存された値を保持する。

プログラム仮引数

編集

プログラム仮引数とは何か

編集

コマンドラインから実行ファイル名などを入力してプログラムを実行する際、実行ファイルに文字列データを渡すことができます。

方法は、コンパイル後に、単にコマンド入力で、たとえば

$ 実行ファイル名 moji

のように、ファイル名の後ろに、渡したい文字列を書けばいい。(上記コマンドの場合なら文字列「moji」が渡される)

このように、実行ファイル自体に対して渡される情報を、プログラム仮引数と呼ぶ。(なお「プログラム仮引数」自体の意味は、単になんらかの関数の定義側で書かれた引数のことであり、実行ファイルとは関係ない。)

例えばコマンドプロンプトにおいて、

C:\example.exe ABC DEF GHI

のようにプログラムを実行した場合、example.exeプログラムのmain関数に対して、「example.exe」、「ABC」、「DEF」、「GHI」という4個の文字列が渡される。

プログラム仮引数を利用するには、ソースコードのmain関数に

void main(int argc, char *argv[]) {
// 以下略

のようにmain関数に引数 int argc, char *argv[] をこの順番で書けばいい。

この順番を守ること。

第一引数の整数型の引数は、コマンドライン入力時の単語の数です。(実行ファイル名も単語1個ぶんとして数える。)

たとえばコマンド入力が

C:\example.exe ABC DEF GHI

なら、argcは4になる。

上記の例の場合

argv[0]は実行ファイル名「example.exe」であり、
argv[1]は 「ABC」 であり、
argv[2]は 「DEF」 であり、
argv[3]は 「GHI」 です。


コード例
#include "stdio.h"
#include "string.h"

void main(int argc, char *argv[]) {

  if (argc == 1) {
    printf("hello");
  }

  if (argc >= 2) {

    // strcmpは文字列比較して同じなら0を返す関数

    if ( strcmp(argv[1], "en")==0 ) {
      printf("hello");
    }

    if ( strcmp(argv[1], "ja")==0 ) {
      printf("こんにちは");
    }
  }
}

コマンド入力は、ファイル名が example.exe なら、コンパイル後にたとえば

example.exe ja

のように入力すれば、日本語で「こんにちは」と答える。

プログラムの実行環境

編集

フリースタンディング環境

編集

フリースタンディング環境でプログラムが利用できるライブラリ機能は、<float.h>, <iso646.h>, <limits.h>, <stdarg.h>,<stdbool.h>, <stddef.h>, <stdint.h> および <stdnoreturn.h>[2]で、これ以外は実装で定義されます[2]

ホスト環境

編集
プログラムの開始
編集
プログラムの実行
編集
プログラム終了処理
編集

関数指定子

編集

inline関数指定子

編集

inline関数指定子は、関数の宣言だけで使用できます。 inline関数指定子は、その関数の呼び出しを可能な限り速くすることを示唆します。 この示唆が効果をもつ程度は、処理系定義とします。 [3]

inline関数は、その関数を呼び出した部分に展開して直接埋め込む。 関数呼び出しにかかる処理を短縮することができるが、コードを複数の部分に展開するためファイルサイズが大きくなる。

inline関数の使用例
#include <stdio.h>

inline int function(int a, int b)
{
        return a + b;
}

int main(void){
        int r = function(1,2);
}

_Noreturn関数指定子

編集

_Noreturn関数指定子は、関数が呼び出し元に戻らないことを示します。 _Noreturn関数指定子と<stdnoreturn.h>ヘッダファイルは、C11で追加されました[4]

_Noreturn void f (void)
{
    abort(); 
}

_Noreturn関数指定子は、関数mainには適用できません。

関数の応用

編集

関数へのポインタ

編集

関数へのポインタとは、ある関数のメモリアドレスを格納し、その関数に間接的にアクセスする方法です。 関数へのポインタは、次のような場合に使われる。 ひとつは、関数を呼び出す際に、関数へのポインタを引数として渡し、呼び出した関数の内部で、関数へのポインタが指す関数を実行する場合。 もうひとつは、関数へのポインタの配列をつくり、if文や、switch文で関数を呼び出すのをやめ、コードを単純にする場合。

関数へのポインタの宣言の記述は次のようになっている。

返却値のデータ型 (*関数へのポインタ名)(引数のリスト):

この宣言では、代入する関数と同じ返却値のデータ型と引数のリストを指定する必要がある。 また、演算子の優先順位のため、「*関数へのポインタ名」を囲う「()」を省略はできない。

関数のメモリアドレスを関数へのポインタに格納する記述は次のようになっている。

関数へのポインタ名 = 関数名;

関数名の後ろに、「()」はいらないことに注意。

関数へのポインタを使って、間接的に関数を呼び出すには、次のように記述します。

(*関数へのポインタ名)(引数のリスト)
//例 関数へのポインタの引数
int main(void)
{
        func2(func1);
}
#include <stdio.h>

void func1() { printf("func1()が呼び出されました。\n"); }

void func2(void (*func)()) {
  printf("func2()が呼び出されました。\n");
  (*func)();
}

int main(void) {
  func2(func1);
}
関数へのポインタの配列
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
  if (b == 0) {
    printf("0で割ることはできません。\n");
    return 0;
  } else {
    return a / b;
  }
}

int main(void) {
  int i, j;
  int arithmetic;
  int (*func[])(int a, int b) = {add, sub, mul, div};
  printf("2つの整数をスペースで区切って入力してください。:");
  scanf("%d %d", &i, &j);
  printf("計算方法を入力してください(0=加法、1=減法、2=乗法、3=除法)。:");
  scanf("%d", &arithmetic);
  if (0 <= arithmetic && arithmetic <= 3)
    printf("答えは%d。\n", (*func[arithmetic])(i, j));
}

再帰的呼出し

編集

再帰的呼出しとは、ある関数がその関数自身を呼び出すことです。 典型的な用途として、数学の階乗(5×4×3×2×1のような計算)などが、よくあげられる。

再帰的呼出しに向いた計算に再帰的呼出しを使うと、ソースコードを簡潔に書ける場合がある。

ただし、再帰的呼出しではハードウェア資源的には弱点があり、スタックを多く占有するので負担になるという弱点もある。 そのため、再帰的呼出しで桁数の多すぎる計算などをすると、与えられたスタック領域を使いはたてしまうと異常終了などのエラーを引きおこす場合もある。 これは、いわゆる「スタック・オーバーフロー」(stack overflow)というエラーの一種です。

再帰を使った階乗の計算
#include <stdio.h>

int factorial(int n) {
  if (n == 0)
    return 1;
  return factorial(n - 1) * n;
}

int main(void) {
  int i;
  printf("整数を入力してください:");
  scanf("%d", &i);
  printf("%dの階乗は%dです。", i, factorial(i));
}

この例では再帰的呼出しを使って、入力された整数の階乗を計算している。 3を入力した場合、処理の流れは以下のようになる。

factorial(3)が呼ばれ
factorial(3)が実行されfactorial(2)が呼ばれ
factorial(2)が実行されfactorial(1)が呼ばれ
factorial(1)が実行されfactorial(0)が呼ばれ
factorial(0)が実行され1が返され
factorial(1)に戻り1*1が返され
factorial(2)に戻り1*1*2が返され
factorial(3)に戻り1*1*2*3が返される
再帰的呼出しを使って2進数表示
編集
再帰的呼出しを使って2進数表示
#include <stdio.h>

void print_binary(int n) {
  if (n < 0) {
    puts("-");
    print_binary(-n);
    return;
  }
  int x = n / 2;
  if (x)
    print_binary(x);
  putchar("01"[x % 2]);
}

int main(void) {
  print_binary(0xbadbeef);
}
実行結果
0101110101101101111101110111

6行目と11行目で print_binary 自身を呼び出し再帰呼び出しとなっています。 print_binary は、リエントラントでない関数 puts と putchar を呼び出しているのでリエントラントでは有りませんが、問題なく再帰呼び出しが行なえます。 このように、再帰可能性(Recursivity)と再入可能性(Reentrancy)は別個の概念で、再入可能性(Reentrancy)はマルチスレッドやシグナルハンドラーで問題になります。

構造体のビットフィールド

編集

ビットフィールドとは、 1ビット以上のビットからなる、 構造体のメンバーの一種です。 ビットフィールドを用いると、 1ビット以上のビットに名前をつけ、 その名前でアクセスできます。

ビットフィールドは、構造体を宣言する時メンバーとして次のように記述します。

データ型 メンバー名:サイズ;

データ型は、(signed) intまたはunsigned intを指定します。 (signed) intを指定すると、上位ビットが符号として扱われる。 メンバー名とサイズとを「:(コロン)」で区切る。 サイズはそのメンバーを何ビットで表すかを指定します。

ビットフィールド
#include <stdio.h>

int main(void) {
  struct sbits {
    unsigned char bit8 : 1;
    unsigned char bit7 : 1;
    unsigned char bit6 : 1;
    unsigned char bit5 : 1;
    unsigned char bit4 : 1;
    unsigned char bit3 : 1;
    unsigned char bit2 : 1;
    unsigned char bit1 : 1;
  } bits;

  bits.bit1 = 1;
  bits.bit2 = 0;
  bits.bit3 = 1;
  bits.bit4 = 0;
  bits.bit5 = 1;
  bits.bit6 = 0;
  bits.bit7 = 1;
  bits.bit8 = 0;

  printf("bitsのメンバーの値は、%d %d %d %d %d %d %d %dです。\n", bits.bit1,
         bits.bit2, bits.bit3, bits.bit4, bits.bit5, bits.bit6, bits.bit7,
         bits.bit8);
  printf("bitsのサイズは、%dです。\n", sizeof(bits));
}
結果
bitsのメンバーの値は、1 0 1 0 1 0 1 0です。
bitsのサイズは、1です。

上の例では、0か1かの値を持つ1ビットの8つのビットフィールドをもつbits構造体を宣言し各ビットフィールドに値を代入しその値を表示している。 なお、unsigned char型をビットフィールドに使えるかはコンパイラによる。

オブジェクト指向プログラミング

編集

C言語は一般にオブジェクト指向プログラミング言語だとは考えられていませんが、従前の構造体やマクロを使うことでクラスの継承に類似した実装を行うことができます。

構造体やマクロを使った継承の例
#include <stdio.h>

struct Shape {
    double x, y;
    void (* print)(struct Shape *);
    double (* area)(struct Shape *);
};

struct Square {
    struct Shape shape;
    double wh;
};
static void printSquare(struct Shape* sp) {
    struct Square *p = (struct Square *)sp;
    printf("Square(x:%f, y:%f, wh:%f)", sp->x, sp->y, p->wh);
}
static double areaSquare(struct Shape* sp) {
    struct Square *p = (struct Square *)sp;
    return p->wh * p->wh;
}
#define Square(x, y, wh) (struct Shape*)&((struct Square){ {x, y, printSquare, areaSquare }, wh })

struct Rectangle {
    struct Shape shape;
    double w, h;
};
static void printRectangle(struct Shape* sp) {
    struct Rectangle *p = (struct Rectangle *)sp;
    printf("Rectangle(x:%f, y:%f, w:%f, h:%f)", sp->x, sp->y, p->w, p->h);
}
static double areaRectangle(struct Shape* sp) {
    struct Rectangle *p = (struct Rectangle *)sp;
    return p->w * p->h;
}
#define Rectangle(x, y, w, h) (struct Shape*)&((struct Rectangle){ {x, y, printRectangle, areaRectangle }, w, h })

struct Circle {
    struct Shape shape;
    double r;
};
static void printCircle(struct Shape* sp) {
    struct Circle *p = (struct Circle *)sp;
    printf("Circle(x:%f, y:%f, r:%f)", sp->x, sp->y, p->r);
}
static double areaCircle(struct Shape* sp) {
    struct Circle *p = (struct Circle *)sp;
    return 3.1415926536 * p->r * p->r;
}
#define Circle(x, y, r) (struct Shape*)&((struct Circle){ {x, y, printCircle, areaCircle }, r })

void print(struct Shape *p ) {
    p->print(p);
}
double area(struct Shape *p ) {
    return p->area(p);
}
int main(void){
    struct Shape* shapes[] = {
        Square(10.0, 20.0, 15.0),
        Rectangle(20.0, 10.0, 12.0, 11.0),
        Circle(15.0, 10.0, 10.0),
    };
    for (int i = 0; i < sizeof shapes / sizeof *shapes; i++) {
        print(shapes[i]);
        printf(", area = %f\n", area(shapes[i]));
    }
}
実行結果
Square(x:10.000000, y:20.000000, wh:15.000000), area = 225.000000
Rectangle(x:20.000000, y:10.000000, w:12.000000, h:11.000000), area = 132.000000 
Circle(x:15.000000, y:10.000000, r:10.000000), area = 314.159265

この例は、継承というより合成の例になっているという批判はありえます。

脚註

編集
  1. ^ N2176 C17 ballot ISO/IEC 9899:2017. ISO/IEC JTC1/SC22/WG14. p. 10, §5.1.2.2.1 Program startup. オリジナルの2018-12-30時点によるアーカイブ。. https://web.archive.org/web/20181230041359/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf. 
  2. ^ 2.0 2.1 N2176 C17 ballot ISO/IEC 9899:2017. ISO/IEC JTC1/SC22/WG14. p. 8, §4. Conformance. オリジナルの2018-12-30時点によるアーカイブ。. https://web.archive.org/web/20181230041359/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf. 
  3. ^ 『JISX3010:2003』p.83「6.7.4 関数指定子」
  4. ^ 『ISO/IEC 9899:2011』p.91「6.7.4 Function specifiers」