Develog

C++의 포인터 본문

Technology/C++

C++의 포인터

Priv 2025. 8. 3. 13:36


 

 

1. 메모리 주소와 포인터

포인터는 값의 복사가 아닌 값의 참조를 기반으로 동작합니다.

C++는 기본적으로 값을 다룰 때 복사 연산(Call by Value)을 사용하기 때문에 원본 값의 오염을 방지하고 메모리 낭비를 방지하기 위해서는 포인터에 대한 이해가 매우 중요합니다.

포인터 변수에는 값 그 자체가 아닌, 값이 저장된 메모리 상의 주소를 담을 수 있습니다. 

메모리 주소를 담을 수 있는 변수가 존재한다면, 포인터 변수에 담을 메모리 주소값 그 자체를 표현하는 것도 가능해야 할 것입니다.

이러한 작업을 수행하기 위해 & 연산자와 * 연산자가 등장합니다.

 


 

2. & 연산자와 * 연산자로 포인터 다루기

포인터 변수는 값이 아닌 값의 주소를 저장하므로, 일반적인 변수와 다르게 취급해야 합니다.

포인터 변수를 선언할 때는 아래와 같이 * 연산자를 사용합니다.

int* pointer_int;

변수를 선언할 때 * 연산자를 붙여주게 되면 해당 변수(pointer_int)는 포인터 변수로 선언함을 의미합니다.

포인터 변수를 선언했으므로 이제 주소 값을 담아야 합니다.

값의 주소를 표현할 때는 & 연산자를 사용합니다. & 연산자는 해당 값의 메모리 주소를 가져오는 역할을 합니다.

int var_int = 6;
int* pointer_int = &var_int;

이제 포인터 변수(pointer_int)에 var_int 변수의 메모리 주소값이 담겼습니다.

메모리 주소는 환경과 조건에 따라 상시 달라지기 때문에 결과는 항상 달라지겠지만, 위의 두 변수(var_int, pointer_int)를 각각 출력하면 아래와 같은 형태로 출력될 것입니다.

int var_int = 6;
int* pointer_int = &var_int;

printf("var_int: %d\n", var_int);
printf("pointer_int: %p\n", pointer_int);
// OUTPUT //
var_int: 6
pointer_int: 0x16f3430f8

편의상 이해하기 쉽도록 std::cout 대신 printf( )를 사용하였습니다.

var_int는 예상할 수 있듯이 6이 그대로 출력되며, pointer_int는 메모리의 주소가 16진수로 출력됩니다.

pointer_int를 출력할 때 printf( )에 적용한 포맷을 보면 %d가 아닌 %p를 사용했음을 알 수 있습니다.

이는 위의 예시 코드처럼 특정 메모리 주소를 출력해야 할 때 사용되는 포맷입니다.

 

2.1. 메모리 주소에 저장된 실제 값에 접근하기

이제부터 조금씩 헷갈리기 시작합니다.

포인터 변수가 기억하는 메모리 주소에 저장된 데이터, 즉 pointer_int가 담고 있는 메모리 주소(0x16f3430f8)에 저장된 실제 값을 출력하고 싶다면 아래와 같이 작성해야 합니다.

int var_int = 6;
int* pointer_int = &var_int;

printf("var_int: %d\n", var_int);
printf("pointer_int: %d\n", *pointer_int);

안타깝게도 새로운 기호가 등장하지는 않았습니다. * 연산자가 다시 등장합니다.
(사실 매번 새로운 기호를 쓰도록 설계했다면 특수기호가 남아나질 않을 것 같기는 합니다)

이번에 사용된 *pointer_int 부분의 *연산자는 앞에서 다루었던 포인터 변수를 선언할 때 사용한 * 연산자와 의미가 다릅니다.

해당 포인터 변수가 기억하는 메모리 주소에 저장된 실제 데이터(정수값 6)에 접근하겠다는 의미가 됩니다.

이 때문에 prinf( )에서도 포맷을 지정할 때 %p가 아닌 %d를 사용하였습니다.

 

2.3. 참조에 의한 호출(Call-by-Reference) 사용하기

Java, C# 등과 같이 고수준 언어를 다룰 때를 떠올려보면, 대부분의 언어가 데이터 타입의 종류, 상황 등에 따라 값에 의한 호출(Call-by-Value) 또는 참조에 의한 호출(Call-by-Reference)을 알아서 결정해 줍니다.

아니면 Python처럼 처음부터 참조에 의한 호출을 기본으로 사용하고 특정한 경우에만 값에 의한 호출을 제한적으로 허용하는 사례도 있죠.

하지만 참으로 골치 아프게도 C/C++는 정반대입니다.

C는 애초부터 값에 의한 호출만 구현되어 있으며, C++ 또한 기본적으로 값에 의한 호출만 사용합니다.

이 때문에 C++에서 참조에 의한 호출 방식을 사용하고 싶다면 포인터의 원리를 이용해 직접 구현해야 합니다.

무슨 이런 괴팍하고 비효율적인 언어가 다 있냐고 욕할 수 있겠지만, C++만 해도 40년이 넘은 언어입니다. 그러려니 해야죠.

 

2.3.1. 참조를 위한 & 연산자

먼저 아래와 같이 간단한 함수를 작성한 뒤 살펴보겠습니다.

void print_sum(int a, int b) {
    int c = a + b;
    
    printf("%d + %d = %d\n", a, b, c);
}

int main() {
    int var_a = 1;
    int var_b = 2;
    
    print_sum(var_a, var_b);
    
    return 0;
}

print_sum( ) 함수는 정수형 값 a와 b를 매개변수로 받아 그 합을 출력하는 매우 단순한 함수입니다.

main( ) 함수에서는 var_a와 var_b를 정수형 변수로 선언하고 1과 2로 초기화했으며, 이 두 변수를 이용해 print_sum( ) 함수를 호출했습니다.

위 예시 코드는 참조에 의한 호출이 아닌 값에 의한 호출을 사용한 방식입니다.

즉, var_a와 var_b의 값이 print_sum( )에 매개변수로 전달될 때 복사 연산을 거친 뒤에 전달된다는 것입니다.

이 경우를 매우 극단적으로 과장해서 표현해 본다면 아래와 같은 불상사가 생길 수도 있습니다.

main( )

- var_a의 용량: 40GB

- var_b의 용량: 40GB

(복사 연산 처리)

print_sum( )

- a의 용량: 40GB (var_a에서 복사됨)

- b의 용량: 40GB (var_b에서 복사됨)

소비된 메모리 총 용량: 40GB * 4 (160GB)

물론, 현실에서 정수형 변수를 고작 4개 사용했다고 해서 메모리를 이렇게 마구잡이로 폭식하지는 않을 것입니다.

하지만 값이 복사된다는 것은 무슨 카드사 리볼빙 이자처럼 값을 호출해 사용하면 사용할수록 메모리 요구 공간도 그만큼 늘어난다는 것을 의미하기 때문에 상당히 꺼림칙한 것은 사실입니다.

그렇다면 var_a와 var_b를 참조에 의한 호출 방식으로 구현해 보겠습니다.

#include<cstdio>

void print_sum(int& a, int& b) {
    int c = a + b;
    
    printf("%d + %d = %d\n", a, b, c);
}

int main() {
    int var_a = 1;
    int var_b = 2;
    
    print_sum(var_a, var_b);
    
    return 0;
}

print_sum( ) 함수를 보면 매개변수에 & 연산자가 붙은 것을 알 수 있습니다.

연산자 종류는 사실 2개가 전부입니다.

하지만 지금 보는 것처럼 이 연산자를 어디에 어떻게 얼마나 사용하느냐에 따라 의미가 완전히 달라지기 때문에 손에 익기 전까지는 정말 미칠 노릇입니다.

여기서 사용된 & 연산자의 의미는 참조에 의한 호출 방식을 이용해 두 매개변수(a, b)를 사용하겠다는 의미입니다.

이제 참조를 통해 사용하므로 원본 데이터라고 할 수 있는 var_a, var_b 변수의 데이터가 오염될 수 있다는 점을 유념하고 코드를 작성해야 합니다.

 


 

3. 런타임과 컴파일 타임

C/C++는 컴파일러를 이용해 작성한 소스 코드를 번역하는 컴파일을 거친 뒤 실행되는 과정을 거치는 대표적인 컴파일 언어입니다.

Python은 프로그램을 실행하면 실시간(런타임)으로 코드를 번역하며 실행하는 대표적인 인터프리터 언어죠.

이 때문에 C++를 다룰 때 메모리의 할당과 해제, 변수의 선언과 제거 등 코드의 수명은 거의 고정된 상태를 유지합니다.

하지만 프로그램을 사용하는 과정에서, 즉 런타임 환경에서 메모리의 할당과 해제 등의 작업처럼 유동적으로 반응해야 하는 경우도 당연히 존재합니다.

이러한 경우, 사용자가 직접 코드를 짤 때 얼마만큼의 메모리 공간이 필요한지, 얼마만큼의 기간 동안 그 공간을 할당해 사용할 것인지를 정의할 수 있어야 합니다.

메모리 공간을 직접 할당하기 위해서는 new 키워드를 사용합니다. 이는 C#, Java 등 다른 언어에서도 익숙하게 볼 수 있는 키워드입니다.

이후 할당된 메모리 공간을 점유하여 사용이 끝나면 이 공간을 점유 해제해야 합니다.

여기서 골치 아픈 이야기가 한 번 더 나오게 됩니다.

C#이나 Java와 같이 일부 언어는 GC(Garbage Collector)가 존재하여 이제 사용되지 않는 메모리 공간이 감지되면 알아서 메모리 공간 할당을 해제하고 정리합니다.

하지만 안타깝게도 C++는 프로그래머를 너무 믿습니다. 아니 애초에 사람도 사람을 안 믿는데 도대체 무슨 배짱인지 솔직히 이해는 안 갑니다.

C++에서는 메모리 공간을 new 키워드로 char 타입의 배열을 아래와 같이 할당했다면,

char* list_str[] = new char[10];

아래와 같이 자신이 직접 메모리 할당을 제거해야 합니다. 여기서는 배열을 선언했으므로 delete 키워드 뒤에 [ ] 기호를 더했습니다.

delete[] list_str;

만약 new 키워드로 할당한 메모리 공간에 대한 제거 과정이 없다면, 메모리 누수 문제가 발생할 수 있어 매우 위험합니다.

물론, OS 상위 계층에서 구동되는 애플리케이션 프로세스는 사용이 끝나면 종료되기 때문에 큰 문제가 없을 수 있습니다.

하지만 OS처럼 상시 구동되고 있는 SW에서 이러한 문제가 발생하게 된다면, 안드로이드 5.0 시절처럼 상당히 심각한 문제가 될 수도 있습니다.

또한 메모리 공간을 할당할 때는 얼마나 많은 공간을 할당할 것인지를 명시해야 합니다.

여기서 한 가지 더 주의할 점은 delete 명령어는 메모리 공간에 접근하는 '방법'을 제거하는 것이지, 그 메모리 공간을 '청소'하는 것이 아니라는 점입니다.

즉, delete 명령어를 사용했다 하더라도 해당 메모리 주소에 접근할 수 있는 방법이 사라지는 것이지, 해당 메모리 주소에 저장되어 있던 데이터까지 사라지는 것을 보장하진 않습니다. 

C++ 프로그래머 분들도 바보는 아니기 때문에 이처럼 위험한 작업을 자동화하여 편리하고 안전하게 사용할 수 있는 '스마트 포인터' 기능을 구현하였습니다.

이 부분에 대한 자세한 설명은 아래 링크를 참고해주십시오.


 

스마트 포인터(최신 C++)

자세한 정보: 스마트 포인터(최신 C++)

learn.microsoft.com



 

4. 포인터와 숫자

포인터는 정수형 타입이 아닙니다.

물론, 컴퓨터가 메모리 주소를 정수 형태로 다루기는 하지만 그것이 포인터 변수가 항상 정수형 타입이라는 의미는 아닙니다.

예를 들어 두 개의 포인터 변수가 가지고 있는 주소 값들을 곱한다고 생각해 봅시다.

그건 각 주소에 들어있는 데이터를 가져와 곱하는 것과 다른 의미입니다. 그냥 주소를 변형하는 것뿐이죠.

int* pt;
pt = 0xB8000000;    // ERROR

위 코드는 타입이 맞지 않아 에러를 발생시킵니다.

pt라는 이름의 포인터 변수를 선언하였고 거기에 주소를 직접 할당하려고 했지만, 타입 에러가 발생합니다.

여기서 사용된 0xB8000000 값은 16진수 값이지, 메모리 주소가 아니기 때문입니다.

int* pt;
pt = (int*) 0xB8000000;

이렇게 코드를 짜는 건 당연히 추천하지 않지만, 이제는 타입 에러가 발생하지 않을 것입니다.

(int*)를 통해 형변환 작업을 거치면서 0xB8000000이라는 값을 메모리 주소로 변환했기 때문입니다.

정말 끔찍하지만, C++는 이 값을 메모리 주소로 인식할 것입니다.

 


 


수고하셨습니다!


'Technology > C++' 카테고리의 다른 글

템플릿 클래스의 람다식 형식 추론  (0) 2025.08.20
const와 constexpr  (0) 2025.08.06
static_assert와 non-type template  (0) 2025.07.30
strcpy() 와 strncpy()  (0) 2025.07.14
C++의 함수 프로토타입  (0) 2024.06.26
Comments