← Documents

포인터와 객체 메모리 · C++ & Java

🧠

한 줄 소개 — "변수·객체가 메모리에 어떻게 올라가는가"를 두 언어로 비교한 노트다. C++ 로 포인터(&·*·포인터 산술·배열)를 직접 들여다본 뒤, 같은 개념이 Java 에선 스택/힙 + 참조 + GC 로 어떻게 추상화됐는지 이어 본다. 알고리즘을 Java 로 풀기 전에 "객체가 메모리에 어떻게 저장되는지"를 이해하면 자료구조 동작이 훨씬 선명해진다.

1 · 변수는 메모리에 어떻게 저장되나 (C++)

변수를 선언하면 그 타입 크기 만큼 메모리(셀, 한 칸 = 1byte)를 차지한다. 할당된 메모리의 맨 앞 셀 주소 가 그 변수의 대표 주소이고, &(ampersand) 로 그 주소를 얻는다.

char lang = 'c'; // 1 byte int year = 2022; // 4 byte double pi = 3.14592; // 8 byte
메모리 셀 (1칸 = 1 byte) c char · 1B 2022int · 4B 3.14592double · 8B ↑ 각 변수의 대표 주소 = 첫 셀의 주소 (&변수 로 확인)
타입 크기만큼 연속된 셀을 차지하고, 첫 셀의 주소가 그 변수의 대표 주소가 된다

2 · Call by value — 깊은 복사

변수를 함수에 값으로 넘기면(call by value), 매개변수를 위한 메모리가 따로 생기고 값이 복사(깊은 복사)된다. 큰 데이터라면 그만큼 메모리가 중복으로 낭비된다.

void func1(int parameter) { /* parameter = argument 의 복사본 (새 메모리) */ } int argument = 10; func1(argument); // argument 값이 parameter 로 깊은 복사됨
Call by value — 값이 "다른 메모리"로 복제된다 argument 10 int · @ 0x..a0 parameter 10 int · @ 0x..c8 (다른 주소!) 깊은 복사 (값 복제) 같은 값 10 이 메모리 두 곳에 중복 — 큰 데이터일수록 낭비. 그래서 "주소만" 넘기는 포인터.
값으로 넘기면 매개변수를 위한 셀이 따로 생기고 값이 복제된다 (서로 다른 주소)
💡

포인터의 당위성 — 값 전체를 복사하지 말고, 그 변수가 있는 메모리 주소만 넘겨 거기에 접근하게 하면 복사 비용이 사라진다. 이것이 포인터를 쓰는 이유다(메모리 절약).

3 · 포인터 — & 와 * (C++)

포인터주소값을 저장하는 변수 다. &변수 로 주소를 얻고, 그 주소는 일반 변수가 아니라 * 로 선언한 포인터 변수 에 담는다. 선언은 int *p, 사용할 땐 p(주소), *p(가리키는 곳의 값, 역참조)다.

int a = 231; int *b = &a; // b = a 의 주소를 담는 포인터 cout << sizeof(a); // 4 (int) cout << &a; // 0x70fd88 (a 의 주소) cout << b; // 0x70fd88 (b 가 담은 주소 = &a) cout << *b; // 231 (역참조 → a 의 값) cout << sizeof(b); // 8 (포인터는 64bit 시스템에서 8byte) cout << &b; // 0x70fd80 (포인터 자신의 주소)
포인터 b 가 변수 a 를 가리킨다 a = 231@ 0x70fd88 b = 0x70fd88포인터 · @ 0x70fd80 *b 로 역참조 b 는 a 의 "주소"를 값으로 가지고, *b 는 그 주소의 내용(231)을 본다
포인터는 가리킬 변수의 주소를 담는다 — b(주소) / *b(값) / &b(포인터 자신의 주소)

int *p 처럼 타입을 붙일까? 포인터는 데이터가 어디서 시작하는지(주소)는 알지만 어디서 끝나는지 모른다. 앞의 타입이 "이 주소에서 몇 byte 를 읽어야 하는지"를 알려준다 — 그래서 포인터 산술에서도 이 타입이 기준이 된다.

4 · 포인터 산술 & 배열 (C++)

p + 1 은 주소에 1 byte 가 아니라 가리키는 타입 크기만큼 더해진다(int 면 +4, double 면 +8). 그래서 배열 과 잘 맞는다 — 배열 이름은 사실상 첫 원소의 주소(포인터) 이고, arr[i]*(arr + i) 와 같다.

int a = 231; int *b = &a; cout << *b + 1; // 232 (값 231 에 +1) cout << b + 1; // 0x70fd7c (주소가 +4 = int 크기만큼 이동) // 배열 = 첫 원소의 주소 void array_func(int *arr, int size) { for (int i = 0; i < size; i++) cout << *(arr + i); // == arr[i] } int numArr[4] = {1,5,2,3}; int size = sizeof(numArr) / sizeof(numArr[0]); // 전체크기/한칸크기 = 개수 array_func(numArr, size); // numArr == &numArr[0]
메모리 격자 — 배열은 연속된 셀, arr[i] = *(arr+i) 1 5 2 3 arr (arr+0)arr+1arr+2arr+3 0x70fd780x70fd7c0x70fd800x70fd84 +4 byte (int 크기) 포인터 +1 은 주소 +1byte 가 아니라 "가리키는 타입 크기"(int=4)만큼 이동한다
배열은 같은 타입 셀이 연속으로 놓인 것 — 첫 주소(arr)에서 타입 크기만큼 건너뛰며 arr+i 로 접근한다
⚠️

배열 decay 함정 — 배열을 함수 매개변수로 넘기면 첫 주소(포인터)만 전달되고 "어디까지가 배열인지" 정보는 사라진다. 그래서 함수 안에서 sizeof(arr)/sizeof(arr[0]) 를 하면 배열 개수가 아니라 포인터 크기(8)/원소크기 가 나와 엉뚱한 값이 된다. → size 를 따로 같이 넘겨야 한다.

5 · Java — 스택 vs 힙, 값 vs 참조

Java 엔 포인터 문법(*·&)이 없지만, 개념은 그대로 살아 있다 — 참조(reference) 가 사실상 "안전하게 감춰진 포인터"다. 메모리는 크게 둘로 나뉜다.

  • 스택(Stack) — 메서드의 지역변수. 기본형(primitive)(int·double·boolean…)은 값 자체 가 여기 올라간다. 메서드가 끝나면 사라진다.
  • 힙(Heap)new 로 만든 객체 가 여기 생성된다. 스택의 참조 변수 는 그 객체의 주소(참조) 만 들고 있다 — C++ 포인터와 정확히 대응.
int a = 10; // 기본형 → 스택에 값 10 자체 Person p = new Person("Lee"); // 객체는 힙에 생성, p(스택)는 그 주소(참조)만 보관 Person q = p; // 참조 복사 — q 와 p 는 "같은 힙 객체"를 가리킴 (C++ 의 두 포인터처럼) q.setName("Kim"); System.out.println(p.getName()); // "Kim" ← 같은 객체이므로
Stack Heap int a = 10 (값) Person p = ⟶ Person q = ⟶ Person 객체name="Kim" 기본형은 스택에 값으로, 객체는 힙에 — 참조 변수(p·q)는 같은 객체 주소를 가리킬 수 있다
Java 의 참조 변수 = 힙 객체의 주소를 든 "포인터". p = q 는 객체가 아니라 주소를 복사한다

6 · Java — == vs equals, 전달, GC

참조가 곧 주소라는 걸 알면 다음이 자연스럽다.

  • == vs .equals()==참조(주소) 가 같은지, .equals()내용(값) 이 같은지 비교한다. 그래서 값 비교엔 반드시 equals.
  • 인자 전달 — Java 는 항상 값 전달(pass by value) 인데, 객체의 경우 그 "값"이 참조(주소)의 복사본 이다. 그래서 메서드 안에서 객체 필드를 바꾸면 원본에 반영되지만, 참조 자체를 다른 객체로 바꿔치기 해도 호출부엔 영향이 없다.
  • GC(Garbage Collection) — C++ 은 delete 로 직접 해제하지만, Java 는 더 이상 아무 참조도 가리키지 않는 힙 객체를 가비지 컬렉터가 자동 회수 한다.
String s1 = new String("Lee"); String s2 = new String("Lee"); System.out.println(s1 == s2); // false — 서로 다른 객체(주소) System.out.println(s1.equals(s2)); // true — 내용은 같음 Person p = new Person("Lee"); p = null; // 이제 이 Person 객체는 아무도 안 가리킴 → GC 대상
🧭

정리 — C++ 포인터에서 직접 보던 "값 vs 주소"가 Java 에선 "기본형 vs 참조", "스택 vs 힙"으로 추상화됐을 뿐 본질은 같다. 이 그림이 머리에 있으면 ==/equals, 얕은/깊은 복사, 컬렉션이 객체를 어떻게 담는지가 한 번에 이해된다.


📓 2022년 제가 알고리즘을 공부하며 C++ 포인터와 Java 객체 메모리를 직접 정리한 노트입니다 · 원본 Notion · C++ Pointer ↗