트랙
/
C
C
/
학습 과정
/
포인터
포인

포인터 에서 C

{other: "%{count}개의 연습 문제"}

포인터 소개

포인터는 값이 다른 변수나 상수의 주소인 변수 또는 상수예요. 포인터는 C의 강력하면서도 잠재적으로 위험한 기능이에요. 포인터를 이해하는 것은 어려울 수 있어요.

int i = 5;

변수에 값을 할당하고 나면 두 가지를 물어볼 수 있어요.

너는 누구니?

나는 i야.

무엇을 가지고 있니?

나는 정수 5를 가지고 있어.

물어볼 수 있는 세 번째 질문도 있어요.

너는 어디에 있니?

나는 0x6dfed4에 있어.

잠깐, 뭐라고요?

변수와 상수는 자신의 값을 메모리 위치에 저장해요. 메모리의 그 위치를 변수나 상수의 주소라고 해요.

주소 연산자와 간접 참조 연산자 (1부)

변수의 주소를 얻으려면 주소 연산자를 사용하는데, 바로 &예요.

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;
}

포인터 연산, 또는 배열 인덱스가 0부터 시작하는 이유

앞의 예제는 배열 인덱스가 왜 1이 아니라 0부터 시작하는지 보여줘요. 배열에 대한 인덱스는 인덱스에 타입의 크기를 곱한 값을 배열의 시작 주소에 더해요. 첫 번째 원소는 배열의 시작과 나란히 있으므로, 시작 주소에 더할 값으로 타입에 0을 곱해서 첫 번째 원소를 얻어요. 두 번째 원소를 얻으려면 시작 주소에 더할 값으로 타입에 1을 곱해요. 타입의 길이가 4바이트라면, 두 번째 원소의 주소는 첫 번째 원소보다 4바이트 더 높아요.

포인터가 특정 타입으로 선언되는 이유도 여기에 있어요. 포인터가 자신이 무엇을 가리키는지 타입을 모른다면, 거기에 1을 더할 때 몇 바이트를 이동해야 하는지 알 수 없어요. 포인터에 1을 더하는 것이 반드시 주소에 1바이트를 더한다는 뜻은 아니에요. 타입의 길이(바이트)에 1을 곱한 만큼 더한다는 뜻이에요.

그런데 아직 끝이 아니에요! *에 대한 이야기가 끝났다고 생각했나요?

간접 참조 연산자 (2부)

이제 주소가 배열 인덱싱과 어떻게 동작하는지 이해했으니, 주소에 저장된 값을 얻지 못한다면 무슨 소용이 있을까요? 어떻게 하면 될까요? 주소에 저장된 값을 얻으려면 간접 참조 연산자를 사용하는데, 바로 *였죠. 이 경우 간접 참조 연산자는 역참조 연산자라고도 하는데, 포인터가 주소를 담고 있는 값을 역참조하기 때문이에요. 역참조할 때 역참조 연산자라고 부르는 것은 포인터를 정의할 때의 간접 참조 연산자와 구별하기 위해서인데, 두 용도 모두에서 *의 기술적인 이름은 간접 참조 연산자예요.

이런! 농담이에요? *는 용도가 몇 개나 되는 거예요?! 곱셈, 포인터 선언, 그리고 이제 역참조까지! 너무 헷갈려요! 그런데 역참조가 정확히 뭐죠?

다음 예제는 역참조에 간접 참조 연산자를 어떻게 사용하는지 보여줘요.

#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 배열, 즉 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라는 리터럴 값을 바꾸려는 것과 같아요. 허용되지 않아요. 그래서 문제는 사실 그것이 포인터라는 게 아니에요. 문제는 그것이 가리키는 대상이에요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요