C言語の学習:標準関数を自作してみよう!【上級編】

C言語の標準関数の作成方法解説ページ(上級編)アイキャッチ

このページにはプロモーションが含まれています

このページは、”標準関数を自作することでC言語の学習をしよう!” という趣旨のページになります。

このシリーズのページとしてはこのページが第2弾で、このページは上級編になります。

初級編に関しては下記リンク先のページに用意しておりますので、より簡単な標準関数の自作に取り組んでみたい方は、下記リンク先から初級編のページに飛んでいただければと思います。

C言語の標準関数の作成方法解説ページアイキャッチ C言語の学習:標準関数を自作してみよう!【初級編】

また、このページの各章の構成や、関数の自作の流れについても上記ページで解説していますので、これらについて知りたい方も上記ページを読んでいただければと思います。

初級編に比べて、この上級編で紹介する標準関数を自作するためには、特に “C言語に対する深い知識” が必要になります。

おそらく “なんとなく存在は知ってるけど使ったことない” と感じているようなものも利用して実装する必要があります。で、その実装を通して、”C言語ってこんなこともできるんだ!” ということを実感していただければと思います。

MEMO

このページの解説やヒントでは int 型のサイズを 4 バイトとして説明しています

環境によって int 型のサイズは異なる可能性があるので注意してください

上級編で自作する関数一覧

それでは標準関数を自作していきましょう!

この上級編では、下記の関数の作戦に挑戦していただきたいと思います!

  • memcpy:メモリをコピーする
  • qsort:配列をソートする
  • strtok:文字列を分離する

memcpy

最初は memcpy です。仕様的には簡単な関数ですが、実装するのは割と難しいです。

スポンサーリンク

スポンサーリンク

関数の仕様

作成する関数の仕様について紹介していきます。

関数の宣言

作成する関数の宣言は下記の通りです。

my_memcpy
void *my_memcpy(void *dst, const void *src, size_t n);

関数の引数

第1引数 dst はコピー先のメモリの先頭アドレス

第2引数 src はコピー元のメモリの先頭アドレス

第3引数 n はコピーするバイト数

関数の動作

src から n バイトのデータを dst にコピーする 

関数の戻り値

コピー先のメモリの先頭アドレス

回答のテンプレート

回答のテンプレートは下記になります。my_memcpy を実装してプログラムを実行し、Clear!!! と表示されれば memcpy の自作関数が完成です。

memcpyの回答のテンプレート
#include <stdio.h>
#include <string.h> 

typedef struct test {
    int a;
    char b;
    unsigned int c;
} TEST;

void *my_memcpy(void *, const void *, size_t);

void *my_memcpy(void *dst, const void *src, size_t n) {
    /* 自作してください */
}

int main(void) {
    char dst1[256];
    char src1[256];
    char *ret1;
    unsigned int dst2;
    unsigned int src2;
    unsigned int *ret2;
    TEST dst3[3];
    TEST src3[3];
    TEST *ret3;

    /* 前準備 */
    memset(src1, 0x77, sizeof(src1));
    strcpy(src1, "Hello! Good bye!");
    memset(dst1, 0xFF, sizeof(dst1));

    /* 自作した関数を実行 */
    ret1 = my_memcpy(dst1, src1, strlen(src1) + 1);

    /* 実行結果の確認 */
    if (ret1 != dst1) {
        /* 戻り値が正しくない! */
        printf("Error1!!!\n");
        return -1;
    }

    /* 実行結果の確認 */
    if (strcmp(dst1, "Hello! Good bye!") != 0) {
        /* ちゃんとコピーできてない! */
        printf("Error2!!!\n");
        return -1;
    }

    /* 実行結果の確認 */
    if (dst1[strlen(src1) + 1] == 0x77) {
        /* コピーしすぎ! */
        printf("Error3!!!\n");
        return -1;
    }

    /* 前準備 */
    src2 = 0x12345678;
    dst2 = 0;

    /* 自作した関数を実行 */
    ret2 = my_memcpy(&dst2, &src2, sizeof(src2));

    /* 実行結果の確認 */
    if (dst2 != src2) {
        /* ちゃんとコピーできてない! */
        printf("Error4!!!\n");
        return -1;
    }

    /* 前準備 */
    memset(dst3, 0x0, sizeof(TEST) * 3);
    memset(src3, 0x0, sizeof(TEST) * 3);

    src3[0].a = 1;
    src3[0].b = 'x';
    src3[0].c = 10;

    src3[1].a = 2;
    src3[1].b = 'y';
    src3[1].c = 11;

    src3[2].a = 3;
    src3[2].b = 'z';
    src3[2].c = 12;

    /* 自作した関数を実行 */
    ret3 = my_memcpy(dst3, src3, sizeof(TEST) * 3);

    /* 実行結果の確認 */
    if (memcmp(src3, dst3, sizeof(TEST) * 3) != 0) {
        /* ちゃんとコピーできてない! */
        printf("Error5!!!\n");
        return -1;
    }

    printf("Clear!!!\n");
    
    return 0;
}

ヒント

ヒントを表示する場合は、表示したいヒントに合わせて下記をクリックしてください。

コピーの仕方がわからない時のヒント

基本的には、src から dst にデータを 1 バイトずつコピーしていくことになります。そしてそれを n バイト分繰り返します。また、コピーは代入によって実現することができます。

そういう意味では、strcpy 関数に似ています。strcpy 関数を自作するときにも src から dst1 バイトずつ(1文字ずつ)コピーしましたよね?

MEMO

strcpy 関数の自作については初級編で紹介していますので、strcpy の自作の仕方が分からない方は下記ページをご参照いただければと思います

C言語の標準関数の作成方法解説ページアイキャッチ C言語の学習:標準関数を自作してみよう!【初級編】

ただし、strcpy 関数とは異なり、文字列をコピーするわけではないので、最後にヌル文字を付加する処理は不要です。また、コピーを行うループの終了条件は、src の文字がヌル文字になった時ではなく、n バイトコピーした時になります。

この辺りを考慮すると、memcpy は下記のようなループで実現できそうですよね?

コピー時のループ1
size_t i;

for (i = 0; i < n; i++) {
    dst[i] = src[i];
}

ですが、上記を実装してコンパイルすると、コンパイルエラーが発生します。

理由は srcdst の型が void * だからです。void * 型のポインタ 変数に対しては、添字演算子([])や間接演算子(*)が使用できません。 

つまり、void * 型のポインタ変数からは、これらのポインタが指す先のデータにはアクセスできない(データの取得ができない・データの格納ができない)ことになります。

ですので、srcdstvoid * 型以外の型に変換してからコピーを行う必要があります。

この変換は、void * 型のポインタ引数を、下記のように異なるポインタ型の変数に代入することで実現することができます(src_byte の型に const を付けているのは、src の型にも const が付いているからです)。

void *からの型変換
型名 *dst_byte = dst;
const 型名 *src_byte = src;

上記変数を用いた場合、コピーを行う際のループ内の処理は下記のようになります。

コピー時のループ2
size_t i;
型名 *dst_byte = dst;
const 型名 *src_byte = src;

for (i = 0; i < n; i++) {
    dst_byte[i] = src_byte[i];
}

型名 と書いた部分には、intshort などの型名を指定する必要があります。

ただし、memcpy の場合、1バイトずつデータをコピーする必要があります。1バイトずつコピーするには 型名 は何にする必要があるでしょうか?

ここが memcpy を自作する上での最大のポイントになります。

例えば 型名 を short にした場合、下記の代入では2バイトのデータのコピーが行われることになります。これは、short の型のサイズが2バイトだからです。

2バイトのデータのコピー
short *dst_byte = dst;
const short *src_byte = src;

dst_byte[i] = src_byte[i];

何を返却すれば良いかがわからない時のヒント

memcpy の戻り値は “コピー先のメモリの先頭アドレス” です。

ですので、引数の dst のアドレスそのものを返却すれば良いです。

スポンサーリンク

解答例

解答例を表示する場合は下記をクリックしてください。

my_memcpyの実装例
void *my_memcpy(void *dst, const void *src, size_t n) {
    size_t i;

    /* char*型に変換 */
    char *dst_byte = dst;
    const char *src_byte = src;

    for (i = 0; i < n; i++) {

        /* 1バイトずつデータをコピー */
        dst_byte[i] = src_byte[i];
    }

    /* dstのアドレスを返却 */
    return dst;
}

スポンサーリンク

解説

解説を表示する場合は下記をクリックしてください。

memcpy を自作する上で一番のポイントになるのが、引数 srcdst の型が void * であるところです。

MEMO

この解説では const については省略して説明していきます

const について詳しく知りたい方は下記ページをご参照いただければと思います

constの解説ページアイキャッチ 【C言語】constの使い方・メリットを解説

引数 srcdst の型を char * もしくは unsigned char * に変換すれば良いことに気づけば、あとは一瞬で自作することができると思います。ですが、深いC言語の知識を持っていないと、このことに気づくのが難しいです。

コピーの仕方がわからない時のヒントでも解説したように、void * 型のポインタ変数に対しては添字演算子([])や間接演算子(*)が使用できません。

そのため、src の指す先のメモリからデータを取得し、dst の指す先のメモリへデータを格納するためには、これらの変数を一旦他のポインタ型に変換する必要があります。これは、ポインタの指す先のデータへのアクセス(取得や格納)は、添字演算子([])と間接演算子(*)でしか行うことができないからです(他の関数を利用しない場合)。

この変換先の型は、char * or unsigned char * である必要があります。これは、charunsigned char の型のサイズが 1 バイトだからです。

ポインタ変数を使用した時の動作は、そのポインタ変数の型の “基になる型” によって異なります(”基になる型” とは、例えば int * なら int 型、char * なら char 型のように、ポインタ変数宣言時に * の前に指定する “型” のことを言っています)。

例えば、ポインタ変数には加算や減算を行うことが可能ですが、ポインタ変数に +1 した際には、ポインタ変数に格納されているアドレスが、そのポインタ変数の型の “基になる型” のサイズ分増加することになります(逆に -1 した際にはそのサイズ分減少する)。

ポインタへの加算結果を図示したもの

また、ポインタ変数には添字演算子([])を使用して “ポインタ変数が指すデータ” にアクセスすることが可能です。

この添字演算子に指定する値を 1 増加させた場合、アクセス先のアドレスは、そのポインタ変数の型の “基になる型” のサイズ分増加することになります(つまり各要素のサイズが “基になる型” のサイズとして扱われる)。

添字演算子を利用したデータアクセスの様子を示した図

また、ポインタ変数に添字演算子([])や間接演算子(*)を利用して代入を行う場合、代入されるデータのサイズは、そのポインタ変数の型の “基になる型” のサイズとなります。

ここまでの解説を踏まえて、例えば下記のように srcdst の型を int * に変換してデータのコピーを行なった場合について考えてみましょう。

int*型に変換した際のコピー1
size_t i;

/* int*型に変換 */
int *dst_int = dst;
const int *src_int = src;

for (i = 0; i < n; i++) {
    dst_int[i] = src_int[i];
}

まず1回目のループで dst のアドレスに src のアドレスから 4 バイト分のデータがコピーされることになります。

void*をint*に変換したときの処理1

さらに、2回目のループでは dst から 4 バイト上位のアドレスに、src から 4 バイト上位のアドレスの 4 バイト分のデータがコピーされることになります。

void*をint*に変換したときの処理2

3回目のループでは dst から 8 バイト上位のアドレスに、src から 8 バイト上位のアドレスの 4 バイト分のデータがコピーされることになります。

void*をint*に変換したときの処理3

こんな感じで、int 型のサイズ分ずつ一気にコピーが行われてしまうため、n 回ループしてしまうと、 n  * int 型のサイズ バイト分のコピーが行われてしまうことになります。これは明らかに memcpy の仕様と異なります(仕様では、コピーするバイト数は n のみ)。

ループ回数を n / int 型のサイズ にすれば解決しそうでもありますが、nint 型のサイズ で割り切れない場合はデータのコピーバイト数が n よりも少なくなってしまいます。

int*型に変換した際のコピー2
size_t i;

/* int*型に変換 */
int *dst_int = dst;
const int *src_int =src;

for (i = 0; i < n / sizeof(int); i++) {
    /* n / sizeof(int) * sizeof(int) バイトのみコピーが行われる */
    dst_int[i] = src_int[i];
}

ですので、ポインタ変数から1バイト単位でデータを処理したい場合は、そのポインタ変数の型は、型のサイズが1バイトである char もしくは unsigned char 型を基にしたポインタ型、つまり char * or unsigned char * にする必要があります。

MEMO

int * 型等に変換して、コピーできるだけ複数バイトでまとめてコピーを行い、端数分だけ char * or unsigned char * 型で処理するのでもオーケーです 

特に char 型は “文字を扱う型” と覚えている方もおられるかもしれませんが、”1バイト単位で処理するための型” という側面もあることを覚えておくと良いと思います。

むしろ重要なのは後者の方です。英字は1バイト単位で表現可能なので(アルファベット大文字 26 文字 + 小文字 26 文字 + その他の文字の数 < 256)、char 型を利用して扱うことが多いというだけです。

基本的には、プログラムで扱うデータの最小単位はバイトですので、その最小単位でデータを扱うことができる charunsigned char は非常に便利です。それよりも大きいデータは、結局は1バイトが集まって出来たデータですので、極論すれば、charunsigned char 型の変数のみで処理することが可能です。

現に自作した memcpy により、1バイトよりも大きな int 型のデータや TEST 型のデータのコピーも、char 型のデータの代入により実現できていますよね?

また、例えば下記のように、int 型のデータを1バイト単位で扱うようなこともできます。

intをバイト単位で扱う
int x = 0x12345678;
char *p_x;
int i;

/* 1バイト単位で扱うためにchar*型のポインタに指させる */
p_x = &x;

for (i = 0; i < sizeof(int); i++) {
    /* 1バイト単位で表示 */
    printf("0x%x, ", p_x[i]);
}
MEMO

x に代入する値を16進数でデータを表記したり、printf%x を指定して16進数で表示するようにしているのは、バイト単位の区切りをわかりやすくするためです

16進数は2桁の値がちょうど1バイト分のデータ(10進数で 0 〜 255)になるので、バイト単位でデータを分析するときに便利です

上記のソースコードをコンパイルして実行すると、結果として 0x78, 0x56, 0x34, 0x12 が表示されると思います。

これはつまり、変数 x には、4バイト単位で見ると 0x12345678 が格納されているようにも思えますが、1バイト単位でみると下の図のように 0x78, 0x56, 0x34, 0x12 の順でメモリに格納されているようなことを示しています。

int型整数をバイト単位で分解した時の結果を示す図

ちょっと横道に逸れてしまいましたが、ではなぜ、memcpy 関数ではこんなにややこしい void * 型を引数として使用しているのでしょうか?

これは memcpy 関数でどんな型のデータでもコピーできるようにするためです。

void * 型の変数では、どんな型のデータでも指すことができるため、引数を void * 型にしてしまえば、どんなデータのアドレスでも指定することができることになります。

例えば、引数の型を int * にしてしまうと、基本的には int 型の配列や int 型の変数のコピーしかできなくなってしまいます。

引数を void * 型にすることで、関数内部で型を変換する必要が出てきますが、それでもどんなデータでも入力できるという、汎用性の面で大きなメリットを得ることができます。

次に自作する関数 qsort に関しても、引数の1つが void * 型になります。この関数を自作してみることで、 void * の扱いにさらに慣れることができると思います。

また、この解説では void * や “ポインタの型” について説明しましたが、これらの詳細は下記ページでも行なっていますので、詳しく知りたい方はぜひ読んでみて下さい!

voidとvoid*型の解説ページのアイキャッチ 【C言語】void型とvoid*型(void型ポインタ)について解説 ポインタの型の解説ページアイキャッチ 【C言語】ポインタの「型」について解説

qsort

次は qsort です。おそらく自作の観点からすると最難関の標準関数になると思います。そもそも使い方もややこしい…。

qsort 関数の特徴は、どんな型の配列でもソート可能であるところです。qsort 関数の自作を通して、どんな型のデータでも扱える関数を作るためのポイントを実感していただければと思います。

スポンサーリンク

スポンサーリンク

関数の仕様

作成する関数の仕様について紹介していきます。

関数の宣言

作成する関数の宣言は下記の通りです。

my_qsort
void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *);

関数の引数

第1引数 elems はソートを開始するデータの先頭アドレス

第2引数 num_elem はソート対象となるデータの個数

第3引数 size_elem はソート対象となる各データのサイズ

第4引数 compare は比較を行う関数のアドレス

qsort関数における各引数の意味合いを示す図

関数の動作

アドレス elems から num_elem 個分のデータを昇順にソートする(1つ1つのデータのサイズは size_elem)。

本来の qsort では、ソートにはクイックソートアルゴリズムが使用されますが、今回はソートさえ行うことができれば、基本的には何のアルゴリズムを用いても良いことにします。

ただし、qsort 関数の引数の都合上、バケットソートのようなデータの比較を行わずにソートするアルゴリズムは使わない方が良いです。

オススメは下記ページで紹介している選択ソートです。オススメな理由は簡単だからです。

選択ソート解説ページのアイキャッチ 選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

また、クイックソートも下記ページで紹介していますので、クイックソートを利用する場合は参考にしてください。

クイックソート解説ページのアイキャッチ クイックソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

これらのページではソートを行う関数も紹介していますので、それを利用していただいても問題ありません。ただし、紹介している関数は int 型のデータをソートすることを考慮して作成していますので、関数の引数や関数の中身は変更する必要があります。

また、再帰処理は使用しても良いですし、使用しなくても良いです。

要は前述の通り、ソートさえできれば良いです。

関数の戻り値

なし

ソート結果は elems の指すデータとなります(つまり elems のデータを関数内で直接変更してソートを行う)

回答のテンプレート

回答のテンプレートは下記になります。my_qsort を実装してプログラムを実行し、Clear!!! と表示されれば qsort の自作関数が完成です。

qsortの回答のテンプレート
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h> /* 乱数生成用 */

void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *));

void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *)) {
    /* 自作してください */
}

/* 文字列比較用の関数 */
int compare_str(const void *p_data1, const void *p_data2) {
    const char *p_str1 = p_data1;
    const char *p_str2 = p_data2;

    return strcmp(p_str1, p_str2);
}

/* 整数比較用の関数 */
int compare_int(const void *p_data1, const void *p_data2) {
    const int *p_int1 = p_data1;
    const int *p_int2 = p_data2;
    int ret;

    if (*p_int1 < *p_int2) {
        ret = -1;
    } else if (*p_int1 > *p_int2) {
        ret = 1;
    } else {
        ret = 0;
    }

    return ret;
}

typedef struct _PERSON {
    int age;
    int weight;
    int height;
} PERSON;

/* 構造体比較用の関数 */
static int compare_age(const void *p_data1, const void *p_data2) {
    int ret;
    const PERSON *person1 = p_data1;
    const PERSON *person2 = p_data2;

    if (person1->age < person2->age) {
        ret = -1;
    } else if (person1->age > person2->age) {
        ret = 1;
    } else {
        ret = 0;
    }
    return ret;
}

int main(void) {

    int i;

    char array1[7][256] = {
        "Sunday",
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday"
    };
    int array2[1024];
    PERSON array3[10];

    /* 乱数生成用 */
    srand((unsigned int)time(NULL));

    /* 前準備はなし(array1は宣言時に初期化ずみ)*/

    /* 自作した関数を実行 */
    my_qsort(&array1[0][0], 7, sizeof(array1[0]), compare_str);
    
    /* 実行結果の確認 */
    for (i = 0; i < 6; i++) {
        if (strcmp(array1[i], array1[i + 1]) > 0) {
            /* ソートがうまく出来ていない */
            printf("Error1!!!");
            return -1;
        }
    }

    /* 前準備 */    
    for (i = 0; i < 1024; i++) {
        /* ランダムに値を設定 */
        array2[i] = rand() % 1024;
    }

    /* 自作した関数を実行 */
    my_qsort(&array2[0], 1024, sizeof(array2[0]), compare_int);
    
    /* 実行結果の確認 */
    for (i = 0; i < 1023; i++) {
        if (array2[i] > array2[i + 1]) {
            /* ソートがうまく出来ていない */
            printf("Error2!!!");
            return -1;
        }
    }

    /* 前準備 */
    for (i = 0; i < 10; i++) {
        /* ランダムに値を設定 */
        array3[i].age = rand() % 100;
        array3[i].height = rand() % 100 + 100;
        array3[i].weight = rand() % 100 + 50; 
    }

    /* 自作した関数を実行 */
    my_qsort(&array3[0], 10, sizeof(array3[0]), compare_age);
    
    /* 実行結果の確認 */
    for (i = 0; i < 9; i++) {
        if (array3[i].age > array3[i + 1].age) {
            /* ソートがうまく出来ていない */
            printf("Error3!!!");
            return -1;
        }
    }

    printf("Clear!!!\n");

    return 0;
}

補足1:比較関数について

初級編で紹介した回答のテンプレートや、このページの最初に自作した memcpy の回答のテンプレートとは異なり、今回は自作する関数以外の関数が3つ存在します。

これら3つの関数の具体的な戻り値は下記のようになります。つまり、引数で指定された2つのデータの大小関係(文字列の場合はアルファベット順的に前にあるか後ろにあるか)を比較する関数になります。

  • compare_str
    • p_data1p_data2 が同じ文字列:0
    • p_data1p_data2 よりもアルファベット順的に前側:0 より小さい値
    • p_data1p_data2 よりもアルファベット順的に後ろ側:0 より大きい値
  • compare_int
    • p_data1p_data2 が同じ整数:0
    • p_data1p_data2 よりも小さい:0 より小さい値
    • p_data1p_data2 よりも大きい:0 より大きい値
  • compare_age
    • p_data1age メンバと p_data2age メンバが同じ整数:0
    • p_data1age メンバが p_data2age メンバよりも小さい:0 より小さい値
    • p_data1age メンバが p_data2age メンバよりも大きい:0 より大きい値

ただし、これらの関数はプロトタイプ宣言されていない& my_qsort 関数よりも下側で定義されているため、my_qsort 関数からは直接呼ぶことができないので注意してください。

その代わり、my_qsort 関数の第4引数 compare に上記の3つの関数のいずれか(適切なもの)のアドレスを指定して実行するようにしていますので、そのアドレスを使用して関数呼び出しを行なってください。

補足2:他の関数の定義について

my_qsort 関数以外にご自身で関数を定義していただいても問題ありません。特に再帰処理を行うような場合は関数を定義した方が実装しやすいと思います。

ただし関数を定義する場合は、my_qsort 関数よりも上側に定義するようにしてください。

スポンサーリンク

使用してもよい関数

memcpy は使用しても良いです。既に自作しているので、memcpy を利用せずに自作した時のコードを利用しても良いです。

また、malloc 関数も必要に応じて使用してください。

スポンサーリンク

ヒント

ヒントを表示する場合は、表示したいヒントに合わせて下記をクリックしてください。

ソートの仕方がわからない時のヒント

そもそもソートの仕方がわからないという方は、選択ソートとクイックソートのアルゴリズムについては下記ページで解説していますので、こちらを参考にしていただければと思います。

選択ソート解説ページのアイキャッチ 選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) クイックソート解説ページのアイキャッチ クイックソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

上記のページでは、int 型の配列をソートするためのソート関数も紹介しています。が、今回はどんな型の配列でもソートを行えるようにするため、上記のページで紹介している関数を作成するよりも難易度は高いです。

ですので、まだソートを実装したことないという方は、まずは上記ページを参考にして int 型の配列をソートする関数を作成することをオススメします。

void * の扱いがわからない時のヒント

まず void * の扱い方がわからない方は、memcpy の自作を思い出しましょう!

void * はどんな型のデータも指せる(どんな型のデータのアドレスも格納できる)反面、間接演算子(*)や添字演算子([])を利用できません。

また、void * 型ポインタへの加算や減算も動作が未定義のため行わない方が無難です。

ですので、void * 型ポインタ変数は、データを指すだけであれば便利ですが、そのポインタ変数が指すデータを扱って処理を行うには不便です。従って、一旦関数内部では、処理を行いやすくするために他の型に変換した方が良いです。

また、1バイト単位で処理できた方が便利なので、char * もしくは unsigned char * に型変換するのが良いです。

例えば、my_qsort 関数の第1引数 elems を下記のように char * 型の byte_elems に代入すれば、これにより elems のアドレスを char * 型のアドレスとして扱うことができます。

void*からchar*への変換
char *byte_elems = elems;

さらに、第3引数 size_elem でソート対象の各データのサイズが指定されています。

つまり、ソート対象の各データの先頭アドレスは、データ同士で size_elem ずつ離れていることになります。

ソート対象の各データのサイズを示した図

ソートを開始するデータの先頭アドレスは byte_elems になりますので、その先頭のデータを第 0 個目のデータとすれば、第 i 個目のデータの先頭アドレスは byte_elems + i * size_elem として計算することができます。

ソート対象の各データのアドレスを示した図

ですので、例えば第 i 個目のデータと、第 j 個目のデータを比較する際には、アドレス byte_elems + i * size_elem に存在するデータとアドレス byte_elems + j * size_elem に存在するデータを比較することになります。

比較の仕方がわからない時のヒント

今回の my_qsort を実装する上でポイントになるのがデータ同士の比較です。

この比較は my_qsort 関数の第4引数 compare のアドレスの関数を呼び出しにより行う必要があります。

この compare は、関数のアドレスを格納するポインタになります。つまり、関数ポインタです。

ちなみに、main 関数での my_qsort 関数の実行部分を見ればわかるとおり、関数のアドレスは 関数名 のみを記述することで指定することが可能です。

例えば下記は、関数 compare_int のアドレスを指定するために、関数名 compare_int を引数に指定する例になります。 

関数のアドレスの指定
my_qsort(&array2[0], 1024, sizeof(array2[0]), compare_int);

関数ポインタを利用した際の関数呼び出しは、関数ポインタを 関数名 のように捉えて行います。

つまり、今回は関数ポインタの変数名が compare なので下記のようにして関数を呼び出すことができます。

compareからの関数呼び出し
戻り値を受け取る変数 = compare(引数1, 引数2, ...);

じゃあ、この関数呼び出し時に指定する引数や、関数呼び出しして受け取る戻り値の型が何になるかというと、これは、my_qsort 関数の第4引数の型を見ればわかります。

my_qsort
void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *);

上記の第4引数において、(*compare) の前に記述されている型が、compare の関数を呼び出した際の “戻り値の型” となります。つまり、compare の関数の戻り値の型は int になります。

さらに (*compare) の後ろに記述されている () 内が、compare の関数を呼び出す際に指定する引数になります。つまり、compare の関数には、void * 型の変数を引数として2つ指定する必要があります(要はどんな型のポインタ変数でも良いということです)。

ですので、例えば my_qsort の中でソート対象のデータの中から “第 i 個目のデータ” と “第 j 個目のデータ” を比較したいときには、下記のようにして関数を実行して比較を行うことになります。

compareの実行
int ret;

ret = compare(第 i 個目のデータの先頭アドレス, 第 j 個目のデータの先頭アドレス)

第 i 個目のデータの先頭アドレス の求め方については、void * の扱いがわからない時のヒントで説明していますので、求め方が分からない時はこちらをご参照ください。

上記を実行することで、retcompare の指す関数の実行結果が格納されます。

戻り値の詳細は回答のテンプレートの補足1に記載していますが、要は戻り値が 0 よりも小さいデータの場合、第1引数に指定したアドレスのデータ(第 i 個目のデータ)の方が、第2引数に指定したアドレスのデータ(第 j 個目のデータ)よりも値が小さい(or アルファベット順的に前側)ということになります。

今回は昇順ソートを行いますので、my_qsort 実行後には、第 i 個目のデータは第 j個目のデータよりも前側にないといけないことになります。

逆に、戻り値が 0 よりも大きいデータの場合、第1引数に指定したアドレスのデータ(第 i 個目のデータ)の方が、第2引数に指定したアドレスのデータ(第 j 個目のデータ)よりも値が大きい(or アルファベット順的に後ろ側)ということになります。

したがって、my_qsort 実行後には、第 i 個目のデータは第 j 個目のデータよりも後ろ側にないといけないことになります。

データの交換の仕方がわからない時のヒント

クイックソートや選択ソートでは、ソートを進める上でデータの交換が必要になります。

my_qsort 関数内でデータの交換を行う際には、交換する各データのサイズが第3引数の size_elem により決まるところがポイントになります。

my_qsort
void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *);

例えば第 i 個目のデータと第 j 個目のデータを交換する場合、”第 i 個目のデータの先頭アドレス から size_elem バイト分のデータ” と “第 j 個目のデータの先頭アドレス から size_elem バイト分のデータ” の交換を行う必要があります(先頭のデータを “第 0 個目” のデータとしています)。

データの交換ではsize_elemバイトのコピーが必要であることを示す図

さらに、この交換は次の3ステップの処理により実現する必要があります。

  1. 第 i 個目のデータの先頭アドレス から size_elem バイト分のデータ” を 退避先メモリの先頭アドレス にコピーする
  2. 第 j 個目のデータの先頭アドレス から size_elem バイト分のデータ” を 第 i 個目のデータの先頭アドレス にコピーする
  3. 退避先メモリの先頭アドレス から size_elem バイト分のデータ” を 第 j 個目のデータの先頭アドレス にコピーする
    データの交換の流れを示す図

いきなり 2. の処理を行なってしまうと第 i 個目のデータが上書きされて無くなってしまうので、これを防ぐために 1. で一旦そのデータを退避してから交換を行う必要があります。

このデータの交換を行う上でのポイントは下記の3つです。

  • アドレスの求め方
  • コピーの仕方
  • 退避先メモリの確保の仕方

まずポイントの1つ目は “アドレスの求め方” です。

データを交換する際には、交換するデータのアドレスを求める必要があります。

例えば 第 i 個目のデータ と 第 j 個目のデータ を交換する場合、第 i 個目のデータの先頭アドレス第 j 個目のデータの先頭アドレス のアドレスを求める必要があります。

このアドレスの求め方については、void *の扱いがわからない時のヒントで説明していますので、求め方がわからない方はこのヒントを参照していただければと思います。

ポイントの2つ目は “コピーの仕方” です。

これは、要は最初に自作した memcpy と同じです。

つまり、void * の引数を他の型(char * or unsigned char *)に変換してから1バイトずつ代入演算子(=)で代入しましょうということです。この代入はデータのサイズ分、つまり size_elem バイト分繰り返し行う必要があります。

ただ、今回は memcpy は使用しても良いことにしていますので、memcpy でコピーしてしまっても良いです。

ポイントの3つ目は “退避先メモリの確保の仕方” です。

前述の通り、データの交換を行う際には退避先となるデータの置き場(メモリ)が必要です。

退避するデータのサイズは size_elem バイトですので、データの交換を行う際には、事前に size_elem バイトのメモリを用意しておく必要があります。

このメモリは malloc 関数で確保することができます。

malloc 関数は、引数に指定したサイズ(バイト単位)のメモリを動的に確保する関数です。

ですので、下記のように malloc 関数を実行してやれば、データの退避先となるメモリを用意することができます。

退避先メモリの確保
tmp = malloc(size_elem);

さらに、malloc 関数の戻り値は確保したメモリの先頭アドレスとなります。つまり、この malloc 関数の戻り値が、退避先メモリの先頭アドレス となります。

スポンサーリンク

解答例

解答例を表示する場合は下記をクリックしてください。

今回はソートするアルゴリズムに応じた2つのソート例を示します。2つとも再帰呼び出しを用いて実装した例になりますが、再帰呼び出しを行わなくても、ソートができていればオーケーです。

クイックソートでソートする場合

クイックソートによりソートを行う関数 my_qsort は下記のようになります。my_qsort から quickSort 関数を呼び出して、ソートを実行しています。

my_qsortの実装例(クイックソート)
/* クイックソートを行う関数 */
void quickSort(void *elems, int left, int right, size_t size_elem, int (*compare)(const void *, const void *)) {
    void *pivot; /* ピボットを指すポインタ */
    void *tmp; /* データ退避用のメモリを指すポインタ */
    int i, j;

    /* elemsをchar*に変換 */
    char *byte_elems = elems;

    /* ソートを行う範囲が1以下の場合 */
    if(left >= right) {
        return;
    }

    /* 退避用のメモリを確保 */
    tmp = malloc(size_elem);
    if (tmp == NULL) {
        /* メモリ確保に失敗 */

        return;
    }

    /* pivot の決定 */
    pivot = byte_elems + size_elem * left;

    i = left;
    j = right;

    /* pivot以下のデータを配列の前半に
       pivot以上のデータを配列の後半に集める */
    while(1) {
        /* pivot以上のデータを左側から探索 */
        while (compare(byte_elems + size_elem * i, pivot) < 0) {
            i++;
        }

        /* pivot以下のデータを右側から探索 */
        while (compare(byte_elems + size_elem * j, pivot) > 0) {
            j--;
        }

        /* i >= j になったということは
           配列の左側にpivot以下のデータが、
          配列の右側にpivot以上のデータが集まったということ */
        if (i >= j) {
            /* 集合の分割処理は終了 */
            break;
        }

        /* 探索した2つのデータを交換 */
        memcpy(tmp, byte_elems + i * size_elem, size_elem);
        memcpy(byte_elems + i * size_elem, byte_elems + j * size_elem, size_elem);
        memcpy(byte_elems + j * size_elem, tmp, size_elem);
 
        /* 交換後のデータの次から探索再開 */
        i++;
        j--;

    }

    free(tmp);

    /* 小さいデータを集めた範囲に対してソート */
    quickSort(elems, left, i - 1, size_elem, compare);

    /* 大きいデータを集めた範囲に対してソート */
    quickSort(elems, j + 1, right, size_elem, compare);

}

void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *)) {

    /* 実処理はquickSort関数で実行 */
    quickSort(elems, 0, num_elem - 1, size_elem, compare);
}

選択ソートでソートする場合

選択ソートによりソートを行う関数 my_qsort は下記のようになります。my_qsort から selectionSort 関数を呼び出して、ソートを実行しています。

my_qsortの実装例(選択ソート)
/* 選択ソートを行う関数 */
void selectionSort(void *elems, int left, int right, size_t size_elem, int (*compare)(const void *, const void *)) {
    int i;
    void *min;
    void *tmp;

    /* elemsをchar*に変換 */
    char *byte_elems = elems;

    /* データ数が1の場合はソート済みなのでソート終了 */    
    if (left == right) {
        return;
    }

    /* 退避用のメモリを確保 */
    tmp = malloc(size_elem);
    if (tmp == NULL) {
        /* メモリ確保に失敗 */

        return;
    }

    /* ひとまずソート範囲の先頭を最小値とみなす */
    min = byte_elems + left * size_elem;

    /* ソート範囲の中から最小値を探索 */
    for (i = left; i <= right; i++) {
        if (compare(min, byte_elems + i * size_elem) > 0) {
            /* 最小値を更新 */
            min = byte_elems + i * size_elem;
        }
    }

    /* ソート範囲の終了点と最小値を交換 */
    memcpy(tmp, byte_elems + left * size_elem, size_elem);
    memcpy(byte_elems + left * size_elem, min, size_elem);
    memcpy(min, tmp, size_elem);

    free(tmp);

    /* ソート範囲を狭めて再度選択ソート */
    selectionSort(elems, left + 1, right, size_elem, compare);
}

void my_qsort(void *elems, size_t num_elem, size_t size_elem, int (*compare)(const void *, const void *)) {

    /* 実処理はselectionSort関数で実行 */
    selectionSort(elems, 0, num_elem - 1, size_elem, compare);
}

スポンサーリンク

解説

解説を表示する場合は下記をクリックしてください。

それでは、qsort 関数を自作する上でのポイントを解説していきたいと思います。

ソートの仕方もポイントの1つではあるのですが、これについては下記のページ等で解説していますので、ソートの仕方のポイントを知りたい方は別途下記ページを参照していただければと思います。

選択ソート解説ページのアイキャッチ 選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) クイックソート解説ページのアイキャッチ クイックソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

ここでは、ソートの仕方以外の部分のポイントを解説していきます。

今回の qsort 関数の自作におけるポイントは、int 型の配列ソート時の関数と比較すると分かりやすいと思います。

ここでは、int 型の配列をソートする選択ソートの関数と比較することで、ポイントを解説していきたいと思います(選択ソートで解説するのは、クイックソートよりも実装内容が簡単だからです)。

int 型の配列をソートする関数 selectionSort は下記になります。

int型の配列を選択ソート
void selectionSort(int *elems, int left, int right) {
    int i;
    int *min;
    int tmp;

    /* データ数が1の場合はソート済みなのでソート終了 */    
    if (left == right) {
        return;
    }

    /* ひとまずソート範囲の先頭を最小値とみなす */
    min = elems + left;
    

    /* ソート範囲の中から最小値を探索 */
    for (i = left; i <= right; i++) {
        if (*min > *(elems + i)) {
            /* 最小値を更新 */
            min = elems + i;
        }
    }

    /* ソート範囲の終了点と最小値を交換 */
    tmp = *(elems + left);
    *(elems + left) = *min;
    *min = tmp;

    /* ソート範囲を狭めて再度選択ソート */
    selectionSort(elems, left + 1, right);
}

上記は、下記ページで紹介している selectionSort 関数を、第1引数を int data[] から int *elems に、また関数内部の処理を添字演算子ではなく間接演算子を利用するように変更したものになります(変更していますが、動作は全く同じです)。

選択ソート解説ページのアイキャッチ 選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

また、上記の関数相当の動作をする関数が、解答例の選択ソートの場合で紹介した selectionSort 関数になります。

同じ関数名で分かりにくいので、これらを intSortvoidSort として呼び分けたいと思います。

この intSortvoidSort とで異なる点は、下記の3つになります。

  • 引数
  • 代入の仕方
  • 比較の仕方

この3つの違いそれぞれについて解説していきたいと思います。

void * 型のポインタを扱うにはサイズが必要なことが多い

まず引数が違いますね!

voidSortの引数
void selectionSort(void *elems, int left, int right, size_t size_elem, int (*compare)(const void *, const void *))
intSortの引数
void selectionSort(int *elems, int left, int right)

第1引数は両者ともにソートを開始するデータの先頭アドレスになりますが、両者で型が異なります。voidSort では、どんな型のデータでもソートできるように、第1引数の方が void * 型になっています。

また、voidSort でのみ、各データのサイズ size_elem と、比較を行う関数のアドレス compare を受け取るように引数を設定しています。

size_elem を受け取るようにしているのは、voidSort では intSort とは異なり、ソート対象の各データのサイズがわからないためです。このサイズが分からないと、各データがどのアドレスに存在するかも分かりませんし、後に説明する代入も行うことができません。

例えばアドレスに関していうと、第 i 個目のデータ(先頭のデータを第 0 個目のデータとしています)の先頭アドレスは、各データのサイズ size_elem を用いて byte_elems + size_elem * i として計算しますので、この計算式より、サイズの情報がないと各データのアドレスの計算のしようがないというところが理解していただけるのではないかと思います(byte_elemsvoid * 型の elemschar * 型に変換したポインタ変数です)。

一方で、void * 型以外の型のポインタの場合、その変数への加算や減算は、その変数の “基となる型”(例えば int * であれば intchar * であれば char)のサイズを考慮して計算が行われます。

例えば int * 型のポインタに +1 すれば、そのポインタに格納されているアドレスは 4 増加することになります。これは int 型のサイズが 4 バイトだからです。また、+2 すれば、アドレスは 8 が増加することになります。

ですので、ソート対象のデータが int 型の配列で、その先頭アドレスが elems に格納されているのであれば、第 i 個目のデータのアドレスは単純に elems + i により計算することが可能です。つまり、void * 型の時とは異なり、各データのサイズはわざわざ引数としてしてもらう必要はありません。

こんな感じで、引数が void * 型の関数の場合、そのポインタ引数の指しているデータのサイズが分からないので、関数内部でアドレス計算などを行う際には、サイズの情報を別途引数で指定してもらう必要があることが多いです。

逆にいうと、サイズさえきちんと引数で受け取れば、どんなサイズのデータでも(どんな型のデータでも)アドレス計算等ができますので、汎用性の高い関数を作成することが可能です。

また、voidSort でのみ比較を行う関数のアドレス compare を受け取るようにしているのは、この関数の内部ではデータの比較を行うことができないからです。これについては後述の関数ポインタを利用して汎用的な比較を実現で説明したいと思います。

memcpy により汎用的な代入が可能

続いて、代入の処理の違いについて解説します。

intSortvoidSort ともに、ソートを行う上でデータの交換を行なっています。

この時に、intSort では代入演算子(=)を利用して処理を行なっていますが、voidSort では memcpy 関数を利用して処理を行っているという違いがあることが確認できると思います。

voidSortの代入
memcpy(tmp, byte_elems + left * size_elem, size_elem);
memcpy(byte_elems + left * size_elem, min, size_elem);
memcpy(min, tmp, size_elem);
intSortの代入
tmp = *(elems + left);
*(elems + left) = *min;
*min = tmp;

上記の、特に voidSort の処理で何を行なっているかよくわからないという方や tmp がどういった変数かが分からない方は、データの交換の仕方がわからない時のヒントを参照していただければと思います。

C言語では、データのコピーを行う処理は、代入演算子(=)による代入 or memcpy 等の標準関数により行うことができます。ただし、前者の代入の場合、右辺と左辺の変数の型により、コピーできるデータの量(バイト数)が固定されてしまいます。

例えば intSort で行っている下記の処理は、tmp のアドレスに対して、elems + left のアドレスのデータをコピーしている処理と捉えることができます。

tmpへのデータコピー
tmp = *(elems + left);

コピーされるデータの量(バイト数)は、int 型のサイズ となります。これは、両辺の変数の型が int だからです。こんな感じで、代入によりデータのコピーは行えますが、コピーされるサイズが変数の型によって固定されてしまいます。

なので、intSort は、int 型のデータのソートにしか利用できません。

その一方で qsort関数では、どんなサイズのデータ(どんな型のデータ)に対してもソートが行えるように、どんな型のデータでもコピーできるように処理する必要があります。

ですので、どんなサイズのデータでもコピーできるように memcpy を利用しています。memcpy であれば、第3引数に指定する値(バイト数)を変更することで、どんなサイズのデータでもコピーすることができます。

ただし、コピーするサイズが分からないとコピーできないので、1つのデータのサイズを引数として渡してもらう必要があります。今回の場合は voidSort 関数の第4引数  size_elem が、そのサイズとなります。

voidSortの引数
void selectionSort(void *elems, int left, int right, size_t size_elem, int (*compare)(const void *, const void *))

以上が、voidSortmemcpy 関数を利用している理由になります。

ここからは補足ですが、前述の通り、memcpy 関数はどんなサイズのデータでもコピーできますので、intSort で行っている代入処理も、下記のように memcpy 関数による処理に置き換えることができます。

memcpyを用いたint型データの代入
memcpy(tmp, elems + left, sizeof(int));
memcpy(elems + left, min, sizeof(int));
memcpy(min, tmp, sizeof(int));

つまり、やろうと思えば、代入は memcpy 関数でのデータコピーにより代用することができます。

ただし、代入ができるのであれば、代入を行なった方が安全です。特に、コピー先のデータの型とコピー元のデータの型が異なる場合、代入によるデータのコピーと memcpy によるデータのコピーの結果が異なることがあるので注意が必要です。

例えば下記は、int 型の変数 dst に char 型の変数 src をコピーする処理を、代入と memcpy それぞれで行なったときの結果を表示するものになります。

代入とmemcpyの比較
char src;
int dst;

src = 0x12;

/* memcpyによるデータのコピー */
dst = 0xFFFFFFFF;
memcpy(&dst, &src, sizeof(char));
printf("%x\n", dst);

/* 代入によるデータのコピー */
dst = 0xFFFFFFFF;
dst = src;
printf("%x\n", dst);

memcpy によるデータのコピー後に表示される結果は ffffff12 になります。つまり、char 型のサイズの1バイト分のみコピーされ(12 の部分)、その他の部分(ffffff の部分) はコピー前の状態が維持されてしまっています。

一方で、代入によるデータのコピー後に表示される結果は 12 になります。つまり、char 型のサイズの1バイト分のみコピーされ(12 の部分)、その他の部分は 0 に置き換わっています。結果として得たい値はおそらくこっちですよね。

こんな感じで、特にコピー先とコピー元とでデータの型が異なる場合、memcpy でデータのコピーを行うと意図しない結果になる可能性が高いので、代入が用いれる場合は代入を利用した方が良いです。

関数ポインタを利用して汎用的な比較を実現

また、intSort と voidSort とでは、比較の仕方が異なります。

voidSortでの比較
if (compare(min, byte_elems + i * size_elem) > 0) {
    /* 略 */
}
intSortでの比較
if (*min > *(elems + i)) {
    /* 略 */
}

intSort では、比較演算子(<>)を利用して直接比較を行なっていますが、voidSort では、関数ポインタ compare を呼び出して比較を行なっています。この compare は、引数として関数呼び出し側から渡されてくる関数ポインタ(関数のアドレス)になります。

なぜ voidSort では、わざわざ引数として関数ポインタ(関数のアドレス)を受け取るようになっているのでしょうか?なぜ voidSort では、intSort 関数のように、直接自身で比較を行わないのでしょうか?

まず、わざわざ引数として関数ポインタを受け取る理由について説明すると、これは voidSort で、どんな型の配列のデータでもソートできるようにするためです。

今回用意した回答のテンプレートでは下記の3つの比較関数を定義していますが、もし voidSort からこれらの関数を直接呼び出してしまうと、特定の型の配列のみのソートしか行えなくなってしまいます。

  • compare_int:2つの引数のデータを int 型として大小関係を比較する
  • compare_str:2つの引数のデータを文字列としてアルファベットの前後関係を比較する
  • compare_age:2つの引数のデータを PERSON 型として、age メンバの大小関係を比較する

例えば、voidSort で下記のように compare_int を直接呼び出してしまうと、比較時には毎回 compare_int 関数が呼び出されるようになります。

int型のソートに限定されてしまう例
if (compare_int(min, byte_elems + i * size_elem) > 0) {
    /* 略 */
}

compare_int は2つの int 型の整数を比較する関数になっているので、voidSort では int 型の配列のソートしかできなくなってしまうことになります。

その一方で、解答例で示した voidSort のように、引数として関数ポインタ compare を受け取るようにし、比較時にその関数ポインタ compare から関数呼び出すようにすれば、引数 compare のアドレスに応じて呼び出す関数が切り替わるようになります。

関数ポインタからの比較関数の呼び出し
void selectionSort(void *elems, int left, int right, size_t size_elem, int (*compare)(const void *, const void *)) {
    /* 略 */
    
    /* 引数で指定されたcompareによって呼び出す関数が切り替わる */
    if (compare(min, byte_elems + i * size_elem) > 0) {
        /* 略 */
    }
    /* 略 */
}

つまり、関数利用者がソートしたい配列の型に応じて適切に比較を行う関数を指定すれば、関数内から実行される比較関数が自動的に切り替わり、その型に応じた適切な比較および適切なソートを行うことができるようになっています。

例えば回答のテンプレートの main 関数でも、整数をソートする場合は compare_int、文字列をソートする場合は compare_str、構造体を age メンバでソートする場合は compare_age という風に、ソートしたいデータに合わせて my_qsort 関数の第4引数に指定する関数を切り替えるようにしています。

こんな感じで、どんな型の配列でもソートできるようにするために、引数として関数ポインタを受け取るようになっています。

続いて、voidSort で intSort のように、直接自身で比較を行わない理由について解説しておきます。

理由は2つあって、1つ目の理由は、前述のようにどんな型の配列でもソートできるようにするためです。どんな型の配列でもソートできるように、引数として関数ポインタを受け取り、その関数ポインタから比較を行う関数を呼び出すようになっています。

もう1つの理由は、そもそも比較を行うことが無理だからです。

無理な理由は、比較を行うための情報が足りていないからです。比較を行うためには、データのサイズとデータの符号の有無の情報が必要です(ここでは説明は省略しますが、整数 or 浮動小数点数 or 構造体のうちのどの型であるかという情報も必要)。

intSort の場合、ソートする配列の各データが int 型であることが前提として作られています。

ですので、その int 型であるという情報から、ソート(比較)を行う対象がサイズ 4 バイトで “符号あり” のデータであるということが関数内で分かります。

その一方で voidSort の場合、ソート(比較)を行う対象のデータのサイズや符号の有無がわかりません。

これは、voidSort では、ソートする配列へのアドレスが void * の引数として渡されるからです。

void * 型のポインタはどの型のデータも指すことができるメリットがある一方、なんの型のデータを指しているのかが分からないというデメリットがあります。

そして、なんの型のデータを指しているかが分からないので比較を行うことができません。もし比較を行うのであれば、別途サイズや符号の有無の情報を引数として受け取る必要があります。

今回の場合は、引数 size_elem で各データのサイズを受け取っていますので、例えば size_elem4 であれば、比較するデータのサイズが 4 バイトであることはわかります。ですが、そのデータを “符号あり” で扱って比較するのか、”符号なし” で扱って比較するのかが分からないと、正確な比較を行うことができません。

例えば “符号なし” で扱った場合の値が 4294967295 だとすれば、”符号あり” で考えると -1 になります。”符号なし” の場合と “符号あり” の場合と値の大きさが全く異なるので、符号の有無が分からないと、比較が正確に行えないことになります(ここでは詳しい説明を省略しますが、そもそも整数ではなく浮動小数点数や構造体の場合もあります)。

つまり、比較はデータのサイズと符号の有無の情報がないと行うことができないのです。これは比較だけでなく、足し算や引き算などの四則演算に関しても同様のことが言えます。

そして、そのデータのサイズと符号の有無の情報とはまさに “型” です。この型そのものが、データのサイズと符号の有無の情報になりますので、int 型や unsigned int 型、char 型等の変数では、プログラマーは演算や比較を行う処理を記述すれば、実行時に自動的にサイズや符号の有無を考慮した結果を得ることができます。

普段何気なく利用している “型” ですが、比較や演算を行う上で非常に重要な役割を果たしています(代入などでも重要)。

この qsort 関数の自作を行うことで、void * を利用したプログラミングを体験し、その中で void * の便利さと不便さの両方や、”型” の重要性を感じ取っていただけたのではないかと思います。

スポンサーリンク

strtok

最後は strtok です。文字列を分離する関数ですね!

strtok の自作においては、これまでの memcpyqsort のように void * は使用しなくても良いですが、変数の生存期間を意識して自作しなければならないところがポイントになります。

スポンサーリンク

スポンサーリンク

関数の仕様

作成する関数の仕様について紹介していきます。

関数の宣言

作成する関数の宣言は下記の通りです。

my_strtok
char *my_strtok(char *str, const char *sep);

関数の引数

第1引数 str は分離対象となる文字列の先頭アドレス

第2引数 sep は区切り文字から成る文字列の先頭アドレス

関数の動作

関数の動作の詳細については下記ページで解説していますので、下記ページを参照していただければと思います。

strtok関数の使い方の解説ページアイキャッチ 【C言語】strtok関数の使い方と注意点(文字列を区切り文字で分離する関数)

ポイントだけ以下に抜粋しておきます。

  • src を sep に含まれるいずれかの区切り文字で分離する
  • srcsep に含まれる区切り文字が複数ある場合、最初の区切り文字の位置で分離を行う
  • srcNULL を指定して実行された場合、直前に分離した位置の次の文字から分離を開始する
  • 分離を開始する位置に区切り文字が存在する場合、次に最初に現れる “区切り文字以外の文字” から分離を開始する

関数の戻り値

分離後の文字列の先頭アドレス

ただし、下記のような場合は NULL を返却する

  • 1回目の strtok 実行時の第1引数 strNULL が指定された
  • 分離を開始する位置から文字列の終端(ヌル文字)までに区切り文字以外の文字が存在しない

回答のテンプレート

回答のテンプレートは下記になります。my_strtok を実装してプログラムを実行し、Clear!!! と表示されれば strtok の自作関数が完成です。

strtokの回答のテンプレート
#include <stdio.h>
#include <string.h>

char *my_strtok(char *, const char *);


char *my_strtok(char *str, const char *sep){
    /* 自作してください */
}


int main(void) {
    int i;
    char sep[] = ",.#*";
    char str1[] = "One,Two#Three*For.Five";
    char str2[] = ",.#One,Two#,Three*For.Five#.,";
    char *ret1;
    char *ret2;
    char *ans[] = {
        "One", "Two", "Three", "For", "Five"
    };

    /* 前準備はなし(変数宣言時に完了)*/

    /* 自作した関数を実行 */
    ret1 = my_strtok(str1, sep);

    /* 実行結果の確認 */
    if (ret1 == NULL) {
        /* strtokの戻り値がおかしい */
        printf("Error1!!!\n");
        return -1;
    }

    i = 0;
    while (ret1 != NULL) {

        /* 実行結果の確認 */
        if (strcmp(ret1, ans[i]) != 0) {
            /* strtokの戻り値がおかしいor分離ができていない */
            printf("Error2!!!\n");
            return -1;
        }

        ret1 = my_strtok(NULL, sep);

        i++;

    }

    /* 実行結果の確認 */
    if (i != 5) {
        /* 分離する文字列の数がおかしい */
        printf("Error3!!!\n");
        return -1;
    }

    /* 自作した関数を実行 */
    ret2 = my_strtok(str2, sep);

    /* 実行結果の確認 */
    if (ret2 == NULL) {
        /* 先頭の区切り文字の扱いがおかしい */
        printf("Error4!!!\n");
        return -1;
    }

    i = 0;
    while (ret2 != NULL) {

        /* 実行結果の確認 */
        if (strcmp(ret2, ans[i]) != 0) {
            /* 先頭の区切り文字の扱いがおかしい */
            printf("Error5!!!\n");
            return -1;
        }

        /* 自作した関数を実行 */
        ret2 = my_strtok(NULL, sep);

        i++;

    }

    /* 実行結果の確認 */
    if (i != 5) {
        /* 先頭の区切り文字の扱いがおかしい */
        printf("Error6!!!\n");
        return -1;
    }

    printf("Clear!!!\n");

    return 0;
}

スポンサーリンク

使用してもよい関数

文字列の長さを求めたい場合は strlen は使用しても良いです。

既に初級編で自作しているので、strlen を利用せずに自作した時のコードを利用しても良いです(そもそも文字列の長さを求めなくても strtok を自作することは可能です)。

ヒント

ヒントを表示する場合は、表示したいヒントに合わせて下記をクリックしてください。

文字列の分離の仕方がわからない時のヒント

文字列を分離することだけを考えた場合、処理の流れは下記のようになります。

  • 分離を開始する位置から区切り文字と一致する文字を探す
  • 見つかった場合:
    • 見つかった区切り文字をヌル文字('\0')で置き換える
    • ヌル文字に置き換えた後、分離を開始した位置のアドレスを返却する
  • 見つからなかった場合:
    • 分離を開始した位置のアドレスを返却する

C言語では、ヌル文字('\0')を文字列の終端として扱うため、上記のように区切り文字をヌル文字に置き換えてやれば、文字列を分離したことになります。

上記の処理の流れを大枠で実装すると次のようになります。分離を開始する位置のアドレスを startとしています。

文字列の分離を行う際のループ処理
/* 文字開始位置から文字列の分離を行う */
for (i = 0; i < strlen(start); i++) {
    for (j = 0; j < strlen(sep); j++) {
        if (start[i] == sep[j]) {
            /* 見つかった場合 */
            見つかった場合の処理;
        }
    }
}

/* 区切り文字が1つも見つからなかった場合 */

見つからなかった場合の処理;

分離を再開する位置を保持する方法がわからない時のヒント

strtok 関数を自作する上で1番のポイントになるのが、第1引数 strNULL が指定されて実行されたときの分離を再開する位置です。

第1引数 str に文字列の先頭アドレスが指定された場合は、文字列の分離は str から開始すれば良いので簡単です(先頭に区切り文字がある場合は区切り文字を飛ばしてから分離を開始する必要はありますが…)。

その一方で、第1引数 strNULL 指定された場合は、直前に分離した文字列の後ろの文字から分離を開始する必要があります。

次の分離を開始する位置を示す図

ただし、この分離を開始する位置は引数では指定されませんので、関数内部でその位置を覚えておく必要があります。

ですが、関数内で宣言した “通常の” 変数は、関数生成とともに作成され、関数終了とともに解放されてしまうため、前に関数内部で変数に格納したデータは全て消え去ってしまいます。

つまり、直前に実行された strtok で、次に分離を開始する位置を変数に格納しておいたとしても、次に strtok が実行されたときにはその変数の中身は消え去ってしまいます。

関数内で宣言した変数が関数終了とともに解放される様子

じゃあどうすれば、次に分離を開始する位置を覚えておけるのかというと、これは static 変数を利用することで実現することができます。

static 変数は、通常の変数とは異なり、プログラムが起動してからプログラムが終了するまでずっと保持し続けられる変数になります。ですので、関数が終了しても解放されませんし、関数が終了しても、変数に格納されたデータはずっと保持し続けられます。

static変数が関数終了後もデータを保持し続ける様子

したがって、my_strtok 関数内で static 変数を宣言し、その static 変数に、分離した文字列の後ろの位置を格納するようにすれば、関数が終了してもその位置を関数内で保持し続けることができます。

static変数で覚えておく位置を示した図

さらに、次に第1引数 strNULL 指定して my_strtok 関数が実行された際に、その static 変数から次に分離を開始する位置を取得することで、適切な位置から分離を再開することができます。

これらの処理の大枠を実装したものが下記となります。static 変数は、下記の next_start の変数宣言のように、通常の変数宣言の前側に static を指定することで作成することができます。

分離を開始する位置の保持
char *my_strtok(char *str, const char *sep){
    static char *next_start = NULL;
    char *start;

    if(str == NULL) {
        start = next_start;
    } else {
        start = str;
    }

    /* 文字列の分離を行う処理を行う */

    /* 次に分離を開始する位置を覚えておく */
    next_start = 分離した文字列の後ろの位置のアドレス;

    /* 略 */
}

上記では next_startstatic 宣言しているので、この next_start 変数は関数が終了しても解放されません(startstatic 宣言していないので解放されます)。ですので、next_start 変数には、前回関数実行時の 分離した文字列の後ろの位置のアドレス が保持し続けられることになります。

また、上記では strNULL の場合は startnext_start に、それ以外の場合は startstr に設定するようにしていますので、start から分離を開始すれば、適切な位置から分離を開始することができます。

今回の例では next_start をポインタとし、分離した文字列の後ろの位置のアドレス を保持するようにしていますが、別にポインタにこだわる必要もなく、配列の添字を保持するようにするのでも良いです。

ここで説明した static 変数に関しては、下記ページで詳細を解説していますので、詳しく知りたい方は是非読んでみてください。

staticローカル変数の解説ページアイキャッチ 【C言語】staticローカル変数の使い方・メリットを解説

スポンサーリンク

解答例

解答例を表示する場合は下記をクリックしてください。

my_strtokの実装例
char *my_strtok(char *str, const char *sep){
    static char *next_start = NULL;
    unsigned int i, j;
    char *start;

    /* 分離を開始する先頭アドレスを設定 */
    if (str == NULL) {
        /* NULLが指定された場合は続きから */
        start = next_start;
    } else {
        /* NULL以外が指定された場合はstrから */
        start = str;
        next_start = NULL;
    }

    /* 分離開始アドレスがNULLならNULLを返却 */
    if (start == NULL) {
        return NULL;
    }

    /* 分離開始アドレスの先頭が区切り文字の場合は飛ばす */
    while (start[0] != '0') {
        for (j = 0; j < strlen(sep); j++) {
            if (start[0] == sep[j]) {
                /* 区切り文字と一致した場合 */

                /* 分離開始のアドレスを1進める */
                start++;
                break;
            }
        }

        if (j == strlen(sep)) {
            /* 先頭が区切り文字でなかった場合 */

            /* 分離処理に移る */
            break;
        }
    }

    /* 分離開始位置の文字ががNULL文字ならNULL返却 */
    if (start[0] == '\0') {
        next_start = NULL;
        return NULL;
    }

    /* 文字開始位置から文字列の分離を行う */
    for (i = 0; i < strlen(start); i++) {
        for (j = 0; j < strlen(sep); j++) {
            if (start[i] == sep[j]) {
                /* 区切り文字と一致した場合 */

                /* 一致した文字をNULL文字に置き換えて分離 */
                start[i] = '\0';

                /* 次に関数を呼ばれた時の分離を再開するアドレスを覚えておく */
                next_start = &start[i] + 1;

                /* 分離開始位置のアドレスを返却 */
                return start;
            }
        }
    }
    
    /* 区切り文字が1つも見つからなかった場合 */

    /* 次に分離を再開する位置はヌル文字なので、
       次に関数を呼ばれた時は直ちににNULLを返却するよう、
       分離を再開するアドレスをNULLに設定しておく */
    next_start = NULL;

    /* 分離開始位置のアドレスを返却 */
    return start;
}

スポンサーリンク

解説

解説を表示する場合は下記をクリックしてください。

では、まずは解答例のソースコードについて簡単に解説していきたいと思います。

static 変数の宣言

今回の strtok 関数を自作する上で一番のポイントになるのは static 変数を利用する点だと思います。解答例では、下記のように next_startstatic 変数として宣言しています。

next_startのstatic変数宣言
static char *next_start = NULL;

static 変数を利用することで、分離を再開する位置を保持する方法がわからない時のヒントで説明したように、関数終了後も変数に格納したデータを保持し続けることが可能になります。

そして、この next_start に分離後の文字列の1つ後ろの位置のアドレスを保持するようにすることで、2回目以降の my_strtok 関数実行時(第1引数 strNULL 指定時)に、前回の分離後の文字列の後ろから分離を再開することができるようになります。

文字列を分離する

その文字列の分離を行なっているのは下記ループの中の処理になります。このループの中では、分離を開始する位置 start を先頭にして区切り文字を探し、最初に見つけた区切り文字をヌル文字('\0')に置き換える処理を行なっています。

文字列の分離処理
/* 文字開始位置から文字列の分離を行う */
for (i = 0; i < strlen(start); i++) {
    for (j = 0; j < strlen(sep); j++) {
        if (start[i] == sep[j]) {
            /* 区切り文字と一致した場合 */

            /* 一致した文字をNULL文字に置き換えて分離 */
            start[i] = '\0';

            /* 次に関数を呼ばれた時の分離を再開するアドレスを覚えておく */
            next_start = &start[i] + 1;

            /* 分離開始位置のアドレスを返却 */
            return start;
        }
    }
}

このヌル文字('\0')への置き換えが “文字列の分離” になります。なぜならC言語では、ヌル文字('\0')が1つ1つの文字列の終端を表すからです。

さらに、分離を開始した位置のアドレスを返却すれば、その戻り値のアドレスが、分離後の文字列の先頭アドレスとなります。

例えば下の図の文字列の場合、区切り文字を ',' とし、分離を開始した位置、つまり 'O' の位置のアドレスが返却されますので、返却されたアドレスを printf("%s") で表示すれば One と表示されることになります。

区切り文字をヌル文字に置き換えたのちに分離を開始した位置のアドレスを返却する様子

分離を再開する位置を保持する

上記の処理において1番のポイントになるのが、return する前に static 変数である next_start を設定しているところです。この next_start は、分離後の文字列の1つ後ろの位置のアドレスを設定しています。

分離後の文字列の後ろの位置のアドレスをstatic変数に格納する様子

さらに、my_strtok 関数先頭付近の下記の部分で、第1引数 strNULL の場合は、分離を開始するアドレスを示す startnext_start を設定するようにしています。これにより、my_strtok 関数が第1引数 strNULL として実行された際に、前回分離した文字列の1つ後ろの文字から分離を再開することができます。

文字列の分離をnext_startから開始するための処理
/* 分離を開始する先頭アドレスを設定 */
if (str == NULL) {
    /* NULLが指定された場合は続きから */
    start = next_start;
} else {
    /* NULL以外が指定された場合はstrから */
    start = str;
    next_start = NULL;
}
MEMO

今回は、next_start に分離後の文字列の1つ後ろの位置のアドレスを設定するようにしていますが、next_start に分離後の文字列の終端文字の位置のアドレスを設定するようにしても、他の処理で辻褄が合うように実装していれば問題ありません

また、アドレスではなく、配列(文字列)の添字を static 変数で保持するようにしておいても良いと思います

区切り文字が見つからなかったときの処理

また、区切り文字が見つかった場合は、その時点で return するようにしていますので、関数最後部分の下記が実行されるのは、区切り文字が見つからなかった時になります(区切り文字よりもヌル文字が先に見つかった)。

区切り文字が見つからなかった時の処理
/* 区切り文字が1つも見つからなかった場合 */

/* 次に分離を再開する位置はヌル文字なので、
    次に関数を呼ばれた時は直ちににNULLを返却するよう、
    分離を再開するアドレスをNULLに設定しておく */
next_start = NULL;

/* 分離開始位置のアドレスを返却 */
return start;

この場合、文字列の最後まで分離を行なったことになりますので、next_startNULL に設定するようにしています。

また、関数の先頭付近では、start が NULL の場合(つまり strnext_startNULL の場合)は、関数が NULL を返却するようにしています。ですので、上記の next_start = NULL を行なっておくことで、次回 my_strtok 関数が第1引数 strNULL として実行された際には必ず NULL を返却することが出来ます。

おそらく、next_start を下記のように設定したとしても、次回 my_strtok 関数が第1引数 strNULL として実行された際には、同様に NULL を返却することはできると思います(なのでどっちでも良い)。

区切り文字なしの場合のnext_startの他の設定例
next_start = &start[i];

先頭の区切り文字を飛ばす

さらに、my_strtok 関数の先頭付近の下記部分では、分離を開始する位置の先頭に区切り文字があった場合に、その区切り文字を飛ばす処理を行なっています。

/* 分離開始アドレスの先頭が区切り文字の場合は飛ばす */
while (start[0] != '0') {
    for (j = 0; j < strlen(sep); j++) {
        if (start[0] == sep[j]) {
            /* 区切り文字と一致した場合 */

            /* 分離開始のアドレスを1進める */
            start++;
            break;
        }
    }

    if (j == strlen(sep)) {
        /* 先頭が区切り文字でなかった場合 */

        /* 分離処理に移る */
        break;
    }
}

区切り文字が先頭にあるかどうかを調べ、あった場合は分離を開始するアドレス start を1文字分ずつ後ろにずらす処理を行なっています。

上記により、分離後の文字列が空文字列になることを防ぐことができます。

以上が、my_strtok 関数(strtok の自作関数)の解答例のソースコードの解説になります。

ポイントは static 変数

前述の通り、この strtok 関数の自作を行う上で1番のポイントになるのが static 変数です。この static 変数を実際に利用してもらうために、自作する関数にこの strtok を選びました。

static を利用することで、関数が終了してもデータを保持することができる変数を作成することができます。

これにより、今回の例のように、前回実行時に格納したデータを次の関数実行時に利用するようなことが可能です。

今回は static 変数を利用しましたが、グローバル変数も static 変数同様にプログラム開始からプログラム終了まで変数が保持されているので、このグローバル変数でも同様のことを行うことができます。

この二つの決定的な違いはスコープですね!

グローバル変数はファイル内のどの関数からも、さらには extern 宣言を利用すればどのファイルの関数からもアクセスすることが可能です。

その一方で static 変数(より詳細には static ローカル変数)は、宣言した関数内からのみアクセス可能です。

この違いを意識して使い分けると良いと思います!

ちなみに extern 宣言については下記ページで解説していますので、詳しく知りたい方は下記ページを読んでみてください!

extern宣言の解説ページアイキャッチ 【C言語】extern宣言について解説(ファイル間で変数を共有)

static 変数利用時は同時動作に注意

ただ、関数内で static 変数を宣言することによるデメリットもあるので注意が必要です。

それは、同時実行すると動作結果に不整合が出る可能性がある点です。C言語では、マルチスレッドを利用することで、同じ関数を同時に実行するようなことが可能です。

例えば、解説例で示した my_strtok 関数が、2つのスレッド(スレッド1スレッド2 とします)から異なる文字列(文字列1文字列2 とします)に対してほぼ同時に実行された場合について考えてみましょう。

この際、まず スレッド1 からの my_strtok 関数実行によって next_startのアドレスが 文字列1 のものに設定されます(分離した文字列の1つ後ろの位置のアドレス)。が、すぐに スレッド2 からの my_strtok 関数が実行により、その next_start のアドレスが 文字列2 に対するアドレスに上書きされてしまいます。

そうなると、次に スレッド1 から、さらに 文字列1 を分離する目的で my_strtok 関数が実行されたとしても(第1引数を NULL として実行)、スレッド2 によって next_start文字列2 に対するアドレスに上書きされているため、関数実行の結果、文字列1 ではなく、文字列2 に対して分離した結果が得られてしまうことになります。

これは、自作した my_strtok 関数だけでなく、本物の strtok 関数においても生じる現象になります。

同時にstrtokが実行された時に結果に不整合が生じる例

こんな感じで結果に不整合が出る原因は、next_startstatic 変数であることです。static でないローカル変数(static 指定せずに関数内で宣言された変数)は、関数が実行されるたびに別の変数として(別のアドレスに)生成されることになります。ですので、同時に関数が実行されたとしても、static でないローカル変数は、同じ変数名であったとしても実体は別の変数(別のメモリ)として扱われます。

そのため、static でないローカル変数だけを利用している関数では、複数のスレッドから同時に実行されたとしても、それぞれのスレッドでは全く独立した変数を利用して動作するため、上記のような不整合は発生しません。

その一方で、static 変数(グローバル変数も)は、関数実行時ではなく、プログラム起動時に生成される変数で、プログラム終了時までずっと同じ変数(同じアドレスのメモリ)として扱われます。

したがって、複数のスレッドから同時に関数が実行されてしまうと、関数内で全く同じ変数を利用している(同じアドレスのメモリを利用している)ため、上記のように意図せずに他のスレッドからデータが上書きされて結果に不整合が発生する可能性があります。

マルチスレッドを利用したことのない方には難しい解説だったかもしれませんが、”static 変数を利用する関数は、同時実行されてしまうと結果に不整合が発生する可能性がある” ということを是非覚えておいていただければと思います。

ちなみにですが、strtok 関数の場合、上記の不整合を解決した strtok_r という関数が存在します。

strtok_r 関数の宣言は下記のようになっています。

strtok_r
#include <string.h>

char *strtok_r(char *str, const char *sep, char **p_next_start);

strtok 関数と比較して第3引数 p_next_start が増えており、この第3引数で、次に分離を開始するアドレスを “関数” と “関数呼び出し側” とでやりとりできるようにしています。つまり、strtok_r 関数内では、次に分離を開始するアドレスを関数内で保持せずに、関数呼び出し側に渡す&関数呼び出し側に指定してもらうようになっています。

要は、関数内で static 変数を使用しないように、関数呼び出し側で必要な情報を保持してもらうようになっているというわけです。

こんな感じで、同時実行を考慮した場合、関数内では static 変数を利用しないようにすることが解決手段の1つになることがあります。他には排他制御なども解決手段になりますが、どの解決手段を利用するべきかは、関数の作りによるので注意してください。

この解説の中で出てきた static・マルチスレッド・排他制御に関しては下記ページで解説していますので、詳しく知りたい方はぜひ読んでみてください。

staticローカル変数の解説ページアイキャッチ 【C言語】staticローカル変数の使い方・メリットを解説 入門者向け!C言語でのマルチスレッドをわかりやすく解説 排他制御解説ページのアイキャッチ 【C言語】排他制御について解説【Mutex】

まとめ

このページでは、標準関数の自作を問題形式で出題し、その標準関数の自作の仕方について解説しました!

今回は上級編ということで、自作はかなり難しかったのではないのでしょうか?また自作することで学べたことも多かったのではないかと思います。自作することで1つでも気づきがあったのであれば幸いです。

基礎的な内容をしっかり身につけている&今回紹介した標準関数を自作できる実力があるのであれば、実際に現場で活躍できるだけのC言語の力はあるんじゃないかなぁと思います。あとは解説の中でも紹介したマルチスレッドや排他制御などが扱えれば即戦力になれる気もします…!

また、今回は上級編でしたが、下記ページでは初級編も用意しておりますので、まだこちらで紹介する関数を自作したことがない方はこちらも是非挑戦してみてください!初級編といえども、atoi 関数あたりの自作は結構難易度が高いです。

C言語の標準関数の作成方法解説ページアイキャッチ C言語の学習:標準関数を自作してみよう!【初級編】

おすすめの書籍(PR)

また、もっと基礎的な内容をもっと網羅的に、問題形式で解く形でC言語を学習したいという方には、下記の 新・解きながら学ぶC言語 第2版 がオススメです!

C言語の入門書で学ぶことを網羅的に問題形式で出題し、それを解くことでC言語の復習をすることができる書籍になります。

このページで出題したような、プログラム作成問題も184問収録されています(穴埋め問題も含めると全部で1436問!)。さらに、プログラム作成問題に関しては1つ1つの問題に対して解説も充実しています。

問題形式でC言語の復習をしたいという方には最適の本だと思います。どんな感じの本かは上記リンク先から試し読みできると思いますので、興味のある方は是非見てみてください!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示