Lecture4 (2018/10/18)
みなさんへのプレゼント
授業中に詳細を話します。
ポインタの基本
こころを落ち着けて教科書の第5章を読みましょう。
第一ステップ (大多数の人が素直にクリアできる点)
復習
変数には型がある。型を使って、実際の変数を格納する領域の確保が行われる。
領域にはアドレスがある。
#include <stdio.h>
int main(void)
{
printf("int \t%d\n", (int) sizeof(int));
printf("long \t%d\n", (int) sizeof(long));
printf("short \t%d\n", (int) sizeof(short));
printf("long int \t%d\n", (int) sizeof(long int));
printf("float \t%d\n", (int) sizeof(float));
printf("double \t%d\n",(int) sizeof(double));
printf("char \t%d\n", (int) sizeof(char));
return 0;
}
そもそもアドレスとは
変数やプログラムはコンピュータのメモリ上に存在する。メモリにはアドレスという連続した番号がついている。
コンピュータは,CPU内の少量の記憶領域(レジスタ; 数個~100個程度)と,CPU外の記憶領域(メインメモリ,主記憶)を用いて計算を行う機械である.
主記憶には,プログラムが使うあらゆるデータが格納されている.
- ブラウザで開いている文書,デスクトップの絵,etc
- C言語の変数,配列,etc.
メモリ(主記憶)
- データやプログラムを記憶する装置
- 電源を切ると中身は消えるが高速に読み書きが可能
- 長く保存したいときはハードディスク(最近ではSSDなどのフラッシュメモリ)に記憶
ポインタとは
- ポインタは、他の変数のアドレスを内容とする変数である。
- ポインタの宣言
int *a;
- aはintへのポインタである
- 「*」をポインタに付けると、そのポインタが指すオブジェクト(つまりは中身)にアクセスできる
int x = 1;
int *ip;
ip = &x;
- 整数型の変数xのアドレスがint型のポインタであるipに代入される
- 「&」はオブジェクトのアドレスを与える演算子
- ポインタの足し算,引き算
- p + n は数字的には,[pが指すアドレス値]+ sizeof(*p) x n,という意味
- sizeof 演算子についてはこちら。p - q も,アドレスの実計算ではなく,[アドレス差]/sizeof(*p) である
- p + n は数字的には,[pが指すアドレス値]+ sizeof(*p) x n,という意味
- ポインタ変数は,1)アドレス値,2)そのアドレスから始まるデータがどの型(どれだけのバイト数を占めるのか)なのか,という情報を保有している
ポインタの使い道1 :
関数等に変数を渡して関数内部で状態を変更させて返したい。参照渡し(call by reference)と値渡し(call by value)
#include <stdio.h>
void swapbyval(int, int);
void swapbyref(int *, int *);
int main()
{
int a = 1, b = 2;
printf("a=%d, b=%d\n", a, b);
swapbyval(a, b);
printf("a=%d, b=%d\n", a, b);
swapbyref(&a, &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
void swapbyval(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
void swapbyref(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
ポインタの使い道2 : mallocによるメモリの確保
動的なメモリ確保.配列を宣言するとき要素数をあらかじめ想定しうる最大の値にしておく必要があった.これはさすがに使いにくい.必要になったときに必要な分だけの要素数を確保することができる.
#include <stdlib.h>
short *buf;
buf = (short *) malloc(sizeof(short)*2000);
buf[2] = 40;
(short *)はキャスト.
sizeof()は変数のサイズを返す関数.
使い終わったらメモリを解放する。
free(buf);
解放し終えてもプログラムが終了すれば自動的に解放される。しかし、バグの元なのでしっかり解放する癖をつけること。
その他便利な関数は以下.
- calloc()
- メモリを確保し,要素をすべて0に初期化する.
- realloc()
- 一度確保したメモリを違うサイズで確保し直す.
- memset()
- メモリの内容をすべて同じ値にする.
- memset(buf, 0, 5)
- 先頭アドレスbufから5バイト分を0で埋める
- memcpy()
- memcpy(dst, src, 5)
- srcのアドレスからdstに5バイト分コピー
- memcpy(dst, src, 5)
ポインタの使い道3 : 柔軟なデータ構造
構造体を連結したリストやツリーなど,様々なデータ構造を表現するにはポインタが不可欠.詳しくはソフト2をお楽しみに.
第二ステップ (聞いて必ず理解しないとヤバい点)
ポインタと配列の関係1: 配列名は最初の要素へのポインタ
int a[4];
と書いたとき,a
は&a[0]
(すなわちa[0]
のポインタ)と同値である.
char name[] = "EEIC";
printf("Hello %s", name);
と書いたと思う.このとき,printf()関数は第二引数に文字列へのポインタを求める.nameで"EEIC"の文字列へのポインタになっている.
ポインタと配列の関係2:ポインタを+1,-1することで要素へアクセスできる
int *pa, a[10];
とあった場合,pa = a;
と pa = &a[0];
は同じ意味。
*(pa+3)
と a[3]
は同じことを意味する
*(pa+i)
は a[i]
と同じことを意味する
pa==100
, だったとしましょう。pa+3
は 103 ではない
pa+3 == 100 + 3 x [int のバイト数] = 100 + 3 x [*p のバイト数] = 112
である
こういう芸当が何故できるのか?
それは,ポインタ変数が,単なるアドレスという整数型の値を格納する変数ではなく,その アドレスに「どれだけのバイト数を占める型のデータが格納されているのか?」まで知っているから.
*p の型の情報も保有しているからである.
ポインタと文字列の関係
char str1[] = "Hello";
char *str2 = "World";
""という表現は文字列が格納された場所の先頭アドレスを返す.
ただし,両者は異なる.前者は書き換え可能,後者は文字列「定数」のため変更する操作は「不定」である.教科書P127
NULLポインタ
どこも指していないことをはっきりさせたいときはNULLポインタを明示的に入れる。
int *p = NULL;
ポインタが分からなくなる主な原因
複数のポインタの宣言
ポインタ変数を複数宣言するときは,
double *ptr1, *ptr2, *ptr3;
であって
double *ptr1, ptr2, ptr3;
ではありません。上記だと,ptr2, ptr3 は普通の double 型の変数です。
一方,ポインタ変数の初期化ですが,宣言時の初期化は下記のようになります。
double x;
double *ptr = &x;
宣言時以外で初期化する場合は,ptr = &x;
となります。x に 1.0 を代入したい時は,*ptr = 1.0;
です。
ポインタ配列
何がポインタで何が中身か分からなくなる例として「ポインタ配列」がある。各要素がポインタであるような配列である。文字列を格納する配列などでよく使う。
int *a[4]; // int型のポインタを要素とする配列
char *s[4];
s[0] = "coffee";
s[1] = "tea";
s[2] = "water";
s[3] = "milk";
要素を自在に入れ替えられるので便利
- char *s[] は「配列です」そして「個々の要素の型が char *,即ち,文字型データへのアドレスです」となります。p.131 の図そのものですね。
- char str[] は char *str と同じです。char *lineptr[] は char **lineptr と同じです。 ポインタポインタです。
- char *lineptr[] と書くと「配列で,一つ一つの要素は文字型へのポインタ」というのが比較的楽に理解できます。でも,char **lineptr となると「ん?」となってしまうかもしれません.
- コンパイラ的には,仮引数の char *lineptr[] と char **lineptr は全くの等価です。
多次元配列
二次元配列がメモリ上でどのように配置されるのか
char daytab[2][13]
はまず一行目の char 13 個があって,次に二行 目の char 13 個が来る
つまりchar *pa = (char *)daytab;
と強引にキャストしてしまうと,*(pa + 13*i + j) と daytab[i][j]
は同じ
関数に渡すべきものがポインタなのか、実体なのか分からなくなる
void
swapbyval(int x, int y);
void swapbyref(int *x, int *y);
int a=1, b=2;
swapbyref(&a, &b);
int *c=3, *d=4;
swapbyref(c, d);
関数ポインタ
関数へのポインタも存在する
授業中の課題
課題1
iとsumをポインタにかえてみよ
#include <stdio.h>
int main()
{
int i,sum; // int *i, *sum;
sum = 0;
for (i=0; i<=10; i++){
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
課題2
次のプログラムが出力として表示する3つの値がそれぞれ何を意味するか説明せよ
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p = (int *) malloc(sizeof(int) * 1);
*p = 100;
printf("*p = %d\n", *p);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
free(p);
return 0;
}
課題3
以下の穴埋めを完成せよ
#include <stdio.h>
int main()
{
char *firstname = "Yoshihiro";
char lastname[9] = "Kawahara";
/* Yoshihiro Kawahara と表示したい */
printf("Name: %s %s\n", ■■■, ■■■);
return 0;
}
課題4
#include <stdio.h>
int main()
{
char *namelist[] = {"Kawahara", "Nakata", "Sakamoto", "Shinoda"};
int i;
/* ポインタ配列で表現された文字列の表示 */
for ( i = 0; i < 4; i++ ) {
printf("Name: %s\n", ■■■);
}
return 0;
}
課題5
% gcc -Wall kadai5.c
などと-Wallオプションをつけてコンパイルせよ
#include <stdio.h>
#include <stdlib.h>
#define NUMALPHA 26
int main()
{
char *str;
int i;
/* mallocによる領域の確保が必要なケース */
str = ■■■; // mallocでアルファベットの数+1文字分領域を確保
/* AからZまでをFor文で入れる */
for(i=0; i<NUMALPHA; i++){
*(str + i) = i + 'A';
}
*(str + i) = '\0';
printf("Alphabet: %s\n", str);
free(str);
return 0;
}
課題6
int strlen1(char *s);
で表されるような,文字列sを受け取って,その長さを返す関数をポインタを使って書け.
(教科書に答えはある)
また,main()からそのstrlenを使うような完動するプログラムとして示せ.
課題7
void strcpy1(char *s, char*t);
で表されるようなtをsにコピーする関数をポインタを使って書け.
(教科書に答えはある)
また,main()からそのstrlenを使うような完動するプログラムとして示せ.
課題8
int strcmp1(char *s, char*t);
で表されるような,文字列比較を行う関数をポインタを使って書け.
s > t で正の値、s < t で負の値、s = tで 0 を返す.この大小関係は一般に文字コード順による.
(教科書に答えはある)
課題9
- int a[30]; という宣言と,int *a[30]; という宣言の違いについて,自分の言葉で説明しなさい。必要に応じて図を使うとよい。図を使った場合は回答をPDFやWordで提出。
課題10
- ポインタのポインンタ int **p; について,それが何を指し示すか、自分の言葉で説明しなさい。必要に応じて図を使うとよい。(使わなくても良い.使う場合はPDFなどにして提出.)