1. C++ Valule, Reference
1.1 C++ Value(값)와 최적화 Intro.
이번 챕터에서는 C++의 값(Value)과 객체 전달 방식, 그리고 성능 최적화 기법을 학습합니다.
특히 C++에서만 제공되는 L-value, R-value, Move Semantics, Return Value Optimization(RVO) 등의 개념을 주로 다룹니다.
이 개념들은 C++의 성능 최적화를 이해하는 핵심 요소이며, 이후 배우게 될 복사 생성자(Copy Constructor)와 이동 생성자(Move Constructor)를 이해하기 위한 기초가 됩니다.
1.2 학습 내용
① 포인터(Pointer)
- C++의 메모리 접근 방식
- 주소(Address)를 이용한 객체 관리
- 레퍼런스와의 차이 이해
왜 배우는가?
C++는 직접 메모리를 다루는 언어이기 때문에, 포인터는 모든 고급 기능의 기반이 됩니다.
② 레퍼런스(Reference)
- 객체의 별칭(Alias)
- 포인터보다 안전한 참조 방식
- 함수 인자 전달 및 반환에서 자주 사용
실무 활용
- 불필요한 복사 방지
- 함수 호출 비용 감소
- const &를 이용한 효율적인 객체 전달
③ Lvalue와 Rvalue
이번 챕터에서 가장 중요한 개념
Lvalue
- 이름을 가진 객체
- 메모리 주소를 가질 수 있는 값
- 여러 번 참조 가능
예시
int a = 10;
a는 Lvalue이다.
Rvalue
- 일시적으로 생성되는 값(Temporary Object)
- 이름이 없는 값
- 일반적으로 한 번만 사용된다.
예시
10
a + b
std::string("Hello")
모두 Rvalue이다.
왜 중요한가?
컴파일러는 L-value인지 R-value인지에 따라, 복사(Copy)와 이동(Move)를 다르게 수행함.
즉, L-value와 R-value를 구분할 수 있어야 C++의 성능 최적화를 이해할 수 있다.
④ std::move
Lvalue를 Rvalue Reference로 변환하는 함수이다.
std::move(obj);
역할
객체를 실제로 이동시키는 것이 아니라
"이 객체는 이제 이동해도 된다."
라고 컴파일러에게 알려주는 역할.
실무 활용
큰 객체를 복사하지 않고
- 문자열(String)
- 벡터(Vector)
- 컨테이너(Container)
등을 효율적으로 이동시킬 때 사용합니다.
⑤ Move Semantics(이동 의미론)
C++11에서 도입된 대표적인 성능 최적화 기능
기존에는
객체 전달
↓
복사 생성자 호출
↓
메모리 복사
가 이루어졌지만,
Move Semantics를 사용하면
객체 전달
↓
이동 생성자 호출
↓
메모리 소유권 이전
이 이루어져 불필요한 복사를 줄일 수 있다.
⑥ Return Value Optimization(RVO)
함수를 통해 객체를 반환할 때 발생하는 불필요한 복사를 컴파일러가 자동으로 제거하는 최적화 기법이다.
예를 들어
MyClass Create()
{
MyClass obj;
return obj;
}
과 같은 코드에서
과거에는
obj 생성
↓
복사
↓
리턴
과정을 거쳤습니다.
하지만 RVO가 적용되면
반환 객체를
최종 위치에 직접 생성
하므로 복사나 이동이 발생하지 않는다.
1.3 학습 흐름
포인터
↓
레퍼런스
↓
Lvalue / Rvalue
↓
std::move
↓
Move Constructor
↓
Move Semantics
↓
Return Value Optimization(RVO)
각 개념은 독립적인 것이 아니라 서로 연결되어 있으며, 앞의 내용을 이해해야 다음 내용을 자연스럽게 학습할 수 있다.
1.4 실무에서 중요한 이유
이 개념들은 대용량 데이터를 다루는 프로그램의 성능 최적화에 직접적인 영향을 줍니다.
예를 들어 다음과 같은 객체를 생각해 보자.
- std::vector
- std::string
- 이미지 데이터
- 대용량 버퍼
- 사용자 정의 클래스
이러한 객체를 단순히 복사하면 많은 메모리와 시간이 소모되죠.
Move Semantics와 RVO를 활용하면 복사 대신 소유권만 이전하거나 복사 자체를 생략할 수 있어 성능을 크게 향상시킬 수 있다.
1.5 학습 포인트
- L-value와 R-value의 차이를 명확히 이해하는 것이 가장 중요하다.
- std::move는 객체를 이동시키는 함수가 아니라 이동이 가능하다고 표시하는 캐스팅 함수라는 점을 기억해야 한다.
- Move Semantics는 불필요한 복사를 줄여 성능을 향상시키는 핵심 기능이다.
- RVO는 컴파일러가 자동으로 수행하는 최적화로, 객체 반환 시 복사나 이동을 생략하여 더욱 효율적인 코드를 생성한다.
* 실무 핵심 포인트
Q1. Lvalue와 Rvalue의 차이는?
- L-value: 이름이 있고 메모리 주소를 가지며, 여러 번 참조할 수 있는 값.
- R-value: 임시 객체 또는 이름이 없는 값으로, 주로 한 번만 사용되는 값.
Q2. std::move()는 객체를 이동시키는 함수인가?
아니다. std::move()는 객체를 R-value Reference로 변환하여 이동 생성자나 이동 대입 연산자가 호출될 수 있도록 하는 역할을 한다.
Q3. RVO가 필요한 이유는?
객체를 함수에서 반환할 때 발생할 수 있는 복사 또는 이동 비용을 제거하여 성능을 높이기 위해 사용된다. 최신 C++에서는 많은 경우 RVO가 자동으로 적용된다.
※ 핵심 정리
- 이번 챕터는 C++의 성능 최적화 메커니즘을 이해하기 위한 핵심 개념을 다룬다.
- 포인터와 레퍼런스를 시작으로 Lvalue/Rvalue, std::move, Move Semantics, RVO까지 이어지는 흐름을 이해하는 것이 중요하다.
- 이러한 개념들은 복사 비용을 줄이고 메모리 사용을 최적화하기 위해 설계되었으며, 현대 C++에서 효율적인 코드 작성과 실무 개발에 필수적인 내용이다.
2. Call by Value, Pointer, Reference
2.1 Intro
이전 챕터에서는
- 프로세스 메모리 모델
- 빌드 프로세스
를 학습했습니다.
이번 챕터는 객체지향 프로그래밍(OOP)을 배우기 전에 반드시 알아야 하는 기초 개념을 다룹니다.
이후 학습 순서는 다음과 같습니다.
포인터
↓
레퍼런스
↓
Lvalue / Rvalue
↓
std::move
↓
Move Constructor
↓
Return Value Optimization(RVO)
↓
클래스(Object Oriented Programming)
이번에 그 첫 단계인 포인터(Pointer)와 레퍼런스(Reference)를 학습합니다.
2.2 함수의 인자 전달 방식
C++에서는 함수에 값을 전달하는 방법이 크게 세 가지입니다.
| 전달 방식 | 특징 |
| Pass by Value | 값을 복사해서 전달 |
| Pass by Pointer | 주소를 전달 |
| Pass by Reference | 객체 자체를 참조 |
이 세 방식은 메모리 사용 방식과 성능이 서로 다릅니다.
2.3 Pass by Value (값 전달)
함수 호출 시 값을 복사하여 전달하는 방식.
예시
void Foo(int a)
{
int b = a + 1;
}
int main()
{
int a = 0;
Foo(a);
}
메모리 동작
main Stack Frame
a = 0
│
│ 복사
▼
Foo Stack Frame
a = 0
b = 1
중요한 점은 Foo의 a와 main의 a는 서로 다른 변수라는 것입니다.
따라서
a = 100;
을 Foo 내부에서 수행해도, main의 a는 변경되지 않습니다.
장점
- 구현이 가장 단순합니다.
- 원본 데이터를 보호할 수 있습니다.
- 작은 자료형(int, char, bool 등)에서는 오버헤드가 거의 없습니다.
단점
복사가 발생.
예를 들어
std::vector<int>
std::string
사용자 정의 클래스
같은 큰 객체는 복사 비용이 상당히 큽니다.
2.4 Pass by Pointer (포인터 전달)
객체의 주소(Address)를 전달합니다.
void Foo(int* p)
{
int b = *p + 1;
}
호출은
Foo(&a);
처럼 주소를 넘깁니다.
메모리 구조
main
a = 0
주소 : 1000
▲
│
│
Foo
p = 1000
Foo는 포인터를 통해 원본 변수에 직접 접근합니다.
원본 수정 가능
예를 들어
*p = 100;
을 수행하면
main
a = 100
으로 변경됨.
즉, 포인터는 원본 객체를 직접 수정할 수 있다.
장점
- 복사가 발생하지 않는다.
- 큰 객체를 효율적으로 전달할 수 있다.
- 원본 수정 가능
단점
포인터는 위험합니다.
예를 들어
nullptr
일 수도 있고 잘못된 주소를 가질 수도 있습니다.
int* p = nullptr;
*p = 10;
이와 같은 같은 코드는 프로그램이 비정상 종료될 수 있습니다.
2.5 Pass by Reference (참조 전달)
객체의 별칭(Alias)을 전달한다.
void Foo(int& a)
{
int b = a + 1;
}
호출은
Foo(a);
입니다.
문법은 값 전달처럼 보이지만, 실제로는 원본 객체를 참조합니다.
* 내부 동작
Compiler Explorer로 확인하면
Pass by Pointer
↓
Assembly
↓
Pass by Reference
의 어셈블리 코드가 거의 동일하게 생성됩니다.
즉, 레퍼런스는 내부적으로 포인터와 거의 동일한 방식으로 구현됩니다.
차이점
문법만 다릅니다.
포인터
*p
레퍼런스
a
레퍼런스는 역참조(*)가 필요 없습니다.
2.6 포인터와 레퍼런스 비교
| 항목 | Pointer | Reference |
| nullptr 가능 | O | X |
| 재참조 가능 | O | X |
| 주소 변경 가능 | O | X |
| 문법 | 복잡 | 간단 |
| 안전성 | 낮음 | 높음 |
2.7 [General-rule] *왜 실무에서는 Reference를 많이 사용할까?
포인터가 반드시 필요한 상황이 아니라면 Reference를 사용하는 것이 좋다.
그 이유는 레퍼런스가
- nullptr가 될 수 없고
- 문법이 간단하며
- 실수를 줄일 수 있기 때문이다.
즉, 더 안전한 코드를 작성할 수 있다.
2.8 [모범사례: 안정성] const Reference
만약 함수가 객체를 읽기만 한다면
void Foo(const int& a)
{
}
처럼 작성합니다.
장점
컴파일러가
a = 10;
같은 코드를 허용하지 않습니다.
즉, 원본 데이터가 변경되지 않음을 보장하죠.
실무에서 가장 많이 사용하는 형태
void Print(const std::string& str);
거의 모든 라이브러리가 이 방식을 사용합니다.
2.9 [효율성과 성능] 큰 객체 전달
이 글에서 중요한 성능 이야기 입니다.
예를 들어
std::vector<int>
100000개
를 Pass by Value 하면 [100000개 복사] 가 발생하죠.
하지만 Reference를 사용하면, [주소 하나만 전달] 하면 됩니다.
즉, 객체 크기와 관계없이, 복사 비용이 없죠.
2.10 왜 Value 전달을 사용하는 경우도 있을까?
많은 초보자가 "Reference가 더 빠른데 모두 Reference를 쓰면 되지 않을까?" 라고 생각합니다.
하지만 그렇지 않습니다.
예를 들어
int
double
char
bool
같은 작은 타입은 복사 비용이 거의 없습니다.
오히려 Reference는 간접 접근(Indirect Access)이 필요하기 때문에 Value가 더 효율적인 경우도 있다.
실무에서는 일반적으로 다음 기준을 사용합니다.
| 자료형 | 추천 |
| int | Value |
| bool | Value |
| enum | Value |
| std::string | const Reference |
| std::vector | const Reference |
| 사용자 정의 클래스 | const Reference |
2.11 다음 챕터와의 연결
std::vector::push_back() 예시
push_back(const T&);
와
push_back(T&&);
두 함수가 존재합니다.
여기서 & 는 L-value Reference 이고, && 는 R-value Reference 이다.
현재는 차이를 이해하기 어렵겠지만, 다음 글에서 L-value와 R-value를 배우면 왜 함수가 두 개 존재하는지 이해하게 됩니다.
2.12 실무에서의 활용
실제 프로젝트에서는 함수 인자를 다음과 같이 선택하는 경우가 많습니다.
// 작은 타입
void SetAge(int age);
// 읽기 전용 큰 객체
void Print(const std::string& name);
// 수정 가능한 객체
void Sort(std::vector<int>& nums);
// 소유권 이전(다음 챕터)
void SetData(std::vector<int>&& data);
이처럼 객체의 크기와 수정 여부에 따라 전달 방식을 선택하는 것이 성능과 코드 안정성 모두에 중요하다.
* 핵심포인트
Q1. Pass by Value와 Pass by Reference의 차이는?
- Pass by Value는 값을 복사하므로 원본이 변경되지 않는다.
- Pass by Reference는 원본 객체를 직접 참조하므로 복사 비용이 없고 원본을 수정할 수 있다.
Q2. Reference가 Pointer보다 좋은 이유는?
- nullptr가 될 수 없고, 문법이 간단하며, 실수를 줄일 수 있어 더 안전하다. 포인터가 꼭 필요한 경우(동적 메모리, 선택적 참조 등)가 아니라면 Reference를 사용하는 것이 일반적이다.
Q3. const T&를 사용하는 이유는?
- 큰 객체를 복사하지 않고 전달하면서도 원본 수정은 방지할 수 있기 때문이다. 성능과 안정성을 모두 확보할 수 있어 실무에서 가장 많이 사용하는 함수 인자 형태이다.
※ 핵심 정리
- 함수 인자 전달 방식은 Pass by Value, Pass by Pointer, Pass by Reference 세 가지가 있다.
- Value는 복사가 발생하지만 원본을 보호하며, 작은 타입에 적합하다.
- Pointer는 주소를 전달하여 원본을 수정할 수 있지만 nullptr 등으로 인해 오류 가능성이 높다.
- Reference는 포인터와 거의 동일한 성능을 가지면서도 더 안전하고 사용하기 쉬워, 실무에서는 포인터보다 선호된다.
- const T&는 큰 객체를 효율적이고 안전하게 전달하는 가장 일반적인 방식이며, 현대 C++에서 매우 중요한 패턴이다.
- 다음 단계에서는 L-value / R-value를 배우며, 이를 바탕으로 Move Semantics와 성능 최적화를 이해하게 된다.
'Fundamental of CS > : : C++' 카테고리의 다른 글
| [Resource Move] std::move() (0) | 2024.07.16 |
|---|---|
| [Resource Move] L-Value, R-Value 그리고 std::move (0) | 2024.07.16 |
| [Compile Process] C++ Code Formatter (Clang-Format) (0) | 2024.07.16 |
| [Compile Process] Unit Test, static code analysis (0) | 2024.07.16 |
| [Compile Process] Attributes 속성 (0) | 2024.07.16 |