[C/C++] Cプログラミングの落とし穴 1.4 整数定数 P9 番外:シフト演算

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

本を読んでいるとシフト演算のことが思い浮かんだので寄り道します。

シフト演算が掛け算を工数の少ない足し算に変換できるため早く計算できるというのは一応理解できますが、どういった足し算が最良なのかを見出す工数が勘定されていません。

例えば10倍の場合は、2^3 = 8と2^1= 2なので8倍と2倍の和になります。これが10000倍だったら2のベキ乗数の組み合わせがどうなるのかすぐには分かりません(後述していますが組み合わせは簡単に分かります)。

とりあえず50倍にする場合の2のベキ乗数の組み合わせをC++のプログラムで算出してみました。

int main()
{   
    int x = 50;
    int x2, x3;
    int i2;
    bool finished = false; 

    int i = 0;
    vector<int> pow_list; 
    while(1){
        int num1 = std::pow(2, i);
        cout << "num1 " << num1 << endl;
        if (num1 > x){
            pow_list.push_back(i-1);
            x2 = x - pow(2,i-1);
            cout << "x2 " << x2 << endl;

            
            while(1){
                int num2 = std::pow(2, i2);
                cout << "num2 " << num2 << endl;
                if (num2 > x2){
                    pow_list.push_back(i2-1);
                    x3 = x2 - pow(2,i2-1);
                    cout << "x3 " << x3 << endl;

                    finished = true;
                    break;
                } else {
                    i2 += 1;
                }
            }
        } else {
            i += 1;
        }

        if (finished) {
            break;
        }

    }

    cout << "i " << i << endl;
    cout << "i2 " << i2 << endl;
}
--------------------------------------------------
出力
--------------------------------------------------
num1 1
num1 2
num1 4
num1 8
num1 16
num1 32
num1 64
x2 18
num2 1
num2 2
num2 4
num2 8
num2 16
num2 32
x3 2 // 50から2^5と2^4を引いた残り
i 6 // 2^6は64なので50から最初に引く数字は2^5=32
i2 5 // 次に50-32=18から2^4=16を引く

// したがって50の内訳は2^5+2^4+2^1

プログラムにより50 = 2^5 + 2^4 + 2^1であることが分かります。

さらに大きい数字の場合はどうやって算出するのでしょうか。

ここまで書いて、50の2進数が110010だから一目瞭然だということに気が付きました。大きな数字であっても2進数を見れば分かります。もっと早く気付くべきでした。

今度は2進数への変換が演算としてどうなっているのか気になってきました。

[C/C++] Cプログラミングの落とし穴 1.4 整数定数 P9

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

C言語では0で始まる整数は8進数として扱われます。0xで始まると16進数です。

int main()
{   
    int x = 010;
    int y = 50;

    int z = x + y;

    printf("z = %d\n",z);
}
--------------------------------------------------
出力
--------------------------------------------------
z = 58
int main()
{   
    int x = 0x1F;
    int y = 50;

    int z = x + y;

    printf("z = %d\n",z);
}
--------------------------------------------------
出力
--------------------------------------------------
z = 81

8進数というのは1byte= 8bitsでしか馴染みがないです。パーミッションでも8進数が使われている、という説明をしているサイトが多くありますが、10進数でも同じですから説明としては不十分だと思います。パーミッションは2進数であり上限は7である、というのが妥当ではないかと。

読み/書き/実行 = 可能/可能/不可 2進数 110 → 8進数 6、10進数 6
読み/書き/実行 = 可能/可能/可能 2進数 111 → 8進数 7、10進数 7


[C/C++] Cプログラミングの落とし穴 1.3 食いしん坊な語彙解析 P8

『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]

コンパイラはトークンを読み取る際、解釈可能な最大長のトークンを取り出そうとします。

例えば、z = y/*xのような式では、/*を一つのトークンとして認識するためコメントの始まりであると解釈します。

現在はインテリセンスが自動的にコメントとして解釈しコードの色を変えるのですぐにわかりますが、このまま強引にコンパイルしてもちゃんと指摘されます。

この式をコンパイラに正確に読み取らせるには、z = y/ *xのように/と*の間に半角スペースを挟むか、z = y/(*x)のようにカッコで囲みます。タブや改行を挟んでも問題なく動作します。

// 間違ったコード
int main()
{   
    int x;
    int y = 50;
    int* ptr;

    ptr = &x;
    *ptr = 20;

    auto z = y/*ptr;

    printf("z = %d\n",z);

}
--------------------------------------------------
出力
--------------------------------------------------
src/test.cpp:13:15: error: unterminated /* comment
    auto z = y/*ptr;
              ^
src/test.cpp:13:15: error: expected ';' at end of declaration
    auto z = y/*ptr;
              ^
              ;
src/test.cpp:17:2: error: expected '}'
}
 ^
src/test.cpp:5:1: note: to match this '{'
{   
^
3 errors generated.
// 正しいコード
int main()
{   
    int x;
    int y = 50;
    int* ptr;

    ptr = &x;
    *ptr = 20;

    auto z = y/ *ptr;
    // あるいは
    // auto z = y/(*ptr);

    printf("z = %d\n",z);

}
--------------------------------------------------
出力
--------------------------------------------------
z = 2

[C言語] レガシー書籍を読む

アプリ開発もひと段落したので、パソコン向けプログラミング言語の嚆矢とも言えるC言語の世界にまたどっぷり浸かりたいと思います。

K&Rは初版(1981年)、第2版(1989年)を単行本で持っていますが、新たに『Cプログラミングの落とし穴』(1990年)を350円で入手しました。ほとんど傷んでいないきれいな状態です。WEB公開されている『CプログラミングFAQ』と合わせてこれから読み進めていきます。

[C++] 変数のメモリアドレス検証他

[M1 Mac, Big Sur 11.6.7]

関数内外の変数がメモリのどの領域に収納されるのか検証しました。

関数外変数は静的領域、関数内変数は全てスタック領域でした。最終的にはOSが決めているようです。

newやvectorは一般的にはヒープ領域と説明されているので意外な結果でした。

vectorの最大要素数は4611686018427387903個です。int は4バイトなので

4611686018427387903*4 = 1.8446744e+19バイト = 1.8446744e+19*8 ビット

これは64ビットシステム 2^64 – 1の8倍に相当します。この出力値はIntel Mac RAM32GB(M1は8GB)でも同じだったのでメモリ容量に比例しているわけでもないです。

つまり実際のハードスペックを考慮しての実装上可能値ではなく、スタックオーバーフロー対策の参考にはなりません。机上の計算を再現するだけのmax_size関数が何のために存在しているのかよく分かりません。

#include <iostream>
#include <vector>

int num0;

int main()
{
    int num1;
    int num2 = 3;

    int *ary;
    ary = new int[5];

    std::vector<int> vec = {1,2,3};
    
    std::cout << "num0 " << &num0 << std::endl;
    std::cout << "num1 " << &num1 << std::endl;
    std::cout << "num2 " << &num2 << std::endl;
    std::cout << "ary  " << &ary << std::endl;
    std::cout << "vec  " << &vec << std::endl;
    std::cout << "vec.max_size() " << vec.max_size() << std::endl;
    
}
--------------------------------------------------
出力例
--------------------------------------------------
num0 0x1048e4128
num1 0x16b5236a8
num2 0x16b5236a4
ary  0x16b523698
vec  0x16b523680
vec.max_size() 4611686018427387903

[C++] vectorの挙動検証 erase

[M1 Mac, Big Sur 11.6.7, clang 13.0.0, NO IDE]

前回、前々回と同様の検証をerase関数で実施しました。

erase関数の場合は、メモリアドレスは削除箇所から前の要素は変わらず、削除箇所から後ろは前に詰める形になります。

イテレータの再設定は必要です。再設定しないとwhile文ではデータが格納されていると認識している旧endを延々と探すことになり、ループが止まりません。

#include <iostream>
#include <vector>

using std::to_string;

int main()
{
    std::vector<int> vec{ 1, 2, 3, 4, 5};
    std::vector<int>::iterator itr = vec.begin();
    
    // vectorの各要素のアドレスを出力
    std::cout << "vector" << std::endl;
    while (itr != vec.end())
    {
        std::cout << &(*itr) << std::endl;
        itr++;
    }

	// 3番目の要素を削除
	vec.erase(vec.begin() + 2);
	
	// イテレータ再設定
	std::vector<int>::iterator itr2 = vec.begin();
	
	// 再度アドレス出力
	std::cout << "vector削除後" << std::endl;
    while (itr2 != vec.end())
    {
        std::cout << &(*itr2) << std::endl;
        itr2++;
    }

	for (int num:vec)
    {
        std::cout << to_string(num) << std::endl;
    }
}
--------------------------------------------------
出力例
--------------------------------------------------
vector
0x13e6068c0
0x13e6068c4
0x13e6068c8
0x13e6068cc
0x13e6068d0

vector削除後
0x13e6068c0
0x13e6068c4
0x13e6068c8
0x13e6068cc
1
2
4
5

[C++] vectorの挙動検証 insert

[M1 Mac, Big Sur 11.6.7, clang 13.0.0, NO IDE]

前回と同様の検証をinsert関数で実施しました。

insertやeraseのようなイテレータが戻り値の関数では、これを受け取ればイテレータを再設定せずに済む場合があります。

先頭に挿入した場合だけ再設定不要で、それ以外の場合は再設定しないと全てのメモリアドレスを確認できません。

#include <iostream>
#include <vector>

using std::to_string;

int main()
{
    std::vector<int> vec{ 1, 2, 3, 4, 5};
    std::vector<int>::iterator itr = vec.begin();
    
    // vectorの各要素のアドレスを出力
    std::cout << "vector" << std::endl;
    while (itr != vec.end())
    {
        std::cout << &(*itr) << std::endl;
        itr++;
    }

	// 要素を挿入
	itr = vec.insert(vec.begin(),6);
	
	// 再度アドレス出力
	std::cout << "vector挿入後" << std::endl;
    while (itr != vec.end())
    {
        std::cout << &(*itr) << std::endl;
        itr++;
    }

	for (int num:vec)
    {
        std::cout << to_string(num) << std::endl;
    }

}
--------------------------------------------------
出力例
--------------------------------------------------
vector
0x1336068c0
0x1336068c4
0x1336068c8
0x1336068cc
0x1336068d0

vector挿入後
0x1336068e0
0x1336068e4
0x1336068e8
0x1336068ec
0x1336068f0
0x1336068f4
6
1
2
3
4
5

[C++] vectorの挙動検証 push_back

[M1 Mac, Big Sur 11.6.7, clang 13.0.0, NO IDE]

vectorの各要素のメモリアドレスおよび要素を追加したときのアドレスの変化をチェックしました。

push_back関数で要素を追加すると、16バイト先のメモリアドレスから再度要素を配置することがわかりました。イテレータを設定していた場合はそれが使えなくなるため、再度設定が必要になります。

#include <iostream>
#include <vector>

using std::to_string;

int main()
{
    std::vector<int> vec{ 1, 2, 3, 4, 5};
    std::vector<int>::iterator itr = vec.begin();
    
    // vector各要素のアドレスを出力
    std::cout << "vector" << std::endl;
    while (itr != vec.end())
    {
        std::cout << &(*itr) << std::endl;
        itr++;
    }

	// 要素を追加
	vec.push_back(6);
	// イテレータを再度設定
	std::vector<int>::iterator itr2 = vec.begin();

	// 再度アドレス出力
	std::cout << "vector要素追加後" << std::endl;
    while (itr2 != vec.end())
    {
        std::cout << &(*itr2) << std::endl;
        itr2++;
    }

	for (int num:vec)
    {
        std::cout << to_string(num) << std::endl;
    }
}
--------------------------------------------------
出力例
--------------------------------------------------
vector
0x1596068c0
0x1596068c4
0x1596068c8
0x1596068cc
0x1596068d0

vector要素追加後
0x1596068e0
0x1596068e4
0x1596068e8
0x1596068ec
0x1596068f0
0x1596068f4
1
2
3
4
5
6

[C++] 参考サイトの選別

C++を体系的にまとめたサイトには説明が不十分なものがあったりします。

std::vectorの説明でコンテナを変更するメンバ関数がpush_backしかないような書き方をしているサイトがあったので、Chromeの拡張機能uBlacklistで検索結果に表示できなくしました。insertやeraseなどを明記しなくても、他にも関連するメンバ関数があることに触れて欲しかったです。

cpprefjp – C++日本語リファレンスが辞書的なサイトとして優れているので、検索する前にまずそこで調べることをお勧めします。

このサイトのような学習過程を記録した個人ブログも中級以上の方には何の参考にもならないですから検索結果に出ないようにすべきでしょう。

cpprefjp – C++日本語リファレンス

[C++] プログラミング学習 2022年7月

アプリ製作を中心にC++を学んできましたが、どうしてもスキルに偏りが生じるため、教材に従って抜けを埋めていきます。

内容へのアクセス性を重視し、以下の教材を電子書籍ではなく単行本として持っています。

1.プログラミング入門 B.ストラウストラップ著:C++07以前のレガシーC++
2.プログラミング言語C++ B.ストラウストラップ著:C++11対応
3.ゲームプログラミングC++

これまで作ってきたアプリではstructやenumなどユーザー定義型のデータをほとんど使っていません。

今回の学習ではこの辺りを中心に学んでいくつもりです。