포인터
포인터란 쉽게 말하자면 주소를 담는 변수다. 하지만 이렇게 말하면 헷갈릴 수도 있기 때문에 그냥 포인터도 하나의 자료형으로 생각하는 것이 편할 것이다. 즉, '특정 자료형 변수의 주소'를 다루는 자료형이라고 생각하는 것이다.
// 포인터의 선언에 대해서는 아래 서술
int a;
float b;
char c;
int* p; // int형 변수 a의 주소를 다루는 int*형 변수 p
float *q; // float형 변수 b의 주소를 다루는 float*형 변수 q
char* r; // char형 변수 c의 주소를 다루는 char*형 변수 r
포인터를 선언하는 방법은 변수 이름 앞에 *를 붙이는 것이다. 하지만 비주얼 스튜디오에서도 자동 완성으로 자료형 바로 뒤에 *가 붙는 것을 보아 int* p와 int *p와 같은 형식이 다 쓰이는 것 같다.
포인터 변수의 출력은 주소를 출력하는 것이므로 %p 라는 서식 지정자를 사용해 출력한다.
포인터를 선언할 때 주의할 점이 있다. 하나의 예를 살펴보자.
int* p, q, r;
기존 일반 변수들의 선언 방법에 익숙하기도 하고, 위에서 말한 '자료형으로 생각' 이라는 문장때문에 위의 예제에서 헷갈릴 여지가 발생한다. 포인터에 익숙하지 않은 사람들은 위의 예제는 int형 포인터 변수 p, q, r 세 개가 선언된다고 생각할 것이다. 그러나 위의 예제는 int형 포인터 변수 p와 int형 변수 q, r이 선언된다. 그 이유는 위에서 말했듯이 정석적인 선언 방법은 변수 이름 앞에 *를 붙이는 것이기 때문이다. int형 변수 p, q, r을 선언하기 위해서는 아래 예제와 같이 하면 된다.
int* p, *q, *r;
📚 포인터 변수의 크기 = 저장한 변수의 자료형 크기?
모든 포인터 변수의 크기는 고정되어있다. 32비트 운영체제는 4 바이트, 64비트 운영체제에서는 8바이트다. 즉 int*, char*, float* double*에 상관없이 크기가 같다는 것이다.
📚 다른 데이터 형식을 가진 포인터 변수에 주소를 대입하면 어떻게 되나?
간단한 예를 통해 살펴보자.
int a;
char *p;
p = &a;
해당 예제에서는 int형 변수 a의 주소를 char형 포인터 변수 p에 집어넣는 실수를 보여주는 예제다. 이렇게 데이터 형식을 통일하지 않는 것은 컴파일은 가능하지만 논리적으로 틀린, 즉, 논리 오류다. 이것이 메모리 상에서 어떻게 처리되는지 알아보자.
char형 포인터 변수 p는 char형 변수의 주소만을 담기 위한 변수다. 그렇기 때문에 char형의 크기인 1 바이트만큼의 주소만 가져온다. 하지만 int형 변수는 4바이트로 되어있다. 즉, 포인터 변수 p가 int형 변수의 일부분을 가리키고 있는 것이다. 이렇게 된다면 잘못된 값을 출력하는 등의 문제가 생길 수 있다.
그럼 포인터를 메모리 구조를 통해 다시 한 번 살펴보자. 해당 예제는 32비트 운영체제를 기준으로 한다.
아래와 같이 변수를 선언한다고 하자.
char ch; // 문자형 변수 ch 선언
char* p; // 문자형 포인터 변수 p 선언
ch = 'A'; // ch에 문자 'A'를 대입
p = &ch; // p에 변수 ch의 주소를 대입
이를 메모리 구조로 나타내면 다음과 같다.
문자형 변수 ch를 선언하면 메모리 상에 할당이 된다. 이때 ch의 시작주소를 1030이라고 하자. 그리고 포인터 변수 p도 하나의 변수이므로 메모리에 4바이트만큼(32비트 기준) 할당된다. 코드의 마지막 줄에서 주소 연산자 &를 통해 ch의 주소인 '1030'을 p에 저장한다. 이렇게 포인터 변수 p는 ch의 주소값인 1030을 갖게 되는 것이다.
역참조 연산자 *
* 연산자는 포인터 변수가 가리키는 곳(포인터 변수가 담고 있는 주소)의 실제 값을 가져온다. 위의 예제를 또 이용한다면, p에는 ch의 주소값인 1030이 담겨있다. 이때 역참조 연산자를 사용해 *p라고 한다면 이것은 p가 가리키는 변수인 ch의 값인 'A'를 가져온다.
역참조 연산자를 이용해 직접 값 바꾸기
*를 이용해 포인터가 가리키는 변수의 실제 값을 바꾸는 방법을 알아보자.
#include <stdio.h>
int main()
{
char ch;
char *p;
ch = 'A';
p = &ch;
printf("%c\n", ch);
*p = 'B';
printf("%c\n", ch);
return 0;
}
// 출력
A
B
직접 ch의 값을 바꾸지 않고, ch의 주소가 담긴 p를 통해 ch의 값을 바꿨다. 이런식으로 변수 선언을 제외한 곳에서 역참조 연산자 *를 사용한다면 포인터 변수가 가리키는 곳의 실제 값을 가져올 수도 있고, 바꿀 수도 있다.
배열과 포인터의 관계
배열과 포인터의 관계에서 가장 중요한 것은 배열 이름 = 포인터라는 것이다. 이전에 쓴 메모리 할당과 주소에서 배열 이름은 '전체 배열의 주소'라는 것을 말했다. 즉, 전체 배열의 주소이므로 포인터와 완전히 같은 역할을 한다는 것이다. 그러므로 다음과 같은 선언이 가능하다.
char s[12];
char* p = s; // 배열 이름에는 &를 붙이지 않는다.
배열 이름과 포인터가 같은 역할을 하기 때문에, 다음과 같은 것이 가능하다.
char s[3];
char* p;
p = s;
일때
s[0] = *(s+0) = *(p+0)
s[0] = *(s+1) = *(p+1)
s[0] = *(s+2) = *(p+2)
이것을 일반화하면 다음과 같다.
배열 a와 포인터 p가 있고, p = &a 일때
1. a[n] = p[n] = *(a+n) = *(p+n) // 포인터를 배열처럼 쓸 수 있다.
2. &a[n] = &p[n] = a + n = p + n
'Programming > C' 카테고리의 다른 글
메모리 할당과 주소 (2) | 2021.05.09 |
---|---|
문자열과 관련 함수 (0) | 2021.05.02 |