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

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

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

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

標準関数(標準ライブラリ関数)の自作は、下記のような観点からC言語の勉強に向いていると思います。

  • 標準関数についてより深く知れる
    • 関数の各引数の意味や関数利用時の注意点など
  • 学んだけど普段あまり使うことのない事を実践できる
    • void * 型・関数ポインタなど(これらは主に上級編で利用します)
  • 入力(引数)と出力(戻り値など)が決まっているのでパズル感覚でプログラミングできる
    • 楽しく学べるのが一番!

難易度は割と高めだと思います。C言語の参考書一通り読んだだけだと難しいかも…。

ですが、分からない点も自身で調べながらプログラミングしていくことで、より一層C言語プログラミングの力をつけることができると思います。

また、解答例や解説も用意していますので、それを読むだけでも力になると思います(が、一度はご自身でプログラミングしてみる事をオススメします)。

各問題には、関数自作時に困りそうな点への “ヒント” も準備していますので、全く関数の作り方の見当がつかないような場合でも、ヒントを読みながら関数の自作が進められるようになっています。

ちなみに、このページは初級編になっています。下記ページに上級編も用意していますので、難しい問題にチャレンジしたい方は下記リンクをクリックしてください。

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

また、このページに載せているソースコードは全て MacOSX 上で確認したものであり、他の環境では動作確認をしていません。コンパイルが通らない、プログラムの動作がおかしいなどの問題があれば、恐れ入りますが問い合わせ等で教えていただけると助かります。

章の構成

まずは、このページの構成や関数の自作の仕方について解説させてください。

このページは自作する関数ごとに章を区切っており、各章の構成は下記のようになっています。

  • 関数の仕様
    • 自作する関数の仕様を説明します
    • 具体的には、関数の宣言・引数・動作・戻り値について説明します
  • 回答のテンプレート
    • 自作する関数以外の部分は、回答のテンプレートとして用意しています
    • ソースコードをコピーし、それを自身の開発環境に貼り付け、そこで関数の自作とプログラム実行をしてみてください
    • プログラムを実行すると、あなたが自作した関数の動作の妥当性を自動的に検証するようになっています
      • 自作した関数が仕様通りの動作をする場合、Clear!!! と表示されます
      • 自作した関数が仕様通りの動作をしない場合、Error1!!! などと表示されます
  • 使用可能な関数(なしの場合あり)
    • 関数を利用しないと自作が大変な場合もありますので、その場合は使用してもよい関数をこの項目に記載します
    • この項目がない場合は、他の関数は使用せずに関数を自作するようにしてください
  • ヒント(非表示)
    • 関数を自作する際に困りそうな点について説明します
  • 解答例(非表示)
    • 私が自作した関数を解答例として紹介します
  • 解説(非表示)
    • 関数を自作する上でポイントになる点について解説します

(非表示)と記載した項目については、初期段階では非表示になっていますが、クリックすることで中身を表示できるようになっています。

また、関数の仕様に関しては、あくまでも自作する関数に対する仕様を説明します。標準関数の仕様そのものとは差がある可能性もあるので注意してください(ほぼ同じになっているとは思います)。

また、解答例に関しては、上記の関数の仕様に合わせたものを示しています。また、あくまでも私が考えて作成したものですので、実際の標準関数の実装とは異なりますので注意してください。

関数の自作の仕方

続いて関数の自作の仕方について解説します。

スポンサーリンク

回答のテンプレートに関数の処理を記述

関数の自作は自由に行なっても良いのですが、今回は回答のテンプレートを用意しています。

回答のテンプレートは、自作した関数の動作の妥当性を “自動的に検証する” ように作っていますので、動作確認には回答のテンプレートを利用していただいた方が便利だと思います。

回答のテンプレートを利用する場合は、テンプレートのソースコードを自身の開発環境にコピペし、/* 自作してください */ とコメントされている部分に関数を実現するための処理を記述してください。

回答のテンプレートの例は下記のようになります(strlen の例です)。

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

size_t my_strlen(const char *);

size_t my_strlen(const char *str){
    /* 自作してください */
}

int main(void) {
    char str[256];
    size_t ret;

    /* 前準備 */
    strcpy(str, "Hello! Good bye!");

    /* 自作した関数を実行 */
    ret = my_strlen(str);

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

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

    return 0;
}

上記のように my_strlen の関数内の処理以外は私の方で用意しています。ちなみに、自作する関数は my_ というプレフィックスをつけるようにしています。これは元々の標準関数の名前と衝突しないようにするためです。

関数呼び出し部分の my_ を削除すれば、本物の標準関数の動作を確認することもできます。

プログラムを実行して動作の確認

上記のソースコードのように、main 関数では、自作する関数を実行するための前準備と関数の動作チェックを行なっています。ですので、関数を自作さえすれば、コンパイルしてすぐに動作確認できるようになっています。

さらに、プログラムを実行すれば、自動的に自作した関数の動作が正しいかどうかをチェックできるようになっています。

自作した関数が仕様通りに動作できている時には Clear!!! と表示されます。ちゃんと標準関数が自作できていますので、次の問題に進んでください。

逆に仕様通りに動作できていない時には Error1!!!といった文言のエラーメッセージが表示されます(1 の部分はチェック内容に応じて変更するようにしています)。エラーメッセージから、どのチェックに引っかかっているかを確認し、そのチェックに引っかからないように関数の修正をしてみてください。

今回は、標準関数の “主たる動作”・”ポイントになる動作” が実現できているかどうかのチェックのみを行うようにしています。逆にいうと、細かい動作のチェックはしていません(例えば引数が NULL の場合のエラーハンドリングなど)。

なので、Clear!!! と表示されたからといって、自作した関数が標準関数と完全に同じ動作をするとは限らないので注意してください。

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

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

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

  • abs:絶対値を求める
  • isupper:英字の大文字かどうかを判断する
  • strlen:文字列長を求める
  • strcpy:文字列をコピーする
  • atoi:文字列を整数に変換する

おそらく難易度順に並んでいると思います。最後の atoi に関しては、他の関数に比べて難易度はダントツに高いと思います。

スポンサーリンク

abs

最初は問題に慣れるために簡単な abs を自作してみましょう!

関数の仕様

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

関数の宣言

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

my_abs
int my_abs(int i);

関数の引数

第1引数の i は任意の整数

関数の動作

i の絶対値を求める

関数の戻り値

i の絶対値

回答のテンプレート

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

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

int my_abs(int);

int my_abs(int i) {
    /* 自作してください */
}

int main(void) {
    int i1, i2, i3;
    int ret1, ret2, ret3;

    /* 前準備 */
    i1 = 9876;

    /* 自作した関数を実行 */
    ret1 = my_abs(i1);

    /* 実行結果の確認 */
    if (ret1 != 9876) {
        /* 正の値が変化してしまっている */
        printf("Error1!!!\n");
        return -1;
    }

    /* 前準備 */
    i2 = -1234;

    /* 自作した関数を実行 */
    ret2 = my_abs(i2);

    /* 実行結果の確認 */
    if (ret2 != 1234) {
        /* 絶対値の計算がおかしい */
        printf("Error2!!!\n");
        return -1;
    }

    /* 前準備 */
    i3 = -2147483648; /* intの最小値 */

    /* 自作した関数を実行 */
    ret3 = my_abs(i3);

    /* 実行結果の確認 */
    if (ret3 != -2147483648) {
        /* 最小値の場合は値は変わらない */
        printf("Error3!!!\n");
        return -1;
    }

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

    return 0;
}

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク

ヒント

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

求めるのは絶対値ですので、i が負の値以外の場合はそのまま返却すれば良いです。

i が負の値の場合は、符号を正に変換してから、値を返却する必要があります。

解答例

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

my_absの実装例
int my_abs(int i) {
    int ret;

    if (i < 0) {
        /* iが負の値の場合のみ-1をかける */
        ret = -1 * i;
    } else {
        /* 負の値でない場合はそのまま */
        ret = i;
    }
    
    return ret;
}

スポンサーリンク

解説

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

最初なので簡単なものを選んでみたつもりでしたがいかがだったでしょうか?

負の値の場合のみ引数 i-1 を掛けるようにすれば絶対値は求めることができます。

絶対値を求めることは結構ありますが、わざわざ abs 関数を使わなくても簡単に求めることが可能であることが実感していただけたのではないかと思います。

ちなみにですが、引数 i の値が -2147483648 の場合、-1 を掛けても結果は -2147483648 のままになります。また本物の abs 関数の戻り値も -2147483648 になります。この理由を考えてみても勉強になると思います!

スポンサーリンク

isupper

次は isupper を自作してみましょう!abs よりは難易度は上がると思いますが、まだまだ簡単な方だと思います。 

関数の仕様

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

関数の宣言

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

my_isupper
int my_isupper(int c);

関数の引数

第1引数の c は整数

関数の動作

c が表す英字が大文字であるかどうかを判断する

関数の戻り値

c が表す英字が大文字である場合は 0 以外の整数

c が表す英字が大文字でない場合は 0

回答のテンプレート

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

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

int my_isupper(int);

int my_isupper(int c) {
    /* 自作してください */
}

int main(void) {
    int c1, c2, c3, c4;
    int ret1, ret2, ret3, ret4;

    /* 前準備 */
    c1 = 'A';

    /* 自作した関数を実行 */
    ret1 = my_isupper(c1);

    /* 実行結果の確認 */
    if (ret1 == 0) {
        /* 大文字と判断できていない */
        printf("Error1!!!\n");
        return -1;
    }

    /* 前準備 */
    c2 = 'Z';

    /* 自作した関数を実行 */
    ret2 = my_isupper(c2);

    /* 実行結果の確認 */
    if (ret2 == 0) {
        /* 大文字と判断できていない */
        printf("Error2!!!\n");
        return -1;
    }

    /* 前準備 */
    c3 = 999999;

    /* 自作した関数を実行 */
    ret3 = my_isupper(c3);

    /* 実行結果の確認 */
    if (ret3 != 0) {
        /* 文字以外が大文字と判断されている */
        printf("Error3!!!\n");
        return -1;
    }

    /* 前準備 */
    c4 = 'z';

    /* 自作した関数を実行 */
    ret4 = my_isupper(c4);

    /* 実行結果の確認 */
    if (ret4 != 0) {
        /* 小文字が大文字と判断されている */
        printf("Error4!!!\n");
        return -1;
    }

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

    return 0;
}

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク

ヒント

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

C言語プログラムにおいては、'A' などのシングルクォーテーションで囲まれる文字は、実は整数として扱われます。各文字に割り当てられている整数を文字コードと呼びます。

文字は整数として扱われるため、文字と整数は比較が可能です。

例えば、引数 c に格納されている整数が 'A' を表す整数であるかどうかは、c == 'A' により判断することができます。>< といった比較演算子で比較を行うことも可能です。

さらに 'A''Z' の大文字の英字は、'A'65 としてアルファベット順に 1 ずつ増やしながら整数が割り当てられています(例えば 'B'66 として扱われる)。

解答例

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

my_isupperの実装例
int my_isupper(int c) {

    if (c >= 'A' && c <= 'Z') {
        /* cが'A'〜'Z'の文字コード内の値であれば大文字 */
        return 1;
    }

    return 0;
}

スポンサーリンク

解説

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

ヒントにも記載した通り、'A''Z' の大文字の英字は、'A'65 としてアルファベット順に 1 ずつ増やしながら整数が割り当てられています。

ですので、引数 c の整数が 'A' 以上かつ 'Z' 以下の値の場合、'A''Z' のいずれかの英字であると判断することができます。

で、シングルクォーテーションで囲まれた文字は、単なる整数として扱うことができるので、上記の判断は次のようにして行うことができます。

大文字の英字かどうかの判断
int my_isupper(int c) {

    if (c >= 'A' && c <= 'Z') {
        /* cが'A'〜'Z'の文字コード内の値であれば大文字 */
        return 1;
    }

    return 0;
}

そして、この時に限り 0 以外の値(上記では 1)を返却するようにすれば、isupper 同様の動作をする関数を自作することができます。

ポイントは文字が整数(文字コード)として扱われる点だと思います。

文字は整数として扱われるので、整数と比較を行うこともできますし、文字同士の引き算や足し算なんかも行うことができます。

この文字同士の引き算や足し算によって英字の大文字小文字変換も簡単に行うことが可能です。

この辺りの話は下記ページで解説していますので、興味のある方はぜひ読んでみてください。

C言語での大文字 ⇔ 小文字変換の方法を解説

スポンサーリンク

strlen

ここからは文字列を扱っていきたと思います!まずは strlen から自作してみましょう!

関数の仕様

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

関数の宣言

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

my_strlen
size_t my_strlen(const char *str);

関数の引数

第1引数の str は文字列の先頭アドレス

関数の動作

str の指す文字列の文字数を求める

関数の戻り値

str の指す文字列の文字数

回答のテンプレート

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

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

size_t my_strlen(const char *);

size_t my_strlen(const char *str){
    /* 自作してください */
}

int main(void) {
    char str[256];
    size_t ret;

    /* 前準備 */
    strcpy(str, "Hello! Good bye!");

    /* 自作した関数を実行 */
    ret = my_strlen(str);

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

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

    return 0;
}

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク

ヒント

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

文字列の最後は必ず '\0' となります。

ですので、引数 str の指す文字から '\0' の直前までの文字数を数えることで、str の指す文字列の文字数を求めることができます。

また、size_t はサイズを表す整数を扱うときに良く利用する型ですが、とりあえず unsigned int 同様の型と考えて実装して良いです(符号なしの整数型)。

解答例

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

my_strlenの実装例
size_t my_strlen(const char *str){

    size_t i;

    /* '\0' になるまでカウント */
    i = 0;
    while (str[i] != '\0') {
        i++;
    }

    return i;
}

スポンサーリンク

解説

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

文字列の最後が '\0' である事を考慮すれば、おそらくそこまで苦労せずに実装できたのではないかと思います。

要は、下記のように文字列の先頭 str[0] から1文字ずつ、文字が '\0' になる直前までカウントアップしていけば良いだけです。

文字数のカウントアップ
i = 0;
while (str[i] != '\0') {
    i++;
}

ここで感じ取っていただきたいのは、このヌル文字 '\0' の大切さです。おそらくあなたが自作した関数もそうであるように、C言語の str 系の関数は全て、文字列の最後が '\0' である事を期待して実装されています(strn 系はまた別だけど)。

ですので、文字列の最後が '\0' でない場合、str 系の関数は上手く動作することができません。例えば上記の解答例で考えてみると、'\0' が現れない限り無限ループになってしまいますよね。

str 系の関数を実行する際は、今回自作した関数のように '\0' がないと上手く動作しないことを考慮し、引数に与える文字列の最後には '\0' を付けるようにしましょう!

スポンサーリンク

strcpy

続いても文字列関連です。関数 strcpy を自作してみましょう!strlen よりもちょっと難しいです。

関数の仕様

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

関数の宣言

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

my_strcpy
char *my_strcpy(char *dst, const char *src);

関数の引数

第1引数の dst は文字列コピー先のバッファの先頭アドレス

第2引数の src はコピー元の文字列の先頭アドレス

関数の動作

src の指す文字列を dst にコピーする

関数の戻り値

コピー先の文字列の先頭アドレス

回答のテンプレート

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

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

char *my_strcpy(char *, const char *);

char *my_strcpy(char *dst, const char *src){
    /* 自作してください */
}

int main(void) {
    char src[256];
    char dst[256];
    char *ret;

    /* 前準備 */
    strcpy(src, "Hello! Good bye!");
    memset(dst, 0xFF, sizeof(dst));

    /* 自作した関数を実行 */
    ret = my_strcpy(dst, src);

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

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

    /* 実行結果の確認 */
    if (strcmp(dst, "Hello! Good bye!") != 0) {
        /* 多分文字列の最後に必要なアレが足りていない */
        printf("Error3!!!\n");
        return -1;
    }

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

    return 0;
}

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク

ヒント

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

strlen 同様に、ポイントは文字列の最後は必ず '\0' となる点です。

もちろん dst の指す文字列の最後も '\0' である必要があります。

解答例

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

my_strcpyの実装例
char *my_strcpy(char *dst, const char *src){

    int i;
    
    /* ヌル文字の直前まで1文字ずつコピー */
    i = 0;
    while (src[i] != '\0') {
        dst[i] = src[i];
        i++;
    }

    /* 最後にヌル文字を付加 */
    dst[i] = '\0';


    return dst;
}

スポンサーリンク

解説

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

基本的には、src[0] から順に、コピー元の文字が '\0' になるまで1文字ずつ文字をコピーしてやれば良いだけです。

文字列のコピー
i = 0;
while (src[i] != '\0') {
    dst[i] = src[i];
    i++;
}

ただし、上記のようなループでは '\0' の文字がコピーされません。このループのみで関数を終了してしまった場合、コピー先の文字列の最後に \0' が付加されないことになります。

この場合、プログラムを実行すると Error3!!! が表示されるようになっています(これ表示された方多いのではないでしょうか?)。

strcpy 関数などのように、文字列が出力となる str 系の関数では、その出力の文字列の最後に '\0' が付加されるようになっています。ですので、strcpy を自作する場合も、コピー先の文字列の最後に '\0' を付加する必要があります。

文字列の最後にヌル文字を付加
/* 最後にヌル文字を付加 */
dst[i] = '\0';

strcpy 関数を使用する上でのポイントも、コピー先の文字列の最後に '\0' が付加される点ですね!

関数内で '\0' が付加されるので、strcpy 関数実行後にわざわざ自身で '\0' を付加する必要はありません。

その一方で、コピー先の文字列の最後に '\0' が付加されるので、第1引数に渡すバッファ(文字の配列)のサイズは、その '\0' が付加される事を考慮して設定する必要があります。

スポンサーリンク

atoi

初級編のラストを飾るのは atoi 関数です。おそらく今までの関数に比較すると難易度はかなり高いです。

関数の仕様

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

関数の宣言

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

my_atoi
int my_atoi(const char *str);

関数の引数

第1引数の str は文字列の先頭アドレス

関数の動作

str の指す文字列を整数に変換する

関数の戻り値

変換後の整数

例えば文字列が "01234" である場合、1234 を返却する

その他、関数の戻り値の詳細は下記の通り

  • 空白文字の扱い
    • 文字列の先頭に空白文字がある場合は、その空白文字を無視して変換を行う
      • 空白文字とは isspace 関数に指定した時に戻り値が 0 以外の値となる文字のこと
      • 空白文字の数はいくつあっても良い
      • 例えば文字列が "        01234" である場合でも、1234 を返却する
  • 符号の扱い
    • 数字の直前の文字に限り、+ or - の符号を表す文字が格納されても良いとする
      • この場合、その符号を考慮して整数に変換する
      • 例えば "-1234" の場合は -1234 を、"+1234" の場合は 1234 を返却する
  • 上記の空白文字・符号の文字以外に数字以外の文字があった場合、その文字の手前(左側)までのみを整数に変換した結果を返却する
    • 例えば "12X34" の場合は 12 を、"X1234" の場合は 0 を、"12X3Y4Z" の場合は 12 を返却する

本来の atoi では、文字列の文字数が多すぎてオーバーフローした場合に、-1 を返却する場合もあるようですが、今回はそのオーバーフロー時の処理までは考慮しなくても良いものとさせてください

回答のテンプレート

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

atoiの回答のテンプレート
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h> /* isspace */

int my_atoi(const char *);

int my_atoi(const char *str) {
    /* 自作してください */
}

int main(void) {
    static char str1[256]; /* staticはスタックオーバーフロー対策 */
    static char str2[256];
    static char str3[256];
    static char str4[256];
    static char str5[256];
    static char str6[256];
    static char str7[256];
    int ret1, ret2, ret3, ret4, ret5, ret6, ret7;

    /* 前準備 */
    strcpy(str1, "1234");

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

    /* 実行結果の確認 */
    if (ret1 != 1234) {
        /* 変換結果がおかしい */
        printf("Error1!!!\n");
        return -1;
    }

    /* 前準備 */
    strcpy(str2, "");

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

    /* 実行結果の確認 */
    if (ret2 != 0) {
        /* 文字数0の時の動作がおかしい */
        printf("Error2!!!\n");
        return -1;
    }
    
    /* 前準備 */
    strcpy(str3, "00001234");

    /* 自作した関数を実行 */
    ret3 = my_atoi(str3);

    /* 実行結果の確認 */
    if (ret3 != 1234) {
        /* 先頭の0扱いがおかしい */
        printf("Error3!!!\n");
        return -1;
    }

    /* 前準備 */
    strcpy(str4, "    1234");

    /* 自作した関数を実行 */
    ret4 = my_atoi(str4);

    /* 実行結果の確認 */
    if (ret4 != 1234) {
        /* 先頭の空白文字の扱いがおかしい */
        printf("Error4!!!\n");
        return -1;
    }

    /* 前準備 */
    strcpy(str5, "    +1234");

    /* 自作した関数を実行 */
    ret5 = my_atoi(str5);

    /* 実行結果の確認 */
    if (ret5 != 1234) {
        /* プラス符号の扱いがおかしい */
        printf("Error5!!!\n");
        return -1;
    }

    /* 前準備 */
    strcpy(str6, "    -1234");

    /* 自作した関数を実行 */
    ret6 = my_atoi(str6);

    /* 実行結果の確認 */
    if (ret6 != -1234) {
        /* マイナス符号の扱いがおかしい */
        printf("Error6!!!\n");
        return -1;
    }

    /* 前準備 */
    strcpy(str7, "    12 34");

    /* 自作した関数を実行 */
    ret7 = my_atoi(str7);

    /* 実行結果の確認 */
    if (ret7 != 12) {
        /* 数字以外の文字の扱いがおかしい */
        printf("Error7!!!\n");
        return -1;
    }

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

    return 0;
}

スポンサーリンク

使用しても良い関数

isspace 関数は使用しても OK としたいと思います。空白文字であるかどうかを判定するために使用してください。

isspace 関数を使用することで、単なるスペースだけでなく、タブなども空白文字として判定する可能です。

isspace
int isspace(int c);

引数 c が表す文字が空白文字の場合、isspace 関数は 0 以外の値を返却します。逆に空白文字でない場合は 0 を返却します。

ただ、回答のテンプレートとしてはスペースのみを空白文字として判定しても Clear!!! と表示されるように作っていますので、スペースのみを空白文字と扱って実装しても問題ありません。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク

ヒント

この atoi の自作では結構やることが多いです。なので、段階的に処理をプログラミングしていく方が良いと思います。ということでヒントも処理に応じて4つのものを用意しました!困っている項目のヒントをクリックして表示して中身を確認してください。

数字が整数に変換できないときのヒント

Error1!!! が表示される方は、数字の整数変換ができていない可能性が高いです。

とりあえず数字以外の文字(空白や符号も含めて)は考慮せずに、数字だけの文字列を整数に変換することを考えてみましょう!

まず、文字列を一気に整数に変換することはできないので、文字列を整数に変換する場合、文字列の先頭から1文字ずつ、整数に変換していく必要があります。

ですので、例えば len を文字列長とすれば、下記のようなループの中で、1文字ずつ整数に変換することになります(文字列長 lenstrlen が自作できれば求められるはずです)。

数字を整数へ変換する際のループ
for (i = 0; i < len; i++) {
    /* 略 */
}

では、どうやって数字を整数に変換するかというと、これは数字から '0' を引くことで変換することが可能です。この原理は下記ページで解説していますので、詳しくは下記ページを参考にしていただければと思います。

数字と数値の変換方法の解説ページアイキャッチ 【C言語】数字⇔数値の変換方法

例えば文字列 str の第 i 文字、つまり str[i] を整数に変換したいのであれば、str[i] - '0' を計算すれば良いです。

ただし、この '0' を引くことで行えるのは、1文字分の整数への変換のみです。整数側の視点で考えると特定の桁1桁分の整数が求まるのみです。

ですので、文字列全体を整数に変換することを考えると、桁を考慮しながら上記の変換を行うことが必要になります。

例えば、"1234" という文字列を先頭の文字から整数に変換する場合、最初の2文字を整数に変換した結果は 12 になっている必要があります。さらに第 2 文字の '3' を変換した際には、最初の2文字を整数に変換した結果を考慮して、変換結果を 123 とする必要があります。

つまり、数字の文字列の第 i 文字までを整数に変換する際には、まず第 i - 1 文字までを整数に変換した結果を * 10 し(各桁を1桁分ずつ桁上がりさせる)、さらにその結果に対し、第 i 文字を整数に変換した結果を足し合わせる必要があることになります(第 i 文字の変換結果を最下位の桁の値とする)。

atoi実装時に、それまでの変換結果を桁上がりさせながら変換を実行する様子を示した図

つまり、前述のループの中で、下記にコメントで示した3つの処理を実行する必要があります。

数字の文字列を整数へ変換する
for (i = 0; i < len; i++) {
    /* これまでの変換結果を1桁分ずつ上げる */

    /* 数字を整数に変換 */

    /* その整数をこれまでの変換結果の最下位の桁の値とする */
}

先頭の空白文字を無視する方法がわからないときのヒント

数字を整数に変換できるようになると、Error1!!! 〜 Error3!!! は表示されなくなるはずです。

Error4!!! が表示される場合は、先頭の空白文字を無視する処理が実現できていないからだと思います。

なので、次は先頭の空白文字を無視するようにしていきましょう!

先頭の空白文字は無視して良いのですから、要は “先頭に存在する空白文字を飛ばして” 文字列の整数への変換を実施してやれば良いことになります。

つまり、下記のようなループで “文字列の整数への変換” を行なっている場合、

数字の文字列を整数へ変換する
for (i = 0; i < len; i++) {
    /* これまでの変換結果を1桁分ずつ上げる */

    /* 数字を整数に変換 */

    /* その整数をこれまでの変換結果の最下位の桁の値とする */
}

下記のようにループを開始する位置を “先頭に存在する空白文字の文字数” としてやれば、空白文字を無視して文字列の整数への変換を実現することができます。

数字の文字列を整数へ変換する
start = 先頭に存在する空白文字の文字数;
for (i = start; i < len; i++) {
    /* これまでの変換結果を1桁分ずつ上げる */

    /* 数字を整数に変換 */

    /* その整数をこれまでの変換結果の最下位の桁の値とする */
}

では、”先頭に存在する空白文字の文字数” はどうやって求めれば良いでしょうか?

まず、文字が空白文字であるかどうかは isspace 関数により判断することが可能です。例えば、str[i] の文字が空白文字であるかどうかは、isspace(str[i]) の戻り値が “0 以外” であるかどうかで判断することができます(0 以外の値の場合は str[i] は空白文字)。

ですので、文字列の先頭の文字から順に、isspace 関数の戻り値が “0 以外” になる文字を数えていくことで、”先頭に存在する空白文字の文字数” を求めることができます。

空白文字を飛ばした位置から文字列の整数への変換処理を開始する様子を示した図

ただ、あくまでも数える必要があるのは “先頭に存在する” 空白文字のみです。先頭以外にある空白文字を数えないように注意しましょう。

例えば "    12  34" のように 1234 の間に存在する空白文字まで数えてしまうと、変換処理を開始する位置が狂うので当然変換結果の整数もおかしくなってしまいます。

符号の扱い方がわからないときのヒント

Error5!!! or Error6!!! のエラーが出る場合、符号の扱いが上手くいっていない or 文字列の先頭の空白文字を無視する処理がうまく実装できていない可能性が高いです。

後者の場合は先頭の空白文字を無視する方法がわからないときのヒントで説明していますので、ここでは前者の符号の扱いが上手くいっていない場合のヒントを出したいと思います。

まずポイントになるのが、符号は数字の直前にしか存在しないという点です。つまり、符号が存在する可能性があるのは、”先頭に存在する空白文字” の次の文字の位置のみになります(空白文字は複数存在する可能性もありますし、逆に0文字の場合もあります)。

つまり、”先頭に存在する空白文字の文字数” を start とすれば、符号が存在する可能性があるのは str[start] の位置になります(この start の求め方は、先頭の空白文字を無視する方法がわからないときのヒントを参考にしてください)。

符号が存在する可能性のある位置を示した図

ですので、まず文字列 str に符号があるかどうかを調べるためには、str[start] が符号であるかどうか、つまり '+' もしくは '-' であるかどうかを確認するようにすると良いと思います。

もし str[start] が符号であるのであれば、整数への変換処理は文字列の第 start + 1 文字から開始する必要があります。つまり変換処理の開始位置を符号の文字分後ろにずらす必要があります。

符号が存在する場合の文字列の整数への変換処理の開始位置を示した図

また、str[start] が符号でないのであれば、整数への変換処理は文字列の第 start 文字から開始する必要があります。

符号が存在しない場合の文字列の整数への変換処理の開始位置を示した図

さらに、str[start] が '-' の場合に限り、文字列を整数へ変換した後、その変換結果に対して -1 を掛けて、負の値に変換してから値を返却する必要があります。

数字以外の文字の扱い方がわからないときのヒント

Error7!!! が表示される場合、文字列 str の中に数字以外の文字が現れた時の変換が上手くできていないはずです。

atoi 関数では文字列 str の中に数字以外の文字が現れた場合、そこまでの文字列を整数に変換した結果を返却するようになっています。

数字以外の文字が現れた時の動作を示した図

要は、その時点で変換は打ち切って即座に結果を返却することになります。

ですので、例えば下記のようなループの中で文字列を整数に変換しようとしている場合、

数字以外の文字は変換しない
for (i = start; i < len; i++) {
    /* 整数への変換処理 */
}

下記のように文字が数字であるかどうかを判断し、数字でない場合は break して即座にループを終了するようにすれば良いです。

数字以外の文字は変換しない
for (i = start; i < len; i++) {
    if (str[i]が数字である) {
        /* 整数への変換処理 */
    } else {
        break;
    }
}

もし break をご存知ない方は、下記ページで解説していますので読んでみてください。

breakとcontinueの解説ページアイキャッチ 【C言語】breakとcontinueについて解説(ループを抜け出す・ループをスキップする)

文字が数字であるかどうかは、isupper 時に説明した時の判断と同様にして行うことが可能です。isupper の時は、文字が 'A''Z' の間のものであるかで判断しましたが、今回は '0''9' のものであるかで判断することになります。

解答例

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

my_atoiの実装例
int my_atoi(const char *str) {

    int i;
    int ret;
    int len; /* strの文字列長 */
    int sign; /* 符号 */
    int start; /* 数字が始まる位置 */
    int num; /* 数字を整数に変換した結果 */

    /* まずはstrの文字数を求める */
    i = 0;
    while (str[i] != '\0') {
        i++;
    }
    len = i;

    /* 返却する値を0に初期化 */
    ret = 0;

    /* 数字が始まる位置を0に初期化 */
    start = 0;

    /* 空白文字を数える */
    for (i = 0; i < len; i++) {
        if (isspace(str[i]) != 0) {
            start++;
        } else {
            break;
        }
    }

    /* 空白以外の文字がない場合は0(ret)を返却 */
    if (start >= len) {
        return ret;
    }

    /* 空白以外の文字の先頭が符号であるかを確認 */
    if (str[start] == '+') {
        sign = 1; /* 符号を覚えておく */
        start = start + 1; /* 数字はstr[start+1]から始まる */
    } else if (str[start] == '-') {
        sign = -1; /* 符号を覚えておく */
        start = start + 1; /* 数字はstr[start+1]から始まる */
    } else {
        sign = 1; /* 符号は正と同様に扱う */
        /* 数字はstr[start]から始まるのでstartの更新は不要 */
    }

    /* 空白文字と符号を除いた文字を数値に変換していく */
    for (i = start; i < len; i++) {
        if (str[i] >= '0' && str[i] <= '9') {
            /* 文字が数字の場合 */

            /* 今までの変換結果を1桁分上げる */
            ret = ret * 10;

            /* 数字を整数に変換 */
            num = str[i] - '0';

            /* 変換した整数を1番下の桁とする */
            ret = ret + num;
        } else {
            /* 文字が数字でない場合は変換終了 */

            break;
        }
    }

    /* 最後に符号を掛けて返却 */
    return sign * ret;
}

スポンサーリンク

解説

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

今回はほぼヒントで解説しているので、どのヒントがどの処理に対応しているのかを中心に解説しておきます(ヒントの順と順番前後するので注意してください)。

まず my_atoi 関数では、最初に引数 str の文字列長を求めています。このやり方は strlen で解説していますので、ここでの説明は省略します。

次に行なっているのが、下記の、文字列の先頭に存在する空白文字の文字数を数える処理です。これは、先頭の空白文字を無視する方法がわからないときのヒントで説明した処理になります。

先頭の空白文字の文字数を数える
/* 数字が始まる位置を0に初期化 */
start = 0;

/* 空白文字を数える */
for (i = 0; i < len; i++) {
    if (isspace(str[i]) != 0) {
        start++;
    } else {
        break;
    }
}

↑ で求まった start が、先頭に存在する空白文字の文字数になります。

続いて、空白文字の次の文字、つまり str[start] が符号であるかどうかの判断と、符号に応じた各種パラメータ startsign の設定を行なっています。これは符号の扱い方がわからないときのヒントで解説した内容の処理で、下記で行なっています。

符号に応じた設定
/* 空白以外の文字の先頭が符号であるかを確認 */
if (str[start] == '+') {
    sign = 1; /* 符号を覚えておく */
    start = start + 1; /* 数字はstr[start+1]から始まる */
} else if (str[start] == '-') {
    sign = -1; /* 符号を覚えておく */
    start = start + 1; /* 数字はstr[start+1]から始まる */
} else {
    sign = 1; /* 符号は正と同様に扱う */
    /* 数字はstr[start]から始まるのでstartの更新は不要 */
}

ここで設定した start が、”文字列の整数への変換処理” の開始位置になります。

str[start]'+' or '-' の場合、”文字列の整数への変換処理” は、str[start + 1] から行う必要があるため、start の値を +1 するようにしています(つまり先頭の空白文字と符号を飛ばした位置から変換処理を行う)。

また、str[start]'+' でも '-' でもない場合は、”文字列の整数への変換処理” は、str[start] から行う必要があるため、start の値は変更しません(つまり先頭の空白文字のみを飛ばした位置から変換処理を行う)。

また、str[start]'-' の場合のみ sign = -1(それ以外は sign = 1)としているので、文字列の整数への変換結果にこの sign を掛けることで、符号を考慮した結果を返却できるようになります(この sign を掛ける処理は関数の一番最後に行なっています)。

以上により、”文字列の整数への変換処理” の開始位置が start に設定されることになるので、あとはその位置から実際の変換処理を実施します。この処理については数字が整数に変換できないときのヒント数字以外の文字の扱い方がわからないときのヒントで解説しており、これを行っているのは下記になります。

文字列の整数への変換処理
for (i = start; i < len; i++) {
    if (str[i] >= '0' && str[i] <= '9') {
        /* 文字が数字の場合 */

        /* 今までの変換結果を1桁分上げる */
        ret = ret * 10;

        /* 数字を整数に変換 */
        num = str[i] - '0';

        /* 変換した整数を1番下の桁とする */
        ret = ret + num;
    } else {
        /* 文字が数字でない場合は変換終了 */

        break;
    }
}

特に、if が成立した時の処理が数字が整数に変換できないときのヒントで解説した内容の処理になります。数字から '0' を引くことで整数に変換する処理と、それまでにすでに変換した結果 ret10 倍してから足し合わせるところがポイントです。また、ループ開始前に ret0 にしておく必要がある点に注意が必要です。

数字の文字から整数への変換は、数字の文字から '0' を引くことで実現できることを覚えておくと、今後役に立つと思います。

また、上記の処理において、if 文の判断文と else 時の処理が数字以外の文字の扱い方がわからないときのヒントで解説した内容の処理になります。

“数字以外の文字” が現れた際には break によりループが終了しますので、その文字より前側(左側)の数字のみを整数に変換した結果が得られることになります。

最後に、整数に変換した結果に対して sign  を掛けて返却すれば、符号を考慮した値を呼び出し元に返却することができます(符号が '-' の場合のみ -1 が掛けられる)。

符号を考慮した値の返却
return sign * ret;

以上が、自作関数 my_atoiの解説になります。

おそらくここまで自作してきた関数の中では一番苦労したのではないかと思います。

単に数字のみの文字列を整数に変換できればクリアにしても良かったのですが、atoi 関数の動作にも詳しくなっていただく&細かい制御にも慣れていただくために、符号の扱いや数字以外の文字の扱いにも挑戦していただきました。

ここまで実装できれば相当C言語プログラミングの力はあると思います。特に文字や文字列の扱いに関してはバッチリだと思います!

まとめ

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

動作チェックなどを自動で行えるようにしておいたので、割と手軽にパズル・クイズ感覚で標準関数の自作に取り組めたのではないかと思います。

今回は文字を扱う標準関数が多くなりましたが、文字を整数として扱えることや、ヌル文字が文字列の最後には必ず必要であることを、標準関数の自作を通じて実感していただけたのではないかと思います。

さらに、下記ページでは上級編を用意しておりますので、もっと難しい標準関数の自作に挑戦してみたい方は是非下記リンクをクリックしてみてください!

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/

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