new, delete

malloc / free 와 대응된다.

int * a = (int*)malloc(sizeof(int));
free(a);
int * arr = (int*)malloc(sizeof(int) * 10);
free(arr);
int * a = new int;
delete a;
int * arr = new int[10];
delete[] arr;

와, 넘나 간단한 것! 주의할 점은 delete[] 정도겠다. 꺽쇠를 적지 않으면 어떻게 되냐고? 예상치 못한 결과를 초래할 수 있다고 한다. (그 반대로 new 로 할당받고 delete[] 로 해제하는 것도 마찬가지)

생성자

객체 생성 시 java 와 조금 다르다.

Date day = new Date(2019, 5, 12);
Date day(2019, 5, 12); // 이렇게만 간단히 적어도 되고
Date day = Date(2019, 5, 12); // 이게 정식 방법 = 명시적 방법

헷갈리면 하나만 기억하자. C++ 에서는 new 가 할당 명령이라는 사실. (malloc 을 못 쓴다고 하진 않았다. 단지 new / delete 는 생성자와 소멸자를 호출해주는 게 차이점이다.)

복사 생성자

말은 거창하지만, 동일 객체를 레퍼런스 인자로 받아서 복사하는 생성자이다.

Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
  cout << "복사 생성자 호출 !" << endl;
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;
}

이걸 조금 더 정형화하면,

T(const T& a);

중요한 점은 레퍼런스 인자에 const 가 붙었으므로, 복사 대상 객체는 수정할 수 없다. 이건 규정은 아니지만, 붙이는게 바람직하다.

마치 const int* 와 같은 뜻이다. (const 가 앞에 붙으면 레퍼런스/포인터 변수가 가리키는 본래 영역의 수정을 금지함.)

여기서 트릭 하나. 두 구문은 같다. 물론 복사 생성자가 있다면 말이다.

Photon_Cannon pc3 = pc2; // 이게 더 직관적이죠?
Photon_Cannon pc3(pc2);

그런데 이 구문은 다르다. 당연히 다르다. 두 번째는 복사 생성자 호출이 안 된다. 그냥 대입 연산이 수행된다.

// 1
Photon_Cannon pc3 = pc2;
// 2
Photon_Cannon pc3;
pc3 = pc2;

디폴트 복사 생성자

모든 멤버 변수를 1 대 1 로 복사하는 생성자가 이미 있다. 이를 디폴트 복사 생성자라고 하자. 위의 복사 생성자가 하는 일을 그대로 해 주기 때문에, 특별한 일을 하지 않는 것이라면 굳이 따로 정의해서 쓸 필요가 없다.

그럼 언제 디폴트 복사 생성자 말고 따로 정의해야 할까? 바로 멤버 변수/객체가 구별되어야 할 때. (특히 문자열..) 두 객체 중 하나가 소멸되면 다른 하나는 메모리 영역을 잃어버려 결국 invalid memory read 가 터진다.

생성자의 초기화 리스트

Photon_Cannon::Photon_Cannon(int x, int y)
  : hp(100), shield(100), coord_x(x), coord_y(y), damage(20)  {}

이렇게 쓰면 된다. 각 멤버 변수 할당은 괄호로 받고, comma 로 구분한다.

이유는? 번거로우니까.. 그리고 초기화 리스트를 사용하면 객체 생성과 대입을 같이 한다. 말하자면, 이런 거다. 레퍼런스나 상수는 선언과 함께 초기화되어야 하기 때문에 초기화 리스트를 선호해야 하는 이유가 여기 있다. 레퍼런스나 상수를 객체에 넣어야 한다면 방법이 없다.

int a = 10;
// 위/아래를 비교해 보자.
int a;
a = 10;

Static 변수/함수

class 안의 static 변수는 항상 존재한다. class 안의 static 함수는 (className)::(staticFunc) 형태로 호출하면 된다. 객체에 대고 호출하는 함수가 아니다.

static 함수에서는 static 변수만 참조할 수 있다. 당연한 것이, 이 static 함수에서는 어느 객체의 멤버 변수인지 특정할 수 없기 때문이다.

explict

class MyString {
public:
    MyString();
    MyString(const char* str);
}

void funcWithMyString(MyString s) { ... }

int main() {
    MyString sStrObj;
    funcWithMyString(sStrObj); // 이것도 되고
    funcWithMyString(MyString("abc")); // 이것도 되는데
    funcWithMyString("abc"); // 이건 안될 것 같.. 응?
}

아니, 된다. MyString 객체의 생성자 중 const char* 를 받을 수 있는 생성자가 존재하면, 그대로 변환해서 실행을 시켜버린다. 이걸 암시적 변환 (implicit conversion) 이라고 한다.

사용자가 만약에 ‘5252, 암시적 변환 하지마라고!’ 라고 이야기 해주려면? 생성자 앞에 explicit 키워드를 붙이면 된다.

class MyString {
public:
    MyString();
    explicit MyString(const char* str);
}

이 키워드는 한 가지 더 의미를 가진다.

MyString s("abc"); // 이건 언제나 되고
MyString s = "abc"; // explict 가 붙으면 이게 안 된다.

Mutable

중요

아무튼 계속 하자.

class A {
  int data_; // 에러!
  // mutable int data_; // 가능하게 만드려면 이렇게..
 public:
  A(int data) : data_(data) {}
  void DoSomething(int x) const {
    data_ = x;  // 불가능!
  }
}

당연히 const 멤버 함수니까 멤버 변수 data_ 를 수정하면 안 된다. 이걸 가능케 하려면, mutable 키워드를 앞에 붙이는 방법이 있다.

왜 필요하죠?

const 함수로 선언해서, 멤버 변수 거의 모두를 수정하면 안 되도록 규정한 곳에서 부득불 ‘일부’ 멤버를 바꿔야 하는 경우에 쓴다. 이 때의 ‘일부’ 가 mutable 키워드를 부여받는 것이고.

프로그래밍 접근을 하면, 이런 변수는 대개 ‘캐시’나 임시 상태 보관용 변수들일 것이다.