Open4

調べもの:C 言語

nukopynukopy

雑に AI に聞きまくる。

Q. C 言語の慣習を知りたいんだけど、定数と構造体の定義はヘッダーファイルに書くのが一般的なの?

net.h
#ifndef NET_H
#define NET_H

#include <stddef.h>
#include <stdint.h>

#ifndef IFNAMSIZ
#define IFNAMSIZ 16
#endif

#define NET_DEVICE_TYPE_DUMMY 0x0000
#define NET_DEVICE_TYPE_LOOPBACK 0x0001
#define NET_DEVICE_TYPE_ETHERNET 0x0002

#define NET_DEVICE_FLAG_UP 0x0001
#define NET_DEVICE_FLAG_LOOPBACK 0x0010
#define NET_DEVICE_FLAG_BROADCAST 0x0020
#define NET_DEVICE_FLAG_P2P 0x0040
#define NET_DEVICE_FLAG_NEED_ARP 0x0100

#define NET_DEVICE_ADDR_LEN 16

#define NET_DEVICE_IS_UP(x) ((x)->flags & NET_DEVICE_FLAG_UP)
#define NET_DEVICE_STATE(x) (NET_DEVICE_IS_UP(x) ? "up" : "down")

struct net_device {
  struct net_device *next;
  unsigned int index;
  char name[IFNAMSIZ];
  uint16_t type;
  uint16_t mtu;
  uint16_t flags;
  uint16_t hlen; /* header length */
  uint16_t alen; /* address length */
  uint8_t addr[NET_DEVICE_ADDR_LEN];
  union {
    uint8_t peer[NET_DEVICE_ADDR_LEN];
    uint8_t broadcast[NET_DEVICE_ADDR_LEN];
  };
  struct net_device_ops *ops;
  void *priv;
};

struct net_device_ops {
  int (*open)(struct net_device *dev);
  int (*close)(struct net_device *dev);
    int (*transmit)(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst
;
};

extern struct net_device *net_device_alloc(void);
extern int net_device_register(struct net_device *dev);
extern int net_device_output(struct net_device *dev, uint16_t type,
                             const uint8_t *data, size_t len, const void *dst);

extern int net_input_handler(uint16_t type, const uint8_t *data, size_t len,
                             struct net_device *dev);

extern int net_run(void);
extern void net_shutdown(void);
extern int net_init(void);

#endif
net.c
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "platform.h"

#include "net.h"
#include "util.h"

/* NOTE: if you want to add/delete the entries after net_run(), you need to
 * protect these lists with a mutex. */
static struct net_device *devices;

struct net_device *net_device_alloc(void) {}

/* NOTE: must not be call after net_run() */
int net_device_register(struct net_device *dev) {}

static int net_device_open(struct net_device *dev) {}

static int net_device_close(struct net_device *dev) {}

int net_device_output(struct net_device *dev, uint16_t type,
                      const uint8_t *data, size_t len, const void *dst) {}

int net_input_handler(uint16_t type, const uint8_t *data, size_t len,
                      struct net_device *dev) {}

int net_run(void) {}

void net_shutdown(void) {}

int net_init(void) {}
nukopynukopy

なぜこの分離が重要なのか?

  • 再利用性:複数の .c ファイルから同じ構造体や定数を使える
  • コンパイル効率:ヘッダーファイルの変更時のみ関連ファイルを再コンパイル
  • インターフェース明確化:公開するものと内部実装を分離
  • 重複定義の回避:#ifndef ガードで重複インクルードを防止

例外的なケース

  • static const定数:ファイル内でのみ使用する場合は .c に書く
  • 巨大な配列:初期化データは .c に書くことが多い
  • インライン関数:小さな関数は static inline でヘッダーに書くことも

この慣習により、インターフェース(.h)と実装(.c)を明確に分離し、保守性の高いコードが書ける。

nukopynukopy

Q. extern とは

nukopynukopy

externの基本的な意味

externは**「この変数や関数は他の場所で定義されている」**ことをコンパイラに伝えるキーワードです。

実際の例で確認

net.hでの使用例

// 51-54行目
extern struct net_device *net_device_alloc(void);
extern int net_device_register(struct net_device *dev);
extern int net_device_output(struct net_device *dev, uint16_t type,
                             const uint8_t *data, size_t len, const void *dst);

これは:

  • 宣言(declaration): 「こういう関数があります」
  • 定義(definition): 実際の実装はnet.cにある

net.cでの実際の定義

// 14行目
struct net_device *net_device_alloc(void) {}

// 17行目  
int net_device_register(struct net_device *dev) {}

externの役割

1. 関数の場合

// ヘッダーファイル(.h)
extern int my_function(int x);  // 宣言

// ソースファイル(.c)
int my_function(int x) {        // 定義
    return x * 2;
}

2. 変数の場合

// file1.c
int global_counter = 0;         // 定義

// file2.c
extern int global_counter;      // 宣言(他のファイルで定義済み)

void increment() {
    global_counter++;           // 使用可能
}

externがない場合との違い

externなし(ヘッダーファイルで)

// header.h
int my_function(int x);         // これでも動くが...

externあり(推奨)

// header.h  
extern int my_function(int x);  // 明示的に外部定義を示す

なぜexternを使うのか?

1. 明示性

「この関数/変数は外部で定義されている」ことが一目で分かる

2. リンク時の問題回避

複数のファイルで同じヘッダーをインクルードしても重複定義エラーが起きない

3. C言語の慣習

特に関数プロトタイプでは extern を明示的に書くのが一般的

実際のリンク過程

# コンパイル時
gcc -c net.c -o net.o      # net_device_alloc の実装をオブジェクトファイルに
gcc -c main.c -o main.o    # net_device_alloc の呼び出しをオブジェクトファイルに

# リンク時  
gcc net.o main.o -o program # extern宣言と実際の定義を結び付ける

まとめ

externは**「宣言だけで、実装は別の場所にある」**ことを示すキーワードです。これにより:

  • ヘッダーファイルでインターフェースを公開
  • 実装は対応する .c ファイルに分離
  • 複数のファイルから同じ関数/変数を安全に使用

C言語のモジュール化の基本的な仕組みとなっている。