포인터와 동적 메모리
포인터와 동적 메모리
학습 흐름
주소 &와 포인터 선언 → 역참조 * → 포인터 대입 추적
↓
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 = 100p ──▶ [ 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| 문장 | 의미 | a | b | p가 가리킴 |
|---|---|---|---|---|
*p = *q + 5 | a = b + 5 | 25 | 20 | a |
p = q | 주소 복사 (화살표 이동) | 25 | 20 | b |
*p = 7 | b = 7 | 25 | 7 | b |
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* p | O | O | |
const int* p (= int const* p) | X | O | 상수를 가리키는 포인터 |
int* const p | O | X | 상수 포인터 (초기화 필수) |
const int* const p | X | X | 둘 다 고정 |
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) |
| 안에서 사용 | *p | r |
| "없음" 표현 | 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 = 94. 배열과 포인터
배열 이름은 대부분의 식에서 첫 원소의 주소(&a[0])로 바뀝니다.
a[i] == *(a + i) &a[i] == a + i p[i] == *(p + i)| 비교 | 배열 int a[5] | 포인터 int* p = a |
|---|---|---|
sizeof | 20 (원소 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++ | 10 | 1 | 10 20 30 40 |
(*p)++ | 20 | 1 | 10 21 30 40 |
*++p | 30 | 2 | |
++*p | 31 | 2 | 10 21 31 40 |
*(p + 1), p[-1], p - arr | 40, 21, 2 | 2 |
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 T | delete p | UB |
new T[n] | delete[] p | UB |
| 문제 | 상황 |
|---|---|
| 메모리 누수 | 해제하지 않은 채 주소를 잃음: 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 = 97. vector와 스마트 포인터
직접 new[]/delete[]를 쓰는 대신 vector<int> v(n);을 쓰면 메모리가 자동으로 해제됩니다. 객체 하나를 동적으로 관리할 때는 unique_ptr<int> p = make_unique<int>(42);처럼 스마트 포인터를 씁니다(<memory>).
핵심 정리
&x는 주소,*p는 가리키는 값.p = q는 주소 복사,*p = *q는 값 복사.const가*왼쪽이면 값 고정, 오른쪽이면 포인터 고정.- 포인터도 값으로 전달된다. 함수 안에서 포인터 변수끼리 바꿔도 바깥은 그대로다.
a[i] == *(a + i),p[0] == *p. 배열 이름은 대입·증가 불가,sizeof가 포인터와 다르다.*p++는 포인터 이동,(*p)++는 값 증가.- 문자열 비교는
strcmp, 복사는strcpy, 길이는strlen. new↔delete,new[]↔delete[]. 해제 후nullptr.
내용을 이해했다면 학습 완료를 체크해 주세요.