第 3 回 C++言語演習

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

3-1. はじめの一歩

それでは、サンプルプログラムを動かしましょう。

  1. Arduino IDE を起動し、ファイル→スケッチ例→ 01 Basics→Blink を選んで 下さい。
  2. ツール→ボードで Arduino UNO を選び、 ツール→シリアルポートで、接続した Arduino が選ばれていることを確認し てください。
  3. そして、右矢印のボタン(→)を押すと、プログラムがコンパイルされ、 Arudino に送られ、そして、実行が始まります。 プログラムが送られる際に黄色いLEDが数回点滅した後、プログラムが実行され、 黄色いLEDがゆっくり点滅を繰り返します。

blink.ino

では、 blink.ino プログラムを見てみましょう。

このプログラムは C++ 言語で書かれています。 C++言語では、/* から */ までに書かれた内容、さらに、一行のうちの // の 後に書かれた部分は コメント と呼ばれ、コンパイル時に無視され、 プログラムの動作に影響を与えません。 そこで、blink.ino からコメントを取り除いた部分を示します。


void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);                   
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);                  
}

通常の C++ 言語のプログラミングでは、 main 関数と呼ばれる関数を定義す ると、それが実行されます。 そして、プログラムは通常は何らかの出力をして、終了します。

しかし、Arduino では、main 関数を定義せず、 setup 関数loop 関数を定義します。 setup 関数は起動時に一回だけ実行され、その後、 loop 関数が何度も繰り返 し実行されます。 そのため、 setup 関数には初期設定を、 loop 関数には繰り返し操作の一回 分のプログラムを記述します。

blink.ino では setup 関数では、Arduino 上の LED を点灯させるモードに指 定しています。 そして、 loop 関数では LEDを点灯、1000ms 待機、LEDを消灯、1000ms 待機という、繰り返すと点滅になる一通りのプログラムが書かれています。

3-2. C++言語によるプログラム

C言語とC++言語

C言語は構造型プログラミング言語として高級言語に含まれますが、 機械語に翻訳(コンパイル)された状態をかなり想像できるようなプログラミン グ言語です。 変数に型があり、式による計算ができ、さらにループや分岐の書式が定められ ています。 これらはそれぞれ機械語に容易に変換できます。 また、C言語の特徴であるポインタも、機械語での間接参照に対応しています。 もともと、UNIXというOSを記述するために作られた言語ですが、何でも書くこ とができる汎用性の高い言語です。

C++言語は、C言語にクラス宣言を加えてオブジェクト指向プログラミングが できるようにした言語です。 最初は、C言語への変換をするように設計されたため、高い互換性がありま した。 特に、クラス宣言とstruct 宣言がほぼ互換性があるようになっています。 C++ でクラス宣言する際、暗黙のメソッドが宣言されるため、書いたプログ ラムとコンパイルした後の機械語が一対一対応はしません。 メモリを効率よく使う様にプログラムを書くのは難しいですが、一方で、組み 込み系のような外部装置などの操作については、外部装置をオブジェクトと して定義すると、外部装置の細部の仕様に拠らず、プログラムの可読性が上 がります。

変数

C++言語で情報処理をするには、基本的には変数を使います。 C++言語では、変数にはがあります。

Arduino UNO に搭載されている ATMega328P と言うマイコンは 8bit と呼ば れています。 これは、マイコンの基本的な演算が 8bit を基本に行われるということです。 つまり、通常のデータは 0〜255(byte 型)、 あるいは -128〜127 (char 型)までの値を扱います。 これより大きな値を扱うには複数回の演算を組み合わせます。 但し、C++言語ではもっと様々な型を使えるようになっていて、コンパイル時 に 8bit のデータ処理を組み合わせた機械語のプログラムに変換されます。

Arduino の開発で使う主な型を示します (本家リファレンス)

主なデータ型

void関数定義の際に用いる、情報なしを示す型
booltrue, false(=0) の 2値のみを取る
byte 0 から 255 までの値を取る
char -128 から 127 までの値を取る(言語リファレンスには明示されてない)
float 浮動小数点数
int通常のプログラミングではよく使われる整数型であるが、ハードウェア の特性により値の範囲が違う。 Arduino UNO では -32768 から 32767 まで。
long int の倍の精度の整数。Arduino では 32bit なので、 -231 から 231-1 まで。

言語リファレンスの array の項では配列変数について説明されてい ます。 様々な型に対して、変数名の後に角括弧[]で数を括って、複数個の変数を数で 定義、使用できます。書式は以下の通りです。


型名 変数名[数];
byte a[10]; // a[0], ..., a[9] まで使える
a[0]=1; //代入できる
a[1] = a[0]*2; //変数として使える
j = a[i*3+1]; //角括弧の中は式でも良い

変数の取扱

変数は宣言、初期化、値の取得、代入などができ、さらに、演算、比較なども できます。

宣言
宣言の構文は以下の通りです。,(カンマ)で区切って複数の変数 を同時に宣言することもできますが、厳密なルールは他書に譲ります。

型名 変数名;
初期化
宣言の際に初期値を与えて初期化できます。 C++ は2つの構文があります。 また、複数の値を同時に指定する場合は中括弧 {, } でくくって指定しま す。初期化の値が足りなかったときは残りの値は全て 0 で初期化されます。

char moji = 'A';  
int x(3); // x = 3
byte ar[] = {1,2,3}; // ar[0] = 1, ar[1] = 2, ar[2] = 3
byte b1[10] = {0}; // b1[0] = ... = b1[9] = 0
値の取得、代入、演算、比較
それぞれ例を示す **************************

関数

関数とは

プログラムは、命令などの組み合わせにより機能を実現し、その機能を組み合 わせて、目的の問題を解決します。 プログラムの特定の部分をまとめ、それに名前を与え、そのプログラムをその 名前で呼び出すことを考えます。 すると、命令の集まりより、その集まりの実現する機能の名前でプログラムを 記述できることになります。 これは、プログラムが読みやすくなります。

C++言語では、このプログラムの集まりを実行し、必ず値を一つ返すこととし、 これを 関数 と呼びます。 C++言語では関数定義の書式は次のようになります。


戻り値の型 関数名(仮引数1の型 仮引数1の名前, 仮引数2の型 仮引数2の名前, ...){
    仮引数を使ったプログラムの集まり
    return 戻り値;
}

単純な例として値を渡すと2倍にして返す関数twiceを定義すると次のようにな ります。


byte twice(byte x){
    return 2*x;
}

これを呼び出すときは関数名と実引数を指定して twice(a) で a の2倍の値を得られますし、twice(3) で 6 が得られます。 なお、 Arduino の環境は C++ なので、参照型を使えます。 これを使うと変数の値を関数内から書き換えられるので、複数の値を関数から 戻すことができます。

次の例は変数の値を2倍にします。


void doubler(byte& x){
  x=2*x;
}

byte 型で宣言された変数 a に対して doubler(a) と呼び出すと、 aの値が2倍になります。

さらにシンプルで実用的な例として、2つの変数を取り替える swap 関数は次のように定義できます。


void swap(byte& x, byte& y){
  byte tmp = x;
  x = y;
  x = tmp;
}

swap(a,b) と呼び出すと、変数aとbの値が交換されます。 なお、この swap 関数は byte 型専用です。 しかし、どんな変数型でも値の交換をする機会はありえます。 そのために考えうる型全てでswap関数を定義することもできますが、 どの型でも同じように使いたい場合、型を固定しない テンプレートという手法もあります。 この手法は奥が深いですが、この本の目的とは若干外れるのでこれ以上は説明 しません。


template <typename T>
void swap(T& a, T& b){
  T tmp = a;
  a = b;
  b = tmp;
}

筆者は経験から、小さいプログラムでも意味のある名前による関数にするとプ ログラムの可読性がよくなると考えます。 また、オブジェクト指向の基礎としても、さまざまな小さな操作にも名前を付け ることは重要です。 逆に著者の好みでないのが、多すぎるコメントや意味のある長い変数名です。 以後のプログラムの説明でも、あまりプログラムにコメントを付けずに、専ら 関数化により可読性を上げることを試みます。

関数の取扱

C言語、C++言語では、関数毎にコンパイルできます。 但し、関数を呼び出す側は、関数の中のプログラムは知らなくても、何を関数に渡すかと、何が関数から得られるかという入出力の 仕様は知らなくてはなりません。 そのため、呼び出し側でも、関数の戻り値型、関数名、仮引数の型だけの宣言はする 必要があります。 この宣言をプロトタイプ宣言と呼びます。


byte twice(byte x);

関数の中のプログラムの定義は一箇所でしか行いませんし、唯一でなければな りません。 しかし、関数は有用であればあるほど多くの呼び出し側から呼ばれます 。 なお、C言語では同じ名前の関数はひとつだけ、C++言語では同じ名前と引数の 型の関数はひとつだけです。

そのため、関数のプロトタイプ宣言を共通ファイルとして集めておき、様々な プログラムでその共通ファイルを読み込むようにします。 プロトタイプ宣言のような宣言を収める共通ファイルをヘッダファイル と呼び、プログラムの冒頭で #include ヘッダファイル名 で読み込みます。

Arduino の スケッチと呼ばれるino ファイルでは、setup 関数と loop 関数 を必ず定義する必要があります。 ino ファイルでは、リファレンスの関数を使用できるようにするために、 Arduino.h ヘッダファイルが自動的にインクルードされています。

別ファイルを作る場合、別ファイルで Arduino のリファレンスの関数を利 用するためには、Arduino.h ヘッダファイルをインクルードする必要があり ます。

状態の保持

制御を行うプログラムを組む場合、制御対象の状態など保持しなければならな い情報があります。 そのため、プログラムの流れの中で保持すべき情報を読み書きする必要があり ます。

前章で説明した setup 関数とloop 関数で共通してデー タを共有することを考えます。 これを実現する手段は、関数の外で変数宣言をする グローバル変数を使用するしかありません。


byte data; //グローバル変数
void setup(){  
  data=0;
}
void loop(){
  data++;
  //...
}

グローバル変数は大きなプログラムを作る場合に解析を困難にします。 一方で、小さいプログラムであればシンプルなプログラムを導きます。

なお、このプログラム例の場合、次のような手法でグローバル変数を回避でき ます。

  1. loop関数を使うのを諦め、 setup 関数内に無限ループを作成する
  2. setup 関数を使うのを諦め、loop 関数内で初期化を行い無限ループ を作成する
  3. loop 関数内で static 宣言で変数を初期化する

static 宣言

static 宣言にはいくつかの種類がありますが、ここでは ローカル変数の static 宣言だけを説明します。

関数内で変数宣言を static 宣言すると、初回の関数呼び出しのときだけ初期 化をし、二回目以降の関数呼び出しでは初期化は行われず、前回の値を保持し ます。 そのため、グローバル変数と同様に状態を保持できます。 但し、他の関数と無条件に値を共有することはできません。

例えば、 blink.ino を次のように書き換えられます。


void setup(){
  pinMode(LED_BUILTIN,OUTPUT);    
}
void loop(){
  static bool flag = true;
  digitalWrite(LED_BUILTIN, flag);
  flag = !flag;
  delay(1000);
}

class 宣言

オブジェクト指向を使うことで、状態などの保持をすること ができます。

オブジェクトとは何かと言われると著者にもはっきり定義できませ んが、そこは読者も使いこなすうちに分かってくると思います。 大まかに言えば操作の対象物です。 オブジェクト自体も変数に入れて操作をします。 その変数へ指示すると、オブジェクトが操作を行います。

オブジェクト指向 プログラミングでは、オブジェクトの操作を行うプログラムと、オブジェクト の操作がどのようなものかを定義するプログラムの2つを別々に作ることにな ります。 オブジェクト指向で外部デバイスなどをマイコンでコントロールするのは扱い 易いです。

さて、例を使って説明をします。 「点滅する内蔵LED」というをオブジェクトを作成し、それに「点滅」 を指示することで、点滅させることを考えます。 C++言語では、オブジェクトの雛形を class 宣言で作成します。 class 宣言の中で、状態を保持する変数であるメンバ変数と、操作 を定義するメンバ関数(メソッド)を定義します。

C++ で点滅する内蔵LEDを扱うには次のようにプログラムを作ります。

  1. 点滅する内蔵LEDのクラス名を Led とし、
  2. loop関数内でクラスに従ってオブジェクトを生成し、変数 led に入れ、
  3. それを、blinkメンバ関数で操作する

つまりプログラムは大まかには次のようなプログラムになります。


class Led {
  byte status; //メンバ変数の定義
public:
  void blink(){  //メンバ関数の定義  
    ...
  }
};
void setup(){
//初期化
}
void loop(){
  static Led led; //これだけで自動的にオブジェクトが生成される
  led.blink();
}

なお、class 宣言もヘッダファイルに入れるべきです。 その場合、プログラム部分は別ファイルとします。 プログラム部分は ::(スコープ演算子) を使って定義します。 つまり、次のように三分割されます。

led.h


class Led {
  byte status; //メンバ変数の定義
public:
  void blink();  //メンバ関数のプロトタイプ宣言
};

led.cpp


#include "led.h"
void Led::blink(){  //スコープ演算子を使ったメンバ関数の定義

}

blinkled.ino


#include "led.h"
void setup(){
//初期化
}
void loop(){
  static Led led;
  led.blink();
}

特に、初期化をするメンバ関数をコンストラクタと呼びますが、 コンストラクタの名前はクラス名と同じで戻り型宣言なし、さらに、メンバ変 数を初期化するのに特殊な文法があります。

例3-1

例えば、0.1秒間隔で k 回点滅した後、1秒消灯するようなクラスを考え、 2回点滅と3回点滅を繰り返すようなことを考えると、 次のようになります。

led.h

class Led {
  byte kaisu;
public:
  Led(byte k);
  void blink();
};
led.cpp

#include "Arduino.h"
#include "led.h"
Led::Led(byte k):kaisu(k){} //コンストラクタのメンバ変数初期化構文
void Led::blink(){
  for(byte i = 0; i < kaisu; ++i){
      //0.1秒点灯
      //0.1秒消灯
  }
  //0.9秒消灯
}
blinkled.ino

#include "led.h"
void setup(){
//初期化
}
void loop(){
  static Led leda(2),ledb(3); //この構文でコンストラクタが呼ばれる
  leda.blink();
  ledb.blink();
}

ポインタ

C++では変数の値をメモリ上の番地で管理することができます。 その管理に使用する変数をポインタ変数と呼びます。

ポインタは値の型に * を付けて、 byte* 型、 int* 型などで変数宣言をしま す。 また、ポインタ変数 p に対して、 p の番地のメモリの内容を取り出す際にも、 同じ * を用いて、 *p と記述します。 p が構造体や、クラスのポインタの場合、メンバ変数 a, メンバ関数 b() な どにアクセスするには p->ap->b() という構文 を使います。

さて、オブジェクトを作成する場合、変数宣言でも作ることが出来ますが、一 方で、 new 演算子コンストラクタを使って生成し、 ポインタ変数で管理する方法もあります。

ポインタによるオブジェクトの管理

Led* p;
p = new Led(2);
...
delete p;

これを用いると、オブジェクトの生成もパラメータ化出来ます。

オブジェクト生成のパラメータ化

#include "led.h"
const byte a[] = {1,3,4,2,0};
Led* p[sizeof(a)/sizeof(a[0])];

void setup(){
  byte i;
  for(i = 0 ; a[i] != 0 ; ++i){
    p[i] = new Led(a[i]); 
  }
  p[i] = NULL;
}

void loop(){
  for(byte i = 0 ; p[i] != NULL ; ++i){
    p[i]->blink();
  }
}

コンパイラの最適化と volatile宣言

C言語やC++言語で書いたプログラムは、そのままコンピュータを制御するわけ ではなく、一旦機械語に翻訳(コンパイル)されて、実行されます。 その際、最適化と呼ばれる様々な手法で、翻訳される機械語が効率 化されます。 例えば、何もしないプログラムは完全に削除されたりすることもあります。

しかし、制御などの場面では、通常の最適化の定石を当てはめられると、意図した 結果が得られない場合があります。 例えば、変数と外部入力が結び付けられていて、変数が変化した時に特定の処 理をするプログラムを考えます。 すると、プログラムの中で変数を変化させる処理はし ない時は、最適化の手法として以下の例で完全にif文そのものが取り除かれて しまうことがあります。

最適化の餌食

bool sensor = false;
void loop(){
  if(sensor){
    //sensorがtrueになった時の処理を書いても
    //どうせ実行されないだろうと取り除かれてしまう
  }
}

そのために、変数を volatile 宣言を使います。この宣言をした変 数に関する処理は最適化で取り除かれず、sensor が true になっ た時は意図通りの処理が実現されます。

volatile 宣言

volatile bool sensor = false;
void loop(){
  if(sensor){
    //sensorが true になった時の処理
  }
}

入力

マイコンの入力を使った演習を行います。

ジャンパーピンを一本用意して下さい。 事務用のクリップをほぐしても使えることは使えますが、線が硬すぎるので、 端子を痛めてしまう可能性があります。 柔らかい金属線がお勧めです。

トライステート

ディジタル信号には High と Low の2つの状態があり、それぞれ電源電圧付 近と、0Vが割り当てられています。 マイコンの端子に対しても、これらのどちらかの電圧を与えると、それを読 み込むことができます。

ところでスイッチをマイコンの端子に接続することを考えて下さい。 スイッチは線がつながっているかつながってないかを切り替えるもので、自 ら電圧を生み出すものではありません。 そのため、一方をマイコンの端子につないで、もう一方をGND または電源に つなぐことで、スイッチを入れた際には、Low または High を入力すること ができます。 ところが、 スイッチを切った状態ではマイコンに何が入力されるのでしょうか? スイッチを切った場合、スイッチからは電圧が入力されません。 スイッチ自体は接続と逆の状態となっていますが、これを ハイインピーダンス と呼びます。 このような状態の時、スイッチを入れた時と反対の電圧を入力させるための 定石があります。 それは、スイッチの手前を抵抗を介して電源や、GNDにつなぐことです。 スイッチの先には抵抗を繋がず直接電源やGNDにつないでいるため、 スイッチをつないだ場合は、スイッチの手前に抵抗がつながっている限りそれは影響せず、スイッチの先と電圧は一致します。 一方で、スイッチを切った場合、スイッチの先と関係なくなるので、マイコ ンの入力端子は抵抗を介して電源やGNDにつながることになりますが、マイ コンとしてはその電圧を認識するようになります。


プルアップ

スイッチの先にGNDがある場合、手前を抵抗を介して電源につなぐことで、 スイッチを入れると Low, 切ると High になります。 このようなハイインピーダンス時に High になるように電圧をかけること をプルアップと呼び、そのための抵抗をプルアップ抵抗と呼ぶ こともあります。 逆に抵抗を介してGNDに落とすことをプルダウンと呼び、その抵 抗をプルダウン抵抗と呼びます。 なお、プルアップ抵抗にふさわしい抵抗値ですが、抵抗値が大きければ大き いほど、スイッチが入っている時の消費電流を抑えることができます。 一方で、スイッチは金属でできているのですが、接点に酸化皮膜ができると 接触不良を起こすことがあります。 そのため、ある程度の電流を流すことで、酸化皮膜に影響を受けにくくする ことができます。 そのため、数mA流れるようにすることが推奨されます。 5Vの電源の場合、1kΩで 5mA 流れるので、1kから4.7kΩ程度の抵抗で、よ く使うものを決めておけば良いでしょう。 電源が3.3Vの場合なども考えると、スイッチは 1kΩでプルアップを定石と しても良いでしょう。

ところが、近年のマイコンは入力端子をプルアップできます。 ATMega328P は入力端子を設定する際に、プルアップも同時に設定できます。 ArduinoのC++言語ではpinMode(x, INPUT_PULLUP)で、x番の ポートを入力に設定すると共に、プルアップします。 x番に何も繋がなければ High、GND と接続すれば Low になります。

例3-2

次のプログラムは、LED を点灯させますが、4番とGNDを線でつなぐと LED が消灯します。


#define INPORT 4
void setup() {
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(INPORT,INPUT_PULLUP);

}
void loop() {
  digitalWrite(LED_BUILTIN,digitalRead(INPORT));
  delay(1);
}

3-3. まとめの演習

演習3-1

Blink.ino のプログラムを改造して、点滅の速さが2倍になるようにしよう

演習3-2

Blink.ino のプログラムを改造して、点滅の速さが1/2倍になるようにしよう

演習3-3

与えた数だけ、0.1秒ずつLEDを点滅した後、1秒消灯する void nblink(byte n) 関数を作りなさい。 また、 次のプログラムと結合して正常に動作することを確かめなさい。


void nblink(byte n);
void setup(){}
void loop(){
  static byte i=0;
  nblink(++i);
  i %= 4;
}

演習3-4

初期化の時に数を与え、blink()メソッドを呼ぶと、与えた数だけ、0.1秒ずつLEDを点滅した後、1秒消灯するクラス Led を作りなさい。 また、 次のプログラムと結合して正常に動作することを確かめなさい。


#include "Led.h"
Led* list[]={new Led(3), new Led(3), new Led(7)};
void setup(){}
void loop(){
  static byte i=0;
  list[i++]->blink();
  i %= sizeof(list)/sizeof(list[0]);
}

演習3-5

volatile 宣言を外すと動きがどう変化するか確かめよう。

まずvolatile 宣言の演習をしてみましょう。 スケッチ例の Blink のプログラムの delayを、 自作の1秒待つ関数 mydelay で置き換えよう。 以下のプログラムで1秒点灯するように ??? の部分に数字を入れよう。

mydelay

void setup(){
  pinMode(LED_BUILTIN,OUTPUT);    
}
void mydelay(){
for(volatile long i=0; i< ???; i++){
  }
}
void loop(){
  digitalWrite(LED_BUILTIN, HIGH);
  mydelay();
  delay(10);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

次に、上記のvolatileを外して、動作を確かめよう。

演習3-6

4番とGNDをつないだときだけ、LED が点灯するプログラムを作りなさい。

演習3-7

4番とGNDをつないだときは、1秒おきに2回LEDが点滅、つないでない時は 1秒おきに短くLEDが点滅するプログラムを作りなさい。

演習3-8

4番とGNDをつないだときは、1秒おきにLEDが1回点滅、2回点滅、3回点滅、 1回点滅,2回点滅、3回点滅と繰り返すが、 つないでない時は同じ回数の点滅を繰り返すプログラムを作りなさい。 つまり、3回点滅の時に線を外せば、そのまま3回点滅を繰り返し、 1回点滅の時に線を外すと1回点滅を繰り返すようにしなさい。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科