
복합 데이터 타입
복합 데이터 타입이란 기본 데이터 타입을 확장하여 조금 더 복잡한 데이터 구조를 만들 수 있게 해주는 타입이다
C++은 대표적으로 다음과 같은 복합 데이터 타입을 제공한다
함수도 기본적으로 복합 데이터 타입에 속한다
void foo(int a, double b)
{
}
//이 함수는 void(int, double) 타입이다 -> 기본 데이터 타입으로 구성된 복합 데이터 타입
Expression (표현식)
우선 C++에서 표현식이란 변수, 연산자, 함수 호출, literal을 조합하여 실행될 수 있는 코드 조각으로 하나의 값을 생성하는 것이라고 정의할 수 있다
C++에서 표현식은 두 가지 중요한 속성을 가지게 되는데, Type과 Value Category이다
10 / 5; //int type의 표현식
x = 5; //ok
5 = x; //error
5 = x가 안되는 이유는 바로 Value Category때문이다
Value Catogory는 이러한 표현식이 어느 위치에 올 수 있는지 결정하는 역할을 한다 (lvalue, rvalue, xvalue, prvalue, glvalue)
(literal값에는 다른 값을 할당할 수 없음)
lvalue, rvalue
C++의 모든 표현식은 lvalue, rvalue로 나뉘게 된다
x = 10; //x는 lvalue (식별 가능하고 메모리 주소를 가지고 있는 객체이기 때문)
lvalue는 수정 가능한 lvalue(일반적인 lvalue), 수정 불가능한 lvalue(const, contexpr)로 나눌 수 있다
x = 10; //10은 literal이고 주소를 가지지 않는다 -> rvalue
일반적으로 연산을 할 때 피연산자를 rvalue로 사용한다, 단 대입연산자 =는 왼쪽 피연산자로 lvalue가 필요하다
lvalue는 필요할 때 자동으로 rvalue로 변환될 수 있다
int x{ 10 };
int y{ 20 };
x = y; //y는 lvalue지만 rvalue로 변환되어 들어간다
template 함수 오버로딩과 참조를 이용하여 해당 value가 lvalue인지 rvalue인지 판단할 수 있다
template <typename T>
constexpr bool bIsLValue(T&)
{
return true
}
template <typename T>
constexpr bool bIsLValue(T&&)
{
return false;
}
//x를 넣으면 true, 5를 넣으면 false가 나오게 된다 (&&는 rvalue 참조이기 때문)
Lvalue Reference
C++에서 참조란 기존 객체의 별칭을 의미한다, 참조를 하게되면 참조된 객체를 직접 조작할 수 있다
(참조변수는 본질적으로 참조된 객체와 동일하다)
modern C++에는 lvalue참조(&)와 rvalue참조(&&)가 존재한다
lvalue참조는 말 그대로 기존 lvalue의 별칭을 만드는 것이다, 타입뒤에 &를 붙여서 lvalue 참조 변수 만들 수 있다
int a{ 10 };
int& refA{ a };
const int& constRefA{ a }; //const도 가능
//refA, constRefA는 lvalue 참조 변수가 됨
a; //10
refA; //10
참조를 사용하게 되면 객체의 원본값을 조작할 수 있다
refA = 20;
//이제부터 a는 20이 됨
참조타입 변수는 반드시 초기화가 되어야 한다 (초기화 하지 않으면 error)
int& refA; //error

일반적인 lvalue 참조타입 변수에는 const lvalue나 rvalue를 할당할 수 없다
const int a{ 100 };
int& refA{ a }; //error
int& refA{ 10 }; //error
const 상수는 변경할 수 없도록 보호하기 위해 참조할 수 없게 하는것이다
참조타입 변수는 한번 바인딩되면 그 이후에 변경이 불가능하다
int a{ 100 };
int b{ 200 };
int& refA{ a };
refA = b; //refA가 b를 참조하겠다는게 아님, 단순히 대입 연산자가 작동함
refA = 300;
std::cout << b << '\n'; //200이 나옴
std::cout << a << '\n'; //원래 참조하던 a가 300으로 나옴
참조타입 변수도 일반 변수와 같은 수명을 가진다
(로컬 변수일 경우 { }가 수명)
참조 타입을 반환하는 함수인 경우 Dangling Ref가 발생할 수 있다
int& foo()
{
int a{ 10 };
return a;
}
int main()
{
int& ref = foo(); //이미 함수가 종료되면서 a라는 local variable은 소멸됨, 소멸된 객체를 참조할 수 있어 의도치 않은 동작이 일어날 수 있다 (소멸된 a객체 메모리 주소에 데이터가 들어갈 수 있다)
}
const lvalue reference
일반적인 lvalue참조로는 const변수를 참조할 수 없다, 하지만 const lvalue 참조를 사용하여 const 변수도 참조가 가능하다 (const lvalue가 아닌 일반 lvalue도 참조 가능함, 마찬가지로 수정은 불가능)
여기에 추가로 const lvalue 참조는 rvalue도 참조가 가능하다 (읽기 전용이라는 보장이 되어있기 때문, 단 이때 임시객체가 생성되고 생성된 임시객체를 참조하게 된다)
일반 lvalue참조는 타입이 무조건 같아야 한다
const int a{ 10 };
const int& constRefA{ a };
constRefA = 230; //error
const int& ref{ 10 }; //ok
const double& refTest = 10; //ok
단 데이터를 읽을 수만 있고 수정할 수 없다
객체를 수정할 필요가 없다면 const lvalue ref를 사용하는것이 좋다 (불필요한 변경 방지, 보통 함수의 param으로 많이 사용한다)
단 위와같이 rvalue를 참조하거나 다른 타입의 객체를 참조하게 되면 임시객체가 생성되어 원본을 참조하지 않게된다
short a{ 10 };
const int& refI = a;
--a;
std::cout << refI << '\n'; //9가되지 않고 10이나옴 (임시객체를 참조하여 --했기 때문)
이렇게 참조가 임시객체를 참조하게 되면 임시객체의 수명은 참조 변수의 수명과 일치하게 된다 (원래 임시객체는 해당 라인이 끝나면 제거됨)
위에서도 a의 임시객체가 계속 살아있게 된다 (dangling ref를 방지하기 위한 시스템)
함수의 인자로서 lvalue 참조
함수의 인자로 값을 전달하게 될 경우 원본값 변경이 불가능해진다 (사본이 전달되기 때문)
void foo(int a)
{
--a;
}
int main()
{
int temp{ 100 );
foo(temp);
std::cout << temp << '\n'; //99로 줄어들지 않고 100이 나온다 (사본이 전달되어 수정되었기 때문에 원본은 그대로임)
}
이때 사본이 생성되기 때문에 간단한 기본타입 객체인 경우 상관 없지만 복잡한 class, struct가 복사될 때 메모리 할당 비용문제가 발생할 수 있다 (불필요한 복사 발생)
따라서 복사 비용(메모리 할당 비용)을 줄이고 원본값 수정을 하기 위해서는 참조 타입 전달을 해야한다
void foo(int& a)
{
--a;
}
int main()
{
int temp{ 100 };
foo(temp);
std::cout << temp << '\n'; //참조 타입 전달로 원본이 수정되어 99가 나옴
}
참조는 기존 객체의 별칭을 만드는것이고 기존 객체와 동일하게 취급되기 때문에 주소가 같다, 하지만 값전달은 사본이 생성되어 넘어가기 때문에 주소가 다르게 나온다
마찬가지로 함수의 인자로 기본 lvalue 참조를 받을 때 const 객체나 rvalue는 넘길 수 없다
void foo(int& a)
{
--a;
}
int main()
{
const int a{ 100 };
foo(a); //error
}
함수에서의 const lvalue 참조 전달
void foo(const in& a)
{
}
원본값을 변경할 수 없도록 제한하기 때문에 의도치 않은 변경을 막을 수 있다
또한 const lvalue 참조에는 rvalue, const lvalue, lvalue가 전부 다 들어갈 수 있기 때문에 좀 더 유연하다 (심지어 다른 타입의 값도 가능)
보통 복사 비용이 적은 기본 타입은 값 타입으로 넘기고 class나 struct등은 const &로 넘기는 방식을 자주 사용한다