icon안동민 개발노트

포인터 기초


포인터의 개념

 포인터는 메모리 주소를 저장하는 변수입니다. 이를 통해 간접적으로 데이터에 접근하고 조작할 수 있습니다. 포인터는 동적 메모리 할당, 효율적인 데이터 구조 구현, 함수에 인자 전달 등 다양한 상황에서 중요한 역할을 합니다.

포인터 선언과 초기화

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

int* ptr;  // int 타입을 가리키는 포인터
char* cptr;  // char 타입을 가리키는 포인터
double* dptr;  // double 타입을 가리키는 포인터
포인터 초기화
int x = 10;
int* ptr = &x;  // x의 주소를 ptr에 저장
 
// 또는
int* ptr2 = nullptr;  // 널 포인터로 초기화 (C++11 이후)

주소 연산자 &와 역참조 연산자 *

  • 주소 연산자 & : 변수의 메모리 주소를 얻습니다.
  • 역참조 연산자 * : 포인터가 가리키는 메모리 위치의 값에 접근합니다.
예제
#include <iostream>
 
int main() {
    int x = 10;
    int* ptr = &x;
 
    std::cout << "x의 값: " << x << std::endl;
    std::cout << "x의 주소: " << ptr << std::endl;
    std::cout << "ptr이 가리키는 값: " << *ptr << std::endl;
 
    *ptr = 20;  // x의 값을 간접적으로 변경
    std::cout << "변경 후 x의 값: " << x << std::endl;
 
    return 0;
}

포인터와 배열

 배열 이름은 첫 번째 요소의 주소를 나타냅니다.

#include <iostream>
 
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* p = arr;  // p는 arr[0]의 주소를 가짐
 
    for(int i = 0; i < 5; i++) {
        std::cout << "arr[" << i << "] = " << *p << std::endl;
        p++;  // 다음 요소로 이동
    }
 
    return 0;
}

포인터 연산

 포인터에 정수를 더하거나 빼면 해당 타입의 크기만큼 주소가 변경됩니다.

int* p = arr;
p++;  // 다음 정수의 주소로 이동 (4바이트 증가)
p += 2;  // 2개의 정수만큼 이동 (8바이트 증가)

널 포인터

 널 포인터는 유효하지 않은 메모리 주소를 나타냅니다.

int* ptr = nullptr;  // C++11 이후
// int* ptr = NULL;  // C++11 이전 (하위 호환성을 위해 여전히 사용 가능)
 
if (ptr == nullptr) {
    std::cout << "ptr은 널 포인터입니다." << std::endl;
}

void 포인터

 void 포인터는 모든 타입의 포인터로 변환될 수 있는 일반적인 포인터 타입입니다.

void* vptr;
int x = 10;
vptr = &x;  // 허용됨
 
// 사용 시 적절한 타입으로 캐스팅 필요
int* iptr = static_cast<int*>(vptr);

const와 포인터

 const와 포인터의 조합은 다음과 같습니다.

int x = 10, y = 20;
 
const int* p1 = &x;  // 포인터가 가리키는 값을 변경할 수 없음
// *p1 = 30;  // 오류
p1 = &y;  // 허용됨
 
int* const p2 = &x;  // 포인터 자체를 변경할 수 없음
*p2 = 30;  // 허용됨
// p2 = &y;  // 오류
 
const int* const p3 = &x;  // 둘 다 변경할 수 없음
// *p3 = 30;  // 오류
// p3 = &y;  // 오류

포인터의 위험성

  1. 댕글링 포인터 : 해제된 메모리를 가리키는 포인터
  2. 메모리 누수 : 할당된 메모리를 해제하지 않음
  3. 버퍼 오버플로우 : 배열의 범위를 벗어난 접근

 예방 방법

  • 포인터 사용 후 nullptr로 설정
  • 스마트 포인터 사용
  • 배열 접근 시 범위 검사

스마트 포인터 소개 (C++ 11 이후)

 C++ 11부터 도입된 스마트 포인터는 메모리 관리를 자동화합니다.

  1. std::unique_ptr : 단일 소유권 포인터
  2. std::shared_ptr : 공유 소유권 포인터
  3. std::weak_ptr : std::shared_ptr의 순환 참조 문제 해결

 간단한 예

#include <iostream>
#include <memory>
 
int main() {
    std::unique_ptr<int> uptr = std::make_unique<int>(10);
    std::cout << "uptr 값: " << *uptr << std::endl;
 
    std::shared_ptr<int> sptr = std::make_shared<int>(20);
    std::cout << "sptr 값: " << *sptr << std::endl;
 
    return 0;
}

연습 문제

  1. 정수형 포인터를 사용하여 두 변수의 값을 교환하는 함수를 작성하세요.
  2. 문자열(C-style string)의 길이를 계산하는 함수를 포인터를 사용하여 구현하세요.
  3. 정수 배열을 가리키는 포인터를 인자로 받아, 배열의 모든 요소를 출력하는 함수를 작성하세요.
  4. void 포인터를 사용하여 어떤 타입의 변수든 그 값을 출력할 수 있는 범용 출력 함수를 구현하세요.


참고 자료

  • C++ 공식 문서의 포인터 섹션 : Pointer declarations
  • "Effective C++" by Scott Meyers (항목 13 : 자원 관리에는 객체가 그만!)
  • "C++ Primer" by Stanley B. Lippman, Josée Lajoie, and Barbara E. Moo (Chapter 2 : Variables and Basic Types)
  • C++ Core Guidelines의 포인터 관련 규칙 : ES.42 : Keep pointers to members simple and direct
  • "A Tour of C++" by Bjarne Stroustrup (Chapter 13 : Resource Management)