[C/C++] Cプログラミングの落とし穴 3.6 数え上げと非対称な境界 P39

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

C言語以前のプログラミング言語(Fortran他)では配列の要素を示す添え字は1から始まりますが、C言語では0から始まります。

そのようなものと慣れ切っている我々とは異なり、当時の学者・実務家は相当混乱したようです。

以下のような1スタートのFor文を書いてしまい無限ループになるケースが結構あったみたいです。今のコンパイラではAbort trapエラーになります。

ではなぜC言語の添字は0スタートなのか。筆者によると引き算だけで要素数をカウントできるようにするためだそうです。

確かに0から9の配列の要素数を算出する式は9 – 0 + 1=10となり引き算の後に1を足す必要があります。0以上10未満という表現であれば10 – 0 = 10となり引き算だけで要素数を導き出せます。

まあこれはこれで最後の要素9と10未満の10に不一致が生じるというデメリットがあるのでどちらが優れているとは言えないと思います。コンピュータ目線で何かメリットがあるのであれば知りたいところです。

#include <cppstd.h>

int main() {
    int i, a[10];

    for (i = 1; i<=10; i++){
        a[i] = 0;
        cout << "i = " << i << endl;
    }

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
Abort trap: 6

[C/C++] Cプログラミングの落とし穴 3.5 ヌルポインタ P38(P99)

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

ヌルポインタが示す先にはヌル文字があるというわけではないという検証です。ヌルポインタが示す記憶領域へのアクセスは基本的には禁じられています。

#include <cppstd.h>

int main() {
    char *p;

    p = nullptr;
    printf("Location 0 contains %d\n", *p);

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
Segmentation fault: 11

[C/C++] Cプログラミングの落とし穴 4.5 外部の型のチェック P68

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

3.3 仮引数としての配列宣言 P37におけるP68への参照を確認しました。

externにより他のファイルで変数を使う場合はポインタと配列をきちんと区別すべきという内容です。同じファイル内のポインタや配列への自動変換のような補助はありません。

下記例ではextern helloポインタになっているため、”配列helloへのポインタ”格納アドレスへアクセスしようとしますが、そのようなものは存在しないのでBus errorになります。

このようなミスは初級者を脱したばかりの中級者がやってしまいそうな感じがします。

#include <cppstd.h>
#include <test2.h>

char hello[] = "hello";

int main() {

    show();

    return 0;
}
#include <cppstd.h>

// extern char hello[]; // 正しくはこちら
extern char *hello;

void show() {

    printf("%s\n",hello);

}
Bus error: 10

//正常であれば
hello

[C/C++] Cプログラミングの落とし穴 3.2 ポインタ P34 malloc

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

本の内容に従ってメモリ確保のコードを書いていたら、malloc関数で確保されるメモリ領域が引数で指定した通りにならないことが判明しました。

詳しく調べてはいませんが、引数が0以上であれば最低16バイトは確保されるようです。なおこれはclangでの検証結果であってgccなどではどうなっているのか不明です。

教科書等では収納するバイト数にヌル文字の1バイト分を加えた数を引数としましょうと教えているはずですが、実際は小さい数であればあまり意味がないということになります。

#include <cppstd.h>
#include <malloc/malloc.h>

int main() {
    char* s = "abc";
    char* t = "def";
    char* r1;
    char* r2;
    char* r3;

    cout << "strlen(s) " << to_string(strlen(s)) << endl;
    cout << "strlen(t) " << to_string(strlen(t)) << endl;

    r1 = (char*)malloc(strlen(s) + strlen(t) +1);
    r2 = (char*)malloc(0);
    r3 = (char*)malloc(10000);

    int size1 = malloc_size(r1); 
    int size2 = malloc_size(r2); 
    int size3 = malloc_size(r3);

    cout << "r1で確保されたサイズ " << to_string(size1) << endl;
    cout << "r2で確保されたサイズ " << to_string(size2) << endl;
    cout << "r3で確保されたサイズ " << to_string(size3) << endl;

    strcat(r1, s);
    strcat(r1, t);

    cout << "r1 " << r1 << endl;

    free(r1);
    free(r2);
    free(r3);

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
strlen(s) 3
strlen(t) 3
r1で確保されたサイズ 16
r2で確保されたサイズ 16
r3で確保されたサイズ 10240
r1 abcdef

[C/C++] Cプログラミングの落とし穴 1.5 文字列と文字定数 P10

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

この本ではC言語の特異性が論じられていてとても興味深いのですが、1.5もなかなかのインパクトでした。

単一引用符で1文字を囲むとASCIIコードになり、二重引用符で囲むと文字列へのポインタになるというのは目からウロコでした。string型は*が付いていなくてもポインタとしての性質を持つということになりますね。

今使っているclangコンパイラでは単一引用符で1文字を囲むとcharになり、複数文字を囲むとintつまりASCIIコードになります。

普段文字列を囲むときは二重引用符しか使いませんが、あえて単一引用符を使うと以下のようなコンパイルエラーになります。

int main() {
    string str1 = 'a';
    string str2 = 'abc';

    cout << "str1 " << str1 << endl;
    cout << "str2 " << str2 << endl;

    printf('a\n');

    return 0;
}
--------------------------------------------------
コンパイルエラー内容
--------------------------------------------------
src/test.cpp:4:12: error: no viable conversion from 'char' to 'std::string' (aka 'basic_string<char>')
    string str1 = 'a';
           ^      ~~~
src/test.cpp:5:12: error: no viable conversion from 'int' to 'std::string' (aka 'basic_string<char>')
    string str2 = 'abc';
           ^      ~~~~~
src/test.cpp:10:5: error: no matching function for call to 'printf'
    printf('a\n');
    ^~~~~~
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h:170:6: note: candidate function not viable: no known conversion from 'int' to 'const char *' for 1st argument
int      printf(const char * __restrict, ...) __printflike(1, 2);
         ^

[C/C++] Cプログラミングの落とし穴 1.4 整数定数 P9 番外 : 2進数への変換

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

本の内容とは関係ないのですが、C++で10進数から2進数へ変換してみました。

参考にしたDelftStackのレファレンスサイトが思いっきり間違っていたので驚きました。弘法も筆の誤りとはこのことでしょうか。

#include <cppstd.h>

string toBinary(int n)
{
    string r;
    while (n != 0){
        r = ( n % 2 == 0 ? "0" : "1" ) + r; // 参考サイトでは末尾に結合(正しくは先頭に結合)
        cout << "r: " << r.c_str() << endl;
        n /= 2;
    }
    return r;
}

int main() {
    int number = 50;
    
    string bin = toBinary(number);

    cout << "decimal: " << number << endl;
    cout << "binary : " << bin << endl;

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
r: 0
r: 10
r: 010
r: 0010
r: 10010
r: 110010
decimal: 50
binary : 110010

参考サイト

[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++] 変数のメモリアドレス検証他

[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