본문 바로가기
연구소👨‍💻/CS연구소

[C++] 포인터, 참조

by 신그자체김상범 2024. 5. 3.

포인터는 C하면 가장 큰 특징으로 생각되는 용어다. 그만큼 질문도 많고 내용도 많다. 그러다보니 겉핥기만 허게되기 십상이다.
우선 내가 여태까지 생각하던 포인터는 무엇인지 생각해 봤다.

포인터는 데이터가 저장된 메모리를 가리키는 데이터인가

포인터는 어떤 의미에선 참조와 비슷한 역할을 한다. 참조 역시 데이터가 저장된 메모리의 정보를 가지고 있다.
이렇게 되면 참조는 포인터와 대조군이다 그럼 참조랑 포인터는 정확히 뭐가 다를까.

일단 내가 아는바에 따르면 참조는 선언과 초기화가 동시에 일어나며 한번 초기화하면 다시 초기화 할 수 없다.
반면 포인터는 한번 변수를 초기화하고 다시 초기화 할 수 있으며 null값을 가질 수도 있다.

G쌤한테 이걸 물어도 크게 틀렸다고 말하진 않았다. 그래도 궁금한건 왜 그런가였다. 아마 포인터의 특성이 동적으로 메모리를 할당하는 것이기 때문에 그런걸까 ?

왜 참조는 초기화후 다시 초기화 될 수 없나?

지쌤은 여기에 이렇게 말한다.


참조는 원본 변수의 메모리 주소를 ‘참조’하는 것이 아니라 원본 변수 자체를 ‘대리’하는 역할을 합니다. 따라서, 참조를 선언할 때 그것이 가리킬 실제 변수가 필요하며, 이것 없이는 참조가 무엇을 참조해야 할지 결정할 수 없기 때문입니다.

초기화되지 않은 참조는 유효하지 않은 메모리 주소를 가리킬 위험이 있으며, 이는 프로그램의 안정성과 예측 가능성을 저하시킬 수 있습니다. C++의 설계 철학 중 하나는 안전성과 명확성을 우선시하는 것이며, 참조의 이러한 특성은 이러한 철학을 반영합니다.


즉 참조는 메모리의 주소를 저장하는 것이 아니라 이미 선언된 변수의 메모리를 저장하는 개념과 같다. 그렇기때문에 한 번만 초기화하고 다시 초기화를 허용하거나 없는 값을 허용하지 않는다.

그리고 포인터의 특성은 이렇게 말한다

이와 달리, 포인터는 초기화되지 않을 수도 있고, NULL이나 nullptr로 명시적으로 초기화될 수도 있습니다. 포인터는 메모리 주소를 저장하는 변수로, 어떤 메모리 주소도 가리킬 수 있기 때문에 더 유연하지만, 이로 인해 발생할 수 있는 오류와 버그도 더 많습니다.


말그대로 포인터는 없거나 유효하지 않은 메모리를 지목할 수 있다. 그리고 이것 때문에 메모리 누수나 오류를 걱정해아한다. 결국 참조가 포인터보다 까다로운 것은 용도가 다르고 위험하기 때문이었다.

헙리적이지만 여기서 끝날 일은 아니다. 만약 참조가 이런 이유로 재초기화 등을 허용하지 않는다면. 왜 포인턴,ㄴ 굳이 그것을 사용하는지에 대한 질문을 해야한다.



포인터가 C++과 같은 프로그래밍 언어에서 여전히 중요한 역할을 하는 이유는 포인터 자체의 장점과 유연성 때문입니다. 포인터는 몇 가지 중요한 기능과 이점을 제공하는데, 이는 참조만으로는 구현이 어려운 부분도 포함됩니다. 주요 장점을 몇 가지 설명드리겠습니다:


1. 동적 메모리 관리: 포인터는 동적으로 할당된 메모리를 가리킬 수 있습니다. 프로그램 실행 중에 메모리의 크기가 변할 필요가 있는 경우, 포인터를 사용하여 메모리를 할당하고 해제할 수 있습니다. 이는 배열 크기 조정, 다양한 데이터 구조(예: 링크드 리스트, 트리 등)의 동적 관리에서 필수적입니다.

2. 저수준 시스템 접근: 포인터를 사용하면 메모리 주소를 직접 조작할 수 있어, 하드웨어나 특정 메모리 위치에 직접 접근해야 하는 시스템 프로그래밍에서 매우 유용합니다. 이는 운영체제 개발, 임베디드 시스템 프로그래밍 등에서 중요한 요소입니다.

3. 함수 포인터와 콜백: 함수 포인터를 통해 함수의 주소를 저장하고, 이를 사용하여 나중에 해당 함수를 호출할 수 있습니다. 이는 콜백 함수, 이벤트 핸들러 등을 구현할 때 사용되며, 프로그램의 확장성과 모듈성을 크게 향상시킵니다.

4. 포인터 산술: 포인터를 사용하면 포인터 산술을 통해 데이터 구조 내에서 효율적으로 이동할 수 있습니다. 예를 들어, 배열의 요소에 접근하거나 배열을 넘나들 때 포인터를 증감시켜 위치를 조정할 수 있습니다.

5. 유연성: 포인터는 `NULL` 또는 `nullptr`로 초기화할 수 있어, 참조되는 객체가 아직 존재하지 않거나 선택적으로 존재하는 경우를 표현할 수 있습니다. 이는 특정 조건에 따라 참조를 선택적으로 설정하고 해제하는 유연한 프로그래밍을 가능하게 합니다.

포인터가 위험을 완전히 보완했다고 보기는 어렵지만, C++에서는 다양한 방식으로 포인터의 안전성을 개선하고 있습니다. 예를 들어, 스마트 포인터(`std::unique_ptr`, `std::shared_ptr` 등)는 자동 메모리 관리를 제공하여 메모리 누수와 같은 문제를 예방할 수 있습니다. 따라서 포인터의 유연성과 장점을 활용하면서도 안전성을 향상시킬 수 있는 방법이 존재합니다.


메모리 주소 자체를 가리키는 일은 동적 메모리 관리를 위해서는 필수적이다는 의견이다. 또한 포인터는 흔히 쓰이는 것처럼 산술이 가능하다는 의견도 있고 결국 선택적으로 할 수 있는 것도 장점이라는 것 같다.

또 여기에 새로운 스마트 포인터라는 개념이 나오는데 스마트 포인터 존재의의중 적어도 하나는 포인터의 문제점인 메모리 누수를 막기 위한 것이라고 생각된다. 이제 거의 포인터와 참조를 구분할 수 있는 정보는 충분히 모인 것 같다. 그러나 아직 궁금한 점이 있었다 왜 C++만 유독 이런 메모리 누수 문제가 많이 제기되는걸까?

메모리 누수같은 문제가 C++에서 특히 자주 붉어지는 건 어째서인가. 메모리 동적 할당은 매우 흔한 개념이니 다른 언어에서도 포인터와 비슷한 개념이 있을텐데 어째서 C++만 메모리 누수 문제가 크게 지적될까?

C++에서 메모리 누수 문제가 자주 언급되는 것은 몇 가지 주요 이유로 인해 발생합니다:

1. 수동 메모리 관리. C++은 프로그래머에게 메모리 할당과 해제의 책임을 맡깁니다. 이는 `new`로 메모리를 할당하고 `delete`로 해제하는 과정에서 정확한 짝을 맞추어 처리해야 한다는 것을 의미합니다. 프로그래머가 메모리 해제를 잊거나 잘못 처리하면 메모리 누수가 발생합니다.

2. 복잡한 프로그램 구조. C++의 프로그램은 종종 복잡하고, 다중 상속, 다중 포인터 사용 등 복잡한 메모리 관계를 포함할 수 있습니다. 이러한 복잡성은 메모리 누수를 추적하고 해결하는 것을 더 어렵게 만듭니다.

3. 표준화 이전의 코드. 많은 오래된 C++ 코드베이스는 스마트 포인터 같은 현대 C++의 메모리 관리 기능을 사용하지 않습니다. 이러한 코드는 메모리 누수의 위험이 더 크며, 이전 코드를 유지보수하거나 업데이트할 때 문제가 발생할 수 있습니다.

4. 포인터와 참조의 혼용. 포인터와 참조를 함께 사용하면서 생기는 혼란도 메모리 누수의 원인이 될 수 있습니다. 포인터는 `NULL`이 될 수 있지만 참조는 그렇지 않기 때문에, 이 두 개념의 혼용은 버그를 유발할 수 있습니다.

다른 프로그래밍 언어에서는 이러한 문제를 다루는 접근 방식이 다릅니다:

자동 메모리 관리: Java나 Python 같은 언어는 가비지 컬렉션을 제공하여 더이상 필요하지 않은 객체를 자동으로 청소합니다. 이는 프로그래머가 메모리 관리를 신경 쓰지 않아도 되게 해줍니다.
스마트 포인터: Rust와 같은 현대 언어는 메모리 안전을 컴파일러 수준에서 강제합니다. Rust는 소유권(ownership)과 빌려주기(borrowing) 모델을 사용하여 메모리 누수를 컴파일 시점에 방지합니다.

결론적으로, C++에서 메모리 누수가 두드러지는 이유는 언어의 유연성과 강력한 기능 때문에 생기는 복잡성과 프로그래머의 메모리 관리 책임 때문입니다. 이를 해결하기 위해서는 스마트 포인터의 사용, 적절한 프로그래밍 관행의 수용, 그리고 현대 C++ 기능의 활용이 중요합니다.

왜 C++에서만 메모리 누수가 문제가 되나? 의 대답은

  1. C++이 자바, 파이썬과 다르게 메모리를 수동으로 관리하기 때문에
  2. 발전된 포인터를 사용하지않은 옛날 코드가 많다
  3. Rust처럼 메모리 안전을 강제하지 않는다.

정도로 나눌 수 있는 것 같다. 여기서 1번이 제일 눈에 띄는데 자바, 파이썬은 가비지 컬렉터로 메모리를 관리하는데 그것이 포인터를 사용하는 C++과 다른점이라는 것이다.
그럼 가비지 컬렉터가 포인터가 다른개념일까? 지쌤에 의히면 그렇다고 한다. 가비지 컬렉터는 포인터의 또다른 대조군이다. 둘은 메모리를 자동으로 관리하냐 수동으로 관리하냐에 따라서 차이가 있고 그말인즉 포인터에서 메모리를 수동으로 관리하는 것은 매우 중요한 개념이라는 것이다.

일단 오늘 배운걸 정리하면

포인터는 메모리의 주소를 저장해놓는 개념으로 다른 변수의 별칭으로서 작용하는 참조에 비해서 메모리 주소 자체를 저장하기에 선언만 한다거나 여러번 초기화를 한다거나 하는 것이 가능하다. 또한 메모리를 수동으로 할당하고 해제하게 해주어 동적 메모리 관리를 할 수 있다. 그러나 그만큼 수동으로 관리하기에 다른언어의 가비지 컬렉터에 비해서 메모리 누수의 위협이 크다. 이런 단점때문에 메모리를 자동으로 관리해주는 스마트포인터라는 개념도 존재한다.

그런데도 아직 스마트포인터의 개념같은건 정확히 이해되지 않았다. 이후에 더 많은 공부를 해야겠다.