Study: Software(SW)/SW: Language

[C++] 객체지향 프로그래밍: static, explicit, mutable, default, delete...

DrawingProcess 2022. 6. 22. 08:39
반응형

static


총 생성된 인스턴스의 수를 구하는 클래스가 있다고 하면? 어떻게 구현할 것인가?
대부분이 전역변수를 설정하여 구한다고 할 것이다.

전역변수의 경우 프로젝트의 크기가 커질수록 실수로 인해 겹쳐서 오류가 날 수 있어 사용하지 않는 편이 좋다.
따라서 전역변수 설정을 피하기 위해서 나온 클래스에만 종속되는 변수가 'static' 이다.

 static 멤버 변수의 경우, 클래스의 모든 객체들이 '공유' 하는 변수로써 각 객체 별로 따로 존재하는 멤버 변수들과는 달리 모든 객체들이 '하나의' static 멤버 변수를 사용하게 된다.
static 멤버 변수의 경우, 멤버 변수들 처럼, 객체가 소멸될 때 소멸되는 것이 아닌, 프로그램이 종료될 때 소멸된다. static 멤버 변수는 아래와 같이 정의한다.

class Marine {
	static int total_marine_num;
    ...
}
int Marine::total_marine_num = 0;

그리고 인스턴수의 개수를 알고 싶다면 생성자, 소멸자에 다음을 추가해주면 된다.

Marine::Marine(int x, int y): coord_x(x), coord_y(y) { total_marine_num++; }
Marine::~Marine(){ total_marine_num--; }

static 멤버 함수는 아래와 같이 정의하며, static 멤버변수에만 접근 가능하다.
this 키워드 사용해야 호출할 수 있는 일반적인 멤버함수나 멤버변수에는 접근할 수 없다.

class Marine {
    static void getMarineNum(){
    	std::cout << total_marine_num << '\n';
    }
    ...
}

explicit


원래 아래와 같이 생성자가 정의되어 있다면?

class MyString {
    MyString(int capacity);
    ...
}
void DoSomethingWithString(MyString s) { // Do something... }
int main() {
    DoSomethingWithString(3);
}

main에서 위와 같이 DoSomethingWithString(3); 쓰더라도 아래와 같이 암시적 변환되어 컴파일된다.

int main() {
    DoSomethingWithString(MyString(3));
}

하지만 아래와 같이 explicit 키워드를 사용한다면?
원하지 않는 암시적 변환을 할 수 없도록 컴파일러에게 명시할 수 있으며, 
DoSomethingWithString(3)처럼 사용할 경우 에러가 발생한다.

class MyString {
    explicit MyString(int capacity);
    ...
}
void DoSomethingWithString(MyString s) { // Do something... }
int main() {
    DoSomethingWithString(3);
}

mutable


원래 const 함수 내부의 멤버변수의 값을 바꾸는 것은 불가능하다.
하지만, 멤버변수를 mutable로 선언하였다면? const 함수에서도 값을 바꿀 수 있다.

#include <iostream>

class A {
  mutable int data_;

 public:
  A(int data) : data_(data) {}
  void DoSomething(int x) const {
    data_ = x;  // 가능!
  }

  void PrintData() const { std::cout << "data: " << data_ << std::endl; }
};

int main() {
  A a(10);
  a.DoSomething(3);
  a.PrintData();
}

default


  • 클래스는 기본적으로 기본 생성자, 소멸자, 복사 생성자, 대입연산자, 이동 생성자, 이동 대입 연산자를 생성해준다.
  • 하지만 기본 생성자는 복사 생성자 또는 이동 생성자가 정의되면 자동으로 만들어주지 않는다.
  • 따라서 기본 생성자를 새로 정의해주어야 하는데 default 함수를 이용하며 간략하게 만들 수 있다.
  • 하지만 default 키워드로 생성자를 만든다면? 얕은 복사만 가능하다.
    • 얕은 복사 (shallow copy) : 실제 데이터가 아닌 주소를 복사
    • 깊은 복사 (deep copy) : 실제 데이터를 복사 (새로운 메모리를 할당)
  • 따라서 "깊은 복사를 하지 않아도 되므로 컴파일러가 구현해주는 디폴트 함수 및 연산자를 사용하겠다" 라고 명시적으로 표시해주는 것
class Widget{
public:
    Widget() = default;    // 생성자를 기본적으로 만들어 줍니다.
    Widget(const Widget& rhs) = default; // 복사생성자도 기본적으로 만들어줘요
private:
    int a;
};
int main() {
    Widget a;
}

delete


  • C++11 이전엔 private 안에 복사 생성자와 대입 연산자를 정의하여 외부에서 접근하지 못하게 하는 방법을 사용하였다.
  • C++11 이후엔 delete 키워드를 통하여 특정 함수에 대한 정의를 금지할 수 있다.
  • 이는 "컴파일러가 생성해주는 디폴트 값이 필요없어 생성되지 않겠다"는 의미이다.
    • delete 키워드가 선언된 복사 생성자를 호출하려고 해보면 다음과 같은 에러 메세지를 준다.
    • cannot be referenced -- it is a deleted function
class Widget{
public:
    Widget() : a(nullptr) { }
    Widget(int _x) : a(new int) { *a = _x; }
    Widget(const Widget& rhs) = delete;
    Widget(Widget&& rhs) = delete;
    Widget& operator=(Widget&& rhs) {
        a = rhs.a;
        rhs.a = nullptr;
        return *this;
    }
    ~Widget() { delete a; }
private:
    int* a;
};
int main() {
    Widget A(1);    // ok, 기본 생성자로 생성합니다.
    Widget B(Widget(1));    //ok, 복사생략으로 기본 생성자로 생성합니다.
    Widget C;
    C = Widget(10);    //ok, 이동 대입 연산자로 초기화 됩니다.
    Widget D = std::move(Widget(20));    //error, 이동 생성자는 삭제되었습니다.
}

참조



반응형