[C++] 참조(Reference) 기초
👻 참조란?
참조(Reference)란 값을 복사 하여 사용하는 방식과는 반대로, 포인터와 비슷하게 해당 원본 데이터에 직접 접근해 사용하는 방식을 의미한다. 원본 자체에 접근하기 때문에 데이터 관리 및 수정이 용이하다는 장점이 있지만 원본을 건드리는 만큼 위험성이 존재한다. 로우레벨(어셈블리) 관점에서 실제 작동 방식은 포인터(int*
)와 같다. 사용 방식은 아래와같이 앰퍼센트(&)를 이용한다.
//int number = 1;
int& reference = number;
reference = 3;
해당 코드는 number라는 변수에 reference라는 또 다른 이름을 지어준 것이다. number 변수를 참조한다는 뜻이다.
🌱 참조 방식을 사용하는 이유
로우레벨에서는 포인터와 같이 동작한다는데 그냥 값 전달 방식이나 주소 전달 방식으로만 사용하면 안 될까?
이러한 방식을 사용하는 이유는 바로 효율성 때문이다. 포인터를 사용한 주소 전달 방식이 값 전달 방식보다 메모리 관리에 훨씬 효율적이지만 사용하는 ->
기호가 복사 전달 시에 사용하는 .
에 비해 굉장히 비효율적이다. 값에 접근할 때는 포인터처럼, 값을 가져올 때는 복사 방식처럼 편하게 가져오기 위한, 각 방식의 장점만 합쳐 만든 중간 단계라고 볼 수 있다.
// 1) 값 전달 방식
void PrintInfoByCopy(StatInfo info) {
cout << "------------------------------" << endl;
cout << "HP : " << info.hp << endl;
cout << "ATT : " << info.attack << endl;
cout << "DEF : " << info.defence << endl;
cout << "------------------------------" << endl;
}
// 2) 주소 전달 방식
void PrintInfoByPtr(StatInfo* info) {
cout << "------------------------------" << endl;
cout << "HP : " << info->hp << endl;
cout << "ATT : " << info->attack << endl;
cout << "DEF : " << info->defence << endl;
cout << "------------------------------" << endl;
}
// 3) 참조 전달 방식
void PrintInfoByRef(StatInfo& info) {
cout << "------------------------------" << endl;
cout << "HP : " << info.hp << endl;
cout << "ATT : " << info.attack << endl;
cout << "DEF : " << info.defence << endl;
cout << "------------------------------" << endl;
}
int main() {
PrintInfoByCopy(info);
PrintInfoByPtr(&info);
PrintInfoByRef(info);
}
💡 StatInfo 구조체가 1000바이트짜리 대형 구조체라면?
- 값 전달 : StatInfo로 넘기면 1000바이트가 복사된다.
- 주소 전달 : StatInfo*는 8바이트 구조체 자체를 가리킨다.
- 참조 전달 : StatInfo&는 8바이트 구조체 자체를 가리키며 값 전달 방식처럼 사용할 수 있다.
👻 포인터 vs 참조
- 성능 : 같다.
- 편의성 : 참조 > 포인터
- 편의성 관련
편의성이 좋다는 것이 꼭 장점만은 아니다. 포인터는 주소를 넘기니 확실하게 원본을 넘긴다는 힌트를 줄 수 있지만, 참조는 자연스럽게 모르고 지나칠 수도 있다. 이름에 참조와 관련된 정보가 담겨있지 않는 경우 값을 복사해서 넘기는지, 참조해서 넘기는지 알기가 힘들다.
포인터와 같은 동작을 하지만 원본값을 참조한다는 정보를 알지 못하면 원본을 훼손할 가능성이 크다는 것을 인지하기 어렵다.
👉 마음대로 고칠 수 있는 부분을 const
를 사용하면 수정 기능을 제어할 수 있다. 참조의 경우 거의 const
를 사용한다. (안전성 증가)
void PrintInfoByRef(const StatInfo& info) {
cout << "------------------------------" << endl;
cout << "HP : " << info.hp << endl;
cout << "ATT : " << info.attack << endl;
cout << "DEF : " << info.defence << endl;
cout << "------------------------------" << endl;
// const를 사용하면 해당 방식으로 수정이 불가하다. => 값 수정 불가
// info.hp = 10000;
}
참고로 포인터도
const
를 사용할 수 있다.*
기준으로 앞에 붙이느냐 뒤에 붙이느냐에 따라 의미가 달라진다.void PrintInfoByPtr(const StatInfo* info); // 앞에 붙였을 때 void PrintInfoByPtr(StatInfo const * info); void PrintInfoByPtr(StatInfo* const info); // 뒤에 붙였을 때
- 별 뒤에 붙인다면 주소값의 수정이 불가하다.
- 별 앞에 붙인다면 해당 주소가 가리키고 있는 데이터의 수정이 불가하다.
- 둘 다 붙이면 주소값과 데이터 모두 수정이 불가하다.
- 초기화 여부
참조 타입은 변수(바구니)의 2번째 이름을 붙여주는 방식이다. 이 말인 즉슨, 참조하는 대상이 없으면 안된다는 뜻이다.
StatInfo* pointer; // 사용 가능
StatInfo& reference; // 사용 불가능
StatInfo& reference = info; // 참조대상 무조건 필요
포인터는 그냥 어떤 값의 주소라는 의미이기 때문에 대상이 실존하지 않을 수도 있다. 포인터에서 ‘없다’는 nullptr
를 사용한다.
// 대상이 없을 때에는 이렇게 사용한다.
// 아무것도 가리키지 않는 상태를 의미한다.
StatInfo* pointer = nullptr;
참조 타입은 이런 nullptr
의 개념이 없기 때문에 상황에 따라 알맞은 방식을 사용해야한다.
💡 포인터와 참조 사이의 전달은 어떻게 할까?
// 포인터 -> 참조 PrintInfoByRef(*pointer); // 참조 -> 포인터 PrintInfoByPtr(&reference);
👉 참조엔 포인터가 가리키는 주소에 있는 값을, 포인터엔 참조되는 값의 주소값을 넘겨주면 된다.
🌱 결론
사실 팀바팀이라 정해진 답은 없다. 구글의 오픈소스를 살펴보면 포인터를 사용하는 것을 볼 수 있고, 언리얼 엔진을 사용하는 경우엔 참조 방식을 애용하는 것을 볼 수 있다. 성능도 거의 비슷하기 때문에 포인터와 참조 방식 둘 다 확실히 익혀두면 상황에 따라 자유자재로 쓸 수 있으니 알아서 잘 사용하면 될 것 같다.
값이 없는 경우도 고려해야 한다면 포인터를 사용하는 것이 좋고, 바뀌지 않고 읽는 용도(readonly)로만 사용한다면 const ref&
방식이 더 좋을 수도 있다. 참조 방식의 단점을 보완하기 위해 명시적으로 호출할 때 OUT
을 정의하여 붙여준다거나 한다면 사용법에 따라 좋은 코드가 될 수 있을 것이다.
그대신, 섞어서 사용하는 건 비추한다. 보기 불편하니까!!
👻 글을 마치며
이번 시간엔 참조 기초에 대해 알아보고 더 나아가 포인터와 참조를 비교해보았다. 처음에 주소를 전달하는 게 참조라고 생각했었는데 약간은 다른 의미라는 것을 알게 되었다. 처음엔 왜 사용해야하나 싶기도 했지만 효율적으로 따져본다면 포인터와 복사 방식의 장점만 가져온 아주 좋은 방식이라 생각된다. 아직은 눈에 익지 않아 변수 앞에 기호를 붙이면 간혹 의미가 헷갈릴 때도 있는데 점점 적응해나가니 개발을 하면서 메모리 관리도 자연스레 생각하게 되는 것 같다. 좋은 습관이 들여지고 있는 것 같아서 기분이 좋다. ☺☺☺
Leave a comment