[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言語] 文字列のコピー ポインタ変数の加算

自製str_copy関数のwhile文をポインタ変数の加算で書くと高速化できました。

以下のコードをそれぞれ走らせ処理時間を測ったところ、リストのインデックス加算に比べて1.5倍の速さになります。

strcpy関数はポインタ変数の加算になっているようです。

#include <stdio.h>

char *str_copy(char *d, const char *s)
{
	char *t = d;

	while (*d++ = *s++)
		;
	return t;
}

int main(void)
{
	char str[128] = "ABC";
	char tmp[128] = "DEF";

	printf("str = \"%s\"\n", str);

	str_copy(str, tmp);

	puts("コピーしました。");
	printf("str = \"%s\"\n", str);

	return 0;
}
#include <stdio.h>

char *str_copy(char *d, const char *s)
{
	int i=0;

	while (d[i] = s[i])
		i++;
	return d;
}

int main(void)
{
	char str[128] = "ABC";
	char tmp[128] = "DEF";

	printf("str = \"%s\"\n", str);

	str_copy(str, tmp);

	puts("コピーしました。");
	printf("str = \"%s\"\n", str);

	return 0;
}
import subprocess,time,datetime

start = time.time()

proc = subprocess.run("list1106_mac2", shell=True, stdout= subprocess.PIPE, stderr = subprocess.PIPE)
print(proc.stdout.decode('UTF-8'))

# 処理時間算出
process_time = time.time() - start
td = datetime.timedelta(seconds = process_time)
dt_now = datetime.datetime.now()

print('ポインタ加算 処理終了 ' + str(td) + ' ' + str(dt_now))

start2 = time.time()

proc = subprocess.run("list1106a_mac2", shell=True, stdout= subprocess.PIPE, stderr = subprocess.PIPE)
print(proc.stdout.decode('UTF-8'))

# 処理時間算出
process_time2 = time.time() - start2
td2 = datetime.timedelta(seconds = process_time2)
dt_now2 = datetime.datetime.now()

print('インデックス加算 処理終了 ' + str(td2) + ' ' + str(dt_now2))
--------------------------------------------------

出力
--------------------------------------------------
str = "ABC"
コピーしました。
str = "DEF"
ポインタ加算 処理終了 0:00:00.007981 2021-07-20 14:05:55.569973

str = "ABC"
コピーしました。
str = "DEF"
インデックス加算 処理終了 0:00:00.012111 2021-07-20 14:05:55.582141

[C言語] ポインタ渡し

ここまで学んだ段階で昨年読み進めていた新明解・入門編に再び目を通しました。

やはり10章 ポインタの説明が難解だったのと、最初の間違い例の解決法としてポインタ渡しを使っているところで戸惑いました。

Pythonユーザーからすれば、関数内で新たな変数が生成しているのに戻り値になっていない、戻り値に相当するはずの変数のポインタが引数になっている、あたりで混乱します。まあC言語が元祖ですからこちらが認識を改める他ないです。

この仕組みを利用すれば、関数の戻り値は引数にポインタ変数を入れることで何個でも設定できます。Pythonでは戻り値が複数の場合はタプルになっており、そこから各要素を取り出す手間がかかります。C言語にはそれがないのでかなり便利です。

教本の内容については、前にも書きましたがポインタの説明に男性名が値(身長)で女性名がポインタという例えを用いるのは分かりにくすぎていかがなものかと思います。

教本のコード例を私でも理解できるように書き改めました。sum_diff関数の第3引数と第4引数はアスタリスクをintに寄せてポインタ変数として表記する方が分かりやすいです。

ただアスタリスクが前に来ると2つ以上同じ行で宣言できなくなります。2つ目以降がint型になります。

int* a,b ではaはポインタ変数になるが、bは整数の変数になる。
int *a,*b が文法的には正しい。

理屈では前につけるべきだと思いますが、仕様上後ろに付ける方が実装する上でトラブルになりにくいです。

以前の記事で先に宣言した変数のアドレスがなぜ後ろの方なのか理由が分からないと書きました。それは後の変数を若い方に割り当てることでメモリからあふれるのを防ぐためではないかと考えています。本当のところを知りたいです。

#include <stdio.h>

void sum_diff(int n1, int n2, int* sum, int* diff)
{
	*sum  = n1 + n2;
	*diff = (n1 > n2) ? n1 - n2 : n2 - n1;
}

int main(void)
{
	int na, nb;
	int wa = 0, sa = 0;

	puts("二つの整数を入力してください。");
	printf("整数A:");   scanf("%d", &na);
	printf("整数B:");   scanf("%d", &nb);

	sum_diff(na, nb, &wa, &sa);

	printf("和は%dで差は%dです。\n", wa, sa);

	printf("naのアドレス %p\n",&na);
	printf("nbのアドレス %p\n",&nb);
	printf("waのアドレス %p\n",&wa);
	printf("saのアドレス %p\n",&sa);

	return 0;
}
--------------------------------------------------

出力
--------------------------------------------------
二つの整数を入力してください。
整数A:20
整数B:10
和は30で差は10です。
naのアドレス 0x7ffeecf685fc
nbのアドレス 0x7ffeecf685f8
waのアドレス 0x7ffeecf685f4
saのアドレス 0x7ffeecf685f0

[C言語] ポインタの概念 その3 Pythonとの比較

数値を代入した場合の2変数のポインタの状態をPythonと比較しました。

C言語ではポインタのアドレスは変わらずxとyで異なるまま、一方Pythonでは同じアドレスになりました。

これが何を意味するのか、手掛かりがあれば調べたいところです。

#include <stdio.h>;
#include <stdlib.h>;

main()
{
int x = 1,y= 2;
int *ip;

    ip =&x;
    y = x;
 
    printf("xの値 %d\n",x);
    printf("yの値 %d\n",y);

    printf("xのアドレス %p\n",&x);
    printf("yのアドレス %p\n",&y);
    printf("*ipのアドレス %p\n",ip);
}
--------------------------------------------------

出力
--------------------------------------------------
xの値 1
yの値 1
xのアドレス 0x7ffeecb5967c
yのアドレス 0x7ffeecb59678
*ipのアドレス 0x7ffeecb5967c
>>> a=0
>>> b=3
>>> print(id(a))
140573474482448
>>> print(id(b))
140573474482544
>>> b=a
>>> print(id(b))
140573474482448
>>> print(id(a))
140573474482448

[C言語] ポインタの概念 その2 メモリ番地他 C89

前回の続きです。

xとyのアドレスが模式図の通りになっているか、以下のコードでそれぞれのアドレスを確認しました。

xのアドレスと*ipのアドレスは同じで図の正しいことが分かりました。yについてはポインタ変数はありませんが、4バイト若い番地が割り当てられています。

intのメモリ領域は4バイトなのでxとyは隙間なく並んでいます。気になることが一点。変数宣言はxが先なのになぜアドレスは後ろなのでしょう。*ipなしで宣言だけしても番地は変わらずでした。

#include <stdio.h>;

main()
{
int x = 1,y= 2;
int *ip;

    ip =&x;
    y = *ip;
    *ip = 0;

    printf("xのアドレス %p\n",&x);
    printf("yのアドレス %p\n",&y);
    printf("*ipのアドレス %p\n",ip);
}
--------------------------------------------------

出力
--------------------------------------------------
xのアドレス 0x7ffee4de267c
yのアドレス 0x7ffee4de2678
*ipのアドレス 0x7ffee4de267c

また、ISOのC90で禁止されているコード途中の変数宣言やC99から導入されたスラッシュ2つでのコメントアウトを試してみました。エラーにはなりませんでしたが、どちらにも警告がありました。

#include <stdio.h>;
#include <stdlib.h>;

main()
{
int x = 1,y= 2;
int *ip;

    ip =&x;
    y = *ip;
    *ip = 0;

    printf("xのアドレス %p\n",&x);
    printf("yのアドレス %p\n",&y);
    printf("*ipのアドレス %p\n\n",ip);

int* ipy; // ポインタの宣言

    ipy = &y;

    printf("yのアドレス %p\n",&y);
    printf("*ipyのアドレス %p\n",ipy);

}
--------------------------------------------------

出力
--------------------------------------------------
<警告内容のみ>
warning: ISO C90 forbids mixing declarations and code

warning: // comments are not allowed in this language