일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 쓰레드
- 한국산업기술대학교
- 옵저버 패턴
- 멀티코어 프로그래밍
- 멀티쓰레드
- C
- 옵저버
- c++
- multi-core
- 프레임워크
- material
- 유니크포인터
- 복사생성자
- observer pattern
- EFFECTIVE C++
- Atomic
- thread
- 메모리관리
- vector
- 스마트포인터
- stl
- Unreal
- MultiCore
- random access
- 디자인패턴
- sequential
- Design Pattern
- Multithread
- 멀티코어
- 게임공학과
- Today
- Total
태크놀로지
[Effective C++] 생성자와 소멸자, 제대로 메모리 관리를 하고 있는가? 본문
생성자 : 새로운 객체를 메모리에 만드는데 필요한 과정을 제어하고 객체의 초기화를 맡는다.
소멸자 : 객체를 없앰과 동시에 그 객체가 메모리에서 적절히 사라질 수 있도록 하는 과정을 제어한다.
대입연산자 : 기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수 : 다음포스팅에서 진행
1. C++이 자동으로 만들어서 호출해 버리는 함수들에 촉각을 세우자
클래스안에 직접 선언해 넣지 않으면 컴파일러가 스스로 선언해 주도록 되어 있는 함수들
class Empty{
public:
Empty() = default;
Empty(const Empty& rhs) = default;
~Empty() = default;
Empty& operator=(const Empty& rhs) = default;
}
암시적(Implicit) 으로 만들어 놓음 (모두 기본형이며 public이다.)
- 기본생성자
- 복사생성자
- 복사대입연산자
- 소멸자
1. 복사생성자 얕은복사로 인한 에러발생
template<class T>
class NameObject {
public:
NameObject(const char* name, const T& value) : nameValue(name), objectValue(value) {}
NameObject(const string name, const T& value) : nameValue(name), objectValue(value) {}
public:
string nameValue;
T objectValue;
};
int main()
{
NameObject<const char*> no1("Smallest Prime Number", "test");
NameObject<const char*> no2(no1); // 복사 생성자 호출
// 자동으로 생성되는 기본 복사생성자에서,
// 각 비트만 그대로 복사함 : 얕은복사가 일어남.
cout << no1.nameValue << endl;
cout << no2.nameValue << endl;
delete no1.objectValue;
cout << no2.objectValue << endl; // Error 발생
// 기본 복사생성자에서 얕은복사로 인해,
// no1.objectValue와 no2.objectValue는 같은 주소를 갖게됨
}
자동으로 생성되어 호출되는 기본 복사생성자 - 객체가 아닌 포인터를 갖고 있을때, 얕은복사로 인한 문제점발생
-> 자동으로 생성되는 기본 복사생성자는, 각 객체의 비트만 그대로 복사하기 때문에 얕은복사가 일어난다.
2. 자동으로 생성된 복사생성자에서 참조자 복사
template<class T>
class NameObject {
public:
NameObject(string& name, const T& value) : nameValue(name), objectValue(value) {}
public:
string& nameValue;
const T objectValue;
};
int main()
{
string newDog("Persephone");
string oldDog("Satch");
NameObject<int> d1(newDog, 2);
NameObject<int> d2(oldDog, 2);
d2 = d1; // Error : 참조자는 원래 자신이 참조하고 있는것과 다른 객체를 참조할 수 없음.
//또한 상수객체도 동일한 에러가 발생
}
참조자는 원래 자신이 참조하고 있는것과 다른 객체를 참조할 수 없음. 또한 상수객체도 동일한 에러가 발생
-> 따라서 이러한 경우 복사 대입연산자를 직접 정의해 주어야한다.
2. 컴파일러가 만들어낸 함수가 필요없으면, 확실히 이들의 사용을 막아놓자
복사가 일어나지 않는(사본이 존재하지 않는) 객체를 만들어보자.
class NonCopyable{
public:
NonCopyable() { cout << "Non 생성자 호출" << endl; };
~NonCopyable() { cout << "Non 소멸자 호출" << endl; };
protected:
// 선언만 함으로서 friend를 통한 호출도 막음
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};
class UniqueObj : private NonCopyable {
public:
UniqueObj() { cout << "Unique 생성자 호출" << endl; }
~UniqueObj() { cout << "Unique 소멸자 호출" << endl; }
};
int main()
{
// private 상속이기 때문에, NonCopyable을 기본베이스로 생성할 수 없음.
// 따라서 virtual 소멸자도 필요없음
// NonCopyable* a = new UniqueObj; // private 상속으로 인해 불가능
UniqueObj a;
UniqueObj b;
b = a; // compile error
}
1. 컴파일러가 자동으로 생성하는 함수는 모두 public이다. 따라서 private으로 사용하지 않는 함수들을 감춰놓자.
2. friend로 접근할 경우는? 선언만 하고 정의를 안 해 버린다.
UniqueObj 클래스 특징
상속은 public일 필요가 없음(private 상속)
- NonCopyable을 기본클래스로 지정하지 못하도록 함.
소멸자는 가상소멸자가 아니어도 됨
- private으로 NonCopyable을 기본클래스로 지정하지 못하도록 막아놓았기 때문에, 쓸데없이 가상 소멸자를 지정할 필요가 없다.
※ 기본클래스로 호출된 객체를 삭제했을경우에, 가상소멸자가 등록되어있지 않으면 파생클래스의 소멸자는 불리지 않는다.
UniqueObj를 생성했을시 생성자와 소멸자 결과
3. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상소멸자로 선언하자
소멸자 호출순서 : 파생클래스 소멸자 -> 기본클래스 쪽으로 거쳐 올라가며 소멸자 호출
- 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될때, 기본 클래스의 소멸자가 비가상으로 설정되어있다면 프로그램 동작은 미정의 사항이라고 되어있음.
class Base{
public: Base(); ~Base();
}
class Data : public Base;
Base* a = new Data;
delete a; // Base의 소멸자는 호출되지만, Data의 소멸자는 호출되지 않음. (Data클래스 메모리릭 발생)
- 무조건 소멸자를 가상으로 설정하는것 또한 좋지 않다.
virtual 소멸자를 지정하면, 가상함수테이블을 생성하기 때문에 객체 크기가 커짐.
class Point{
public:
Point(); ~Point();
private:
int x,y;
}
- int 2개 (64bit)
- 가상소멸자일경우: int 2개(64bit) + vtable(32bit) = 96bit
위와 같이 쓸데없이 가상소멸자를 설정할경우, 64비트 레지스터에 한번에 못들어감.
- 가상 소멸자를 설정하는 클래스에 대한 상황들
1. 모든 기본 클래스가 다형성으로 설정되어 있지 않음.
- string, STL 컨테이너 타입경우 모두 비가상 소멸자이다.
2. 기본 클래스로 쓰일 수 있지만, 다형성은 갖지 않도록 설계된 클래스
- 위와 같은 NonCopyable, 표준 라이브러리의 input_iterator_tag (기본 클래스의 인터페이스를 통한 파생 클래스 객체의 조작이 허용되지 않음. private 상속)
(1~3) 정리
- 컴파일러는 경우에 따라 클래스에 대해 기본생성자, 복사생성자, 복사대입연산자, 소멸자를 암시적으로 만들어 놓을 수 있다.(모두 default)
- 컴파일러에서 자동으로 제공하는 기능을 허용하지 않으려면, 사용하지 않는 멤버 함수를 private으로 선언한 후 구현은 하지 않은채로 둔다. (NonCopyable과 같은 경우로도 사용가능)
- 다형성을 가진 기본 클래스는 반드시 가상소멸자를 선언해야함. 즉 어떤 클래스가 가상 함수를 하나라도 갖고있으면, 이 클래스의 소멸자도 가상소멸자여야 함.
- 무조건 가상 소멸자를 선언하는건 좋지 않음. (상황에 따른, 다형성을 가진 기본클래스만 가상소멸자 선언)
4. 소멸자에서의 예외 발생을 개발자가 관리할 수 있도록 설정하자.
소멸자에서 예외처리를 하여 프로그램 안전성을 높이자.
- 상황에 따라 선택 1. 예외로그남기기 / 2. 프로그램종료
- 소멸자에서 예외가 발생하면 안된다. 최소한이라도 try catch를 통해 위와같은 상황으로 대비하자.
예제1 소멸자에서 예외발생
class DBConnection {
public:
DBConnection() = default;
DBConnection(const DBConnection& rhs) = default;
static DBConnection& create()
{
static DBConnection a;
return a;
}
bool close()
{
//...
return false;
}
};
// DBConnection을 관리하는 클래스
class DBConn {
public:
DBConn(const DBConnection _db) : db(_db) {
}
~DBConn()
{
db.close(); // 여기서 예외가 발생할경우?
// db는 닫히지 않은채로 객체가 소멸해버림.
// 해결방법 : 상황에 따라 선택지 선택
// 1. 프로그램을 끝내거나 / 2. 호출 실패 로그 남김
}
private:
DBConnection db;
};
위 코드의 위험성 : db는 닫히지 않은채로 객체가 소멸해버림.
해결방법 : 상황에 따라 선택지 선택
- 1. 프로그램을 끝내거나
- 2. 호출 실패 로그 남김
예제2 소멸자에서 예외발생
class DBConn {
public:
DBConn(const DBConnection _db) : db(_db), closed(false) {
}
~DBConn()
{
// 실패했을때를 대비해 예외처리 추가
if(!closed)
try
{
closed = db.close(); // 사용자가 연결을 안닫았으면 여기서 닫기 시도
if (!closed) throw closed;
}
catch (const bool& e)
{
// 1. exit
//exit(true);
// 2. close log 출력
cout << "Exception Log : DB Is not closed" << endl;
}
}
// 사용자 편의 함수
void close()
{
db.close();
closed = true;
}
private:
DBConnection db;
bool closed;
};
DBConn 소멸자에서 실패했을때를 대비해 예외처리 추가
5. 객체 생성 및 소멸 과정중에 절대로 가상함수를 호출하지말자.
- 생성자, 소멸자에서 가상함수 호출 금지
호출과정에서 기본클래스의 생성자 실행도중, 파생클래스가 아직 초기화 되어있지 않기때문에 가상함수를 알수없다.
소멸자 또한 파생클래스 소멸 -> 기본클래스 소멸과정에서 파생클래스가 소멸되었기때문에 정보를 알수없다.
생성자 호출순서 : 기본클래스 -> 파생클래스
소멸자 호출순서 : 파생클래스 -> 기본클래스
* 파생클래스 객체의 기본클래스 부분이 생성되는 동안은, 그 객체의 타입은 기본클래스이다.
대처방법
class Transaction {
public:
explicit Transaction(const string& logInfo) {
logTransaction(logInfo);
}
virtual ~Transaction() = default;
/*virtual*/ void logTransaction(const string& logInfo) const{
cout << "Log : " << logInfo << endl;
}
};
class BuyTransaction : public Transaction {
private:
// 미초기화된 데이터 멤버
int param1;
int param2;
int param3;
public:
// 필요한 초기화 정보를 파생클래스 쪽에서 기본클래스 생성자로 올려주도록 만듬
BuyTransaction(const string& param) : Transaction(createLogString(param)) {}
private:
// 중요! 정적함수인 이유
// 정적함수이기 때문에 생성이 끝나지도 않은
// BuyTransaction의 미초기화된 데이터 멤버를 실수로 건드릴 위험도 없다.
static string createLogString(const string& param) {
return param;
}
};
1. 생성자에서 호출되는 logTransaction을 비가상 멤버함수로 변경
2. 파생클래스 생성자들로 하여금 로그정보를 기본생성자에게 전달한다.
※ 중요! createLogString가 정적함수인 이유
정적함수이기 때문에 생성이 끝나지도 않은 BuyTransaction의 미초기화된 데이터 멤버를 실수로 건드릴 위험도 없다.
(4-5) 정리
- 소멸자에서 예외가 발생하면 안된다. 예외처리로 안정성 업!
- 생성자 혹은 소멸자안에서 가상함수를 호출하면 안된다. 실행중인 생성/소멸자에서 파생클래스는 아직 생성되지 않음.
스터디 진행후 토론내용 정리
1. 기본클래스 함수들은 호출한것만 생성한다.
en.wikipedia.org/wiki/Special_member_functions
2. 기본생성자를 정의하지않고, 파라미터값이 있는 생성자만 정의했을때 operator()를 사용하지 않으면 컴파일에러발생
class TEST {
public: TEST(int a);
}
TEST a; // compile error : operator()를 사용하여 기본생성자 호출
출처
'C++' 카테고리의 다른 글
[Effective C++] 객체 기반 방식의 자원관리 (0) | 2020.12.02 |
---|---|
[Effective C++] 대입연산자, 제대로 메모리 관리를 하고 있는가? (0) | 2020.11.24 |
[Effective C++] 객체 생성시 초기화 방법 (0) | 2020.11.18 |
[Effective C++] 왜 const를 사용해야하는가 (0) | 2020.11.18 |
[C++/STL] Ranking System 분석 (0) | 2020.09.25 |