대입 연산자  
 위와 같은 문장이 있다. 여기서 b는 A클래스의 b객체이다. 이 문장은 다음과 같이 묵시적으로 변환이 일어난다. A a(b); 결국 a라는 이름으로 객체 생성할때 복사 생성자를 호출 하는 형태가 된다. 그럼 다음과 같은 문장은 어떤 일이 일어날까?
  1. A a;   
  2. B b;   
  3. a = b;  
 여기서 a = b; 이 문장은 a(b) 이런 형태로 묵시적 변환이 일어 나지 않는다. 위와 같은 대입연산을 썼지만, 객체 생성문장이 아니기 때문이다. 그럼 여기서 a = b;는 다음과 같이 해석 될 것이다. a.operator=(b); 복사생성에서 호출되는 대입연산과 오버로딩 되어 있는 operator 함수 호출에서의 대입연산을 혼동하지 말자

 대입 연산자 오버로딩  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     friend ostream& operator<<(ostream& os, const Point& p);   
  10.     Point& operator = (const Point &p)   
  11.     {   
  12.         x = p.x;   
  13.         y = p.y;   
  14.         return *this;   
  15.     }   
  16. };   
  17.   
  18. ostream& operator<<(ostream& os, const Point& p)   
  19. {   
  20.     os<<"["<<p.x<<", "<<p.y<<"]";    
  21.     return os;   
  22. }   
  23.   
  24. void main()   
  25. {   
  26.     Point p1(1, 3);   
  27.     Point p2(10, 30);   
  28.     cout<<p1<<endl;   
  29.     cout<<p2<<endl;   
  30.   
  31.     p1=p2;    
  32.     cout<<p1<<endl;   
  33. }  

  위에서 말한것과 같이, 31번째 라인에서는 a.operator=(b); 이런 형태로 대입연산 오버로딩이 일어날 것입니다. 하지만 여기 소스코드에서는 어디에서도 대입 연산을 정의 하는 함수를 찾아 볼 수 없다. 그것은 바로 대엽 연산자 오버로딩에 대해서만 "디폴트 대입 연산자"가 존재하기 때문이다. 그래서 우리가 명시적으로 써주지 않아도 이 디폴트 대입 연산자를 호출해 문제가 발생하지 않는 것이다. 
 만약 우리가 정의 한다고 하면 아래와 같이 정의 할 수 있겠다. 
  1. Point& operator = (const Point & p);   
  2. {   
  3.     x = p.x; y = p.y;   
  4.     return *this;   
  5. }  
 왜 참조형으로 리턴을 하냐면, 대입 연산도 p1 = p2 = p3; 이런 형태의 대입연산도 쓰기 때문에, 이것을 감안해서 참조형으로 리턴을 해줘야 한다. 그럼 이렇게 디폴트 대입 연산자가 있는데 굳이 우리가 정의를 해서 쓸 일이 있을까? 반드시 정의를 해서 써야 하는 경우가 있는데, 그 경우가 바로 깊은 복사(Deep Copy)가 요구 되어 지는 경우이다. 이와 같이 디폴트 대입 연산자의 문제점을 알아 볼 수 있다. 

 문제점  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Person {   
  5. private:   
  6.     char* name;   
  7. public:   
  8.     Person(char* _name);   
  9.     Person(const Person& p);   
  10.     ~Person();   
  11.     friend ostream& operator<<(ostream& os, const Person& p);   
  12. };   
  13.   
  14. Person::Person(char* _name){   
  15.     name= new char[strlen(_name)+1];   
  16.     strcpy(name, _name);   
  17. }   
  18. Person::Person(const Person& p){   
  19.     name= new char[strlen(p.name)+1];   
  20.     strcpy(name, p.name);   
  21. }   
  22. Person::~Person(){   
  23.     delete[] name;   
  24. }   
  25.   
  26. ostream& operator<<(ostream& os, const Person& p)   
  27. {   
  28.     os<<p.name;   
  29.     return os;   
  30. }   
  31.   
  32. int main()   
  33. {   
  34.     Person p1("One");   
  35.     Person p2("Two");      
  36.   
  37.     cout<<p1<<endl;   
  38.     cout<<p2<<endl;   
  39.   
  40.     p1=p2;  //error   
  41.   
  42.     cout<<p1<<endl;   
  43.   
  44.     return 0;   
  45. }  
 위의 소스 코드를 실행해 보면 컴파일에는 문제가 없지만, 런타임에서 문제가 생긴다. 그럼 이런 문제는 왜 일어 나는 것일까?
 여기서는 Person 클래스의 p1과 p2 객체를 생성하는데 p1과 p2는 객채 내에 name이라는 포인터가 있어서 문자열의 길이 만큼 동적 할당을하고 이 저장한 공간을  p1 객체가 포인터를 유지하는 형태를 띠게 되는데, 동적할당은 Heap 영역에 p1과 p2객체는 스택에 생성이 된다.
 그 후, 40번째 라인의 p1 = p2; 은 p1.operator=(p2) 이것으로 해석 되므로, 대입 연산자 오버로딩이 일어 날 것이다. 우선 여기서는 대입 연산자가 따로 정의 되어 있지 않으므로, 디폴트 대입 연산자를 사용해서 p2가 인자로 전달되어 멤버간의 복사가 일어 날 것이다. (p2가 지니는 값을 p1에 복사하는 것이다.) 하지만 p2의 멤버는 객체의 포인터이다. 그래서 객체의 포인터 값을 p1에다 복사 할텐데, 처음 가리키고던 "One"이 끊어지고 p1은 "Two"를 가리키게 된다. 이게 바로 문제다. 
 이 문제는 복사 생성자에서도 생겼던 문제 인데, 하나의 문자열을 두개의 객체가 참조 하다 보니까 소멸자 호출에 있어서 문제가 생기고,(이것은 복사생성자에서도 생겼던 문제) 포인터를 잃어 버렸기 때문에 프로그램 종료될때까지 메모리 공간에서 메모리를 잡아 먹게 되어 메모리 유출이 발생하는 문제가 발생된다. 그래서 이 경우에는 대입 연산자를 꼭 재정의 해줘야 한다. 
 일단 포인터가 끊어지고 생기는 메모리 유출에 대한 문제를 해결하고, 메모리 공간을 다시 할당해 복사를 해주자. 아래는 새로 정의된 대입 연산자 함수와 결과 출력이다. 이제는 문제 없이 출력되는 것을 알 수 있다.
  1. Person& Person::operator=(const Person& p)   
  2.     {   
  3.         delete []name;   
  4.         name= new char[strlen(p.name)+1];    
  5.         strcpy(name, p.name);    
  6.         return *this;   
  7.     }  

Posted by 모과이IT
,
쉬프트 연산자 <<, >>  
 : 쉬프트 연산자란, 우리들이 흔히 쓰는 cout, cin, endl 키워드를 쓸때 앞이나 뒤에 붙이는 연산자이다. 일반적으로 표준 라이브러리에 std라는 namespace에는 cout이나 cin에 관련된 클래스들이 있다. 그것이 바로 istream과 ostream이다. 이런 namespace를 일일이 입력하기 싫어서 using namespace 라는 간편한 문법을 쓰기도 한다. C++에서는 이런 쉬프트 연산자도 연산자 오버로딩이 가능하다.

 쉬프트 연산자 오버로딩  
  1. int main(void)   
  2. {   
  3.     Point p(1, 3);   
  4.     cout<<p;   
  5.   
  6.     return 0;   
  7. }  
 메인함수를 보자. 어떤 Point 클래스가 있고, 이 Point 클래스를 p라는 객체를 통해 생성하고 있고, 이 객체를 cout을 이용해 출력하려는 문장이다. 하지만 기본적으로 cout은 기본자료형만 출력을 하지 객체인 사용자 타입의 자료형은 출력을 할 수가 없다. 4번째 줄을 멤버 함수로의 오버로딩 해석에 의해 풀어 써 보자. 
 cout.operator<< (p) 즉, p객체를 인자값을 받을 수 있도록 cout 객체에는 operator<< 연산자가 오버로딩 되어 있어야 하는데, 표준 std의 ostream 클래스에서도 쉬프트 연산에 대한 오버로딩이 정의 되어 있지 않다. 그래서 우리가 표준 라이브러리를 건드리면서까지 객체를 출력하는 일은 불가능 할 것이다. 
 이로서 멤버 함수로의 오버로딩으로는 객체를 출력할 수 없다는 것을 알 수 있었다. 그럼 전역 함수의 오버로딩을 보자.전역함수로의 오버로딩에서는 cout << p 문장은 operator (cout, p); 로 풀어 써지고, 이것은 논리적으로 문제가 없다. 그럼 전역함수로 오버로딩으로 다음과 같이 함수를 정의 할 수 있겠다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     friend ostream& operator<<(ostream& os, const Point& p);   
  10. };   
  11.   
  12. ostream& operator<<(ostream& os, const Point& p)   
  13. {   
  14.     os<<"["<<p.x<<", "<<p.y<<"]"<<endl;    
  15.     return os;   
  16. }   
  17.   
  18. int main(void)   
  19. {   
  20.     Point p(1, 3);   
  21.     cout<<p;   
  22.   
  23.     return 0;   
  24. }  

Posted by 모과이IT
,
교환 법칙?  
 3 + 2 라는 연산이 있다. 여기에서 피연산자 위치를 다르게 바꿔도 ( 2 + 3 ) 동일한 결과를 가져오게 하는 법칙이 바로 교환법칙이다. 우리는 연산자 오버로딩을 사용함에 있어서 이렇게 교환법칙도 성립할 수 있게 만들어야 한다. 아래의 연산자 오버로딩을 보자.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point operator+(int val); //operator+라는 이름의 함수   
  11. };   
  12. void Point::ShowPosition() {    
  13.     cout<<x<<" "<<y<<endl;    
  14. }   
  15. Point Point::operator+(int val) {   
  16.     Point temp(x+val, y+val);   
  17.     return temp;   
  18. }   
  19.   
  20. int main(void)   
  21. {   
  22.     Point p1(1, 2);   
  23.     Point p2=p1+3;    
  24.     p2.ShowPosition();   
  25.   
  26.     return 0;   
  27. }  
 23번째 라인의 Point p2=p1+3; 이 문장은 C++에서는 p1.operator+( 3 ) 이와 같이 인식되는것을 우리는 앞서 알아봤었다. 교환법칙이 성립되려고 하면 다음과 같이 써도 에러가 나지 않아야 한다. 
  1. Point p3 = 3 + p1;   
 하지만 실제로 저 부분을 소스코드에 집어 넣고 컴파일 해보면 에러가 생기는 것을 알 수 있다. 
(Visual Studio 2010에서는 컴파일 하지 않아도 에러를 실시간으로 잡아내는 기능이 추가 되었다)

 문제 제기  
 그럼 왜 이런 교환법칙이 성립이 안되는 것일까? 자세히 한번 알아보자. 
  1. Point p3 = 3 + p1;  
 여기에서 숫자 3은 int형 데이터이고,  p1은 객체이다 타입이 다르기 때문에 덧셈이 불가능 하다. 그래서 오버로딩 되어 있는 operator +함수를 호출 하게 된다. 이 operator + 함수를 호출 하기 전에 이것이 멤버 함수인지 전역함수 인지 일단 알아 볼텐데, 우리는 멤버 함수로서 정의을 했으므로, 멤버 함수에서의 객체는 무조건 왼쪽이 기준으로 operator+ 함수를 호출하므로 3.operator+ (p1) C++에서는 이러한 문장으로 인식이 될 것이다. (멤버 함수로의 오버로딩시 무조건 연산자의 왼쪽이 피연산자의 기준이 된다.)
 여기서 바로 문제가 생긴다. 숫자 3은 int형 데이터로 operator+라는 함수가 멤버로서 존재할리 없다. 이 문장은 논리적 으로 문제가 있는 것이다. 그러면 우리는 교환법칙이 성립 되도록 추가적인 코드를 만들어야 한다.

 문제 해결  
 : 우리는 멤버 함수 오버로딩으로는 문제가 있다는 것을 알았다. 그러면 전역 함수 오버로딩시에는 어떻게 될까? Point p3 = 3 + p1; 이 문장은 전역함수에서 인식될때 왼쪽에 있는 피연산자가 첫번째 인자로, 오른쪽에 피연산자가 두번째 인자로 가기 때문에 operator+ (3, p);  로 C++에서 인식되고 이것은 논리적으로 문제가 전혀 없다. 결론이 나왔다. 우리는 교환법칙을 성립 해주기 위해서 전역함수 오버로딩을 제공해주기만 하면 되는것이다. 바로 아래와 같이 말이다. 결과도 이상없이 출력되는 것을 알 수 있다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point operator+(int val); //operator+라는 이름의 함수   
  11.     friend Point operator+(int val, const Point& p);    
  12. };   
  13. void Point::ShowPosition() {    
  14.     cout<<x<<" "<<y<<endl;    
  15. }   
  16. Point Point::operator+(int val) {   
  17.     Point temp(x+val, y+val);   
  18.     return temp;   
  19. }   
  20.   
  21. Point operator+(int val, Point& p)   
  22. {   
  23.     return p+val;   
  24. }   
  25.   
  26. int main(void)   
  27. {   
  28.     Point p1(1, 2);   
  29.     Point p2=p1+3;   
  30.     p2.ShowPosition();   
  31.   
  32.     Point p3=3+p2;   
  33.     p3.ShowPosition();   
  34.   
  35.     return 0;   
  36. }  


 임시 객체  
  1. int a = 3 + 4;  
 위와 같이 선언된 문장이 있다. 우리는 보통 생각하기에 이 문장은 int 형 데이터가 4Byte 이므로 메모리 공간에 4Byte의 메모리 공간만 할당한다고 생각하겠지만, 이 경우에 메모리 공간에 3과 4라는 상수도 CPU에 의해서 연산이 이루어 져야 하므로 메모리 공간에 올라간다. 
 그럼 이렇게 상수도 메모리 공간에 올라가니까 다음 라인에서 이 숫자를 참조가 가능할까? 불가능하다. 물론 변수 a는 참조가 가능하다. 무슨 차이가 있을까? 바로 상수3과 4는 변수 a와 같이 이름이 없기 때문이다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA{   
  5.     char name[20];   
  6. public:   
  7.     AAA(char* _name){   
  8.         strcpy(name, _name);   
  9.         cout<<name<<" 객체 생성"<<endl;   
  10.     }   
  11.     ~AAA(){   
  12.         cout<<name<<" 객체 소멸"<<endl;   
  13.     }   
  14. };   
  15.   
  16. int main(void)   
  17. {   
  18.     AAA aaa("aaa Obj");   
  19.     cout<<"--------임시 객체 생성 전---------"<<endl;   
  20.     AAA("Temp Obj");   
  21.     cout<<"--------임시 객체 생성 후---------"<<endl;   
  22.     return 0;   
  23. }  
 위 소스 코드는 임시 객체 생성 코드 이다. 여기에서 AAA("Temp Obj");는  AAA aaa("aaa Obj"); 와 비교해보면 무엇이 틀린지 금방 알 수 있다. 바로 AAA("Temp Obj")는 이름이 없다. 이 문장은 AAA라는 객체 클래스를 생성하는데 "Temp Obj"라는 문자열을 받는 함수를 호출하라는 의미이다. 물론 객체는 생성하지만, 이름이 없어서 해당 라인을 벗어나면 바로 소멸 된다. 이렇게 만들어지는 순간은 존재했다가 해당 라인에서 벗어나면 소멸하는 객체를 바로 임시 객체 라고 한다. 임시 객체의 생성과 소멸은 결과를 보면 충분히 이해 할 수 있을 것이다. (해당 라인에서 생성, 벗어나자마자 소멸을 cout으로 출력해주는것을 볼 수 있다.)


 이 임시객체라는 것은 경우에 따라서는 컴파일러에 따라 최적화가 되기 때문에 가급적이면 사용하는 것이 좋다. 위의 예제를 임시객체를 이용해 다음과 같이 바꿀 수도 있을 것이다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point operator+(int val); //operator+라는 이름의 함수   
  11.     friend Point operator+(int val, const Point& p);    
  12. };   
  13. void Point::ShowPosition() {    
  14.     cout<<x<<" "<<y<<endl;    
  15. }   
  16. Point Point::operator+(int val) {   
  17.        
  18.     return Point(x+val, y+val); //이 줄에서 임시객체는 소멸 전이므로 그 전에 리턴을 한다.   
  19. }   
  20.   
  21. Point operator+(int val, Point& p)   
  22. {   
  23.     return p +val;   
  24. }   
  25.   
  26. int main(void)   
  27. {   
  28.     Point p1(1, 2);   
  29.     Point p2=p1+3;   
  30.     p2.ShowPosition();   
  31.   
  32.     Point p3=3+p2;   
  33.     p3.ShowPosition();   
  34.   
  35.     return 0;   
  36. }  

Posted by 모과이IT
,
증가/감소 연산자 오버로딩  
 이전에는 사칙연산인 이항연산자 오버로딩에 대해서 알아 봤는데 이번에는 단항 연산자인 증가(++), 감소(--) 연산자 오버로딩에 대해서 알아 보자. 만약 P객체가 증가가 된다면 멤버함수와 전역함수에서는 C++의 약속에 의해 어떻게 표현 될까? 이전의 이항 연산자 오버로딩때와 마찬가지로 아래와 같이 됨을 알 수 있을 것이다. 
  1. p.operator++ ( ) //멤버   
  2. operator ++(p)   //전역  
그럼 단항 연산자 오버로딩의 예제 소스코드를 한번 살펴보자. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point& operator++(); //멤버 함수   
  11.     friend Point& operator--(Point& p);    
  12. };   
  13. void Point::ShowPosition(){    
  14.     cout<<x<<" "<<y<<endl;    
  15. }   
  16.   
  17. Point& Point::operator++() //멤버함수 정의    
  18. {   
  19.     x++;   
  20.     y++;   
  21.     return *this;   
  22. }   
  23.   
  24. Point& operator--(Point& p) //전역함수   
  25. {   
  26.     p.x--;   
  27.     p.y--;   
  28.     return p;   
  29. }   
  30.   
  31. int main(void)   
  32. {   
  33.     Point p(1, 2);   
  34.     ++p;    
  35.     p.ShowPosition();     
  36.   
  37.     --p;    
  38.     p.ShowPosition();     
  39.   
  40.     ++(++p);   
  41.     p.ShowPosition();     
  42.   
  43.     --(--p);   
  44.     p.ShowPosition();     
  45.   
  46.     return 0;   
  47. }  
 여기에서 눈여겨 봐야 될 것은, 24번째줄에 operator++ 멤버 함수는 리턴 타입이 포인터의 참조를 리턴 하고 있는것을 알 수 있다. 물론 전역도 마찬가지로 참조를 리턴해주고 있다. 그럼 왜 참조형을 리턴해주는 것일까? 그것은 바로 40번째 라인과 같은 ++(++p) 형태의 연산을 허용해 주기 위해서이다.  ++(++p)는 어떤 의미를 갖는가? 
  1. void main()   
  2. {   
  3.     int n;   
  4.     ++(++n);   
  5. }  
 여기 n이라는 변수가 있다. ++(++n) 연산을 하면, ( )괄호 연산에서 n은 2에서 3의 값이 되고 이 3이 된 n을 ++( n)  이와 같이 증가 시켜 줘서 결국 4란 값이 되는것이다. 여기서 알 수 있는 것은 괄호 연산 앞의 증가 연산은 어디에 영향을 미치느냐 하면은, 바로 변수 n에 영향을 미치기 때문에 이런 증가 연산이 성립이 되는 것이다. 결국 변수 or 객체를 기준으로 증가 연산을 하는 것이다. 

 따라서 ++(++p) 연산에서도 괄호 안의 연산인 p.operator++() 함수 호출이 끝나고 나서 이 자리에 p가 그대로 리턴이 돼야 이런 증가 연산이 제대로 이루어 질 수 있다. 
  1. Point& Point::operator++() //멤버함수 정의    
  2. {   
  3.     x++;   
  4.     y++;   
  5.     return *this;   
  6. }  
 멤버 함수 정의부를 보면 return 타입이 *this인 것을 알 수 있다. 여기서의 this는 Point 객체의 포인터를 의미하는데, *를 붙였으니까 Point객체 자신을 의미한다. 그럼 나 자신을 무엇으로 리턴 하느냐? 바로 Point& 즉, 참조로 리턴을 하는 것이다. 
  괄호 안은 p.operatr++()이 되고 이 연산이 끝나고 p의 참조를 리턴하는데 p의 참조는 p와 같다. ++(p참조) -> p참조 . operator++() 이 것이 된다. p참조는 p자신 이므로, p.operator++() 이 되는 것과 같다. 그래서 결과가 ' 3 4 '로  연산이 제대로 되는 것이다. 


 만약 참조로 리턴 안하고 Point로 리턴 한다면 어떻게 될까? 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point operator++(); //멤버 함수   
  11.     friend Point operator--(Point& p);    
  12. };   
  13. void Point::ShowPosition(){    
  14.     cout<<x<<" "<<y<<endl;    
  15. }   
  16.   
  17. Point Point::operator++() //멤버함수 정의   
  18. {   
  19.     x++;   
  20.     y++;   
  21.     return *this;   
  22. }   
  23.   
  24. Point operator--(Point& p) //전역함수   
  25. {   
  26.     p.x--;   
  27.     p.y--;   
  28.     return p;   
  29. }   
  30.   
  31. int main(void)   
  32. {   
  33.     Point p(1, 2);   
  34.     ++p;    
  35.     p.ShowPosition();     
  36.   
  37.     --p;    
  38.     p.ShowPosition();     
  39.   
  40.     ++(++p);   
  41.     p.ShowPosition();     
  42.   
  43.     --(--p);   
  44.     p.ShowPosition();     
  45.   
  46.     return 0;   
  47. }  
 그냥 Point를 리턴 한다면, 나 자신을 복사 해서 리턴 하는것이다. 그래서 ++(++p) 에서 괄호 안의 연산후에 이 자리에 오는 것은 p가 아니라 p객체의 복사본이 리턴되는것이다. 그리고 p객체의 복사본을 가지고 ++연산을 하게 되는것이다. 결과적으로 리턴하는 순간에 새로운 객체를 만들어서 리턴하는 꼴이 되므로 p객체는 단지 한번만 증가 하게 되는 것이다. (이것이 문제가 된다)


 선연산과 후연산의 구분  
 단항 연산자 ++, --를 변수나 객체의 앞에 붙이느냐 뒤에 붙이느냐에 따라 증가를 먼저 하고 연산을 할것인가, 연산을 먼저 한 후, 값을 증가 시킬 것인지 나뉘게 된다. 
 - ++ p : 변수의 값을 먼저 증가 시킨후 연산
 - p++ : 연산후 값 증가
 우리가 단항 연산자 오버로딩을 설계할 때, 이처럼 문법상 기본 자료형이 하는 일을 따라 간다면 우리는 선연산 후연산이 다르게 동작 되어야 하는것을 알 수 있는 것이다. 그래서 우리는 호출 되는 함수의 구분을 위해 C++은 또 하나의 약속을 해야 하는 것이다.  다음과 같이 말이다. 
 - ++p 은 p.operator ++( );
 - p++ 은 p.operator ++(Data Type);

 여기서 선언된 데이터 타입은(int, double 등등의...)  ++ 연산을 구분 지어 주기 위해서만 의미를 가진다. 예제 소스를 보자.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}    
  9.     void ShowPosition();    
  10.     Point& operator++(); //++p   
  11.     Point operator++(int); // p++   
  12. };   
  13. void Point::ShowPosition(){    
  14.     cout<<x<<" "<<y<<endl;    
  15. }   
  16.   
  17. Point& Point::operator++(){   
  18.     x++;   
  19.     y++;   
  20.     return *this;   
  21. }   
  22. Point Point::operator++(int){   
  23.     Point temp(x, y);  // Point temp(*this);   
  24.     x++;   
  25.     y++;   
  26.     return temp;   
  27. }   
  28.   
  29.   
  30. int main(void)   
  31. {   
  32.     Point p1(1, 2);    
  33.     (p1++).ShowPosition();    
  34.     p1.ShowPosition();     
  35.   
  36.     Point p2(1, 2);   
  37.     (++p2).ShowPosition();    
  38.     return 0;   
  39. }  

 결가 값을 보면 (p1++).ShowPosition(); 이 부분에 1 2 값을 출력하는 것을 알 수 있다. p1객체 뒤에 증가 연산자가 왔으므로, 이것은 후 증가 의미를 가지기 때문이다. 그후 p1을 출력해보면, 후 증가의 값을 눈으로 확인해볼 수 있는 것이다. 

 그럼 증가 하기 이전의 값을 얻기 위한 함수는 어떻게 만들까? 그 함수는 바로 이 부분이다. 
  1. Point Point::operator++(int){   
  2.     Point temp(x, y);  // Point temp(*this);   
  3.     x++;   
  4.     y++;   
  5.     return temp;   
  6. }  
 
증가하기 이전에 값을 만든 다음에 (temp) 그 다음에 값을 증가 시키고, 
리턴 할때는 증가 하기 이전에 객체를 이전하면 된다. 이 함수의 경우에는 참조로 리턴이 안됐는데, 이 함수는 
참조로 리턴 할 수 없다. 
 왜냐? 참조로 리턴(return) 할 수 없는것이 무엇인가? 바로 지역 변수, 지역객체 이다. 여기서 temp는 
지역적으로 선언된것이므로 참조로 리턴 될 수 없다. 만약 리턴 된다고 해도 리턴 되고 나서 이 리턴값은 바로 사라지기 때문이다. 이게 바로 연산자 오버로딩의 한계점이라고 할 수 있다.
Posted by 모과이IT
,
연산자 오버로딩(Operator Overloading)은 C++ 문법과 우리와의 약속이라고 할 수 있는데, 우리가 함수를 정의하는데 이어서 operator라는 키워드와 연산자를 붙여서 함수의 이름을 만들 경우에 (operator + 이렇게 ) 우리가 일반적인 함수를 호출 방법 말고도, 연산자만을 이용해서도 함수가 호출 되도록 해주겠다는 이야기이다.

 멤버 함수에 의한 오버로딩  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     Point operator+(const Point& p);   
  11. };   
  12. void Point::ShowPosition(){    
  13.     cout<<x<<" "<<y<<endl;    
  14. }   
  15. Point Point::operator+(const Point& p){   
  16.     Point temp(x+p.x, y+p.y);   
  17.     return temp;   
  18. }   
  19.   
  20. int main(void)   
  21. {   
  22.     Point p1(1, 2);   
  23.     Point p2(2, 1);   
  24.     Point p3=p1+p2;   
  25.     p3.ShowPosition();   
  26.   
  27.     return 0;   
  28. }  
 그럼 위의 소스 코드 24번째 줄의 p1+p2는 어떤의미를 갖는지 알아 보자. 여기서 p1 , p2는 객체 이므로 기본적으로 '+' 덧셈 연산이 불가능하다. 그래서 C++은 어떤 약속을 하였느냐 하면, '+' 라는 사칙연산을 할려고 하는게 아니라,  앞에 operator 라는 키워드를 붙여서 p1과 p2를 이용해서 operator +라는 함수를 호출해주게 되는 것이다. 
 그래서 이제 C++은 'operator +' 라는 함수를 호출 해야 하는데, C++에서는 함수를 만드는 방법 멤버 함수, 전역함수 두가지가 있다. 그러므로 'operator +'는 이 둘로 정의 될 수 있는 것이다. 우리가 멤버로 만들건, 전역으로 만들건 C++은 'operator +'에 대해 알아서 처리해줄 수 있다. 그럼 멤호 함수에 의한 오버로딩은 어떻게 일어 나는지 알아 보자. 
 24번째 라인의 p1 + p2;  이것은 "이항 연산자의 왼쪽에 오는 객체의 operator + 함수를 호출 하면서, 이항연산자의 오른쪽의 피연산자를 인자로 전달한다" 라는 의미가 된다. 즉 
  1. p1.operator +(p2);   
이렇게 바뀐다는 말이다. 
  덧셈 연산이 끝나면 p1+p2 자리에 temp 라는 이름의 객체의 복사본이 리턴되서 들어 오게 된다. 그리고 리턴된 값은 p3를 초기화 해준다. 여기서 temp 객체는 포인터 클래스의 객체 이므로, 이 경우의 복사 생성자가 호출 되는 것도 알 수 있다. temp 함수의 연산에 의해서 아래와 같이 결과가 나오는 것도 알 수 있다. 


 전역 함수에 의한 오버로딩  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class Point {   
  5. private:   
  6.     int x, y;   
  7. public:   
  8.     Point(int _x=0, int _y=0):x(_x), y(_y){}   
  9.     void ShowPosition();   
  10.     friend Point operator+(const Point& p1, const Point& p2);   
  11. };   
  12. void Point::ShowPosition(){    
  13.     cout<<x<<" "<<y<<endl;    
  14. }   
  15.   
  16. Point operator+(const Point& p1, const Point& p2) //전역함수다   
  17. {   
  18.     Point temp(p1.x+p2.x, p1.y+p2.y);   
  19.     return temp;   
  20. }   
  21.   
  22. int main(void)   
  23. {   
  24.     Point p1(1, 2);   
  25.     Point p2(2, 1);   
  26.     Point p3=p1+p2;   
  27.     p3.ShowPosition();   
  28.   
  29.     return 0;   
  30. }  
 위의 소스코드는 전역함수를 friend 선언해주고있다. friend 선언을 해줌으로서,  operator+ 함수를 포인트 클래스 객체의 private 멤버에 직접 접근이 가능하다. (friend는 연산자 오버로딩에 주로 사용된다.)
 그럼 전역함수에서는  p1 + p2; 가  C++의 약속에 의해 어떤 식으로 해석이 되는 것일까? 바로 아래와 같이 해석된다. 
  1. operator+ (p1, p2);  
 전역함수는 함수 이름만 있어도 호출이 가능하므로, 위와 같은식으로 해석되는 것을 알 수 있다. 여기까지 멤버함수, 전역 함수에 의한 오버로딩을 알아 봤다. 
Posted by 모과이IT
,
다중 상속이란?  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA{   
  5. public:   
  6.     void String1(){   
  7.         cout<<"AAA::String1"<<endl;   
  8.     }   
  9. };   
  10.   
  11. class BBB{   
  12. public:   
  13.     void String2(){   
  14.         cout<<"BBB::String2"<<endl;   
  15.     }   
  16. };   
  17.   
  18. class CCC : public AAA, public BBB{    
  19. public:   
  20.     void ShowString(){   
  21.         String1();   
  22.         String2();   
  23.     }   
  24. };   
  25.   
  26. int main(void)   
  27. {   
  28.     CCC ccc;   
  29.     ccc.ShowString();   
  30.   
  31.     return 0;   
  32. }  
 위의 소스 코드만 보면 감이 올것이다. 다중 상속이란 18번째 줄처럼 둘 이상의 클래스를 동시에 상속하는 것이다. 

 다중 상속의 모호성  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA{   
  5. public:   
  6.     void String(){   
  7.         cout<<"AAA::String"<<endl;   
  8.     }   
  9. };   
  10.   
  11. class BBB{   
  12. public:   
  13.     void String(){   
  14.         cout<<"BBB::String"<<endl;   
  15.     }   
  16. };   
  17.   
  18. class CCC : public AAA, public BBB{   
  19. public:   
  20.     void ShowString(){   
  21.         String();  // AAA::String();   
  22.         String();  // BBB::String();   
  23.     }   
  24. };   
  25.   
  26. int main(void)   
  27. {   
  28.     CCC ccc;   
  29.     ccc.ShowString();   
  30.   
  31.     return 0;   
  32. }  
 위의 소스 코드와 같이 다중 상속의 관계에서 AAA,BBB 클래스에 같은 이름을 가진 멤버 함수가 있다고 하면, 이때 모호성이 발생한다. 과연 이 String 함수는 어느 객체의 함수를 호출해 주는 것이냐 알수 없기 때문이다. 주석친 부분 처럼 범위 지정 연산자를 이용한 문제 해결 방법도 있지만, 아주 코드가 복잡해 지는걸 알수 있는것이다. 
Posted by 모과이IT
,
동작 원리  
 : 가상함수는 어떻게 동작을 할까? 객체가 생성되면 멤버 함수는 메모리의 코드영역에 올라가게 된다. 우리가 생성한 객체는 코드 영역에 있는 이 멤버함수를 공유하게 것이죠. 하지만 클래스에 한개 이상의 가상 함수가 있을 경우에는 컴파일러는 실제 호출되어야 할 함수의 위치 정보를 가지고 있는 가상 함수 테이블(Vritual Table) 이라는 것을 만들고, 클래의 객체에는 가상 함수 테이블을 위한 포인터가 멤버 변수로 추가 시킵니다. 일반적으로 가상 함수가 있는 객체는 가상 함수 테이블에 있는 함수만을 호출하는 원칙을 가지고 있다.

 가상 함수의 단점  
 : 가상 함수를 사용해서 생기는 단점은 두가지를 들 수 있다. 첫번째로, 가상함수를 쓰면 가상함수 테이블을 만드므로 그만큼의 메모리 공간의 소모가 일어 나게 되기 마련이고, 두번째로 직접 가야 할 부분을 이 가상함수 테이블을 거쳐 가야 하니까 속도면에서 약간의 차이가 나게 되는 단점이 있습니다. 이렇게 단점이 있는것을 왜 쓰냐? 그만큼의 장점 부분이 차지하는 부분이 더 크기 때문입니다.
Posted by 모과이IT
,
곧바로 예제부터 보자.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6.     char* str1;   
  7. public:   
  8.     AAA(char* _str1){   
  9.         str1= new char[strlen(_str1)+1];   
  10.         strcpy(str1, _str1);   
  11.     }   
  12.     ~AAA(){         // virtual ~AAA()   
  13.         cout<<"~AAA() call!"<<endl;   
  14.         delete []str1;   
  15.     }   
  16.     virtual void ShowString(){   
  17.         cout<<str1<<' ';   
  18.     }   
  19. };   
  20.   
  21. class BBB : public AAA   
  22. {   
  23.     char* str2;   
  24. public:   
  25.     BBB(char* _str1, char* _str2) : AAA(_str1){   
  26.         str2= new char[strlen(_str2)+1];   
  27.         strcpy(str2, _str2);   
  28.     }   
  29.     ~BBB(){   
  30.         cout<<"~BBB() call!"<<endl;   
  31.         delete []str2;   
  32.     }   
  33.     virtual void ShowString(){   
  34.         AAA::ShowString();   
  35.         cout<<str2<<endl;   
  36.     }   
  37. };   
  38.   
  39. int main()   
  40. {   
  41.     AAA * a=new BBB("Good""evening");   
  42.     BBB * b=new BBB("Good""morning");   
  43.   
  44.     a->ShowString();    
  45.     b->ShowString();   
  46.   
  47.     cout<<"-----객체 소멸 직전----"<<endl;   
  48.     delete a;   
  49.     delete b;   
  50.   
  51.     return 0;   
  52. }  
 위 코드를 보면, AAA클래스에서 생성자에서 동적 할당 하기에 소멸자에서 메모리 해제 하고 있고 마찬가지로 BBB클래스의 생성자에서 동적 할당을 하고 있어서 소멸자에서 메모리 해제 하고 있는 형태를 가지고 있다. BBB클래스의 객체가 소멸될때, AAA클래스의 소멸자도 호출이 된다. 
  BBB클래스 객체가 생성이 되면, AAA클래스의 생성자에 의해서도 메모리 공간 동적 할당 할것이고, BBB클래스도 마찬가지이다. 이 두곳에서 할당된 메모리 공간이 적절히 해제 될것이기에 우리가 신경 쓰지 않아도 되지만, 여전히 문제가 존재 한다. 

 문제제기  
  1. int main()   
  2. {   
  3.     //AAA * a=new BBB("Good", "evening");   
  4.     BBB * b=new BBB("Good""morning");   
  5.   
  6.     a->ShowString();    
  7.     b->ShowString();   
  8.   
  9.     cout<<"-----객체 소멸 직전----"<<endl;   
  10.     //delete a;   
  11.     delete b;   
  12.   
  13.     return 0;   
  14. }  
 위의 코드는 정상적인 코드 이다. BBB클래스의 객체가 생성되는 과정에서 AAA클래스의 생성자도 호출되므로, 소멸될때는 AAA,BBB클래스의 소멸자가 아래와 같이 다 호출이 되는것을 알 수 있다.

그럼 다음 코드를 보자.
  1. int main()   
  2. {   
  3.     AAA * b=new BBB("Good""morning");   
  4.     b->ShowString();   
  5.   
  6.     cout<<"-----객체 소멸 직전----"<<endl;   
  7.   
  8.     delete b;   
  9.   
  10.     return 0;   
  11. }  
 위의 경우를 생각해보자. BBB클래스는 AAA클래스를 상속하기 때문에 선언부가 3번째 줄처럼 바뀔수도 있을 것이다. 하지만 실행해보면, AAA클래스의 소멸자만 호출 되고 있음을 알 수 있다. 바로 이 부분에서 메모리의 유출이 발생된다. 


 AAA클래스의 포인터로 참조 하지만, 생성되는 객체는 B클래스의 객체이기 때문에, AAA,BBB 클래스에서도 생성자부분에서 메모리 공간이 동적 할당되는데는 문제가 없다. 하지만 객체가 소멸될때 A클래스 내에서 동적 할당한 메모리 공간은 반환되지만, BBB는 반환되지 않았던 것이다. 객체가 소멸하고자 했을때 소멸의 주체가 되는 포인터가 AAA클래스의 포인터였기 때문에 이런 일이 발생한다. 

 문제 해결 - 가상 소멸자 사용  
 : 그럼 위 문제를 어떻게 해결 할 것인가? 바로 Virtual 소멸자(virtual destructor)을 써줌으로써 간단하게 해결이 된다. 아래와 같이 간단하게 virtual 키워드만 소멸자 앞에 붙여주면 되는 것이다. 
  1. class AAA   
  2. {   
  3.        
  4. public:   
  5.        
  6.     virtual ~AAA(){         // virtual만 붙여주면 된다.   
  7.         cout<<"~AAA() call!"<<endl;   
  8.         delete []str1;   
  9.        
  10. };  
 virtual 소멸자의 경우 AAA클래스를 상속하고 있는 BBB클래스의 소멸자를 호출하게 된다. 이어서 BBB클래스가 AAA클래스를 상속하고 있으므로 AAA클래스의 소멸자를 호출하게 됨으로써 소멸자들이 정상적으로 호출되는 결과를 볼수 있다. 

Posted by 모과이IT
,
virtual 특성의 상속  
 앞서 우리는 오버라이딩이란 무엇인지 알아 보았다. 그 중 virtual 이라는 키워드를 잠깐 알아 봤는데, 한가지 빼먹은 것이 있었다. 바로 오버라이딩에서의 virtual의 특징은 그것의 특성도 상속된다는 것이다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6. public:   
  7.     virtual void fct(){   
  8.         cout<<"AAA"<<endl;   
  9.     }   
  10. };   
  11.   
  12. class BBB : public AAA   
  13. {   
  14. public:   
  15.     void fct(){       
  16.         cout<<"BBB"<<endl;   
  17.     }   
  18. };   
  19.   
  20. class CCC : public BBB   
  21. {   
  22. public:   
  23.     void fct(){   
  24.         cout<<"CCC"<<endl;   
  25.     }   
  26. };   
  27.   
  28.   
  29. int main(void)   
  30. {   
  31.     BBB* b=new CCC;   
  32.     b->fct();   
  33.   
  34.     AAA* a=b;   
  35.     a->fct();   
  36.   
  37.     delete b;   
  38.     return 0;   
  39. }  
  위의 소스코드에서 BBB 클래스가 상속하고 있는 AAA 클래스의 Virtual void fct함수가 B클래스의 fct 함수에 의해서 오버라이딩 되어 지고 있다. 이 경우 오버라이딩을 하는 BBB 클래스의 fct 함수도 자동으로 virtual 키워드가 들어가게 된다. (명시적으로 써주나 안써주나 똑같지만, 코드의 가독성을 위해 써주는 것이 좋다. )
  1. class BBB : public AAA   
  2. {   
  3. public:   
  4.     virtual void fct(){       
  5.         cout<<"BBB"<<endl;   
  6.     }   
  7. };  
 C클래스도 마찬가지이다.아래와 같이 써주자. 
  1. class CCC : public BBB   
  2. {   
  3. public:   
  4.     virtual void fct(){   
  5.         cout<<"CCC"<<endl;   
  6.     }   
  7. };  
이것을 실행하면 어떤 결과를 출력하느냐? 앞서 오버라이팅 포스팅 부분에서도 언급했듯이 아래와 같은 결과를 출력하는것을 알 수 있다.

 만약 BBB,CCC 클래스가 없다고 가정할 경우를 생각해보자. AAA클래스만 있다고 하면, virtual은 의미를 가지지 않는다. virtual이라는 키워드는 오버라이딩 관계에 의해 상속되어질때만 의미를 지니기 때문이다. 

 정적/동적 바인딩  
 이번에는 정적 바인딩(Static Binding) 동적 바인딩 (Dynamic Binding)이 무엇인지 알아 보기 전에 바인딩(Binding)이란 무엇인지 알아 보자. 바인딩이란 프로그램 소스에 쓰인 각종 내부 요소, 이름, 식별자들에 대해 값 혹은 속성을 확정하는 과정을 일컫는 말이다.  이 과정이 빌드 중에 이루어지면 정적 바인딩이라고 하고, 실행 중에 이루어지면 동적 바인딩이라고 한다. 
 그럼 한번 예를 들어 보자 아래와 같이 A,B,C가 상속관계에 있고 abc라는 함수가 virtual로 선언되어 있다고 하자. 
 만약 메인 함수에서 다음과 같이 코딩을 했다고 해보자.
  1. void main()   
  2. {   
  3.     A* a = new [ 어떤것 ] //[] 안에 들어갈것이 무엇인지는 모른다.   
  4.     a->abc();   
  5. }  
 객체 포인터 선언에 '어떤것'이 무엇인지 알아야 어떤것이 호출되는지 알수 있다. 포인터에 따라서 호출되는 함수가 결정되는 것이 아니라, 그 포인터가 가리키는 객체에 따라서 호출되는 함수가 결정된다는 것이 바로 동적 바인딩(Dynamic Binding)이다. 즉, 실제로 대상에 따라서 호출되는 함수가 상황에 따라 달라지는 것을 의미한다. 
 그럼 정적바인딩이란 무엇인가 ? 다음과 같은 선언이 있다고 해보자. 
  1. void main()   
  2. {   
  3.     int static_binding =2   
  4. }  
 이 선언 부분에서 데이터 타입이 int로 정해지는 것과 그 타입의 변수명이 static_binding으로 정해지는 것이 바로 정적 바인딩(Static Binding) 이다. 

 C++의 가상 함수의 바인딩은 문서상으로는 동적 바인딩으로 되어 있으나, 구현상으로는 런타임 성능을 높이기 위해 정적 바인딩을 쓰고 있다. 즉, 컴파일중에 아예 가상 함수 테이블을 파생 클래스에 맞게 바꿈으로써, 겉보기에는 파생 클래스 타입에서 오버라이드한 가상 함수를 호출하는 것처럼 보이게 만드는 것이다. 

 오버라이딩된 함수 호출하기  
 : 오버라이딩된 함수를 호출하는 방법은 간단하다. 소스코드를 보면 한눈에 알아 볼 수 있다.
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6. public:   
  7.     virtual void fct(){   
  8.         cout<<"AAA"<<endl;   
  9.     }   
  10. };   
  11.   
  12. class BBB : public AAA   
  13. {   
  14. public:   
  15.     void fct(){   
  16.         AAA::fct();   
  17.         cout<<"BBB"<<endl;   
  18.     }   
  19. };   
  20.   
  21. int main(void)   
  22. {   
  23.     AAA* a=new BBB;   
  24.     a->fct();     
  25.   
  26.     return 0;   
  27. }  
 위와 같이 fct함수를 그냥 호출 하는 것이 아니라, 16번째 줄처럼 명시적으로 정의가 선언되어 있는 클래스를 범위 지정 연산자로 지정해 주는 것이다. 이 AAA::fct() 코드부분의 의미는 "AAA클래스에 있는 fct 함수를 호출하라" 이다.

 순수 가상 함수와 추상래스  
 우선 가상함수에 대해서 복습해보자. 가상 함수(Virtual Function)는 파생 클래스가 안전하게 재정의할 수 있는 함수이다. 만약 상속 관계가 아니라면 가상 함수를 선언할 필요가 없으므로 가상 함수는 상속 계층내에서만 의미가 있으며 파생 클래스에게 재정의 기회를 주기 위해 존재하는 것이라고 할 수 있다. 
 하지만 이 가상 함수를 반드시 재정의해야만 하는 것은 아니다. 기반 클래스의 동작을 그대로 쓰고 싶으면 단순히 상속만 받고 변경할 필요가 있을 때만 재정의하면 된다. Base 클래스가 가상 함수를 만드는 이유는 혹시라도 재정의하고 포인터로 호출할 때를 대비한 것이다. 가상 함수는 재정의해도 되는 함수이지 반드시 재정의해야 하는 함수는 아닌 것이다. 
 그럼 순수 가상 함수는 어떤가? 순수 가상 함수(Pure Virtual Function)는 파생 클래스에서 반드시 재정의해야 하는 함수이다. 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지지 않으며 따라서 이 상태에서는 호출할 수 없다. 본체가 없다는 뜻으로 함수 선언부의 끝에 =0이라는 표기를 하는데 이는 함수만 있고 코드는 비어 있다는 뜻이다. 아래와 같이 클래스 단에서 선언한다.
  1. class Pure   
  2. {   
  3. public:   
  4.     virtual int vFunction() = 0;   
  5. };  
 또 이런 클래스(하나이상 가상함수를 지닌 클래스)를 추상 클래스(Abstract Class) 라고 일컫는다. 추상클래스는 완전한 클래스가 아니므로 객체화 될 수 없다. 
Posted by 모과이IT
,
오버라이딩 (Overriding)  
 : 오버라이딩(Overriding)이란 기본 클래스에 선언된 멤버와 같은 형태의 멤버를 파생 클래스에서 선언하는 것이다. 오버라이딩에 관해서 더 자세히 알아 보기 전에 오버라이딩을 재정의로 알고 있으신분들은 오버라이딩에 등장하는 특성이 재정의 인거지 오버라이딩 자체는 재정의가 아닌것을 명심하고 들어가보자.

 은닉의 효과  
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6. public:   
  7.     void fct(){   
  8.         cout<<"AAA"<<endl;   
  9.     }   
  10. };   
  11.   
  12. class BBB : public AAA   
  13. {   
  14. public:    
  15.     void fct(){ //AAA 클래스의 fct() 함수를 오버라이딩.   
  16.         cout<<"BBB"<<endl;   
  17.     }   
  18. };   
  19.   
  20. int main(void)   
  21. {   
  22.     BBB b;   
  23.     b.fct();   
  24.   
  25.     return 0;   
  26. }  

  소스코드와 같은 상황에서 "AAA 클래스의 fct 함수는 BBB 클래스에 의해서 오버라이딩 되었다" 라고 한다. B객체를 생성하면 그림과 같은 형태를 가지는데 오버라이딩 되면 A클래스의 fct 함수는 B 클래스의 fct 함수에 의해서 가려지게된다. (이것은 은닉됐다고 한다.)
23번째 줄의 b.fct();는  B클래스의 fct 함수를 호출하게 된다. 따라서, 오버라이딩이라는 특성이 은닉이라는 특성을 보여준다. 그럼 이 은닉된 A 클래스의 fct 함수는 사용할 수 없는 것인가? 그렇지 않다

 보는 시야를 달리하자, 포인터  
 : 바로 이 은닉된것을 보게 하는것이 바로 포인터이다. 포인터를 이용하면 아래 그림과 같이 B객체를 B클래스의 포인터로 가리키면은 B클래스로 바라고, B클래스의 fct 함수를 호출이 가능한것이고, A클래스의 포인터로 B클래스를 가르키면, 그것은 A클래스를 바라보는 것이 된다. 그렇기 때문에 클래스 포인터의 접근권한은 A클래스에 종속이 되는 것이다. 
 그러면 직접 그렇게 되는지 소스 코드를 이용해 알아 보자. 출력 결과를 보면 그렇게 된다는 것을 알 수 있을 것이다. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6. public:   
  7.     void fct(){   
  8.         cout<<"AAA"<<endl;   
  9.     }   
  10. };   
  11.   
  12. class BBB : public AAA   
  13. {   
  14. public:   
  15.     void fct(){   
  16.         cout<<"BBB"<<endl;   
  17.     }   
  18. };   
  19.   
  20. int main(void)   
  21. {   
  22.     BBB* b=new BBB;   
  23.     b->fct();   
  24.   
  25.     AAA* a=b;   
  26.     a->fct();   
  27.   
  28.     delete b;   
  29.     return 0;   
  30. }  

 가상? Virtual  
 : 그럼 멤버 함수를 가상으로 선언하면 어떻게 될까 가상이란 "실재 존재하지 않는것을 존재하는 것처럼 느끼게 하는것" 이다 fct 함수를 virtual로 선언하겠다. 이것은 원래는 이 함수는 없는 건데 내가 있는것 처럼 하겠다. 쉽게 말해 다시 말해 AAA 클래스 fct함수는 우리 눈에는 보이지만, 없는 것으로 해라는 말이다. 그럼 virtual 키워드를 쓰면 어떤 차이가 있는지 알아보자. 
  1. #include <iostream>   
  2. using namespace std;   
  3.   
  4. class AAA   
  5. {   
  6. public:   
  7.     virtual void fct(){   
  8.         cout<<"AAA"<<endl;   
  9.     }   
  10. };   
  11.   
  12. class BBB : public AAA   
  13. {   
  14. public:   
  15.     void fct(){   
  16.         cout<<"BBB"<<endl;   
  17.     }   
  18. };   
  19.   
  20. int main(void)   
  21. {   
  22.     BBB* b=new BBB;   
  23.     b->fct();   
  24.   
  25.     AAA* a=b;   
  26.     a->fct();   
  27.   
  28.     delete b;   
  29.     return 0;   
  30. }  


 메인에서 B클래스의 포인터로 A클래스의 포인터로 가르키고 있는데 a->fct(); 부분에서 A클래스의 포인터를 가르키면 A 클래스로 그 함수 부분을 호출 해야 할텐데 가리키는 곳을 가 봤더니 virtual 로 선언되어 있다. virtual은 없는것으로 치라는 것이다. 없으니까 어떤 것이라도출은 해야 하니 fct를 오버라이딩 하고 있는 BBB 클래스의 FCT 함수가 대신 호출이 일어 난다. 이런 특성이 바로 virtual을 이용한 재정의 이다. 그럼 우리는 다음과 같은 결론을 낼 수 있다. 
오버라이딩은 virtual 키워드는 넣어 주지 않으면 은닉의 특성은 넣어 주면 재정의 특성을 가진다


저작자 표시
Posted by 모과이IT
,