【備忘録】詳説 Cポインタ

はじめに

この記事は「詳説 Cポインタ」を読んで
忘れそうなことや重要だったことのメモです。

自分用のメモなのでかなり読みにくいかもしれませんが
ご了承ください。

詳しく学びたい方はぜひご購入ください。

www.amazon.co.jp




本の全体像

この本ではポインタについて徹底的に掘り下げられています。



第1章 ポインタの復習


  • メモリは3つに分類される
    • 静的(static) / 大域(Global)
      • プログラム開始時に割り当てられる
      • プログラム終了まで維持
    • 自動
      • 関数の中で定義される
      • 関数が実行されている間のみ存在する
    • 動的
      • ヒープから割り当てられる
      • 解放されるまで存在する


  • null
    • NULLマクロの定義 #define NULL ((void *)0)


  • voidへのポインタ
    • charへのポインタと同じ表現、メモリ上の並び
    • 基本的に他のポインタと等しくなることはない
      • 2つのvoidへのポインタがNULLのとき等しいとされる
    • sizeof(void *)は問題ないが、sizeof(void)は不正


  • ポインタの大きさは、コンピュータの種類とコンパイラによって決まる


  • const
    • 定数へのポインタ(const int *)
      • 間接参照(*x)はOK
      • 間接参照で値変更はNG
      • しかしポインタ自身は定数ではないため書き換えることができる


変数名 ポインタ
int O -
int * O O
const int X -
const int * O X
int *const (定数ポインタ) X O
const int *const (定数への定数ポインタ) X X

(変更できる: O , 変更できない : X)

  • const int * = int const *どちらでも可



第2章 C言語の動的メモリ管理


  • malloc
    • 引数が0の時は処理系依存(nullポインタや0バイトのメモリを返すなど)
    • キャストするべきか
      • voidへのポインタはあらゆるポインタ型に代入できるためキャストは必要ない
      • しかし意図を明示することやキャストが必要なコンパイラとの互換性のためにキャストすべきという意見も


  • 解放済みポインタにはNULLを代入し、ぶら下がりポインタをつくらない


  • プログラム終了時のメモリ解放
    • OSによってはメモリをOSの責任で管理するものと、プログラムの責任で管理するものがある
    • すべてのメモリを解放するとプログラムや実行時間が長くなったり、バグが増えるなどのデメリットもある
    • OSの責任でメモリを管理する場合は、メモリを解放するメリットとして再利用することがあげられる
      • プログラム終了後はOSがリソースを回収するため、メモリを解放する理由はあまりない



第3章 ポインタと関数


  • 使う場面
    • 関数へのポインタ渡し
    • 関数のポインタ


参考 : https://www.wake-mob.jp/2020/11/blog-post.html


  • 定数へのポインタを関数の引数に渡すことで、関数の中でデータ変更することを許容しない


  • 関数ポインタ
    • 関数のアドレスを持っているポインタ
    • 関数の名前自身は関数のアドレスとして評価される
    • 関数ポインタは別の型の関数ポインタにキャストできる
      • ただし適切な引数が与えられているか確認しないため慎重に行うこと

名前自身がアドレスとして評価されている


  • 関数ポインタで遊んでみる ( おまけ )
#include <stdio.h>

typedef int (*operation)(int, int);
static operation operations[128] = {NULL};

int    add(int n1, int n2){return (n1 + n2);}
int    sub(int n1, int n2){return (n1 - n2);}

void   initOperations()
{
    operations['+'] = add;
    operations['-'] = sub;
}

int    calc(char opcode, int n1, int n2)
{
    operation f = operations[opcode];
    return f(n1, n2);
}

int    main()
{
    operation operations[128] = {NULL};
    initOperations(operations);

    int n1 = 4;
    int n2 = 2;

    printf("%d\n", calc('+', n1, n2));
    printf("%d\n", calc('-', n1, n2));

    return (0);
}


足し算と引き算を計算できる、シンプルなプログラム

関数ポインタを使うことで計算の切り替えを綺麗に書ける



第4章 ポインタと配列


2次元配列のイメージ

int matrix[2][3] = {{1,2,3},{4,5,6}}; のとき...

index ポインタ
matrix[0][0] 1 0x7fffd297af20
matrix[0][1] 2 0x7fffd297af24
matrix[0][2] 3 0x7fffd297af28
matrix[1][0] 4 0x7fffd297af2c
matrix[1][1] 5 0x7fffd297af30
matrix[1][2] 6 0x7fffd297af34

上記の例は 連続的であるが

メモリが連続することは保証できない


たいていの場合連続になるが、ヒープの状態に依存する


(例)

(const int) { 100 }
(int[3]) { 1,2,3 }


  • ジャグ配列
    • 各行が異なる列数で構成される2次元配列の一種

(例)

int (*(arr2[])) = {
    (int[]) {0,1,2,3},
    (int[]) {4,5},
    (int[]) {6,7,8},
};



第5章 ポインタと文字列


  • C言語の文字列は2種類ある
    • バイト文字列 (char)
    • ワイド文字列 (wchar_t)
      • 16または32ビットの文字であるワイド文字のための型


  • 文字列定数 (文字列リテラル)
    • 基本的にリテラルプールに割り当てられる (プログラムが必要とするメモリを減らす)
    • リテラルプールは文字列を構成する文字を格納しておく場所


コンパイラによって、文字列定数が変更される場合がある

#include <stdio.h>

int main(void){
    char *x = "AAA";
    *x = 'B';
    printf("%s\n",x); // コンパイラによってはBAAとでる
    return (0);
}


const を付けて定数にすると安全

const char *x = "AAA";


文字列の領域について

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void){
    char *a1 = "42";
    char *a2 = "42";

    char *b1 = (char *)malloc(sizeof(char) * strlen("42"));
    char *b2 = (char *)malloc(sizeof(char) * strlen("42"));

    char c1[] = "42";
    char c2[] = "42";

    printf("a1 = %p, a2 = %p\n",a1,a2); // リテラルプール
    printf("b1 = %p, b2 = %p\n",b1,b2); // ヒープ
    printf("c1 = %p, c2 = %p\n",c1,c2); // スタック
    return (0);
}


実行結果

a1 = 0x55dfb685d004, a2 = 0x55dfb685d004
b1 = 0x55dfb81b62a0, b2 = 0x55dfb81b62c0
c1 = 0x7ffedc9952e2, c2 = 0x7ffedc9952e5


上から順に

リテラルプール, ヒープ , スタック

の位置で確保される


領域のイメージ



第6章 構造体とポインタ


  • 構造体用メモリの割り当てと解法を繰り返すと、オーバーヘッドが生じパフォーマンスが悪くなる場合がある
    • 保存場所を用意することでオーバーヘッドを小さくできる


本記事には載せませんが, リンクリストの図が非常にわかりやすかったです



第7章 セキュリティの問題と不適切なポインタの使用


  • アドレス空間配置のランダム化 (ASLR)
    • メモリのデータをランダムに配置 (コード、スタック、ヒープ)



  • マクロ宣言よりtypedefのほうが良い


  • ポインタの初期化忘れの確認
    • assert(p!=NULL)がおすすめ
    • 条件式が偽だった場合プログラムが終了する



  • DoS攻撃 (2種類ある)
    • フラッド攻撃 (大量のリクエスト)
    • サービスの脆弱性を利用して例外処理を引き起こす ( segmentation faultなど )
      • これにより権限を奪取できるわけではないが、プログラムやサーバーの速度が低下する可能性がある


  • 境界付きポインタ
    • 有効な範囲でだけ使用できるポインタ
    • 配列の要素以外をアクセスできないように条件分岐で指定


  • 配列はメモリ上連続した領域に割り当てられることが保証されている
  • しかし、構造体は上記が保証されていないためポインタの算術演算は行うべきではない


  • シグネチャ (引数の数や型の順序などの組み合わせ)が異なる関数のアドレスを関数ポインタに入れたらだめ


  • たいていのOSはプログラム終了時にメモリを0で上書きなどの処理はしない
  • したがって解放する前にmemsetなどでデータをクリアにするべき



第8章 残りの話題



(例)

0x12345678 の場合

78から格納するのがリトルエンディアン

12から格納するのがビックエンディアン


  • 型パンニング
    • ある方から別の型に変換する際に、unionなどを使って型システムを壊す手法

参考 : http://www.itsenka.com/contents/development/c/union.html


  • restrict
    • コンパイラにその変数の別名が存在しないことを伝える
    • ポインタの参照先をキャッシュし、より効率的なコードになる (コンパイラによる)
    • 別名を持つポインタを使った場合の実行結果は未定義


  • スレッド間でポインタを共有する場合はミューテックスを用いてデータの保護をする


以下の記事が参考になりました

minus9d.hatenablog.com


参考 : https://medium-company.com/%E3%83%9D%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%95%E3%82%A3%E3%82%BA%E3%83%A0/


以下の記事が参考になりました

admarimoin.hatenablog.com



感想


1章から6章はポインタの基礎+αという感じでした。

網羅的に一度学びなおしたい人には良いかと思います。


また6章ではリンクリストやキュー、スタックなどの実装の説明がありました。

上記の実装の勉強をしたい方は是非読んでみましょう


7章のセキュリティ関連の話題も面白かったですが

具体的な攻撃の方法が書かれていなかったのが残念でした...


ほかの本と比べ、より専門的な内容となっていますが

ポインタをより詳しく知りたい方は是非読んでみてください。