아주 오~~~랜만에 포스팅합니다! 너무 오래되서 전에 무슨 강을 공부했는지도 기억이 가물해서...;; 역시 사람의 망각 곡선은 무시할게 못되죠...ㅠㅠㅠㅠ 이럴때는 천재들이 부럽네요! 무튼 오늘은 virtual 의 원리와 다중상속에 대해서 공부해보도록 하겠습니다.

 

[가상 함수의 기본 원리]

 

 가상 함수가 동작하는 원리에 대해서 간단하게 알아보겠습니다.

 

#include <iostream>

using std::endl;

using std::cout;

 

class A

{

    int a;

    int b;

public:

    virtual void fct1() { cout<<"fct1(...)"<<endl; }

    virtual void fct2() { cout<<"fct2(...)"<<endl; }

};

 

class B : public A

{

    int c;

    int b;

public:

    virtual void fct1() { cout<<" overriding fct1(...)"<<endl; }

    void fct3() { cout<<"fct3(...)"<<endl; }

};

 

int main(void)

{

    A* aaa = new A();

    aaa->fct1();

 

    B* bbb = new B();

    bbb->fct1();

    return 0;

}

 

 먼저 A 클래스를 보면 virtual로 선언된 2개의 가상함수가 존재합니다. 마찬가지로 B 클래스에도 하나의 가상함수가 존재하지요. 중요한 부분은 B 클래스가 A 클래스를 상속했습니다만 fct1() 이라는 함수명이 중복되어 오버라이딩이 됩니다. 당연히 B 클래스 입장에서 fct1() 함수를 호출하면 "overriding fct1(...)"이 출력이 되겠죠? 마찬가지로 B 클래스 입장에서 fct2() 함수를 오출하면 A 클래스를 상속했으니 A 클래스의 fct2() 함수를 호출하게 되겠구요~

 

 즉, B 클래스의 입장에서 보게되면 오버라이딩된 A 클래스의 가상 함수 fct1에 대한 정보가 없기 때문에 B 클래스의 fct1 함수가 대신 호출 되는 것입니다. 이것이 바로 가상 함수의 기본원리 입니다.

 

[다중 상속에 대한 이해]

 

 결론 부터 말씀드리자면 다중 상속은 왠만하면 피해하는게 좋습니다. 이유를 간단하게 얘기하자면 클래스들의 관계가 복잡해지고, 관리하기에도 어렵습니다.

 

#include <iostream>

using std::cout;

using std::endl;

 

class AAA{

public:

   void String1() { cout<< " AAA::String1"<<endl; }

};

 

class BBB{

public:

   void String2() { cout<<" BBB::String2"<<endl; }

};

 

class CCC : public AAA, public BBB{

public:

   void ShowString()

   {

      String1();

      String2();

    }

};

 

int main(void)

{

   CCC ccc;

   ccc.ShowString();

   return 0;

}

 

 CCC 클래스는 AAA 클래스와 BBB 클래스를 동시에 상속하고 있습니다. 결과 값은 예상할 수 있겠죠? 딱히 문제되는 부분 없습니다. 자 여기서 다중 상속을 할 경우 주의해야 할 사항이 몇 가지 있습니다. 다음 예제를 통해서 살펴보도록 하겠습니다.

 

#include <iostream>

using std::cout;

using std::endl;

 

 

class AAA{

public:

void String() { cout<< " AAA::String"<<endl; }

};

 

 

class BBB{

public:

void String() { cout<<" BBB::String"<<endl; }

};

 

 

class CCC : public AAA, public BBB{

public:

  void ShowString()

  {

    String();

    String();

   }

};

 

 

int main(void)

{

    CCC ccc;

    ccc.ShowString();

    return 0;

}

 

 AAA 클래스와  BBB 클래스가 지니고 있는 맴버 함수의 이름이 똑같습니다. 때문에 컴파일 에러가 발생하게 됩니다. 무엇을 호출해야될지 모르기 때문이죠. 때문에 다음과 같이 변경해주어야 합니다.

 

 

 String() ----> AAA::String();

 String() ----> BBB::String();

 

위와 같은 다중상속의 모호성은 이렇게 쉽게 해결해보았습니다.

 

#include <iostream>

using std::cout;

using std::endl;

 

 

 

class AAA{

public:

void String1() { cout<< " AAA::String"<<endl; }

};

 

 

 

class BBB : public AAA{

public:

void String2() { cout<<" BBB::String"<<endl; }

};

 

 

 

class CCC : public AAA{

public:

void String3() { cout<<" CCC::String"<<endl; }

};

 

class DDD : public BBB, public CCC{

public:

   void ShowString()

   {

      String1();

      String2();

      String3();

    }

};

 

 

 

int main(void)

{

   DDD ddd;

   ddd.ShowString();

 

   return 0;

}

 

 DDD 클래스가 결과적으로 AAA 클래스를 두번이나 상속하게 되어있습니다. 이게 바로 문제가 되는 부분입니다. 즉 AAA 클래스의 맴버 함수 String1 을 BBB 클래스를 통해서, 그리고 CCC 클래스를 통해서 상속받고 있게되죠. DDD 클래스의 볼드 처리된 부분을 보시면 이 String1이 BBB 클래스를 통해서 상속받은 함수인지 CCC 클래스를 통해서 상속받은 함수인지 어떠한 함수를 호출할지 몰라서 결국 컴파일 에러를 발생시킵니다.

 

 이러한 문제는 virtual 상속을 통해서 간단하게 해결 할 수 있습니다.

 

class BBB : virtual public AAA{

public:

void String2() { cout<<" BBB::String"<<endl; }

};

 

 

 

 

class CCC : virtual public AAA{

public:

void String3() { cout<<" CCC::String"<<endl; }

};

 

 둘다 모두 AAA 클래스를 virtual 상속하고 있습니다. 반드시 둘다 virtual 상속을 해야합니다. 이제부터는 BBB 클래스와 CCC 클래스를 다중 상속한다 하더라도, AAA 클래스 안에 존재하는 맴버들은 한번만 상속이 이뤄지게 됩니다. 이것이 바로 virtaul 상속을 하는 이유입니다.

 

 결론은 다중 상속은 클래스의 관계를 복잡하게 흐려놓습니다. 다중 상속으로 밖에 해결이 안 되는 문제도 존재하지 않는다고 하니까 가급적이면 사용하지 않는 편이 좋다고하죠^^;; 오랜만에 포스팅하니까 현기증이..... 이번강은 여기서 마치도록 하겠습니다.

 

 

8강에 이어서 상속과 다형성에 대하여 계속 살펴보겠습니다.

 

[Static Binding 과 Dynamic Binding]

 

 *오버라이딩*

 

#include <iostream>

using namespace std;

 

class AAA

{

   public:

     void fct()

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

    BBB b;

    b.fct();

    return 0;

 }

 

 위 예제를 보시면 Base 클래스인 AAA의 맴버 함수로 fct()가 존재하고 AAA를 상속하는 Derived 클래스 BBB도 맴버 함수로 fct()가 정의되어 있습니다. 그러나 위 예제를 실행시켜보면 컴파일 오류없이 정상적으로 "BBB"가 출력되는걸 알 수 있습니다.

 

 이처럼 Base 클래스에 선언된 형태의 함수를 Derived 클래스에서 다시 선언하는 현상을 가리켜 오버라이딩(overriding)이라고 합니다. 즉 오버라이딩은 이전에 정의된 함수를 숨기는 특성을 지닌다고 이야기할 수 있습니다. 아 그리고 오버로딩과 오버라이딩을 혼돈해서는 안됩니다! 기억이 나지 않으시면 복습하세요~

 

 그렇다면 상속되면서 오버라이딩된 함수는 호출이 불가능해진걸까요? 또 그건 아닙니다. 다음예제를 보겠습니다.

 

class AAA

{

   public:

     void fct()

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new BBB;

   b->fct();

 

   AAA* a = b;

   a->tct();

 

   delete b;

   return 0;

}

 

--->결과값

BBB

AAA

 

 볼드처리된 부분에서는 BBB객체를 생성하고 있고, 포인터 b를 이용해서 fct 함수를 호출하고 있습니다. BBB 객체를 BBB 포인터로 바라볼 경우 BBB 클래스 내에 선언된 맴버 함수 fct만 보이게됩니다. 때문에 첫 번째 함수 호출에서는 "BBB"가 출력이 되겠죠? 그리고나서 포인터 b가 지니고 있는 주소값을 포인터 a에 대입해줍니다. 포인터형이 서로다른대 가능하냐구요? 당연히 가능하겠죠? 이유는 8강에서 설명했을겁니다. BBB 클래스는 AAA 클래스를 상속하기 때문에 AAA* 는 AAA와 BBB 객체의 주소값 모두를 표현할 수 있습니다. 그리고 마지막으로 포인터 a를 가지고 fct()함수를 호출하고 있습니다.

 

 이와 같이 오버라이딩되어 가려진 함수지만 AAA의 포인터로 접근할 경우에는 AAA 클래스의 fct함수가, BBB의 포인터로 접근할 경우에는 BBB 클래스의 fct함수가 호출됩니다. 호출되지 않는 함수가 가려지는거겠구요~

 

*맴버함수의 가상선언*

 

 virtual 키워드를 통해 가상으로 선언하는 방법을 살펴보겠습니다.

 

class AAA

{

   public:

     virtual void fct() // 가상함수

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new BBB;

   b->fct();

 

   AAA* a = b;

   a->tct();

 

   delete b;

   return 0;

}

 

 virtual 키워드를 사용함으로써 fct()함수를 가상 함수로 선언해주었습니다. 그러나 실행결과는

 

BBB

BBB

 

 가 출력이 됩니다. 아까 전과는 조금 다른 결과값을 나타내죠? AAA 타입의 포인터로 객체를 가리키건, BBB 타입의 포인터로 객체를 가리키건 오버라이딩된 함수만 호출되고 있습니다. 위 예제에서는 AAA 클래스에 선언되어 있는 fct 함수를 가상의 함수, 즉 존재하지 않는 함수로 선언하고 있습니다.

 

class AAA

{

   public:

     virtual void fct() 

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct() //virtual void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

class CCC : public BBB

{

  public:

     void fct() //virtual void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new CCC;

   b->fct();

 

   AAA* a = b;

   a->fct();

 

   delete b;

   return 0;

}

 

---->결과값

CCC

CCC

 

 AAA 클래스의 fct 함수는 virtual 선언되어 있고, BBB 클래스의 fct 함수가 오버라이딩 하고있습니다. 이런 경우 BBB 클래스의 fct 함수도 가상함수가 됩니다. 즉 BBB 클래스 선언에서의 void fct() 가 virtual void fct()가 되는겁니다. 마찬가지로 CCC 클래스의 fct 함수가 BBB 클래스의 fct 함수를 오버라이딩 하고 있습니다. 왜냐하면 BBB 클래스의 fct 함수가 가상함수이기 때문이죠!

 

 이렇게되면 최종적으로 오버라이딩한 함수를 제외한 나머지 함수는 가려지게 됩니다. AAA, BBB에 있는 함수는 가려지게되고 CCC 클래스에 선언된 fct 함수만 보이게 됩니다. b->fct() 에서는 BBB 타입의 포인터를 가지고 fct 함수를 호출하고 있습니다. 따라서 BBB 클래스의 맴버 함수 fct 를 호출하는 데서부터 시작은 이뤄집니다. 다만 함수가 가상함수이므로 오버라이딩한 함수가 호출이 되겠죠? 

 

 자 다시한번 정리해봅시다. 만약 BBB 클래스의 포인터형(BBB*)으로 b를 선언하고 b->fct() 를 호출하면 BBB 에있는 fct()가 호출이 되어야 하지만 virtual 함수이므로 BBB의 fct() 대신 CCC의 fct() 함수가 호출됩니다. 마찬가지로 a->fct() 를 호출하면 AAA의 fct() 대신 BBB의 fct()가 호출되는대 이 함수도 virtual 함수이므로 다시 CCC 클래스의 fct()함수가 호출됩니다.

 

 [static binding 과 dynamic binding]

 

 스태틱 바인딩과 다이나믹 바인딩이 무엇인지 우리는 이미 알고 있습니다.

 

class AAA

{

  public:

    virtual void fct()

    {

      cout<<"AAA"<<endl;

    }

};

 

class BBB : public AAA

{

  public:

    void fct()

    {

       cout<<"BBB"<<endl;

    }

};

 

int main(void)

{

  BBB b;

  b.fct(); //static binding

 

  AAA* a = new BBB;

  a->fct(); //dynamic binding

  return 0;

}

 

 b.fct() 의 경우 BBB 클래스에 선언되어 있는 fct 함수가 호출됩니다. 하지만 a->fct() 함수는 상황에 따라서 달라집니다. 포인터 a가 가리키는 것이 AAA 객체라면 AAA클래스의 fct 함수가 호출되겠지만, BBB 객체라면 BBB 클래스의 fct 함수가 호출이 될것입니다. 왜냐하면 AAA클래스의 맴버 함수 fct가 virtual로 가상함수 선언이 되었기 때문이죠. 이러한경우 static binding, dynamic binding 이라고 정의합니다. 이러한 dynamic binding은 다형성의 하나의 예 입니다. 쉽게 얘기하자면 모습은 같은데 형태는 다른겁니다. a라는 이름의 포인터가 가리키는 대상에 따라서 기능의 형태가 다르게 나타나는거죠. 이러한 다형성의 예를 하나 더 들어보자면 ' * ' 연산자가 있을겁니다. 상황에 따라서 곱셈 연산자로 또는 포인터 연산자로 사용하기 때문입니다.

 

 

 이번에는 범위 지정 연산자(::)를 통해서 오버라이딩된 함수를 호출해보는 예제를 살펴봅시다.

 

class AAA

{

   pulbic:

      virtual void fct()

       {  cout <<"AAA"<<endl; }

};

 

class BBB : public AAA

{

    public:

       void fct()

          {

             AAA::fct();

             cout<<"BBB"<<endl;

           }

};

 

int main(void)

{

   AAA* a = new BBB;

   a->fct();

 

   cout<<endl;

 

   a->AAA::fct();

   return 0;

}

 

 결과값 --->

AAA

BBB

 

AAA

 

 첫 번째 볼드처리된 부분에서의 함수 호출은 AAA 클래스에 선언되어 있는 fct 함수를 호출하라는 의미입니다. 따라서 BBB 클래스의 fct() 함수가 호출되면 AAA 클래스의 fct() 함수도 호출이 될겁니다.

 두 번째 볼드처리된 부분의 형태로도 Base 클래스에 존재하는 오버라이딩된 함수의 호출이 가능합니다. 결과값을 확인해보시길 바랍니다~

 

 자 우리는 앞서 얘기한 Employee Problem 문제를 virtual 함수를 이용하여 해결할 수 있습니다. 수정된 부분은 다음과 같습니다.

 

class Employee

{

  protected:

    char name[20];

  public:

    Employee(char* _name);

    const char* GetName();

    virtual int GetPay()

    {  return 0;  }

};

Employee::Employee(char* _name)

{

  strcpy(name,_name);

}

const char* Employee::GetName()

{

   return name;

}

 

 단지 GetPay 함수가 가상 함수로 선언되어 추가로 들어간 것 밖에 없습니다. 그러나 우리는 급여 리스트 출력 부분에서 주석처리된 부분 ( cout<<"salary: "<<empList[i]->GetPay()<<endl; ) 을 이러한 방법으로 해결 할 수 있습니다. Department 클래스에 선언되어 있는 empList 는 Employee 포인터 배열이지만 정작 가리키고 있는 대상들은 Permanent 객체이거나 Temporary 객체 였습니다. 문제는 Permanent 객체와 Temporary 객체는 GetPay 함수를 지니고 있지만, Employee 클래스에는  GetPay 함수가 선언되어 있지 않다는 문제 때문에 결국 컴파일 에러를 발생시킨 것 이었죠.

 

 즉, 가상 함수로 선언했기 때문에 Employee 클래스의 GetPay 함수가 호출되는 것이 아니라, 오버라이딩한 Derived 클래스의 GetPay 함수가 호출됩니다.

 

class Employee

{

  protected:

    char name[20];

  public:

    Employee(char* _name);

    const char* GetName();

    virtual int GetPay()=0;

};

Employee::Employee(char* _name)

{

  strcpy(name,_name);

}

const char* Employee::GetName()

{

   return name;

}

 

 위와 같이 선언되어 있는 형태를 순수가상함수라고 하는데 별건 아닙니다. 단지 컴파일러에게 GetPay 함수는 호출될 일이 없고, 일부러 선언만하고 정의는 하지 않는 함수라고 알려주는 겁니다.

 

[virtual 소멸자]

 

 가상함수 말고도 소멸자에 virtual 키워드를 사용할 수 있습니다. 과연 어떠한 상황에서 소멸자에 virtual 키워드를 사용해야 할까요? 다음 예제를 살펴보도록 하겠습니다.

 

class AAA

{

  char* str1;

public:

  AAA(char* _str1)

  {

     str1 = new char[strlen(_str1)+1];

     strcpy(str1, _str1);

   }

   ~AAA()

  {

    cout<<"~AAA() call "<<endl;

    delete []str1;

   }

   virtual void ShowString()

   {

     cout<<str1<<' ';

    }

};

 

class BBB : public AAA

{

   char* str2;

public:

   BBB(char* _str1, char* _str2) : AAA(_str1)

   {

      str2 = new char[strlen(_str2)+1];

      strcpy(str2, _str2);

   }

   ~BBB()

   {

     cout<<"~BBB() call"<<endl;

     delete []str2;

   }

   virtual void ShowString()

   {

       AAA::ShowString();

       cout<<str2<<endl;

    }

};

 

int main()

{

   AAA* a = new BBB("Good", " evening");

   BBB* b = new BBB("Good", "morning");

 

   a->ShowString();

   b->ShowString();

 

   delete a;

   delete b;

   return 0;

}

 

 결과값---->

Good evening

Good morning

~AAA() call

~BBB() call

~AAA() call

 

 조금만 주의깊게 살펴보시면 위 코드를 이해하는데 별다른 무리는 없을겁니다. 결과값을 보시면 BBB 클래스의 소멸자가 한번 덜 호출되었다는 것을 알 수 있습니다. 볼드 처리된 부분이 문제입니다. AAA 타입 포인터 a로 BBB 객체를 가리키고 있습니다. 그리고 포인터 a를 통한 BBB 객체의 소멸을 시도하고 있습니다. 이러한 과정에서 문제가 생긴겁니다. 왜냐하면 포인터 a가 가리키는 객체는 BBB 객체이지만 AAA타입의 포인터로 가리키고 있기 때문에 컴파일러는 BBB 객체를 AAA 객체로 인식하여 AAA 클래스의 소멸자만이 호출된겁니다.

 

 이러한 문제의 해결법은 간단합니다.

 

virtual ~AAA()

{

  cout<<"~AAA() call"<<endl;

  delete []str1;

}

 

으로 변경해주면 됩니다.

 

변경 후의 결과값은

 

Good evening

Good morning

~BBB() call

~AAA() call

~BBB() call

~AAA() call

 

 일단 AAA 클래스의 소멸자를 호출하려 할겁니다. 그러나 소멸자가 가상 함수로 지정되었으므로 Derived 클래스의 소멸자를 우선 호출하게 됩니다. 그 다음에는 BBB 클래스의 소멸자는 AAA 클래스를 상속하고 있기 때문에 다시 AAA 클래스의 소멸자가 호출됩니다. 이해되셨죠?

 

 이번 강은 여기까지 입니다. 다음 강에서는 virtual 의 추가적인 내용과 다중 상속을 공부해보도록 하겠습니다~!

 

 

 

 저번 강의에 이어서 상속에 관하여 조금 더 공부해보도록 하겠습니다.

 

[상속된 객체와 포인터]

 

*객체 포인터 : 객체의 주소 값을 저장할 수 있는 포인터*

 

 AAA 라는 클래스가 있다면 포인터 AAA* 는 AAA 객체의 주소 값과 AAA 클래스를 상속하는 Derived 클래스 객체의 주소 값도 저장이 가능합니다. 여기서 AAA 클래스의 포인터를 가리켜 객체 포인터라고 정의하게되죠. 아래 예제를 살펴보도록 하죠.

 

#include <iostream>

 using namespace std;

 

class Person

{

 public:

   void Sleep()

   {

      cout << "Sleep" <<endl;

    }

 };

 

class Student : public Person

{

  public:

    void Study()

    {

       cout << "Study" <<endl;

     }

  };

 

class PartTimeStd : Public Student

{

  public:

     void Work()

     {

        cout<<"Work"<<endl;

     }

};

 

int main(void)

{

  Person* p1 = new Person;

  Person* p2 = new Student;

  Person* p3 = new PartTimeStd;

 

  p1->Sleep();

  p2->Sleep();

  p3->Sleep();

 

 return 0;

}

 

 볼드처리된 부분을 살펴보면 p1에서는 딱히 궁금한점은 보이지 않을꺼구요~ p2는 Student 클래스가 Person 클래스를 상속하고 있으므로 문제가 되지 않으며 Person 포인터는 Student 객체의 주소값을 표현할 수 있으므로 역시나 문제가 되지 않습니다. p3도 마찬가지로 Person클래스를 상속하게 되므로 문제가 없겠죠? Student 클래스를 상속하긴 하지만 다시 Student 클래스는 Person 클래스를 상속하므로 결국엔 PartTimeStd 클래스도 Person 클래스를 상속하게 되는겁니다.

 

 이러한 예제를 조금 논리적으로 해석해도록 하겠습니다. Student 객체는 Student 객체이자 동시에 Person 객체가 되는 것 입니다. 왜냐하면 "Student는 Person이다" 라고 얘기할 수 있습니다. 그래서 다음과 같이 "Student 클래스의 객체는 Person 클래스 객체이다" 라고 말 할 수 있겠죠? 따라서 Person 타입의 포인터로 Student객체의 주소값을 저장할 수 있는겁니다. 다른말로 하자면 Person 타입의 포인터로 Student 객체를 가리킬 수 있다는 의미죠.

 

 마찬가지로 PartTimeStd 클래스의 객체는 PartTimeStd 객체이자, Student 객체이면서 동시에 Person 객체도 되는 것 입니다. 따라서 PartTimeStd 타입의 포인터 뿐만 아니라 Person 타입의 포인터, Student 타입의 포인터로도 가리키는 것이 가능합니다.때문에 다음과 같은 문장은 모두 올바르게 실행됩니다.

 

int main(void)

{

  Person* p1 = new PartTimeStd;

  Student* p1 = new PartTimeStd;

  PartTimeStd* p1 = new PartTimeStd;

 

  p1->Sleep();

  p2->Sleep();

  p3->Sleep();

  return 0;

 }

 

 자 여기서 하나 살펴보고 넘어갈 문제가 있습니다. 저번장에서 설명드렸습니다만 Employee Problem 생각 나시죠? 그 문제를 지금까지 배운 내용을 토대로 수정해보도록 하겠습니다. 물론 완성본은 아닙니다^^

 

class Employee

{

  protected:

   char name[20];

  public:

   Employee(char * _name);

   const char* GetName();

};

Employee::Employee(char* _name)

{

  strcpy(name,_name);

}

char* Employee::GetName()

{

   return name;

}

 

class Permanent : public Employee

{

  private:

    int salary;

  public:

    Permanent(char* _name, int sal);

    int GetPay();

};

Permanent::Permanent(char* _name, int sal) : Employee(char* _name)

{

  salary = sal;

}

int Permanent::GetPay()

{

  return salary;

}

 

class Department

{

  private:

    Employee* empList[10];

    int index;

  public:

    Department(): index(0) {};

    void AddEmployee(Employee* emp);

    void ShowList();

 };

void Department::AddEmployee(Employee* emp)

{

   empList[intdex++]=emp;

}

void Department::ShowList()

{

  for(int i = 0 ; i < index ; i++)

  {

    cout<<"name:"<<empList[i]->GetName()<<endl;

    cout<<"salary:"<<empList[i]->GetPay()<<endl; // 컴파일 오류!

   }

 }

 

class Temporary : public Employee

{

 private:

   int time;

   int pay;

 public:

   Temporary(char* _name, int _time, int _pay);

   int GetPay();

};

Temporary::Temporary(char* _name, int _time, int _pay) : Employee(_name)

{

   time = _time;

   pay = _pay;

}

int Temporary::GetPay()

{

   return time*pay; //곱셈연산

}

 

 일단은 볼드 처리된 부분을 살펴보시길 바랍니다. 상속하는 부분은 익히 아실거고 (Employee*) 부분은 전혀 문제가 되지 않음도 알겠지요? Permanent 클래스 Employee 클래스를 상속하고 있으므로, Employee 타입의 포인터는 Permanent 객체를 가리킬 수 있겟죠.

 

 볼드처리된 부분중에 컴파일 오류가 생기는 부분 보이시죠? 이 문제의 원인은 객체 포인터의 권한에 있습니다. 이 부분을 조금 더 자세하게 다루기 위해서 아까 사용했던 예제들을 조금 변경해서 살펴보도록 하겠습니다.

 

class Person

{

 public:

   void Sleep()

   {

      cout << "Sleep" <<endl;

    }

 };

 

class Student : public Person

{

  public:

    void Study()

    {

       cout << "Study" <<endl;

     }

  };

 

class PartTimeStd : Public Student

{

  public:

     void Work()

     {

        cout<<"Work"<<endl;

     }

};

 

int main(void)

{

  Person* p3 = new PartTimeStd;

 

  p3->Sleep();

 

  p3->Study(); //컴파일 오류

  p3->Work(); //컴파일 오류

 

 return 0;

}

 

 

 포인터 p3가 가리키는 객체의 Sleep 함수를 호출하고 있습니다. p3가 가리키는 객체는 partTimeStd 객체이므로 Sleep 함수가 존재합니다. 따라서 호출이 가능하겠죠? 그러면 컴파일 오류가 발생하는 2,3 번째 문장은 각각 p3가 가리키는 객체의 Study 함수와 Work 함수를 호출하고 있습니다. 분명 p3는 PartTimeStd 객체이므로 문제될 것이 없어보이죠? 그러나 컴파일 에러가 발생합니다.

 

 객체 포인터의 특성때문에 위와 같은 문제가 발생했습니다. 객체 포인터의 특성이란? AAA 클래스의 객체 포인터는 가리키는 대상이 어떠한 객체이건, AAA 클래스 타입 내에 선언된 맴버와 AAA클래스가 상속한 클래스의 맴버에만 접근이 가능하다! 라는 성질을 가지고 있습니다. 조금 더 쉽게 얘기하자면, 포인터 p3가 무엇을 가리키건,p3는 Person 타입의 포인터이므로 Person 클래스 내에 선언된 맴버에만 접근이 가능하다는 것입니다.

 

 때문에 아까

 cout<<"salary:"<<empList[i]->GetPay()<<endl;

 이러한 부분에서도 객체 포인터의 특성때문에 컴파일 에러가 발생하는 것입니다. empList[i]는 Employee 타입의 포인터이지만 GetPay 함수는 Employee 클래스의 맴버 함수가 아니고 Permanent 클래스의 함수이기 때문입니다. 따라서 접근할 수 있는 권한이 없는겁니다.

 

[상속된 객체와 참조]

 

이번에는 상속된 객체와 참조의 관계를 살펴보겠습니다.

 

*객체 레퍼런스 : 객체를 참조할 수 있는 레퍼런스*

 

마찬가지로 AAA 클래스의 레퍼런스(AAA&)는 AAA객체 뿐만 아니라, AAA클래스를 상속하는 Derived 클래스의 객체도 참조 가능합니다. 다음 예제를 통해서 설명하도록 하겠습니다.

 

class Person

{

 public:

   void Sleep()

   {

      cout << "Sleep" <<endl;

    }

 };

 

class Student : public Person

{

  public:

    void Study()

    {

       cout << "Study" <<endl;

     }

  };

 

class PartTimeStd : Public Student

{

  public:

     void Work()

     {

        cout<<"Work"<<endl;

     }

};

 

int main(void)

{

   PartTimeStd p;

   Student& ref1 = p;

   Person& ref2 = p;

  

   p.Sleep();

   ref1.Sleep();

   ref2.Sleep();

   return 0;

 }

 

 ref1, ref2 라는 이름을 p 객체에 부여하고 있습니다. 레퍼런스란 이름을 하나 더 부여하는 행위와 같다고 설명했었죠? 따라서 p, ref1, ref2 모두 같은 객체입니다. 객체 p 는 Student 클래스와 Person 클래스를 상속하고 있기 때문에 볼드처리된 부분이 문제될건 없습니다.

 

*레퍼런스의 권한*

 : AAA 클래스의 레퍼런스는 참조하는 대상이 어떠한 객체이건, AAA클래스 타입 내에 선언된 맴버와 AAA클래스가 상속한 클래스의 맴버에만 접근이 가능하다.

 

자 그렇다면 다음과 같은 예제는 어떻게 될까요?

 

itn main(void)

{

  PartTimeStd p;

  p.Sleep();

  p.Study();

  p.Work();

 

  Person& ref = p;

  ref.Sleep();

  ref.Study(); //컴파일 에러

  ref.Work(); //컴파일 에러

  return 0;

}

 

 Person 클래스는 맴버로 Study와 Work를 가지고 있지않죠? 충분히 이해가능하신 내용이라 추가 설명없이 넘어가도록 하겠습니다!

 

 이번강의는 여기서 마치도록 하구요 다음 강의는 다소 이해하기 어려운 내용일 수 있어서 이번 강의을 충분히 이해하시고 9강을 공부하시길 바랍니다!

 

 

 오늘은 C++언언에서의 상속에 대한 내용을 공부해보도록 하겠습니다. 상속의 기본개념부터 생성 및 소멸 과정, 다양한 형태의 상속을 배워보겠습니다~!! 상속을 공부하기 전에 몇가지 이야기를 해보도록 하겠습니다.

 

다음 예제는 급여 관리 시스템의 프로그램 코드입니다. 이 시스템에서의 직원의 근무 형태는 오직!!! 고용직(Permanent)하나 뿐 입니다. 클래스는 이름과 급여정보 정도를 저장할 수 있도록 간단히 정의하였습니다.

 

class Permanent

{

  char name[10];

  int salary;

public:

  Permanent(char* _name, int sal);

  const char*  GetName();

  int GetPay();

};

 

Permanent::Permanent(char* _name, int sal)

{

  strcpy(name, _name);

  salary = sal;

}

 

const char* Permanent::GetName()

{

  return name;

}

 

int Permanent::GetPay()

{

  return salary;

}

 

 다음은 위에서 정의한 Permanent 객체를 저장하고 관리하기 위한 클래스입니다.

 

class Department

{

  Permanent*  empList[10];

  int index;

public:

  Department() : index(0) {};

  void AddEmployee(Permanent* emp);

  void ShowList();

};

 

void Department::AddEmployee(Permanent* emp)

{

  empList[index++] = emp;

}

void Department::ShowList()

{

  for(int i = 0; i < index; i++)

  {

    cout<<"name:"<<empList[i]->GetName()<<endl;

    cout<<"salary:"<<empList[i]->GetPay()<<endl;

    cout<<endl;

  }

}

 

 다음은 메인 함수 입니다.

 

int main()

{

   Department department;

 

   department.AddEmployee(new Permanent ("box", 1000));

   department.AddEmployee(new Permanent ("bop", 2000));

 

   department.ShowList();

   return 0;

 }

 

 위의 코드들은 잘 이해하셨으리라 생각됩니다. 그렇다면 이러한 프로그램을 잘 사용하고 있다가 회사 업무의 형태가 바뀌어서 프로그램 기능의 변경을 요구할 수도 있고, 추가적인 기능을 요구하게되면 프로그램의 변경 및 확장이 수월해야 합니다. 즉, 객체지향에서 중요시하는 것 중 하나는 요구사항의 변화에 따른 프로그램 유연성입니다.

 

 자 그럼 회사가 부서도 많아지고, 직원도 늘어나게 되었습니다. 첫 번째로 고용의 형태를 늘려봅시다. 새로운 고용형태는 판매직(Sales Person), 임시직(Temporary)입니다. 또한 급여제도는 기본급과 인센티브제도를 도입했다고 가정합시다.

 

                       <급여 계산방식>

고용직 : 기본급여

판매직 : 기본급여 + 인센티브

임시직 : 일한 시간 X 시간당 급여

 

우리가 하고자하는 작업은 전혀 단순한게 아닙니다. 생각해보세요. 판매직과 임시직의 등장으로 기존에 정의되어 있던 Department 클래스가 변경되어야 합니다. 또한 판매직, 임시직의 객체 저장을 위하여 배열을 늘려야하고 배열의 인덱스 정보를 담고 있는 맴버 변수도 늘려야합니다. 또한 AddEmployee 기능도 판매직과 임시직을 위해 추가되어야 합니다. 제가 하고자 하는 이야기는 임시직과 판매직 클래스를 등장시켜도 기존에 있던 클래스에 전혀 영향을 미치지 않은 방법. 다시 말해서 새로운 클래스가 추가되어도 프로그램의 다른 영역에는 전혀 변경이 가해지지 않는 방향을 추구해야된다는 말입니다.

 

 이러한 문제를 Employee Problem 이라고 하도록하겠습니다. Employee Problem을 해결하기 위한 방법은 천천히 설명드리도록 하겠구요 해결하기 위해 필요한 요소들을 지금부터 설명하도록 하겠습니다.

 

[상속의 개념]

 

 "Student 클래스가 Person 클래스를 상속한다" 를 생각해봅시다. 학생은 학생이기전에 사람이라는 하나의 객체적인 성격을 나타내고 있습니다. 즉, Student 클래스는 Person 클래스가 지니고 있는 모든 맴버 변수와 맴버 함수를 물려받아야 합니다. 다음 예제를 통해서 이해해보도록 하겠습니다.

 

class Person

{

  int age;

  char name[20];

public:

  int GetAge()

 {

    return age;

  }

  char* GetName()

 {

   return name;

  }

 Person(int _age=1, char* _name="noname")

 {

   age = _age;

   strcpy(name, _name);

  }

};

 

class Student : public Person

{

  char major[20];

public:

  Student(char* _major)

 {

   strcpy(major, _major);

  }

  char* GetMajor()

 {

   return major;

  }

  void ShowData()

 {

   cout<<"name : "<<GetName()<<endl;

   cout<<"age : "<<GetAge()<<endl;

   cout<<"major : "<<GetMajor()<<endl;

  }

};

 

 위 예제에서 볼드 처리된 class Student : public Person 부분은 Student 클래스가 Person클래스를 Public 상속한다는 선언입니다. Public 상속에 관해서는 조금 있다가 설명드리도록 하겠습니다. 이제 상속을 했으니 Student 클래스는 Person 클래스를 상속하므로 Student 객체는 Person 클래스에 선언되어 있는 맴버를 모두 포함하게 되었습니다. 때문에 Person 에서 정의한 맴버 변수나 맴버 함수를 Student 클래스에서 그대로 사용할 수 있게되었죠.

 그리고 이제부터는 Person 처럼 상속되는 클래스를 Base 클래스, Student 클래스처럼 상속하는 클래스를 Derived 클래스라고 표현하도록 하겠습니다.

 

[객체의 생성 및 소멸]

 

#include <iostream>

using namespace std;

 

class AAA

{

public:

  AAA()

  {

     cout<<"AAA() call"<<endl;

   }

  AAA(int i)

  {

     cout<<"AAA(int i) call"<<endl;

   }

};

 

class BBB: public AAA

{

public:

  BBB()

  {

     cout<<"BBB() call"<<endl

   }

   BBB(int j)

  {

     cout<<"BBB(int j) call"<<endl;

   }

};

 

int main(void)

{

  cout<<"객체 1 생성"<<endl;

  BBB bbb1;

 

  cout<<"객체 2 생성"<<endl;

  BBB bbb2(2);

 

  return 0;

}

 

 

 *실행결과----->

객체 1 생성

AAA() call

BBB() call

객체 2 생성

AAA() call

BBB(int j) call

 

"실행결과만 봐도 아~ 대충 이렇게 생성이되는구나!" 라는 감이 오지않나요? BBB bbb1 을 생성할때는 인자가 없으므로 void 생성자가 호출될 것 이고, BBB bbb2(2) 를 생성할때는 인자가 있으므로 인자 값을 받을 수 있는 생성자를 호출할 것 입니다.

 

 객체생성의 기본순서를 살펴보겠습니다.

제일 먼저 메모리 공간을 할당하는 것이 먼저입니다. 위 예제 같은 경우는 BBB의 맴버만을 고려하는 것이 아니라, BBB가 상속하고있는 AAA의 맴버까지 고려해서 메모리 공간이 할당됩니다.

그 다음으로는 메모리 공간을 할당했으니, 생성자를 호출합니다. BBB의 경우 BBB클래스의 생성자를 호출하겠죠? 이렇게 생성자를 호출하고 생성자의 몸체 부분을 실행하려고 보니까 AAA클래스를 상속하고 있는게 아닙니까? 때문에 BBB 클래스의 생성자가 호출은 되었지만, 몸체 부분이 실행되지 않습니다. 왜냐하면 BBB 클래스가 상속하고 있는 AAA클래스의 생성자가 우선적으로 실행되어야 하기때문입니다. 생각보다 중요한 과정입니다. 그렇다면 AAA() 와 AAA(int i) 생성자 중에서 어떤 것이 호출될까요? 별 다른 선언이 존재하지 않는 이상 기본적으로 Base 클래스의 void 생성자가 호출됩니다. 그럼 별 다른 선언은 어떤 것일까 하는 궁금증이 생기기 마련입니다. 다음 선언을 봅시다!

 

 BBB(int j) : AAA(j)

{

   cout<<"BBB(int j) call"<<endl;

}

 

이러한 선언에서는 AAA(int i) 생성자가 호출 될겁니다. 어디서 본 것 같은 느낌이 들지 않나요? 바로 맴버 이니셜라이져 선언입니다. AAA맴버를 j로 초기화 하라는 의미입니다. 즉, j를 인자로 받을 수 있는 AAA클래스의 생성자를 호출하라! 라는 의미죠.

 

마지막으로 메모리 공간 할당도 끝났고, Base 클래스의 생서자 호출 및 실행도 끝이 났으니 Derived 클래스의 생성자가 호출되고 실행되며 끝이 납니다. 총 3단계로 나눠 이야기할 수 있습니다.

 

자 그럼 아까 Studen와 Person 클래스를 위에서 배운 과정을 이용해 좀 더 프로페셔널하게 수정해봅시다.

 

 

class Person

{

  int age;

  char name[20];

public:

  int GetAge()

 {

    return age;

  }

  char* GetName()

 {

   return name;

  }

 Person(int _age=1, char* _name="noname")

 {

   age = _age;

   strcpy(name, _name);

  }

};

 

class Student : public Person

{

  char major[20];

public:

 

 Student(int _age, char* _name, char* _major)

 {

    age = _age;    //컴파일 에러

    strcpy(name, _name);  //컴파일 에러

    strcpy(major, _major);

  }

 

  char* GetMajor()

 {

   return major;

  }

  void ShowData()

 {

   cout<<"name : "<<GetName()<<endl;

   cout<<"age : "<<GetAge()<<endl;

   cout<<"major : "<<GetMajor()<<endl;

  }

};

 

int main(void)

{

  Student Box(20, "Boxbop" , "computer");

  Box.ShowData();

 

  return 0;

}

 

 볼드 처리된 첫 번째 부분을 봅시다. 자신이 상속하고 있는 Person 클래스의 맴버 변수를 초기화하는 선언입니다. 그러나 조금 이상하지 않나요? 분명히 Person 클래스에서  age와 name은 private로 설정해놨습니다만 Student 클래스에선 그냥 사용하고 있죠. 그렇다고 age와 name을 public 으로 선언하는 것은 좋지 않습니다. 때문에 다음과 같은 과정이 필요합니다.

 

Student(int _age, char* _name, char* _major) : Person(_age, _name)

 {

        strcpy(major, _major);

  }

 

여기서는 Person 클래스의 생성자를 명시적으로 호출하고 있습니다. Student 클래스 생성자의 첫 번째와 두 번째 인자로 전달된 값들은 그대로 Person 클래스의 생성자를 호출하는데 사용하고 있습니다. 게다가 Person 클래스의 생성자는 pulbic 으로 선언되어 있기 때문에 아무문제가 없습니다. 이제 정보 은닉을 무너뜨리지 않고, 객체 생성 시 모든 맴버 변수들을 초기화 할 수 있게 되었습니다.

 

 이번에는 객체의 소멸과정이 어떻게 이루어지는지 알아봅시다.

 

class AAA

{

public:

  AAA()

  {

     cout<<"AAA() call"<<endl;

   }

  ~AAA()

  {

     cout<<"~AAA() call"<<endl;

   }

};

 

class BBB: public AAA

{

public:

  BBB()

  {

     cout<<"BBB() call"<<endl

   }

   ~BBB()

  {

     cout<<"~BBB() call"<<endl;

   }

};

 

int main(void)

{

 BBB bbb; 

  return 0;

}

 

*실행결과---->

AAA() call

BBB() call

~BBB() call

~AAA() call

 

객체의 생성 과정과 달리 Derived 클래스의 소멸자가 먼저 실행이 되고, Base 클래스의 소멸자가 그 다음으로 실행됩니다. 마찬가지로 상속하고 있는 클래스의 객체는 자신이 상속하고 있는 클래스의 소멸자까지도 호출하고 있습니다.

 

[private, protected, public 맴버]

 

protected 맴버는 외부에서 보면 private 으로 보이고, 상속 관계에서 보면 public 으로 보입니다. 즉 protected 맴버는 private 맴버와 똑같습니다. 다만, private 맴버와 달리 상속 관계에 놓여 있는 경우에 접근을 허용할 뿐입니다.

 

 class AAA

{

  private:

    int a;

  protected:

    int b;

};

 

class BBB : public AAA

{

   public:

    void SetData()

    {

        a = 10;

        b = 20; //b는 protected 맴버이므로 상속관계에서는 접근허용!!!

     }

};

 

int main(void)

{

   AAA aaa;

   aaa.a = 1; //컴파일 에러!!! private 맴버이므로!

   aaa.b = 2; //컴파일 에러!!! protected 맴버이지만 외부 접근은 허용이 안되므로!

 

   BBB bbb;

   bbb.SetData();

 

   return 0;

}

 

 protected 맴버가 어떠한 성질을 가지고 있는지 아시겠죠? 아까 Student, Person 클래스의 문제에서도 age와 name을 private가 아닌 protected 맴버로 설정해주면 별도의 수정없이 사용할 수 있겠지요?

 

[ 3가지 형태의 상속]

 

이번에는 상속의 형태를 간단하게 알아보도록 하겠습니다. 역시 상속의 형태도 마찬가지로 private, protected, public 처럼 3가지 형태가 존재합니다. 아래의 표?를 보시죠!

 

                                     public상속        protected상속        private상속 (상속 형태)

   pulbic 맴버                public              protected                   private

   protected 맴버          protected               protected                   private

   private 맴버              접근 불가               접근 불가                  접근 불가

  (Base 클래스)

 

예를들어 Protected 맴버는 pulbic 상속을 하게되면 protected가 됨을 알 수 있습니다. 상속하는 클래스에선 사용이 가능하고 외부 접근은 불가능하게되죠. private맴버는 어떠한 상속을 하더라도 접근이 불가능 하구요. 각각이 어떠한 의미를 갖는지는 잘 이해하실거라 믿습니다. 이번 강의는 여기서 마치도록 하구요 다음 강에서는 상속과 다형성에 대해서 살펴보도록 하겠습니다~

 

 

 

 얼마만에 포스팅을 하는건지..... 다사다난했습니다....ㅠㅠㅠㅠㅠ 이제다시 블로그 활동 열심히 하려구요! 늦은시간에도 불구하고 C++ 6강을 강행하도록 하겠습니다! 하두 오랫만이라 ㅠㅠㅠㅠㅠㅠ 바보가 된 느낌....

 

[const 키워드]

 

 일단 const라는 키워드에 대해서 복습을 해봅시다! 복습은 과해도 괜찮아요~

 

1. const 키워드는 변수의 선언 앞에 붙어서 변수를 상수화 한다.

ex) const int number = 1;

      number = 2; //여기서 컴파일 오류가 발생합니다.

 

2. const 키워드는 포인터가 가리키는 데이터를 상수화 한다.

ex) int number = 1;

     const int* ptr = &number;

     *ptr = 2; // 컴파일 오류가 발생!!

 

3. const 키워드는 포인터 선언 시 이름 앞에 붙어서 포인터 자체를 상수화한다.

ex) int number1 = 1;

     int number2 = 2;

     int* const ptr = &number1;

     *ptr = 10; // 가능!!!

     ptr = &number2; // 컴파일 오류가 발생!!

 

 기억들 잘 나시나요~? 이번에는 맴버 변수를 상수화하고 초기화하는 방법에 대해서 알아보도록 하겠습니다.

 

[멤버 변수의 상수화 그리고 초기화]

 

class Student

{

   const int id;

   int age;

   char name[20];

};

 

이렇게 Student라는 클래스를 정의했다고 해봅시다. 멤버 함수라던지 클래스명.id 과 같은 방법으로 맴버 변수 id를 수정할 수 있을까요~? 물론 학생이라는 클래스를 사용하여 하나의 객체를 만들었을 때, 학번(id)라는 부분은 한번 정의되면 절대 바뀌지않는 불변의 값을 갖게 되므로 상수화를 시켜주는 것이 좋습니다. 그렇다면 초기화는 어떻게 해야되는 걸까요? 단순히 cons int id = 121212;와 같은 방법이라면 바보같지만 클래스를 정의하는 아무런 의미가 없겠죠?

 

 그래서 "멤버 이니셜라이져(member initializer)"라는 문법을 제공합니다. 이 문법을 이용하면 const 멤버 변수를 초기화하는 것이 가능하죠~

 

class Student

{

   const int id;

   int age;

   char name[20];

 

 public:

  Student(int _id, int _age, char* _name) : id(_id)

 {

    age = _age;

    strcpy(name,_name);

 }

 

};

 

 볼드 처리된 부분은 "맴버 변수 id를 매개 변수 _id로 초기화 하라" 라는 의미입니다. const 맴버 변수는 반드시 이니셜라이저를 사용하여 초기화해야 합니다.

 

[const 멤버 함수]

 

 마찬가지로 맴버 함수에도 const 키워드를 사용할 수 있습니다. 맴버 함수가 상수화되면, 이 함수를 통해서 맴버 변수의 값이 변경되는 것은 허용되지 않습니다.

 

class Student

{

   const int id;

   int age;

   char name[20];

 

 public:

  Student(int _id, int _age, char* _name) : id(_id)

 {

    age = _age;

    strcpy(name,_name);

 }

 

  void Show() const

{

   cout<<id<<","<<age<<","<<name<<endl;

 }

 

};

 

 Show() 함수는 단순한 출력함수 이기때문에 내부에 있는 맴버 변수를 특별하게 변경하는 일이 없습니다. 때문에 맴버 변수의 상수화를 해주는게 좋겠죠~?

 

 또 다른 예를 한번 들어보도록 하겠습니다.

 

class count

{

   int cnt;

 

public:

 int*  GetPtr() const    //컴파일 에러

{

   return &cnt;

 }

void Show() const     //컴파일 에러

{

   ShowIntro();

   cout<<cnt<<endl;

 }

void ShowIntro()

{

   cout<<" count의 값 :" <<cnt<<endl;

 }

};

 

 위 예제에서는 두 부분에서 컴파일 에러가 발생합니다. 첫 번째 함수에서는 함수 내에서 맴버 변수를 조작하지 않음에도 불구하고 컴파일 에러를 발생시킵니다. 두 번째 함수에서도 마찬가지 입니다. 그렇다면 왜 컴파일 에러가 발생할까요?

 

 답은 상수화된 함수는 상수화되지 않은 함수의 호출을 허용하지 않을 뿐만 아니라, 맴버 변수의 포인터를 리턴하는 것도 허용하지 않습니다. 무슨 말인지 선뜻 이해가 안가시죠? 첫 번째 함수를 다시 한번 살펴봅시다. 이 함수는 상수 함수임에도 불구하고 맴버 변수의 포인터를 리턴하고 있습니다. 포인터를 말이죠. 그렇다면 이 포인터를 전달받은 영역에서 이 리턴 값을 가지고 충분히 맴버 변수의 조작이 가능해집니다. 즉 이 함수 자체가 데이터 조작의 경로가 되기 때문입니다. 두번째 함수는 그렇다면 왜 컴파일 에러를 발생시킬까요? 첫 번째 함수보다는 좀 더 이해하기 어렵긴 합니다. 그러나 ShowIntro() 함수는 상수화된 함수가 아니기 때문에 맴버 변수를 조작할 가능성이 있기 때문입니다. 때문에 ShowIntro() 함수의 호출을 허용하지 않는 것 입니다.

 

 컴파일 에러를 해결하기 위해 다음과 같이 수정해봅시다.

 

class count

{

   int cnt;

 

public:

 const int*  GetPtr() const   

{

   return &cnt;

 }

void Show() const   

{

   ShowIntro();

   cout<<cnt<<endl;

 }

void ShowIntro() const

{

   cout<<" count의 값 :" <<cnt<<endl;

 }

};

 

 이렇게 함으로써 GetPtr() 함수의 포인터가 가리키는 데이터 자체를 상수화한다는 의미입니다. 나머지는 설명안해도 이해하시겠죠? 참고로 const가 뒤에 붙어있으면 포인터 자체를 상수화하고 const가 앞에 붙어있으면 포인터가 가리키는 데이터를 상수화 한다는거 기억하시고 계시죠?

 

[const 객체]

 

 변수를 선언할 때 const 키워드를 붙여서, 변수를 상수화하듯이 객체도 생성과 동시에 상수화하는 것이 가능합니다.

 

ex)

 int main()

{

   const AAA aaa(1);

   return 0;

 }

 

[const와 함수 오버로딩]

 

함수 오버로딩은 상수 함수냐 아니냐에 따라서도 성립이 됩니다.

즉, void function(int n) const { .... }

     void function(int n) { .... }

위 2개의 함수는 서로 다른 함수로 취급합니다.

 

ex)

 class AAA

{

  int number;

 

public:

 

  AAA(int _number) : number(_number) {} //초기화 (이니셜라이져)

  void Show()

  {

     cout<<" Show() 호출 "<<endl;

     cout<<number<<endl;

  }

  void Show() const

  {

    cout<<" Show() const 호출 "<<endl;

    cout<<number<<endl;

  }

};

 

int main()

{

  const AAA aaa1(1);

  AAA aaa2(2);

 

  aaa1.Show();

  aaa2.Show();

  return 0;

 }

 

 ====> 출력결과

 Show() const 호출

 1

 Show() 호출

 2

 

참고하실 사항은 보통 상수 함수보다 상수화되지 않은 함수가 우선순위가 높습니다. 즉 둘 다 호출이 가능한 상황이라면 상수화되지 않은 함수가 호출된다는 뜻입니다. 그리고 상수 객체는 상수화되어 있지 않은 함수는 호출이 불가능합니다.

 

[클래스 static]

 

 이번에는 static키워드를 가지고 공부해봅시다. 마찬가지로 클래스의 맴버 변수나 맴버 함수를 static으로 선언할 수 있습니다.

어떤 상황에서 static 키워드를 사용하면 좋을까요? 그 중 한가지는 전역 변수가 필요한 상황입니다. 다음 예제를 먼저 살펴봅시다.

 

 int count = 1;

 

class Person

{

  char name[20];

  int age;

public:

  Person(char* _name, int _age)

  {

    strcpy(name,_name);

    age = _age;

    cout<<count++<<" 번째 Person 객체 생성 "<<endl;

   }

  void Show()

  {

    cout<<name<<","<<age<<endl;

   }

};

 

int main(void)

{

  Person p1("box", 10);

  Person p2("bop", 20);

 

  return 0;

 }

 

 위 예제에서 볼드 처리된 부분을 주의 깊게 살펴봅시다. Person 객체가 생성될 때마다 참조해야 하는 변수를 전역 변수로 선언해 두고 있습니다. 때문에 모든 객체가 공유할 수 있고 객체의 수를 카운트 하는 것이 가능하겠죠?

 그러나 위 예제의 문제점은 존재합니다. 전역변수 count는 Person클래스에 종속적이지만 그럼에도 불구하고 전역 변수로 선언되어 있어서 다른 영역에서 접근할 위험이 존재합니다. 그렇다면 다음과 같이 변경해봅시다.

 

  

class Person

{

  char name[20];

  int age;

  int count;

public:

  Person(char* _name, int _age)

  {

    count = 1;

    strcpy(name,_name);

    age = _age;

    cout<<count++<<" 번째 Person 객체 생성 "<<endl;

   }

  void Show()

  {

    cout<<name<<","<<age<<endl;

   }

};

 

int main(void)

{

  Person p1("box", 10);

  Person p2("bop", 20);

 

  return 0;

 }

 이렇게 수정하면 원하는 결과 값이 나올까요? p1도 p2도 출력은 "1 번째 Person 객체 생성" 이라고만 뜰겁니다. 즉, 생성되는 객체마다 독립된 count 변수를 가지게 됩니다. 그렇다면 다시 다음과 같이 수정해봅시다.

 

class Person

{

  char name[20];

  int age;

  static int count;

public:

  Person(char* _name, int _age)

  {

    strcpy(name,_name);

    age = _age;

    cout<<count++<<" 번째 Person 객체 생성 "<<endl;

   }

  void Show()

  {

    cout<<name<<","<<age<<endl;

   }

};

 

int Person::count = 1; //static 맴버 초기화

 

int main(void)

{

  Person p1("box", 10);

  Person p2("bop", 20);

 

  return 0;

 }

 

 위의 예제는 우리가 원하는 조건을 모두 만족시키며 의도한 결과값을 출력합니다. 결론부터 말씀드리자면

static 맴버는 main 함수가 호출되기도 전에 메모리 공간에 올라가서 초기화됩니다. 따라서 public으로 선언이 된다면 객체 생성 이전에도 접근이 가능합니다. 그리고 객체의 맴버로 존재하는 것이 아닙니다. 다만 선언되어 있는 클래스 내에서 직접 접근할 수 있는 권한이 부여된 것 입니다. 이 두가지 의미 잘 이해하시길 바랍니다.

 

 추가적으로 좀 더 설명 드리자면 객체가 생성되지 않음에도 불구하고 static 맴버 변수에 접근 하는 것이 가능하다는 사실입니다. 왜냐면 static 맴버는 main 함수가 호출되기도 전에 메모리상에 초기화되어 올라가기 때문입니다. 그리고 static 맴버는 객체의 맴버가 아니라는 점입니다. 다만 이미 초기화되서 메모리 공간에 올라가 있는 n에 직접 접근할 수 있는 권한만 가지고 있다는 의미입니다.

 

 다음 예제는 따로 설명안하겠습니다. 소스와 출력값을 차분히 분석해보시길 바랍니다.

 

1.

 class AAA

{

public:

  static int n;

};

 

int AAA::n = 1;

 

int main(void) //객체를 생성하지 않았음에도 static변수를 출력

{

   cout<<AAA::n<<endl;

   AAA::n++;

   cout<<AAA::n<<endl;

 

   return 0;

 }

 

====>결과값

1

2

============

2.

 class AAA

{

   int val;

   static int n;

public:  //n은 객체의 맴버 변수가 아니지만 맴버 변수처럼 직접 접근의 권한을 가짐

   AAA(int a = 0)

 { val = a, n++; }

   void Show

 { cout << val <<","<<n<<endl; }

};

 

int AAA::n = 1;

 

int main(void)

{

  AAA a1(10);

  a1.Show();

 

  AAA a2(20);

  a2.Show();

 

  return 0;

}

 

====>결과값

val: 10

n: 2

val: 20

n: 3

============

 

 이번 강은 여기까지 하도록 하겠습니다. 너무 오랜만에 학술 포스팅을 한거라 어떤지 모르겠네요. 그래도 대충 감은 잡은 듯 하니까 앞으로 다시 열심히 해야겠습니다. 다음 강부터는 상속에 대하여 공부해보도록 하겠습니다.

 


 오랜만에 포스팅합니다~ 이번 2월달에는 블로그에 신경을 많이 못 써주었네요 ㅠㅠㅠ그래도 블로그 오픈 약 2달만에 2만명이라는 방문자를 달성했습니다 우와..... 열심히 하겠습니다! 정말 좋은 정보들 얻어가셨으면 좋겠다는게 제 바램입니다. 조만간 리뷰도 올리고 제품소개도 할 예정입니다^^ 학술블로그만 너무 올리는 것 같아서....ㅎㅎㅎ  자 그럼 오늘도 열공하겠습니다!

 [복사 생성자]

 int value1 = 10;
 int value2(10);

 첫 번째 문장과 두 번째 문장은 서로 동일합니다. C언어 스타일이냐 C++언어 스타일이냐만 다르지 기능은 완벽하게 똑같습니다. 

 객체로 넘어가보겠습니다. 그렇다면 클래스 AAA를 정의했다고 합시다.

 AAA a1(10);
 AAA a2 = 10;

 마찬가지로 객체의 생성에 있어서도 C스타일 초기화와 C++초기화가 가능합니다. 그러나 객체 생성과정에서는 AAA a2 = 10; 을 AAA a2(10); 으로 묵시적인 변환이 됩니다. 사실 결과적으로는 같다고 보셔도 무방합니다. 물론 차이를 보이를 경우도 있지만 일단은 결과적으로 두개의 코드는 동일하다고 생각하시면 됩니다~

 이번에는 생성자 오버로딩을 잠깐 살펴보도록 하겠습니다. 지루하실까봐 예제는 최소화 하도록 하겠습니다^-^ 다음과 같은 3개의 생성자중에 복사생성자가 숨어있습니다~

 
AAA()
 {
    cout<<" AAA() callin !!! "<<endl;
  }
 AAA(int i)
 {
    cout<<" AAA(int i) callin !!! "<<endl;
  }
 
AAA(const AAA& a)
 {
    cout<<" AAA(const AAA& a) callin !!! "<<endl;
  }

 생성자에 대해서는 이미 공부한바 있으니까 충분히 별도의 설명없이도 이해하실 거라고 생각하구요~ 그렇다면 복사 생성자를 얘기하기전에 여기서 무엇을 말하고 싶을걸까요? 세 번쩨 생성자의 모습이 복사생성자의 모습입니다. 즉, 자기 자신과 같은 형태(자료형)의 객체를 인자로 받을 수 있는 생성자를 복사생성자라고 합니다.

 여기서보면 매개 변수 선언에서 const가 붙어있는게 보이실 겁니다. 인자로 전달된 객체의 내용의 변경을 허용하지 않겠다는 선언입니다. 그렇다면 & 선언은 무엇일까요? 인자로 전달된 객체를 '레퍼런스'로 받겠다는 선언입니다. 레퍼런스도 중요하게 설명한적 있으니까 혹시 기억이 나지 않으시면 다시한번 레퍼런스 부분을 정독해보세요~~~

 [디폴트 복사 생성자]

이름만 들어도 대충 감이 오시죠~? 사용자가 특별히 정의해주지 않아도 이미 정의 되어있는 디폴트 복사 생성자라는 이야기입니다.

class point
{
   int x , y;
     public:
           point(int _x, int _y)
          {  x = _x; y = _y }
           void show
          { cout <<x<<' '<<y<<endl; }
 };

 int main(void)
{
  point p1(10,20);
  point p2(p1);

  return 0;
 }

 point p1(10,20);는 정수형 변수 2개를 인자로 받으면서 객체를 생성하고 있습니다. 어? 그러나 point p2(p1); 이라는 생성자는 따로 정의를 했나요? 정의하지 않았습니다. 때문에 컴파일 오류가 발생할거라고 생각하시지만 그렇지 않습니다. 그러나 결과값을 확인해보면 알겠지만 정상적으로 실행이되고 point p2(p1);이라는 문장이 정확하게 객체를 복사하고 있습니다.

 point(const point& p)
{
   x = p.x;
   y = p.y;
 }

 위와 같은 문장이 자동으로 삽입되었다는걸 추측할 수 있겠죠? 이렇게 자동으로 삽입되는 복사생성자를 가리켜 디폴트 복사 생성자라고 합니다. 물론 복사 생성자는 클래스에 따라서 조금씩 달라지기는 합니다. 즉 클래스마다 정의되어 들어가는 디폴트 복사 생성자의 형태는 다를 겁니다. 그러나 point 객체의 경우 디폴트 복사 생성자는 위 예제와 동일한 모양을 가지고 있습니다.

 [깊은 복사를 하는 복사 생성자]

 디폴트 복사 생성자가 있으니까 굳이 복사 생성자를 정의할 필요가 없다는 생각을 갖을 수도 있습니다. 그러나 디폴트 복사 생성자에는 부족한 부분이 존재합니다. 우리가 복사 생성자를 직접 정의해야되는 경우도 자주 발생하기도 하구요. 그림으로 설명하고 싶지만..... ㅠㅠㅠ 용서해주십쇼

 person이라는 클래스 많이 보셨죠? person 클래스의 객체 p1, p2를 생성했다고 합시다. 맴버 변수는 이름, 전화번호, 나이 정도가 있겠군요.

 person p1( "boxbop" , "010-1234-5678", 10);
 person p2=p1; // person p2(p1); 동일한 문장

여기서 중요한건 이름과 전화번호 입니다. 맴버변수의 자료형이 주소값이기 때문이죠. char* name 이나 char *phone 으로 정의되어 있습니다. 객체를 초기화할때 boxbop 과 010-1234-5678은 메모리에 저장되어있고 저장되어있는 위치의 주소값을 사용하는거 다들 아시죠? 그러나 p2(p1)과 같은 문장으로 복사를 하게되면 메모리에 있는 주소값만 복사를 하는겁니다. 실제로 메모리에는 boxbop 과 010-1234-5678은 각각 하나씩만 존재를 하게되죠. 진정한 복사는 이러한 내용도 모두 똑같이 그리고 하나씩 더 존재를 해야되는거죠? 그러나 위와 같은 문장으로는 메모리의 주소값만 복사할 뿐 실질적인 메모리의 내용까지는 복사하지 않습니다. 이런걸 얕은 복사와 깊은 복사라고 합니다. 

 이러한 얕은복사의 문제점 때문에 프로그래머는 복사생성자를 다시 정의할 필요성이 있는 겁니다. 그러면 깊은 복사를 한번 구현해보겠습니다.

 person::person(const person& p)
{
    name = new char[strlen(p.name)+1];
    strcpy(name, p.name);

    phone = new char[strlen(p.phone)+1];
    strcpy(phone, p.phoen);
    age = p.age;
 }

 전에 한번 이런 생성자를 구현한적이 기억나시나요~? 생성자 내에서 동적 할당을 하면 반드시 제공해야되는 것이 있습니다!!! 소멸자입니다. 잊으시면 안됩니다~ 그래야 메모리 누수가 발생하지 않습니다. 아래와 같은 소멸자 꼭 잊지마시구요~

 person::~person()
{
    delete []name;
    delete []phoen;
 }

 [복사 생성자가 호출되는 시점]

복사 생성자가 호출되는 시점은 다음과 같이 세 가지 형태로 분류할 수 있습니다.
 1. 기존에 생성된 객체로 새로운 객체를 초기화하는 경우
 2. 함수 호출 시 객체를 값에 의해 전달하는 경우
 3. 함수 내에서 객체를 값에 의해 리턴하는 경우
각각의 경우를 예제로 살펴보도록 하겠습니다.

1. 기존에 생성된 객체로 새로운 객체를 초기화하는 경우

AAA obj1;
AAA obj2 = obj1;

2. 함수 호출 시 객체를 값에 의해 전달하는 경우

void function(AAA a)
{
    a.showdata();
 }

 int main()
{
    AAA obj(30);
    function(obj);

    return 0;
 }

 여기서 볼드처리된 부분을 좀 더 설명해보겠습니다. 여기서 첫 번째로 인자를 받을 매개 변수를 위한 메모리 공간이 할당됩니다. 매개변수가 a이므로 a를 위해 할당된 메모리 공간이 형성됩니다. 사실 매개 변수 a는 클래스의 객체입니다만 정확하게는 객체라고 표현하지 않습니다. 생성자의 호출이 없었기 때문이죠. 두 번째로 전달 인자 값의 복사입니다. 위에서 생성한 객체 obj가 함수의 전달 인자로 넘어 왔으므로, obj가 지니고 있는 값을 매개 변수 a에 복사해야 합니다. 매개 변수 a의 복사 생성자를 호출하면서 복사의 대상이 되는 객체 obj를 인자로 전달하게 됩니다. 여기서 중요한 것은 매개 변수로 생성되는 객체(a)의 복사 생성자가 호출되는 것입니다. 이제는 매개 변수 a는 객체라고 불릴 수 있게 됬습니다.

3. 함수 내에서 객체를 값에 의해 리턴하는 경우

int function(void)
{
    int a = 10;
    return a;
 }

int main()
{
    cout<<function(void)<<endl;
    return 0;
 }

function 함수가 리턴한 값을 출력하고 있습니다. 리턴된 값을 출력할 수 있다는 것은 함수를 호출한 영역으로 값이 리턴되었다는 이야기입니다. 여기서 중요한 사실은 한가지 입니다. 리턴되는 값은 받아주는 변수가 없더라도, 함수를 호출한 영역으로 복사되어 넘어갑니다. 스택 영역에 main 함수의 영역과 function 함수의 영역이 있을때 리턴 되는 값은 function 함수의 영역에서 일단은 main 함수의 영역으로 넘어간다는 이야기입니다.

AAA function(void)
{
    AAA a(10);
    return a;
 }

 int main()
{
    function();
    return 0;
 }

위 예제에서 function 함수는 객체 a를 생성한 다음, 이 객체를 값에 의해 리턴하고 있습니다. 따라서 함수를 호출한 영역으로 객체가 복사되어 넘어갈 것 입니다. a객체의 복사본이 function 함수 영역에서 main 함수 영역으로 넘어가겠죠? 그렇다면 과연 어떻게 a 객체의 복사본이 생성되느냐 하는 것 입니다. 복사본도 객체기 때문에 복사본을 위한 메모리 공간이 할당되어야 합니다. 그 다음에는 복사본의 생성자가 호출되는데 원본 객체 a를 인자로 전달받을 수 있는 생성자, 즉 복사 생성자가 호출됩니다. 즉 객체 a를 인자로 전달하여 복사본에 똑같이 담아냅니다. 직관적으로 생각했던 것과 비슷하죠~?아마? 이번 복사 생성자에 대한 내용은 이해가 잘 안된다고 걱정할 필요는 없습니다. 나중에 천천히 이해해도 되니까요~

 이상으로 복사 생성자에 대한 내용을 마치도록 하겠습니다. 딴짓거리 하면서 포스팅 하느라 생각보다 오래걸렸네요^-^;;

 


 이번 장에서는 본격적으로 '클래스'에 대해서 설명하겠습니다. 저번 장에서는 C언어의 구조체 관점(?)에서 살펴보았죠. 클래스의 아주 중요한 정보은닉, 캡슐화에 대한 개념을 설명하고 생성자, 소멸자 그리고 기타 필요한 설명을 이어나가도록 하겠습니다~

 정보 은닉(Information Hiding)

정보 은닉은 객체 내부의 존재하는 데이터를 숨긴다고 생각하시면 됩니다. 결론부터 말씀드리자면 객체의 외부에서 객체 내에 존재하는 맴버 변수에 직접 접근하는 권한을 허용하면 안됩니다. 즉, 객체 내에 존재하는 맴버 변수에 직접 접근하는 것은 정보 은닉에 위배됩니다.

 class point
{
   public:
          int x;
          int y;
 };

 int main(void)
{
  point p;
  p.x = 10;
  p.y = 10;
  return 0;
 }

 point 라는 클래스는 x좌표와 y좌표값을 맴버 변수로 정의하고 있습니다. 메인 함수내에서 p라는 point형 객체를 만들어준 뒤에 p.x = 10; p.y = 10; 라는 코드를 통해 클래스 맴버 변수의 값을 바꾸어 줍니다. 즉, 이처럼 외부에서 맴버 변수로의 직접 접근이 가능해집니다. 
 
 그렇다면 어떻게 정보 은닉을 할 수 있을까요? 모든 맴버 변수를 private로 선언해주어야 합니다. 그러나 private 선언 후에는 직접 접근을 할 수 없으므로 간접 접근을 위한 특별한 '경로'를 만들어 주어야 합니다. 맴버 함수로 말이죠~

 class point
{
   private:
          int x;
          int y;
   public:
        void change_point(int _x, int _y)
        {
             x = _x;
             y = _y;
         }
 };

 int main(void)
{
   point p;
   p.change_point(10,10);
   return 0;
 }

 클래스의 선언에서 private나 public 키워드를 사용하지 않고 그냥 선언해준다면 기본적으로 private로 선언이 됩니다. change_point함수는 public 맴버이므로 외부에서 접근이 가능하고, private로 선언된 맴버 변수에도 접근이 가능합니다. 즉, 클래스 외부에서 이 함수를 이용해서 맴버 변수 x, y에 간접적인 접근이 가능해지죠. 보통 이런 함수를 엑세스 함수(메소드)라고 부릅니다.

 이처럼 정보은닉은 어려운 개념은 아닙니다~ 직관적으로 쉽게 이해할 수 있는 부분이죠~ 또 다른 개념인 캡슐화에 대해서 알아보겠습니다. 

 캡슐화

캡슐화란? 관련 있는 데이터와 함수를 하나의 단위로 묶는 것 입니다. 즉, 관련 있는 데이터와 함수를 클래스라는 하나의 캡슐 내에 모두 정의하는 것 입니다. 

 class point
{
   private:
          int x;
          int y;
   public:
        void change_point(int _x, int _y)
        {
             x = _x;
             y = _y;
         }
        int getx() {return x;}
        int gety() {return y;}
 };

 class pointview
{
  public:
       void showdata(point p)
       {
           cout << "x 값 : " << p.getx() <<endl;
           cout << "y 값 : " << p.gety() <<endl;
        }
  };

 int main()
{
    point p;
    p.change_point(10, 10);
   
    pointview view;
    view.showdata(p);
    return 0;
 }

 감이 오시나요~? 사실상 pointview 클래스는 상당히 불필요한 존재입니다. point클래스에 좌표 값을 출력할 함수가 없으므로 새로운 클래스를 정의하여 출력시키고 있죠. 그것도 view.showdata(p)라는 call-by-value를 통해서 아주 무겁게(?) 출력하고 있습니다. 무겁게라는 말이 이해 안가시면 레퍼런스 쪽을 다시 공부하고 오세요~!!! 무튼 캡슐화란 관련 있는 데이터와 함수를 하나의 클래스로 정의하는 것이라고 하였습니다. 그런데 위의 예제에서 보면 x, y좌표에 관련된 데이터와 함수가 두 개의 클래스로 양분되어 있습니다. 이는 캡슐화의 원칙에 어긋나버리게 되는거죠~ 그러므로 캡슐화에 맞도록 수정해보겠습니다~

 class point
{
   private:
          int x;
          int y;
   public:
        void change_point(int _x, int _y)
        {
             x = _x;
             y = _y;
         }
        int getx() {return x;}
        int gety() {return y;}
   
        void showdata();
 };

void point::showdata()
{
           cout << "x 값 : " << x <<endl;
           cout << "y 값 : " << y <<endl;
}
     

 int main()
{
    point p;
    p.change_point(10, 10);
  
    p.showdata();
    
     return 0;
 }

 캡슐화는 생각보다 상당히 중요한 개념입니다. 실제로도 이 부분을 무시하고 프로그래밍하면 추 후에 문제가 발생했을때 상당한...뻘..짓(?)으로 이어지게 되죠~

 생성자(constructor) & 소멸자(destructor)

바로 예제를 보면서 설명해보겠습니다.

class person
{
  private:
     char name[20];
     char phone[20];
     int age;

  public:
     void show();
 };

void person::show()
{
   cout<<name<<phone<<age<<endl;
 }

int main()
{
   person p = {"kim", "010-1111-2222", 10};
   p.show();
   return 0;
 }

 진한 부분을 보시면 객체를 생성과 동시에 원하는 값으로 초기화 하려고 하고있습니다. 그러나 중요한 점은 맴버 변수가 private로 선언되어 있다는 점이죠. 즉, 위와 같은코드의 접근은 허용되지 않습니다. 그렇다고 엑세스 함수를 사용해서 접근을 하면 말처럼 "생성과 동시에 초기화" 가 아니지 않습니까~ 액세스 함수를 사용하면 "생성 후 초기화" 가 될 뿐이죠.. 이러한 문제를 해결해 주는 것이 생성자입니다.

 class person
{
   char name[20] = "boxbop";
   char phone[20] = "010-1111-2222";
   int age = 10;
 }

 그렇다고 이러한 초기화 방법이 가능할까요?! 분명히 클래스나 구조체에서는 선언하는데 있어서 맴버를 초기화 할 수 없습니다~ 무조건 맴버 변수를 선언!만 할 수 있습니다. 그러나 C#, java에서는 위와 같은 초기화가 가능하긴 합니다만 C나 C++에서는 절대 불가능합니다~ 참고하시구요~

 그렇다면 생성자는 어떤모양일까요~? 그전에 객체의 생성과정을 잠깐 살펴보겠습니다. 객체는 메모리를 할당한 후에 -> 생성자를 호출합니다. 생성자는 무엇이냐면 함수이고, 클래스의 이름과 같은 이름을 가지고 있고, 리턴하지도 않고 리턴 타입도 선언되지 않습니다. 다음 예제를 살펴봅시다~

class person
{
  private:
     char name[20];
     char phone[20];
     int age;

  public:
     void show();
   
     person()
     {
          char name[20] = "boxbop";
          char phone[20] = "010-1111-2222";
          int age = 10;
      }
      
 };

볼드 처리된 부분이 생성자 입니다. 리턴 타입도 없고 리턴도 없습니다. 클래스의 이름과도 같죠~? 딱 보시고 아! 생성자구나! 라고 생각하면 됩니다~ 따라서

int main(void)
{
   person p;
   p.show();
  
   return 0;
 }

 위와 같은 코드의 출력은 boxbop, 010-1111-2222, 10 을 출력하게 됩니다. 생성자 때문에 단지 객체를 생성만 해주었을 뿐인대 초기화가 되어있죠~ 물론 생성자가 없는 상태에서 객체를 생성해주면 쓰레기 값이 들어있습니다. 생성자를 좀 더 바꾸어 봅시다.

     person(char* _name, char* _phone, int _age)
     {
         strcpy(name, _name);
         strcpy(phone, _phone);
         age = _age;        
      }
      

 이렇게 바꾸어 주었습니다. 설마 name = _name; 이렇게 코딩하시는 분들 계시죠~? 그렇게 하면 안되는거 아시겠죠~? 모르신다면 포인트쪽을 다시 한번 공부해오시길 부탁드리겠습니다~ 힌트는 메모리 주소!!! 여기까지~ㅋㅋㅋ

 자 그렇다면 위와 같은 생성자를 어떻게 사용할까요~? 예제를 통해 보여드리겠습니다.

 int main()
{
   person p (boxbop,010-1111-2222,10);
   p.show();
   return 0;
 }

 객체를 생성과 동시에 초기화 한 것이 보이시죠~? 이렇게 생성자는 객체 생성 시 원하는 값으로 초기화하기 위한 용도로 사용됩니다~

 디폴트(Default) 생성자

생성자에는 몇 가지 특징이 있습니다.
1. 생성자를 정의하지 않으면 디폴트 생성자가 자동 삽입됩니다.
2. 생성자도 함수이므로 오버로딩이 가능합니다.
3. 생성자도 함수이므로 디폴트 매개 변수의 설정이 가능합니다.

class point
{
   int x,y;
   public:
   point() {}
 }

볼드 처리 된 부분이 디폴트 생성자의 형태입니다. 그러나 디폴트 생성자같은 경우에는 프로그래머가 정의해 놓은 생성자가 하나라도 존재하면 디폴트 생성자가 자동으로 삽입되지 않습니다.

class point
{
     int x,y;
   public:
     point(int _x, int _y) { x = _x, y = _y; }
 }

int main(void)
{
   point p1(10, 20); // 가능
   point p2; // 불가능!!!

   return 0;
 }

위와 같은 예제는 컴파일 에러를 발생시키죠~ 그러나 생성자도 오버로딩이 된다고 하지 않았습니까~? 다음과 같이 수정해주면 됩니다.

class point
{
     int x,y;
   public:
     point(int _x, int _y) { x = _x, y = _y; }
     point() {}
 }

int main(void)
{
   point p1(10, 20); // 가능
   point p2; // 가능 (디폴트 생성자)

   return 0;
 }

 위 예제는 디폴트 생성자를 따로 삽입했습니다. 때문에 p2의 선언이 가능해진 것 입니다. 그리고 생성자도 함수라고 하지 않았습니까~? 때문에 디폴트 매개변수를 설정할 수 있습니다. 다음과 같이 말이죠.

class point
{
     int x,y;
   public:
     point(int _x=0, int _y=0) { x = _x, y = _y; }
     
 }

int main(void)
{
   point p1(10, 20); // 가능
   point p2; // 가능 (디폴트 매개 변수)

   return 0;
 }

디폴트 매개 변수 때문에 디폴트 생성자가 없어도 p2의 선언이 가능합니다.

 생성자와 동적할당

이번에는  생성자 내에서 메로리 공간을 동적 할당하는 경우에 대해서 살펴보겠습니다. 예제는 조금 복잡하겠네요~

#include <iostream>
using std::cout;
using std::endl;

class person
{
           char *name, *phone;
           int age;
     public:
           person(char* _name, char* _phone, int _age);
           void show();
 };

 person::person(char* _name, char* _phone, int _age)
{
      name = new char[strlen(_name)+1];
      strcpy(name, _name);
      phone = new char[strlen(_phone)+1];
      strcpy(phone, _phoen);
      age = _age;
 }
 void person::show()
{
     cout<<name<<phoen<<age<<endl;
 }

int main()
{
     person p("kim", "010-1111-2222",10);
     p.show();
     return 0;
 }

 p라는 객체를 생성하고 있죠? 제일 먼저 메모리 공간이 할당되고, p라는 이름이 부여됩니다. 그다음으로는 생성자를 호출하면서 선언되어 있던 문자열과 정수가 인자로 전달되죠. 생성자 내에서는 전달된 문자열의 길이를 계산해서 메모리 공간을 할당하고 문자열을 복사합니다.
 결과적으로 객체 p는 main 함수 내에서 생성되었으므로 스택 영역에 할당이 됩니다만 맴버 변수 name과 phone이 가리키는 메모리 공간은 힙영역이 됩니다. 그러나 위 코드에서는 생성자 내에서 동적 할당한 메모리 공간을 해제해 주지 않고 있습니다. 따라서 다음과 같이 해결해주어야 합니다.
    
#include <iostream>
using std::cout;
using std::endl;

class person
{
           char *name, *phone;
           int age;
     public:
           person(char* _name, char* _phone, int _age);
           void show();
           void delmemory();
 };

 person::person(char* _name, char* _phone, int _age)
{
      name = new char[strlen(_name)+1];
      strcpy(name, _name);
      phone = new char[strlen(_phone)+1];
      strcpy(phone, _phoen);
      age = _age;
 }
 void person::show()
{
     cout<<name<<phoen<<age<<endl;
 }
 void person::delmemory()
{
   delete []name;
   delete []phone;
 }

int main()
{
     person p("kim", "010-1111-2222",10);
     p.show();
     p.delmemory();
     return 0;
 }

 이렇게 보면 문제가 해결된 듯 합니다. 그러나 사실은 그렇지 않죠. 만약 이런식의 클래스가 여러개가 있다고 한다면 상당히 골치아파집니다. 귀찮죠.... 그리고 메모리 해제를 놓칠 수 있습니다. 사람은 완벽하지 않기 때문이죠. 그래서 등장한 것이 소멸자(destructor)입니다.

 소멸자(destructor)

 객체의 소멸 과정도 객체의 생성 과정과 마찬가지로 소멸자 호출 -> 메모리 반환이라는 비슷한 과정을 거치게 됩니다. 소멸자의 특징은 다음과 같습니다.

1. 함수입니다.
2. 클래스의 이름 앞에 '~'가 붙습니다.
3. 리턴, 리턴 타입이 존재하지 않습니다.
4. 매개 변수를 받지 않고, 오버로딩, 디폴트 매개 변수의 선언도 불가능합니다.

 바로 다음과 같은 예제를 살펴보겠습니다.

#include <iostream>
using std::cout;
using std::endl;

class person
{
           char *name, *phone;
           int age;
     public:
           person(char* _name, char* _phone, int _age);
           void show();
           ~person();
 };

 person::person(char* _name, char* _phone, int _age)
{
      name = new char[strlen(_name)+1];
      strcpy(name, _name);
      phone = new char[strlen(_phone)+1];
      strcpy(phone, _phoen);
      age = _age;
 }
 person::~person()
{
      delete []name;
      delete []phone;
 }
 void person::show()
{
     cout<<name<<phoen<<age<<endl;
 }


int main()
{
     person p("kim", "010-1111-2222",10);
     p.show();
     return 0; /*리턴과 동시에 p객체 소멸
                   이 부분에서 소멸자 호출*/
 }

 볼드 처리된 부분을 보시면 리턴타입도, 인자도 받지 않습니다. 그리고 소멸자는 객체 소멸시 자동적으로 호출됩니다. 즉 소멸자의 가장 중요한 특징은 객체 소멸 시 반드시 한번 호출된다는 것입니다.

 정리하자면 생성자 내에서 메모리를 동적 할당하는 경우, 이를 해제하기 위해서 반드시 소멸자를 정의해야 합니다!

 디폴트(default) 소멸자

디폴트 생성자를 살펴보았으니 어렵지 않게 이해할 수 있습니다. 디폴트 생성자 처럼 아무것도 정의해주지 않으면 자동으로 삽입이 됩니다.

class point                                     class point
{                                                   {
   int x, y;                                          int x, y;
 public:                                           public: 
   void show();                                   point() {}
 }                                                    ~point() {}
                                                       void show()
                                                      }

 왼쪽과 오른쪽의 코드는 완전하게 동일합니다. 대충 이해가 가셨죠~?

 클래스 그리고 배열

C++에서는 이러한 객체 포인터 배열도 선언이 가능합니다. C언어에서 구조체 배열기억나시죠~? 동일한 개념입니다. 다만 객체 별이 생성되기 위해서는 void생성자의 호출이 요구됩니다. 일단 보시죠

class point
{
         int x, y;
    public:
         point()
         {
             cout<<"void생성자 호출"<<endl;
             x = y = 0;
          }
         point(int _x, int _y)
         {
             x = _x, y = _y;
          }
         void change(int _x, int_y) { x = _x, y = _y; }
 };

 int main()
 {
      point array[3]; //void 생성자의 호출이 요구됨

      array[0].change(1,1);
      array[1].change(2,2);
      array[2].change(3,3):

      return 0;
  }

 출력결과를 보시면 "void생성자 호출" 이라는 메시지가 총 3번 출력됩니다. 즉, 객체 배열이 생성되기 위해서는 void 생성자의 호출이 요구된다는 이야기죠. 일단 아시다시피 배열 안에 객체가 존재합니다. 바로 이어서 객체 포인터 배열로 넘어가겠습니다. 객체 포인터 배열이란 객체를 가리킬 수 있는 포인터로 구성이 되어 있는 배열을 의미합니다. 추가적으로 다음 예제를 통해서 어떻게 객체를 동적으로 생성 및 소멸하는지도 알아보겠습니다.

class point
{
         int x, y;
    public:
         point()
         {
             cout<<"void생성자 호출"<<endl;
             x = y = 0;
          }
         point(int _x, int _y)
         {
             x = _x, y = _y;
          }
         void change(int _x, int_y) { x = _x, y = _y; }
 };

 int main()
 {
      point *array[3];
  
      array[0] = new point(1,1);
      array[1] = new point(2,2);
      array[2] = new point(3,3);

      array[1]->change(4,4); //포인터에 접근은 ' -> ' 연산자 사용

      delete array[0];
      delete array[1];
      delete array[2];

      return 0;
  }

 메인함수 첫 번째 줄에서는 포인터 배열을 선언하고 있습니다. 따라서 point객체 3개를 가리킬 수 있는 배열이 생성됩니다. 즉 point객체의 주소값을 저장할 수 있는 배열이 생성되죠. new point(1,1)은 1, 1을 인자로 받을 수 있는 point 클래스의 생성자를 호출하면서 point객체를 생성합니다. 물론 힘영역에 생성이 될 것이고, 생성된 객체의 주소 값이 point* 형으로 반환될 것입니다. 단, 여기서는 객체의 배열이 생성되는 것이 아니므로 void생성자의 호출이 요구되지는 않습니다. 동적할당 과정에서 다르게 정의되어 있는 생성자를 호출하기 때문이기도 하구요~ 이해가 충분히 가셨을꺼라 믿습니다~

 this 포인터

맴버 함수 내에서는 this라는 이름의 포인터를 사용할 수 있습니다. this 포인터는 포인터의 개념을 잘 잡고 있다면 예제만으로도 아주 쉽게 이해할 수 있을겁니다.

class point
{
   public:
      person*  GetThis()
      {
          return this; //this 포인터를 리턴
       }
  };

int main()
{
    point *ptr = new person();
    cout<<"포인터 ptr의 값 :"<< ptr <<endl;
    cout<<"ptr의 this 값 :" << ptr->GetThis()<<endl;
    return 0;
 }

출력값을 살펴보시면 알겠지만 포인터 ptr의 값과 ptr의 this값이 동일합니다. this는 자기 자신을 가리키는 용도로 사용되는 포인터 입니다. 자기 참조 포인터라고 하죠. this 포인터의 유용함을 한번 더 살펴보겠습니다.

 class data
{
    int aaa;
   public:
    data(int aaa)
    {
        aaa = aaa;
     }
  };

위와 같은 클래스가 있다고 봅시다. 볼드처리된 부분을 보시면 의아해 하실겁니다. 왼쪽의 aaa와 오른쪽의 aaa가 서로 어떤 aaa인지 구분이 안가죠. 맴버 변수인 aaa인지 매개변수로 받는 aaa인지 말입니다. 우리의 의도는 맴버변수 aaa에 매개변수 aaa를 대입하는 겁니다. 때문에 왼쪽이 맴버변수, 오른쪽이 매개변수가 되어야 우리가 의도한 코드가되는거죠. this포인터를 이용하여 다음과 같이 변경해줍니다.
 class data
{
    int aaa;
   public:
    data(int aaa)
    {
        this->aaa = aaa;
     }
  };

만약 이러한 클래스를 메인함수에서 data a(111);으로 생성해주었다고 해봅시다. 이 객체는 주소값 0x11번지에 할당되어 있습니다. 이 상황에서 클래스 내부에 있는 this는 0x11번지를 가리키는 포인터가 되는겁니다. 때문에 this->aaa는 0x11번지에 할당된 객체의 aaa라고 인식을 하게됩니다. 신기하지않나요~? 객체의 주소 값을 가지고 지역 변수(매개변수)에 접근을 하다니 말입니다!!! 그러나 이러한 문제는 변수의 이름을 바꾸어주는 것으로 상당히 간단하게 해결할 수 있겠죠~?

 friend 선언

private으로 선언된 맴버 변수는 외부 접근이 허용되지 않는다고 설명했습니다. 그러나 frined 선언을 통해서 private으로 선언된 맴버 변수의 접근을 허용할 수 있습니다.

class number
{
         int val;
      public:
         number()
         {
              val = 0;
          }
          friend void setting (number& c, int val);
 };

 void setting(number& c, int val) // 이건 전역함수입니다!!!
{
     c.val = val;
 }

 분명히 클래스 외부로 나온 setting함수는 전역 함수 입니다. 그럼에도 불구하고 c.val 이라는 코드로 number 객체의 private 맴버인 val에 접근을 하고 있지요. 이것이 가능한 이유는 클래스 내에서 setting 함수를 friend 키워드를 통해 정의해주었기 때문입니다. 즉, 전역함수 void setting(number& c, int val)을 friend로 선언하고 있습니다.
 다시 말하자면 클래내에서 함수를 정의할때 friend 키워드를 붙여주게 되면 이는 전역함수라고 취급을 해버리면서 선언되는 전역함수는 해당 클래스의 private 맴버 변수에도 접근을 허용하게 해줍니다.

 클래스에 대한 friend 선언

friend 선언은 클래스에도 사용이 가능합니다. 그러나 여기서는 방향성에 대해서 주의하셔야 합니다.

class A
{
   private:
        int number;
        friend class B; //B에게 내 모든걸 다줄꺼야!
 };

 class B
{
   private:
        void setting(A& a, int value)
        {
              a.number = value;
         }
 };

 클래스 A에서 보시면 B 클래스를 friend로 선언하고 있습니다. 이것은 A클래스는 B클래스에게 private 영역의 접근을 허용하겠다는 의미가 됩니다. "B 클래스는 나의 private 맴버 변수에 접근을 해도 좋다!!" 라고 알려줍니다. 그러나 B클래스는 A클래스를 friend로 선언하지 않았기 때문에 A클래스는 B클래스의 private 맴버 변수에 접근할 수 없게 됩니다. 이것이 friend 선언의 단방향성 입니다.

 이번 장은 여기서 마치도록 하겠습니다.... 생각보다 많이 길어졌네요 ㅠㅠㅠ 사실 어제 포스팅하다가 졸려서 백업해놓고 오늘 마무리했습니다. 그래도 나름 오래 걸렸어요...ㅠㅠㅠ 오늘은 토요일인대... 불토인대...ㅠㅠㅠㅠㅠ흑흑 열심히 해야죠!

 


 드디어 클래스를 공부하는군요. 클래스와 우리가 기존에 알고 있던 구조체를 같이 언급하면서 설명하도록 하겠습니다. 프로그래밍을 할 때, 관련 있는 데이터를 하나로 묶습니다. 관리하기도, 프로그래밍 하기에도 편하기 때문이죠. 그래서 구조체를 사용했습니다. 즉, 부류를 형성하는 데이터들을 하나의 자료형으로 정의해서, 관리 및 프로그램 구현에 도움을 주고있습니다.

 지금까지 우리는 구조체를 공부하면서 구조체 내부에 오직 변수만 정의하여 사용해왔습니다. 그러나 클래스는 함수까지도 포함을해서 정의합니다. 어떠한 데이터들이 부류를 형성한다고 가정해 봅시다. 예를들어 은행업무에서의 고객이름, 비밀번호, 계좌번호, 잔액은 모두 고객에 대한 정보로 부류를 이루고 있습니다. 출금기능과 입금기능은 함수로 구현을 했고 이러한 기능도 고객 정보에서 구현이 됩니다. 결론적으로 클래스의 개념은 구조체의 개념을 포함하고 있습니다. 클래스는 변수와 함수를 포함하기 때문이죠

 이제부터 클래스 내부에 정의되어 있는 변수를 맴버변수, 함수를 맴버 함수로 이야기 하겠습니다. 그리고 클래스의 변수는 객체라고 부르겠습니다. 클래스를 정의하는 방법은 구조체와 아주 비슷하니까 예제보시고 아 이렇구나~라고 생각만 하세요~

 클래스 맴버의 접근제어 방식에 대하여 알아보겠습니다. 무슨말이냐~ 하면요 클래스 안에 선언되어 있는 맴버의 접근 허용 범위를 이야기하는 것 입니다. 

  public, protected, private

이렇게 3개의 키워드가 존재합니다. 클래스 내에서 변수를 가지고 노는 것이 내부 접근, 클래스를 벗어나 다른 곳에서 클래스의 변수(맴버변수)를 가지고 노는 것이 외부 접근이라고 생각하시면 됩니다. 이러한 접근을 제어하는 역할을 하는데요 한번 살펴보도록 하겠습니다. 아! protected 키워드는 나중에 상속을 공부할때 언급하기로 하고 일단은 public과 private만 설명하겠습니다.

 class person
{
   private:
          int height;
   public:
          int age;
          void hchange();
          void achange();
          void show()
          {
                 cout<<"hi boxbop"<<endl;
           }
 };

 int main(void)
{
   person p;
   p.height = 10; //에러
   p.age = 10;  //가능
   p.show();  //가능
 }

 height 라는 변수는 private, 그외에는 public을 지정해주었습니다.
메인 함수에서 이러한 맴버 변수들에 접근하고 있습니다 요런걸 외부 접근이라고 하죠~ 여기서 height의 값을 10으로 바꾸는 부분에서는 에러를 발생시킵니다. 그외에는 public으로 설정해주었으니까 가능하구요. private는 맴버가 선언이 되면 클래스의 내부 접근만 허용하겠다는 이야기 입니다. 반면에 public은 클래스 외부 접근도 허용하겠다는 이야기가 되죠~

 클래스 내부의 함수를 외부에 정의하는 법을 배워보겠습니다. 이 방법을 왜 공부해보냐면 위의 클래스 같은 경우에는 함수의 구현부분이 상당히 짧습니다. 그러나 실제로는 짧지가 않죠! 때문에 클래스가 상당히 조잡(?)해지는 경우가 있을 수 있는데요 이를 방지하기 위해서 살펴볼거랍니다~ 위 예제에 이어서 설명하도록 하겠습니다.

 void p::hchange()
{
   heigh = 10;
 }
 void p::achange()
{
   age = 10;
 }

요렇게 작성해주면 됩니다. 물론 클래스 정의 부분과 메인함수의 사이에요~ 만약 ' p:: '라는 선언이 존재하지 않는다면 이는 전역함수가 되버리지만 저런 선언때문에 클래스 외부에서도 클래스의 함수를 구현할 수 있는겁니다. 즉, 맴버 함수의 선언만 클래스 내부에 두고, 정의(구현)는 클래스 밖으로 빼낼 수 있습니다.
 
 사실 클래스의 맴버 함수를 내부에 정의한다는 것은 외부에 정의하는 것과 달리 인라인(in-line)으로 처리할 것을 요구합니다. 

  inline void p::hchange()
{
   heigh = 10;
 }
 inline void p::achange()
{
   age = 10;
 }

 이렇게 앞에 inline키워드를 붙여주면 비록 함수의 정의가 클래스 외부에 있다고 하더라도 인라인화가 가능합니다. 

 오늘은 여기까지입니다. 피곤해서... 다음 장은 클래스에 대하여 마저 알아보도록 하겠습니다.

+ Recent posts