Posted at

C言語でPythonのappendを使いたかったので作りました


はじめまして

みなさん、はじめまして。

当記事のうp主「takuto-t」と申します。

今までの業務経験や、独学で勉強した知識もたまってきましたので、インプットばかりではなく、アウトプットにも力を入れようと思い、アカウントを作成しました。


アウトプット(アクティブラーニング)をすると知識が定着し忘れにくくなると、あるメンタリストDさんが書籍を出しておりました。

その本を読んで、私もできるエンジニアになってチヤホヤされたい、という下心があるのは言わないお約束☆


今までで C言語Linux の開発経験が多いため、それらをメインに記事を書いていきたいと思います。


概要

C言語で文字列操作を行う機会が頻繁にあり、Pythonの append メソッドを「C言語でも使用できたらいいな」と思い、当記事を書きました。

完全にPythonの append メソッドと同じというわけにはいきませんが、ほぼ同じように使用できる append 関数をC言語で作ってみました。


GitHub

当記事のソースコードは、以下のURLからクローンすることができます。

C言語によるappendもどきのソースコード


appendとは

Pythonにおける append とは、作成済みのリストの末尾に新しい要素を追加するメソッドです。

以下は、 append メソッドのフォーマットです。

リスト.append(新しい要素)

例えば、リスト list の末尾に新しく要素を追加するには、以下のようにします。

>>> list = [1, 2, 3, 4, 5]

>>> list
[1, 2, 3, 4, 5]
>>> list.append(6) # 「6」を末尾に追加する
>>> list
[1, 2, 3, 4, 5, 6] # 「6」が末尾に追加された
>>>

C言語でもPythonの append メソッドと同じように、配列の末尾に新しい要素を追加できる関数を実装してみます。

最終的には、以下のフォーマットで配列の末尾に新しい要素を追加できる関数にしていきます。

append(動的配列, 新しい要素)   /// 「動的」という所がミソです(後述)


設計方針

残念ながら、C言語では 既存の配列 の末尾に新要素を追加してくれる機能はありません。

そこで下図のように、 新しい動的配列 を用意し、そこに元データをコピーしてから新しい要素を追加するようにします。もちろん、コピー元の動的配列は メモリ解放します!!

あと、終端文字は ¥0 にします。¥0 であれば、文字列にも対応できますしね。

(Pythonだと、終端をどのように扱っているか分からなかったから、ポピュラーな ¥0 にしたというのもあります)

append_0.png


append関数の処理フロー


  1. コピー元の動的配列(既存配列)よりサイズが 1+終端文字 分大きい 動的配列を用意します (*1)。
    終端文字は 配列長の大きさには含まないものとして設計 しています(その方が便利だったりします)。

    (*1) サイズを取得する関数も実装します!!(後述する len 関数です!!)

append_1.png


  1. コピー元のデータを、コピー先の動的配列(新しく用意した配列)にコピーします。

append_2.png


  1. 新しい要素を、コピー先の動的配列の コピー元の動的配列のサイズ+1 のところに代入します。

append_3.png


  1. コピー先の動的配列の末尾に終端文字を代入します。

append_4.png


  1. コピー元の動的配列は、 そのまま残すとメモリを無駄に消費する ことになるので、解放します。

append_5.png


  1. コピー先の動的配列の先頭アドレスをリターンして、 append関数の処理は完了 になります。

append_6.png


append関数の書式


append関数の書式

#define append(src, appItem)    src = (typeof(src))_append(src, sizeof(src[0]), appItem)

void *_append(void *src, size_t unitSize, void *appItem)

毎回引数に配列のサイズを指定するのもめんどくさいので、マクロ関数で自動的に配列サイズを渡すようにしています。


引数

引数名

説明

src
voidポインタ:void*
新しい要素を追加したい動的配列

unitSize
サイズ型:size_t
動的配列 src の1つの要素サイズ(*1)

appItem
voidポインタ:void*
新しく追加する要素

(*1) void型でなければ 、マクロ関数 append(src, appItem) が自動的に引数を指定してくれます。


戻り値

新しく要素を追加した配列の先頭アドレスを返します。


len関数の処理フロー

「配列のサイズを取得する」といったら strlen 関数を思い浮かべる人が多いのではないでしょうか?(完全に主観です ごめんなさい)

strlen 関数ですと char 型の配列しか対応できないため、 int 型配列や構造体配列などに対応できなくなってしまいます。そこで、 void型を除くあらゆる型 の動的配列のサイズを取得できる関数が必要になってきます。

Pythonの append メソッドをC言語で再現する記事ですが、 len 関数も必要になりますので、その関数の設計方針も説明していきます。


len関数の書式


len関数の書式

#define len(src)    _len(src, sizeof(src[0]))

unsigned int _len(const void *src, size_t unitSize)

これも append 関数と同様に、毎回引数に配列のサイズを指定するのもめんどくさいので、マクロ関数で自動的に配列サイズを渡すようにしています。


引数

引数名

説明

src
voidポインタ:void*
サイズを取得したい動的配列

unitSize
サイズ型:size_t
動的配列 src の1つの要素サイズ(*1)

(*1) void型でなければ 、マクロ関数 len(src) が自動的に引数を指定してくれます。


戻り値

動的配列 src の終端文字 \0 までの要素数。

※終端文字は要素数には 含めません


ソースコード


len関数

#include <stdio.h>

#include <memory.h>
#include <stdlib.h>

#define len(src) _len(src, sizeof(src[0]))
unsigned int _len(const void *src, size_t unitSize)
{
unsigned int length;

/// ① 終端文字までカウント
for (length = 0; *((const char*)src + (length * unitSize)) != '\0'; length++);

return length;
}


append関数

#include <stdio.h>

#include <memory.h>
#include <stdlib.h>

#define APPEND_POS 1 /// 新要素追加分の要素サイズ
#define NULL_TERM_POS 1 /// ヌルターミネート分の要素サイズ

#define append(src, appItem) _append(src, sizeof(src[0]), appItem)
void *_append(void *src, size_t unitSize, void *appItem)
{
/// ① srcのサイズを取得
unsigned int size = _len(src, unitSize);

/// ② 配列に何も格納されていない場合
if (size == 0)
{
memcpy(src, appItem, unitSize);
}

/// ③ 新しい配列空間を獲得
void *dst = malloc(unitSize * (size + APPEND_POS + NULL_TERM_POS));

/// ④ 獲得した配列空間にコピー元のデータをコピー
memcpy(dst, src, unitSize * (size + 1));

/// ⑤ appItemを代入
memcpy((char*)dst + (unitSize * size), appItem, unitSize);

/// ⑥ 終端文字 ('\0') を代入
*((char*)dst + (unitSize * (size + NULL_TERM_POS))) = '\0';

/// ⑦ コピー元を解放
free(src);

return dst;
}


動作確認

ここで紹介するソースコードとMakefileは GitHubにプッシュしてあるものと同じ です。

ぜひクローンして、動かしてみて下さいね。


動作確認用ソースコード

#include <stdio.h>

#include <memory.h>
#include <stdlib.h>

#define APPEND_POS 1 /// 新要素追加分の要素サイズ
#define NULL_TERM_POS 1 /// ヌルターミネート分の要素サイズ

#define len(src) _len(src, sizeof(src[0]))
unsigned int _len(const void *src, size_t unitSize)
{
/// 略
}

#define append(src, appItem) src = (typeof(src))_append(src, sizeof(src[0]), appItem)
void *_append(void *src, size_t unitSize, void *appItem)
{
/// 略
}

int main(void)
{
printf("***** ***** ***** ***** ***** ***** *****\n");
printf("** int型の整数を追加してみます!! **\n");
printf("***** ***** ***** ***** ***** ***** *****\n");
{
typedef int TYPE_1;
TYPE_1 *list_1 = (TYPE_1*)malloc(1);;
TYPE_1 appItem_1;
unsigned int i = 0;

memset(list_1, '\0', sizeof(list_1));

appItem_1 = 0x01;
append(list_1, &appItem_1);
for (i = 0; i < len(list_1); i++)
{
printf("0x%X ", *(list_1 + i));
}
printf("\n----- ----- -----\n");

appItem_1 = 0x02;
append(list_1, &appItem_1);
for (i = 0; i < len(list_1); i++)
{
printf("0x%X ", *(list_1 + i));
}
printf("\n----- ----- -----\n");

appItem_1 = 0x04;
append(list_1, &appItem_1);
for (i = 0; i < len(list_1); i++)
{
printf("0x%X ", *(list_1 + i));
}
printf("\n----- ----- -----\n");

appItem_1 = 0x08;
append(list_1, &appItem_1);
for (i = 0; i < len(list_1); i++)
{
printf("0x%X ", *(list_1 + i));
}
printf("\n----- ----- -----\n");

free(list_1);
}

printf("\n\n");
printf("***** ***** ***** ***** ***** ***** *****\n");
printf("** 文字を追加してみます!! **\n");
printf("***** ***** ***** ***** ***** ***** *****\n");
{
typedef char TYPE_2;
TYPE_2 *list_2 = (TYPE_2*)malloc(1);;
TYPE_2 appItem_2;
unsigned int i = 0;

memset(list_2, '\0', sizeof(list_2));

appItem_2 = 'a';
append(list_2, &appItem_2);
for (i = 0; i < len(list_2); i++)
{
printf("%c ", *(list_2 + i));
}
printf("\n----- ----- -----\n");

appItem_2 = 'b';
append(list_2, &appItem_2);
for (i = 0; i < len(list_2); i++)
{
printf("%c ", *(list_2 + i));
}
printf("\n----- ----- -----\n");

appItem_2 = 'c';
append(list_2, &appItem_2);
for (i = 0; i < len(list_2); i++)
{
printf("%c ", *(list_2 + i));
}
printf("\n----- ----- -----\n");

free(list_2);
}

printf("\n\n");
printf("***** ***** ***** ***** ***** ***** *****\n");
printf("** 文字列を追加してみます!! **\n");
printf("***** ***** ***** ***** ***** ***** *****\n");
{
typedef char* TYPE_3;
TYPE_3 *list_3 = (TYPE_3*)malloc(1);;
TYPE_3 appItem_3;
unsigned int i = 0;

memset(list_3, '\0', sizeof(list_3));

appItem_3 = "First";
append(list_3, &appItem_3);
for (i = 0; i < len(list_3); i++)
{
printf("%s ", *(list_3 + i));
}
printf("\n----- ----- -----\n");

appItem_3 = "Second";
append(list_3, &appItem_3);
for (i = 0; i < len(list_3); i++)
{
printf("%s ", *(list_3 + i));
}
printf("\n----- ----- -----\n");

appItem_3 = "Third";
append(list_3, &appItem_3);
for (i = 0; i < len(list_3); i++)
{
printf("%s ", *(list_3 + i));
}
printf("\n----- ----- -----\n");

free(list_3);
}

retrun 0;
}


ちょっと補足

私が自作した append 関数には、弱点が1つあります。

それは、 append 関数の第二引数に 直接新しい要素を指定することができない ことです。

appItem = 4;            /// 一度変数に入れてから

append(list, &appItem); /// 第二引数を指定しないとダメ

append 関数の汎用性を求めるあまりに、第二引数を void* 型にしてしまったことが原因です。

以下のように、直接新しい要素を追加しようとすると コンパイルエラー になってしまいます。

append(list, 4.); /// コンパイルエラー

そのため、一度変数に新しい要素を追加してからappend 関数の第二引数を指定するという2度手間が発生しております。

この解決方法は現在分かっておりません(ごめんなさい)。


コンパイル(Makefile)

動作確認用ソースコードをコンパイルします。

以下のgccコマンドを実行すれば、実行ファイル append を生成できます。

$ gcc -g append.c -o append

コンパイルオプション -g は、付けなくても問題ないです。主が癖でよく付けるだけです(GDBをよく使うので...)。

Makefileでコンパイルする場合は、以下のMakefileを作成して実行します。

# -------------------------

# コンパイラ
# -------------------------
CC = gcc

# -------------------------
# 各コンパイルオプション
# -------------------------
OPTS += -g # -gオプションは付けなくても大丈夫です

# -------------------------
# ソースファイル
# -------------------------
SRC_FILES += append.c

# -------------------------
# ターゲット(実行)ファイル
# -------------------------
.PHONY: append
TARGET = append

# -------------------------
# コンパイルオプション統合
# -------------------------
CFLAG += $(OPTS)

# -------------------------
# コンパイル実行
# -------------------------
.PHONY: all
all: $(TARGET)

$(TARGET):
$(CC) $(CFLAG) $(SRC_FILES) -o $(TARGET)

# -------------------------
# clean
# -------------------------
.PHONY: clean
clean:
$(RM) $(TARGET)


動作結果

$ ./append

***** ***** ***** ***** ***** ***** *****
** int型の整数を追加してみます!! **
***** ***** ***** ***** ***** ***** *****
0x1
----- ----- -----
0x1 0x2
----- ----- -----
0x1 0x2 0x4
----- ----- -----
0x1 0x2 0x4 0x8
----- ----- -----

***** ***** ***** ***** ***** ***** *****
** 文字を追加してみます!! **
***** ***** ***** ***** ***** ***** *****
a
----- ----- -----
a b
----- ----- -----
a b c
----- ----- -----

***** ***** ***** ***** ***** ***** *****
** 文字列を追加してみます!! **
***** ***** ***** ***** ***** ***** *****
First
----- ----- -----
First Second
----- ----- -----
First Second Third
----- ----- -----


次回

append メソッドで追加した要素を、 pop メソッドで削除したくなる時もあります(ありますよね? 迫真)。

そんなことで、次回はC言語で pop 関数を実装していきます ☆彡


終わりに

「もっとこうしてほしい☆彡」とか「こういった記事も書いてほしい♥」とかございましたら、コメント頂ければ幸いです!!