jam 블로그

[C++] 004. 클래스의 완성 본문

개발 및 관련 자료/C

[C++] 004. 클래스의 완성

kid1412 2013. 5. 12. 20:04
728x90

I. 정보 은닉

  • 정보 은닉의 필요성

    • 은닉화 : 객체의 회부에서 객체 내에 존재하는 멤버 변수에 직접 접근하는 것을 허용하지 않으면 된다.(접근을 하지 못하게 하려면 private를 쓰면 된다.)
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
    public:
     int x;
     int y;
    };
  4. int main()
    {
     int x,y;
     cout<<"좌표 입력 : ";
     cin>>x>>y;
     Point p;
     p.x=x;
     p.y = y;
     cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
     return 0;
    }
  • 이 소스의 가장 큰 문제 : 객체의 외부에서 객체 내에 존재하는 멤버 변수에 직접 접근할 수 있는 권한을 준다는 것이다.
  • 정보 은닉의 적용

    •  위의 소스의 정보는 은닉하려면 아래와 같이 소스를 짜면 된다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
    public:
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x){x= _x;}
     void SetY(int _y){y= _y;}
    };
  4. int main()
    {
     int x,y;
     cout<<"좌표 입력 : ";
     cin>>x>>y;
  5.  Point p;
     p.SetX(x);
     p.SetY(y);
     cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
     return 0;
    }
  • 클래스 안에 public: 처럼 선언이 없으면 기본적으로 private로 선언된다.

    • 멤버 함수의 이름 중에서 Get, Set으로 시작되는 함수들은 대부분 멤버 변수의 접근을 위한 것이다. 보통 access 매소드라고 한다.
    • 위의 소스에서 오류 검사 같은 것을 추가 하려면 밑의 소스처럼 바꾸자.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
    public:
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x);
     void SetY(int _y);
    };
  4. void Point::SetX(int _x)
    {
     if (_x<0||_x>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     x = _x;
    }
  5. void Point::SetY(int _y)
    {
     if (_y<0||_y>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     y = _y;
    }
    int main()
    {
     int x,y;
     cout<<"좌표 입력 : ";
     cin>>x>>y;
  6.  Point p;
     p.SetX(x);
     p.SetY(y);
     cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
     return 0;
    }
  • x와 y의 좌표를 0~100까지의 범위를 두고 그 이하 또는 그 이상일 경우 오류를 처리하는 조건문은 넣은 것이다.

II. 캡슐화

  • 캡슐화의 기본 개념

    • 관련 있는 데이터와 함수를 하나의 단위로 묶는 것이다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
    public:
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x);
     void SetY(int _y);
    };
  4. void Point::SetX(int _x)
    {
     if (_x<0||_x>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     x = _x;
    }
  5. void Point::SetY(int _y)
    {
     if (_y<0||_y>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     y = _y;
    }
    class PointShow
    {
    public:
     void ShowData(Point p)
     {
      cout<<"x좌표 : "<<p.GetX()<<endl;
      cout<<"y좌표 : "<<p.GetY()<<endl;
     }
    };
    int main()
    {
     int x,y;
     cout<<"좌표 입력 : ";
     cin>>x>>y;
  6.  Point p;
     p.SetX(x);
     p.SetY(y);
     PointShow show;
     show.ShowData(p);
     return 0;
    }
  • 위의 소스를 보면 캡슐화 처럼 보이지만(안보일 수도 있다) 이거는 캡슐화에 실패한 예제이다

    • 캡슐화는 하나의 클래스로 정의하는 것인데 위에 소스를 보면 클래스가 2개로 나눠져 있다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
    public:
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x);
     void SetY(int _y);
     void ShowData();
    };
  4. void Point::SetX(int _x)
    {
     if (_x<0||_x>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     x = _x;
    }
  5. void Point::SetY(int _y)
    {
     if (_y<0||_y>100)
     {
      cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
     }
     y = _y;
    }
    void Point::ShowData()
    {
     cout<<"x좌표 : "<<x<<endl;
     cout<<"y좌표 : "<<y<<endl;
    }
  6. int main()
    {
     int x,y;
     cout<<"좌표 입력 : ";
     cin>>x>>y;
  7.  Point p;
     p.SetX(x);
     p.SetY(y);
     p.ShowData();
     return 0;
    }
  • 위의 소스를 보면 Point 클래스는 x,y좌표에 관련된 데이터와 함수가 하나의 클래스 안에 존재한다. 적절히 캡슐화가 된것이다.

III. 생성자(constructor)와 소멸자(destructor)

  • 생성자의 필요성

    • 객체를 생성과 동시에 초기화 하기 위해서
  1. #include <iostream>
  2. using namespace std;
  3. const int SIZE = 20;
  4. class Person
    {
     char name[SIZE];
     char phone[SIZE];
     int age;
    public:
     void ShowData();
    };
  5. void Person::ShowData()
    {
     cout<<"name : "<<endl;
     cout<<"phone : "<<endl;
     cout<<"age : "<<endl;
  6. }
  7. int main()
    {
     Person p = {"KIM","013-123-1234",22};
     p.ShowData();
     return 0;
    }
  •  위의 소스를 보면 Person p = {"KIM","013-123-1234",22}; 이걸로 객체를 초기화 하려고 하지만 오류가 난다. 왜?

    • 당연히 private로 선언되어 있는 변수들을 객체의 외부에서 접근해서 초기화 하려고 하니 오류가 난다. 다음 예제를 보자
  1.  #include <iostream>
  2. using namespace std;
  3. const int SIZE = 20;
  4. class Person
    {
     char name[SIZE];
     char phone[SIZE];
     int age;
    public:
     void ShowData();
     void SetData(char* _name,char* _phone,int _age);
    };
  5. void Person::ShowData()
    {
     cout<<"name : "<<name<<endl;
     cout<<"phone : "<<phone<<endl;
     cout<<"age : "<<age<<endl;
  6. }
  7. void Person::SetData(char *_name, char *_phone, int _age)
    {
     strcpy(name,_name);
     strcpy(phone,_phone);
     age = _age;
    }
  8. int main()
    {
     Person p;
     p.SetData("KIM","013-123-1234",22);
     p.ShowData();
     return 0;
    }
  • SetData라는 함수를 만들어서 초기화를 시키고 있다. 하지만 이는 바람직한 형태가 아니다.
  • 다음과 같은 형태의 초기화는 허용되지 않는다.
  1. class AAA
  2. {
  3. int a =10;

  4. int b =20;

  5. };
  6. struct AAA
  7. {
  8. int a =10;

  9. int b = 20;

  10. }
  • 단, JAVA나 C#은 허용이 된다고 합니다.

 

  • 생성자와 객체의 생성 과정

    • 모든 객체는 다음 두 단계를 반드시 거친다.

      • 메모리 할당
      • 생성자 호출
    • 생성자의 외형적 특징

      • 함수이다.
      • 클래스의 이름과 같은 이름을 지닌다.
      • 리턴하지도 않고, 리턴 타입도 선언되지 않는다.
  1.  #include<iostream>
  2. using namespace std;
  3. class AAA
    {
     int i,j;
    public:
     AAA()
     {
      cout<<"생성자 call"<<endl;
      i = 10; j = 20;
     }
     void ShowData()
     {
      cout<<i<<' '<<j<<endl;
     }
    };
  4. int main()
    {
     AAA aaa;
     aaa.ShowData();
     return 0;
    }
  • 위에 소스를 보면 클래스 안에 AAA() 라는 것을 볼수 있는데 그것이 생성자 이다.

    • 생성자를 호출하면서 i,j를 초기화 시킨다.

4.jpg 

  •  첫번째 그림은 객체생성 1단계(메모리 할당)을 나타낸거다.
  • 두번째 그림은 객체생성 2단계(생성자 호출)이다.
  1. #include<iostream>
  2. using namespace std;
  3. class AAA
    {
     int i,j;
    public:
     AAA(int _i,int _j)
     {
      cout<<"생성자 call"<<endl;
      i = _i;
      j = _j;
     }
     void ShowData()
     {
      cout<<i<<' '<<j<<endl;
     }
    };
  4. int main()
    {
     AAA aaa(111,222);
     aaa.ShowData();
     return 0;
    }
  •  위의 소스는 객체를 생성과 동시에 초기화되면서, 원하는 값을 초기화했기 대문이다.

    • 생성자는 다른 용도(?)로 사용될 수 있지만 가급적 멤버변수를 초기화하는 용도로만 사용.
  1.  #include <iostream>
  2. using namespace std;
  3. const int SIZE = 20;
  4. class Person
    {
     char name[SIZE];
     char phone[SIZE];
     int age;
    public:
     Person(char* _name,char* _phone, int _age);
     void ShowData();
    };
  5. void Person::ShowData()
    {
     cout<<"name : "<<name<<endl;
     cout<<"phone : "<<phone<<endl;
     cout<<"age : "<<age<<endl;
  6. }
  7. Person::Person(char *_name, char *_phone, int _age)
    {
     strcpy(name,_name);
     strcpy(phone,_phone);
     age = _age;
    }
  8. int main()
    {
     Person p("KIM","013-123-1234",22);
     p.ShowData();
     return 0;
    }
  • 생성자를 생성한 다음 메인에서 Person p(("KIM","013-123-1234",22);로 초기화 하고 있다.

    • Person p("KIM","013-123-1234",22);와 같은 방법으로는
    • Person p = Person("KIM","013-123-1234",22); 이지만 위에 방법을 더 잘 쓴다.
  • 디폴트 생성자와 생성자의 특징

    • 생성자를 하나도 정의하지 않으면 디폴트 생성자가 자동 삽입
    • 생성자도 함수이므로 오버로딩이 가능하다
    • 생성자도 함수이므로 디폴트 매개 변수의 설정이 가능하다
  1. class Point
  2. {
  3. int x, y;

  4. public:

  5. Point(){}

  6. }
  •  위의 소스에서 생성자인 Point(){}를 보면 인자값을 받지도 않고 아무런 기능이 없다. 이것이 디폴트 생성자이다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x, y;
  4. public:
     
      Point(int _x, int _y)
      {
       x = _x;
       y = _y;
      }
     void ShowData()
     {
      cout<<x<<' '<<y<<endl;
     }
  5. };
  6. int main()
    {
     Point p1(10,20);
     p1.ShowData();
  7.  Point p2;                  //error
     p2.ShowData();    
     return 0;
    }
  • 위 소스는 잘못 되었다. 무엇이 잘못 되었을까?

    • 메인에 Point p2에서 에러가 나는데 이유는 p2는 디폴트 생성자를 호출하는데 클래스 안에는 디폴트 생성자가 없다.(생성자가 하나라고 있으면 디폴트 생성자는 없다.)
    •  2가지 방법으로 오류를 고쳐보겠다.
  1.  class Point
    {
     int x, y;
  2. public:
       Point(int _x, int _y)
      {
       x = _x;
       y = _y;
      }
      Point()
      {
       x=y=0;
      }
     void ShowData()
     {
      cout<<x<<' '<<y<<endl;
     }
  3. };
  • 클래스 안에 디폴트생성자를 만들어주면 된다. (이것이 첫번째 방법)
  1.  class Point
    {
     int x, y;
  2. public:
       Point(int _x = 0, int _y = 0)
      {
       x = _x;
       y = _y;
      }
     void ShowData()
     {
      cout<<x<<' '<<y<<endl;
     }
  3. };
  • 생성자도 함수이므로 오버로딩뿐만 아니라 디폴트 매개변수를 설정할 수 있다.

 

  •  생성자와 동적 할당
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
     char *name;
     char *phone;
     int age;
    public:
     Person(char* _name,char* _phone,int _age);
     void ShowData();
    };
  4. Person::Person(char* _name,char* _phone,int _age)
    {
     name = new char[strlen(_name)+1];
     strcpy(name,_name);
  5.  phone = new char[strlen(_phone)+1];
     strcpy(phone, _phone);
  6.  age = _age;
    }
    void Person::ShowData()
    {
     cout<<"name : "<<name<<endl;
     cout<<"phone : "<<phone<<endl;
     cout<<"age : "<<age<<endl;
    }
    int main()
    {
     Person p("KIM","013-123-1234",22);
     p.ShowData();
     return 0;
    }

 12.jpg

  •  위 예제 처럼 하면 중요한 문제가 있다.

    • 동적으로 할당한 메모리 공간을 해제를 안해주는 것이다. 그래서 메모리 누수 현상이 발생 하므로 해제를 시켜줘야 한다.

 

  • 소멸자의 특징과 필요성

    • 모든 객체는 생성과정 처럼 반드시 소멸 거친다.

      • 소멸자 호출
      • 메모리 반환(해제)
    • 특징

      • 함수이다.
      • 클래스의 이름 앞에 ~(틸드)가 붙는 형태의 이름을 지닌다.(즉, 생성자 앞에 ~를 붙이면 된다.)
      • 리턴하지도 않고 리턴타임도 선언되지 않는다.
      • 매개 변수를 받을 수 없다. 따라서 오버로딩도 불가능하고, 디폴트 매개 변수 선언도 불가능하다.  
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     AAA()
     {
      cout<<"생성자 호출"<<endl;
     }
     ~AAA()
     {
      cout<<"소멸자 호출"<<endl;
     }
  4. };
  5. int main()
    {
     AAA aaa1;
     AAA aaa2;
     return 0;
    }
  • AAA aaa1;, AAA aaa2가 실행 되면서 생성자를 호출 하여 2번 생성자 호출이라는 문구가 뜨고 main함수 호출이 끝나면서(프로그램 종료하면서) 두번 소멸자를 호출 하게 된다.

 

  • 디폴트 소멸자

    • 디폴트 생성자 처럼 생성하면 되며(또는 알아서 컴퓨터가 생성 하지만) 디폴트 생성자 앞에 ~만 붙이면 된다.

 IV. 클래스와 배열

  • 객체배열과 생성자
  1. #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
  4. public:
     Point()
     {
      cout<<"Point() call!"<<endl;
      x=y=0;
     }
     Point(int _x, int _y)
     {
      x= _x;
      y = _y;
     }
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x){x = _x;}
     void SetY(int _y){y = _y;}
    };
  5. int main()
    {
     Point arr[5];
  6.  for (int i = 0;i<5;i++)
     {
      arr[i].SetX(i*2);
      arr[i].SetY(i*3);
     }
     for (int j = 0; j<5;j++)
     {
      cout<<"x : "<<arr[j].GetX()<<' ';
      cout<<"y : "<<arr[j].GetY()<<endl;
     }
     return 0;
    }
  • Point 클래스를 배열로 선언 한 것이다. 밑의 그림처럼 되어있다.

  13.jpg

  • 객체 포인터 배열
  1. #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x;
     int y;
  4. public:
     Point()
     {
      cout<<"Point() call!"<<endl;
      x=y=0;
     }
     Point(int _x, int _y)
     {
      x= _x;
      y = _y;
     }
     int GetX(){return x;}
     int GetY(){return y;}
     void SetX(int _x){x = _x;}
     void SetY(int _y){y = _y;}
    };
  5. int main()
    {
     Point *arr[5];
  6.  for (int i = 0;i<5;i++)
     {
      arr[i] = new Point(i*2,i*3);
     }
     for (int j = 0; j<5;j++)
     {
      cout<<"x : "<<arr[j]->GetX()<<' ';
      cout<<"y : "<<arr[j]->GetY()<<endl;
     }
     for (int k=0;k<5;k++)
     {
      delete arr[k];
     }
     return 0;
    }
  • 추가가 된 부분은 Point *arr[5]와 new로 할당해주는 부분과 delete로 해제 해주는 부분이다.

14.jpg 

V. this 포인터의 의미

  •  this 포인터에 관한 간단한 설명을 하면 자기자신을 가리키는 포인터라고 보면 됩니다.
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     Person* GetThis()
     {
      return this;
     }
    };
  4. int main()
    {
     cout<<"***** p1의 정보 *****"<<endl;
     Person *p1 = new Person();
     cout<<"포인터 p1 : "<<p1<<endl;
     cout<<"p1의 this : "<<p1->GetThis()<<endl;
  5.  cout<<"***** p2의 정보 *****"<<endl;
     Person *p2 = new Person();
     cout<<"포인터 p2 : "<<p2 <<endl;
     cout<<"p2의 this : "<<p2->GetThis()<<endl;
     return 0;
    }
  • 위의 소스를 실행시켜 보면 출력값이 포인터 값이나 그에 대한 this 값이나 같다.

 

  • this 포인터의 용도
  1. #include <iostream>
  2. using namespace std;
  3. class Data
    {
     int aaa;
     int bbb;
    public:
     Data(int aaa, int bbb)
     {
      this->aaa = aaa;
  4.   this->bbb = bbb;
     }
     void printAll()
     {
      cout<<aaa<<" "<<bbb<<endl;
     }
    };
    int main()
    {
     Data d(100,200);
     d.printAll();
     return 0;
    }
  • 용도로는 연산자 오버로딩에 쓰인다.

VI. 전역함수에 대한 friend 선언

  • friend는 private으로 선언된 멤버 변수의 접근을 허용할 수 있다.
  1. #include <iostream>
  2. using namespace std;
  3. class Counter
    {
     int val;
    public:
     Counter()
     {
      val = 0;
     }
     void Print() const
     {
      cout<<val<<endl;
     }
     friend void SetX(Counter& c,int val);
    };
  4. void SetX(Counter&c, int val)
    {
     c.val = val;
    }
    int main()
    {
     Counter cnt;
     cnt.Print();
  5.  SetX(cnt,2002);
     cnt.Print();
     return 0;
    }
  • 위 소스를 보면 friend void SetX 라고 되어있다. SetX 함수를 friend로 설정하였기 때문에 void SetX는 private에 속해있는 val를 쓸 수 있다.
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    private:
     int data;
     friend class BBB;
    };
  4. class BBB
    {
    public:
     void SetData(AAA& aaa, int val)
     {
      aaa.data = val;
     }
    };
  5. int main()
    {
     AAA aaa;
     BBB bbb;
  6.  bbb.SetData(aaa,10);
     return 0;
    }
  • 위의 소스를 보면 (실행해도 보이는건 없음 ㅋㅋ) friend class BBB를 썼는데 class BBB를 friend 하겠다는 것으로 AAA의 private에 있는 val를 참조 할 수 있다.
  • friend는 연산자 오버로딩에 많이 쓰이는데 너무 쓰면 유지 보수 하기 힘든 프로그램이 되어 버린다.

문제

4 -1

문제1

직사각형을 나타내는 Rectangle 클래스와 원을 나타내는Circle 클래스를 디자인 해 보자. 이 두 클래스는 넓이를 구하는 기능과 둘레를 구하는 기능을 지녀야 한다.

 

문제2

 시,분,초 정보를 지닐수 있는 Time 클래스를 정의해 보자. 이 클래스는 멤버 변수가 지니고 있는 데이터를 적절히 출력하는 기능을 지녀야 한다. 하나는 [시,분,초]의 형식을 띄며, 또 하나는 초 단위로 계산한 출력 결과를 보여준다.

 

문제3

명함 정보를 지닐수 있는 클래스를 정의해 보자. 믈래스의 이름은 NameCard이고 이름, 전화번호, 주소, 직급 정보를 저장할 수 있어야 한다. 생성자 내에서 동적 할당하고, 소멸자에게 할당 받은 메모리를 해제하는 형식으로 구현하자.

 

4 -2

연습문제 4 -1의 3번 문제를 통해서 NameCard 클래스를 정의하였다. 이제 이 클래스를 적절히 활용하는 main 함수를 만들어 보기로 하자.

사용자로부터 NameCard 객체에 들어갈 데이터를 입력받기로 하자. 총 3명의 데이터를 순차적으로 입력받고, 다시 순차적으로 출력하는 형태로 예제를 작성하자. 단 3명의 데이터를 입력받기 위해서 배열을 선언 하되, 객체 포인터 배열을 선언하는 형태를 취하도록 하자. 

'개발 및 관련 자료 > C' 카테고리의 다른 글

[C++] 006. static 멤버와 const 멤버  (0) 2013.05.12
[C++] 005. 복사 생성자  (0) 2013.05.12
[C++] 003. 클래스의 기본  (0) 2013.05.12
[C++] 002. C기반의 C++ 2  (0) 2013.05.12
[C++] 001. C기반의 C++ 1  (0) 2013.05.12
Comments