Skip to content

C++ 메모

Contents

Sources

memo

  • 변수 초기화 시 = 를 사용하면 narrow type conversion(doubleintchar) 시 데이터가 유실됨. 그러나 {} 를 사용하면 데이터가 유실될 때 에러를 발생시켜줌. 따라서 {} 로 변수 초기화하는 게 더 좋음.

    그러나 특별한 상황이 아닌 경우 타입형으로 auto 를 사용함. 특별한 상황이란 코드 독자에게 타입형이 명확하게 읽혀야만 하는 경우, 또는 변수의 정확도를 명확하게 지정해야 하는 경우(float 가 아닌 double)

  • constexpr: 런타임 때 계산되는 게 아니라 컴파일 때 계산. 변수, 함수에 사용 가능. 성능 향상이 됨. const 는 런타임에 계산 가능한 상수를 의미함.

  • & 를 변수에 사용하면 주솟값을 반환할 때 사용되고, 타입형에 사용하면 레퍼런스를 얻을 때 사용됨. 레퍼런스는 일반 파라미터나 포인터와 달리 해당 변수의 값을 직접 수정 가능.

    레퍼런스가 포인터에 비해 나은 점은 포인터는 데이터에 접근할 때 * 를 써야 하는데 레퍼런스는 필요없음.

    파라미터로 전달된 값이 수정되지 않아야 하는 경우에도 레퍼런스를 사용한다면, 파라미터로 값을 복사하는 비용이 사라지기 때문에 const T& 와 같은 식으로 사용하면 성능 향상이 이루어짐.

  • if (auto n = v.size(); n!=0) 와 같이 if 문에서 변수를 초기화할 수 있다. 이 변수 n 을 if문 안에서 사용할 수 있다. 이 if문을 더 단순하게 if (auto n = v.size()) 로 줄일 수 있다. n 이 0 이 아닌지 검사한다.

  • 이 팁들은 C++ Core Guidelines 의 부분집합이다.

  • 가능한한 원시 타입 보다 유저 정의 타입(struct, class, enum 등)을 사용하는 게 더 좋다. 그것이 사용하기 더 쉽고, 에러가 적고, 개발자에게 주어진 일에 대부분 더 적합하고, 심지어 더 빠르다.

  • struct 사용법

    struct Vector {
        int sz; // number of elements
        double elem; // pointer to elements
    };
    
    Vector v;
    
    void vector_init(Vector& v, int s) {
        v.elem = new double[s]; // allocate an array of s doubles
        v.sz = s;
    }
    
  • 위와 같이 new 는 힙 메모리(동적 메모리)를 할당해주며, 이 메모리는 delete 로 제거할 때까지 소멸되지 않는다.

  • struct 멤버에 접근할 때 . 으로 접근하면 되고, 포인터 struct 의 멤버에 접근할 때는 -> 로 접근하면 된다. 그러나 C++ 에는 레퍼런스를 보통 사용하므로 -> 는 C 언어에서의 레거시인듯.

  • 레퍼런스/포인터 동작 이해하는 예시:

    int x = 2;
    int y = 3;
    int p = &x;
    int q = &y; // now p!=q and *p!=*q
    p = q; // p becomes &y; now p==q, so (obviously)*p == *q
    

    int x = 2;
    int y = 3;
    int& r = x; // r refers to x
    int& r2 = y; // now r2 refers to y
    r = r2; // read through r2, write through r: x becomes 3
    

  • class 사용법

    class Vector {
    public:
        Vector(int s) :elem{new double[s]}, sz{s} { } // constr uct a Vector
        double& operator[](int i) { return elem[i]; } // element access: subscripting
        int size() { return sz; }
    private:
        double elem; // pointer to the elements
        int sz; // the number of elements
    };
    

    생성자는 초기화되지 않은 멤버 변수들을 제거하는 역할을 한다. 즉, 클래스의 모든 변수를 초기화해야 하는 의무를 갖는다.

    본질적으로는, struct 와 class 는 서로 같다. struct 는 단지 public 이 디폴트인 class 일 뿐이고, struct 를 위한 생성자나 멤버 함수를 외부에 정의할 수 있기 때문.

  • union 이란 모든 멤버들이 같은 주소에 할당되는 struct 이다. 즉, union 은 가장 큰 공간을 차지하는 멤버 변수의 공간을 할당 받는다. union 이 사용되는 상황은 멤버 변수 중에서 오직 하나만 사용하는 경우이다. 이 경우 struct 를 사용하면, 나머지 변수를 사용하지 않기 때문에 공간이 낭비된다. 따라서 union 을 사용한다.

    enum Type { ptr, num }; // a Type can hold values ptr and num (§2.5)
    struct Entry {
        string name; // str ing is a standard-librar y type
        Type t;
        Node p; // use p if t==ptr
        int i; // use i if t==num
    };
    void f(Entry pe)
    {
        if (pe>t == num)
        cout << pe>i;
        // ...
    }
    

    이 경우 p 와 i 가 동시에 사용되지 않는다. 따라서 다음과 같이 하면 좋다.

    union Value {
        Node p;
        int i;
    };
    struct Entry {
        string name;
        Type t;
        Value v; // use v.p if t==ptr; use v.i if t==num
    };
    void f(Entry pe)
    {
        if (pe>t == num)
        cout << pe>v.i;
        // ...
    }
    
  • 위의 Union 예제에서 type 을 의미하는 Type t 와 union 이 실제로 갖고 있는 타입 사이의 대응관계가 올바르도록 유지하는 과정에서 에러가 발생하기 쉽다. 이 에러를 피하기 위해 보통은, 로우레벨에서 계속해서 코딩하지 않고, union 과 type 사이의 대응관계를 class 에서 캡슐화하고 그것에 접근하는 멤버 함수를 만들어두어서 union 을 올바르게 사용하도록 한다. 실제로 union 을 직접 다루는 것은 지양해야 하고, 최소화되어야 한다.

    이 경우 variant 라는 STL 타입을 사용하여 union 같은 로우레벨 인터페이스를 직접 다루는 것을 피하는 것이 더 낫다.

  • enum 타입은 다음과 같이 사용될 수 있다.

    enum class Color { red, blue , green };
    enum class Traffic_light { green, yellow, red };
    Color col = Color::red;
    Traffic_light light = Traffic_light::red;
    

    enum 은 정수형 값의 작은 집합을 표현하는데에 좋다. enum 을 자주 사용하면 코드 가독성이 높아지고, 에러가 감소하여 좋다.

    enum class 를 사용하면 enum 이 강하게 타입화되고 그것의 enumerator 들이 스코프 안에 놓이게 된다. 즉, Color::red 와 같이 사용해야 한다. 이로써 Traffic_light::red 와 같은 전혀 다른 context 에 있는 상수인 red 라는 상수를 잘못 사용하는 경우가 사라진다.

    enum class 가 enum 보다 더 좋다.

  • C++ 은 함수, 사용자 정의 타입, 클래스, 템플릿으로 모듈화된다.

  • 다음과 같이 선언을 하고, 구현을 따로 한다.

    double sqrt(double); // the square root function takes a double and returns a double
    
    class Vector {
    public:
        Vector(int s);
        double& operator[](int i);
        int size();
    private:
        double elem; // elem points to an array of sz doubles
        int sz;
    };
    

    그리고 어딘가에서 다음과 같이 구현하면 된다.

    double sqrt(double d) // definition of sqrt()
    {
        // ... algorithm as found in math textbook ...
    }
    Vector::Vector(int s) // definition of the constructor
    :elem{new double[s]}, sz{s} // initialize members
    {
    }
    double& Vector::operator[](int i) // definition of subscripting
    {
        return elem[i];
    }
    int Vector::siz e() // definition of size()
    {
        return sz;
    }
    
  • 다음과 같이 모듈화될 수 있다.

    그러나 이 형식은 컴파일 비용이 높고 에러가 쉽게 발생하는 C언어로부터의 레거시이다. C++20 이상에서는 moduleimport 를 통하여 더욱 현대적으로 모듈화가 진행된다.

  • namespace 를 사용하면 선언들을 묶을 수 있고, 다른 이름과 충돌되지 않게 할 수 있다.

  • throw 키워드는 실행흐름을 예외 핸들러로 넘겨준다.

    try{ } catch() { } 문을 사용하면 에러를 핸들링할 수 있다.

  • 파라미터를 전달하는 기본 방식은 복사다. 그러나 복사는 비효율적이다. 따라서 파라미터를 reference 로 전달하는 것이 효율이다. 이때 const-reference 로 파라미터를 전하면 원래 값이 불변한다는 것이 보장된다.

    그런데 성능을 좀 더 엄밀하게 향상시키려면 작은 값은 그냥 전달하고, 큰 값만 reference 로 전달하면 좋다. "작은" 의 정의는 복사해도 상관없을만큼 비용이 매우 작은 작은 값이라는 의미이다. 그래서 이 의미는 컴퓨터에 따라 달라지지만, 보통은 둘 또는 셋의 포인터들의 사이즈 혹은 그보다 더 작은 값이라고 보면 된다.

    함수 반환값의 기본 방식도 복사이다. 물론 작은 값에 대해서 복사해도 좋다. 만약 함수의 로컬에 존재하지 않는 객체에 대한 접근을 반환하고 싶다면 reference 를 반환하면 된다. 큰 객체에 대한 포인터를 반환하는 것은 구식이고, 에러가 발생하기 쉽다.

  • 큰 객체를 리턴할때 복사하지말고 move 라는 생성자를 사용하면된다

  • 반환타입을 auto 로 정의할 수 있다. 그러나 인터페이스가 불안정해진다.
  • struct 반환 예시

    struct Entry {
        string name;
        int value;
    };
    Entr y read_entr y(istream& is) // naive read function (for a better version, see §10.5)
    {
        string s;
        int i;
        is >> s >> i;
        return {s,i};
    }
    
    auto e = read_entry(cin);
    cout << "{ " << e.name << " , " << e.value << " }\n";
    
    auto [n,v] = read_entry(is);
    cout << "{ " << n << " , " << v << " }\n";
    
    map<string,int> m;
    // ... fill m ...
    for (const auto [key,value] : m)
        cout << "{" << key "," << value << "}\n";
    
    void incr(map<string,int>& m) // increment the value of each element of m
    {
        for (auto& [key,value] : m)
        ++value;
    }
    
  • 생성자에 ~ 를 붙이면 소멸자를 선언할 수 있다.

    class Vector {
    public:
        Vector(int s) :elem{new double[s]}, sz{s} // constr uctor: acquire resources
        {
            for (int i=0; i!=s; ++i) // initialize elements
            elem[i]=0;
        }
        ˜Vector() { delete[] elem; } // destr uctor: release resources
        double& operator[](int i);
        int size() const;
    private:
        double elem; // elem points to an array of sz doubles
        int sz;
    };
    

    클래스는 일반 변수처럼 소멸된다.

    void fct(int n)
    {
        Vector v(n);
        // ... use v ...
        {
            Vector v2(2n);
            // ... use v and v2 ...
        } // v2 is destroyed here
        // ... use v ..
    } // v is destroyed here