C++ 개념 정리

🏷️ c++

수정일 : 2024-12-09


본 내용은 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);