포인터는 값이 다른 변수나 상수의 주소인 변수 또는 상수예요. 포인터는 C의 강력하면서도 잠재적으로 위험한 기능이에요. 포인터를 이해하는 것은 어려울 수 있어요.
int i = 5;
변수에 값을 할당하고 나면 두 가지를 물어볼 수 있어요.
너는 누구니?
나는 i야.
무엇을 가지고 있니?
나는 정수 5를 가지고 있어.
물어볼 수 있는 세 번째 질문도 있어요.
너는 어디에 있니?
나는 0x6dfed4에 있어.
잠깐, 뭐라고요?
변수와 상수는 자신의 값을 메모리 위치에 저장해요. 메모리의 그 위치를 변수나 상수의 주소라고 해요.
변수의 주소를 얻으려면 주소 연산자를 사용하는데, 바로 &예요.
int i = 5;
int *i_address = &i;
잠깐, i_address 앞에 있는 *는 무슨 뜻이죠? 곱하기라는 뜻 아닌가요?
타입 뒤에 *를 쓰고 그 뒤에 식별자를 쓰는 것이 그 타입의 변수에 대한 포인터를 선언하는 방법이에요.
*는 포인터가 주소를 담고 있는 값을 간접적으로 가리키기 때문에 간접 참조 연산자라고 해요.
그런데 왜 다른 변수의 주소를 담기 위해서만 변수를 만들죠?
애초에 왜 변수의 주소에 신경 써야 하죠?
음, 변수가 배열 안에 있을 때 변수가 어디에 있는지가 중요해요.
다음과 같이 배열을 만들면
int my_ints[] = {1, 2};
my_ints의 값은 사실 그 주소이고, 이는 첫 번째 원소의 주소와도 같아요.
두 번째 원소의 주소는 첫 번째 원소의 주소에 시스템에서의 int 크기를 더한 값이 돼요.
이걸 더 잘 보려면 다음을 실행해 봐요.
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
앞의 예제는 배열 인덱스가 왜 1이 아니라 0부터 시작하는지 보여줘요.
배열에 대한 인덱스는 인덱스에 타입의 크기를 곱한 값을 배열의 시작 주소에 더해요.
첫 번째 원소는 배열의 시작과 나란히 있으므로, 시작 주소에 더할 값으로 타입에 0을 곱해서 첫 번째 원소를 얻어요.
두 번째 원소를 얻으려면 시작 주소에 더할 값으로 타입에 1을 곱해요.
타입의 길이가 4바이트라면, 두 번째 원소의 주소는 첫 번째 원소보다 4바이트 더 높아요.
포인터가 특정 타입으로 선언되는 이유도 여기에 있어요.
포인터가 자신이 무엇을 가리키는지 타입을 모른다면, 거기에 1을 더할 때 몇 바이트를 이동해야 하는지 알 수 없어요.
포인터에 1을 더하는 것이 반드시 주소에 1바이트를 더한다는 뜻은 아니에요.
타입의 길이(바이트)에 1을 곱한 만큼 더한다는 뜻이에요.
그런데 아직 끝이 아니에요! *에 대한 이야기가 끝났다고 생각했나요?
이제 주소가 배열 인덱싱과 어떻게 동작하는지 이해했으니, 주소에 저장된 값을 얻지 못한다면 무슨 소용이 있을까요?
어떻게 하면 될까요?
주소에 저장된 값을 얻으려면 간접 참조 연산자를 사용하는데, 바로 *였죠.
이 경우 간접 참조 연산자는 역참조 연산자라고도 하는데, 포인터가 주소를 담고 있는 값을 역참조하기 때문이에요.
역참조할 때 역참조 연산자라고 부르는 것은 포인터를 정의할 때의 간접 참조 연산자와 구별하기 위해서인데, 두 용도 모두에서 *의 기술적인 이름은 간접 참조 연산자예요.
이런!
농담이에요?
*는 용도가 몇 개나 되는 거예요?!
곱셈, 포인터 선언, 그리고 이제 역참조까지!
너무 헷갈려요!
그런데 역참조가 정확히 뭐죠?
다음 예제는 역참조에 간접 참조 연산자를 어떻게 사용하는지 보여줘요.
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
괄호를 사용하면 포인터 연산을 할 때 의도를 더 분명하게 할 수 있어요. 다음 예제는 프로그래머가 의도한 대로 출력되지 않을 수 있어요.
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
*와 ++는 우선순위가 같지만, 오른쪽에서 왼쪽으로 평가돼요.
첫 번째 줄에서 my_ptr은 먼저 두 번째 원소로 증가하고, 그다음 역참조돼요.
두 번째 줄에서, 첫 번째 줄에서 두 번째 원소로 증가한 my_ptr은 먼저 역참조되고, 그다음 역참조된 값이 증가해요.
위 예제에 다음 줄을 추가하면
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
그 줄에서 우리는 포인터를 배열 너머로 증가시켰어요.
배열을 넘어섰고, 메모리에서 다음 4바이트를 가리키는데, 포인터는 이를 int로 해석하지만, 사실 그 바이트들은 다른 변수에 속할 수도 있어요.
그 4바이트는 double의 일부일 수도 있고, 4개의 char일 수도 있어요.
문제는 포인터로 그 바이트들을 들여다봐서도 안 된다는 거예요.
다음을 사용할 수도 있었어요
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
배열 인덱싱은 포인터로 구현되기 때문에, 배열을 넘어설 위험도 똑같이 적용돼요.
그리고 문자열은 char의 배열이므로, 포인터를 문자열에도 사용할 수 있어요.
문자열은 종종 다음과 같이 포인터 문법으로 선언해요
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
그런데 한 가지 질문이 있어요. 왜 별표를 식별자 옆에 두죠? 그 타입에 대한 포인터니까 타입 옆에 두면 안 되나요? 왜냐하면 이건
int* my_ptr1, my_ptr2_that_is_really_an_int;
int에 대한 포인터 두 개를 선언하는 것이 아니라, int에 대한 포인터 하나와 int 하나를 선언하기 때문이에요.
int에 대한 포인터 두 개를 선언하려면 다음과 같아요
int *my_ptr1, *my_ptr2;
그래서 모호함을 줄이려면 *를 식별자 가까이에 두는 게 좋아요.
아니면 한 줄에 식별자를 하나만 선언하거나요.
그리고 *에 아직 물리지 않았다면, 이제 다음 주제로 들어가 봐요.
문자열의 배열을 다음과 같이 표현하는 것을 흔히 볼 수 있어요
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
분명하지 않을 수 있지만, 문자열의 배열은 char 배열의 배열에 대한 포인터로 선언되고 초기화돼요.
char 배열의 배열이 print_strings()에 전달될 때, 시그니처는 이를 char에 대한 포인터의 포인터로 설명해요.
그러면 char **가 정말로 char 배열에 대한 포인터의 배열에 대한 포인터이고, 단일 char의 포인터에 대한 포인터가 아니라는 것을 어떻게 알 수 있죠?
알 수 없어요.
이것이 포인터 사용을 혼란스럽게 만드는 많은 것 중 하나예요. 다음은 컴파일될 수도 있어요
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
하지만 print_strings()는 의도한 대로 사용되지 않았어요.
다른 시그니처를 사용해도 도움이 안 돼요.
시그니처를 이렇게 바꿔도
void print_strings(char *passed_in_strings[]) {
// code snipped
}
여전히 컴파일되고 여전히 알아볼 수 없는 값을 출력할 수 있어요.
하지만 단일 char에 대한 포인터의 포인터를 전달하는 것은 실제로 의미가 없어요.
대개 char **를 보면, 그것은 char 배열에 대한 포인터의 배열에 대한 포인터를 나타내요.
우리는 함수에 전달되는 포인터를 봤어요. 그런데 왜 함수에 포인터를 전달할까요?
C에서는 인자가 값으로 함수에 전달돼요. 즉, 값이 인자로부터 복사되고 그 복사본은 함수에 지역적이에요. 함수가 끝나면 복사된 값도 함께 사라져요.
함수 안에서 값을 바꾸려고 하면 어떻게 되는지 봐요
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
왜냐하면 변수 이름이 같더라도, swap()의 a와 b는 main()의 a와 b와 같지 않기 때문이에요.
그것들은 a와 b 값의 _복사본_이에요.
swap을 의도한 대로 동작하게 바꿀 수 있을까요?
그럴 수 있어요!
a와 b의 주소를 포인터로 전달하면 돼요.
그런데 함수가 포인터를 복사하는데, 그게 무슨 소용이 있을까요?
복사된 주소도 여전히 같은 주소이기 때문에 소용이 있어요.
주소를 역참조하면 main()에 정의된 원래 값에 접근할 수 있어요.
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
이제 swap에 *가 많아졌지만, 이제 의도한 대로 동작해요.
간접 참조 연산자를 읽는 방법은 "~에 있는 값"이에요.
그래서 *a는 a에 있는 값이에요.
이게 문자열에서는 어떻게 동작할까요?
배열 변수의 값은 배열이 시작하는 주소라는 것을 알아요.
그리고 문자열을 char 배열, 즉 char my_string[]로 정의하거나 char에 대한 포인터, 즉 char *로 정의할 수 있다는 것도 알아요.
이 둘은 어떻게 다를까요?
이 코드에서는 char * 인자를 describe_me()에 전달해요.
adjective 인자는 char 배열로 정의되고 함수에서는 char에 대한 포인터로 받아들여요.
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
adjective에 지속적인 변경을 가할 수 있었는데, 이는 정의된 char 배열이 char에 대한 포인터로 받아들여질 수 있음을 보여줘요.
이제 adjective를 char에 대한 포인터로 직접 정의해 봐요.
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
포인터가 강력하지만 잠재적으로 위험하다는 것을 기억하나요?
그런데 왜 동작하지 않을까요?
배열은 포인터이고 포인터는 char 배열과 함께 동작했는데요!
그 이유는, 앞에서는 adjective가 main()에 속한 변수인 char 배열로 정의되었기 때문이에요.
이제 adjective는 문자열 _리터럴_로 정의되는데, 이는 chars들이 프로그램 전체에 속하는 읽기 전용 정적 메모리에 쓰인다는 뜻이에요.
둘 다 포인터지만, 서로 다른 것을 가리켜요.
문자열 리터럴 값을 바꾸려는 것은 5라는 리터럴 값을 바꾸려는 것과 같아요.
허용되지 않아요.
그래서 문제는 사실 그것이 포인터라는 게 아니에요.
문제는 그것이 가리키는 대상이에요.