다음과 같이 객체를 throw할 수 있는데, nested block 즉, try{ }가 종료가 되었으므로
Animal a를 통해 만들어진 객체 a는 소멸자에 의해 소멸하게 된다.
Q. 그렇다면 왜 출력문에서 catch문 뒤에 소멸자가 한 번 더 출력된 것일까?
A. 그건 바로 catch(Animal& a)에서 생성자가 한 번 더 생성되었다는 것의 반증이 될 것이다.
생성자는 처음 객체생성되었을 때, 초기화를 위해 딱 한번만 호출된다는 것이 기본 개념이고, 핵심이기 때문이다.
따라서 catch 블록이 종료된 이후 Animal& a를 통해 생성된 객체를 다시 소멸자가 소멸해줘서 소멸자가 두번 출력된 것!
※ Exception 클래스 (예외 클래스)
Exception 클래스: 예외처리를 위해 exception 헤더에서 제공하는 클래스 (표준 라이브러리인 std namespace에 존재)
이런 예외클래스도 클래스이기에 상속은 물론 다형성, 생성자와 연산자에도 적용 가능하다.
§ what()
what은 exception 클래스에서 하나의 문자열 포인터를 반환하는 가상멤버함수이다.
what()은 exception 클래스에서는 아무 의미가 없지만 파생클래스에서 원하는 문자열을 출력할 수 있게 재정의 해준다!
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class NewException : public exception{ //새로운 Exception NewException은 exception클래스를 상속받음
public:
const char* what() const noexcept override{ // what 함수의 오버라이딩 진행
return "NewException";
}
};
int main(){
try{
string str;
str.resize(-100);
}
catch (exception& e){
try {
throw NewException(); // 예외 발생시 새로운 Exception throw
}
catch (const NewException& newException){
cout << "My exception is " << newException.what() << endl; // NewException의 what()에서 전달받은 문자열 출력
}
}
}
§ 표준 Exception 클래스
#include <exception>는 exception 클래스로부터 파생된 다양한 표준 exception 클래스를 정의하고 있다.
[가장 기초클래스가 되는 2개의 클래스, logic과 runtime]
logic_error 클래스는 일반적인 논리에 관한 오류들을 처리
runtime_error 클래스는 프로그램실행하는 동안 발생할 수 있는 다양한 오류들을 처리
- C++에서 template은 다른 객체지향언어에서 부르는 "일반화를 뜻하는"제네릭(generic)이다.
- template 사용은 약간의 비용(실행비용)이 발생하지만 전체적으로 프로그램의 크기와 난이도를 줄일 수 있다
예를들어 int, double, char 타입에 대한 최솟값을 구하는 min 함수를 만들어 보자면 아래와 같이 3개의 함수가 필요하다.
int min (int a, int b) { return a < b ? a : b; }
double min (double a, double b) { return a < b ? a : b; }
char min (char a, char b) { return a < b ? a : b; }
하지만 template을 사용한다면 모든 data type에 대한 정의가 하나의 함수로 가능하다. 아래처럼 말이다.
template<typename T>
T min (T a, T b) { return a < b ? a : b; }
cf. 위의 T는 템플릿 매개변수라 부른다.
※ 템플릿 함수와 함수 템플릿
※ 템플릿 함수
-템플릿을 기반으로컴파일러가 만들어 내는 함수(템플릿 기반의 함수임을 표기한 것)
- 즉,템플릿 기반의 호출이 가능한“함수”라는 점에서 차이가 있다.
※ 함수 템플릿
- 함수 템플릿은 함수를 만들어 낸다.(기능은 결정되어 있으나 자료형은 결정 X)
- 따라서 함수 템플릿으로 다양한 자료형의 함수를 만들 수 있다.
- 함수를 만드는데 사용되는 템플릿으로 호출이 가능한 함수가 아닌 “템플릿”이다
※ 템플릿의 종류
- 함수 템플릿: 함수의 오버로딩을 확장한 개념
template<typename T>
T min (T a, T b) { return a < b ? a : b; }
- 클래스 템플릿:클래스를 만드는 템플릿으로 내부의 멤버변수.함수의 data type 지정시 사용
- 템플릿 클래스: 클래스 템플릿을 기반으로 컴파일러가 만든 클래스이다. 다만, 클래스 템플릿 기반 객체생성에는 반드시 <int>와 같은 자료형을 명시해 줘야 한다!
template <typename T>
class Animal{
private:
T num;
public:
Animal(T num) : num(num) {}
T eat(const T& ref);
};
template <typename T>
T Animal<T>::eat(const T& ref) {}
int main() {
Animal<int> a(4);
a.eat(4);
}
- 타입 템플릿: using으로 data type을 템플릿으로 지정시 사용
template <typename T>
using ptr = T*;
ptr<int> x; // int *x와 동일한 선언
- 변수 템플릿: 변수에 적용할 수 있는 템플릿 (C++14 이후부터 적용)
template <typename T>
constexpr T PI = T(3.141592653589793238462643L);
※ 템플릿의 특수화
- 특정 자료형으로 생성된 객체에 대해 구분이 되는 다른 행동양식 적용하기 위함
- 즉, 템플릿을 구성하는 멤버함수 일부 혹은 전체를 모두 다르게 행동시킬 수 있음
Q. 문자열 비교할 때, 사전순이 맞을까? 아니면 길이순이 맞을까?
A: 이런 특수 상황에 따라 예외가 필요하기에 사용하는 것이 바로 템플릿의 특수화이다.
※ 함수 템플릿과 static 지역변수
함수템플릿을 기반, 컴파일러는 ‘템플릿 함수’들을 만들어 낸다.
따라서 static 지역변수도 템플릿 함수 별로 각각 존재하게 된다.
※ 클래스 템플릿과 static 멤버변수
static멤버변수는 변수가 선언된 클래스의 객체간 공유가 가능.
따라서 클래스별 static 멤버변수를 유지하게 된다.
좌) 함수 템플릿과 static 지역변수 우) 클래스 템플릿과 static 멤버변수
※ 템플릿기반 템플릿 클래스 객체를 저장하는 객체 생성법
※ Advanced Template
python에서 다음과 같이 print함수를 사용할 때, 인자로 전달되는 것들을 모두 출력할 수 있다.
print(1, 3.1, "abc")
그렇다면 C++에서도 이와 같은 방법으로 출력에서 인자에 대한 사전정보가 없을 때, 출력을 조금 더 유동적으로 하는 방법은 없을까?
위에서 사용한 recursion진행 대신 사용하는 방법으로 variadic template에서 가변인자 처리를 위해 parameter pack을 사용하는데, 이때! C++17부터 fold expression이 제공되어 parameter pack을 더 쉽게 사용할 수 있게 된 것이다.
- 인자가 1개:
1) Unary right
(E op ...) ex_ (E1 op (... op (EN-1 op EN)))
2) Unary left
(... op E) ex_ (((E1 op E2) op ...) op EN)
unary right fold
E1 op ( ... op ( En-1 op En))
- 전달받은 parameter pack을 위와 같은 표현식으로 변경 - 뒤에 있는 인자 부터 먼저 순차적으로 연산해서 결과 값을 생성
이를 위해서((AnotherClass*)(ptr.get()))와 같이 강제로 포인터를 얻어 캐스팅을 해줄 수 있지만 전혀 C++ 답지 못하다.
따라서 static_pointer_cast / dynamic_pointer_cast / const_pointer_cast 가 추가되었다.
이를 통해 안전하고도 편한 스마트 포인터 캐스팅이 가능해진 것이다.
vector<shared_ptr<MediaAsset>> assets;
assets.push_back(shared_ptr<Song>(new Song(L"Himesh Reshammiya", L"Tera Surroor")));
assets.push_back(shared_ptr<Song>(new Song(L"Penaz Masani", L"Tu Dil De De")));
assets.push_back(shared_ptr<Photo>(new Photo(L"2011-04-06", L"Redmond, WA", L"Soccer field at Microsoft.")));
vector<shared_ptr<MediaAsset>> photos;
copy_if(assets.begin(), assets.end(), back_inserter(photos), [] (shared_ptr<MediaAsset> p) -> bool {
// Use dynamic_pointer_cast to test whether
// element is a shared_ptr<Photo>.
shared_ptr<Photo> temp = dynamic_pointer_cast<Photo>(p);
return temp.get() != nullptr;
});
for (const auto& p : photos) {
// We know that the photos vector contains only
// shared_ptr<Photo> objects, so use static_cast.
wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl;
}
변수의 type을 강제로 다른 type으로 변경하는 것으로 자료형간 || 포인터간 형변환시 사용된다.
C/C++은 변수의 type을 변경해 처리해야 하는 경우가 빈번하게 발생한다.
Q. 외부 library 사용시, 인자로 넘겨야할 변수가 char인데 외부의 library가 unsigned char를 사용한다면?
A: 개발자는 unsigned char로 변경해서 넘겨주어야 컴파일 에러가 발생하지 않는다.
int print(unsigned char *str){
cout << str << endl;
}
int main() {
char str[20] = "Hello, world!";
print(str); // type casting이 필요 (Err)
print(reinterpret_cast<unsigned char*>(str));
}
※ Type Casting의 종류
캐스트는 크게 2가지로 나눌 수 있다.
- 묵시적 캐스트 (implicit cast): 캐스트 연산자를 사용하지 않고 형변환이 이뤄지는 경우
- 명시적 캐스트 (explicit cast): 캐스트 연산자를 사용하여 형변환이 이뤄지는 경우
※ static_cast
[사용시기]: 논리적으로 변경가능한 경우
- static_cast의 특성은 묵시적 캐스트와 비슷하다 보면 된다.
- 묵시적 캐스트는 컴파일 시점에서 '무결성'을 검사하는데 이때, '허용'과 '컴파일러에 의한 값 변환' 두 관점이 있다.
- static_cast는 형변환에 의한 타입 확인을 compile 시간에 정적으로 수행한다.
§ 명시적 형변환 §
float f;
int a = static_cast<int>(f);
char *str = static_cast<char*>(f); // Err!
※ const_cast
[사용시기]: 포인터, 참조형에서만 사용가능const 및 volatile 제거할 때 사용된다!
- const_cast는 상수성이나 volatile(최적화 제외 변수)의 속성을 제거할 때 사용한다.
§ 명시적 형변환 §
int x = 10;
const int *pt_const_x = new int(10);
int *ptx;
ptx = const_cast<int*>(pt_const_x);
*ptx = 20; // 20에서 10으로 값 변경
const int &rt_const_x = x;
int& rtx = const_cast<int&>(rt_const_x);
※ reinterpret_cast
[사용시기]: 명시적 변환과 동작이 동일해 대신 사용된다. 단, const 사용 변환대상은 불가!
- 어떤 포인터 타입도 어떤 포인터 타입으로든 변환이 가능!
- [정수 -> 포인터] 타입도, [포인터 -> 정수]타입으로도 가능하다.
- 강력한 casting 같지만 특수 케이스가 아닌이상 사용을 잘하지 않는 것을 추천 (포인터가 강제 형변환되서)
§ 명시적 형변환 §
int *ptr = new int(10);
char *str;
str = reinterpret_cast<char*>(ptr);
*str = 20; // 10 -> 20으로 값변경
§ const 지정자 사용시, 명시적 형변환 §
const int *ptr = new int(10);
char *str;
str = reinterpret_cast<char*>(const_cast <int*> (ptr));
*str = 20; // 10에서 20으로 값변경
※ dynamic_cast
[사용시기]: class의 포인터, 참조변수간 형변환 시 안전하게 down casting을 위해 사용.
Runtime conversions로 RTTI(Requires Runtime Type Information)
단, parent에 virtual 함수가 존재해야 정상작동!
- run time에 동적으로 상속계층관계를 가로지르거나 down casting시 사용됨
- 기본클래스에 virtual 멤버함수가 하나도 없다면, 다형성을 갖는게 아님(단형성)
∴따라서 dynamic_cast는 다형성을 띄지 않은 객체간 변환은 불가능!
§ 명시적 형변환 §
#include <iostream>
using namespace std;
class Blog {
public:
Blog() { cout << "Blog()\n"; };
virtual ~Blog() { cout << "~Blog()\n"; };
void Show() { cout << "This is Blog Class\n"; }
};
class Tistory : public Blog {
public:
Tistory() { cout << "Tistory()\n"; };
virtual ~Tistory() { cout << "~Tistory()\n"; };
void Show() { cout << "This is Tistory Class\n"; }
};
int main(void) {
Blog* pBlog = new Blog();
pBlog->Show();
Tistory* pTistory = dynamic_cast<Tistory*>(pBlog);
if (pTistory == nullptr) { //티스토리 클래스의 포인터가 nullptr이 나올떄.
cout << "Runtime Error\n";
}
else {
pTistory->Show();
}
delete pBlog;
system("pause");
}
※ static_cast VS dynamic_cast
[static_cast]: 정적으로 형변환을 해도 아무 문제가 없다 (= 이미 어떤 녀석인지 알고 있다는 뜻), Fast
[dynamic_cast]: 동적으로 형변환을 시도 해본다는 뜻 (= 이녀석의 타입을 반드시 알아봐야 한다는 뜻), Slow (RTTI)
따라서 dynamic_cast를 이용해 Runtype의 해당 타입을 명확히 알아봐야 하고 (RTTI, Requires Runtime Type Info)
그렇지 않은 경우, static_cast를 이용해 변환 비용을 줄이는 것이 좋다. (동적타입체크를 안해도 되서)
// 비행기에 여러 직군의 사람들이 탑승했다.
// 한 승객이 갑자기 급성 맹장염에 걸려 의사가 급하게 수술을 해야 한다.
class Passenger {...};
class Student : public Passenger{
...
void Study();
};
class Teacher : public Passenger{
...
void Teach();
};
class Doctor : public Passenger{
...
void Treat();
void Operate();
};
int main() {
typedef vector<Passenger *> PassengerVector;
PassengerVector passengerVect;
Passenger* pPS = new Student();
if (pPS){
passengerVect.push_back( pPS );
// 비행기 타자마자 공부한다고 치고~
// pPS가 명확하게 어느 클래스의 인스턴스인지 알고 있다.
// 이 경우엔 굳이 비용이 들어가는 dynamic_cast가 아닌, static_cast를 쓰는게 낫다.
Student* pS = static_cast<Student *>( pPS );
pS->Study();
}
Passenger* pPT = new Teacher();
if ( pPT ){
passengerVect.push_back( pPT );
}
// Doctor 역시 비슷하게 추가.
...
// 응급 환자 발생. passengerVect 중 의사가 있다면 수술을 시켜야 한다.
PassengerVect::iterator bIter(passengerVect.begin());
PassengerVect::iterator eIter(passengerVect.end());
for( ; bIter != eIter; ++bIter ) {
// Passenger 포인터로 저장된 녀석들 중 누가 의사인지 구분해야 한다.
// 런타임 다형성 체크에 의해 Doctor가 아닌 녀석들에 대한 형변환 결과는 NULL
Doctor* pD = dynamic_cast<Doctor *>(*bIter);
if (pD){
pD->Operate();
}
}
}
위 예제는 static_cast와 dynamic_cast를 구분해서 언제 쓰는게 좋은지 알 수 있는 예제이므로 잘 분석해보자.
Q. 만약, 위 코드의 전체 승객 중 의사를 찾아내는 과정에서 dynamic_cast가 아니라, static_cast를 사용하였다면 어떻게 될까?
static_cast는 동적 타입체크를 하지 않고, Student와 Teacher는 Person의 파생 클래스이므로
변환 연산 규정에도 위배되지 않으므로, 그냥 타입 변환이 일어난다.
하지만, 변환 결과는 애초 기대했던 바와 전혀 다릅니다. 실제 Student 클래스 타입이지만,
Doctor 클래스 타입으로 타입 변환이 되면서Doctor 클래스 고유 멤버 함수에 대한 접근이 불가능해진다.
포인터가 가리키는 메모리 내용을 Doctor 클래스에 맞춰서 해석하기에
Student의 내용 중 일부가 Doctor 멤버 필드에 엉뚱하게 들어가거나, 슬라이스 문제가 발생할 수 있다.
다시 말해, 껍데기만 Doctor 클래스이지 내용은 전혀 Doctor의 것이 아니게 되는데,
멤버 필드에 접근시 엉뚱한 값이 들어가 있거나, 런타임 Err가 발생할 수 있다.
- 하나의 이름으로 오버로딩 된 수만큼 다양한 기능을 제공해 주기에 연산자 오버로딩을 통해 기존 연산자 외의 다른 기능을 추가할 수 있다.
- 연산자 오버로딩에는 2가지 방법이 있다.
1. 멤버함수에 의한 방법
2. 전역함수에 의한 방법
1. pos1.operator+(pos2); // by 멤버함수
2. operator+(pos1, pos2); // by 전역함수
ex)
const Animal& a를 인자로 받지만 사실 내부적으로는 오른쪽처럼 this도 함께 인자로 받는다는 사실!
단, this는 컴파일러가 가정하기 때문에 직접 명시하지 않는다.
아래의 경우에는 operator를 멤버함수로 구현할 경우 operator의 인자는 2가지이다.
1. 직접 명시된 인자
2. 현재의 object(this) //컴파일러가 가정해서 직접 명시하지 않음
따라서 아래의 경우에도 내부적으로는 Animal& operator+(this, const Animal& a) {...} 가 된다.
위와 아래의 예제에서 출력의 차이가 발생하는 이유는 다음과 같다.
위의 예제의 경우, string이라는 변수에 this->name과 a.name이 합성된 값을 저장하고,
newName을 return하므로 dog.name즉, this의 값은 원래 값이 유지되는 것이다.
아래 예제의 경우, name+=a.name은 this의 name값을 인자 a의 a.name과 합성한 것이므로 dog객체의 this->name값이 변조되는 것이다.
cf. 멤버함수 기반으로만! overloading이 가능한 연산자
= 대입연산자
() 함수 호출 연산자
[] 배열 접근 연산자(Index 연산자)
-> 멤버 접근을 위한 포인터 연산자
이들은 객체를 대상으로 진행해야 의미가 있는 연산자 => 멤버함수 기반으로만 연산자 오버로딩을 허용한다.
- 구조
Point operator= (const Point& ref){
Point pos( . . . );
return pos;
}
- early binding (정적 바인딩): 컴파일시간에 바인딩 되는 것 (초기화 리스트, 일반적 변수)
- late binding (동적 바인딩): 실행시간(run time)에 바인딩 되는 것 => virtual 선언된 모든 것!
간단히 말하자면 변수의 데이터 형이 결정되는 시점에 따라 나뉜다.
조금 더 자세히 말하자면 다음과 같다.
정적 바인딩: 컴파일시 관련 라이브러리, 객체와 링크해 실행 모듈을 만드는 것
동적 바인딩: 컴파일시 정보를 갖고 있지만 실제 실행시간에 해당 객체와 링크하여 실행 모듈을 만드는 것.
정적 바인딩을 사용하면 컴파일 시 데이터 형이 정해지므로실행에 효율적이고,
동적 바인딩을 사용하면 실행 시 데이터 형이 변경되므로 적응력이 뛰어난 프로그램을 제작할 수 있다.
※ 동적 바인딩의 발생
동적 바인딩의 발생은 다음과 같다
- 기본/파생클래스 내 멤버함수가 가상함수 호출
- 외부함수에서 기본클래스의 포인터로 가상함수 호출
- 다른 클래스에서 가상함수 호출
이렇게 가상함수가 호출되면 실행 중 객체 내에 오버라이딩(overriding)된 가상함수를 동적으로 찾아 호출한다.
§ 오버라이딩한 함수를 직접 호출하는 동적바인딩 예시코드 (객체지향 프로그래밍을 위한 C++의 이해)
컴퓨터 입장에서p와c모두Parent를 가리키는 포인터들이므로, 당연히 아래처럼 호출했을 때
p->paint();
c->paint();
모두 Parent 의 print() 가 호출되어야 하겠지만, 실제로는 print() 가 가상함수므로,
"실제로 p 와 c 가 가리키는 객체의 print()",
즉 p->f() 는 Parent 의 print()를, c->f() 는 Child 의 print()가 호출된다.
이와 같은 일이 가능한 이유는 print()를 가상함수로 만들었기 때문입니다.
※ virtual function (가상함수)
- 가상함수:기본 클래스 내의 함수를 파생클래스에서 재정의 (같은 이름의 함수를 overriding)하고자 할 때 사용.
- 가상함수가 포인터에 의해 호출된 경우, 포인터가 가리키는 객체의 클래스에 따라 컴파일러가 결정해 호출한다.
- 따라서 기본클래스 내의 멤버함수 앞에 virtual을 통해 overriding이 가능하다.
이런 가상함수가 나오게 된 계기는 다음과 같은 의문 때문이다.
Q. 포인터변수 자료형에 따라 호출되는 함수가 달라지는 것에 문제가 있지 않을까?
A. virtual을 이용하는 가상함수는 overriding하는 함수도 가상함수가 되어버린다.
즉, 가상함수로 선언되면 해당 함수호출시 포인터의 자료형 기반 호출이 아닌 포인터변수가 실제 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.
※ 순수가상함수
- 함수의 몸체가 정의되지 않은, 구현이 없는 가상 함수로 구현 대신, 가상함수에 0(NULL)값을 대입하면 된다.
- 가상함수: 상속관계(기본/파생클래스)에서 오버라이딩을 할 수 있는 함수
- 순수가상함수: 파생클래스에서 구현되어야하는 함수, 인터페이스로 이 클래스를 사용하겠다는 의미
class MiddleDerived1 : virtual public Base { . . . };
class MiddleDerived2 : virtual public Base { . . . };
class LastDerived : public MiddleDerived1, public MiddleDerived2 { . . . };
※ 다형성 (polymorphism)
- 객체지향의 매우 중요한 요소로 동질이상(同質異像)을 의미. (모습은 같은데 형태가 다름)
즉, 문장은 같은데 결과는 다르다는 뜻이다.
ex) virtual로 선언된 같은 이름, 다른 클래스의 함수를 동일한 포인터 변수로 받아도 다른 결과값이 나올 수 있다.
- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.
★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.
※깊은복사
- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법
- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.
위의 코드에서 아래 코드를 추가해주면 된다.
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장
즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.
§ 또 다른 예제
DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
// 자신만의 동적데이터공간 할당
mArray = new int[mSize];
// 다른 객체로부터의 데이터 복사
for (int i = 0; i < mSize; i++) {
mArray[i] = other.mArray[i];
}
}
※ 복사생성자 호출 시점:객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.
1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때. [Person p2 = p1;]
2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때
3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.
- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)
2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시
출처) 윤성우의 열혈C++프로그래밍
3. 객체를 반환하지만 참조형으로 반환하지 않는경우
출처) 윤성우의 열혈C++ 프로그래밍
cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!
Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?
A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.
예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.
{
Person p1(5);
cout << "부모 객체 p1 생성" << endl;
}
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;
만약 실행되었다면 다음과 같이 출력이될 것이다.
부모의 생성자
부모 객체 p1 생성
부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자
하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.
이해를 위해 또 다른 예시를 들어 보겠다.
{
Person p1(5);
}
cout << "부모 객체 p1 생성" << endl;
이렇게 된다면 출력은 어떻게 될까?
이는 아래와 같다.
부모의 생성자
부모의 소멸자
부모 객체 p1 생성
즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.
cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.
※ 임시객체를 이용한 obj.func().func(); 연산
※ 반환시 만들어진 (임시)객체의 소멸
1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.
2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
또 다른 예시)
※ r-value 참조
r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에 Double Ampersand(&&)를 붙여 표기한다.
이런 더블 엡퍼센드를 r-value 참조자라 부른다.
사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다
프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.
/*---------------일반 변수--------------- */
void f(){
int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸
/*---------------r value 참조 변수--------------- */
void f(){
int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸
위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다
- 이동생성자가 호출되면얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move)방식으로 객체를 생성.
- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.
Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?
A. 아래 코드를 통해 설명하겠다.
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class Animal {
private:
char *name;
int age;
public:
Animal(int age_, const char* name_) {
age = age_;
name = new char[strlen(name_) + 1];
strcpy(name, name_);
}
Animal(const Animal & a) { //복사생성자에 의한 깊은복사
age = a.age;
name = new char[strlen(a.name) + 1];
strcpy(name, a.name);
cout << "복사생성자" << endl;
}
Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
age = a.age;
name = a.name;
cout << "이동생성자" << endl;
a.name = nullptr;
}
~Animal(){
cout <<"Destructor!!" << endl;
if(name)
delete [] name;
}
void changeName(const char *newName) {
strcpy(name, newName);
}
void printAnimal() {
cout << "Name: " << name << " Age: " << age << endl;
}
};
int main() {
Animal A(10, "Jenny");
A.printAnimal();
vector<Animal> vec; // Animal 벡터 타입
// Animal 벡터 객체 삽입
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
A.printAnimal();
for (int i = 0; i < 5; i++) {
vec[i].printAnimal();
}
return 0;
}
복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.
이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.
[이동생성자 사용 시 주의할 점]
1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)
2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함