SW 전공

SW 전공 이론 학습

이론 목록으로 돌아가기

메모리, 포인터와 참조

프로그램 메모리 구조와 C 포인터, Java·Python 참조, 얕은 복사·깊은 복사를 학습한다.

예상 읽기 9

1. 프로그램의 대표 메모리 영역

운영체제와 실행 형식에 따라 세부 구조는 달라질 수 있지만, 개념적으로 다음 영역을 구분한다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
높은 주소
┌──────────────────────────┐
│ 스택                     │ 함수 호출 프레임·지역변수
│        ↓                 │
├──────────────────────────┤
│          사용 가능 공간  │
├──────────────────────────┤
│        ↑                 │
│ 힙                       │ 동적 할당 객체·메모리
├──────────────────────────┤
│ 데이터·BSS               │ 전역·정적 변수
├──────────────────────────┤
│ 코드 영역                │ 실행 명령
└──────────────────────────┘
낮은 주소
  • 스택: 함수 호출과 반환에 따라 자동 관리된다.
  • 힙: 동적으로 할당하며 명시적 해제 또는 가비지 컬렉션으로 관리한다.
  • 정적 영역: 프로그램 실행 기간 동안 유지되는 전역·정적 데이터를 저장한다.
  • 코드 영역: 실행할 명령을 저장하며 일반적으로 직접 수정하지 않는다.

2. C 포인터의 기본

포인터는 메모리 주소를 저장하는 변수이다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int x = 10;
int *p = &x;

printf("%d\n", *p); // 10
*p = 20;             // x를 20으로 변경
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
p 변수                    x 변수
┌─────────────┐           ┌──────────┐
│ 주소 0x1000 ├──────────►│ 값 10    │
└─────────────┘           └──────────┘
      &x = 0x1000              *p = 10
  • &x: x의 주소
  • *p: p가 가리키는 위치의 값
  • int *p: int를 가리키는 포인터
  • NULL: 유효한 객체를 가리키지 않음을 표현하는 포인터 값

*는 선언에서는 포인터형을 나타내고, 식에서는 역참조 연산자가 될 수 있다.

3. 포인터와 배열

배열 이름은 많은 식에서 첫 원소를 가리키는 포인터처럼 변환되지만 배열과 포인터 변수는 동일한 개념은 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[3] = {10, 20, 30};
int *p = a;

printf("%d\n", a[1]);      // 20
printf("%d\n", *(p + 1)); // 20

포인터 연산은 바이트가 아니라 가리키는 자료형의 크기 단위로 이동한다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
p       p+1     p+2
│        │       │
▼        ▼       ▼
[10]    [20]    [30]

배열 범위를 벗어난 위치를 역참조하면 정의되지 않은 동작이 될 수 있다.

4. 동적 메모리 관리

C에서는 malloc, calloc, realloc, free를 사용한다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int *numbers = malloc(5 * sizeof *numbers);
if (numbers == NULL) {
    return 1;
}

for (int i = 0; i < 5; i++) {
    numbers[i] = i * 10;
}

free(numbers);
numbers = NULL;

대표 오류는 다음과 같다.

  • 메모리 누수: 할당한 메모리를 더 이상 해제할 수 없는 상태
  • 댕글링 포인터: 해제되었거나 수명이 끝난 객체를 계속 가리키는 포인터
  • 이중 해제: 같은 메모리를 두 번 해제
  • NULL 역참조: 유효한 객체가 없는 포인터를 사용
  • 버퍼 범위 초과: 할당 범위를 넘어 읽거나 쓰기
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
malloc → 주소 보관 → 사용 → free → 포인터 사용 금지
                              └─ 필요하면 NULL 대입

5. 포인터의 const 조합

C코드 영역 안에서 좌우로 이동할 수 있습니다.
const int *p1;       // 가리키는 값을 p1을 통해 변경하지 않음
int * const p2 = &x; // p2 자체가 다른 주소를 가리키지 않음
const int * const p3 = &x; // 둘 다 제한

선언을 오른쪽에서 왼쪽으로 읽으면 도움이 된다.

6. Java의 참조

Java의 클래스형 변수는 객체 자체가 아니라 객체를 가리키는 참조값을 저장한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
Box a = new Box(10);
Box b = a;
b.value = 20;
System.out.println(a.value); // 20
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
a ─┐
   ├────► Box 객체 { value: 20 }
b ─┘

ab가 같은 객체를 가리키므로 한 참조를 통해 객체 상태를 바꾸면 다른 참조에서도 보인다. a == b는 기본적으로 두 참조가 같은 객체를 가리키는지 비교하고, 내용 동등성은 적절히 구현된 equals로 비교한다.

Java는 가비지 컬렉션을 사용하지만 파일·소켓·DB 연결 같은 외부 자원은 별도 정리가 필요할 수 있다. 이런 자원은 try-with-resources 같은 구조로 닫는다.

7. Python의 객체 참조

Python 변수는 객체에 붙는 이름으로 이해할 수 있다.

PYTHON코드 영역 안에서 좌우로 이동할 수 있습니다.
a = [1, 2]
b = a
b.append(3)
print(a)  # [1, 2, 3]
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
이름 a ─┐
        ├────► 리스트 객체 [1, 2, 3]
이름 b ─┘

불변 객체인 정수·문자열·튜플은 내용 자체를 변경하지 않고 새 객체에 재바인딩된다.

PYTHON코드 영역 안에서 좌우로 이동할 수 있습니다.
x = 10
y = x
y += 1
print(x, y)  # 10 11

8. 얕은 복사와 깊은 복사

중첩 객체를 복사할 때 내부 객체를 공유하는지 확인해야 한다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
원본 ─────► [ 내부 리스트 A ]

얕은 복사 ─► 새 바깥 리스트 ─┐
                             └──► 내부 리스트 A 공유

깊은 복사 ─► 새 바깥 리스트 ───► 새 내부 리스트 B
PYTHON코드 영역 안에서 좌우로 이동할 수 있습니다.
import copy

original = [[1], [2]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)

shallow[0].append(9)
print(original)  # [[1, 9], [2]]
print(deep)      # [[1], [2]]

Java에서도 객체 복사는 단순 참조 대입, 필드 복사, 복사 생성자 등 방법에 따라 공유 범위가 달라진다.

9. 함수 호출과 주소 전달

C코드 영역 안에서 좌우로 이동할 수 있습니다.
void increment(int *p) {
    if (p != NULL) {
        (*p)++;
    }
}

int main(void) {
    int x = 5;
    increment(&x);
    // x는 6
    return 0;
}

포인터를 함수에 전달하는 것도 포인터 값의 복사이다. 복사된 포인터가 같은 객체 주소를 담고 있어 원본 객체를 변경할 수 있는 것이다.

10. 배열의 포인터 변환 예외

배열 이름은 대부분의 식에서 첫 원소 포인터로 변환되지만 대표 예외가 있다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[5];
sizeof a;  // 배열 전체 크기
&a;        // 배열 전체를 가리키는 포인터: int (*)[5]

함수 매개변수의 int a[]는 실제로 int *a로 조정되므로 함수 안의 sizeof a는 배열 전체 크기가 아니라 포인터 크기다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
int *p[4]    : int 포인터 4개의 배열
int (*p)[4]  : int 4개짜리 배열을 가리키는 포인터

괄호 위치가 자료형을 바꾼다.

11. one-past 포인터와 포인터 차이

배열의 마지막 원소 바로 다음 위치를 가리키는 포인터는 비교·반복 종료 표식으로 만들 수 있지만 역참조하면 안 된다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[4] = {1,2,3,4};
for (int *p=a; p<a+4; ++p) {
    printf("%d ", *p);
}

포인터 뺄셈과 순서 비교는 같은 배열 객체 안의 원소 또는 one-past 포인터 사이에서만 의미가 보장된다. 서로 무관한 배열의 포인터를 빼거나 순서 비교하지 않는다.

12. realloc의 안전한 사용

realloc은 같은 주소를 확장할 수도 있고 새 위치로 옮길 수도 있다. 실패하면 NULL을 반환하며 원래 블록은 그대로 유효하다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int *temp = realloc(numbers, new_count * sizeof *numbers);
if (temp != NULL) {
    numbers = temp;
} else {
    /* numbers는 여전히 유효하므로 필요하면 해제 */
}

numbers = realloc(numbers, ...)처럼 즉시 덮어쓰면 실패 시 원래 주소를 잃어 누수가 발생할 수 있다. 성공해 블록이 이동했다면 이전 주소와 그 주소를 복사한 별칭은 더 이상 사용하면 안 된다.

13. 소유권·수명·별칭 검사표

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
누가 할당했는가?
누가 해제할 책임이 있는가?
현재 포인터가 유효한 객체 수명 안에 있는가?
같은 주소를 가진 다른 별칭이 있는가?
함수 종료 후 지역 객체 주소를 반환하지 않았는가?

free(p); p=NULL;은 p를 통한 재사용은 막지만, 같은 주소를 가진 다른 포인터까지 자동으로 NULL로 만들지는 않는다.

14. 이중 포인터로 호출자 포인터 변경

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int allocate_one(int **out) {
    if (out == NULL) return 0;
    *out = malloc(sizeof **out);
    if (*out == NULL) return 0;
    **out = 42;
    return 1;
}

C는 포인터값도 값으로 전달하므로 호출자의 포인터 변수 자체를 변경하려면 그 포인터의 주소, 즉 이중 포인터를 전달한다.

확인 문제

  1. 함수 밖의 int a[5]에 대해 sizeof a는 무엇의 크기를 구하는가?
  2. int *p[4]int (*p)[4]의 차이는 무엇인가?
  3. 배열의 one-past 포인터를 역참조해도 되는가?
  4. realloc 실패 시 원래 메모리 블록은 어떻게 되는가?
  5. free(p); p=NULL;이 다른 별칭 포인터까지 안전하게 만드는가?