포인터란 특정 데이터가 갖는 메모리의 주소값을 보관하는 변수이다.
포인터 변수는 '베이스_타입 *포인트 변수'의 형식으로 선언된다.
여기서 베이스 타입은 포인트 변수가 나타내는 메모리 장소에 들아갈 값의 자료형으로 포인트 변수 역시 같은 데이터 타입으로 선언해야 한다는 점을 주의해야 한다.
char a = 'A'
char *p; //포인트 변수 p가 가리키는 메모리 장소는 char형
p = &a; //같은 데이터 타입을 가지고 있어야한다.
포인터 변수 관련 연산자
&연산자는 변수의 주소를 추출할 때 사용하는 연산자이다.
즉 위에서 &a가 나타내는 것은 a라는 변수의 '주소'이다. 그렇기에 &a를 대입할 수 있는 것은 주소값을 보관하는 포인터로 선언된 p가 되는 것이다.
*연산자는 포인터가 가리키는 장소의 값을 의미하는 것이다. 즉 *p가 나타내는 것은 a에 들어있는 값 'A'를 뜻하게 되는 것이다.
p = &a; //포인터 변수 p에 변수 a의 '주소값' 대입
*p = 'B'; //포인터가 가리키는 주소의 값에 B를 대입, 즉 a = B;와 같은 뜻임
포인터 변수의 연산자 우선순위
char *p; // 포인터 변수 p선언
p //포인터 변수 p
*p // 포인터 변수 p가 가리키는
*p++ //포인터가 가리키는 값을 가져온 다음, 포인터를 한칸 증가한다.
//주의해야 할 점은 포인터를 한칸 증가하는 것은 값이 증가하는 것이 아니라 주소값이 변하는 것이다.
*p-- //포인터가 가리키는 값을 가져온 다음, 포인터를 한칸 감소한다.
(*p)++ //포인터가 가리키는 값을 증가시킨다.
예시를 들어보자.
char *p;
char c[3] = {'A', 'B', 'C' };
p = &c[1];
printf("*p++ = %c \n", *p++); //(1)
printf("*p = %c \n", *p); //(2)
위와 같은 상황에서 출력값은 어떤 것이 나올까.
먼저 p가 가리키는 것은 배열 c[1]의 주소값이고 *p는 배열 c[1]의 요소인 'B'를 가리키게 되는 것이다.
(1) *p++는 포인터 p가 가리키는 주소의 값을 먼저 가져오고 그 다음 포인터 p의 값을 한칸 증가시키는 연산이다. 그렇다면 'B'가 먼저 출력된 다음 포인터 p(주소값)이 증가하는 순서일 것이다.
(2) 위의 1번에서 주고값이 한칸 증가했기 때문에 p가 가리키고 있는 것은 c[1]의 주소가 아닌 c[2]의 주소가 된다. 때문에 *p도 'B'가 아닌 'C'가 출력될 것이다.
포인터의 포인터 변수
int a;
int *p;
int **pp; //포인터의 포인터 선언
p = &a; //변수 a의 주소값을 포인터 p에 연결
pp = &p; //포인터 p의 주고값을 포인터의 포인터 pp에 연결
포인터 변수의 다양한 타입
void *p; //특정되지 않은 데이터 타입을 가리키는 포인터 변수
int *pi; //정수형 변수를 가리키는 포인터 변수
int **pp; //정수형 변수를 가리키는 포인터를 가리키는 포인터
struct test *ps //test타입의 구조체를 가리키는 포인터
void(*f)(int); //f는 함수를 가리키는 포인터
char *argv[]; // 포인터들의 배열, argv라는 배열의 값들이 포인터로 이루어 지는 것임
명시적 형 변환(type casting)
변수가 가지고 있는 데이터 타입은 전환이 가능하다.
int a;
char c;
a = c; //(X) 서로의 데이터 타입이 다르기 때문에 호환되지 않는다
a = (int *)c; //(O) char타입의 변수 c를 int형으로 전환, 데이터타입을 같게 만들어 호환될 수 있다.
** 배열의 이름은 배열의 시작 주소를 나타내는 포인터라는 것을 알아둬야 한다.
** 포인터의 사칙연산은 포인터가 가리키는 객체의 단위로 움직인다. 즉 포인터의 데이터 타입의 바이트 크기만큼 메모리 주소가 이동하는 것이다.
ex) 포인터 p에 대하여 p+1은 바로 다음 객체의 주소를 가리키는 것.
p[0] = *p //서로 같은 대상을 가리킨다고 한다면
p[1] = *p + 1
p[2] = *p + 2
... p[n] = *p + n
화살표 (->) 연산자
화살표 연산자는 구조체 포인터를 이용하여 구조체 안의 필드에 접근 할 수 있는 연산자이다.
원래 구조체 필드에 접근하기 위해서는 멤버연산자 (.)을 이용해야 한다고 했는데 구조체 포인터를 이용해서 접근하는 것도 가능하다.
struct {
char name[3];
int age;
char address[30];
} Person, *p //구조체 포인터 선언
p = &Person; //포인터에 구조체 Person의 주소 저장
strcpy(p->name, "james"); //p가 가리키는 구조체 멤버 name에 james대입
p->age = 19; //p가 가리키는 구조체 멤버 age에 19 대입
strcpy(p->address, "seoul"); //p가 가리키는 멤버 address에 seoul대입
p->name == (*p).name == Person.name // 셋은 같은 의미임
자체 참조 구조체(self-rerential structure)
자체 참조 구조체(Self-referential structure)는 구성 요소 중에 자기 자신을 가리키는 포인터가 한 개 이상 존재하는 구조체를 말한다. 연결 리스트나 트리를 구성할 때 많이 등장한다.
typedef struct List{
char data;
struct List* link;
}List; //List 구조체의 필드 link가 자신의 구조체를 가리키는 포인터이다 -> 자체 참조 구조체
call by value는 함수 호출 시 인수로 전달되는 변수의 '값'을 함수의 매개변수에 복사하여 전달하는 방식이다. 예시로 두 변수의 값을 바꾸는 함수 swap을 call by value 방식으로 작성했을 때 결과는 어떻게 출력될까.
void Swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main(){
int n = 1, m = 2;
swap (n, m);
printf("n = %d, m = %d\n", n,m);
}
// 결과: n = 1, m = 2
swap을 실행하였지만 n과 m의 값은 바뀌지 않은 모습을 볼 수 있다.
그 이유는 함수 안의 매개변수 a, b는 메모리상 n, m과 서로 다른 주소값을 가지고 있기 때문이다. 말그대로 값을 '복사'하여 함수의 매개변수에 전달했을 뿐 그것이 원본값에 영향을 줄 수는 없다는 것이다.
그렇다면 원본값에도 영향을 주기 위해서는 어떤 방식을 이용해야 할까.
* call by address(주소에 의한 전달)
call by address는 함수 호출시 인수로 전달되는 변수의 '주소값'을 함수의 매개변수(포인터)에 전달하는 방식이다.
void Swap(int *a, int *b) //주소값을 저장하는 포인터로 매개변수 선언
{
int tmp;
tmp = *a; //a의 주소가 가리키는 값을 tmp에 대입
*a = *b; //b의 주소가 가리키는 값을 대입을 a의 주소가 가리키는 값에 대입
*b = tmp; //tmp에 저장된 값을 b의 주소가 가리키는 값에 대입
}
//이렇게 서로의 값을 바꾸는 함수 완성
int main(){
int n = 1, m = 2;
swap (&n, &m); //n과 m의 주소값을 인수로 전달
printf("n = %d, m = %d\n", n,m);
}
// 결과: n = 2, m = 1
함수의 매개변수를 포인터로 설정하고 인수의 주소값을 전달하게 된다면 포인터 a, b에는 인수 n과 m의 주소가 저장될 것이다. 이렇게 하면 a와 b에 저장된 주소가 가리키는 값이 n과 m에 저장된 값을 가리킬 수 있게 된다.
값을 복사하여 전달하는 call by value와 달리 call by address는
주소값을 전달함으로써 값을 직접적으로 연결시키기 때문에 원본값에도 영향을 미칠 수 있는 것이라 이해하면 될 것 같다.
정적 메모리 할당은 메모리의 크기가 프로그램이 시작하기 전 컴파일 단계에서 결정되는 것을 말한다. 프로그램의 수행 도중에 이미 할당된 메모리의 크기는 변경될 수 없다.
그렇게 때문에 원래 결정된 크기보다 더 큰 입력값이 들어올 때 처리하는 것이 힘들고 반대로 더 작은 값이 들어오면 메모리의 공간을 낭비하게 된다.
동적 메모리 할당은 프로그램의 실행 도중 메모리를 할당 받는 것을 말한다. 필요한 만큼 할당을 받고 필요할 때에는 메모리의 공간을 다시 반납하기도 한다. 그렇게 때문에 메모리 공간을 효율적으로 이용하는 것이 가능하다.
동적 메모리 할당에 관련된 라입러리 함수들은 다음과 같다.
malloc(size) : size의 바이트 만큼 메모리를 할당받는다. malloc의 반환값은 void이기 때문에 할당받은 메모리를 저장하는 포인터 변수의 데이터 타입에 맞게 명시적 형변환을 해주어야 한다.
sizeof 연산자 : 변수나 타입의 바이트 크기를 반환하는 연산자이다.
free(var) : 변수나 타입의 크기를 반환하는 기능으로 메모리를 반납할 때 이용한다.
동적 메모리 할당 코드 예시
char *p;
int *q;
struct Book *r; //구조체 포인터 선언
p = (char *)mallaoc(100); //동적메모리 할당
q = (int *)malloc(sizeof(int));
r = (struct Book *)malloc(sizeof(struct Book));
// 동적 메모리 사용
free(p); //동적 메모리 반납
free(q);
free(r);