본문 바로가기
프로그래밍/C

포인터의 개념

by 길냥이 2025. 6. 3.
728x90

목차

  1. 포인터를 사용하기 전에..
  2. 메모리의 작동 원리
  3. 주소 연산자 &와 address
  4. 포인터의 선언과 사용
  5. 간접 참조 연산
  6. const를 이용한 포인터

 

1. 포인터를 사용하기 전에..

그동안 우리가 배운 변수, 연산, 배열, 함수 등의 개념은 대부분 다른 프로그래밍 언어에도 그대로 존재합니다.

하지만 C는 C만의 특출난 무기가 있으니, 바로 포인터(pointer)입니다.

 

이 포인터를 통해 우리는 원하는 방식으로 데이터를 사용할 수 있고, 메모리에 직접적으로 접근할 수 있죠.

만약 "포인터를 만지다 실수로 메모리를 건드려서 컴퓨터가 꺼졌어요!" 라는 일이 일어난다면..

 

음. 물론 요즘은 문제가 없어요. 

현대의 운영 체제는 사용자 프로그램이 접근 가능한 메모리 영역을 제한하기 때문에, 일반적으로는 운영체제에서 이를 막아버립니다. 

하지만 언제나 예외는 있는 법...

C는 저수준 언어이기 때문에, 메모리에 대한 세밀한 제어를 허용하지만.. 우리가 모든 걸 책임져야 하죠.

만약 OS커널에서 C코드가 실행된다면...!

 

뭐 그렇습니다. 포인터는 잘 다뤄줘야 해요.

포인터는 항상 널로 초기화하고, 포인터 연산을 정확히 이해해서 사용하도록 합시다.

 

그래서, 포인터가 뭘까요?

 

2. 메모리의 작동 원리

메모리는 우리가 컴퓨터를 가지고 데이터를 넣고 쓰는 공간입니다.

쉽게 말하면, 데이터를 보관하는 커다란 선반이에요.

 

예를 들면, 우리가 선반 어딘가에 라면이랑 향신료 등을 넣어 둔 상황이에요.

라면과 향신료가 저장된 데이터라면, 메모리는 선반인 거죠.

 

만약 우리가 라면을 먹으려면 어떻게 해야 할까요?

정답은 못먹습니다. 선반 어디에 라면이 있는지 모르잖아요.

정확히 몇 번 선반에 라면이 있죠? 선반 하나하나 열어볼 건가요?

 

하지만 우리가 선반 130번에 라면이 들어 있다고 알고 있다면?

그러면 우리는 130번 선반으로 이동해 라면을 꺼내 먹을 수 있어요.

 

이게 메모리의 작동 원리입니다. 

 

다시 프로그래밍으로 돌아와서, int 변수 a가 100번째 메모리부터 할당되었다면...

주소 값 0 ~ 129  130 131 132 133 134
메모리 ? ~ ? 0000 0000 0000 0000 0000 0000 0000 0100 ?

※ 실제 저장 방식은 리틀엔디안/빅엔디안에 따라 다르지만 단순화되었습니다.

 

int형 변수는 4바이트의 값이고, 1바이트는 8비트이므로 32비트에요.

 

주소 값 하나당 8비트가 할당되므로, a가 만약 8이라면 다음과 같이 저장되죠.

 

즉, 컴퓨터는 저 130~133까지의 4바이트를 변수 a란 이름으로 사용하고 있던 거에요.

그렇다면 우리는 다른 생각을 할 수 있죠. 130~133이 변수 a라면... 130 또한 변수 a처럼 사용할 수 있지 않을까?

그게 바로 포인터에요.

 

우리는 a를 통해 저 4바이트에 접근할 수 있고, 수정할 수 있지요.

하지만 반대로 주소 값 130을 이용해서 a에 접근할 수도 있어요. 이게 바로 포인터 값이랍니다.

 

결국 지금까지는 변수명으로 메모리 공간을 간단히 사용해 왔지만, 사실 그 밑에는 언제나 주소가 작동하고 있었던 거죠.

 

3. 주소 연산자 &와 address

이제 이 저장공간을 주소로 사용하는 방법에 대해 알아보죠.

여기서 주소라 하면, 변수가 할당된 메모리 공간의 시작 부분을 의미해요.

시작 주소를 알면 변수의 크기만큼 이 메모리를 조작할 수 있죠.

 

주소는 주소 연산자 &를 이용해서 구합니다. scanf에서 몇 번 본 친구죠.

가벼운 예제부터 사용해 봅시다.

 

포인터를 print할 때는 %p를 사용하세요.

#include <stdio.h>

int main(void) 
{
    int a = 0;
    double b = 0.0;

    printf("int형 변수의 주소 : %p\n", &a);
    printf("double형 변수의 주소 : %p\n", &b);
    
    return 0;
}
자, 한번 출력을 봅시다.
int형 변수의 주소 : 000000BF75FFF764
double형 변수의 주소 : 000000BF75FFF788
 
 
흠, 출력이 뭔가 이상해요.
 
이유는 간단한데, 메모리 주소는 일반적으로 16진수로 표현됩니다.
0000 0000(2진수)라면 16진수로는 0 0으로도 표현할 수 있기 때문이에요.
 
이 숫자들은 실제로 변수들이 물리적 메모리의 어느 위치에 있는지 나타내는 값이에요.
BF75FFF762 BF75FFF763 BF75FFF764 BF75FFF765 BF75FFF766 BF75FFF767 BF75FFF768 BF75FFF769
BF75FFF772 BF75FFF773 BF75FFF774 BF75FFF775 BF75FFF776 BF75FFF777 BF75FFF778 BF75FFF779
BF75FFF782 BF75FFF783 BF75FFF784 BF75FFF785 BF75FFF786 BF75FFF787 BF75FFF788 BF75FFF789

즉 실제로는 저렇게 된다는 거죠. 좀 잘렸지만 double형은 뒤에 6바이트만큼 더 사용할 거에요.

 

4. 포인터의 선언과 사용

자, 이제 이 망할 포인터를 사용해 봅시다.

 

포인터의 값은 주소 연산자 &를 통해 얻고,
포인터가 가리키는 값을 사용할 땐 간접 참조 연산자 *를 이용합니다.

참고로 *는 포인터 연산자라고도 불립니다.


하지만 쓰임에 따라 전혀 다른 역할을 하니 주의가 필요해요.

 

#include <stdio.h>

int main(void) 
{
    int a = 0;   //변수 선언
    int* p;      //포인터 변수 선언

    p = &a;             //포인터 변수에 a의 주소 대입
    printf("%d\n", p);   // a의 주소 출력
    printf("%d\n", &a);  // a의 주소 출력
 
    printf("%d\n", *p);   // p의 포인터 연산 출력
    printf("%d\n", a);    // a값 출력

    return 0;
}

이게 뭐람

출력은 더 가관입니다.

-529270620
-529270620
0
0
 
좋아요, 하나씩 뜯어볼까요?
 
먼저 int* p;는 포인터 변수를 선언합니다.
int에 속지 말고 *라는 다른 자료형이 존재한다고 생각하면 좋아요.
즉, *라는 자료형(포인터 타입)을 가진 변수 p가 존재하는 겁니다.
 
그리고 이제 p에 &a를 대입하는데, 이는 &a의 주소를 p가 가짐을 의미합니다.
따라서 이 코드를 통해 p는 a의 주소를 저장한 포인터가 됩니다.
 
그럼 출력을 보죠.
문제는 아랫줄이에요. printf("%d\n", *p); 하니까 왜 a값이 나올까요?
 
위에서 말했지만, p는 *형태의 자료형을 가져요. 
*p는 포인터 p가 가리키는 메모리 위치에 저장된 값, 즉 a의 값이에요.
p는 a의 주소를 담은 자료형이므로(*, 포인터 자료형) *p는 결국 a와 같은 값이 되는 거죠.
 
자! 포인터가 복잡하시나요?
복잡하고 어려운게 당연합니다. C는 포인터가 가장 어렵게 설계되어 있거든요.
하지만 익숙해진다면, 가장 유연하고 강력한 C의 도구가 될 겁니다!

 

여담으로, 포인터 연산자를 편하게 배우기 위해 int를 무시하라고 했지만 실제로 무시하지는 마세요, 

int라는 자료형에 *라는 자료형이 추가로 붙는 거랍니다. 기존 자료형과 결합해서 새로운 포인터형을 만드는 것이죠.

 

5. 간접 참조 연산

포인터가 어떤 변수의 주소를 가지면, 그 이후에는 간접 참조 연산자를 통해 가리키는 변수를 자유롭게 사용할 수 있습니다. 

한번 알아보도록 하죠.

#include <stdio.h>

int main(void) 
{
    int a = 10;
    int b = 15;
    double avg = 0;

    int* pa;
    int* pb;
    double* pavg = &avg;

    pa = &a;
    pb = &b;

    *pavg = (*pa + *pb) / 2.0;

    printf("%.2lf\n", *pavg);


    return 0;
}

이제 이거 어떻게 돌아가는지도 모르겠나요?

 

출력은 다음과 같습니다.

12.50

걱정 마세요, 분리된 부분 하나하나 알아보죠.

 

int a = 10;
int b = 15;
double avg = 0;

 

두 정수 a와 b를 선언합니다. 

avg는 평균을 담은 double형 변수에요.

 

int* pa;
int* pb;
double* pavg = &avg;

 

pa와 pb는 각각 a와 b를 가리킬 정수형 포인터입니다. a와 b가 int형이니 포인터도 int여야겠죠.

pavg는 avg를 가리킬 실수형 포인터입니다. 여기서는 포인터 선언과 동시에 avg를 가리켰지만, 아래와 같은 방법도 있어요.

 

pa = &a;
pb = &b;

 

pa와 pb에 각각 a와 b의 주소를 저장합니다. 

이제 *pa == a, *pb == b 가 되게 된답니다. 위쪽에 double*가 선언과 동시에 초기화된거랑 다르죠?

 

*pavg = (*pa + *pb) / 2.0;

 

위에 써있는 지식이랑 결합하면..

avg = (a + b) / 2.0;이랍니다.

 

printf("%.2lf\n", *pavg);

 

평균값을 소수점 둘째 자리까지 출력합니다.

 

자! 포인터로도 우리가 그동안 해 오던 작업을 그대로 처리할 수 있답니다.

포인터 없이 하던 작업은 사실 메모리 주소를 이용해서도 동일하게 처리할 수 있어요.

 

잠깐... 이러면 그냥 변수를 쓰는게 나은거 아닌가요?

괜히 어려워지기만 하는거 같은데...

 

라고 하지만, 포인터는 여러 상황에서 없어서는 안 되는 존재입니다.

 

함수를 스왑하거나, 

동적 메모리를 할당하거나, 

배열과 문자열을 처리하거나,

혹은 자료구조의 리스트를 연결/구현하거나,

함수 간 데이터를 공유하는 것 말이죠.

 

그리고 특정 상황에서 포인터가 유용하게 사용된답니다.

대표적으로 임베디드 프로그래밍이나, 배열을 탐색하거나, 구조체에 접근하는 것 말이죠.

특히 함수를 복사할 때 매우 빠르게 사용되는데, 값을 하나하나 복사하지 말고, 값의 주소만 넘기면 속도가 훨씬 빨라진답니다.

 

6. const를 이용한 포인터

const 예약어를 포인터에 사용하면 이는 가리키는 변수의 값을 상수로 지정한다는 의미로, 변수에 사용하는 것과는 다른 의미를 가진답니다.

 

대표적으로 

const int* p;를 선언해 볼까요?

 

다음과 같은 코드가 있습니다.

int a = 10;

const int* p = &a;

자, 이러면 p == &a (p는 a의 주소를 가리킴) 이고,

*p == a (p가 가리키는 값은 a) 가 된답니다.

 

그런데.. 만약 const가 없다면?

*p = 20;을 한다면 a == 20이 된답니다.

*p를 이용한 간접 참조를 통해 a의 값을 직접 바꿀 수 있게 되는 것이죠.

 

하지만 const가 있다면 *p = 20;이 불가능해요.

 

하지만 a 자체에는 const가 없으므로 a = 30; 등은 가능하답니다.

이러면 *p == 30이 되겠죠.

 

엄.. 이런 걸 어디다 쓰냐고요?

대표적으로는 문자열 상수를 인수로 받는 함수들이에요. 

문자열 상수는 프로그램 실행 중 메모리의 읽기 전용 영역에 저장됩니다.

즉, 문자열 상수는 값이 바뀌면 안 되므로, 매개변수를 통해서 값을 바꿀 수 없도록 설정한답니다.

 

const는 값을 바꾸면 안 되는 데이터를 보호하기 위해 쓰이며..

또는 편집자에게 이 값이 읽기 전용이라고 알려주려고 사용된답니다.

이상한 외부 참조에 의해 값이 바뀌는 것을 보호하고, 프로그램의 안정성과 코드 품질을 높여준답니다.

 

근데.. 뭔가 다른 방법도 가능할 거 같아요!

int* const p;는 어떨까요??

 

이건 의미가 달라요. 위에 말했던 const int* p; 는 포인터 변수를 통한 간접 참조를 막는다면,

int* const p;는 포인터 자체의 주소가 고정되지만 간접 참조(값 변경)는 가능하도록 만들어요.

 

앗, 그렇다면...

const int* const p;는 어떨까요?

이건 주소 변경도 불가능하고, 값도 변경 불가능한 포인터랍니다!

 

#include <stdio.h>

int main(void) 
{
    int a = 10;
    const int* p1 = &a; // 값 변경 불가, 주소 변경 가능
    // *p1 = 20; // 오류!
    // p1 = &b; // 가능

    int* const p2 = &a; // 주소 변경 불가, 값 변경 가능
    // *p2 = 20; // 가능
    // p2 = &b; // 오류!

    const int* const p3 = &a; // 둘 다 불가
    // *p3 = 20; // 오류!
    // p3 = &b; // 오류!

    return 0;
}

예제는 다음과 같아요.


요약하면, 

const int* p;는 "포인터가 가리키는 주소의 변수값을 바꾸지 마!" 고

int* const p;는 "포인터가 가리키는 주소의 값을 바꾸지 마!"고

const int* const p;는 "포인터가 가리키는 주소와 변수값 모두 바꾸지 마!"랍니다.

728x90

'프로그래밍 > C' 카테고리의 다른 글

배열과 포인터의 관계  (2) 2025.06.05
포인터 이해하기  (0) 2025.06.04
문자를 저장하는 배열  (0) 2025.06.02
배열의 선언과 사용  (0) 2025.06.01
여러 가지 함수 유형  (0) 2025.04.23