1학년 / 2학기 / C++ / 4강

포인터와 동적 메모리

포인터와 동적 메모리

학습 흐름

주소 &와 포인터 선언 → 역참조 * → 포인터 대입 추적
        ↓
const 포인터 4가지 → 포인터 매개변수 (값 / 포인터 / 참조 비교)
        ↓
배열과 포인터 → 포인터 연산과 *p++ → C 문자열 함수
        ↓
new / delete → 메모리 누수와 댕글링 포인터

1. 주소와 포인터

기호위치뜻
&x식변수 x의 주소
int* p선언p는 int의 주소를 저장하는 포인터
*p식p가 가리키는 곳의 값(역참조)
int& r선언참조(별명). 주소 연산자와 다른 의미
int n = 42;
int* p = &n;      // p는 n을 가리킴
*p = 100;         // n = 100
p ──▶ [ n : 100 ]
  • 포인터 자료형은 가리킬 값의 자료형과 맞아야 합니다. double* d = &n;은 컴파일 오류입니다.
  • 아무것도 가리키지 않을 때는 nullptr로 초기화합니다. 초기화하지 않은 포인터나 nullptr을 역참조하면 UB(보통 비정상 종료)입니다.
  • int* p = 5;처럼 정수를 바로 넣는 것은 컴파일 오류입니다.

다음 출력을 쓰시오.

int a = 10, b = 20;
int* p = &a;
int* q = &b;
*p = *q + 5;
p = q;
*p = 7;
cout << a << ' ' << b << ' ' << *p << ' ' << *q << ' ' << (p == q);
정답 확인
25 7 7 7 1
문장의미abp가 가리킴
*p = *q + 5a = b + 52520a
p = q주소 복사 (화살표 이동)2520b
*p = 7b = 7257b

p = q는 값이 아니라 주소를 복사합니다. 이후 *p와 *q는 같은 변수입니다.

다음 선언에서 (1) p[0] == n의 참·거짓, (2) p = &k;의 대입 가능 여부를 쓰고, (3) 배열 arr의 arr[i]를 포인터 연산으로 쓰시오.

int n = 3;
int& k = n;
int* p = &n;
정답 확인

(1) 참. p[0]은 *(p + 0) = *p = n입니다. (2) 가능. k는 n의 별명이므로 &k는 &n과 같은 주소입니다. (3) *(arr + i).


2. const와 포인터

const가 *의 왼쪽에 있으면 가리키는 값을, 오른쪽에 있으면 포인터 자체를 고정합니다.

선언*p = 5 (값 변경)p = &m (가리키는 곳 변경)읽는 법
int* pOO
const int* p (= int const* p)XO상수를 가리키는 포인터
int* const pOX상수 포인터 (초기화 필수)
const int* const pXX둘 다 고정
const int c = 9;
int* p4 = &c;          // 컴파일 오류: const 값을 바꿀 수 있는 포인터로 가리킬 수 없음
const int* p5 = &c;    // OK

컴파일 오류가 나는 줄을 모두 고르시오.

int n = 1, m = 2;
const int* p1 = &n;
*p1 = 5;               // (1)
p1 = &m;               // (2)
int* const p2 = &n;
*p2 = 5;               // (3)
p2 = &m;               // (4)
const int* const p3 = &n;
*p3 = 5;               // (5)
p3 = &m;               // (6)
const int c = 9;
int* p4 = &c;          // (7)
const int* p5 = &c;    // (8)
정답 확인

오류: (1), (4), (5), (6), (7). const int*는 값을, int* const는 주소를 바꿀 수 없습니다. const int*가 n처럼 const가 아닌 변수를 가리키는 것은 괜찮습니다(그 포인터로 바꿀 수 없을 뿐).


3. 포인터 매개변수: 값 / 포인터 / 참조

void s1(int a, int b)   { int t = a; a = b; b = t; }        // 복사본 교환
void s2(int* a, int* b) { int t = *a; *a = *b; *b = t; }    // 가리키는 값 교환
void s3(int* a, int* b) { int* t = a; a = b; b = t; }       // 지역 포인터만 교환
void s4(int& a, int& b) { int t = a; a = b; b = t; }        // 원본 교환
구분포인터 매개변수참조 매개변수
선언void f(int* p)void f(int& r)
호출f(&x)f(x)
안에서 사용*pr
"없음" 표현nullptr 가능불가(반드시 대상이 있음)
다른 대상으로 바꾸기가능불가

int x = 1, y = 2;에서 시작해 아래를 차례로 실행할 때 각 호출 뒤의 x y를 쓰시오.

s1(x, y);   s2(&x, &y);   s3(&x, &y);   s4(x, y);
정답 확인
호출x y이유
s1(x, y)1 2값 복사본만 교환
s2(&x, &y)2 1*a, *b로 원본 교환
s3(&x, &y)2 1포인터 변수(복사본)끼리만 교환, 값은 그대로
s4(x, y)1 2참조로 원본 교환

s3이 함정입니다. 포인터도 값으로 전달되므로 함수 안에서 a = b를 해도 호출한 쪽에는 영향이 없습니다.

포인터 매개변수로 여러 결과를 돌려주기:

void minmax(const int a[], int n, int* mn, int* mx) {
    *mn = *mx = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] < *mn) *mn = a[i];
        if (a[i] > *mx) *mx = a[i];
    }
}
int lo, hi;
minmax(arr, 4, &lo, &hi);     // {4, -2, 9, 0} → lo = -2, hi = 9

4. 배열과 포인터

배열 이름은 대부분의 식에서 첫 원소의 주소(&a[0])로 바뀝니다.

a[i]  ==  *(a + i)        &a[i]  ==  a + i        p[i]  ==  *(p + i)
비교배열 int a[5]포인터 int* p = a
sizeof20 (원소 5개 × 4바이트)8 (64비트 환경의 주소 크기)
a = p;, a++컴파일 오류 (배열 이름은 대입·증가 불가)p++ 가능
함수 매개변수 int a[]실제로는 int* a와 같음

포인터 연산

p + 1은 주소를 1바이트가 아니라 원소 하나만큼(int면 4바이트) 이동합니다. 같은 배열 안의 두 포인터를 빼면 원소 개수 차이가 나옵니다.

*p++와 (*p)++ (우선순위)

후위 ++가 단항 *보다 먼저 묶입니다.

식묶음하는 일식의 값
*p++*(p++)포인터를 다음 칸으로, 값은 이동 전 칸이동 전 원소
(*p)++가리키는 값을 1 증가증가 전 값
*++p*(++p)포인터를 먼저 이동이동 후 원소
++*p++(*p)값을 먼저 증가증가 후 값

다음 출력을 쓰시오.

int arr[] = {10, 20, 30, 40};
int* p = arr;
cout << *p++ << ' ';
cout << (*p)++ << ' ';
cout << *++p << ' ';
cout << ++*p << ' ';
cout << *(p + 1) << ' ' << p[-1] << ' ' << p - arr << '\n';
for (int v : arr) cout << v << ' ';
정답 확인
10 20 30 31 40 21 2
10 21 31 40
식출력p 위치배열
*p++10110 20 30 40
(*p)++20110 21 30 40
*++p302
++*p31210 21 31 40
*(p + 1), p[-1], p - arr40, 21, 22

int a[5] = {1, 2, 3, 4, 5}; int* p = a;일 때 출력과 오류를 판정하시오. (64비트 환경)

cout << sizeof(a) << ' ' << sizeof(p) << ' ' << *(a + 2) << ' '
     << *(p + 4) << ' ' << (a + 2 == &a[2]) << '\n';
a = p;      // (가)
a++;        // (나)
p++;        // (다)
정답 확인

출력: 20 8 3 5 1. (가)와 (나)는 컴파일 오류(배열 이름은 대입·증가할 수 없음), (다)는 정상입니다.

2차원 배열도 메모리에 한 줄로 있으므로 int* p = &m[0][0];이면 *(p + i * 열수 + j)가 m[i][j]입니다. int m[2][3]에서 *(p + 4)는 m[1][1]입니다.


5. C 문자열과 <cstring>

C 문자열은 '\0'으로 끝나는 char 배열입니다(기초는 3단원). 포인터로 순회할 수도 있습니다.

const char* s = "abc";
while (*s) cout << *s++;      // '\0'(0)이면 거짓 → 종료. 출력: abc
함수역할주의
strlen(s)'\0' 앞까지의 문자 수sizeof(배열 전체 크기)와 다름
strcpy(dst, src)복사dst가 충분히 커야 함
strcat(dst, src)뒤에 이어 붙이기dst가 충분히 커야 함
strcmp(a, b)같으면 0, a가 사전순으로 앞이면 음수, 뒤면 양수대문자(65~)가 소문자(97~)보다 앞

다음 출력을 쓰시오.

char a[20] = "Hello";
char b[] = "World";
cout << strlen(a) << ' ' << sizeof(a) << ' ' << sizeof(b) << ' ';
strcat(a, b);
cout << a << ' ' << strlen(a) << ' ';
cout << (strcmp("abc", "abd") < 0) << ' ' << strcmp("abc", "abc") << ' '
     << (strcmp("b", "a") > 0) << ' ' << (strcmp("Zoo", "apple") < 0) << '\n';
정답 확인
5 20 6 HelloWorld 10 1 0 1 1

"Zoo" < "apple"인 이유: 'Z'(90)가 'a'(97)보다 작기 때문입니다.


6. 동적 메모리: new / delete

int* one = new int(42);        // int 하나
delete one;
 
int n;
cin >> n;
int* arr = new int[n];         // 실행 중에 크기 결정
arr[0] = 1;                    // 일반 배열처럼 사용
delete[] arr;                  // 배열은 delete[]
arr = nullptr;                 // 댕글링 방지
할당해제짝이 틀리면
new Tdelete pUB
new T[n]delete[] pUB
문제상황
메모리 누수해제하지 않은 채 주소를 잃음: p = new int(1); p = new int(2);
댕글링 포인터해제한 뒤에도 그 주소를 사용: delete p; *p = 3;
이중 해제같은 메모리를 두 번 delete (UB). nullptr에 대한 delete는 안전
스택 변수 해제int x; delete &x; (UB). new로 받은 것만 해제

다음 출력을 쓰고, 마지막에 필요한 문장을 쓰시오.

int n = 4;
int* d = new int[n];
for (int i = 0; i < n; i++) d[i] = i * i;
int* e = d + 1;
e[1] = 100;
for (int i = 0; i < n; i++) cout << d[i] << ' ';
정답 확인

0 1 100 9. e는 d[1]을 가리키므로 e[1]은 d[2]입니다. 마지막에 delete[] d;가 필요합니다. e를 따로 해제하면 이중 해제가 됩니다.

이중 포인터

int x = 5;
int* p = &x;
int** pp = &p;    // 포인터의 주소
**pp = 9;         // x = 9

7. vector와 스마트 포인터

직접 new[]/delete[]를 쓰는 대신 vector<int> v(n);을 쓰면 메모리가 자동으로 해제됩니다. 객체 하나를 동적으로 관리할 때는 unique_ptr<int> p = make_unique<int>(42);처럼 스마트 포인터를 씁니다(<memory>).


핵심 정리

  1. &x는 주소, *p는 가리키는 값. p = q는 주소 복사, *p = *q는 값 복사.
  2. const가 * 왼쪽이면 값 고정, 오른쪽이면 포인터 고정.
  3. 포인터도 값으로 전달된다. 함수 안에서 포인터 변수끼리 바꿔도 바깥은 그대로다.
  4. a[i] == *(a + i), p[0] == *p. 배열 이름은 대입·증가 불가, sizeof가 포인터와 다르다.
  5. *p++는 포인터 이동, (*p)++는 값 증가.
  6. 문자열 비교는 strcmp, 복사는 strcpy, 길이는 strlen.
  7. new↔delete, new[]↔delete[]. 해제 후 nullptr.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석