티스토리 뷰
해당 글은 '윤성우의 열혈 C++ 프로그래밍'을 참고하여 작성함. (이하 해당 도서라고 지칭)
해당 도서의 상속파트에서 저자가 강조한 부분들 중 내가 느끼기에 중요한 규칙 3가지를 정리해보려고 한다. 이 3가지는 다음과 같다.
1. 접근제한의 기준은 객체가 아닌 클래스다.
2. 클래스의 멤버는 해당 클래스의 생성자에서 초기화한다.
3. 생성자에서 동적할당한 메모리 공간은 소멸자에서 해제한다.
본격적인 내용에 앞서 용어 정리를 해보려고 한다. 보통 클래스간 상속관계에서 하위 클래스에게 상속하는 클래스를 부모 클래스, 상위 클래스에게 상속 받는 클래스를 자식 클래스라고 한다. C++에서는 보통 부모 클래스를 기초 클래스, 자식 클래스를 유도 클래스라고 한다고 하니 앞으로 해당 용어를 사용할 것이다.
접근제한의 기준은 객체가 아닌 클래스다.
#include <iostream>
using namespace std;
class Basic {
private:
int basicNum;
public:
Basic() :basicNum(0) {}
Basic(int basicNum) :basicNum(basicNum) {}
void print() {
cout << basicNum << ", ";
}
};
class Derived : public Basic {
private:
int derivedNum;
public:
Derived() {}
Derived(int derivedNum) :derivedNum(derivedNum) {}
Derived(int basicNum, int derivedNum) :Basic(basicNum), derivedNum(derivedNum) {}
void print() {
//cout << basicNum << ", "; //오류발생!
Basic::print();
cout << derivedNum << endl;
}
};
int main() {
Derived d1(10);
Derived d2(20, 30);
d1.print(); //0, 10
cout << endl;
d2.print(); //20, 30
return 0;
}
위 경우에서 Derived
클래스의 오버라이딩된 print
함수 내에서 Basic
클래스의 private 멤버변수인 basicNum
에 직접 접근하게 되면 컴파일 오류가 발생한다.
Derived
클래스의 객체에는 basicNum
멤버변수가 포함되어 있어 아무리 private으로 선언되어 있다고 하더라고 Derived
의 멤버함수 내에서 직접 접근이 가능할 것이라고 생각할 수 있다. 하지만 접근제한의 기준은 객체가 아닌 클래스이다. 즉 basicNum
멤버변수가 Derived
클래스 객체에 포함되어 있다고 하더라도 Derived
클래스가 아닌 Basic
클래스 내부에서 private으로 선언되었기 때문에, Basic
클래스 내부에서만 직접 접근이 가능하고 Basic
클래스 외부에서는 Basic
클래스의 public 멤버함수를 통해 간접적으로만 접근이 가능한 것이다.
다만 만약 basicNum
멤버변수가 protected(자기 자신, 유도 클래스 내에서만 접근 가능), public(모두 접근 가능)의 접근제한자로 선언되어있다면 Derived
클래스 내에서도 직접 접근이 가능해진다.
클래스의 멤버는 해당 클래스의 생성자에서 초기화한다.
위 코드에서 d1
의 객체 생성 시, derivedNum
을 인자로 전달하는 생성자를 호출하였다. 해당 생성자에서는 basicNum
을 별도로 초기화하는 코드가 보이지 않는데 실제 print
함수의 출력 결과를 보면 basicNum
이 0으로 초기화된 것을 볼 수 있다. 어떻게 된 일인지 살펴보자.
해당 도서의 저자는 객체의 생성과정을 다음 세단계로 나누어 설명하고 있다. 이를 기반으로 객체 생성 시나리오를 써보려 한다.
1단계: 메모리 공간의 할당
2단계: 이니셜라이저를 이용한 멤버변수(객체)의 초기화
3단계: 생성자의 몸체부분 실행
d1
객체를 생성하기 위해 derivedNum
을 인자로 받는 생성자를 호출한다. 호출된 Derived
생성자를 실행하기에 앞서 상속받은 멤버변수 basicNum
을 초기화하기 위해 basicNum
이 멤버변수로 선언되어 있는 Basic
클래스의 생성자를 호출한다. 이 경우 Derived
생성자에서 특별히 호출할 Basic
생성자를 지정하지 않았으므로 기본 생성자를 호출한다. 이후 basicNum
변수를위한 메모리 공간을 할당하고 이니셜라이저를 통해 basicNum
을 0으로 초기한 후 Basic
생성자의 몸체부분을 실행한다.
이후 Derived
생성자에서 자신의 멤버변수 derivedNum
를 위한 메모리 공간을 할당하고 이니셜라이저를 통해 인자로 넘겨준 값으로 해당 변수를 초기화한다. 이렇게 멤버변수의 초기화를 모두 마쳤으므로 Derived
생성자의 몸체부분을 실행하게 된다. 반면 d2
객체를 생성하기 위한 생성자에서는 basicNum
을 인자로 받는 별도의 Basic
생성자 호출을 지정하였으므로 Basic
의 기본생성자는 호출되지 않는다. 이를 통해 basicNum
(기초 클래스의 멤버변수)와 derivedNum
(유도 클래스의 멤버변수)가 초기화되는 시점이 서로 다르다는 것을 알 수 있다.
정리하자면 클래스의 멤버(변수)는 클래스의 생성자에서 초기화되므로, 유도 클래스의 생성자에서 기초 클래스의 멤버변수를 초기화하기 위한 별도의 기초 클래스 생성자 호출을 지정해주지 않으면 기초 클래스의 기본 생성자가 호출되는 것이다. 만약 이때(기초 클래스의 생성자 별도로 지정하지 않은 경우) 기초 클래스의 기본 생성자가 정의되어있지 않으면 당연히 컴파일 에러가 난다. (오류 메시지 'constructor for 'Derived' must explicitly initialize the base class 'Basic' which does not have a default constructor')
생성자에서 동적할당한 메모리 공간은 소멸자에서 해제한다.
#include <iostream>
#include <cstring>
using namespace std;
class Basic {
private:
char* basicName;
public:
Basic(char* basicName) {
this->basicName = new char[strlen(basicName) + 1];
strcpy(this->basicName, basicName);
}
~Basic() {
delete []basicName;
cout << "~Basic" << endl;
}
void print() {
cout << basicName << ", ";
}
};
class Derived : public Basic {
private:
char* derivedName;
public:
Derived(char* basicName, char* derivedName) :Basic(basicName) {
this->derivedName = new char[strlen(derivedName) + 1];
strcpy(this->derivedName, derivedName);
}
~Derived() {
delete []derivedName;
cout << "~Derived" << endl;
}
void print() {
Basic::print();
cout << derivedName << endl;
}
};
int main() {
Derived d("hello", "world");
d.print();
return 0;
}
위에서는 딱히 짚고 넘어가지 않았지만 클래스 간 상속관계가 존재할 때, 유도 클래스 생성자 호출 시 기초 클래스의 생성자가 호출되며 이후 기초 클래스의 멤버변수를 초기화하고 기초 클래스 생성자의 몸체 부분을 모두 실행한다. 이후 다시 유도 클래스의 멤버변수를 초기화하고 유도 클래스 생성자의 몸체 부분을 실행하게 된다. 반면 소멸자 호출은 반대로 유도 클래스 → 기초 클래스의 순서로 진행된다. 위 코드의 실행결과를 통해 이를 확인할 수 있다.
실행결과
Basic(char* basicName)
Derived(char* basicName, char* derivedName)
hello, world
~Derived
~Basic
각 클래스의 소멸자에서는 생성자에서 할당한 메모리 공간(Basic
클래스 - basicName
, Derived
클래스 - derivedName
)을 해제하고 있는데, 확인해본 결과 유도 클래스의 소멸자에서 기초 클래스의 생성자에서 할당한 메모리 공간 해제도 가능하긴 하다. 다만 기초 클래스에서 할당한 메모리 공간을 가리키는 멤버변수가 private이면 유도 클래스의 소멸자에서 해당 변수 접근 시 컴파일 오류가 난다. 또한 유도 클래스의 소멸자에서 할당 해제한 공간을 다시 기초 클래스의 소멸자에서 할당 해제하면 'double free detected'와 같은 런타임 오류가 발생하게 된다.
결론적으로 클래스간 정보은닉을 위해 멤버변수를 private으로 선언하는 경우가 많고 중복 메모리 해제 오류를 방지하기 위해, 'A 클래스의 생성자에서 할당한 메모리 공간은 A 클래스의 소멸자에서 할당 해제'하는 것이 좋다.
다음 글에서는 상속의 다형성과 밀접한 개념인 가상함수(virtual
)에 대해 다룰 것이다.
끝.
'C++' 카테고리의 다른 글
[개념] C++의 형변환 연산자 (0) | 2022.07.02 |
---|---|
[개념] C++의 다형성 - virtual 함수 (0) | 2022.06.27 |
[개념] C++의 복사 생성자 (0) | 2022.06.22 |
[개념] C++의 멤버 이니셜라이저 (0) | 2022.06.20 |
[개념] C/C++의 헷갈리는 const (0) | 2022.06.18 |
- Total
- Today
- Yesterday
- 디자인 패턴
- spring aop
- spring
- mockito
- SSE
- Git
- 서블릿 컨테이너
- servlet filter
- junit5
- Transaction
- JPA
- QueryDSL
- 단위 테스트
- Gitflow
- C++
- facade 패턴
- ParameterizedTest
- github
- Front Controller
- rest api
- 전략 패턴
- Java
- Assertions
- Linux
- 모두의 리눅스
- Spring Security
- vscode
- spring boot
- 템플릿 콜백 패턴
- FrontController
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |