🙄

C++ による数値シミュレーション入門 (2/4)|C++ の基本構文

2024/05/19に公開
2

東工大情報理工学院 高安研究室 で開催されている新入生向けのプログラミングゼミの資料を一部公開します.

本稿は C++ による数値シミュレーション入門 の第 2 部です.

ここでは,C++ の基本構文を解説します.

Hello world

プログラミング言語入門の慣例にならって,まずは Hello world! を出力してみましょう.

C++ ファイルの拡張子には cpp を使います.Visual Studio Code などの適当なエディタ上で hello.cpp というファイルを作成し,下記のようなソースコードを作ります.

hello.cpp
#include <iostream>

int main() // プログラムを実行すると最初に呼び出される
{
    std::cout << "Hello world!" << std::endl; // ターミナルに Hello world! と出力
    return 0; // プログラムが正常に終了したことを表す
}
  • #include <iostream>, std::cout, std::endl はおまじないだと思って流してください.第 3 部(ライブラリ) の標準入出力の章で解説します.
  • main() はプログラムの実行開始点で,プログラムが実行されると最初に呼び出されます.
  • {}で囲まれた部分が一つのブロックです.Pythonではインデントが一つのブロックになっています.
  • 文字列はダブルクオーテーション " で囲みます.
  • 文の終わりにはセミコロン;を付けます. 文(statement)とは,プログラムの概念です.こちら (wikipedia) をご参照ください.
  • return 0 は,main 関数の最後に記述することで,プログラムが正常に終了したことをオペレーティングシステムに伝える役割があります.途中でエラーが発生した場合には 0 以外の数値が返されます.最初のうちはこの意味に深くこだわらなくても大丈夫で,main 関数の最後に記述するということだけ覚えておけば十分です.
  • コメントアウトするには // を前に付けるか,/* */ で囲います.

続いて,hello.cppコンパイルします.コンパイルとは,人間が書いたプログラムをコンピュータが理解できる機械語(バイナリコード)に変換することです.この変換を行なうプログラムをコンパイラと言います.

$ g++ -std=c++17 hello.cpp
  • g++ は C++ のコンパイラの一つで,Linux や UNIX 系のシステムで広く使われています.
  • -std=c++17 では C++ のバージョンを指定しています.17 は 2017 年に対応していて,他に c++11,c++14,c++20 などがあります.2011 年以降は 3 年ごとに更新され,その度に便利な機能が追加されています.
  • 最新の c++23 ではかなり現代化 (Python 化) が進んでいます (https://w.wiki/9Dj9).まだすべての機能が各種コンパイラに搭載されておらず (https://en.cppreference.com/w/cpp/compiler_support/23),ドキュメントも少ない状態ですが,いずれこれがスタンダードになっていくでしょう.
  • 本稿では c++17 を使っていきます.

コンパイルすると,コンパイラによって a.out というファイルが作られます.これは実行ファイルと呼ばれ,ターミナル上で下記のようにファイル名を入力すると,プログラムが実行されます.

$ ./a.out
Hello world!
  • ./ は現在のディレクトリを指します.
  • g++-o name のオプションをつけることで,実行ファイルに名前をつけることができます.この際,実行ファイルには拡張子を付けないことが多いです.
$ g++ -std=c++17 hello.cpp -o hello 
$ ./hello
Hello world!

g++ に渡すオプションやファイル名の順番は,一部のケースを除いて通常は関係ありません.例えば,以下の2つのコマンドは同じ結果をもたらします.

$ g++ -std=c++17 hello.cpp -o hello
$ g++ hello.cpp -o hello -std=c++17

g++ には他にもいくつかオプションがあります.代表的なものには以下の2つがあります.

  • -Wall : 通常よりも多くの警告メッセージを出す.
  • -O1, -O2, -O3 : 最適化オプション.数字が大きいほど最適化レベルが高い.

C++ でも C 言語の printf 関数を使うことができます.

hello_printf.cpp
#include <stdio.h>
int main() // プログラムを実行すると最初に呼び出される
{
    printf("Hello world!\n"); // ターミナルに Hello world! と出力
    return 0; // プログラムが正常に終了したことを表す
}

(おまけ)コンパイル時のオプション指定の自動化

g++ で毎度コンパイルオプションを書くのは面倒だと思います.シェルに fish をお使いの場合,config ファイルに下記 abbr を追加すると,g<space> と打てばコマンドを展開してくれます(展開後に編集もできる).

~/.config/fish/config.fish
abbr g g++ -std=c++17 -O3

Bash でも alias で似たことを実現できます.

~/.bashrc
alias g++="g++ -std=c++17 -O3"

一般に,alias はコマンドを編集できない,コマンド履歴の可読性が落ちる等の欠点があります.g++ の場合は上記オプションをめったに変更しないので,どちらを使っても構わないと思います.

基本型

数値型 int, double

  • int: 整数を表す.4 バイト.取りうる値の範囲は -2,147,483,648 ~ 2,147,483,647
  • double: 倍精度浮動小数点.8 バイト.

オーダー 10^6 を超える整数を扱いたい場合は,long long を使います.long long 型は 8 バイト以上のサイズを持つことが保証されており,オーダー 10^{18} までの整数を表現できます.

浮動小数型には float 型(4 バイト)もあります.計算精度が落ちますので,C++ で数値計算を行なう場合には float 型は絶対に使わないでください. 特に,Python に慣れている人は要注意です.Python の float 型は 8 バイトの浮動小数点で,C++ や他の言語の double 型に該当します.

他の言語と同様に,加減乗除・剰余演算が定義されています.除算と剰余には注意点があります.

  • 除算:int 型同士の割り算では端数が切り捨てられ,int 型になります.マイナスの数は0に近づくように丸められます.
  • 剰余:マイナスの数を含む剰余の符号は割られる数の符号と一致するように定義されています.
// 変数の宣言(型宣言が必要)
int year = 2024;
double USD_TO_JPY = 151.42; // at 2024.03.23

// 除算(整数は切り捨て)
std::cout << 2 / 3 << std::endl;    // 0
std::cout << -2 / 3 << std::endl;   // 0
std::cout << 2.0 / 3 << std::endl;  // 0.666667
std::cout << -2 / 3.0 << std::endl; // -0.666667

// 剰余(符号は割られる数の符号と一致)
std::cout << 2 % 3 << std::endl;   //  2
std::cout << 2 % -3 << std::endl;  //  2
std::cout << -2 % 3 << std::endl;  // -2
std::cout << -2 % -3 << std::endl; // -2

また,整数型 int にはインクリメント ++,デクリメント -- が定義されています.

  • ++i のように変数の前に付けると,インクリメントしてから,その値が使われます.
  • i++ のように変数の後に付けると,値を使ってから,インクリメントされます.

下記のように,単体で使う分には同じ結果になります.

++i;
i++;

本稿では ++i をメインに使っていきます.これは,頻繁に利用する下記のようなドキュメントの流儀に合わせ,書き写した場合にも一貫性が保たれるようにするためです.

++ii++ のどちらが速いか等の記事が散見されますが,もし速度に差があったとしても,そこに拘った時間以上に時間が生み出されることはありません.研究成果に繋がらないことに時間を割くことは慎まれるように.もちろん,趣味の時間をお使いになる分には一向に構いません.

インクリメントした値を使う場合には結果が変わります.

int a = 0;
int b; // 宣言だけして初期化しない

b = ++a; // aをインクリメントしてからbに代入
// a = 1, b = 1

b = a++; // aをbに代入してからaをインクリメント
// a = 2, b = 1

ただし,このようなインクリメントの使い方は紛らわしく,バグの温床になります.研究でのプログラミングで最も重要なことは「正確さ」です.正確に書くために,次点として「理解しやすさ」が重要になります. 上の処理は,次のように書くと手順がはっきりして理解しやすくなります.++aa++ のどちらを使っても結果が変わらないので,バグの心配もいりません.

int a = 0;
int b; // 宣言だけして初期化しない

++a;   // aをインクリメント (a++でも結果は同じ)
b = a; // bに代入
// a = 1, b = 1

b = a; // bに代入
++a;   // aをインクリメント (a++でも結果は同じ)
// a = 2, b = 1

真偽値型 bool

  • true, falseの2値を取る
  • 否定・AND・OR演算が定義されている
bool A = true;
bool B = false;
bool not_A = !A;       // false
bool A_and_B = A && B; // false
bool A_or_B = A || B;  // true

// 出力時は true→1, false→0 と置き換えられる
std::cout << not_A << std::endl;   // 0
std::cout << A_and_B << std::endl; // 0
std::cout << A_or_B << std::endl;  // 1

文字 char

  • 1バイトの ASCII 文字を表す
  • シングルクオーテーションで囲む
  • 区切り文字 (delimiter) の指定などに使う
char delim = ','; // カンマ区切り
delim = ' ';      // スペース区切り
delim = '\n';     // 行単位で区切る

文字列型 string は基本型には含まれていません.C 言語ではchar配列として文字列を定義していましたが,C++ では標準ライブラリに string が追加されました.string の使い方は第 3 部で説明します.

制御文

  • 条件分岐:if, switch
  • ループ:for, while
  • ループの中断:continue, break

使用例を示します.switchwhile の使い方は必要になったら調べてください.

for (int i = 0; i < 4; ++i)
{
    if (i % 2 == 1) continue; // 奇数をスキップ
    std::cout << i << std::endl;
}
// 出力:0と2

for (int i = 0; i < 4; ++i)
{
    if (i % 2 == 1) break; // 奇数が出てきたらループを終了
    std::cout << i << std::endl;
}
// 出力:0のみ

{} の中の文が一つの場合は,{}を省略できます.また,C++はセミコロン ; の有無で文の終わりを判別するため,改行の有無で処理は変わりません.つまり,次の4つの if 文はまったく同じ処理になります.

if (true) /* do something */

if (true) 
    /* do something */

if (true){
    /* do something */
}

if (true)
{
    /* do something */
}

関数

  • 宣言:戻り値型 関数名(型 引数名){ 処理; }
  • 引数にはデフォルト値を指定できる 戻り値型 関数名(型 引数名=デフォルト値)
  • ただし,デフォルト値のある引数はデフォルト値のない引数よりも後に指定する
// べき乗を計算(底は非負整数,指数は整数)
// オーバーフローの確認のため,戻り値型を int で実装
int power(int base, int exponent = 0)
{
    if (exponent == 0) return 1;
    return base * power(base, exponent - 1);
}

void print_power()
{
    std::cout << power(10, 9) << std::endl;  // 1000000000
    std::cout << power(10, 10) << std::endl; // 1410065408
    // int 型で10^10を計算するとオーバーフロー(桁あふれ)が発生
}

値渡しと参照渡し

関数に値を渡す方法には,値渡しと参照渡しの2つがあります.

値渡しでは,関数に引数を渡す際にその値のコピーが作成されます.関数内で引数の値を変更しても,元の変数の値には影響を与えません.つまり,関数が引数の値をローカルで操作するだけで,外部の変数には影響を与えないため,安全性が高いと言えます.一方で,値渡しはコピーにコストのかかる大きなデータ構造やクラスの扱いには向きません.このような場合に参照渡しを使います.

関数の引数を定義する際,変数名の前に & を付けると参照渡しになります.参照渡しでは,引数として変数のアドレスが渡されます.このため,関数内で引数の値を変更すると元の変数の値も変更されます.これにより関数が外部の変数を直接操作できるようになりますが,誤って変数の値を変更するリスクも伴います.

#include <iostream>

// x: 値渡し,y: 参照渡し
void update(int x, int &y)
{
    ++x; // この変更は関数の外に影響を与えない
    ++y; // 元の変数の値を変更
}

int main()
{
    int a = 0;
    int b = 0;
    update(a, b);
    std::cout << a << std::endl; // 0
    std::cout << b << std::endl; // 1
    return 0;
}

コマンドライン引数

ターミナルからコンパイルしたプログラムを実行する際,引数を渡すことができます.この引数をコマンドライン引数と言い,main 関数に次の引数を追加することで値を受け取ることができます.

  • argc:コマンドライン引数の数が格納される
  • argv:コマンドライン引数の配列で,argv[0] には実行ファイル名,argv[1] から argv[argc-1] までの各要素には,コマンドラインで指定された引数が格納される.
#include <iostream>
int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; ++i)
    {
        std::cout << i << ": " << argv[i] << std::endl;
    }
    return 0;
}

char *argv[] を理解するにはポインタの知識が必要になります.本稿ではポインタは解説しませんので,おまじないと思って受け入れてください.

これをコンパイルして実行すると次のようになります.

$ ./a.out arg1 arg2
0: ./a.out
1: arg1
2: arg2

課題 2-1(ゼロで割る)

  • int 型の数値を 0 で割るとどうなりますか?例えば,1 / 0
  • double 型の数値を 0 で割るとどうなりますか?例えば,1.0 / 0
  • こうしたバグを防ぐために,どのような対策が考えられますか?

課題 2-2(複利計算)

元金 x 円を年利 r %で t 年運用すると x\times(1+\frac{r}{100})^t 円になります.これを計算するプログラムを書いてください.満たすべき要件は下記の通りです.

  • x, r, t を非負整数とし,コマンドライン引数から受け取る
  • コマンドライン引数の数が合わない場合は,それが分かるようにメッセージを出力する
  • x, r, t のいずれかが定義域外の場合も,それが分かるようにメッセージを出力する.ただし,整数以外が入力される場合は想定しなくてよい
  • 出力は複利計算の結果のみでよい

下記をベースにプログラムを完成させてください.なお,std::stoi() は文字列型として受け取ったコマンドライン引数を int 型に変換しています.

hukuri.cpp
#include <iostream>
#include <string>  // std::stoi (文字列をint型に変換)

// calculate x*(1+r/100)^t
double fukuri(int x, int r, int t)
{
    return 0;
}

int main(int argc, char *argv[])
{
    // input
    int x = std::stoi(argv[1]);
    int r = std::stoi(argv[2]);
    int t = std::stoi(argv[3]);

    // output
    std::cout << fukuri(x, r, t) << std::endl;
    return 0;
}

Discussion

yaito3014yaito3014

数値型のサイズについて、現代の多くの環境において int, float が 4 バイトで double が 8 バイトであることは確かですが long int のサイズが 8 バイトであると言い切るのは難しいと思われます。
8 バイト以上のサイズを持つ保証がある整数型を使いたい場合は long long int を使うべきですし、厳密に 8 バイトの整数型を要求する場合は <cstdint> 及び <stdint.h> ヘッダの std::int64_t 及び int64_t を使うべきではないでしょうか。

禅定印仏座像禅定印仏座像

ご指摘ありがとうございます.ご指摘の通りだと思いますので,記述を修正しました.
以下の二点について,今回初めて勉強させていただきました.お礼申し上げます.

  • long int は一般的には 4 バイト以上のサイズを持つことが保証されているが,8 バイトで定義される環境は一部に過ぎないこと
  • long long int は 8 バイト以上のサイズを持つことが保証されているが,必ずしも厳密に 8 バイトで定義されるわけではないこと