Skip to content

Latest commit

 

History

History
488 lines (351 loc) · 15.8 KB

File metadata and controls

488 lines (351 loc) · 15.8 KB

標準入力と文字と文字列

普通プログラムには、入力があって出力があるものです。 出力はHello, World!の項でも使ったwrite系関数で可能です。 では、入力はどうなんでしょう?

std.stdio.readln

まずは使ってみる

readlnを使えば標準入力(コンソール)から1行得ることができます。

// example00400.d
import std.stdio;

void main()
{
    string str = readln();

    write(str);
}

このプログラムを実行してみると、いつもと違い、プログラムが終わらないと思います。 なぜ終わらないかというと、標準入力に1行もないからです。 さて、コンソールにhogehogeとか適当に打ってみましょう。 すると、オウム返ししてきます。

オウム返しされましたか? おめでとうございます! これであなたは晴れて真のプログラマになりました。 入力のない今までのプログラムには、1メートル程度の価値しかありません。
(ちなみに、出力のないプログラムには1ミリの価値もありません。むしろCPU時間を無駄に消費するゴミです)

2行目も欲しい場合には、2回呼び出せばいいのは想像できますよね。

// example00401.d
import std.stdio;

void main()
{
    string line1 = readln();
    string line2 = readln();

    write(line2);
    write(line1);
}

今回のプログラムは、2行の入力を反転させて出力します。

readlnの詳細

readlnは、標準入力から1行拾ってきます。 気づいて欲しいのですが、writelnでなくてwriteを使ってます。 というのは、readlnの結果の最後には改行文字がすでに入っているからです。

文字と文字列とは?

文字コード

さて、改行文字とかいうよくわからない言葉が出て来ましたが安心してください。 簡単に説明します。

今あなたがプログラムを打ち込んでいるテキストエディタは、どうやって文字を区別しているでしょうか? つまり、「文字に対してどのように2進数の値を割り当ててるのでしょうか?」ということです。 実はこれが文字型の値で、文字列の要素です。

実際、文字は文字コードと呼ばれる符号化によって、1バイトから4バイト程度まで数値が振られています。 文字コードにはいろいろあって、例えばASCIIやShift JISやUTF-8, UTF-16, UTF-32などです。

D言語は、文字がUTF-8やUTF-16, UTF-32でエンコーディングされていることを前提に設計されています。 それぞれchar, wchar, dcharという型に対応します。 文字それぞれの値の例を示すと以下のようになっています。

'a' => 97
'A' => 65
'@' => 64
'&' => 38

文字の数値を確認する方法を教えましょう。 '<1文字>'とすると、文字のUTFのコードが得られます。 この'<1文字>'を文字リテラルと呼びます。

// example00402.d
import std.stdio;

void main()
{
    writefln("'%1$s' => %1$d(%1$08X)", 'a');
    writefln("'%1$s' => %1$d(%1$08X)", 'A');
    writefln("'%1$s' => %1$d(%1$08X)", '@');
    writefln("'%1$s' => %1$d(%1$08X)", '&');
    writefln("'%1$s' => %1$d(%1$08X)", '');
}

write系関数のフォーマットでいままで見たことがないのが出て来ました。 %n$<format>とすることで、その場所に入れられる引数を指定出来ます。 nは1から始まる整数で、<format>の部分は今までどおりの設定です。

出力は、最初は<文字リテラル> => <10進数表現>(<16進数表現>)となっています。

'a' => 97(00000061)
'A' => 65(00000041)
'@' => 64(00000040)
'&' => 38(00000026)
'あ' => 12354(00003042)

改行文字と制御文字

では本題ですが、テキストエディタはどうやって改行の情報をテキストデータと共に保持していると思いますか?

実は、改行の情報も1文字(Windowsでは2文字)と数えられているのです。 ですから、readln()で取得した文字列の最後には改行文字が入っています。

さて、では文字列の改行文字を消してみましょう。 std.string.chompを使います。

import std.stdio, std.string;

void main()
{
    string line1 = readln();
    string line2 = readln();

    write(chomp(line1));
    write(line2);
}

今度のプログラムも2行打ち込んでみてください。 先ほどとは違って、1行に連なって出力されたと思います。 つまり、最初の行の改行文字が正常に消えました。

逆にwriteで改行文字を使って改行してみましょう。

import std.stdio;

void main()
{
    char ln = '\n';     // \n で改行を表す。
    write("Foo", ln, "Bar\n");

    write("foo\r\n");   // OSによっては"\r\n"の2文字だったり
    write("foo\r");     // '\r'だけだったりする。
}

プログラムの結果はOSによって異なります。

このように、改行文字を使うと改行を自在に操れます。 改行以外にも、いろんな制御文字がありますが、使用頻度が低いのでここでは紹介しません。

文字列の簡単な操作をする

文字についてはわかったかと思います。 次は文字列です。

文字列は、「文字の列」という名前の通り、文字が順番に並んでいます。 文字列はstring, wstring, dstringという型があります。 それぞれUTF-8, UTF-16, UTF-32エンコーディングされた文字列です。

文字列は"<文字列>"というリテラルがあり、これはstring型です。 もしwstringにしたいなら"<文字列>"w、dstringにしたいなら"<文字列>"dとします。

// example00403.d
import std.stdio;

void main()
{
    string str =   "foo \n";
    wstring wstr = "foo \n"w;
    dstring dstr = "foo \n"d;

    writeln(str);
    writeln(wstr);
    writeln(dstr);
}
$ rdmd example00403.d
foo 
 あ
foo 
 あ
foo 
 あ
  • 文字列の先頭の文字を得る。

文字列は実は配列なのですが、少し先走ると配列の先頭要素はarr[0]で取得できます。 dstringはそれでも問題無いのですが、stringwstringはそれではダメなのです。

// example00404.d
import std.stdio;

void main()
{
    string str =   "山田太郎";
    wstring wstr = "鈴木次郎"w;
    dstring dstr = "斎藤三郎"d;

    writeln(str[0]);
    writeln(wstr[0]);
    writeln(dstr[0]);
}
$ rdmd exmaple00404.d
?
鈴
斎

上記例ではstringのみ失敗しましたが、wstringでも実はwstr[0]は安全ではありません。 よって、通常は以下のようにstd.array.frontを使います。

// example00405.d
import std.array, std.stdio;

void main()
{
    string str =   "山田太郎";
    wstring wstr = "鈴木次郎"w;
    dstring dstr = "斎藤三郎"d;

    writeln(str.front);
    writeln(wstr.front);
    writeln(dstr.front);
}
$ rdmd example00405.d
山
鈴
斎

ちゃんとできましたね。 しかし、注目して欲しいのは、front(str)でなくてstr.frontなことです。 これは、UFCS(Uniform Function Call Syntax)といい、「関数呼び出しがfoo(a, b, c, ...)などの場合に、a.foo(b, c, ...)と書ける」記法です。 また、foo(a)はUFCSでa.foo()となりますが、D言語の特徴で()を外してもいいのでa.fooとなります。 以前は、aが配列(stringは配列と言いましたね)の場合だけに許された記法でしたが、dmd 2.059でUFCSとして、どのような型でも可能になりました。

「じゃあ、front(str)と書くのはいけないのか?」という話ですが、結論からいうとD言語のお作法的にダメです。 次の項で説明するpopFrontemptyもUFCSを使って書くのがお作法です。 これはRangeという考え方に沿っていますが、このRangeについて説明するのはかなり後になるでしょう。

  • 2文字目以降を得る

山田さんの山だけとれても嬉しくないので、田も取ってみたいところです。 というわけで田も出力できるようにしましょう。

// example00405.d
import std.array, std.stdio;

void main()
{
    string str = "山田太郎";

    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);

    writeln(str.empty);     // 文字列が空か? => false
    str.popFront();
    writeln(str.empty);     // 文字列が空か? => true
}
$ rdmd example00406.d
山 : 山田太郎
田 : 田太郎
太 : 太郎
郎 : 郎
false
true

この通り、山田太郎さん完全体です。

std.array.popFrontを使えば、文字列の先頭から1文字削除することができます。 frontの場合とは違い、popFrontstr.popFront()という風に最後に()を付けるのがお作法です。

std.array.emptyは、文字列が空かどうか判定できます。 文字列に文字が一切含まれていないなら、trueを返します。

  • 文字列から部分文字列を取り出す

実は、string型の"山田太郎"の文字列の"田太"を抜き出すのは至難の業です。 もし、dstring型であれば、a[1 .. 3]とできるのですが、string型ではそのようなことができません、残念。

しかし、もしstring型に入っている文字が半角英数字だと仮定できるなら、a[1 .. 3]としても大丈夫です。 つまり、"yamada taro""da ta"を取り出すことは簡単なのです。 dは先頭から5文字目で、taroのaは先頭から9文字目です。 なので、次のソースコードを動かせばda taが手に入ります。

import std.stdio;

void main()
{
    string str = "yamada taro";

    writeln(str[5-1 .. 9]); // da ta
}

str[5-1 .. 9]a[b .. c]はスライス演算子と呼びます。 5-1としているのは、プログラミングでは数え始めを0にするからです。 つまり、1番目は0です。2番目は1。これを0-originと呼びます。 文字列の先頭文字(1番目の文字)を取りたかった時にstr[0]としたのはそのためです。 今回の場合は、5番目の文字を指したいので、添字は4です。

「じゃあ、9番目の文字を指すのに、なぜ9なの?8では?」という疑問が湧くかもしれません。 D言語では、a[b .. c]c<最終要素の添字> + 1にします。 これはD言語の仕様で決められたことで、この方が都合がいいのです。 たとえば、strから取得したい文字列の最初の添字がsで、n文字取得したい場合には、str[s .. s + n]と記述できます。 これらのことについては近々配列の項で紹介します。

readf

readlnを使用することによって、1行の文字列をコンソールから入力することができるようになりました。

では、話は変わって、あなたが電卓を作りたいとします。 readlnで得られるのは文字列ですが、作りたいのは電卓なので数値が欲しいのです。 標準入力から数値を得る方法の一つとして、readf関数があります。

まずは使ってみる

次のように使用します。

import std.stdio;

void main()
{
    int n1;

    write("好きな数値を1つ入れてください----");

    readf("%s", &n1);

    writefln("あなたが入力した数値は%sですよね", n1);
}

readfに注目してください。 &n1となっています。&は演算子で、&n1はポインタになります。

ポインタとは?

ポインタ(Pointer)というのは、「指し示すもの」というそのままの意味です。 つまり、&n1n1を指し示すものなわけです。 難しい書き方をすると、&演算子は左辺値を受け取り、左辺値のポインタを返します。

n1というのは変数で、必ずメモリの何処かにあるはずです。 &n1は、そのメモリのアドレス値になります。 というわけで、ポインタは日本語では「住所(Address)」だと言われたりします。

readfn1に値を書き込みたいので、n1がメモリ上のどこにいるかを知っている必要があります。 ですから、アドレス演算子&を使ってn1の住所を「ここに数値を入れてくれ」という意味で渡します。

ポインタの型は、通常T*という形で表されます。Tは任意の型で、たとえばintに対するポインタならint*という型になります。

readfn1のポインタ&n1を使って、n1の値を書き換えているのですから、ポインタ値を用いて値を書き換えたり、取得する方法があるはずです。 それが、ポインタ演算子*です。 積a * bと同じ記号ですが、ポインタ演算子は単項演算子なので曖昧になることはありません。 ポインタ演算子*pは、ポインタ型のpを受け取って、pが指す値を左辺値として返します。 左辺値なので、*p = n;のように代入が可能です。

アドレス演算子とポインタ演算子は次のように使います。

// example00406.d
import std.stdio;

void main()
{
    int a = 12;
    int* p = &a;

    writeln("aの値: ", a);

    *p = 12;    // ポインタの指す住所への代入
    writeln("ポインタが指す値: ", *p);
    writeln("aの値: ", a);
}
$ rdmd example00406.d
aの値: 12
ポインタが指す値: 24
aの値: 24

C言語とかC++だとポインタは重要なのですが、D言語だとほとんど出現しない機能です。 しかし、配列やクラスなどの根底に存在する概念ですから、理解し使えることはかなり重要です。

これら以外の標準入力からの取得

標準入力は、std.stdio.stdin.byLinestd.stdio.stdin.byChunkでループを回して処理することも可能です。 readlnやreadfよりももっと詳細に操作したいなら、std.stdio.stdin.rawReadなどもあります。 しかし、これらを使うにはforeachや配列への理解が必要なので今回は省略します。

問題 -> 解答

  • readlnを使って3行取得して、各行の先頭2文字を削って表示するプログラムを作ってください。
    ヒント: readln -> popFront() -> write

  • writeln("%2$s - %1$x", 10, 16);の表示の結果を予想してください。

  • 次のプログラムはどのような出力をするか予想してください。

import std.stdio;

void main()
{
    int a = 1, b = 2;
    int* p = &a, q = &b;

    p = &(++*q);

    writeln(*p);
    writeln(*q);
}

おわりに

お疲れ様です。 入力の文字列から数値を得る方法はreadf以外にもいろいろ存在します。 たとえば、std.conv.toを使いto!int("123")と書けば、int型の値123が得られたりしますがこれは別の機会で。

今回は問題が少し実践的になってるかと思います。 次からは条件分岐ifや、ループ文for, foreach, whileなどもっとプログラムらしいものを紹介していきます。

キーワード

  • std.stdio
  • std.stdio.readln
  • 文字(Charactor)
  • 文字列(String)
  • front, popFront(), empty
  • UFCS(Uniform Function Call Syntax)
  • std.stdio.readf
  • ポインタ(Pointer)