
암시적 형변환 (implicit type conversion)
다음과 같은 코드는 어떤 결과를 출력할까?
void print(double value)
{
std::cout << value << '\n';
}
int main()
{
print(10);
}
double 타입인 매개변수에 정수인 10을 넘긴 상태이지만 문제 없이 컴파일되고 실행된다
이는 컴파일러가 타입 불일치를 알아차리고 암시적으로 형변환을 했기 때문이다 (int -> double)
이때 literal 값 뿐 아니라 변수의 값도 암시적 형변환이 진행된다
int x {10};
print(x); //같은 결과
하지만 사실 타입 변환이라고 하지만 이는 실제로 값이나 타입을 변경하는게 아닌 새로운 값을 생성하는 방식으로 처리된다
정리하면 int가 double로 변하는게 아니고, 10이 10.0으로 변하는게 아닌 double타입의 10.0 값이 새롭게 생성되는것이다
이때 새롭게 생성될 때 직접 초기화 방식 () 이 사용되어 새로운 값을 생성한다
이러한 암시적 형변환은 단점이 존재한다, 바로 데이터 손실이 발생한다는 점이다
void print(int x)
{
std::cout << x << '\n';
}
int main()
{
print(5.5);
return 0;
}
double타입인 5.5가 int로 암시적 형변환되어 5라는 값이 나오게 된다, 따라서 데이터 손실이 발생한다
이렇게 데이터 손실이 발생할 수 있는 경우는 컴파일러 warning 수준에 따라 컴파일이 불가능할 수 있다
(int -> double은 괜찮지만 double -> int는 데이터 손실 발생으로 warning이 발생한다)
(중괄호 초기화의 장점이 보이는 부분)
위와 같은 데이터 손실 경고를 출력하지 않고 암시적이 아닌 프로그래머의 명시적 형변환은 다음과 같이 처리한다
static_cast
static_cast<int>(10.5); //double타입의 10.5를 int타입으로 형변환 한다
print(static_cast<int>(10.5)); //warning X
char a {97};
std::cout << a << "value is" << static_cast<int>(a) << '\n'; //a value is 97로 출력
#include <cstdint>
std::int8_t foo{ 65 };
std::cout << foo << std::endl; //A
std::cout << static_cast<int>(foo) << std::endl; //65
이때 변수 자체는 형변환의 영향을 받지 않는다 (foo가 int가 되는게 아님, 새롭게 생성된 값이 int로 형변환 되는것이기 때문이다)
unsigned <-> signed 사이의 변환도 가능하다, 하지만 오버플로, 언더플로가 발생하면 modulo rapping이 발생하여 전혀 다른 값이 나오게 되니 unsigned <-> signed의 형변환은 지양한다
int main()
{
int s { -1 };
std::cout << static_cast<unsigned int>(s) << '\n'; // prints 4294967295
unsigned int u { 4294967295 };
std::cout << static_cast<int>(u) << '\n'; //prints -1
return 0;
}
(modulo rapping으로 인해 전혀 의도하지 않은 값이 출력됨)
추가로 std::int8_t를 사용할 때 주의할 점은 cin에서 사용하면 문제가 발생할 수 있다는 점이다
std::int8_t foo{};
std::cin >> foo;
std::cout << static_cast<int>(foo) << std::endl;
이때 만약 35를 입력하면 int8_t는 char로 간주되어 char sequence로 해석되게 되고 3, 5 각각 다른 문자로 해석된다, 따라서 3이 cin 입력 buffer에서 추출되고 5는 입력 buffer에 남아있게 된다
이때 3은 ASCII로 51이기 때문에 51값이 출력되게 된다
const (상수)
상수란 한번 정해지고 절대 변하지 않는 수를 상수라고 한다, 따라서 한번 초기화 된 후 변경될 수 없는 변수를 상수 변수라고 한다 (const variable)
const는 타입 한정자 (type qualifier)라고 한다
상수 변수는 다음과 같이 선언한다
const 타입 변수명 = 값;
const int foo = 100;
const가 type의 앞, 뒤 둘 중 아무곳에 와도 상관은 없지만 보통 표준 영어 관례상 앞에다 사용하는게 일반적이다 (뒤에다 const를 붙이는 방식을 east const라고 칭한다)
const 변수는 선언과 동시에 반드시 초기화 되어야 하고 이후에 절대 변경될 수 없다
const int foo { 100 };
foo = 20; //error
단 이때 const 변수의 초기화 값은 상수가 아닐 수 있다
int x {100};
const int foo {x}; //x는 상수 아님
const변수는 초기화 후 재할당이 불가능하다는 점 이외에는 일반적인 변수와 같은 방식으로 동작한다 따라서 특별한 명명 규칙은 필요없다
(이전에는 prefix로 k가 붙거나 _를 사용하기도 했지만 굳이 필요없다)
함수의 매개변수에도 const를 붙일 수 있다
void foo(const int x)
{
}
int main()
{
foo(100);
foo(200);
}
함수의 매개변수로 const를 사용할 때 초기화는 인자로 처리한다, 하지만 modern C++에서는 보통 일반 값 타입 변수는 const로 사용하지 않는다 (어차피 사본이기 때문에 변경되도 상관이 없고 함수 호출 종료 시 소멸될 값이다, 하지만 해당 함수 내부에서 인자 값을 변경하지 못하게 하는 용도로 쓸만하다 ex) bool)
참조나 포인터 전달에는 const를 유용하게 사용한다
함수의 return value도 const 사용이 가능하다, 이때 기본 type (int, float 등)은 const가 무시된다 (어차피 의미 없음)
const int GetValue()
{
return 10;
}
//의미 없음
또한 참조나 포인터가 아닌 값 타입으로 return 시에도 const는 의미가 없다 (어차피 소멸될 임시 사본이다)
const의 장점
상수처리의 장점에는 여러가지가 있다
const 이외에 macro를 이용하여 상수 선언이 가능하다
#define TEST 100;
std::cout << TEST << std::endl; //100출력
전처리기는 해당 파일을 처리할 때 TEST를 100으로 전부 변경한다, 이때 TEST와 같은 대체 텍스트는 상수이다
하지만 일반적으로 상수 처리를 할 때 전처리 방식은 사용하지 않는다
가장 큰 문제는 바로 전처리 지시문이기 때문에 C++ 문법에 영향을 받지 않아 범위 규칙을 따르지 않는다는 것이다
void TestFunction()
{
#define test 100;
}
void FooFunction(int test) //이름 충돌
{
}
int main()
{
std::cout << test << std::endl; //이게 가능함, 전처리 지시문은 C++문법 (범위규칙)을 따르지 않기 때문
}
그렇기 때문에 추후에 해당 매크로 상수 이름 충돌이 발생할 수 있고 원하지 않는곳에서 상수 매크로 처리가 발생할 수 있다
다른 이유는 매크로를 사용하면 디버깅이 어려워진다는 점이다, 전처리 단계에서 이미 처리되기 때문에 런타임에 디버깅 제한이 걸린다
이미 세상에서 고정된 값과 같은 상수는 전역으로 선언해 다른 파일에서 공통으로 사용하는것이 좋다
ex) pi와 같은 값
volatile
C++23기준으로 C++에는 const 이외에 단 하나의 타입 한정자가 존재한다, 이는 volatile이다
volatile은 해당 변수가 언제든 바뀔 수 있음을 컴파일러에게 알린다 (굉장히 드물게 사용되며 최적화를 비활성화 한다 -> 컴파일러 최적화에 의해 옛날 cache된 값을 계속해서 사용하는걸 방지한다)
const와 volatile을 합쳐 cv-qualifiers라고 칭한다
literal
literal은 코드에 직접 삽입되는 값을 의미한다
return 10; //10은 literal
std::cout << "Kelvin" << std::endl; //Kelvin은 literal
이러한 literal들도 type이 존재한다 (10은 정수, Kelvin은 문자열 등)
다양한 접미사로 literal type변경이 가능하다

대부분 이러한 접미사를 자주 사용하지는 않는다 (f제외)
이러한 접미사들은 대부분 대소문자 구분을 하지 않지만 예외가 존재한다
(s, sv는 소문자만 가능, l은 숫자로 보일 수 있기 때문에 L을 사용하는걸 선호)
std::cout << 5L << '\n'; //long
std::cout << 5u << '\n'; //unsigned int
사실 대부분의 케이스에서 위와 같은 정수형 literal 접미사는 필요가 없다
int a { 5 };
unsigned int b { 6 };
long c { 7 };
위의 코드에서는 컴파일러가 알아서 6(int)를 unsigned int로, 7(int)를 long으로 타입 변환하기 때문이다
부동 소수점 literal에서는 f가 중요하다
일반적으로 double이 부동 소수점 default이기 때문이다, float으로 literal type을 변경하려면 f 접미사를 붙여야 한다
float a {5.f};
float f {10.4}; //warning
부동 소수점 literal값을 표현할 때 지수상수인 e를 사용할 수 있다
double foo {6.02e23}; //6.02 x 10^23
문자열 literal은 " "안에 기입하여 작성한다, 결국 문자열은 문자의 집합이다
"Kelvin";
C++에서 문자열 type은 기본 type이 아니다, 기본적으로 C++에서 문자열을 사용할 때는 C-Style의 문자열을 사용한다

C-Style 문자열 타입이 아닌 std::string iteral은 임시 객체가 생성되어 해당 표현식 종료 시 소멸되기 때문에 바로 사용해야 한다 (std::string_view는 static메모리 영역에 저장됨)
magic number
magic number란 추후에 변경될 수 있는 의미가 불분명한 문자적 숫자를 의미한다 (꼭 숫자일 필요는 없음, 문자, 문자열도 가능)
const int maxStudentsPerSchool{ numClassrooms * 30 };
setMax(30);
여기서 30과 같은 수를 magic number라고 한다, 정확히 어떤 의미인지 알기 힘들고 추후에 변경될 가능성이 존재한다, 이러한 magic number를 사용하는건 지양해야 한다
(유지보수에 굉장히 나쁘다)
magic number 대신 const 상수를 사용하는걸 권장한다