😑

C++で詰まったことのメモ

2023/02/14に公開

C++にて、詰まったことをいろいろメモしていきます。
随時更新

最終更新 : 2023/09/20

見慣れない変数宣言

sdk周りのソースコードで出てきた。
以下のような形式で型 変数名(value)の形で初期化変数宣言をすることができる。

    int test01(234);
    float test02(2.45);
    std::cout << "test00 : " + std::to_string(test00) << std::endl;
    std::cout << "test01 : " + std::to_string(test01) << std::endl;
    

structのメモリ管理

sizeof()に構造体変数を渡すと、想定より大きな値が返されるることがある。
構造体は各メンバのための領域が連続して確保される。しかし、処理効率やエラー回避のためのパディングと呼ばれる隙間を空けて、構造体の領域が確保されることがある
このパディング領域を含めた領域のサイズを返すため、大きくなる。

struct{
	short s;
	int i;
} x;

この場合、shortで2byte、intで4byte使うため、処理系や環境によって異なるが64bitでメモリを管理する環境だとするなら以下のように64bitをまたぐように変数を管理すると色々問題が起こる。
https://www.mm2d.net/main/legacy/c/c-15.html

このメモリバス幅が意味していることは、 CPU がメモリから情報を読み出す場合 8Byte 単位で扱っているということで、 それより細かい単位でのアクセスはできないということになります (取り出すときの指定ポインタの精度が 8Byte 単位であるというイメージ)。
さて、 CPU にとってメモリアクセスというのは非常に遅い処理なので、できる限り避けたい処理です。 ところが、たとえばint型変数がメモリ上に以下のように配置されてしまった場合、 この変数の値を取り出すにはメモリアクセスが本来なら1回ですむはずが、2回必要になってしまいます。 またレジスタに格納する際、ビットシフトを行い結合させるなどの作業が必要となってきます。 (CPUによってはこのようなメモリ配置をしようとすると落ちてしまうものもあるらしいです。)
[引用] https://www.mm2d.net/main/legacy/c/c-15.html

そのため、メモリアクセスの配置方が決められている。


https://www.mm2d.net/main/legacy/c/c-15.html

Switch文の仕様

Switch文の中で変数定義・宣言をしようとすると、エラーを吐かれる。
参考
[C言語] switch文の中の変数の定義がエラーとなる

example1
#include <stdio.h>
 
int main(void) {
    int i;
    
    for (i = 0 ; i < 100 ; i++) {
        switch (i%2) {
            case 0:
                char msg_even[] = "EVEN";
                printf("%3d is %s\n", i, msg_even);
                break;
            case 1:
                char msg_odd[] = "ODD";
                printf("%3d is %s\n", i, msg_odd);
                break;
            default:
                printf("%3d is UNKNOWN\n", i);
                break;
        }
    }
    return 0;
}
  1. 原因
    C言語の場合、switch文が『一つのブロック・スコープ』であるため、スコープの途中で変数宣言を行うことができない。
    C++の場合、case文を『jumpラベル』として扱うため、case文に飛ぶ(jumpする)前に全ての変数の初期化が終わっていなければならない。
    [引用] https://www.chihayafuru.jp/tech/index.php/archives/2990

goto + ラベルについて
(第5章 飛んでいきな)[http://www7b.biglobe.ne.jp/~robe/cpphtml/html03/cpp03005.html]

よくわかっていないが、とにかくswtichの中ではそのままでは宣言できないこととなっている。
なので、どうしてもやりたい場合はswitchの中に{}を書いてブロックを作ることで回避できる。

改善版
#include <stdio.h>
 
int main(void) {
    int i;
    
    for (i = 0 ; i < 100 ; i++) {
        switch (i%2) {
            case 0:
                {
                    char msg[] = "EVEN";
                    printf("%3d is %s\n", i, msg);
                }
                break;
            case 1:
                {
                    char msg[] = "ODD";
                    printf("%3d is %s\n", i, msg);
                }
                break;
            default:
                printf("%3d is UNKNOWN\n", i);
                break;
        }
    }
    return 0;
}

以下のような例でこの事例にぶつかった
ss << で0でfillしたかったが、それが宣言扱いになってしまった。

example2
switch(clipDigit){
	case 0:
	{
	    ss << std::setw(2) << std::setfill('0') << clipCounter;
	    std::string s1(ss.str());
	    clipname = "C" + s1;
	}
	    break;
	case 1:
	{
	    ss << std::setw(3) << std::setfill('0') << clipCounter;
	    std::string s2(ss.str());
	    clipname = "C" + s2;
	}
	    break;

サイズと型

C言語では整数型のサイズが処理系定義になっているため、予期せぬエラーが発生する可能性がある。
C99から別途サイズ固定の型を追加された。
#include <cstdint>(C++標準)or stdint.h(C言語標準)のインクルード必要

cstdintの詳細について

<cstdint>ヘッダでは、ビット数が規定された整数型の別名、およびマクロを提供する。これらの機能は、std名前空間に属することを除いてC言語の標準ライブラリ<stdint.h>ヘッダと同じである。
[引用] https://cpprefjp.github.io/reference/cstdint.html

8bit 8bit
符号 あり なし
型名 int8_t uint8_t
備考 一般のsigned char相当 一般のunsigned char相当
16bit 16bit
符号 あり なし
型名 int16_t uint16_t
備考 一般のsigned short相当 一般のunsigned short相当
32bit 32bit
符号 あり なし
型名 int32_t uint32_t
備考 一般のsigned int相当 一般のunsigned int相当
64bit 64bit
符号 あり なし
型名 int64_t uint64_t
備考 一般のsigned long相当 一般のunsigned long相当

charは処理系定義

charがunsigned charかsigned charかはその処理系によってどっちかに決まる。

どっちか判断するには、 #include <limits.h>を使う。
#include <limits.h>には#defineで環境依存のものがマクロで書かれているみたいで、自分の環境では以下のようになった。

std::cout << "char min : " << CHAR_MIN << std::endl; //-128
std::cout << "char max : " << CHAR_MAX << std::endl; //127
std::cout << "char bit : " << CHAR_BIT << std::endl; //8

参考
char と int の変換キャストの留意点 - C++ プログラミング

24bit補数表現 整数/実数を扱いたい

FreeD1 protocolを扱いたい時にこの問題がおきた。
24bit & 補数表現整数を作りたいという場面

方法1

ビットフィールド宣言を使って行う方法

#include <cstdint>

// 24ビット符号付き整数を表現する構造体
struct Int24 {
	int32_t value : 24;
	//ビットフィールド宣言

	// コンストラクタ
	Int24(int32_t val) : value(val) {}

	// 暗黙の型変換
	operator int32_t() const {
		return value;
	}
};

// 使用例
Int24 a = -8388608; // -2^23
Int24 b = 8388607;  // 2^23 - 1
Int24 c = a + b;    // -1

ビットフィールドの宣言で使用できる型は_Bool、signed int、unsigned intです。この例ではunsigned intを使っていて、その日が猛暑日であったかどうかなどを1ビットで表せるようにしてあります。ビットフィールドのメンバーへのアクセスは、ビットフィールドではない構造体のメンバーと同じように “.”演算子を利用します。
[引用]https://atmarkit.itmedia.co.jp/ait/articles/1204/26/news140_2.html

方法2

camikura/touchdesigner-shotoku-vr-chop
上のgithubで行われていた方法
shotokuというVRクレーンシステムで使われているFreeD1のプロトコルのための実装となっている。

touchdesigner-shotoku-vr-chop/ShotokuVRCHOP.cpp
に実際の処理が書かれている。

hexToInt()が実際の処理定義部分

ShotokuVRCHOP.cpp
int hexToInt(unsigned char d1, unsigned char d2, unsigned char d3)
{
	int v = (
	
		_lrotl(d1, 24) & 0xff000000 |
		_lrotl(d2, 16) & 0x00ff0000 |
		_lrotl(d3, 8) & 0x0000ff00);
	
	return v / 0x100;
	//ここで0x100で割る意味は、最下位byteを単純に削るという意味。
	//最初から24bitをビットシフトで入れない理由は歩数表現の処理をint(32bit)にやらせるため
}
	
	
void readRotation(unsigned char data[29]) {
	auto rx = this->hexToInt(data[5], data[6], data[7]) / 32768.0 + this->rotate[0];
	auto ry = this->hexToInt(data[2], data[3], data[4]) / 32768.0 + this->rotate[1];
	auto rz = this->hexToInt(data[8], data[9], data[10]) / 32768.0 + this->rotate[2];

	this->chanValues[3] = rx;
	this->chanValues[4] = ry;
	this->chanValues[5] = rz;
}

C++において value/0x100 は、変数 value の値を16進数表記で表現した場合に、下位2桁を除いた上位桁の値を表します。具体的には、 0x100 は16進数で256に相当するため、この式は value の値を256で割り、整数除算(切り捨て)した結果を返します。つまり、 value の下位2桁を除いた上位桁の値を得ることができます。
例えば、 value が16進数表記で 0x123456 の場合、 value/0x100 は 0x1234(10進数表記で4660)となります。

バイナリから解析

何らかの通信プロトコルを生のバイナリから解析したい場面などがあると思う。

byte order

メモとして残しておく。
ビッグエンディアン・リトルエンディアンのなどのルール

IBM汎用機、SPARC、JAVAではビックエンディアン
インテル製CPUではリトルエンディアンを使っている模様。

どちらかを示すために、BOM(byte order mark)と呼ばれるコードをデータの先頭に負荷させる場合がある。
BOMは16進数で「FEFF」だとの並びだとビッグエンディアン、「FFFE」の並びだと、リトルエンディアンになっている。

Char*に数値として得られている場合(内部表現として記録)

つまり、unsigned char* binary = 0x51みたいな状態。
charの実態の内部表現として1byte記録されている場合。

以下のようにcoutのようなことをした場合はutf-8などの文字コードの場合、
7bitまでは ASCIIのルールに従うため
3のような出力がされる。

std::cout << "binary : "<< binary << std::endl;
	
binary : 3

このunsigned char(1byte)を使って、
複数のbyteから成る32bitのint(float)のようなものを作りたいときは

以下のように、bitシフトでずらしていけば作れる。

int32_t i_byte = i_1_byte + (i_2_byte << 8) + (i_3_byte << 16) + (i_4_byte << 24);

別な章で言っているbyteからfloatを作る奴はこのintを使えばできあがる。

255のような内部表現数値をcoutした場合

utf-8などを使っていた場合、この文字セットのルール上7bit(10進数でいう128)から、256すぎ(ちょっとあいまい)まで定義が存在しないため、
自分の環境だと\377のように出力された。
これは8進数で表すマーク\をつけて8進数で表示されているだけなので、露頭に迷わないこと。

Char*に文字として数値が得られている場合(not 内部表現として記録)

つまり、unsigned char* binary = "51"のような状態。
charの内部表現ではなく、文字列として1byteが記録されている。

coutのような標準出力で出した場合、51と出力される。
内部表現では「5」と「1」で構成されているため、ASCIIで表現できるなら0x35 0x31になっている。

この文字列表現を内部表現に変更するには以下のようなステップを踏む

unsigned char class_name::getUcharToBinaryUchar(unsigned char _byte){

unsigned char _i_byte = 0;
    for(int i = 0; i < strlen(_byte); i++){ //文字数だけ繰り返す
        if(i == (strlen(_byte)-1) ){//数字の1桁め
            
            _i_byte += _byte[i] - '0';
        }
        else{//数字の2,3桁め
            _i_byte += pow(10,(strlen(_byte) - i - 1)) * (_byte[i] - '0');
        }
        
    }
    return _i_byte;

これで、"51"51(10進数)になる。
ここではunsigned charでreturnするようにしているが、_i_byteの部分をintにしても成り立つ。
intに同じ数値が入る。

引数がC++のstd::stringの場合もほぼ同じだが載せておく。

unsigned char class name::getStringToBinaryUchar(std::string _string){
    
    const char* _byte = _string.c_str();
    
    unsigned char _i_byte = 0;
    for(int i = 0; i < strlen(_byte); i++){
        if(i == (strlen(_byte)-1) ){//1桁め
            
            _i_byte += _byte[i] - '0';
        }
        else{//2,3桁め

            _i_byte += pow(10,(strlen(_byte) - i - 1)) * (_byte[i] - '0');
        }
        
    }
    return _i_byte;
}

floatをbyteからつくる

色々方法があるのだろうが、共用体を使うと個人的には楽だった。

参考
浮動小数点数の内部表現を取得してみよう
浮動小数点型の数値はメモリ上でどのように格納されているのか

/* float型のデータサイズは32ビットなので、32ビット整数型( int )とコンビを組みます */
union { float f; int i; } a;
int i;
a.f = -2.5f;

printf( "%f ( %08X )\n", a.f, a.i );

/* ビットの列を表示します */
for( i = 31; i >= 0; i-- ){
printf( "%d", ( a.i >> i ) & 1 );
}
printf( "\n" );

/* 指数部( 1ビット )、指数部( 8ビット )、仮数部( 23ビット )を取り出します */
printf( "符号部 : %X\n", ( a.i >> 31 ) & 1 );
printf( "指数部 : %X\n", ( a.i >> 23 ) & 0xFF );
printf( "仮数部 : %X\n", a.i & 0x7FFFFF );
実行結果
-2.500000 ( C0200000 )
11000000001000000000000000000000
符号部 : 1
指数部 : 80
仮数部 : 200000

メモリまわり

memcpy

#include <string.h>のインクルードする必要がある。
配列や文字列の変数については初期化以外では=で代入(コピー)できない。
ただ、配列のコピーを行いたいときはある。
C言語が提供するmemcpy関数を使用すると配列を丸ごとコピーできる。
これは配列に限ったものではなく、メモリ領域を別のメモリ領域にコピーするという汎用的なメモリコピーに使用できる。

void* memcpy(void* addr1, const void* addr2, size_t len);
addr1 : コピー先の先頭アドレス
addr2 : コピー元の先頭アドレス
len : コピーするバイト数
return : addr1と同様

いわゆる浅いコピー(shallow copy)と言われるもの。配列や構造体自身は複製される一方、内部に持つポインタがさしている先の領域までは複製されない。つまり、コピー元とコピー先で同一のものを指し、共有してしまうという事が起きる。

header fileのリンクとstatic/externの関係

https://atmarkit.itmedia.co.jp/ait/articles/1404/11/news045_2.html

Discussion