C 언어(12) - 포인터

C 언어 강의, 포인터 이해

Featured image

🔚 짧게 하는 복습

✅ 1. for 문을 이해하자

✅ 2. while 문을 이해하자

✅ 3. 특히 무한 루프 while이나 중첩된 for 문은 꼭 연습하자.

혹시 기억이 안 난다면, 다시 돌아가자


오늘의 수업 전, 메모리 구조에 대해 까먹었거나 들어본 적 없다면, 이 포스트를 꼭 읽고 돌아와야 한다.

기본적으로 포인터는 메모리 구조를 알아야 하기 때문이다.

여기서부터 수많은 코포자(코딩 포기자)가 쏟아져 나왔던 분야다.

사실은 어렵기보다는(물론 어렵기도 하다.) 어색한 개념이라 처음에 이해하기 힘들다.

필자도 처음 포인터를 배울 때, 이해가 안 돼서 한참을 버벅댔던 경험이 있다.

최대한 쉽게 따라올 수 있도록 설명하겠다.


포인터란?

우선 잠깐 과거 수업을 복습해보자.

int a = 7;

이라는 코드가 실행될 때,

위 사진과 같이 임의의 주소부터 시작해서 4바이트의 공간에 7이라는 값이 할당된다는 것을 우리는 배웠다.

포인터다른 변수의 주솟값을 저장하는 자료형이다.

(주솟값을 저장할 변수의 자료형) *(변수 이름);

이렇게 선언한다. 예를 들면, 아래와 같이 작성할 수 있다.

int *p;
char *p2;
double *p3;

곱셈 연산자와 어떻게 구별하냐고 하면, 곱셈 연산자는 양쪽에 값이 있고 중간에 연산자가 들어가야 하지만(이를 이항 연산자라고 한다), 포인터에서 쓰는 *연산자는 하나의 변수 앞에서만 쓰인다. (이를 단항연산자라고 한다.)

그런데 이 포인터 자료형은 아무 값이나 대입하면 안 된다. 메모리에 직접 접근하는 것은 위험하기 때문이다.

메모리에 어떤 값이 대입되지 않았을 때는 쓰레깃값이 들어있기도 하고, 컴퓨터 구조상 접근 자체가 허용이 안 되는 메모리도 있기 때문이다.

그래서 실제로 사용하려면 어떤 변수의 시작 주솟값을 넘겨줄 방법이 필요하다.

어떤 변수의 시작 주소를 결과로 가지는 연산자는 & 연산자이다. 이 역시 이항 연산자로 쓰일 때는 비트 연산자이지만, 단항연산자로 쓰일 때는 시작 주솟값을 계산하는 연산자이다.

익숙하게 느껴진다면 복습을 잘한 것이다. scanf에 들어가는 &도 같은 의미라고 배웠기 때문이다.

이제 예제를 통해서 직접 보자.

혹시 따로 코드를 작성했는데 결과가 다르게 나온다고 놀라지 말자.

이 코드는 심지어 실행할 때마다 결과가 다르게 나온다…! 그 이유는 배웠던 것처럼 변수를 저장하기 위한 시작 주소를 무작위로 설정하기 때문이다.

아무튼, 이 코드에서 주목할 점은 &a와 p의 값은 항상 같게 나온다는 점이다.

그리고 또 주목할 것은, p 역시 시작 주솟값이 있다는 점이다.

포인터도 값을 가지는 변수이고 어느 곳에는 저장이 되어야 하는데, 값을 저장한다는 게 메모리의 어떤 부분에 할당하는 것이니까 사실 생각해보면 당연하다.

편의상 p의 시작 주소를 37, a의 시작 주소를 179라고 해서 그림으로 설명해보면, (편의상 그림의 메모리 한 칸을 4바이트로 간주)

이렇게 되는 것이다. 그리고 이게 포인터라는 이름이 붙은 이유이다. p 변수가 a의 시작 주솟값을 가지고 있는 모습이 마치 p가 a를 가리키는 것(point) 같기 때문이다.

그런데 사실 시작 주솟값을 가지는 것까지는 알겠는데, 이게 도대체 왜 필요하냐고 할 수 있다.

조금만 기다리자. 앞으로 나올 분야들은 포인터 없이는 대화가 안 된다.

역참조 연산

포인터 변수는 특별한 기능을 가지는데, 역참조 연산이라는 것이 가능하다.

이는 포인터가 가리키는 변수의 값을 결과로 가지는 연산이다. 즉 a가 3이라는 값을 가지고, p가 a의 포인터 변수라면 *p는 3의 값을 가지는 것이다.

이는 간단하게 변수 앞에 *을 붙이면 되는데, 아까는 변수 앞에 붙이면 포인터 변수 선언이라고 해놓고 이게 무슨 소리일까?

int *p = &a;// 이렇게 하면 포인터 변수 선언

*p += 1;
int b= *p;
/*
선언 후 이렇게 사용하면 역참조 연산
*/

포인터 변수를 선언할 때 *을 붙이면 이 변수가 포인터라고 알려주는 것이고, 포인터 변수를 선언 후 그 뒤에 * 연산자를 앞에 붙이면 역참조 연산을 의미한다.

위의 코드를 보면, *p는 a와 완벽히 같은 값을 가짐을 알 수 있다.

더 재미있는 사실도 있는데,

이렇게 코드를 작성하면 분명 *p의 값을 바꿨는데, a의 값도 바뀜을 알 수 있다.

위의 코드를 보면 포인터 변수의 유용함을 잘 알 수 있는데, 일반 정수형 변수 b에 a를 대입한다는 것은 그냥 a에 있는 값만 대입한다는 뜻이다.

한 번 대입하고 나면, b에 무슨 일을 해도 a의 값에는 영향을 주지 않고, 그 반대로 a에 무슨 일을 해도 b의 값에는 영향을 주지 않는다.

반면, 포인터 변수는 가리키는 변수가 가지는 현재 값*p로 가지기 때문에, *p가 바뀌면 a도 동시에 바뀌며 a의 값이 바뀌어도 *p의 값이 동시에 바뀐다.

그리고 a의 값을 a를 통해서 접근하는 것을 직접 참조, a의 값을 p를 통해서 접근하는 것을 간접 참조라고 한다..


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 메모리 구조를 통한 포인터의 원리를 이해한다.

✅ 2. 자료형에 맞게 포인터 변수를 선언하는 법을 안다.

✅ 3. 포인터 변수의 역참조, 간접 참조 그리고 직접 참조를 이해한다.

⚠️ 포인터 변수에 직접 주솟값을 대입하지 말자. 꼭 주소연산자를 통해 대입하는 방법을 이용하자.

💣 과제, 아무 것도 안 가리키는 포인터는 포인터는 어떻게 초기화할까? (Null을 검색해보자)

🔜 더 공부해보기,

  1. 정수형, 실수형, 문자형 포인터는 각각 몇 바이트를 가질까? sizeof()를 통해서 확인해보자. 그리고 읽어볼 거리 (1)을 읽어보자.

  2. 읽어볼 거리(1) - 왜 포인터들은 모두 같은 크기를 가질까?

  3. 읽어볼 거리(2) - 그럼 왜 포인터는 자료형을 가질까?