티스토리 뷰
해당 글은 '윤성우의 열혈 C++ 프로그래밍'을 참고하여 작성함. (이하 해당 도서라고 지칭)
자바는 기본적으로 대입 연산이나 매개변수 전달에 객체의 참조를 전달하는 방식인 반면 C++은 복사 개념을 도입하면서 조금 복잡한 부분이 있는 것 같다. C++에서의 대입 연산, 매개변수 전달, 함수의 반환 시 호출되는 복사 생성자에 대해 알아보기에 앞서, 멤버 이니셜라이저의 개념을 먼저 살펴보려고 한다.
멤버 이니셜라이저란 생성자에서 멤버변수 초기화를 위해 사용되는 문법으로, 보통 멤버변수로 선언된 객체의 생성자 호출에 많이 활용된다. 이에 대한 간단한 예제는 다음과 같다.
#include <iostream>
using namespace std;
class Simple {
private:
int num;
public:
Simple(int num) :num(num) {}
};
class SoSimple {
private:
int num1;
const int num2;
Simple simple;
public:
SoSimple(int n1, int n2, int n) :num2(n2), simple(n) {
num1 = n1;
}
};
위와 같은 이니셜라이저를 통해 초기화되는 멤버변수는 선언과 동시에 초기화가 이뤄진다. 즉 이니셜라이저를 통해 초기화된 num2
, simple
멤버변수는 다음 문장이 실행되는 것이라고 볼 수 있다.
int num2 = n2;
Simple simple(n); //생성자 호출
반면 생성자의 몸체에서 대입연산을 통해 초기화된 num1
멤버변수는 다음 문장이 실행되는 것과 같다.
int num1;
num1 = n1;
따라서 선언과 동시에 초기화해야 하는 const
변수나 참조형 변수가 클래스의 멤버변수로 선언된 경우 두 변수는 생성자의 몸체가 아닌 항상 멤버 이니셜라이저로 초기화되어야 한다.
클래스의 멤버변수가 일반 자료형인 경우 멤버 이니셜라이저를 이용한 초기화와 생성자 몸체 내부에서의 초기화 사이에 성능상 차이가 별로 없을지도 모르지만, 객체인 경우에는 성능에 차이가 나타날 수 있다. 만약 후자의 방법으로 초기화한다면 최악의 경우 생성자가 총 2번 호출될 수 있다.
class Simple {
private:
int num;
public:
Simple() :num(0) {
cout << "기본 생성자" << endl;
}
Simple(int num) :num(num) {
cout << "인자 1개 생성자" << endl;
}
int getNum() {
return num;
}
void setNum(int num) {
this->num = num;
}
};
class SoSimple {
private:
int num1;
const int num2;
Simple simple;
public:
SoSimple(int n1, int n2, int n) :num2(n2) {
cout << num1 << endl;
cout << num2 << endl;
cout << simple.getNum() << endl;
num1 = n1;
simple = Simple(n);
cout << simple.getNum() << endl;
}
};
int main() {
SoSimple(10, 20, 30);
return 0;
}
실행결과
기본 생성자
2056516117(쓰레기값)
20
0
인자 1개 생성자
30
위와 같은 실행결과가 나오는 이유를 생각해보자. 먼저 해당 도서의 필자는 객체의 생성과정을 다음과 같이 정리하였다.
1단계: 메모리 공간의 할당
2단계: 이니셜라이저를 이용한 멤버변수(객체)의 초기화
3단계: 생성자의 몸체부분 실행
즉 생성자의 몸체부분이 실행되기 전 클래스 멤버변수의 메모리 공간 할당과 초기화가 발생한다는 것이다. 따라서 생성자 몸체 부분을 실행하기 전, 멤버 이니셜라이저를 통해 초기화 값을 지정하지 않은 num1
에는 쓰레기 값이 저장되고 객체 simple
을 초기화하기 위해 (별도로 생성자를 지정하지 않아) 기본 생성자가 호출되는 것이다.
이후 생성자의 몸체 내부에서 매개변수로 전달된 값을 이용해 객체를 생성한 후, 객체 멤버변수에 대해 대입연산을 실행하면 어떻게 될까?
사실 처음에는 복사 생성자가 호출될 것이라고 예상했다. 그러나 실제 결과를 보면 복사 생성자는 호출되지 않는다. 그 이유를 생각해보면 너무 당연하다. 좌변의 객체는 이미 기본 생성자를 통해 생성된 상태이기 때문에 우변의 생성자를 통해 생성된 객체의 멤버변수가 단순히 좌변 객체의 멤버변수로 복사될 뿐이다. (이에 대한 내용은 다음 복사 생성자 글에서 좀더 자세히 살펴보고자 한다.)
해당 동작을 확인해보기 위해 Simple
, SoSimple
클래스의 생성자를 다음과 같이 수정해보았다.
Simple() :num(0) {
cout << "기본 생성자" << endl;
cout << this << endl;
}
Simple(int num) :num(num) {
cout << "인수 1개 생성자" << endl;
cout << this << endl;
}
// ...
SoSimple(int n1, int n2, int n) :num2(n2) {
num1 = n1;
simple = Simple(n);
cout << &simple << endl;
}
실행결과
기본 생성자
0133F730
인수 1개 생성자
0133F730
0133F730
즉 객체의 대입연산 후에도 기본 생성자를 통해 생성된 객체의 주소에는 변함이 없는 것을 확인할 수 있다.
그러나 위 코드에서는 불필요한 객체가 1개 더 생성된다. 따라서 멤버변수로 선언된 객체를 생성자에서 초기화하려는 경우 다음 2가지 방법을 사용하는 것이 성능상 더 좋다.
Simple(int n1, int n2, int n) :num1(n1), num2(n2), simple(n) {}
Simple(int n1, int n2, int n) :num1(n1), num2(n2) {
simple.setNum(n);
}
1. 멤버 이니셜라이저를 통해 초기화
2. 기본 생성자를 통해 생성된 객체의 setter 함수로 객체의 멤버변수 직접 초기화
이때 생성자는 1번만 호출되며 호출되는 생성자의 종류는 각각 다르다.
위의 경우에서는 생성자에 일반 자료형(primitive type) 인자가 선언되었지만 다음과 같이 생성자의 매개변수로 객체가 전달된다면 복사 생성자가 호출될 수 있다. 다음 글에서는 복사 생성자의 개념과 호출 시점에 대해 알아보고자 한다.
Simple(int n1, int n2, Simple simple) :num1(n1), num2(n2), simple(simple) {}
끝.
'C++' 카테고리의 다른 글
[개념] C++의 형변환 연산자 (0) | 2022.07.02 |
---|---|
[개념] C++의 다형성 - virtual 함수 (0) | 2022.06.27 |
[개념] C++의 상속 3계명? (0) | 2022.06.24 |
[개념] C++의 복사 생성자 (0) | 2022.06.22 |
[개념] C/C++의 헷갈리는 const (0) | 2022.06.18 |
- Total
- Today
- Yesterday
- 디자인 패턴
- github
- Gitflow
- 서블릿 컨테이너
- Linux
- QueryDSL
- mockito
- rest api
- 템플릿 콜백 패턴
- Git
- C++
- facade 패턴
- 모두의 리눅스
- servlet filter
- 전략 패턴
- Java
- vscode
- Front Controller
- spring
- junit5
- spring boot
- ParameterizedTest
- FrontController
- Transaction
- JPA
- SSE
- Spring Security
- spring aop
- Assertions
- 단위 테스트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |