다음과 같이 객체를 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 클래스에서는 아무 의미가 없지만 파생클래스에서 원하는 문자열을 출력할 수 있게 재정의 해준다!
위에서 사용한 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>(newSong(L"Himesh Reshammiya", L"Tera Surroor")));
assets.push_back(shared_ptr<Song>(newSong(L"Penaz Masani", L"Tu Dil De De")));
assets.push_back(shared_ptr<Photo>(newPhoto(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 (constauto& 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;
}
[static_cast]: 정적으로 형변환을 해도 아무 문제가 없다 (= 이미 어떤 녀석인지 알고 있다는 뜻), Fast
[dynamic_cast]: 동적으로 형변환을 시도 해본다는 뜻 (= 이녀석의 타입을 반드시 알아봐야 한다는 뜻), Slow (RTTI)
따라서 dynamic_cast를 이용해 Runtype의 해당 타입을 명확히 알아봐야 하고 (RTTI, Requires Runtime Type Info)
그렇지 않은 경우, static_cast를 이용해 변환 비용을 줄이는 것이 좋다. (동적타입체크를 안해도 되서)
// 비행기에 여러 직군의 사람들이 탑승했다.// 한 승객이 갑자기 급성 맹장염에 걸려 의사가 급하게 수술을 해야 한다.classPassenger {...};
classStudent :public Passenger{
...
voidStudy();
};
classTeacher :public Passenger{
...
voidTeach();
};
classDoctor :public Passenger{
...
voidTreat();
voidOperate();
};
intmain(){
typedef vector<Passenger *> PassengerVector;
PassengerVector passengerVect;
Passenger* pPS = newStudent();
if (pPS){
passengerVect.push_back( pPS );
// 비행기 타자마자 공부한다고 치고~// pPS가 명확하게 어느 클래스의 인스턴스인지 알고 있다.// 이 경우엔 굳이 비용이 들어가는 dynamic_cast가 아닌, static_cast를 쓰는게 낫다.
Student* pS = static_cast<Student *>( pPS );
pS->Study();
}
Passenger* pPT = newTeacher();
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)값을 대입하면 된다.
- 가상함수: 상속관계(기본/파생클래스)에서 오버라이딩을 할 수 있는 함수
- 순수가상함수: 파생클래스에서 구현되어야하는 함수, 인터페이스로 이 클래스를 사용하겠다는 의미
- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.
★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.
※깊은복사
- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법
- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.
위의 코드에서 아래 코드를 추가해주면 된다.
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장
즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.
§ 또 다른 예제
DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
// 자신만의 동적데이터공간 할당
mArray = newint[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의 원칙을 준수하는 것보다
프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.
/*---------------일반 변수--------------- */voidf(){
int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸/*---------------r value 참조 변수--------------- */voidf(){
int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸
위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다
- 이동생성자가 호출되면얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move)방식으로 객체를 생성.
- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.
Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?
A. 아래 코드를 통해 설명하겠다.
#include<iostream>#include<cstring>#include<vector>usingnamespace std;
classAnimal {private:
char *name;
int age;
public:
Animal(int age_, constchar* name_) {
age = age_;
name = newchar[strlen(name_) + 1];
strcpy(name, name_);
}
Animal(const Animal & a) { //복사생성자에 의한 깊은복사
age = a.age;
name = newchar[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;
}
voidchangeName(constchar *newName){
strcpy(name, newName);
}
voidprintAnimal(){
cout << "Name: " << name << " Age: " << age << endl;
}
};
intmain(){
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();
}
return0;
}
복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.
이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.
[이동생성자 사용 시 주의할 점]
1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)
2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함