생성자
c++을 사용하여 알고리즘 문제를 풀때에도 class로 원하는 형태의 객체를 정의할 일이 종종 있다.
이경우 보통 아래와 같이 생성자를 통해 클래스의 멤버변수를 초기화 해준다.
class Edge {
public:
int from;
int to;
int distance;
Edge(int from, int to, int distance) {
this->from = from;
this->to = to;
this->distance = distance;
}
bool operator <(Edge & edge) {
return this->distance < edge.distance; //오름 차순 정렬
}
};
멤버 이니셜라이저
다른 사람의 코드를 참조하다 처음 보는 방식을 배우게 되었다. 바로 이니셜라이저 혹은 멤버 이니셜라이저라고 불리는 방법이다. 이니셜라이저를 통해 멤버 변수를 초기화 하는 방법은 아래와 같다.
class Edge {
public:
int from;
int to;
int distance;
//Edge(int from, int to, int distance) {
// this->from = from;
// this->to = to;
// this->distance = distance;
//}
Edge(int from, int to, int distance) : from(from), to(to), distance(distance) {}
bool operator <(Edge & edge) {
return this->distance < edge.distance; //오름 차순 정렬
}
};
추가적으로 이니셜라이저로 배열 변수를 초기화 하는 방법은 아래와 같다.
class Edge {
public:
int node[2];
int distance;
Edge(int from, int to, int distance) {
this->node[0] = from;
this->node[1] = to;
this->distance = distance;
}
Edge(int from, int to, int distance) : node{ from, to }, distance(distance) {}
bool operator <(Edge & edge) {
return this->distance < edge.distance; //오름 차순 정렬
}
};
처음 봤을 때 클래스 멤버변수를 초기화 하는 코드라는 것은 직관적으로 느껴졌는데 확신을 가지기 위해 알아보았다. 하지만 공부를 하면서 멤버변수를 초기화 하는 두 방식의 차이점은 무엇인지 궁금했다.
결과부터 말하자면 아래와 같이 반드시 이니셜라이저를 사용해야만 하는 경우가 존재한다.
1.상수(const) 멤버 변수를 초기화 하는 경우
2.레퍼런스 멤버 변수를 초기화 하는 경우
3.객체 멤버를 초기화 하는 경우
4.상속받은 부모클래스의 멤버변수를 초기화 하는 경우
1.상수(const) 멤버변수를 초기화 하는 경우
아래와 같이 클래스의 상수 멤버를 고정된 값으로 초기화 하는 경우에는 생성자나 이니셜라이저를 통해 초기화 할 필요가 없으므로 문제가 되지 않는다.
class A {
public:
const int MAX = 10000;
};
하지만, 상수 멤버를 객체 생성마다 다른 값(= 원하는 값 = 입력받는 값)으로 초기화 하고자 할 경우 아래와 같이 생성자를 통해서는 초기화가 불가능하다.
여기서 생성자 방식과 이니셜라이저 방식의 근본적인 차이점이 나타나는데
생성자는 int num; num = a; 처럼 선언후 값을 할당하는 것이고
이니셜라이저는 int num = a; 처럼 선언과 동시에 값을 할당하여 초기화 해주는 것이다.
즉, const 변수의 초기화는 선언과 동시에 값을 할당해 주어야 하기 때문에 선언후 값을 할당하는 생성자 초기화가 불가능한 것이다.
따라서 아래와 같이 이니셜라이저 방식으로 상수 변수를 초기화 해주어야 한다.
class A {
public:
const int MAX;
A(int max) : MAX(max) {}
};
한가지 신기했던 점은 필드 선언부보다 이니셜라이저가 우선된다는 것이었다.
이 처럼 클래스 필드에서 const int a = 4; 와 같이 선언해주어도 이니셜라이저를 통해 객체 생성시 2를 매개변수로 받는 경우 이니셜라이저가 우선되고 필드에 선언 자체는 무시되는 것으로 보인다.
2.레퍼런스 멤버 변수를 초기화 하는 경우
래퍼런스 변수도 상수 변수처럼 선원과 동시에 초기화를 해줘야 하는 특성이 있다. 따라서 마찬가지로 생성자로는 초기화가 불가능하고 이니셜라이즈를 통해 초기화를 해야한다.
단, 래퍼런스 변수의 경우 const로 선언되지 않았다면 아래와 같이 생성자를 통해 새로운 값을 할당하는 것은 가능하다.
즉, 다시 한번 강조하지만 래퍼런스 변수도 선언과 동시에 초기화 해주어야 한다는 특성이 중요한 것이다.
3.객체 멤버 변수를 초기화 하는 경우
자바에서 has a 개념처럼 한 클래스A에서 클래스 B를 멤버변수로 가지는 것이 가능한데, 이런 경우 B클래스에 default 생성자가 없다면 문제가 발생할 수 있다. 아래의 예시를 통해 이해해보자.
오류 원인은 B b;(line 16) 선언부이지 생성자 초기화 부분이 아니다.
생성자를 만들기 전에 선언부에서는 빨간줄이 나타나지 않아 처음엔 생성자에서 할당하는 부분이 오류의 원인이라고 잘못 생각했지만 그림2 처럼 구현부가 비어있는 생성자를 만들어도 오류가 발생하는 것을 보고 B b;가 오류의 원인인 것을 알게 되었다. c++과 자바에서 객체화(=인스턴스화)에 차이 때문에 헷갈렸는데
자바에서는 new 로 인스턴스화 할때 생성자를 호출하는 반면 c++에서는 B b; 이와 같이 선언만 해주어도 default 생성자가 호출되어 객체가 생성된다. 동일하게 B b(2); 이와 같이 선언만 해주어도 오버로딩 생성자로 객체를 생성한다.
따라서 위의 경우 B클래스에 default 생성자가 없는데 B b; 객체화 시 default 생성자를 호출하기 때문에 오류가 발생하는 것이었다.
따라서 이처럼 default 생성자가 없는 클래스 멤버 변수를 초기화 하는 경우 또한 아래와 같이 이니셜라이즈를 통해 초기화를 해주어야 한다.
4.상속받은 부모클래스의 멤버변수를 초기화 하는 경우
먼저 코드를 통해 문제가 발생하는 경우를 살펴보자
위와 같이 상속 받은 부모 클래스에 default 생성자가 없다면 문제가 발생한다.
이는 c++도 자바와 동일하게 자식클래스의 생성자가 먼저 부모 클래스의 생성자를 호출하기 때문이다.
자바의 경우 부모클래스가 오버로딩된 생성자만 가지는 경우 super 키워드를 통해 오버로딩 된 생성자를 호출해 주면 문제를 해결할 수 있지만 c++의 경우 이러한 super 키워드가 존재하지 않는다.
따라서 이러한 경우 경우 c++에서는 super키워드 대신 아래와 같이 이니셜라이저를 통해 부모 클래스의 멤버 변수를 초기화 해주면 문제를 해결할 수 있다.