최근 수정 시각 : 2024-11-03 15:55:39

C++/문법/자료형

파일:관련 문서 아이콘.svg   관련 문서: C++
,
,
,
,
,

파일:상위 문서 아이콘.svg   상위 문서: C++/문법
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터 · 구조체) · C++(자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript · Haskell
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · deprecated · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 자료형 한정자 (Type Qualifier)
2.1. const2.2. volatile2.3. & 참조
2.3.1. const&
2.4. && 참조2.5. 포인터
3. using4. 값 범주 (Value Category)

1. 개요

C++의 자료형에 관해 포괄적으로 설명하는 문서.

2. 자료형 한정자 (Type Qualifier)

2.1. const

const Type identifier;
상수 (Constant)
다른 자료형 앞에 붙어 값이 불변함을 나타낸다. 가령 const int는 변하지 않는 정수 값임을 의미한다. 사용자 단에서는 변수 선언 시, 함수의 결과값을 const로 표시하여 그 값을 변경하지 말라는 지시 효과를 볼 수 있다. 함수 구현 시에는 매개 변수에 붙여 사용자의 실수를 줄이고 예측할 수 없는 값의 수정을 막아준다.
만약 포인터와 조합했을때는 포인터가 가리키는 값이 불변인지, 아니면 포인터 자체가 불변인지가 달라진다. 예를 들어 const int* ptr;const int의 포인터이므로, *ptr로 접근한 값은 const라서 수정할 수 없다. 그러나 ptr이라는 변수 자체는 더하고 빼고 등등 임의의 연산도 할 수 있다. int* const ptr;ptr 자체가 불변이지만 *ptr로 접근한 값은 그냥 int라서 바꿀 수 있다.

잘 사용되는 예로는 문자열 포인터가 있다. const char* string은 변하지 않는 문자값의 포인터를 의미한다. 그러나 포인터 자체가 아니라 문자의 값이 상수이기 때문에 변수 string에 1을 더하고 빼는 등 값을 변경할 수 있다. 표준 라이브러리의 <algorithm> 모듈에서는 이를 이용해 자료구조 뿐만 아니라 문자열에 대해서도 동일한 연산을 지원한다. 반면 char* const& stringstring이 가리키는 char* 값 하나는 *string = 'B'; 처럼 언제든지 값이 바뀔 수 있다. 그러나 string 변수는 불변이다.

사실 상수 포인터, 상수를 가리키는 포인터는 현재 C++에서는 고려할 필요가 없다. C++17에서 string_view, span의 도입으로 웬만한 포인터 사용을 대체할 수 있기 때문이다 [1]. 이해하기 어렵다면 다른 자료형 앞에 붙어야 의미가 있다는 것을 기억하자.

2.2. volatile

volatile Type identifier;
휘발성 (Volatile)
다른 자료형 앞에 붙어 캐시에 대해 최적화를 막고 항상 값이 보이도록 한다. 이를 이해하려면 운영체제와 컴파일러에 대해 이해가 필요하다. 컴파일러는 바이너리를 구축하는 과정에서 인라이닝, 캐싱, 파이프라인 분기 예측, 상수식 평가 등을 동원해 가장 빠른 바이너리를 만든다. 최적화를 하는 과정에서 몇가지 변수와 상수 표현식으로 선언된 상수는 없어진다. 컴파일 시간에 평가할 수 있는 구문은 미리 작성한다. 그리고 평가되지 않을 구문 역시 아예 코드에서 배제된다. 심지어는 메서드의 존재 자체가 없어지고 모든 쓰임새가 반환값으로 대체될 수 있다. 예를 들어 어떤 동일한 for문이 여러 곳에서 반복된다면 for문의 내용을 진짜 반복하는 대신에 결과를 미리 계산해놓고 가져다 쓸 것이다.

이 과정은 거의 대부분의 경우 이득을 가져다 주지만, 변수의 존재 삭제와 구문 단축이 문제가 된다. while(bool 변수);로 문맥의 흐름을 막았다고 해보자. 그럼 while문 안의 bool 변수가 다른 스레드에서 false로 바뀌면 무한 루프를 빠져나갈까? 정답은 그렇지 않다. 계속 갇혀있다. 왜냐하면 while안의 변수는 최적화를 위해 메모리를 읽지 않고 컴파일 순간 정해진 캐시의 값을 계속 가져오기 때문이다. 이를 다른 스레드에서 보이지 않는 값이 되었다고 한다. 또다른 예로는 프로그램 내내 같은 주소를 가리키는 포인터의 값을 읽을 필요가 있다고 해보자. 이러면 컴파일러는 컴파일 당시에 해당 포인터가 가리켰던 값만을 가져온다. 그래서 해당 포인터가 가리키는 값이 변경되어도 프로그램에선 바뀐 값을 알지 못한다. 바로 이럴때 volatile을 붙여서 캐시말고 항상 메모리에서 가져오도록 만들 수 있다. 즉 휘발성이라는 말은 현재 변수에서 읽어올 값이 임시적이라는 의미다.

참고로 volatile은 컴파일러의 캐시 최적화, 인위적인 코드 패치 순서 수정[2]을 막을 뿐이므로 다중 스레드를 사용할 때는 여전히 값을 읽기 순서에 따른 동기화 문제가 발생할 수 있다.

2.3. & 참조

Type& lvalue = identifier;
좌측값 참조자 (L-Value Reference)
type& identifier = value;와 같이 &를 다른 자료형 뒤에 붙여 값이 참조 형태임을 나타낸다. 가령 int&는 다른 정수를 참조하는 변수임을 의미한다. 함수의 매개변수에서도 똑같이 사용한다. 변수에 값을 할당하거나, 수정할 때 다른 접두사, 접미사 없이 변수의 이름만 사용할 수 있다. 참조한다는 것은 해당 변수가 스스로 값을 가지지 못하고, 다른 변수의 본체를 가리킨다는 뜻이다. 마치 포인터처럼 말이다.

그러면 C의 포인터랑 대체 다른 것이 뭔가? 라면 일단 일반 사용자 단에서는 변수를 사용할 때 (*handle)이나 handle->...을 사용하지 않아도 된다. 메모리에 대해 조금 알고 있다면 포인터처럼 임의 주소 참조나 보안 문제가 없을 거라는 예상을 할 수 있다. 그러나 깊게 파고들어가면 아주 이상한 특징이 있다. 참조자는 값이 아니다. 라는 것을 명심해야 한다. 참조 변수 자체는 어떤 주소, 고유한 값을 가지지 않는다. 즉 참조 변수는 스스로 존재할 수 없다[3]. 바로 이름만 가진 변수다. 참조 변수는 명백하게 존재하고 여기저기 갖다 쓸 수 있지만 참조 변수가 가리키는 어떤 변수만 조작할 수 있을 뿐, 정작 참조 변수 자체는 건드릴 방법이 없다. 이것이 포인터와의 차이점이다. 결론적으로 참조 변수는 다른 변수의 별칭(Alias)이라고 볼 수 있다. 그래서 참조형을 사용할 때는 어떠한 값의 복사나 수정 오버헤드도 일어나지 않는다. 어떠한 조작 없이 그냥 원래 변수에 다른 식별자를 붙여주었을 뿐이기 때문이다! 만약 & 연산자를 변수 앞에 붙여 주소를 얻으면 참조 변수의 주소가 아니라, 가리키는 변수의 주소가 나온다. 주소를 가져오는 표준 라이브러리의 std::addressof 함수를 사용해도 마찬가지다.

그러나 참조형을 사용할 때 주의점이 있다. 임시 값과 리터럴 값을 저장할 수가 없다. 왜냐하면 임시 값과 리터럴 값은 어떤 변수에 저장하기 전까진 이름이 없어서 스스로 존재할 수 없는 값이기 때문이다. 당연히 고유 주소도 없으며 수정할 수 없는 값들이다. 때문에 const가 아닌 좌측 참조 변수는 임시 값 및 리터럴 값을 받을 수 없다.

2.3.1. const&

const Type& const_lvalue = identifier;
포인터와 같은 원리로, const int&와 같이 const를 참조 자료형 앞에 붙여 매개변수가 가리키는 값을 수정하지 말라고 지시를 내릴 수 있다. 그렇지만 좌측 참조자의 진정한 의의는 C++안에 있는 모든 유형의 값을 전부 담을 수 있다는 것이다. 이는 일반적으로 생각하는 클래스같은 복합 자료형에만 적용되지 않는다. const&는 독특하게 어떤 값이던 대입할 수 있다. 가령 const int& power = 9000;, struct A{}; const A& aaa = {};처럼 그 어떤 값도 담을 수 있다. 복사 불가능한 클래스, 이동 불가능한 클래스, 크기가 4GB짜리인 배열 등도 아무런 문제가 없다. 어떻게 가능한 것일까?

<C++ 예제 보기>
#!syntax cpp
int original_v1 = 1'000'000;
int original_v2 = 7'000'000;
int original_v3 = 9'000'000;

int& ref_v1 = original_v1;
int& ref_v2 = original_v2;
int& ref_v3 = original_v3;

ref_v1 = 1000; // original_v1의 값이 1000이 된다.
int* handle_v1 = &ref_v1; // original_v1의 주소를 가져온다.

ref_v1 = original_v2; // 가리키는 변수가 바뀌지 않는다. original_v1의 값이 7000000이 된다.
ref_v1 = ref_v2; // 둘 다 가리키는 변수가 바뀌지 않는다. original_v1의 값이 7000000이 된다.
*handle_v1 = original_v3;// 가리키는 변수가 바뀌지 않는다. original_v1의 값이 9000000이 된다.

const int original_c = 1'000'000;
const int& ref_c = original_c; // 상수 변수는 상수 참조형으로 받아야 한다.
C++에서 참조 변수를 쓴다는 것은 변수에 다른 이름을 붙여주는 것이라고 설명했다. 그런데 9000같은 값은 이름이 없는 리터럴 값이다. 원래 이 값들은 우측값 (Right-Value)이라고 부르는 값이였다. 전통적으로 C언어에서는 임시 값이 선언, 대입, 비교문에서 식의 오른쪽에 놓이는 경향이 있어서 이렇게 지칭되어 왔다 [4]. 그런데 C에서 포인터로 행하던 복사 없는 일관성있는 참조가 C++의 & 참조 하나만으로는 불가능하다는 문제가 있었다. 만약 어떤 함수에서 정수, 구조체 변수를 참조한다고 했을때 포인터 방식에서는 포인터 매개변수를 써서 오버로딩을 통해 적어도 일관성있는 함수들을 만들 수 있다. 주소가 필요하기 때문에 값을 담을 중계 변수를 하나 만들어야 하겠지만 말이다. 그런데 참조 변수를 만들어서 문제 많은 포인터를 대체하려고 했더니, 여전히 똑같은 짓을 해야한다는 문제가 있었다. 아직도 정수 그대로를 함수에 전달할 수 없었다. 그래서 const& 한정자는 임시 객체에 이름을 붙여주고 임시 값과 리터럴 값을 참조할 수 있게 되었다. 앞서 설명했듯이 여전히 & 참조 변수는 이것이 불가능하다. 당장 int& value = 9000; 따위는 오류가 발생한다.

2.4. && 참조

Type&& rvalue;
const Type&& const_rvalue;
우측값 참조자 (R-Value Reference)
C++11에서 추가된 핵심 문법이다. 컴파일러에게 메모리의 중복 할당을 방지하며 재사용을 지시하고, 값의 깊은 복사를 막는 역할을 한다. 그래서 &&를 다른 말로 이동 연산자라고도 부른다. &&& 참조형처럼 여전히 스스로 존재할 수 없는 존재다. &&변수의 이름 자체도 아무런 의미가 없다. 사용자가 직접 && 자료형을 명시하던가, 아니면 함수의 도움 없이는 C++에서 &&는 항상 &로, const&&는 항상 const&로 연역된다. &&&보다 더 불안정해서, 존재가 바스라지는 존재다.

C언어에서 깊은 복사를 막기 위해 메모리 풀링, 포인터 사용을 오랜 세월 해왔다. C++의 & 참조자 역시 최대한 얕은 복사로의 유도를 했을 뿐, 어쩔 수 없이 임시 객체 생성, 변수의 중복 선언 등으로 인한 오버헤드가 있었다. 시스템 자원의 중복도 큰 문제가 되었다. 가령 스레드, 뮤텍스, GDI 객체, 핸들은 시스템에서 생성되고 관리된다. 사용자는 운영체제 호출을 통해 간접적으로 제어할 수 있다. 그런데 생성, 제어는 그렇다 치고 이 중복된 자원들이 파괴되는 경우가 있을 것이다. 이때 다른 곳에서 핸들이 파괴된 일을 모르면 잘못된 운영체제 호출이 발생하고, 이 오류는 단순한 런타임 오류와는 궤를 달리할 것이다.

기존에는 한정자 없는 생성자 또는 const& 생성자 뿐이었고, 이를 본질적으로 구분할 수 없다는 문제가 있었다. 생성할 때 변수에 넣지 않고 CThread work_thread{CThread{ th_id, x, y }}; 처럼 시스템 자원 객체를 바로 전달받아도 복사 생성자에서 필연적으로 const CThread& 임시 객체가 생성되어 버린다. 이때 보이지 않는 const CThread& 객체는 work_thread에 시스템 자원을 순순히 넘겨주는 것처럼 보여도, 만약 소멸자에서 시스템 자원을 해제하도록 했다면 work_thread는 생성하자마자 죽은 객체가 된다. 이를 막으려면 두가지 방법이 있다. 임시 객체인지 표시하는 플래그를 넣던가, 자원을 해제하는 전역 함수를 별도로 만들어야 하는데, 모두 최적화, 깔끔함 둘 다 만족시키지 못한다. &&는 &를 하나 더 붙여 컴파일러와 사용자에게 복사, 참조와는 구분하게 하고 중복 자원의 문제도 깔끔하게 해결한다. 객체를 생성하는 방법을 하나 더 제시함으로써 많은 문제가 해결된 것이다.

사실 &&는 입문 시기에는 직접 쓸 필요가 없다. 컴파일러가 알아서 복사, 이동 생성자를 만들어주니까. 그러나 진도를 조금만 넘겨도 혜성처럼 등장하고, 고급 단계에서 이해하지 못하면 C++의 알 수 없는 기전에 좌절할 수 있다. C++의 표준 라이브러리에서는 <utility> 모듈의 std::move라는 함수로 간편한 이동 연산을 제공한다. 또는 사용자가 직접 static_cast<T&&>(value)로 지시할 수 있다.

2.5. 포인터

Type* ptr;
Type*& reference_of_ptr = ptr;

const Type* ptr_to_const;
const Type*& reference_of_ptr_to_const = ptr_to_const;

Type *const constant_ptr;
Type *const& reference_of_constant_ptr = constant_ptr;

const Type *const constant_ptr_to_const;
const Type *const& reference_of_constant_ptr_to_const = constant_ptr_to_const;
*를 자료형 뒤에 붙이면 어떤 주소를 담고 있는 자료형임을 나타낼 수 있다. 참조형의 경우 참조형 자체의 변수는 얻을 수 없으며 참조형이 가리키는 원본 변수의 주소를 담아야 한다. 정확하게는 포인터의 참조형만을 받을 수 있고 참조형의 포인터는 불가능하다. 작성할 때는 *&&& 앞에 붙여야 한다. 그 외에는 C언어에서의 사용법과 다르지 않다.

3. using

using Alias = Type;

template<typename... Ts>
using TemplateAlias = Type<Ts...>;
키워드 using을 사용하여 자료형의 별칭(Alias)을 선언할 수 있다. C의 typedef 구문을 대체하는 구문으로써 가독성 상승을 비롯하여 템플릿을 사용할 수 있게 되었다. 별칭의 이름으로 주어지는 식별자는 사용자가 구현한 클래스나 템플릿과 마찬가지로 새로운 자료형으로 분류된다. 즉 이 구문은 변수를 선언하는 것처럼 형식을 선언하는 것이라고 볼 수 있다. 실제로 표준에서도 자료형을 선언한다고 표현한다. 그러나 참조 변수와 같은 맥락으로 이해해야 한다. 기존의 자료형에 다른 이름을 붙이는 것이기 때문에, 기존의 자료형을 사용하더라도 using으로 선언한 자료형과 호환된다. 템플릿도 마찬가지라서 void Function(std::vector<int>& vec);이라는 함수가 있다면, 이 함수에는 using IntVector = std::vector<int>;와 그냥 std::vector<int> 모두를 인자로 전달할 수 있다.

4. 값 범주 (Value Category)

파일:상세 내용 아이콘.svg   자세한 내용은 C++/명세 문서
번 문단을
값 범주 부분을
참고하십시오.


[1] 그러나 C++14 이하의 버전을 사용해야만 하는데 문자열 처리를 구현해야 하면 공부해서 나쁠 건 없다.[2] MSVC[3] 이를 객체(Object)가 아니다라고 한다.[4] 예를 들어 if (handle == NULL) 같은 경우

파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r247에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r247 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)