[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法ミス 1000入力

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

前回の続きです。

不出来なコードにscanfで1000を入力した場合は、以下のようになります。

coutでは格納されたデータはASCIIコードとして認識し、printfでは%dなので整数として認識しています。

%dは符号あり整数(8ビットなので-128以上127以下)です。入力値によってはマイナスになったりします。

2進数 11101000の符号あり整数への変換(マイナスと仮定)
1を減算 11100111
反転 00011000
10進数へ変換 24
したがって 2進数 11101000は-24

ASCIIコードは0から127までの128文字ですから、-24の場合は該当文字がありません。

結局メモリに格納された2進数をどのように解釈するか、どのデータ型として読み出すかで出力内容が変わってきます。

%sを%dとしてしまうのをエラーにしないと前回、今回のようなトラブルになりますし、いっそのことコンパイルエラーにしてもらいたいものです。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        scanf("%d",&c);
        // 正しくは
        // scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("%d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16f56f698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
1000
cout c: ?
printf c: -24
3

[C/C++] Cプログラミングの落とし穴 4.4 実引数 P68 scanf文法ミス 10000入力

フォーマット指定子を間違えた場合にプログラムがどういった挙動を示すのか検証しました。

scanf関数で%sと入力すべきところを%dとした場合、データ型がcharからintに変わるためデータサイズが増えてしまい、ややこしいことになります。

とりあえず図にまとめてみました。新・明解C言語に出てきそうな見た目です。

実はchar cの格納領域を1バイト確保するのは間違いで、末尾のヌル文字を考慮した2バイト確保が正解になります。本来はchar c[2]と書くべきですが、そのままにしています。

今回は10000を入力した場合を扱いましたが、100や1000を入れるとまた違った趣きの出力になります。気が向けば次回以降記事にします。

#include <cppstd.h>

int main() {
    int i;
    char c;

    cout << "iのアドレス: " << &i << endl;
    cout << "iのサイズ: " << to_string(sizeof i) << endl;

    cout << "cのアドレス: " << &c << endl;
    cout << "cのサイズ: " << to_string(sizeof c) << endl;

    for (i=0;i<5;i++){
        scanf("%d",&c);
        // 正しくは
        // scanf("%s",&c);

        cout << "cout c: " << c << endl;
        printf("printf c: %d\n",c);

        printf("printf i: %d\n",i);
       
    }
    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16f633698
iのサイズ: 4
cのアドレス: 
cのサイズ: 1
10000
cout c: 
printf c: 16
printf i: 39

[C/C++] Cプログラミングの落とし穴 3.8 演算子 P52

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

ビット演算子&で計算してみました。各桁のビット同士で論理積を算出します。

10と12をビットAND計算すると8になります。

#include <cppstd.h>

int main() {
    int x = 10;
    int y = 12;

    int z = 10 & 12;

    bitset<8> x_bs(x);
    bitset<8> y_bs(y);
    bitset<8> z_bs(z);

    cout << "x_bs = " << x_bs << endl; 
    cout << "y_bs = " << y_bs << endl; 
    cout << "z_bs = " << z_bs << endl; 
    cout << "z = " << z << endl;

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
x_bs = 00001010
y_bs = 00001100
z_bs = 00001000
z = 8

[C/C++] Cプログラミングの落とし穴 3.7 評価順序 P50

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

代入式にインクリメントを含めると予想外な挙動が見られます。インクリメントは代入式の後に単独で書くべきだとしています。

確かにそのようなコードを書いてみると、法則性がよくわからない結果になりました。

#include <cppstd.h>

int main() {
    int x[5] = {1,2,3,4,5};
    int y[3];

    int i = 0;
    while (i < 3){
        cout << "x[" << i << "] = " << x[i] << endl;
        y[i] = x[i];
        i++;
    }

    cout << "y[0] = " << y[0] << endl;
    cout << "y[1] = " << y[1] << endl; 
    cout << "y[2] = " << y[2] << endl; 

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
x[0] = 1
x[1] = 2
x[2] = 3
y[0] = 1
y[1] = 2
y[2] = 3
#include <cppstd.h>

int main() {
    int x[5] = {1,2,3,4,5};
    int y[3];

    int i = 1;
    while (i < 4){
        cout << "x[" << i << "] = " << x[i] << endl;
        y[i] = x[i++];
    }

    cout << "y[0] = " << y[0] << endl;
    cout << "y[1] = " << y[1] << endl; 
    cout << "y[2] = " << y[2] << endl; 

    return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
x[1] = 2
x[2] = 3
x[3] = 4
y[0] = 0
y[1] = 0
y[2] = 2

[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

参考サイト