C LANGUAGE TECHNOLOGY

【C言語】ポインタとは

悩んでいる人

C言語のポインタを教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOSの授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校コンピュータサイエンス学部2021年の世界大学学術ランキングで20位)で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発
  • プログラミング歴15年以上,習得している言語: C/C++Solidity,Java,Python,Ruby,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開

こういった私から学べます.

ポインタ

ポインタとは,変数や関数等が置かれたメモリ上のアドレスにアクセスするための機能です.

C言語は,OSを開発するためのプログラミング言語として作られたので,アドレスを操作するような低レベルな演算が可能です.

ポインタを利用すると,アドレスを利用して間接的にメモリ中の変数や関数等にアクセスできます.

ポインタ変数

ポインタ変数とは,変数や関数等が配置されているアドレスを格納できる変数のことです.

そして,そのアドレスを元に各種の演算を行います.

ポインタ演算子の使い方

int型の変数のアドレスを格納できる変数(int型のポインタ変数)は,ポインタ演算子を利用して以下のように定義します.

ここで,注意してほしいのは,ポインタ変数はpであり,*pではないということです.

*pという変数は存在しません.

つまり,ポインタ変数pは,int *型という変数だと思って下さい.

その意味では,以下のように「int* p;」と書いた方がわかりやすいかもしれません.(C++言語では,この書き方が一般的です.)

しかし,C言語では上記の書き方「int *p;」が一般的ですので,こちらの書き方を採用します.

また,複数のポインタ変数を定義する時に以下のように書くのは間違いです.

これは,int型のポインタ変数paと,普通のint型の変数pb,pcが定義されたことになります.

正しくは,以下のように書きます.

ポインタ関連の演算子は下表になります.

演算子意味
&変数が格納されているアドレス
*ポインタが指すアドレスの内容

例えば,以下のように定義されているとします.

3行目で「p = &b;」とbのアドレスをpに代入しています.

ポインタ変数pがbのアドレスを保持することを「ポインタpは変数bを指している」と言い,pに保持されている変数bのアドレスを元に変数bにアクセスできます.

このことを「ポインタによる間接参照」と言います.

また,4行目のように書くとpが保持しているアドレスの内容を変数aに代入します.

変数名pの前にポインタ演算子*を付けると,「pが保持しているアドレスの内容」を意味します.

また,以下のように書くと,pが指す内容(変数bの値)が10になります.

このように,ポインタを利用すると,アドレスを利用して間接的に他の変数にアクセスできます.

ポインタ変数を利用するコード

ポインタ変数を利用するコードは以下になります.

ここで,uintptr_tは,inttypes.hでtypedefで定義されているポインタを格納するために十分な領域を確保する符号なし整数型です.

このコードでは,まず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とすると保持しているアドレスの内容を参照すること

実行結果は以下になります.

※アドレスの値は実行毎に変動します.

ポインタと関数の引数:値渡しと参照渡しの違いをswap関数で解説

C言語では,関数の引数は値渡し(call by value)なので,呼ばれた関数で仮引数を変更しても呼んだ関数側で実引数を変更することはできません.

しかし,ポインタを利用してアドレスを参照渡し(call by reference)することで,間接的に呼び出し側の実引数を変更できます.

つまり,呼ばれた関数側で渡されたアドレスを元に,呼んだ関数側の変数を参照します.

値渡しと参照渡しの違いをswap関数のコードを例として紹介します.

値渡しのswap関数とポインタ渡しのswap2関数を作成し,関数の動作の違いを調べます.

swap関数では,実引数a,bが仮引数a,bにコピーされ,swap関数内の仮引数の値はaとbが交換されます.

しかし,main関数内のa,bの値は交換されません.

これに対して,swap2関数では,19行目の「swap2(&a, &b);」で引数をポインタで渡しているので,swap2関数にmain関数の変数a,bのアドレスを渡します.

これはポインタ渡しです.

呼ぶ関数側では,変数の前の&を忘れないようにして下さい.

また,この場合の関数の動作は以下のフェーズで実行します.

  1. aのアドレス&a番地をpaに渡し,bのアドレス&b番地をpbに渡す.
  2. tmpにpa(&a番地)が指している内容(0)を代入する.
  3. pa(&a番地)が指す記憶域にpb(&b番地)が指している内容(1)を代入する.
  4. pb(&b番地)が指す記憶域にtmp(0)を代入する.

このように,ポインタを利用することで,呼び出し側の関数(main関数)の変数a,bの値を交換できました.

実行結果は以下になります.

swap関数は正しく値を交換できていませんが,swap2関数は正しく値を交換できていることがわかります.

swap関数とswapマクロの詳細を知りたいあなたは,こちらからどうぞ.

C言語 swap
【C言語】swap関数とswapマクロで値を交換

こういった悩みにお答えします. こういった私から学べます. 目次1 swap2 swap関数2.1 int型のswap関数2.2 double型のswap関数3 swapマクロ3.1 データ型を引数で ...

続きを見る

ポインタと配列

C言語では,配列とポインタは非常に密接な関係です.

配列のデータは連続したアドレスを持っているので,ポインタを配列のあるアドレスに設定すると,そのポインタを操作(インクリメントや各種演算等)して配列の任意のデータにアクセスできます.

ポインタ変数と配列の使い方

例えば,以下のようにポインタ変数と配列が定義されているとします.

ここで,文法的に配列名は配列の先頭アドレスを示します.

したがって,以下のように書くとpに配列aの先頭アドレス(つまり&a[0])を代入します.

※もちろん,「p = &a[0];」と書いても文法上は正しいですが,通常は単に配列名のみを利用します.

このとき,以下のように書くとa[0]に0を代入できます.

また,以下のように書くとa[1]に1を代入できます.

int型のサイズは32ビット(4バイト)ですが,ポインタ演算では自動的に変数のサイズを考慮してアドレスを変化させます.

ここで,pがA番地を指しているとすると,p+1はA + 1番地ではなくA + 4番地を指すので,正しいアドレスに値を代入できます.

ポインタ変数は,配列と同じ形式で利用できます.

以下のように書いてもa[1]に1を代入できます.

ポインタを利用すると,部分配列を容易に作成できます.

例えば,以下のようにすると,p[0]はa[2],p[1]はa[3],p[2]はa[4]になる部分配列を実現できます.

ポインタと配列を利用するコード

ポインタと配列を利用するコードは以下になります.

このコードでは,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]を同時に表示して,部分配列であることを確かめます.

実行結果は以下になります.

ポインタと文字列

ポインタと文字列の関係について紹介します.

C言語では,C++言語のstd::stringクラスやPython言語のstringクラスのような文字列型はありません.

文字列は,char型の配列(文字配列)として表現します.

文字列の最後は必ず'\0'(NULL文字,char型の1バイトの0)で終わらなければなりません.

これに対して,'\0'が複数存在してもよい(つまり文字列が複数個含まれてもよい)文字の羅列を文字列リテラルと呼びます.

例えば,以下の"Hello"は文字列かつ文字列リテラルです.

"Hello"のoの後には自動で’\0’が追加されます.

以下の"HELLO\0Hello\0hello"は文字列リテラルです.

次に,文字配列と文字列へのポインタの違いを学びましょう.

以下のように書くと文字配列を作成できます.

この場合,配列の大きさは「5文字+‘\0’の1文字 =6」がコンパイラにより自動で計算され,大きさ6の文字配列に各々の値が代入されます.

つまり,以下のようになります.

これに対して,以下のように書くと,システムが用意した領域に”Hello”という文字列が格納され,その先頭ポインタがポインタ変数pに代入されます.

関数内の変数はポインタ変数のみしかありません.

文字列が格納されているシステムが用意した領域に書き込むことは,文法的には禁じられていることに注意して下さい.

この理由として,この領域は読み込み専用の領域(ROM等)でも構わないという文法的なルールがあるからです.

コードを作成している場合には,その領域はメモリ中に取られるので書き込みできる場合が多いですが,移植性を考慮する場合は書き込みをしない方が無難です.

もし書き込みしたい場合は,文字配列を利用しましょう.

ポインタと文字列の関係を理解するコードは以下になります.

このコードは,文字列リテラルを文字配列aに代入し,ポインタpを指すところを変化させ,出力結果がどうなるかを確かめることです.

同時に,システムが用意した領域に文字列を用意し,その先頭ポインタをポインタ変数bに代入しています.

文字列リテラルaを格納するアドレスと,ポインタbが指している文字列を格納するアドレスが異なることに注意して下さい.

また,各種方法でアドレスを指定するようにしています.

実行結果は以下になります.

※アドレスは実行毎に変動します.

ポインタと多次元配列

C言語では,2次元以上の多次元配列と,多次元配列に対応するポインタを作成できます.

次元分だけ大カッコ[]を書くと,その次元の多重配列を定義できます.

例えば,3*4のint型の2次元配列aは以下のように定義します.

多次元配列では,添字は右から左へ(この例では[4]から[3]へ)変化していきます.

また,この2次元配列を指すことができるポインタは以下のように定義します.

 [4]は後ろ側の添字(a[3][4]の[4])を表します.

例えば,「int a[3][10];」のの2次元配列aを指すことができるポインタは以下のように定義します.

つまり,C言語では添字は一番右から変化します.

後ろ側の大きささえわかれば,(コンパイラは)ポインタを1変化させた時のアドレスの変化量が計算できます.

また,以下のように定義すると,int型のポインタ変数の要素数が4の配列になってしまい,意味が異なるので注意して下さい.

多次元配列のコードは以下になります.

実行結果は以下になります.

ポインタとmain関数の引数:argc,argv,envp

argc,argvの書式

main関数は,引数をとることができます.

main関数の引数とは,実行ファイルに与えるコマンドラインからの引数に対応します.

書式は以下のようになります.

argcとargvという変数名は,歴史的にこの名前が利用されています.

他の変数名でも可能ですが,この名前を利用した方が良いでしょう.

argcにはコマンド名を含めた引数の数が自動的に設定されます.

argvは,引数が格納されている文字列の配列へのポインタです.

argv[0]には,コマンド名が格納されている文字列へのポインタが自動的に設定され,argv[1]にはコマンドラインの第1引数が格納されている文字列へのポインタが設定されます.

同様にして,argv[argc - 1]には,コマンドラインの最後の引数へのポインタが設定されます.

例えば,コマンドラインから以下のように入力した場合を考えます.

この場合,argcは3,argv[0]は"a.out",argv[1]は"arg",argv[2]は"arg2"になります.

注意事項をまとめると,以下になります.

  • argc:コマンド名も含む数
  • argv[0]:コマンド名の文字列へのポインタ
  • argv[1]:第1引数へのポインタ
  • argv[argc – 1]:最後の引数へのポインタ

argc,argvを利用するコード

argcとargvを利用するコードは以下になります.

実行結果は以下になります.

argc,argv,envpの書式

main関数は,さらにもう1つ引数をとることができ,OSから環境変数を取得できます.

この場合の書式は以下になります.

envpという変数名も歴史的にこの名前が利用されています.

他の変数名でも可能ですが,この名前を利用して下さい.

envpは,環境変数が格納されている文字列の配列へのポインタです.

envp[0]には,最初の環境変数が格納されている文字列へのポインタが設定され,envp[1]には2番目の環境変数が格納されている文字列へのポインタが設定されます.

同様にして,envp[i]がNULLポインタになるまで環境変数が設定されます.

※NULLポインタはポインタ型(void *)の0であり,末尾を示す意味等で利用されます.

envpを利用するコード

envpを利用するコードは以下になります.

実行結果は以下になります.

あなたの実行環境毎に表示される値が変動しますので,確認してみましょう!

関数ポインタ

関数ポインタは関数のアドレスを指すポインタのことです.

変数と同様に,関数もメモリ上に配置されて実行するので,アドレスを持っています.

C言語では,関数のアドレスを格納できるポインタ変数も定義できます.

そのポインタ変数を利用して関数を間接的に呼び出したり,関数の引数に渡すことができます.

関数ポインタの使い方

例えば,int型の引数を1つとり,int型を返す関数へのポインタ変数pfは以下のように定義します.

この変数pfに「int func(int);」とプロトタイプ宣言されている関数のアドレスを代入するためには,以下のように書きます.

このように,関数ポインタにより,関数を変数のように扱うことができます.

ポインタ変数pfを利用してfunc関数を引数に3を渡して実行するためには,以下のように書きます.

これは,以下と同様です.

ここで,以下のように書くと戻り値がint *型のpf関数のプロトタイプ宣言になってしまいますので注意して下さい.

関数ポインタを利用するコード

関数ポインタを利用するコードは以下になります.

このコードでは,sum関数の第1引数がint型の関数へのポインタになっています.

sum関数は,関数へのポインタを受け取り,第2引数をその関数の引数として実行し,その値を返しています.

実際には,このような回りくどい呼び出し方をする必要はありません.

このコードは関数ポインタの利用例だと考えて下さい.

実行結果は以下になります.

関数ポインタの実例

関数ポインタのLinuxカーネルにおける実例を紹介します.

kernel/sched/sched.hのsched_class構造体の定義は以下になります.

sched_class構造体のメンバとして多くの関数ポインタがあることがわかります.

kernel/sched/core.cのpick_next_task関数でsched_class構造体を利用しています.

4行目でsched_class構造体のポインタ変数classを定義し,33行目で「p = class->pick_next_task(rq);」と関数ポインタで呼び出しています.

Linuxカーネルを学びたいあなたは,こちらからどうぞ.

Linuxカーネル
元東大教員から学ぶLinuxカーネル

こういった私から学べます. Linuxカーネルとは,C言語で開発されたオープンソースのOSです. Linuxカーネルは主に以下のコンピュータで広く利用されています. スーパーコンピュータ サーバ An ...

続きを見る

まとめ

C言語のポインタを紹介しました.

具体的には,ポインタ変数,ポインタ演算子,関数の引数,配列,文字列,多次元配列,main関数の引数(argc,argv,envp),関数ポインタを解説しました.

ポインタはC言語で最も難しい機能の1つですので,何度も読み直して確実に習得しましょう.

コンピュータの本質がわかります!

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

友だち追加

独学が難しいあなたは,C言語を学べるおすすめのオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , , , , , , , , , , , , ,