CとCPP - kentakozuka/yetos GitHub Wiki
一次式
-
識別子
-
定数
-
文字列リテラル
-
式を
()
で囲んだもの -
式に対して演算子を適用したり、演算子で式と式をつなぎ合わせたものも式
-
すべての式は型を持つ
-
式がどこかの記憶領域を意味している場合は左辺式(lvalue, locator value)
-
式が単なる値を意味している場合は右辺式
- 識別子(変数名または関数名)からスタート
- 識別子に近いほうから優先順位に従って派生型を解釈する。優先順位は次の順;
- 宣言をまとめるための
()
- 配列の
[]
、関数の()
- ポインタの
*
- 型指定子(左端にある
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言語 ポインタ完全制
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では多次元配列は配列の配列
- サイズは派生元の型のサイズ
- 関数型からはポインタ型以外は派生できない
- 配列型から関数型を派生できない
型名 | 概要 |
---|---|
基本型 | 処理系依存 |
ポインタ | 処理系依存 |
配列 | 派生元の型のサイズ * 配列の要素数 |
関数 | サイズは計算できない |
-
operator
-
式に対して何らかの演算を行い、その結果をの式を返す記号
-
&
: アドレス演算子 -
*
: 間接演算子 -
[]
: 添字演算子
int *foo_p;
このときの *
は区切り子であり、間接演算子ではない。
- それが指し示す変数の全体の場所を示す
- 普通の変数と同様、メモリ上に配置
- 「ポインタ型の値」とはメモリアドレス
- ポインタがどんな型を指しているかはコンパイラが覚えている
- メモリサイズはCPUによって決まる。最近のCPUでは4バイトが多い。
- 実行時、ポインタ変数の実体はアドレスの値が入るだけ。
- コンパイル時は以下の情報も含む
- 領域の大きさ
- それが指し示すものの型
コンパイル時 | 実行時 |
---|---|
アドレス, 領域のサイズ, 実体の型 | アドレス |
- コンパイラはポインタがどんな型を指しているかを覚えている
- 1を加算するとそのポインタが指す型のサイズだけ増加される
- 式の中では配列は「その先頭要素へのポインタ」に読み替えられる
-
p[i]
は*(p + i)
のシンタックスシュガー
ai = arr[i];
は
-
arr
が先頭アドレスに指し -
arr + i
でポインタ演算が行われ -
*(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
; 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 演算子は動的にメモリを割り当てる
- OSのメモリ管理がないとできない
- libc++のnewは内部でmalloc()を呼んでいる
- 配置new (placement new, プレイスメントnew)は、インスタンスを特定のメモリアドレスに配置するための機能
- OSがなくても使える
- OSが入っていない組み込み機器でも簡単に使用できるツールが集まった標準ライブラリ。
- OSの機能に依存する部分(
malloc
やprintf
など)は別の関数に切り出されており、使用者が独自に実装することができる。
- 引数の型や個数が異なる同名の関数を定義できるように、元々の関数名に引数の型名(+α)をくっつけたシンボルに変換される
- 関数マングリングを防ぐには
extern "C"
を使う
- C++としてコンパイルするときに定義され、Cのコンパイルでは定義されない
#ifdef __cplusplus
// do something for only C++
#else
// do something else for C
#endif
- 列挙体の型名の前の
enum
- 構造体の型名の前の
struct
両方で使用する場合は、省略しない
構造体の各フィールドを詰めて配置するためのコンパイラ拡張。コンパイラは何も指定されていない場合、変数のアラインメントを守るためにフィールド間に隙間(パディング)を挿入する。しかし、ハードウェアの仕様で定まったデータ構造を構造体として表現するためには、コンパイラが勝手に隙間を挿入してはまずい。そのため、このコンパイラ拡張を指定して、隙間の挿入を防止する
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)