C言語のポインタを教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
ポインタとは
ポインタとは,変数や関数等が置かれたメモリ上のアドレスにアクセスするための機能です.
C言語は,OSを開発するためのプログラミング言語として作られたので,アドレスを操作するような低レベルな演算が可能です.
ポインタを利用すると,アドレスを利用して間接的にメモリ中の変数や関数等にアクセスできます.
C言語でポインタは難しいと言われていますが,本記事を何度も読み込むことでポインタの本質が理解できます!
ポインタ変数
ポインタ変数とは,変数や関数等が配置されているアドレスを格納できる変数のことです.
そして,そのアドレスを元に各種の演算を行います.
ポインタ演算子の使い方
int型の変数のアドレスを格納できる変数(int型のポインタ変数)は,ポインタ演算子「*」(アスタリスク)を利用して以下のように定義します.
1 |
int *p; |
ここで,ポインタ変数はpであり,*pではないことに注意して下さい.
*pという変数は存在しません.
つまり,ポインタ変数pは,int *型という変数だと思って下さい.
その意味では,以下のように「int* p;」と書いた方がわかりやすいかもしれません.(C++言語では,この書き方が一般的です.)
しかし,C言語では上記の書き方「int *p;」が一般的ですので,こちらの書き方を採用します.
1 |
int* p; |
また,複数のポインタ変数を定義する時に以下のように書くのは間違いです.
これは,int型のポインタ変数paと,通常のint型の変数pb,pcが定義されたことになります.
1 |
int *pa, pb, pc; /* NG */ |
正しくは,以下のように書きます.
1 |
int *pa, *pb, *pc; /* OK */ |
ポインタ関連の演算子は下表になります.
演算子 | 意味 |
---|---|
& | 変数が格納されているアドレス |
* | ポインタが指すアドレスの内容 |
例えば,以下のように定義されているとします.
1 2 3 4 |
int a, b, *p; b = 1; p = &b; a = *p; |
3行目で「p = &b;」とbのアドレスをpに代入しています.
ポインタ変数pがbのアドレスを保持することを「ポインタpは変数bを指している」と言い,pに保持されている変数bのアドレスを元に変数bにアクセスできます.
このことを「ポインタによる間接参照」と言います.
また,4行目のように書くとpが保持しているアドレスの内容を変数aに代入します.
変数pの前にポインタ演算子*を付けた「*p」は,「pが保持しているアドレスの内容」を意味します.
また,以下のように書くと,pが指す内容(変数bの値)が10になります.
このように,ポインタを利用すると,アドレスを利用して間接的に他の変数にアクセスできます.
1 |
*p = 10; |
ポインタ変数を利用するコード
ポインタ変数を利用するコードは以下になります.
ここで,uintptr_tは,inttypes.hでtypedefで定義されているポインタを格納するために十分な領域を確保する符号なし整数型です.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <inttypes.h> int main(void) { int a, b, *p; b = 1; p = &b; a = *p; printf("&a = 0x%016lx, &b = 0x%016lx, &p = 0x%016lx\n", (uintptr_t) &a, (uintptr_t) &b, (uintptr_t) &p); printf("a = %2d, b = %2d, p = 0x%016lx\n", a, b, (uintptr_t) p); *p = 10; printf("a = %2d, b = %2d, p = 0x%016lx\n", a, b, (uintptr_t) p); p = &a; *p = 0; printf("a = %2d, b = %2d, p = 0x%016lx\n", a, b, (uintptr_t) p); return 0; } |
このコードでは,まずint型の変数a,bとint型へのポインタ変数pを定義しています.
変数bには1を代入し,ポインタ変数pにはその変数bのアドレスを代入しています.
変数aには,ポインタ変数pの指す内容,つまり変数bの内容である1が代入されます.
そして,変数a,b,pの各アドレスを16進数で表示してから,変数a,b,pの中身をそれぞれ表示しています.
printf関数のフォーマット指定子には「%016lx」と書きます.
※%016lx:0は上位ビットを0埋め,16は16進数の数値(1個4ビット)* 16個 = 64ビット,lxはlong型の数値を16進数で表示するという意味です.
次に,ポインタ変数pを利用して間接的にbの中身を10に書き換え,各変数を表示しています.
最後に,ポインタ変数pに変数aのアドレスを代入し,そのポインタ変数pの指す内容(変数a)を0にして各々の変数を表示しています.
ポインタを扱う時には,以下のことに注意して下さい.
- あくまでポインタ変数はpであって*pではないこと(*pという変数は存在しない)
- ポインタ変数pはアドレスを保持すること
- *pとすると保持しているアドレスの内容を参照すること
実行結果は以下になります.
※アドレスの値は実行毎に変動します.
1 2 3 4 5 6 |
$ gcc pointer.c $ a.out &a = 0x00007ffdd11be618, &b = 0x00007ffdd11be61c, &p = 0x00007ffdd11be620 a = 1, b = 1, p = 0x00007ffdd11be61c a = 1, b = 10, p = 0x00007ffdd11be61c a = 0, b = 10, p = 0x00007ffdd11be618 |
ポインタと関数の引数:値渡しと参照渡しの違いをswap関数で解説【ポインタのメリットを理解】
C言語では,関数の引数は値渡し(call by value)なので,呼ばれた関数で仮引数を変更しても呼んだ関数側で実引数を変更することはできません.
しかし,ポインタを利用してアドレスを参照渡し(call by reference)することで,間接的に呼び出し側の実引数を変更できます.
つまり,呼ばれた関数側で渡されたアドレスを元に,呼んだ関数側の変数を参照します.
C言語では「参照渡し」と「ポインタ渡し」(call by pointer)は同じ意味で利用しますが,C++言語では「参照渡し」と「ポインタ渡し」は異なる意味になりますので注意して下さい.
このように,呼び出し側の実引数を変更できることが,ポインタのメリットになります.
例えば,Python言語では関数から複数の返り値を返すことができますが,C言語では関数から1つの返り値しか返すことはできません.
なので,関数から複数の値を返したい(関数内で複数の値を変更したい)場合は,ポインタを利用することになります.
値渡しと参照渡しの違いをswap関数のコードを例として紹介します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> void swap(int a, int b); void swap2(int *pa, int *pb); int main(void) { int a = 0, b = 1; printf("a = %d, b = %d\n", a, b); swap(a, b); printf("swap: a = %d, b = %d\n", a, b); swap2(&a, &b); printf("swap2: a = %d, b = %d\n", a, b); return 0; } void swap(int a, int b) { int tmp; tmp = a; a = b; b = tmp; } void swap2(int *pa, int *pb) { int tmp; tmp = *pa; *pa = *pb; *pb = tmp; } |
値渡しのswap関数とポインタ渡しのswap2関数を作成し,関数の動作の違いを調べます.
swap関数では,実引数a,bが仮引数a,bにコピーされ,swap関数内の仮引数の値はaとbが交換されます.
しかし,main関数内のa,bの値は交換されません.
これに対して,swap2関数では,19行目の「swap2(&a, &b);」で引数をポインタで渡しているので,swap2関数にmain関数の変数a,bのアドレスを渡します.
これはポインタ渡しです.
呼ぶ関数側では,変数の前の&を忘れないようにして下さい.
また,この場合の関数の動作は以下のフェーズで実行します.
- aのアドレス&a番地をpaに渡し,bのアドレス&b番地をpbに渡す.
- tmpにpa(&a番地)が指している内容(0)を代入する.
- pa(&a番地)が指す記憶域にpb(&b番地)が指している内容(1)を代入する.
- pb(&b番地)が指す記憶域にtmp(0)を代入する.
このように,ポインタを利用することで,呼び出し側の関数(main関数)の変数a,bの値を交換できました.
実行結果は以下になります.
swap関数は正しく値を交換できていませんが,swap2関数は正しく値を交換できていることがわかります.
1 2 3 4 5 |
$ gcc swap.c $ a.out a = 0, b = 1 swap: a = 0, b = 1 swap2: a = 1, b = 0 |
swap関数とswapマクロの詳細を知りたいあなたはこちらからどうぞ.
ポインタと配列
C言語では,配列とポインタは非常に密接な関係にあります.
配列のデータは連続したアドレスを持っているので,ポインタを配列のあるアドレスに設定すると,そのポインタを操作(インクリメントや各種演算等)して配列の任意のデータにアクセスできます.
ポインタ変数と配列の使い方
例えば,以下のようにポインタ変数と配列が定義されているとします.
ここで,文法的に配列名は配列の先頭アドレスを示します.
1 |
int *p, a[10]; |
したがって,以下のように書くとpに配列aの先頭アドレス(つまり&a[0])を代入します.
1 |
p = a; |
※もちろん,「p = &a[0];」と書いても文法上は正しいですが,通常は単に配列名のみを利用します.
このとき,以下のように書くとa[0]に0を代入できます.
1 |
*p = 0; |
また,以下のように書くとa[1]に1を代入できます.
1 |
*(a + 1) = 1; |
私のPC環境ではint型のサイズは32ビット(4バイト)ですが,ポインタ演算では自動的に変数のサイズを考慮してアドレスを変化させます.
ここで,pがA番地を指しているとすると,p+1はA + 1番地ではなくA + 4番地を指すので,正しいアドレスに値を代入できます.
ポインタ変数は,配列と同じ形式で利用できます.
以下のように書いてもa[1]に1を代入できます.
1 |
p[1] = 1; |
ポインタを利用すると,部分配列を容易に作成できます.
例えば,以下のようにすると,p[0]はa[2],p[1]はa[3],p[2]はa[4]になる部分配列を実現できます.
1 |
p = &a[2]; |
ポインタと配列を利用するコード
ポインタと配列を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define MAX 10 #define OFFSET 2 int main(void) { int *p, a[MAX]; int i; p = a; for (i = 0; i < MAX; i++) { *(p + i) = i; } for (i = 0; i < MAX; i++) { printf("a[%d] = %d\n", i, a[i]); } putchar('\n'); for (i = 0; i < MAX; i++) { p[i] = MAX - 1 - i; } for (i = 0; i < MAX; i++) { printf("a[%d] = %d\n", i, a[i]); } putchar('\n'); p = &a[OFFSET]; for (i = 0; i < MAX - OFFSET; i++) { printf("p[%d] = %d, a[%d] = %d\n", i, p[i], i + OFFSET, a[i + OFFSET]); } return 0; } |
このコードでは,int型の配列aとポインタ変数を定義しています.
まず,ポインタ変数pを配列の先頭を指すように初期化し,そのpを利用して配列aの初期化を行います.
具体的には,a[0]には0が,a[1]には1が順に代入され,a[9]には9が代入されます.
その後,配列aの値を全て表示しています.
次に,ポインタ変数pを配列のように記述して,配列aに間接的に値を代入し,配列aの値を全て表示します.
最後に,ポインタ変数pに配列a[2]のアドレスを代入し,部分配列を作成します.
そして,ポインタ変数pを利用した配列aの部分配列(p[0]~p[7])と配列a[2]~a[9]を同時に表示して,部分配列であることを確かめます.
実行結果は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ gcc pointer_and_array.c $ a.out a[0] = 0 a[1] = 1 a[2] = 2 a[3] = 3 a[4] = 4 a[5] = 5 a[6] = 6 a[7] = 7 a[8] = 8 a[9] = 9 a[0] = 9 a[1] = 8 a[2] = 7 a[3] = 6 a[4] = 5 a[5] = 4 a[6] = 3 a[7] = 2 a[8] = 1 a[9] = 0 p[0] = 7, a[2] = 7 p[1] = 6, a[3] = 6 p[2] = 5, a[4] = 5 p[3] = 4, a[5] = 4 p[4] = 3, a[6] = 3 p[5] = 2, a[7] = 2 p[6] = 1, a[8] = 1 p[7] = 0, a[9] = 0 |
ポインタと文字列
ポインタと文字列の関係について紹介します.
C言語では,C++言語のstd::stringクラスやPython言語のstringクラスのような文字列型はありません.
文字列は,char型の配列(文字配列)として表現します.
文字列の最後は必ず'\0'(NULL文字,char型の1バイトの0)で終わらなければなりません.
これに対して,'\0'が複数存在してもよい(つまり文字列が複数個含まれてもよい)文字の羅列を文字列リテラルと呼びます.
例えば,以下の"Abc"は文字列かつ文字列リテラルです.
"Abc"のcの後には自動で’\0’が追加されます.
1 |
"Abc" |
以下の"ABC\0Abc\0abc"は文字列リテラルです.
1 |
"ABC\0Abc\0abc" |
次に,文字配列と文字列へのポインタの違いを学びましょう.
以下のように書くと文字配列を作成できます.
この場合,配列の大きさは「3文字+‘\0’の1文字 =4」がコンパイラにより自動で計算され,大きさ6の文字配列に各々の値が代入されます.
1 |
char a[] = "Abc"; |
つまり,以下のようになります.
1 2 3 4 |
a[0] = 'A'; a[1] = 'b'; a[2] = 'c'; a[3] = '\0'; |
これに対して,以下のように書くと,システムが用意した領域に”Abc”という文字列が格納され,その先頭ポインタがポインタ変数pに代入されます.
1 |
char *p = "Abc"; |
関数内の変数はポインタ変数のみしかありません.
文字列が格納されているシステムが用意した領域に書き込むことは,文法的には禁じられていることに注意して下さい.
この理由として,この領域は読み込み専用の領域(ROM等)でも構わないという文法的なルールがあるからです.
コードを作成している場合には,その領域はメモリ中に取られるので書き込みできる場合が多いですが,移植性を考慮する場合は書き込みをしない方が無難です.
もし書き込みしたい場合は,文字配列を利用しましょう.
ポインタと文字列の関係を理解するコードは以下になります.
このコードは,文字列リテラルを文字配列aに代入し,ポインタpを指すところを変化させ,出力結果がどうなるかを確かめることです.
同時に,システムが用意した領域に文字列を用意し,その先頭ポインタをポインタ変数bに代入しています.
文字列リテラルaを格納するアドレスと,ポインタbが指している文字列を格納するアドレスが異なることに注意して下さい.
また,各種方法でアドレスを指定するようにしています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <inttypes.h> int main(void) { char a[] = "ABC\0Abc\0abc"; char *b = "abc"; char *p; printf("a: %s, b: %s\n", a, b); printf("addr(a[0-3]) = 0x%016lx, 0x%016lx, 0x%016lx, 0x%016lx\n", (uintptr_t) a, (uintptr_t) &a[1], (uintptr_t) a + 2, (uintptr_t) &a[3]); printf("addr(b[0-3]) = 0x%016lx, 0x%016lx, 0x%016lx, 0x%016lx\n", (uintptr_t) b, (uintptr_t) &b[1], (uintptr_t) b + 2, (uintptr_t) &b[3]); p = a; /* A of ABC. */ printf("%s\n", p); p = &a[4]; /* A of Abc. */ printf("%s\n", p); p = &a[8]; /* a of abc. */ printf("%s\n", p); return 0; } |
実行結果は以下になります.
※アドレスは実行毎に変動します.
1 2 3 4 5 6 7 8 |
$ gcc pointer_and_string.c $ a.out a: ABC, b: abc addr(a[0-3]) = 0x00007ffc1c48ef0c, 0x00007ffc1c48ef0d, 0x00007ffc1c48ef0e, 0x00007ffc1c48ef0f addr(b[0-3]) = 0x000056075f910008, 0x000056075f910009, 0x000056075f91000a, 0x000056075f91000b ABC Abc abc |
ポインタと多次元配列
C言語では,2次元以上の多次元配列と,多次元配列に対応するポインタを作成できます.
次元分だけ大カッコ[]を書くと,その次元の多重配列を定義できます.
例えば,3*4のint型の2次元配列aは以下のように定義します.
1 |
int a[3][4]; |
多次元配列では,添字は右から左へ(この例では[4]から[3]へ)変化していきます.
また,この2次元配列を指すことができるポインタは以下のように定義します.
[4]は後ろ側の添字(a[3][4]の[4])を表します.
1 |
int (*p)[4]; |
例えば,「int a[3][10];」のの2次元配列aを指すことができるポインタは以下のように定義します.
1 |
int (*p)[10]; |
つまり,C言語では添字は一番右から変化します.
後ろ側の大きさがわかれば,(コンパイラは)ポインタを1変化させた時のアドレスの変化量が計算できます.
また,以下のように定義すると,int型のポインタ変数の要素数が4の配列になってしまい,意味が異なるので注意して下さい.
1 |
int *p[4]; |
多次元配列のコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define IMAX 3 #define JMAX 4 int main(void) { int a[IMAX][JMAX]; int (*p)[JMAX]; int i, j; for (i = 0; i < IMAX; i++) { for (j = 0; j < JMAX; j++) { a[i][j] = i + j; } } p = a; /* p points to &a[0][0]. */ for (i = 0; i < IMAX; i++) { for (j = 0; j < JMAX; j++) { /* output elements of a with p. */ printf("a[%d][%d] = %d\n", i, j, p[i][j]); } } putchar('\n'); p++; /* p points to &a[1][0]. */ for (i = 0; i < IMAX - 1; i++) { for (j = 0; j < JMAX; j++) { printf("p[%d][%d] = %d, a[%d][%d] = %d\n", i, j, p[i][j], i + 1, j, a[i + 1][j]); } } return 0; } |
実行結果は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ gcc multidimensional_array.c $ a.out a[0][0] = 0 a[0][1] = 1 a[0][2] = 2 a[0][3] = 3 a[1][0] = 1 a[1][1] = 2 a[1][2] = 3 a[1][3] = 4 a[2][0] = 2 a[2][1] = 3 a[2][2] = 4 a[2][3] = 5 p[0][0] = 1, a[1][0] = 1 p[0][1] = 2, a[1][1] = 2 p[0][2] = 3, a[1][2] = 3 p[0][3] = 4, a[1][3] = 4 p[1][0] = 2, a[2][0] = 2 p[1][1] = 3, a[2][1] = 3 p[1][2] = 4, a[2][2] = 4 p[1][3] = 5, a[2][3] = 5 |
ポインタとmain関数の引数:argc,argv,envp
argc,argvの書式
main関数は,引数をとることができます.
main関数の引数とは,実行ファイルに与えるコマンドラインからの引数に対応します.
書式は以下のようになります.
1 |
int main(int argc, char *argv[]) |
argcとargvという変数名は,歴史的にこの名前が利用されています.
他の変数名でも可能ですが,他人が読んだ時に理解しやすいので,argcとargvという名前を利用した方が良いでしょう.
argcとargvの意味は以下になります.
- argc:コマンド名を含めた引数の数が自動的に設定
- argv:引数が格納されている文字列の配列へのポインタ
argv[0]には,コマンド名が格納されている文字列へのポインタが自動的に設定され,argv[1]にはコマンドラインの第1引数が格納されている文字列へのポインタが設定されます.
同様にして,argv[argc - 1]には,コマンドラインの最後の引数(argc - 1番目の引数)が格納されている文字列へのポインタが設定されます.
例えば,コマンドラインから以下のように入力した場合を考えます.
この場合,argcは3,argv[0]は"a.out",argv[1]は"arg",argv[2]は"arg2"になります.
1 |
$ a.out arg arg2 |
注意事項をまとめると,以下になります.
- argc:(コマンド名も含む)引数の数
- argv[0]:コマンド名の文字列へのポインタ
- argv[1]:第1引数の文字列へのポインタ
- argv[argc – 1]:最後の引数(argc - 1番目の引数)の文字列へのポインタ
argc,argvを利用するコード
argcとargvを利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int main(int argc, char *argv[]) { int i; printf("argc = %d\n", argc); for (i = 0; i < argc; i++) { printf("argv[%d] = %s\n", i, argv[i]); } return 0; } |
実行結果は以下になります.
1 2 3 4 5 6 7 8 9 |
$ gcc argc_argv.c $ a.out argc = 1 argv[0] = a.out $ a.out arg arg2 argc = 3 argv[0] = a.out argv[1] = arg argv[2] = arg2 |
argc,argv,envpの書式
main関数は,さらにもう1つ引数をとることができ,OSから環境変数を取得できます.
この場合の書式は以下になります.
1 |
int main(int argc, char *argv[], char *envp[]) |
envpという変数名も歴史的にこの名前が利用されています.
他の変数名でも可能ですが,この名前を利用して下さい.
envpは,環境変数が格納されている文字列の配列へのポインタです.
envp[0]には,最初の環境変数が格納されている文字列へのポインタが設定され,envp[1]には2番目の環境変数が格納されている文字列へのポインタが設定されます.
同様にして,envp[i]がNULLポインタになるまで環境変数が設定されます.
※NULLポインタはポインタ型(void *)の0であり,末尾を示す意味等で利用されます.
envpを利用するコード
envpを利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> int main(int argc, char *argv[], char *envp[]) { int i; for (i = 0; envp[i] != NULL; i++) { printf("envp[%d] = %s\n", i, envp[i]); } return 0; } |
実行結果は以下になります.
あなたの実行環境毎に表示される値が変動しますので,確認してみましょう!
1 2 3 4 5 |
$ gcc envp.c $ a.out envp[0] = USER=XXX envp[1] = LOGNAME=XXX ... |
関数ポインタ
関数ポインタは関数のアドレスを指すポインタのことです.
変数と同様に,関数もメモリ上に配置されて実行するので,アドレスを持っています.
C言語では,関数のアドレスを格納できるポインタ変数も定義できます.
そのポインタ変数を利用して関数を間接的に呼び出したり,関数の引数に渡すことができます.
関数ポインタの使い方
例えば,unsigned int型の引数を1つとり,unsigned int型を返す関数へのポインタ変数pfは以下のように定義します.
1 |
unsigned int (*pf)(unsigned int); |
この変数pfに「unsigned int func(unsigned int);」とプロトタイプ宣言されている関数のアドレスを代入するためには,以下のように書きます.
1 |
pf = func; |
このように,関数ポインタにより,関数を変数のように扱うことができます.
ポインタ変数pfを利用してfunc関数を引数に3を渡して実行するためには,以下のように書きます.
1 |
(*pf)(3); |
これは,以下と同様です.
1 |
func(3); |
ここで,以下のように*pfの前後にある()がない場合,返り値がunsigned int *型のpf関数のプロトタイプ宣言になってしまいますので注意して下さい.
1 |
unsigned int *pf(unsigned int); |
関数ポインタを利用するコード
関数ポインタを利用するコードを紹介します.
以下のコードでは,sum関数の第1引数がunsigned int型の関数へのポインタになっています.
※関数の引数に渡す関数ポインタのことをコールバック関数(呼び出し返す関数)と呼びます.
sum関数は,関数へのポインタを受け取り,第2引数nをその関数の引数として実行し,1~nまでの和を返します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> unsigned int sum(unsigned int (*pf)(unsigned int), unsigned int n); unsigned int sum_for(unsigned int n); unsigned int sum_recursion(unsigned int n); int main(void) { unsigned int n; printf("Please input a natural number: "); scanf("%u", &n); printf("sum(%u) = %u\n", n, sum(sum_for, n)); printf("sum(%u) = %u\n", n, sum(sum_recursion, n)); return 0; } unsigned int sum(unsigned int (*pf)(unsigned int), unsigned int n) { return (*pf)(n); } unsigned int sum_for(unsigned int n) { unsigned int sum = 0; while (n > 0) { sum += n; n--; } return sum; } unsigned int sum_recursion(unsigned int n) { if (n == 0) { return 0; } else { return n + sum_recursion(n - 1); } } |
実行結果は以下になります.
1から3までの和が6と表示されていることがわかります.
1 2 3 4 5 |
$ gcc function_pointer.c $ a.out Please input a natural number: 3 sum(3) = 6 sum(3) = 6 |
関数ポインタの配列を利用して四則演算をするコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> double myadd(double a, double b); double mysub(double a, double b); double mymul(double a, double b); double mydiv(double a, double b); int main(void) { double a, b; double (*func[])(double a, double b) = {myadd, mysub, mymul, mydiv}; int nr_funcs = sizeof(func) / sizeof(func[0]); int i; printf("Please input two real numbers: "); scanf("%lf%lf", &a, &b); for (i = 0; i < nr_funcs; i++) { printf("%lf\n", (*func[i])(a, b)); } return 0; } double myadd(double a, double b) { return a + b; } double mysub(double a, double b) { return a - b; } double mymul(double a, double b) { return a * b; } double mydiv(double a, double b) { return a / b; } |
実行結果は以下になります.
1.2と3.4の四則演算の結果が表示されます.
1 2 3 4 5 6 7 |
$ gcc function_pointer2.c $ a.out Please input two real numbers: 1.2 3.4 4.600000 -2.200000 4.080000 0.352941 |
関数ポインタの実例【sched_class構造体】
関数ポインタのLinuxカーネルにおける実例を紹介します.
kernel/sched/sched.hのsched_class構造体の定義は以下になります.
sched_class構造体のメンバとして多くの関数ポインタがあることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
struct sched_class { #ifdef CONFIG_UCLAMP_TASK int uclamp_enabled; #endif void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); void (*yield_task) (struct rq *rq); bool (*yield_to_task)(struct rq *rq, struct task_struct *p); void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags); struct task_struct *(*pick_next_task)(struct rq *rq); void (*put_prev_task)(struct rq *rq, struct task_struct *p); void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first); #ifdef CONFIG_SMP int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf); int (*select_task_rq)(struct task_struct *p, int task_cpu, int flags); void (*migrate_task_rq)(struct task_struct *p, int new_cpu); void (*task_woken)(struct rq *this_rq, struct task_struct *task); void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask, u32 flags); void (*rq_online)(struct rq *rq); void (*rq_offline)(struct rq *rq); struct rq *(*find_lock_rq)(struct task_struct *p, struct rq *rq); #endif void (*task_tick)(struct rq *rq, struct task_struct *p, int queued); void (*task_fork)(struct task_struct *p); void (*task_dead)(struct task_struct *p); /* * The switched_from() call is allowed to drop rq->lock, therefore we * cannot assume the switched_from/switched_to pair is serialized by * rq->lock. They are however serialized by p->pi_lock. */ void (*switched_from)(struct rq *this_rq, struct task_struct *task); void (*switched_to) (struct rq *this_rq, struct task_struct *task); void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio); unsigned int (*get_rr_interval)(struct rq *rq, struct task_struct *task); void (*update_curr)(struct rq *rq); #define TASK_SET_GROUP 0 #define TASK_MOVE_GROUP 1 #ifdef CONFIG_FAIR_GROUP_SCHED void (*task_change_group)(struct task_struct *p, int type); #endif }; |
kernel/sched/core.cのpick_next_task関数でsched_class構造体を利用しています.
4行目でsched_class構造体のポインタ変数classを定義し,33行目で「p = class->pick_next_task(rq);」と関数ポインタで呼び出しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
static inline struct task_struct * pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { const struct sched_class *class; struct task_struct *p; /* * Optimization: we know that if all tasks are in the fair class we can * call that function directly, but only if the @prev task wasn't of a * higher scheduling class, because otherwise those lose the * opportunity to pull in more work from other CPUs. */ if (likely(prev->sched_class <= &fair_sched_class && rq->nr_running == rq->cfs.h_nr_running)) { p = pick_next_task_fair(rq, prev, rf); if (unlikely(p == RETRY_TASK)) goto restart; /* Assumes fair_sched_class->next == idle_sched_class */ if (!p) { put_prev_task(rq, prev); p = pick_next_task_idle(rq); } return p; } restart: put_prev_task_balance(rq, prev, rf); for_each_class(class) { p = class->pick_next_task(rq); if (p) return p; } /* The idle class should always have a runnable task: */ BUG(); } |
構造体を学びたいあなたはこちらからどうぞ.
Linuxカーネルを学びたいあなたは,こちらからどうぞ.
まとめ
C言語のポインタを紹介しました.
具体的には,ポインタ変数,ポインタ演算子,関数の引数,配列,文字列,多次元配列,main関数の引数(argc,argv,envp),関数ポインタを解説しました.
ポインタはC言語で最も難しい機能の1つですので,何度も読み直して確実に習得しましょう.
コンピュータの本質がわかります!
ポインタの使い方を深掘りしたいあなたは以下の記事がおすすめです!
- ポインタ(*):malloc/calloc/realloc/alloca関数と可変長配列で動的にメモリ確保
- ポインタ(*)と構造体:連結リストとは
- ポインタのポインタ(ダブルポインタ)(**)と構造体:CSVファイルの読み書き
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!