CとCPP - kentakozuka/yetos GitHub Wiki

Clang

一次式

  • 識別子

  • 定数

  • 文字列リテラル

  • 式を()で囲んだもの

  • 式に対して演算子を適用したり、演算子で式と式をつなぎ合わせたものも式

  • すべての式は型を持つ

  • 式がどこかの記憶領域を意味している場合は左辺式(lvalue, locator value)

  • 式が単なる値を意味している場合は右辺式

宣言/型名の読み方

  1. 識別子(変数名または関数名)からスタート
  2. 識別子に近いほうから優先順位に従って派生型を解釈する。優先順位は次の順;
  • 宣言をまとめるための()
  • 配列の[]、関数の()
  • ポインタの*
  1. 型指定子(左端にある int, doubleなど)を解釈する

型名は識別子の宣言から識別子を取り除く

Declaration of identifier English Declaration of type English
int foo; foo is int. int type of int
int foo[3]; foo is array[3] of int.
int foo[3][4]; foo is array[10] of array[4] of int.
int *foo[3]; foo is array[3] of pointer to int.
int (*foo)[3]; foo is pointer to array[3] of int. int (*)[3]; type of pointer to array[3] of int.
int foo(int a); foo is function(int a) returning int.
int (*foo)(int a); foo is pointer to function(int a) returning int. int (*)(int a); type of pointer to function(int a) returning int.

新・標準プログラマーズライブラリ C言語 ポインタ完全制

Cの型モデル

int (*foo[10])(int a);は「foo is array[10] of pointer to function(int a) returning int」において、

  • "int" は 基本型(basic type)
  • それ以外は派生型(derived type)

分類は3つ

  • オブジェクト型(char, int、配列、ポインタ、構造体など)
  • 関数型
  • 不完全型

派生型

3種類

  • ポインタ
  • 配列
  • 関数
ポインタ型派生
  • サイズは派生元の型のサイズ
配列型派生
  • Cでは多次元配列は配列の配列
  • サイズは派生元の型のサイズ
Screen Shot 2022-07-25 at 16 02 59
関数型派生
  • 関数型からはポインタ型以外は派生できない
  • 配列型から関数型を派生できない

サイズ

型名 概要
基本型 処理系依存
ポインタ 処理系依存
配列 派生元の型のサイズ * 配列の要素数
関数 サイズは計算できない

演算子

  • operator

  • 式に対して何らかの演算を行い、その結果をの式を返す記号

  • &: アドレス演算子

  • *: 間接演算子

  • []: 添字演算子

間接演算子と区切り子

int *foo_p;

このときの * は区切り子であり、間接演算子ではない。

ポインタ

  • それが指し示す変数の全体の場所を示す

ポインタ変数

  • 普通の変数と同様、メモリ上に配置
  • 「ポインタ型の値」とはメモリアドレス
  • ポインタがどんな型を指しているかはコンパイラが覚えている
  • メモリサイズはCPUによって決まる。最近のCPUでは4バイトが多い。
  • 実行時、ポインタ変数の実体はアドレスの値が入るだけ。
  • コンパイル時は以下の情報も含む
    • 領域の大きさ
    • それが指し示すものの型
コンパイル時 実行時
アドレス, 領域のサイズ, 実体の型 アドレス

ポインタ演算

  • コンパイラはポインタがどんな型を指しているかを覚えている
  • 1を加算するとそのポインタが指す型のサイズだけ増加される

ポインタと配列

  • 式の中では配列は「その先頭要素へのポインタ」に読み替えられる
  • p[i]*(p + i) のシンタックスシュガー
ai = arr[i];

  1. arr が先頭アドレスに指し
  2. arr + i でポインタ演算が行われ
  3. *(arr + i) で配列のi番目の要素の値が ai に入る
ポインタを使う方が配列をイテレートする速度が早い理由

現在ではコンパイラが最適化を行うため、差がでることはまずない

for (i = 0; i < max; i++) {
  // do something with arr[i]
}

ループ内にポインタ演算 (*(arr + i))が発生する

for (p = &arr[0]; p != &arr[max]; p++) {
  // do something with arr[p]
}

ループ内にポインタ演算は発生しない発生する

ポインタとアセンブリ言語

void foo() {
  int i = 42;
  int* p = &i;
  int r1 = *p;
  *p = 1;
  int r2 = i;
  uintptr_t addr = reinterpret_cast<uintptr_t>(p);
  int* q = reinterpret_cast<int*>(addr);
}
_Z3foov:
  push rbp
  mov rbp, rsp

  ; int i = 42;
  mov dword ptr [rbp - 4], 42

  ; int* p = &i;
  lea rax, [rbp -4]
  mov qword ptr [rbp - 16], rax

  ; int r1 = *p;
  mov rax, qword ptr [rbp - 16]
  mov ecx, dword ptr [rax]
  mov dword ptr [rbp - 20], ecx

  ; *p = 1;
  mov rax, qword ptr [rbp - 16]
  mov dword ptr [rax], 1

  ; int r2 = i;
  mov ecx, dword ptr [rbp -4]
  mov dword ptr [rbp - 24], ecx

  ; uintptr_t addr = reinterpret_cast<uintptr_t>(p);
  mov rax, qword ptr [rbp - 16]
  mov qword ptr [rpb - 32], rax

  ; int* q = reinterpret_cast<int*>(addr);
  mov rax, qword ptr [rbp - 32]
  mov qword ptr [rbp - 40], rax

  pop rbp
  ret
_Z3foov:
  push rbp
  mov rbp, rsp
Screen Shot 2022-07-13 at 12 26 08
  ; int i = 42;
  mov dword ptr [rbp - 4], 42
  • メモリアドレス [rbp - 4](変数 i) に 42 を書き込む
  • [xxx] をアドレス xxx へのメモリアクセス(インテル記法)
  • dword はメモリアドレスが4バイトを示す
  • ptrは [] がメモリアクセスだと明示する
  ; int* p = &i;
  lea rax, [rbp -4]
  mov qword ptr [rbp - 16], rax
  • rax = rbp -4
  • メモリアドレス [rbp - 16](変数 p) に rax を書き込む
  • qword はメモリアドレスが8バイトを示す
  ; int r1 = *p;
  mov rax, qword ptr [rbp - 16]
  mov ecx, dword ptr [rax]
  mov dword ptr [rbp - 20], ecx
  • p([rbp - 16])が指す先の値を読み、raxに書き込む
  • rax が指すメモリ領域から4バイトを読み込み、ecxに書き込む
  • ecx から r1([rbp - 20])に書き込む
  ; *p = 1;
  mov rax, qword ptr [rbp - 16]
  mov dword ptr [rax], 1
  ; int r2 = i;
  mov ecx, dword ptr [rbp -4]
  mov dword ptr [rbp - 24], ecx
  ; uintptr_t addr = reinterpret_cast<uintptr_t>(p);
  mov rax, qword ptr [rbp - 16]
  mov qword ptr [rpb - 32], rax
  ; int* q = reinterpret_cast<int*>(addr);
  mov rax, qword ptr [rbp - 32]
  mov qword ptr [rbp - 40], rax
  pop rbp
  ret
命令 説明 備考
mov a, b b から a に値をコピー
lea a, [b - c] メモリアドレスを計算した結果をレジスタに書き込む。a = b - c. Load Effective Address
  • ポインタ変数と普通の変数に違いはなく、どちらも mov 命令で読み書きするだけ
  • [] を使ったメモリアクセスを2回やるか、1回やるかの違い

new 演算子と配置 new

  • new 演算子は動的にメモリを割り当てる
  • OSのメモリ管理がないとできない
  • libc++のnewは内部でmalloc()を呼んでいる

配置new

  • 配置new (placement new, プレイスメントnew)は、インスタンスを特定のメモリアドレスに配置するための機能
  • OSがなくても使える

Newlib

  • OSが入っていない組み込み機器でも簡単に使用できるツールが集まった標準ライブラリ。
  • OSの機能に依存する部分(mallocprintf など)は別の関数に切り出されており、使用者が独自に実装することができる。

C++

関数マングリング

  • 引数の型や個数が異なる同名の関数を定義できるように、元々の関数名に引数の型名(+α)をくっつけたシンボルに変換される
  • 関数マングリングを防ぐには extern "C"を使う

CとC++を同時に使うときの注意点

__cplusplus マクロ

  • C++としてコンパイルするときに定義され、Cのコンパイルでは定義されない
#ifdef __cplusplus
// do something for only C++
#else
// do something else for C
#endif

C++で省略できる予約語

  • 列挙体の型名の前の enum
  • 構造体の型名の前の struct

両方で使用する場合は、省略しない

コンパイラ拡張

__attribute__((packed))

構造体の各フィールドを詰めて配置するためのコンパイラ拡張。コンパイラは何も指定されていない場合、変数のアラインメントを守るためにフィールド間に隙間(パディング)を挿入する。しかし、ハードウェアの仕様で定まったデータ構造を構造体として表現するためには、コンパイラが勝手に隙間を挿入してはまずい。そのため、このコンパイラ拡張を指定して、隙間の挿入を防止する

Cの歴史

UNIXというOSを開発するために作られた。(実際はCよりUNIXの方が先に作られた。)

  • 1969年: UNIXの開発が開始。初期のUNIXはアセンブリで書かれた。(by Ken Thompson, Dennis Ritchie, and others)
  • 1969年ごろ: Ken ThompsonがB言語を開発
  • 1971年: Dennis RitchieがBを改良してNB(New B)を開発。後のC言語。
  • 1973年: Ken ThompsonがUNIXをほぼすべてをCで書き直す
  • 1978年: K&R本の初版発行。
  • 1988年: ANSI C準拠の K&R本発行
  • 1989年: ANSI C が採択
  • 1990年: ANSI C が ISO の規格に採択(ISO-IEC 9899:1990)
  • 1993年: ANSI C が JIS の規格に採択(JIS X3010:1993)
⚠️ **GitHub.com Fallback** ⚠️