다른 객체 또는 값의 별칭으로 사용되는 C++타입. &를 붙여서 표기한다.
int a = 10;
int &b = a; // a에 대한 참조 b. -> a의 별명이 생겼다라고 생각
cout << a << ' ' << b << '\n';
// 10 10 같은 값이 출력된다.
위의 예시에서 스택 영역에 int형 변수인 a를 위한 공간이 생성되어 있다. 그리고 그 공간에 10이 할당되어 있을 것이다. 그 다음 a를 참조하는 변수인 b가 생성된다.
결과적으로 같은 메모리 공간을 가르키고 있는 변수가 2개 생성된 것이다. a와 b의 주솟값도 물론 동일할 것이다. 이러한 현상을 별칭이 생겼다고 해서 ‘aliasing’이라고도 한다.
같은 메모리 공간을 참조하고 있기 때문에, a와 b중 하나를 변경하면 둘 다 영향을 받을 것이다. 즉, 참조자는 선언과 동시에 대입되는 변수와 동일한 변수가 되는 것이다.
int &a = NULL // NULL로 선언이 불가하다.
int &b; // 할당이 되지 않았다.
int &a = b;
a = c;
//이는 참조하는 대상을 변경하는 것이 아니다. 참조하는 값을 c로 변경한 것이다. b를 c로 바꾼 것과 같다.
변수의 메모리 공간을 가리키는 변수. *를 붙여서 표기한다
int a = 10;
int *b = &a;
위의 예시에서 스택 영역에 int형 변수인 a를 위한 공간이 생성되어 있다. 그리고 그 공간에 10이 할당되어 있을 것이다. 그리고 스택 영역의 다른 어딘가에 b가 할당될 것이다. b에 a의 주소값(&a)을 저장하도록 했으므로, 메모리의 현 상태는 아래와 같을 것이다.

편의상 a, b의 주소값은 각각 Ox1234, Ox5678로 설정했다. 이때, int *b = &a; 에서의 &a는 참조가 아닌, a의 주소값을 의미하는 것에 주의하자.
b에는 10이라는 값이 들어있는 것이 아닌, a의 주솟값이 들어있다. 실제로 b를 출력해보면 Ox1234가 나올 것이다. 이 때, *b = 20; 을 실행시킨다면, b가 가리키고 있는 곳의 값을 20으로 바꾼다는 의미이다. 결국, a와 b모두 20이 될 것이다.
이때, 참조(Reference)와 다른 점은 포인터는 자체적으로 메모리 공간을 가진다는 것이다. b의 값은 a의 주솟값이지만, 실제로 &a와 &b 를 비교해보면 다른 값이 나올 것이다.
if(a != NULL) 과 같이 검사 권장.값에 의한 호출. 매개변수로 전달하는 값을 복사해서 함수에 넘겨주는 방식.
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int main(){
int x = 3, y = 5;
swap(x, y); // Call by value
return 0;
}
위 코드의 의도는 a, b의 값을 교환하기 위한 코드이다. 하지만 이를 실제로 실행시켜보면, 값들은 전혀 바뀌지 않는 것을 확인할 수 있다. x의 값은 a에 복사되고, y의 값은 b에 복사되어 함수의 매개변수로 전달되었기 때문이다. 실제로 a와 b는 함수의 블록이 종료되면 같이 소멸한다.
이와 같이 Call By Value는 값을 복사해서 전달하기 때문에, 함수 외부에서 선언된 변수가 변경되지 않는다. 하지만, 값의 복사가 일어나기 때문에 추가적인 오버헤드가 발생할 것이다.
참조에 의한 호출. 주소 값을 인자로 전달하는 함수 호출 방식
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main(){
int x = 3, y = 5;
swap(&x, &y); // Call by reference
return 0;
}
이전의 swap()함수를 다음과 같이 변경했다. 실행시켜보면, 이제는 의도한대로 잘 작동하는 것을 확인할 수 있다. 매개변수로 x, y의 주소값을 전달하기 때문에, 함수 외부에서 선언된 변수에도 접근할 수 있다.
값의 복사가 없기 때문에, 추가적인 비용문제는 해결됐지만, 외부 변수 값이 의도하지 않은대로 변경될 수 있다는 단점이 있다.
Dangling Pointer란 이미 해제된 메모리 영역을 계속해서 가리키고 있는 포인터를 말하는 것. 유효하지 않은 메모리를 가리키고 있기 때문에, 세그멘테이션 에러가 날 수 있다.
int main()
{
int* p = new int(5);
delete(p); // 메모리 해제
//p = nullptr 권장
*p = 10; // ???
return 0;
}
위 예시에서 p는 delete() 를 통해서 댕글링 포인터가 되었다. *p = 10 을 만나는 순간, Segmentation fault가 발생할 것이다. 상황에 따라서 오류가 발생하지 않고, 작동할 수 있지만, 이는 의도한 바가 아님으로 오작동의 원인이 된다.
이를 방지하기 위해서 메모리를 해제하는 경우 해당 포인터 변수를 nullptr로 바꾸고, 포인터 변수를 사용할 때마다 nullptr인지 확인하는 버릇이 필요하다.
만약, 포인터 변수 p를 참조하는 또 다른 포인터 변수(q)가 있는 상태에서, p가 댕글링 포인터가 된다면, p를 참조하고 있던 q도 같이 댕글링 포인터가 된다. 이와 같은 경우는 추적하기 힘들기 때문에 메모리 누수 및 버그를 발생시킬 수 있다. 이러한 문제는 스마트 포인터를 사용하면 어느정도 해소할 수 있다.
C++에서 동적 메모리 관리를 단순화하는 도구이다. 포인터와 비슷하게 동작하지만, 적절한 시점에 자동으로 메모리를 해제하여 메모리 누수를 방지한다.
일반적으로 c++에서 동적 메모리를 할당하면, 직접 메모리에서 해제해야한다. 이를 방치한다면, 메모리 누수가 발생하고, 이는 메모리 사용량의 증가로 인해 프로그램의 성능을 저하시키는 주요한 원인이 된다. 이를 해결하기 위해 등장한 것이 바로 스마트 포인터이다. 이는 헤더 안에 정의되어 있다.
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> p(new int(5));
return 0;
}
위의 예시에서 unique_ptr<int> 는 정수 타입의 스마트 포인터이다. main() 함수의 블록이 종료되어 포인터가 사라질 때, 메모리도 자동으로 해제된다.
스마트 포인터의 원리는 생성자, 소멸자와 관련이 있다. 생성자에서 메모리를 할당하고, 소멸자에서 메모리를 해제한다. 또한, 소유권(ownership)이라는 개념을 도입하여 동일한 메모리에 대한 접근을 관리한다. 위 코드에서 unique_ptr 은 이름에서 예상할 수 있듯이, 하나의 포인터만이 메모리를 소유할 수 있도록 해준다.
‘유일한’ 포인터이다. 동일한 메모리를 가리키는 두개의 포인터 객체가 존재할 수 없다. 하지만, 포인터의 소유권은 이전할 수 있다.
**// 1**
unique_ptr<int> p1(new int(5));
//unique_ptr<int> p2 = p1; 에러
**// 2**
unique_ptr<int> p1(new int(5));
unique_ptr<int> p2 = move(p1);
1번 코드에서 p1을 p2로 복사하려고 했지만, 에러가 난다. 2번은 move() 를 통해서 p1에서 p2로 소유권을 이동했다. 이렇게 되면, p1은 nullptr이 되고, p2는 p1이 가리켰던 메모리를 가리키게 된다.
메모리에 대한 공유 소유권을 제공한다. 여러 개의 포인터 객체가 동일한 메모리를 가리킬 수 있다. 내부적으로 레퍼런스 카운팅을 수행하여, 메모리를 가리키는 객체의 수를 추적하여 메모리 누수를 방지한다.
shared_ptr<int> p1(new int(5));
shared_ptr<int> p2 = p1;
이전과는 달리 더 이상 에러가 나지 않는다. p1, p2모두 동일한 메모리를 가리키며, 레퍼런스 카운트는 2이다. 레퍼런스 카운트가 0이 되면 메모리가 해제된다.
{
shared_ptr<int> p1(new int(5));
{
shared_ptr<int> p2 = p1;
// p1, p2는 동일한 메모리. 카운트 2
}// p2가 블록을 벗어남으로 삭제. 카운트 1
}// p1도 블록을 벗어남으로 삭제. 카운트 0. 메모리 해제
하지만, 순환 참조 문제를 야기할 수 있기 때문에, 주의해서 사용해야 한다.
기본적으로 shared_ptr과 유사하지만, 가리키는 객체의 수명에 영향을 주지 않는 약한 참조를 제공한다. 이는 순환 참조 문제를 방지하기 위해 사용된다. 순환 참조란 서로가 서로를 참조하는 상황으로, 스마트 포인터가 메모리를 해제하지 못하게 만든다.
struct B;
struct A{
shared_ptr<B> b_ptr;
};
struct B{
shared_ptr<A> a_ptr;
};
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->b_ptr = b;
b->a_ptr = a; // 순환참조 생성
위의 예시에서 A, B는 서로를 참조하고 있으므로 레퍼런스 카운트가 절대로 0이 되지 않는다. → 메모리 누수가 발생한다.
struct B;
struct A{
shared_ptr<B> b_ptr;
};
struct B{
weak_ptr<A> a_ptr; // 약한 참조 사용
};
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->b_ptr = b;
b->a_ptr = a;
이는 순환참조를 해결한 코드이다. A가 파괴되면 weak_ptr은 자동적으로 nullptr로 설정된다. 이를 통해서 순환참조를 해결할 수 있다.