Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

C言語のグローバル変数とexternについて

f:id:fa11enprince:20200628230841j:plain
C言語では言語仕様上、グローバル変数は良く使うと思います。
できるだけ避けるのは言うまでもありませんが。
そこでよく混乱するのがexternではないでしょうか?
ヘッダなんかをインクルードすると
あれ?そういえばexternって……どうなんだっけ…ってことになります。

私なんかはなんかの参考書でプログラミングを学び始めたときに、
externつけてもつけなくても一緒というような怪しげな解説を見た記憶があり、
それによって、余計に混乱してしまいました。
ただし、関数に関しては一緒です。
ここより正確な記載のあるサイトを見つけましたのでURLを記載します。
C言語のexternキーワードについて(関数編) – cloudtofu
いまだに検索流入が多い(2019年8月時点)のでちょっとびっくりします。それだけC言語が息の長い言語であり、
年々使用者が減少しているのでしょうね。私自身もCはもう5年以上触っていません。
いまなら限られた環境でない限りはC++(もしくはGoかRustかもしれない)を使うでしょうね。

externは
他の場所に定義があって、宣言ですよ
って明示するためのものです。

宣言と定義について

厳密な説明ではないのですが、
C言語における宣言とは値や中身がかかれていないものです。
例えば、

int g_value;
extern int g_value2;
int foo(void);

は宣言です。
一方、定義は

int g_value = 0;
int foo(void) {
    return g_value;
}

などです。

グローバル変数を使うときどうすればいいか、
基本的にヘッダ側(.h)はextern付の宣言をして、.cファイルのどこかに
externなしの定義を書きます。その際に初期値を代入します。
これでほぼOKです。
もちろん、.h側にexternなしの変数宣言をしてはいけません。
ヘッダファイルにはいろいろお作法があるのですが、
きちんと書かれているものが少ないように思われます。
そういうわけで巷には間違って書かれている
ヘッダファイルがあふれているのではないでしょうか?

ただし、これでは定義がどこにあるのか、
しかも一つでなくてはならないので、管理が複雑になり、混乱します。
しかも仮定義という厄介な概念があり、もっと事情は複雑です。

こうしておけばよい

最初に最終版を書きます。
GLOBAL_VALUE_DEFINEDをmainのあるファイルにdefineし
マクロでextern有無しを制御します。
初期値を0以外に指定したいときはやはりマクロで制御します。

test.h

#ifndef TEST_H_INCLUDED_
#define TEST_H_INCLUDED_

#ifdef GLOBAL_VALUE_DEFINE
  #define GLOBAL
  #define GLOBAL_VAL(v) = (v)
#else
  #define GLOBAL extern
  #define GLOBAL_VAL(v)
#endif

GLOBAL int g_value;   // この場合は最初の定義で0で初期化
/* GLOBAL int g_value GLOBAL_VAL(1); */

void foo(void);

#endif /* TEST_H_INCLIDED_ */

test.c

#include "test.h"

void foo(void) {
    g_value++;
}

myapp.c

#define GLOBAL_VALUE_DEFINE
#include <stdio.h>
#include "test.h"

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

Makefile

あとコンパイルがやや面倒なのでMakefileを書きます。
Makefileの解説をすると長くなるので割愛します。

CC           = gcc
CFLAGS       = -Wall
DEBUGFLAGS   = -O0 -D _DEBUG -g
PROG         = myapp
SOURCES      = myapp.c test.c
OBJS         = $(SOURCES:.c=.o)
INCDIR       =
LIBDIR       =
LIB          =

.PHONY: all
all: $(SOURCES) $(PROG)

# Primary Target
$(PROG): $(OBJS)
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -o $@ $^ $(INCDIR) $(LIBDIR) $(LIB)

# Suffix Rule
.c.o:
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -c $< $(INCDIR) $(LIBDIR) $(LIB)

.PHONY: clean
clean:
        $(RM) $(OBJS) $(PROG)

コンパイル&リンク

$ make
gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

実行結果

$ ./myapp.exe
0
1
2

解説

結論はわかったとしてどうしてこうするのかというのを説明します。
話を単純化するために例えば次のような2つのファイルがあるとします。

test.c

extern int g_value;

void foo(void) {
    g_value++;
}

myapp.c

#include <stdio.h>

int g_value = 0;
/* extern */ void foo(void);

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

もちろんこれは次のようにコンパイルすればちゃんと問題なく動きます。

コンパイル

gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

補足ですが関数定義はexternがあってもなくても外部結合(=ファイルの外から見える)ので
あってもなくてもよいです。

ここで、test.c側のextern int g_value;
int g_value = 0;
に書き換えると、当然

test.o:test.c:(.bss+0x0): `g_value' が重複して定義されています

というようなエラーがでます。
つまり、.hファイルに単純にくくりだしてint g_value = 0;
として両者の.cでincludeすると同じことが起こります。
そのようなことを防ぐために最初のようにマクロで制御しています。

しかし、厄介なのがANSI Cの仮定義という概念で、
externをかかず、いろんなファイルでint g_value;とした場合は……

test.c

int g_value; /* 他に定義がないので int g_value = 0;の定義として扱う */
...

myapp.c

#include <stdio.h>

int g_value; /* 既にg_valueの定義があるので、extern int g_value;(宣言)として扱う */
...

これはなんとコンパイルが通ります。理由は上記コードのコメント部分です。

その他constのグローバル変数について

constにも同じことが言えます。
ただし、constでexternかどうか気にしたことがないぞ?っていう人もいるかもしれません。
……そもそも伝統的なCを使っている人は#defineを使っているかもしれませんが。
実はconstの場合、CとC++で微妙に振る舞いが違います。
externを使わない場合、
Cの場合は外部リンケージ
C++の場合は内部リンケージとなります。

参考リンク

Linuxを使いたいけど、vi(Vim)って難しいよね。できれば使いたくない!……ってひと向けnanoエディタ


Linuxって使うのにいろいろハードルがあるのですが、
その一つの大きな障壁がコマンドライン上でのテキスト編集作業ではないでしょうか?
デフォルトで用意されていてかつメジャーなのが
viというこれまた変な操作体系のエディタで
最初使ったらなんじゃこりゃ。入力できないし、編集できないし……。
終了の仕方がわからず、そっとターミナルを閉じた……。
なんて人は多いんじゃないでしょうか?僕です。
第一、viなんてよっぽどのモノ好きでないと使いこなせるようになりません。
というか必要に迫られないと覚えられないようなものです。
Linuxが使いたいだけなのに変な操作体系を覚えさせられる…。
おれはLinuxをやりたいんだ。
でもなぜかviの学習をしている……。ってなんだかおかしいですよね。
GUIが整っている環境なら、geditというエディタを使って編集という手もありますが、
なかなかそうもいかず、ターミナル(黒い画面)経由でアクセスしてなんてことも多いですよね。
また、VPSなんか使って最小構成にしていると、GUIすらないし……。
ああ、もう、FTPかSCPでファイル取ってきてWindows上で編集してまたあげるか……
それは過去の僕です。
やっぱりviか……となるんですが、ターミナル上でもメモ帳感覚で使えるエディタがあるのです!
それがnanoエディタ。CentOSならだいたい最初から入っていることが多いです。
もしない場合は下記コマンドでインストールできます。(ルートユーザでやってね!)

使ってみよう

# yum install nano

起動は次のようにします。ファイルネームを引数にしてあげます。

$ nano [filename]

今回はtestというファイル名でファイルを作ってみるので

$ nano test

でエンターダンッ!
CentOSのターミナル上でつかうとこうなります(Ubuntu/Macでも使えるよ!)。

日本語環境をちゃんと整え切っていないので英語になっていますが、
日本語にも対応できます。
基本的にメモ帳感覚で使えます。
保存がCtrl+Oなのがちょっと奇妙ですが……。
もちろんvi同様カスタマイズができて、homeに.nanorcを置いて
何か書いてやればカスタマイズも可能です。
ちなみにviな人が使って油断するとjjjjjkkkkkとか思わず入力してしまいます。
※私はVimmerです

関連記事

Javaで優先順位をつけて複数のキーでソートする方法



久々にPureなJavaを書きました(*´Д`)

複数のキーでソートする書き方の説明

いろいろ書き方はありますが、
対象のクラスにComparableをimplementsして
compareTo()をオーバーライドすると
ソート順を定義できます。
そのうえで、

-1(左が先)
 0(同じ)
 1(右が先)

の値を返してやればいいのですが、
複数ある時は、
第1条件の判定結果を変数に格納してやり、
それで0の時は第2条件を判定、
さらに0の時は第3条件を判定……
さらに0の時は第4条件を判定……
とすると、第1条件、第2条件、第3条件………の順でソートされます。

ちなみに並び順を変えたいときは定数なり、enumなりでcompareTo()の中を分岐させます。

サンプルコード

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
class Student implements Comparable<Student> {
    // 並び順にあまり意味はない…並び替えの名前もテキトー
    public static enum SortMethod {
        GRADE_CLAZZ_ASC,   /* 学年(ASC) > 学級(ASC) > 名前(ASC) > 班長(T>F) */
        GRADE_CLAZZ_DESC,  /* 学年(DSC) > 学級(DESC) > 名前(DESC) > 班長(F>T) */
    }
    @Getter
    @Setter
    private static SortMethod method = SortMethod.GRADE_CLAZZ_ASC;

    private int grade; /* 学年 */
    private String clazz; /* 学級 */
    private String name; /* 名前 */
    private boolean isLeader; /* 班長 */

    @Override
    public int compareTo(Student o) {
        int result = 0;
        if (method == SortMethod.GRADE_CLAZZ_ASC) {
            result = Integer.valueOf(this.grade).compareTo(Integer.valueOf(o.grade));
            if (result == 0) {
                result = this.clazz.compareTo(o.clazz);
            }
            if (result == 0) {
                result = this.name.compareTo(o.clazz);
            }
            if (result == 0) {
                result = Boolean.valueOf(this.isLeader).compareTo(Boolean.valueOf(o.isLeader));
            }
        } else if (method == SortMethod.GRADE_CLAZZ_DESC) {
            result = -Integer.valueOf(this.grade).compareTo(Integer.valueOf(o.grade));
            if (result == 0) {
                result = -this.clazz.compareTo(o.clazz);
            }
            if (result == 0) {
                result = -this.name.compareTo(o.clazz);
            }
            if (result == 0) {
                result = -Boolean.valueOf(this.isLeader).compareTo(Boolean.valueOf(o.isLeader));
            }
        }
        return result;
    }

    @Override
    public String toString() {
        return "学年 [" + this.grade + "] クラス [" + this.clazz + "] 名前 [ "
                + String.format("%1$-20s", this.name)  // 20文字右スペース埋め (こんな便利なメソッドあったんだ)
                + "] " + (isLeader ? "班長" : "");
    }
}

public class SortSample {
    public static void main(String[] args) {
        // テキトーにデータをぶち込む
        List<Student> students = new ArrayList<>(
                Arrays.asList(
                        new Student(6, "A", "Robin Givens", true),
                        new Student(6, "A", "Richard Burgi", false),
                        new Student(2, "A", "Sarah Walker", false),
                        new Student(2, "A", "Jon Casey", false),
                        new Student(2, "B", "Morgan Grimes", false),
                        new Student(1, "A", "Elenor Batorwski", false),
                        new Student(6, "B", "Agatha Christie", true),
                        new Student(5, "A", "Tony Todd", true),
                        new Student(5, "A", "Matthew Bomer", false),
                        new Student(4, "A", "Linda Hamilton", false),
                        new Student(3, "A", "Lauren Cohan", true),
                        new Student(2, "A", "Chales Bartowski", true),
                        new Student(5, "B", "Rachel Bilson", true),
                        new Student(4, "A", "Nicole Richie", false),
                        new Student(1, "B", "Devon Woodcomb", false)
                )
        );
        Student.setMethod(Student.SortMethod.GRADE_CLAZZ_ASC);
        Collections.sort(students);
        for (Student s : students) {
            System.out.println(s);
        }
        System.out.println("------------------------------------------------");
        // Stream APIでソート
        Student.setMethod(Student.SortMethod.GRADE_CLAZZ_DESC);
        students.stream().sorted((e1, e2) -> e1.compareTo(e2)).forEach(System.out::println);
        System.out.println("------------------------------------------------");
    }
}

Lombokを使っていますので、とりあえず、Mavenを書きます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SortSample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

実行結果

学年 [1] クラス [A] 名前 [ Elenor Batorwski    ] 
学年 [1] クラス [B] 名前 [ Devon Woodcomb      ] 
学年 [2] クラス [A] 名前 [ Sarah Walker        ] 
学年 [2] クラス [A] 名前 [ Jon Casey           ] 
学年 [2] クラス [A] 名前 [ Chales Bartowski    ] 班長
学年 [2] クラス [B] 名前 [ Morgan Grimes       ] 
学年 [3] クラス [A] 名前 [ Lauren Cohan        ] 班長
学年 [4] クラス [A] 名前 [ Linda Hamilton      ] 
学年 [4] クラス [A] 名前 [ Nicole Richie       ] 
学年 [5] クラス [A] 名前 [ Tony Todd           ] 班長
学年 [5] クラス [A] 名前 [ Matthew Bomer       ] 
学年 [5] クラス [B] 名前 [ Rachel Bilson       ] 班長
学年 [6] クラス [A] 名前 [ Robin Givens        ] 班長
学年 [6] クラス [A] 名前 [ Richard Burgi       ] 
学年 [6] クラス [B] 名前 [ Agatha Christie     ] 班長
------------------------------------------------
学年 [6] クラス [B] 名前 [ Agatha Christie     ] 班長
学年 [6] クラス [A] 名前 [ Richard Burgi       ] 
学年 [6] クラス [A] 名前 [ Robin Givens        ] 班長
学年 [5] クラス [B] 名前 [ Rachel Bilson       ] 班長
学年 [5] クラス [A] 名前 [ Matthew Bomer       ] 
学年 [5] クラス [A] 名前 [ Tony Todd           ] 班長
学年 [4] クラス [A] 名前 [ Nicole Richie       ] 
学年 [4] クラス [A] 名前 [ Linda Hamilton      ] 
学年 [3] クラス [A] 名前 [ Lauren Cohan        ] 班長
学年 [2] クラス [B] 名前 [ Morgan Grimes       ] 
学年 [2] クラス [A] 名前 [ Chales Bartowski    ] 班長
学年 [2] クラス [A] 名前 [ Jon Casey           ] 
学年 [2] クラス [A] 名前 [ Sarah Walker        ] 
学年 [1] クラス [B] 名前 [ Devon Woodcomb      ] 
学年 [1] クラス [A] 名前 [ Elenor Batorwski    ] 
------------------------------------------------

あとがき

けっこうこういうコード書くはずなのだけど、ぐぐってもほとんど出てこなかったので書きました。
っていうかあれか、たいていDBから取ってくるときに既にソートして取ってるから
そのあと並び替えするなんてことあんまりしないですよね。
本当はStrategyパターンを書きたかったのですが、前書きで終了してしまいました。
次はStrategyパターンを書きます。

とおもったけど、そもそもStream APIでComparator.comparing()/Comparator.thenComparing()を使えばOKです。
blog1.mammb.com

C++のmapとJavaのmapの挙動が違う件

f:id:fa11enprince:20200701021345j:plain
C++でmapを書いててinsertしまくってましたが、
あれれ?Javaと挙動が違うということに気付きました。
mapでinsertするとC++ではキーが重複したときに、
insertされず、valueが上書きされない。
一方、Javaのほうはputしたときに
最後にputしたものでvalueが上書きされる
というようになっていました。

C++のコード

#include <iostream>
#include <string>
#include <unordered_map>

using namespace std;

int main()
{
    unordered_map<string, int> strmap;
    string k;
    int v;
    while (cin >> k >> v)
    {
        if (cin.eof())   // Ctrl+D (Unix/Linux), Ctrl+Z(Windows)
        {
            break;
        }
        strmap.insert({k, v});
    }

    for (const auto& s : strmap)
    {
        cout << s.first << " => " << s.second << '\n';
    }

    return 0;
}

実行結果

途中まで入力してCtrl+DまたはCtrl+Zで終了しています

aaa 1
bbb 1
aaa 2
bbb 2
bbb => 1
aaa => 1

お、おう、わざと無理やり最後の値でinsertするってことが
単純にはできないのか…

Javaのコード

package purin;

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class Test1 {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Scanner in = new Scanner(System.in);
        while (in.hasNext()) {
            String buf = in.nextLine();
            String[] bufs = buf.split(" ");
            map.put(bufs[0], Integer.parseInt(bufs[1]));
        }
        for (Map.Entry<String, Integer> m : map.entrySet()) {
            System.out.println(m.getKey() + " => " + m.getValue());
        }
        if (in != null) {
            in.close();
        }
    }
}

実行結果

aaa 1
bbb 1
aaa 2
bbb 2
aaa => 2
bbb => 2

うん、普段触ってる言語はこっちの動きだ……

C++で同じようなことを実現する方法

※追記 こんなことしなくてよいです!
詳細は id:yasuharu519 さんのコメントを参照ください。
operator[]で要素追加できるなんて盲点でした(´Д` )!!

#include <iostream>
#include <string>
#include <unordered_map>

using namespace std;

int main()
{
    unordered_map<string, int> strmap;
    string k;
    int v;
    while (cin >> k >> v)
    {
        if (cin.eof())   // Ctrl+D (Unix/Linux), Ctrl+Z(Windows)
        {
            break;
        }
        // not found
        if (strmap.find(k) == strmap.end())
        {
            strmap.insert({k, v});
        }
        // found
        else
        {
            strmap[k] = v;
        }
    }

    for (const auto& s : strmap)
    {
        cout << s.first << " => " << s.second << '\n';
    }

    return 0;
}

実行結果はJavaのと同様なので省略します。

うん。要はちゃんとチェックしろってことですね。
find()で見つからないときは最後の要素の次(iterator::end())を返します。
最後の要素でなくて最後の要素の次ですね。C++を使い慣れている人には常識ですが……。
ここ重要なのでテストに出ます。