본 내용은 2023 MISRA-CPP 가이드라인을 공부하면서 개념을 정리한 것이다.
참조(Reference)와 포인터(Pointer)
포인터(Pointer)
메모리 주소를 저장하는 변수
1// 변수 선언
2int a = 10;
3int* b = &a; // a의 주소를 저장
4
5// 매개변수로 전달
6void add(int* a, int* b) {
7 *a + *b;
8}
9
10add(&a, &b);
참조(Reference)
변수에 별칭을 부여하는 것
-
이름으로 대상을 가리킨다.
-
제약사항
- 참조는 선언과 동시에 초기화해야 한다.
- 참조는 한 번 초기화되면 다른 변수로 변경할 수 없다.
- 참조는 NULL로 초기화할 수 없다.
1// 변수 선언
2int a = 10;
3int& b = a; // a의 별칭을 b로 지정
4
5// 매개변수로 전달
6void add(int& a, int& b) {
7 a + b;
8}
9add (a, b);
템플릿 (Template)
템플릿은 함수나 클래스를 정의할 때, 타입을 일반화하여 코드를 작성하는 방법
함수 템플릿
1template <typename T>
2T add(T a, T b) {
3 return a + b;
4}
클래스 템플릿
1template <typename T>
2class Point {
3public:
4 T x, y;
5 Point(T x, T y) : x(x), y(y) {}
6};
템플릿 특수화 (Template Specialization)
템플릿을 특정 타입에 대해 구체화하는 것
함수 템플릿 명시적 특수화
1template <typename T>
2T add(T a, T b) {
3 return a + b;
4}
5
6// int 타입에 대한 특수화
7template <>
8int add(int a, int b) {
9 return a + b;
10}
함수 템플릿 부분 특수화
1template <typename T>
2T add(T a, T b) {
3 return a + b;
4}
5
6// 포인터 타입에 대한 부분 특수화
7template <typename T>
8T add(T* a, T* b) {
9 return *a + *b;
10}
속성 (Attribute)
컴파일할때 특정 메시지를 생성하거나 컴파일러가 특정 동작을 수행할 수 있도록 함
[[noreturn]] (~c++11)
함수가 정상적으로 호출자에게 제어권을 반환하지 않음을 나타냄
- [[noreturn]]으로 선언된 함수가 정상적인 반환을 시도하는 경우 undefined behavior 발생
1[[noreturn]] void error() {
2 throw "error";
3}
4int32_t main() {
5 error();
6 return 0; // dead code로 최적화될 수 있음
7}
[[maybe_unused]] (~C++17)
사용하지 않는 변수, 함수, 매개변수, 타입에 대한 경고를 컴파일러가 무시하도록 명시
1[[maybe_unused]] int a = 10;
[[nodiscard]] (~C++17)
함수의 반환값을 무시하는 경우 컴파일러가 경고하도록 명시
- 반대로 (void) 형태로 캐스팅하여 경고를 억제할 수 있음
1[[nodiscard]] int add(int a, int b) {
2 return a + b;
3}
[[fallthrough]] (~C++17)
switch문에서 case 라벨을 사용할 때, break를 사용하지 않는 경우 경고를 억제
1switch (a) {
2 case 1:
3 std::cout << "1" << std::endl;
4 [[fallthrough]]; // 의도적으로 2로 넘어간다는 것을 명시
5 case 2:
6 std::cout << "2" << std::endl;
7 break;
8 default:
9 std::cout << "default" << std::endl;
10 break;
11}
if constexpr (~C++17)
compile time에 조건문을 처리할 수 있도록 함
- 조건이 거짓인 경우 해당 블록은 컴파일되지 않음
- 템플릿과 비슷한 기능을 수행
- 템플릿은 compile time에 모든 코드를 생성하지만 if constexpr는 조건에 따라 코드를 생성
1template <typename T>
2void print(T value) {
3 if constexpr (std::is_same_v<T, int>) {
4 std::cout << "int: " << value << std::endl;
5 } else if constexpr (std::is_same_v<T, double>) {
6 std::cout << "double: " << value << std::endl;
7 } else {
8 std::cout << "unknown type" << std::endl;
9 }
10}
Lambda (~C++11)
익명 함수를 정의하는 표현식
- 간단한 함수를 선언할 때 사용
1auto add = [](int a, int b) {
2 return a + b;
3};
Transient/Non-transient lambda
- Transient lambda
즉시 호출되고 소멸되는 람다 함수
1 int x = 10;
2 auto result = [](int x) {
3 return x * 2;
4 }(); // 즉시 호출
- Non-transient lambda
메모리에 저장되어 여러 번 호출할 수 있는 람다 함수
1std::function<int32_t()> add = [](int a) {
2 return a + 10;
3};
4
5int32_t result = add(10);
Closure (~C++11)
lambda expression에서 외부 변수에 대한 참조를 캡처하는 방식
1int a = 10;
2auto add = [&a](int b) {
3 return a + b;
4};
Capture
lamda 함수에서 외부 변수를 함수 내부로 가져오는 것
-
lambda 함수가 정의된 시점의 외부 변수를 사용할 수 있도록 함
-
parameter 방식과의 비교
1// parameter 방식
2void addAndPrint(int a, int b) {
3 std::cout << a + b << std::endl;
4}
5
6int main() {
7 int x = 10;
8 int y = 20;
9 addAndPrint(x, y); // 매번 호출할 때 값을 전달해야 함
10}
1// capture 방식
2int x = 10;
3int y = 20;
4
5auto addAndPrint = [&x, &y]() { // 외부 변수 x와 y를 참조로 캡처
6 std::cout << x + y << std::endl;
7};
8
9addAndPrint(); // 매개변수 없이도 외부 변수에 접근 가능
Capture List
[&]
: 모든 외부 변수를 참조로 캡처[=]
: 모든 외부 변수를 값으로 캡처[a]
: 변수 a를 값으로 캡처[&a]
: 변수 a를 참조로 캡처
Escape Sequence
문자열 리터럴에서 특정 문자를 표현하기 위해 사용하는 문자열
Escape Sequence | Description |
---|---|
\' |
작은 따옴표 |
\" |
큰 따옴표 |
\? |
물음표 |
\\ |
역슬래시 |
\a |
벨 소리 |
\b |
백스페이스 |
\f |
폼 피드 |
\n |
개행 |
\r |
캐리지 리턴 |
\t |
수평 탭 |
\v |
수직 탭 |
\0 |
널 문자 |
\nnn |
8진수 |
\xnn |
16진수 |
Encoding
C++에서는 문자 리터럴에 접두사를 붙여 인코딩을 지정할 수 있음
Encoding | Prefix | Example |
---|---|---|
Literal Wide | L |
L'A' |
Literal UTF-8 | u8 |
u8'A' |
Literal UTF-16 | u |
u'A' |
Literal UTF-32 | U |
U'A' |
ASCII | none | '\x41' |
UTF-8 | none | '\xC3\xA9' |
Unicode (UTF-16) | none | '\u0041' |
Unicode (UTF-32) | none | '\U00000041' |
Cast Operator
C++에서는 다양한 형변환 연산자를 제공
static_cast
논리적으로 형변환이 가능한 경우에 사용
- 기본 타입 간의 변환, 포인터/참조 간의 명시적 변환에 사용
- 컴파일 시점에 검증 -> 논리적으로 맞지 않은 경우 컴파일 에러 발생
1int a = 10;
2double b = static_cast<double>(a);
dynamic_cast
상속 관계에서의 형변환을 위해 사용
- 런타임 시점에 안전한 형변환을 보장
- 반드시 가상 함수가 있는 클래스에서만 사용 가능
- 다운캐스팅 시, 형변환이 불가능한 경우 nullptr을 반환
1class Base {
2public:
3 virtual void print() {}
4};
5class Derived : public Base {
6public:
7 void print() override {}
8};
9
10Base* base = new Derived();
11Derived* derived = dynamic_cast<Derived*>(base); // 안전한 다운캐스팅
const_cast
포인터 또는 참조형의 const 속성을 제거하기 위해 사용
- 상수형 -> 비상수형으로 변환
- 불변 데이터를 수정하는 데 사용하면 undefined behavior 발생
1const int a = 10;
2int* b = const_cast<int*>(&a);
reinterpret_cast
포인터 형변환이나 비논리적 형변환을 위해 사용
- 메모리 레이아웃을 기반으로 포인터를 변환
- undefined behavior를 발생시킬 수 있음
- 완전히 다른 타입으로 변환이 가능하지만, 안정성을 보장하지 않음
1int a = 10;
2int* b = reinterpret_cast<int*>(&a);
constexpr (~C++11)
컴파일 시간에 평가되는 표현식을 생성하기 위해 사용
변수
- compile time 상수에만 사용 가능
1constexpr int num1 = 10;
2
3int a = 20;
4constexpr int num2 = a; // error: 변수 a는 runtime에 결정되므로 사용 불가
함수
- 함수가 compile time에 실행되도록 보장
- 인자 값이 compile time 상수인 경우 -> constexpr 함수로 동작
- 인자 값이 compile time 상수가 아닌 경우 -> constexpr 함수로 동작
- body에서 불가능한 구문
- goto
- try-catch
- 초기화 수행이 없는 변수 선언
- 리터럴 타입이 아닌 변수 선언
- static 변수 선언
- tls(thread local storage) 변수 선언
- 등…
1constexpr int fibonacci(const int n) {
2 if (n <= 1) return n;
3 return fibonacci(n - 1) + fibonacci(n - 2);
4}
5fibonacci(10); // compile time에 다 계산되어서 55로 대체됨
lvalue, rvalue 참조
lvalue
객체를 가리키는 메모리 위치를 나타내는 표현식
- lvalue 참조
1int b = 10;
2int& a = b; // a는 b의 참조자 (lvalue 참조)
3a = 20;
- 멤버 함수에서의 사용 예시
1class MyClass {
2public:
3 void show() & {
4 std::cout << "lvalue function called (object is lvalue)" << std::endl;
5 }
6};
7
8MyClass obj;
9obj.show(); // lvalue 객체에서 호출
rvalue
메모리에 위치에 할당된 값을 나타내는 표현식
-
주소가 없거나, 임시로 할당된 값
-
주로 표현식이나 함수의 반환값이다.
-
move semnatics를 사용하여 자원을 효율적으로 이동시키는데 사용한다.
-
rvalue 참조 (~C++11)
1int&& a = 10;
2
3void show() && {
4 std::cout << "rvalue function called (object is rvalue)" << std::endl;
5}
- 멤버 함수에서의 사용 예시
1class MyClass {
2public:
3 void show() && { // rvalue에서만 호출 가능
4 std::cout << "rvalue function called (object is rvalue)" << std::endl;
5 }
6};
7
8MyClass().show(); // rvalue 객체에서 호출
가상함수
상속 관계에서 동적 바인딩을 위해 사용
- 가상 함수를 가진 클래스를 다형성 클래스(polymorphic class)라고 함
- 순수 가상 함수를 가진 클래스를 추상 클래스(abstract class)라고 함
virtual 지정자
- 가상 함수 : 오버라이딩이 선택적임
- 순수 가상 함수 : 오버라이딩이 필수임
1class Base {
2public:
3 // 가상함수
4 virtual void show() {
5 std::cout << "Base::show()" << std::endl;
6 }
7
8 // 순수 가상함수
9 virtual void print() = 0;
10};
override 지정자
오버라이딩을 명시적으로 표시
1class Derived : public Base {
2public:
3 void show() override {
4 std::cout << "Derived::show()" << std::endl;
5 }
6};
final 지정자
상속을 방지하는 키워드
- 클래스에 사용 : 상속을 방지
- 함수에 사용 : 오버라이딩을 방지
1// 상속을 방지
2class Car final { ... };
3
4// 상속과 같이 쓰이는 경우
5class Derived final : public Base { ... };
6
7class Base {
8public:
9 // 오버라이딩을 방지
10 void show() final { ... }
11};
enum vs enum class
enum
기존의 C 스타일 열거형
- 컴파일 시점에 정수형 상수로 변환 -> 성능 저하가 없음
- 다른 enum 영역이라도 변수명이 중복되면 충돌 발생
1enum Color {
2 RED,
3 GREEN,
4 BLUE
5};
6Color color = RED;
enum class (~C++11)
enum의 확장된 버전
- 묵시적 형변환이 불가능 -> 명시적 형변환 필요
- enum 영역이 격리되어 변수명이 중복되어도 충돌이 발생하지 않음
1enum class Color {
2 RED,
3 GREEN,
4 BLUE
5};
6Color color = Color::RED;
explicit
지정자
단일 인자 생성자의 암시적 형변환을 방지하기 위해 사용
- 단일 인자 생성자 앞에 사용
1class MyClass {
2public:
3 explicit MyClass(int a) : a(a) {}
4}
5
6MyClass obj = 10; // error: 암시적 형변환 불가
7MyClass obj(10); // ok
숨겨진 friend
함수
클래스의 private 멤버에 접근할 수 있도록 하는 함수
- 멤버함수가 아니지만 클래스의 private 멤버에 접근할 수 있음
- 클래스 내부에 정의해야 함
- ADL로만 호출 가능
1namespace MyMath {
2 class Point {
3 int x, y;
4
5 Point(int x, int y) : x(x), y(y) {}
6
7 friend void operator+(Point& a, Point& b) {
8 a.x += b.x;
9 a.y += b.y;
10 }
11 };
12}
13int32_t main() {
14 MyMath::Point a(10, 20);
15 MyMath::Point b(30, 40);
16
17 a + b;
18}
ADL (Argument Dependent Lookup)
함수 호출 시 인자의 네임스페이스를 검색하여 함수를 찾는 것
1namespace MyMath {
2 struct Point {
3 int x, y;
4 Point(int x, int y) : x(x), y(y) {}
5 };
6
7 void operator+(Point& a, Point& b) {
8 a.x += b.x;
9 a.y += b.y;
10 }
11}
12int32_t main() {
13 MyMath::Point a(10, 20);
14 MyMath::Point b(30, 40);
15
16 a + b; // ADL로 인해 MyMath::operator+ 함수 호출
17 MyMath::operator+(a, b); // ADL이 없었다면 명시적으로 호출
18}
std::variant
(~C++17)
여러 타입 중 하나를 저장하는 클래스
- union과 똑같이 동작하지만, type-safe함
1std::variant<int, double, std::string> value = 1;
2cout << std::get<int>(value) << endl; // 1
3
4value = 3.14;
5cout << std::get<double>(value) << endl; // 3.14
6
7value = "hello";
8cout << std::get<std::string>(value) << endl; // hello
멤버, 비멤버 함수
- index() : 현재 저장된 타입의 인덱스를 반환
- holds_alternative() : 특정 타입이 저장되어 있는지 확인
- get() : 특정 타입의 값을 반환 (타입이 맞지 않으면 예외 발생)
- get_if() : 특정 타입의 값을 반환 (타입이 맞지 않으면 nullptr 반환)
- visit() : 저장된 타입을 몰라도 처리 가능
1std::variant<int, double, std::string> value;
2
3value = "hello";
4value.index(); // 2 (타입 순서대로 index 형성)
5
6std::holds_alternative<double>(value); // true
7
8value = 1;
9std::get<int>(value); // 1
10std::get_if<int>(&value); // 1
11
12std::visit([](auto& arg) {
13 std::cout << arg << std::endl;
14}, value);