포인터

iwtkmn_0219·2022년 7월 14일
0

C 언어

목록 보기
1/2
post-thumbnail

1. 포인터가 무엇인가?

컴퓨터는 메모리를 가지고 해당 메모리에 우리가 사용하는 프로그램들은 정보를 알아서 저장합니다. 그러나 여기서 말하는 ‘알아서’는 ‘개발자가 정한 방식으로’를 의미합니다.

우리의 컴퓨터는 기본적으로 비트(bit)를 기본 단위로 저장합니다. 비트 8개가 모여서 바이트(byte)가 되고 1000바이트가 1킬로바이트(KB)가 됩니다. (1 KB = 8000 bit)C언어가 다루는 메모리는 바이트를 기본적으로 다룹니다.

위의 그림처럼 각 바이트들은 고유한 주소값을 가집니다. 말을 한 번 바꿔보겠습니다. 우리가 다룰 메모리들은 고유한 주소값을 가집니다. 주소값은 컴퓨터의 실제 메모리안에서의 실제 위치를 가리킵니다. 그렇기에 우리가 직접 해당 데이터에 접근할 수 있습니다. 이러한 특성 때문에 C언어가 메모리를 관리하는데에 있어서 강점을 가지는 언어라고 하는 것입니다. 파이썬의 경우 파이썬을 개발한 사람의 의도대로 데이터를 관리하기 때문에 메모리를 직접적으로 관리하는 것은 어렵습니다.

아직 이 주소값이 포인터라고 단정지으면 안됩니다. 왜냐하면 8 bit(1 byte)로 표현할 수 있는 정수의 최댓값은 255이기 때문입니다. 분명 우리가 사용하는 정수형은 32 bit(4 byte)까지 표현이 가능합니다. 정수형만 보더라도 바이트하나에 변수가 하나 저장된다라는 말은 지나치게 틀린 말이라는 것을 알 수 있습니다.

그렇다면 이러한 정보를 어떠한 방법으로 저장하는가에 대해 궁금증이 들어야만 합니다만, 단순하게도 여러 바이트를 사용하면 됩니다. 즉, 여러 주소에 정보를 저장하면 됩니다. 연속적인 바이트에 걸쳐서 정보를 저장하고 첫 바이트의 주소를 해당 변수의 주소로 지정해주면 됩니다. 위의 그림에서 어떠한 변수 xx가 1, 2, 3, 4 의 공간을 차지한다면 그 변수의 주소값은 1이 됩니다.

2. 포인터 변수

우리가 다루게 될 포인터는 이 ‘주소값’입니다. 변수가 저장된 ‘공간’을 가리키는 것이죠. 한가지 유의해야 할 것은 주소값이 정수로 표현되기는 하지만 정수와 동일하지는 않습니다. 그렇기 때문에 우리는 정수형(int)이 아닌 포인터 변수에 저장할 것입니다.

포인터 변수는 다음과 같이 선언합니다.

int *p;
int* p;

위 코드의 의미는 정수형 데이터를 담고 있는 메모리의 주소값 정도로 해석할 수 있습니다. 정수형 포인터라고 할 수 있습니다.

int* p;
double* q;
char* r;
int* *w;  // ...?

위 코드의 마지막 줄을 보고 ?를 띄웠다면 일단 띄워놓은 상태로 이어서 진행해보겠습니다.

1) 주소 연산자

포인터에서는 추가적으로 해당 변수의 주소값을 불러오는 &연산자(주소연산자)를 사용합니다. 변수 x에 대해서 &x를 사용하게 되면 x가 저장된 메모리의 주소값을 불러옵니다.

따라서 다음과 같은 코드를 작성할 수 있습니다.

int i = 10;
int* p;
p = &i;

위의 코드에서는 i에 10이라는 정수형 데이터를 저장하고 p라는 정수형 포인터에 i가 저장된 메모리의 주소값을 저장하였습니다.

2) 참조 연산자

포인터에서 다루는 추가적인 연산자로 *도 존재합니다. 여기서의 *는 포인터를 선언할 때 사용했던 *와는 다른 의미라는 것을 명심해야합니다. 사용예시를 우선 보겠습니다.

int i = 10;
int* p = &i;
printf("%d", *p);
int &j = i;
int *q = &j;

위의 코드에서 첫 번째 줄과 두 번째 줄은 아까 작성했던 코드와 동일한 코드입니다. 마지막 줄을 자세히 보면 가 p옆에 붙어 있는 것을 볼 수 있습니다. 마지막줄의 는 해당 주소값에 저장되어 있는 데이터를 꺼내오는 역할을 해줍니다. 즉 출력되는 값이 10이 되는 것이죠. 굳이 코드를 이상하게 써보자면 다음과 동일한 의미를 가진다는 것도 이해할 수 있습니다.

int i = 10;
printf("%d", *&i);

3) 주의 사항

배열에서 그랬듯이 포인터도 마찬가지로 초기화 시켜주지 않으면 현재 포인터가 메모리의 어디를 가리키고 있는지 알 수 없습니다. 혹여 이상한 곳을 가리키게 되면 의도치 않게 내 컴퓨터 자체를 손상시킬 수도 있습니다.(그래도 요즘은 컴파일러가 알아서 실행하지 않고 종료해줌) 그러므로 배열처럼 항상 초기화하는 습관을 들이도록 합시다.

3. 포인터 장난질

포인터를 말그대로 메모리 공간을 가리키기 때문에 여러개의 포인터가 하나의 공간을 가리킬수도 있습니다. 즉, 이러한 표현이 가능합니다.

int a = 10;
int* p = &a;
int* q = &a;  // q = p; 의 방식으로도 할당이 가능

위와 같은 구조를 구성하기 때문에 *p를 수정할 경우 *q에 대한 값도 변경되며 심지어 a의 값도 변경됩니다.

만약 위의 그림을 보고 점점 머리가 꼬인다면 아주 좋은 징조라고 할 수 있습니다. 그림을 천천히 이해해봅시다.

p는 정수형 포인터입니다. 그런데 a를 가리키는 화살표의 시작이 p의 내부라는 것을 확인할 수 있습니다. a에 대해서 적혀있는 부가 설명을 보았을 때는 분명 저 동그라미도 일종의 데이터일 것입니다. 네 맞습니다. 그래서 아까 물음표를 띄웠던 질문에 대한 답도 가능합니다. 포인터가 포인터를 가리키는 행위도 가능하다는 것이죠. 이에 대한 코드를 작성해보도록 하겠습니다.

int** p;
int* q;
int a = 10;
q = &a;
p = &q;
printf("%d", **p);

위의 코드에서 p는 포인터를 가리키는 포인터를 의미합니다. 이중 포인터라고도 부릅니다. 위의 코드를 조금더 보기쉽게 그림으로 표현해보겠습니다.

pq라는 정수형 포인터가 담겨있는 메모리의 주소값을 저장합니다. qa라는 정수형 데이터가 담겨있는 메모리의 주소값을 저장합니다. 마지막에 출력을 할 때에는 두번의 접근이 필요합니다. *p를 작성하게 되면 q에 담겨있는 a라는 정수형 데이터가 담긴 메모리의 주소값을 출력하기 때문입니다. 결국 printf(”%d”, *q);와 동일한 역할을 하는 것입니다.

정수형 포인터도 결국은 일종의 변수라는 근본에는 변함이 없다는 것을 기억해야합니다. 그래야 포인터를 더 잘 이해할 수 있습니다.

☠️주의 사항

int a = 10, b;
int* p = &a;
int* q = &b;
p = &a;
*q = *p;

위의 코드는 지금까지 썼던 코드들과는 다르게 값에 대한 접근을 통해 값만을 바꿔줍니다. 보기 쉽게 그림으로 보겠습니다.

*pa에 저장되어 있던 값을 *qb에 복사 해주는 과정을 의미합니다.

4. 함수에서의 포인터

1) 파라미터

함수에 파라미터로 포인터를 입력한다면 변수의 주솟값을 직접 넘겨준다는 의미입니다. 우리는 이 순간 ‘C가 데이터가 저장된 공간에 직접 접근할 수 있다’라는 말이 뇌리를 스쳐지나가야만 합니다. 함수에서 포인터를 통해 메모리 공간에 접근하고, 직접 그 데이터를 수정한다면 함수 밖의 변수에 실질적인 영향이 간다는 것을 인지해야합니다. 이러한 포인터의 특징은 여러개의 값을 입력받아서 단 하나의 값을 출력한다는 함수의 단점을 보완할 수 있습니다.

그렇다면 함수에서 포인터를 사용하는 예시를 보도록 하겠습니다. 가장 대표적인 예시가 바로 우리도 지금까지 사용해왔던 scanf("%d", &x);입니다. scanf도 함수 중 하나입니다. scanf는 값을 입력받는 역할을 하는 함수입니다. 포인터를 알기 전이라면 값을 입력받고, return을 통해 값을 반환받아서 변수에 저장하는 것이 쉽게 떠올릴 수 있는 방법일 것입니다. 그러나 우리는 scanf를 사용할 때 &x를 파라미터로 입력해줍니다. 지금 보니 변수 x가 저장된 메모리의 주소값인것을 알아볼 수 있습니다. 이 때문에 scanfx의 직접적인 공간에 접근합니다. 따라서 scanf는 함수 외부의 변수를 직접 변경할 수 있는 것입니다.

위와 같은 이유로 scanf를 다음과 같이 작성할 수도 있습니다.

int x;
int* p;

p = &x;
scanf("%d", p);

✨만약 값을 수정하고 싶지 않다면?

만약 위와 다르게 정말 참조만 하고 싶다면 const를 사용합니다. 만약 const로 설정하였으나 수정을 시도한다면 컴파일에러를 일으킵니다. 즉 명시적으로 해당 포인터의 값을 수정하지 않겠다고 알려주는 용도입니다. 해당 문법을 사용하는 예시를 들어보겠습니다.

int compare(const void *a, const void *b) {
		return *(int*)a - *(int*)b;
}

qsort(정렬할 배열, 배열 크기, 원소 크기, 정렬 함수);

위의 코드는 C언어에서 stdlib.h라이브러리의 qsort를 사용하여 배열을 정렬할 때 사용하는 매개함수입니다. 당장은 위의 함수를 온전히 이해할 수 없지만, 해석해봤을 때 *a, *b 이 두가지는 값에 대한 수정이 발생하지 않는다는 것을 알 수 있습니다.

2) 반환

함수에서 파라미터로 사용했다면 반환형으로 사용할 수도 있다. 가장 대표적인 예시 코드입니다.

int* maxAddress(int* left, int* right)
{
    if (*left > *right) {
        return left;
    } else {
        return right;
    }
}

int main(){
		int* p;
		int i;
		int j;
		…
		p = maxAddress(&i, &j);
		*p = 2147483647
}

위의 코드에서 p가 반환받는 값은 i, j중 더 큰 값을 가지고 있는 변수의 주소값입니다. 따라서 마지막 줄의 경우 i, j중 더 큰 값을 직접 2147483647로 바꿔줍니다.

0개의 댓글