013. 예외처리(Exception Handling)

I. 기존의 예외처리 방식

  • 예외를 처리하지 않는 프로그램의 오류
  1. #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
  4.  cout<<"a/b의 몫 : "<<a/b<<endl;
     cout<<"a/b의 나머지 : "<<a%b<<endl;
     return 0;
    }
  • 위의 소스는 정말 쉬운 소스이다.

    • 하지만 치명적인 오류가 있다. 다들 알다시피 b가 0일때의 문제이기 때문이다.

 

  • 전통적인 스타일의 예외처리
  1.  #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
  4.  cout<<"a/b의 몫 : "<<a/b<<endl;
     cout<<"a/b의 나머지 : "<<a%b<<endl;
     return 0;
    }
  • 위와 같은 식으로 하면 당연히 오류가 발생하는 것을 막을수는 있다.

    • 하지만 예외처리를 위한 코드 부분과 일반적인 프로그램의 흐름을 위한 코드 부분을 명확히 구분 짓지 못한다.

      •  짧은 소스일때는 상관 없지만 길경우 구별하기 힘들다.

 

II. 기본적인 예외 처리 메커니즘(try, catch, throw)

  • try

    • 예외 발생에 대한 검사 범위를 설정 할 때 사용한다.
  1. try
  2. {
  3. //예외 발생 예상 코드
  4. }
  •  catch

    • 예외를 처리하는 코드 구간을 선언할때 사용한다. try 구간 내에서 발생한 예외 상황을 처리하는 코드가 존재하는 영역이다.
  1. catch(처리되어야 할 예외의 종류)
  2. {
  3.  //예외를 처리하는 코드가 존재할 위치
  4. }
  •  try와 catch

    • try바로 뒤에 catch가 등장한다.
  • throw

    • 예외 상황이 발생 하였음을 알릴때 사용
  1. try
  2. {
  3. if(예외 상황 발생)
  4. throw ex;
  5. }
  6. catch(exception ex)
  7. {
  8. 예외 상황 처리
  9. }

 

  •  예외 처리 메커니즘의 적용
  1. #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
     try
     {
      if(b==0)
       throw b;
      cout<<"a/b의 몫 : "<<a/b<<endl;
      cout<<"a/b의 나머지 : "<<a%b<<endl;
     } 
     catch (int exception)
     {
      cout<<exception<<" 입력."<<endl;
      cout<<"입력 오류! 다시 실행하세요"<<endl;
     }
      return 0;
    }
  • 위와 같이 하면 된다.

 

  • 예외 처리 적용 시 프로그램의 흐름
  1.  #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
     try
     {
      cout<<"try block start"<<endl;
  4.   if(b==0)
       throw b;
      cout<<"a/b의 몫 : "<<a/b<<endl;
      cout<<"a/b의 나머지 : "<<a%b<<endl;
  5.   cout<<"try block end"<<endl;
     } 
     catch (int exception)
     {
      cout<<"catch block start"<<endl;
  6.   cout<<exception<<" 입력."<<endl;
      cout<<"입력 오류! 다시 실행하세요"<<endl;
     }
     cout<<"Thack You!"<<endl;
     return 0;
    }
  • 위 소스를 실행하면 어떻게 흘러가는지 알수 있다.

 

III. Stack Unwinding(스택 풀기)

  • 전달되는 예외
  1.  #include <iostream>
  2. using namespace std;
  3. int divide(int a, int b);
  4. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
     try
     {
      cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
     } 
     catch (int exception)
     {
      cout<<exception<<" 입력."<<endl;
      cout<<"입력 오류! 다시 실행하세요"<<endl;
     }
     return 0;
    }
  5. int divide(int a, int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 처리되지 않은 예외는 전달된다는 것이다.

    • 이러한 현상을 가리켜 스택 unwinding이라고 한다.
  1.  #include <iostream>
  2. using namespace std;
  3. void fct1();
    void fct2();
    void fct3();
  4. int main()
    {
     try
     {
      fct1();
     }
     catch(int ex)
     {
      cout<<"예외 : "<<ex<<endl;
     }
     return 0;
    }
  5. void fct1()
    {
     fct2();
    }
  6. void fct2()
    {
     fct3();
    }
  7. void fct3()
    {
     throw 100;
    }
  • 예외가 전달되는 과정이 함수의 스택이 풀리는 순서와 일치하기 때문에 스택 unwinding 이라고 한다.

 

  • 처리되지 않은 예외
  1.  #include <iostream>
  2. using namespace std;
  3. int divide(int a, int b);
  4. int main()
    {
     int a,b;
     cout<<"입력 : ";
     cin>>a>>b;
     cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
     return 0;
    }
  5. int divide(int a, int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 위와 같은 소스를 실행 시켜서 오류를 나게 해보자

    • b가 0이면 예외가 발생을 한다. 하지만 여기서 보면 예외를 처리해 주는 부분이 존재하지 않는다.
    • 이러한 경우 stdlib.h 안에 abort 함수가 호출되면서 프로그램을 종료 시킨다.
  1.  #include <iostream>
    #include <stdlib.h>
  2. using namespace std;

  3. int main()
    {
     abort();
     cout<<"end"<<endl;
     return 0;
    }
  • 위와 같은 소스를 실행 시키면 오류메시지를 띄우면서 cout<<"end"<<endl; 위에서 종료가 된다.
  1. #include <iostream>
  2. using namespace std;
  3. int divide(int a,int b);
  4. int main()
    {
     int a, b;
  5.  cout<<"두개 입력 : ";
     cin>>a>>b;
  6.  try
     {
      cout<<"a/b의 몫 : "<<divide(a,b)<<endl;
     }
     catch (char exception)
     {
      cout<<exception<<" 입력."<<endl;
      cout<<"입력 오류!"<<endl;
     }
     return 0;
    }
  7. int divide(int a,int b)
    {
     if(b==0)
      throw b;
     return a/b;
    }
  • 위의 소스를 보면 예외를 처리해 주는 부분이 있다.

    • 다만 catch구문에서 char형 예외를 처리하겠다는 것만 있기 때문에 abort 함수가 호출된다.

 

  • 전달되는 예외 명시하기

    • 함수를 정의하는데 있어서 전달될수 있는 예외의 종류를 명시해 줄 수 있다.
  1. int fct(double b) throw(int)
  2. {
  3. ...
  4. }
  •  상황에 따라서 int형 예외가 전달될 수 있음을 선언하고 있는 것이다.

    • int형이 아닌 다른형의 예외가 발생하면 윗부분에서 봤다시피 abort함수가 호출된다.
  1. int fct(double b) throw(int, double, char*)
  2. {
  3. ...
  4. }
  • 위에 처럼 둘이상의 예외종류를 선언해 줄 수 있다.
  1. int fct(double b) throw()
  2. {
  3. ...
  4. }
  • 위와 같은 경우는 어떠한 예외도 전달하지 않는다는 것이다.

    • 만약에 예외가 전달된다면 abort함수가 호출된다.

 

  • 하나의 try 블록과 여러개의 catch 블록
  1.  #include <iostream>
  2. using namespace std;
  3. int main()
    {
     int num;
     cout<<" input : ";
     cin>> num;
  4.  try
     {
      if(num>0)
       throw 10;
      else
       throw 'm';
     }
     catch (int exp)
     {
      cout<<"int형 예외 발생"<<endl;
      
     }
     catch (char exp)
     {
      cout<<"char형 예외 발생 "<<endl;
     }
     return 0;
    }
  • 위의 소스 처럼 0보다 크면 int형 예외, 그 나머지는 char형 예외라고 지정을 해놓았다.

    • 위에 처럼 try~catch~catch 이런식으로 선언할수 있는데 역시나 마찬가지로 try와 catch 블록 사이에 다른 문장이 존재할 수 없다.
  1.  #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  cout<<"계좌번호 입력 :";
     cin >> acc;
     cout<<"비밀번호 입력 : ";
     cin >> id;
     if (strcmp(account,acc)||sid!=id)
      throw AccountExpt(acc,id);
  9.  cout<<"출금액 입력 : ";
     cin>>money;
  10.  if(balance<money)
      throw money;
     balance -=money;
     cout<<"잔액 :"<<balance<<endl;
     return 0; 
    }
  • 위의 소스는 적절하게 예외처리를 안한 상태이다.
  1. #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  try
     {
      cout<<"계좌번호 입력 :";
      cin >> acc;
      cout<<"비밀번호 입력 : ";
      cin >> id;
      if (strcmp(account,acc)||sid!=id)
       throw AccountExpt(acc,id);
     }
     catch(AccountExpt& expt)
     {
      cout<<"다시 입력을 확인하세요"<<endl;
      expt.What();
     }
  9.  try
     {
  10.   cout<<"출금액 입력 : ";
      cin>>money;
  11.   if(balance<money)
       throw money;
      balance -=money;
      cout<<"잔액 :"<<balance<<endl;
     }
     catch(int money)
     {
      cout<<"부족 금액 :"<<money-balance<<endl;
     }
  12.  return 0; 
    }
  • 실행시켜 보면 알겠지만 예외처리는 해주었다고 하지만 엉망이다.

    • 참고로 catch(AccountExpt& expt) 이렇게 한 이유는 객체가 복사되는 부담을 줄이기 위한 것이기 때문에 꼭 이렇게 할 필요는 없다.
  1.  #include <iostream>
  2. using namespace std;
  3. char* account="1234-4576";
    int sid = 1122;
    int balance = 1000;
  4. class AccountExpt
    {
     char acc[10];
     int sid;
  5. public:
     AccountExpt(char* str,int id)
     {
      strcpy(acc,str);
      sid=id;
     }
     void What()
     {
      cout<<"계좌 : "<<acc<<endl;
      cout<<"비번 : "<<sid<<endl;
     }
  6. };
  7. int main()
    {
     char acc[10];
     int id;
     int money;
  8.  try
     {
      cout<<"계좌번호 입력 :";
      cin >> acc;
      cout<<"비밀번호 입력 : ";
      cin >> id;
      if (strcmp(account,acc)||sid!=id)
       throw AccountExpt(acc,id);
      cout<<"출금액 입력 : ";
      cin>>money;
  9.   if(balance<money)
       throw money;
      balance -=money;
      cout<<"잔액 :"<<balance<<endl;
     }
     catch(AccountExpt& expt)
     {
      cout<<"다시 입력을 확인하세요"<<endl;
      expt.What();
     }
     catch(int money)
     {
      cout<<"부족 금액 :"<<money-balance<<endl;
     }
  10.  return 0; 
    }
  • 위와 같이 try 문에 다 집어 넣어야 정상적으로 된다.

    • 이유는 앞전에서 봤을때 비밀번호가 이상하면 예외처리를 부르고 나서 또 다시 그다음에 이어서 실행이 되기때문이다.

 

IV. 예외 상황을 나타내는 클래스의 설계

  •   예외를 발생시키기 위해서 클래스를 정의하고 객체를 생성하였다.

    • 이러한 객체를 예외 객체라고 하며, 예외 객체를 위해 정의되는 글래스를 가리켜 예외 클래스라 한다.

 

V. 예외를 나타내는 클래스의 상속

  • catch 블록에 예외가 전달되는 방식

    • 이어서 선언되어 있는 catch 블록에 예외가 전달되는 형태를 보면 함수 오버로딩과 유사하다.
    • 단, 오버로딩된 함수는 딱 봐서 매개변수가 일치하는 함수가 호출되고 예외를 처리할 catch 블록은 위에서 부터 순차적으로 비교를 이루어지고나서 결정이 난다.

 

  • 상속 관계에 있는 예외 객체의 전달
  1.  #include <iostream>
  2. using namespace std;
  3. class ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptA 예외"<<endl;
     }
    };
  4. class ExceptB:public ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptB 예외"<<endl;
     }
    };
  5. class ExceptC : public ExceptB
    {
    public:
     void What()
     {
      cout<<"ExceptC 예외"<<endl;
     }
    };
  6. void ExceptFunction(int ex)
    {
     if(ex == 1)
      throw ExceptA();
     else if(ex == 2)
      throw ExceptB();
     else
      throw ExceptC();
    }
  7. int main()
    {
     int exID;
     cout<<"발생시킬 예외의 숫자 : ";
     cin>>exID;
     try
     {
      ExceptFunction(exID);
     }
     catch(ExceptA& ex)
     {
      cout<<"catch(ExceptA& ex)에 의한 처리"<<endl;
      ex.What();
     }
     catch(ExceptB& ex)
     {
      cout<<"catch(ExceptB& ex)에 의한 처리"<<endl;
      ex.What();
     }
     catch(ExceptC& ex)
     {
      cout<<"catch(ExceptC& ex)에 의한 처리"<<endl;
      ex.What();
     }
     return 0;
    }
  • 입력을 1,2,3을 해도 ExceptA에 의한 처리로만 나온다. 왜그럴까?

    • 앞에서 말했다시피 catch는 순차적으로 비교를 한다. A를 먼저 비교 할텐데 1,2,3이 예외가 발생하면 각각의 catch에 가겠지만 상속을 받았기 때문에 A에서도 예외가 적용이 되어서 A로만 찍힌다.
    • 다음과 같이 바꾸어 보자.
  1.  #include <iostream>
  2. using namespace std;
  3. class ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptA 예외"<<endl;
     }
    };
  4. class ExceptB:public ExceptA
    {
    public:
     void What()
     {
      cout<<"ExceptB 예외"<<endl;
     }
    };
  5. class ExceptC : public ExceptB
    {
    public:
     void What()
     {
      cout<<"ExceptC 예외"<<endl;
     }
    };
  6. void ExceptFunction(int ex)
    {
     if(ex == 1)
      throw ExceptA();
     else if(ex == 2)
      throw ExceptB();
     else
      throw ExceptC();
    }
  7. int main()
    {
     int exID;
     cout<<"발생시킬 예외의 숫자 : ";
     cin>>exID;
     try
     {
      ExceptFunction(exID);
     }
     catch(ExceptC& ex)
     {
      cout<<"catch(ExceptC& ex)에 의한 처리"<<endl;
      ex.What();
     }
      catch(ExceptB& ex)
     {
      cout<<"catch(ExceptB& ex)에 의한 처리"<<endl;
      ex.What();
     }
     catch(ExceptA& ex)
     {
      cout<<"catch(ExceptA& ex)에 의한 처리"<<endl;
      ex.What();
     }
     return 0;
    }
  • 앞전의 소스와 달라진 점은 catch 블록의 순서가 달라졌다.

    • 바꾼 이유는 IS-A관계는 역으로 성립하지 않음을 이용한 것이다. 즉, A 예외는 C예외가 아니다.

 

VI. new 연산자에 의해 전달되는 예외

  • new 연산자에 의해서 메모리 할당에 실패 했을 경우 NULL포인터가 리턴된다고 했었다.

    • C++표준에서는 new 연산자가 메모리 할당에 실패했을 경우 bad_alloc 예외가 전달된다고 한다.
    • 자세한건 MSDN을 참조하는게 좋다.
  1.  #include <iostream>
    #include <new>
  2. using namespace std;
  3. int main()
    {
     try{
      int i=0;
      while(1)
      {
       cout<<i++<<"번째 할당"<<endl;
       double(*arr)[10000] = new double[10000][10000];
      }
  4.  }
     catch(bad_alloc ex)
     {
      ex.what();
      cout<<endl<<"End"<<endl;
     }
     return 0;
    }
  • 위의 소스를 실행 시켜 보면 무한루프를 돌면서 메모리 공간만 할당하고 있다.

    • 어느 순간 new 연산이 실패로 돌아가면 bad_alloc 예외가 발생하고 END를 찍게된다.

 

VII. 예외처리에 대한 나머지 문법 요소

  • 모든 예외를 처리하는 catch 블록
  1. try
  2. {
  3. }
  4. catch(...)
  5. {
  6. }
  •  위에서 보면 "..." 의 선언은 모든 예외를 다 처리하겠다는 선언이다.(잘 사용하지는 않는다.)

 

  • 예외 다시 던지기
  1.  #include <iostream>
  2. using namespace std;
  3. class Exception
    {
    public:
     void what()
     {
      cout<<"Simple Exception"<<endl;
     }
    };
  4. void ThrowException()
    {
     try
     {
      throw Exception();
     }
     catch(Exception& t)
     {
      t.what();
      throw;
     }
    }
  5. int main()
    {
     try
     {
      ThrowException();
     }
     catch(Exception& t)
     {
      t.what();
     }
     return 0;
    }
  • 먼저 ThrowException()에서 예외를 처리하고 catch에서 throw로 예외를 던졌다.

    • 던져진 예외는 메인에 있는 ThrowException();으로 가고 그다음에 있는 catch에서 다시 한번 예외가 처리되어 Simple Exception이 2번 찍힌다.
    • 다음과 같은 경우 예외를 다시 던질 것을 고려해 보자.

      • catch 블록에 의해 예외를 잡고 보니, 처리하기가 마땅치 않다. 다른 catch블록에 의해서 예외가 처리되길바란다.
      • 하나의 예외에 대해서 처리되어야 할 영역(예외가 발생했음을 알려줘야 할 영역)은 둘 이상이다.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

012. 템플릿(Template)  (0) 2008.12.07
011. string 클래스 디자인  (0) 2008.12.07
010. 연산자 오버로딩  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
Posted by kid1412
,

012. 템플릿(Template)

I. 템플릿에 대한 이해

  • 간단히 말해서 템플릿을 쓰는 이유는

    •  함수의 리턴 값이나 받는 인자값에 대한 자료형을 정해주지 않는 다는 것이다.

#include <iostream>
using namespace std;

template<typename T>
T add(T a,T b)
{
 return a+b;
}

int main()
{
 cout<<add(10,20)<<endl;
 cout<<add(1.1,2.1)<<endl;
 cout<<add(10,1.1)<<endl;
 return 0;
}

  • 위에 소스에서 템플릿을 선언 할때는

    • template<typename T> T 함수명(인자값) 이런식으로 나가면 된다.

      • 참고로 template<typename T>와 template<class T>가 같은 것이다.(대부분 전자를 많이 쓰는 거 같다.)
    • cout 3줄중 위에 2줄은 같은 형 즉 int형이면 int형만 double이면 double형만 써서 잘 되지만

      • 그 아래 줄에는 int형과 double형을 같이 넣으면 T에 대한 모호성 때문에 오류가 난다.

 

II. 함수 템플릿

  • 함수 템플릿의 정의

    • 템플릿 함수가 아닌 함수 템플릿이라고 부르는 것이 맞는 표현이라고 한다.

 

  • 둘 이상의 타입에 대해서 템플릿화하기

#include <iostream>

using namespace std;

template<typename T>
void ShowData(T a,T b)
{
 cout<<a<<endl;
 cout<<b<<endl;
}

int main()
{
 ShowData(1,2);
 ShowData(3,2.5);
 return 0;
}

  • 앞전에서 와 같이 메인에서 오류가 난다.

    • 둘이상의 형을 같이 받을수 없기 때문이다. 어찌해야 할까?

 #include <iostream>

using namespace std;

template<typename T1,typename T2>
void ShowData(T1 a,T2 b)
{
 cout<<a<<endl;
 cout<<b<<endl;
}

int main()
{
 ShowData(1,2);
 ShowData(3,2.5);
 return 0;
}

  • 위와 같은 식으로 자료형이 두가지 이상 결정될 수 있다.

 

  • 함수 템플릿의 특수화(template specialization)

 #include <iostream>

using namespace std;

template<typename T>
int Sizeof(T a)
{
 return sizeof(a);
}

int main()
{
 int i = 10;
 double e = 7.7;
 char* str = "Goodday Commander";
 cout<<Sizeof(i)<<endl;
 cout<<Sizeof(e)<<endl;
 cout<<Sizeof(str)<<endl;

 return 0;
}

  •  위의 소스는 전달된 인자를 참조해서 차지하는 메모리 크기를 바이트 단위로 리턴하는 소스이다.

    • 만약 전달되는 인자가 문자열을 가리키는 포인터일 경우 문자열의 길이를 리턴해 주기를 원한다면 아래와 같이 하면 된다.

 #include <iostream>

using namespace std;

template<typename T>
int Sizeof(T a)
{
 return sizeof(a);
}
template <>
int Sizeof(char* a)
{
 return strlen(a);
}

int main()
{
 int i = 10;
 double e = 7.7;
 char* str = "Goodday Commander";
 cout<<Sizeof(i)<<endl;
 cout<<Sizeof(e)<<endl;
 cout<<Sizeof(str)<<endl;

 return 0;
}

  • 위의 소스를 보면 template<> 이렇게 선언했는데 특수화를 선언한 것이다.

    • 즉 template<typename T> 에서 예외로 따로 사용할꺼 있다면 특수화를 쓰면 된다.

 

III. 클래스 템플릿

  • 클래스 템플릿의 정의

#include <iostream>

using namespace std;

class Data
{
 int data;
public:
 Data(int d)
 {
  data = d;
 }
 void SetData(int d)
 {
  data=d;
 }
 int GetData()
 {
  return data;
 }
};

int main()
{
 Data d1(0);
 d1.SetData(10);
 Data d2(100);

 cout<<d1.GetData()<<endl;
 cout<<d2.GetData()<<endl;
 return 0;
}

  •  위의 소스는 int형 데이터 하나를 저장할 수 있는 Data 클래스 이다.

    • 위의 소스를 template형식으로 바꾸어 보자

 

#include <iostream>

using namespace std;

template <typename T>
class Data
{
 T data;
public:
 Data(T d)
 {
  data = d;
 }
 void SetData(T d)
 {
  data=d;
 }
 T GetData()
 {
  return data;
 }
};

int main()
{
 Data<int> d1(0);
 d1.SetData(10);
 Data<char> d2('a');

 cout<<d1.GetData()<<endl;
 cout<<d2.GetData()<<endl;
 return 0;
}

  •   클래스에 템플릿 하는 방법은 위와 같이 하면 된다.

    • 메인에서 d1을 int로 생성하겠다는 의미이다.
    • d2는 char로 생성하겼다는 의미이다.

 

  •  클래스 템플릿의 선언과 정의 분리

 #include <iostream>

using namespace std;

template<typename T>
class Data
{
 T data;
public:
 Data(T d);
 void SetData(T d);
 T GetData();
};

template<typename T>
Data<T>::Data(T d)
{
 data = d;
}

template<typename T>
void Data<T>::SetData(T d)
{
 data = d;
}

template<typename T>
T Data<T>::GetData()
{
 return data;
}

  • 위의 소스를 보면 Data<T>::SetData() 이런식으로 되어있는데 Data::SetData()라고 안한 이유

    • Data라고 하였다면 그냥 클래스의 멤버 함수를 정의한것이다. Data<T>라고 하였다면 클래스 템플릿을 선언한 것이다.
    • 그리고 template<typename T>를 선언을 또 해주었냐면 T가 무엇인지 보충 설명해 주는 역할을 하기 때문이다.

 

  • 스택 클래스의 템플릿화

    • 스택 : LIFO(Last Input, First Out)특징을 지니는 자료구조이다.
    • push : 데이터를 저장하는 행위
    • pop : 쌓아 올려진 데이터를 꺼내는 행위

#include <iostream>

using namespace std;

class Stack
{
private:
 int topIdx;
 char* stackPtr;
public:
 Stack(int s =10);
 ~Stack();
 void Push(const char& pushValue);
 char Pop();
};

Stack::Stack(int len)
{
 topIdx = -1;
 stackPtr = new char[len];
}
Stack::~Stack()
{
 delete[] stackPtr;
}

void Stack::Push(const char &pushValue)
{
 stackPtr[++topIdx] = pushValue;
}

char Stack::Pop()
{
 return stackPtr[topIdx--];
}


int main()
{
 Stack stack(10);
 stack.Push('A');
 stack.Push('B');
 stack.Push('C');

 for (int i=0;i<3;i++)
 {
  cout<<stack.Pop()<<endl;
 }
 return 0;
}

  •  위의 소스는 Stack의 역할을 보여주는 것이다.

    • 이제 이것을 템플릿화 해보겠다.

 #include <iostream>

using namespace std;

template <typename T>
class Stack
{
private:
 int topIdx;
 T* stackPtr;
public:
 Stack(int s =10);
 ~Stack();
 void Push(const T& pushValue);
 T Pop();
};

template<typename T>
Stack<T>::Stack(int len)
{
 topIdx = -1;
 stackPtr = new T[len];
}
template<typename T>
Stack<T>::~Stack()
{
 delete[] stackPtr;
}

template<typename T>
void Stack<T>::Push(const T& pushValue)
{
 stackPtr[++topIdx] = pushValue;
}
template<typename T>
T Stack<T>::Pop()
{
 return stackPtr[topIdx--];
}


int main()
{
 Stack<char> stack1(10);
 stack1.Push('A');
 stack1.Push('B');
 stack1.Push('C');

 for (int i=0;i<3;i++)
 {
  cout<<stack1.Pop()<<endl;
 }

 Stack<int> stack2(10);
 stack2.Push(10);
 stack2.Push(20);
 stack2.Push(30);

 for (int i=0;i<3;i++)
 {
  cout<<stack2.Pop()<<endl;
 }
 return 0;
}

  • 위와 같이 자주 사용하는 자료구조와 알고리즘을 템플릿으로 정의해 놓으면 좋다.

    • STL이라는 템플릿 모음이 있다.

 

IV. 템플릿의 원리 이해

  • 여기는 책을 읽어보면 될듯 하다.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

013. 예외처리(Exception Hangling)  (0) 2008.12.07
011. string 클래스 디자인  (0) 2008.12.07
010. 연산자 오버로딩  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
Posted by kid1412
,

11. string 클래스 디자인

I. C++ 표준 라이브러리

  • 책을 읽어보시오

 

II. 표준 string 클래스

  1.  #include <iostream>
    #include <string>
    using namespace std;
  2. int main()
    {
     string str1 = "Good";
     string str2 = "morning";
     string str3 = str1+str2;
  3.  cout<<str1<<endl;
     cout<<str2<<endl;
     cout<<str3<<endl;
  4.  str1 += str2;
  5.  if (str1==str3)
     {
      cout<<"equal!"<<endl;
     }
     string str4;
     cout<<"문자열 입력 : ";
     cin>>str4;
     cout<<"입력한 문자열 : "<<str4<<endl;
     return 0;
    }
  •  예시이다.

 

III. 사용자 정의 string 클래스

  • 구현해야 할 string 클래스의 특징이다.

    • 첫번째, 생성자, 소멸자, 복사 생성자 그리고 대입 연산자를 정의해야한다.
    • 둘째, 문자열을 인자로 전달받는 생성자를 정의해야 한다.
    • 셋째, + 연산자를 오버로딩해야 한다.
    • 넷째 <<연산자를 오버로딩해야한다.
    • 다섯째 +=연산자를 오버로딩해야한다.
    • 여섯째 내용 비교를 하도록 == 연산자를 오버로딩해야한다.
    • 일곱째 >> 연산자를 오버로딩해야한다.
  1. #include <iostream>
  2. using std::cout;
    using std::ostream;
    using std::istream;
    using std::endl;
  3. class string
    {
     int len;
     char* str;
    public:
     string(const char* s=NULL);
     string(const string& s);
     ~string(){}
  4.  string& operator=(const string& s);
     string& operator+=(const string& s);
     bool operator==(const string& s);
     string operator+(const string& s);
  5.  friend ostream& operator>>(ostream& os, const string& s);
     friend istream& operator<<(istream& is, string & s);
    };
  • 첫째인 생성자, 소멸자, 복사 생성자, 대입 연산자의 정의에 대한 것이다.
  1. string::string(const char *s)
    {
     len=(s!=NULL?strlen(s)+1:1);
     str = new char[len];
  2.  if(s!=NULL)
     {
      strcpy(str,s);
     }
    }
  • string 클래스에 필요로 하는 생성자이다.
  1. string::string(const string &s)
    {
     len=s.len;
     str=new char[len];
     strcpy(str,s.str);
    }
  2. string::~string()
    {
     delete []str;
    }
  • 위의 소스는 소멸자와 깊은 복사를 하는 복사 생성자 정의이다.
  1. string& string::operator =(const string &s)
    {
     delete []str;
     len=s.len;
     str = new char[len];
     strcpy(str,s.str);
     return *this;
    }
  • 위의 소스는 대입연산자 정의이다.
  1. string string::operator +(const string &s)
    {
     char* tStr=new char[len+s.len-1];
     strcpy(tStr,str);
     strcat(tStr,s.str);
  2.  string temp(tStr);
     delete []tStr;
     return temp;
    }
  •  operator+에 대한 연산자의 오버로딩이다.
  1. ostream& operator<<(ostream& os, const string& s)
    {
     os<<s.str;
     return os;
    }
  • 문자열을 출력할수 있도록 <<연산자를 오버로딩 한것이다.
  1. string& string::operator +=(const string &s)
    {
     len = len+s.len-1;
     char* tStr=new char[len];
     strcpy(tStr,str);
     delete []str;
  2.  strcat(tStr,s.str);
     str=tStr;
     return *this;
    }
  • operator += 연산자의 오버로딩이다.
  1.  bool string::operator ==(const string &s)
    {
     return strcmp(str,s.str)? false:true;
    }
  • 내용을 비교하는 ==연산자 오버로딩이다.
  1. istream& operator>>(istream& is,string& s)
    {
     char str[100];
     is>>str;
  2.  s=string(str);
     return is;
    }
  • >>연산자의 오버로딩이다.
  1. #include <iostream>
  2. using std::cout;
    using std::ostream;
    using std::istream;
    using std::endl;

  3. class string
    {
     int len;
     char* str;
    public:
     string(const char* s=NULL);
     string(const string& s);
     ~string();
  4.  string& operator=(const string& s);
     string& operator+=(const string& s);
     bool operator==(const string& s);
     string operator+(const string& s);
  5.  friend ostream& operator<<(ostream& os, const string& s);
     friend istream& operator>>(istream& is, string & s);
    };
    string::string(const char *s)
    {
     len=(s!=NULL?strlen(s)+1:1);
     str = new char[len];
  6.  if(s!=NULL)
     {
      strcpy(str,s);
     }
    }
    string::string(const string &s)
    {
     len=s.len;
     str=new char[len];
     strcpy(str,s.str);
    }
  7. string::~string()
    {
     delete []str;
    }
    string& string::operator =(const string &s)
    {
     delete []str;
     len=s.len;
     str = new char[len];
     strcpy(str,s.str);
     return *this;
    }
    string string::operator +(const string &s)
    {
     char* tStr=new char[len+s.len-1];
     strcpy(tStr,str);
     strcat(tStr,s.str);
  8.  string temp(tStr);
     delete []tStr;
     return temp;
    }
    ostream& operator<<(ostream& os, const string& s)
    {
     os<<s.str;
     return os;
    }
    string& string::operator +=(const string &s)
    {
     len = len+s.len-1;
     char* tStr=new char[len];
     strcpy(tStr,str);
     delete []str;
  9.  strcat(tStr,s.str);
     str=tStr;
     return *this;
    }
    bool string::operator ==(const string &s)
    {
     return strcmp(str,s.str)? false:true;
    }
    istream& operator>>(istream& is,string& s)
    {
     char str[100];
     is>>str;
  10.  s=string(str);
     return is;
    }
    int main()
    {
     string str1 = "hello world";
  11.  cout<<str1<<endl;
     return 0;
    }
  • 여지껏 만든 것이 string헤더에 있는 string과 같은 역할을 하는 것을 만들어 보았다.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

013. 예외처리(Exception Hangling)  (0) 2008.12.07
012. 템플릿(Template)  (0) 2008.12.07
010. 연산자 오버로딩  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
Posted by kid1412
,

010. 연산자 오버로딩

I. 연산자를 오버로딩한다는 것은 어떤 의미인가?

  •  operator+라는 이름의 함수
  1. #include <iostream>
  2. using namespace std;
  3. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     void ShowPosition();
     void operator+(int val);
    };
  4. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  5. void Point::operator+(int val)
    {
     x += val;
     y += val;
    }
  6. int main()
    {
     Point p(3,4);
     p.ShowPosition();
  7.  p.operator +(10);
     p.ShowPosition();
     return 0;
    }
  •  operator + 에 10을 전달하면 각각의 수에 10을 더해진다 여기서 조금 변형을 해본다.
  1. int main()
    {
     Point p(3,4);
     p.ShowPosition();
  2.  p+10;
     p.ShowPosition();
     return 0;
    }
  • 위의 소스를 보면 p+10이라고 했는데도 되었다.

    • 이유는 위에서 operator + 라고 지정을 해주었기 때문에 +를 쓰면 알아서 operator+로 해석을 한다.
    • 이것이 연산자 오버로딩이다.

 

II. 연산자를 오버로딩하는 두 가지 방법

  • 멤버 함수에 의한 연산자 오버로딩
  1. #include <iostream>
    using namespace std;
    class Point
    {
    private:
     int x,y;
    public:
     Point(int _x =0,int _y = 0):x(_x),y(_y){}
     void ShowPosition();
     Point operator+(const Point& p);
    };
    Point Point::operator +(const Point& p) cost
    {
     Point temp(x+p.x,y+p.y);
     return temp;
    }
    void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  2. int main()
    {
     Point p1(1,2);
     Point p2(2,1);
     Point p3 = p1+p2;
     p3.ShowPosition();
     return 0;
    }
  •  Point Point::operator+(const Point& p) 이 구문을 보면 operator+ 함수를 Point 객체로 받고 있다. 단 성능 향상을 위해서 레퍼런스로 받고 있으며 전달 인자의 변경을 허용하지 않기 위해서 const를 썼다. 함수 내에서 멤버 변수의 조작을 할 필요가 없으므로 함수도 const를 했다. 안정성을 위한 것이다.

 

  • 전역 함수에 의한 오버로딩
  1. #include <iostream>
    using namespace std;
    class Point
    {
    private:
     int x,y;
    public:
     Point(int _x =0,int _y = 0):x(_x),y(_y){}
     void ShowPosition();
     friend Point operator+(const Point& p1,const Point& p2);
    };
    Point operator +(const Point& p1,const Point& p2)
    {
     Point temp(p1.x+p2.x,p1.y+p2.y);
     return temp;
    }
    void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  2. int main()
    {
     Point p1(1,2);
     Point p2(2,1);
     Point p3 = p1+p2;
     p3.ShowPosition();
     return 0;
    }
  • 이번에는 +연산자를 오버로딩하면서 전역함수로 나타낸 것이다. friend는 private에 있는 변수에 접근하기 위에서 쓴 것이다.

    • 객체지향에는 전역이라는 개념이 존재하지 않기 때문에 가급적 멤버함수를 이용하는 것이 좋다. (간결해 진다고 한다.그러나 전역함수를 써야만 하는 경우도 있다.)

 

  • 오버로딩이 가능한 연산자의 종류

    • 오버로딩이 불가능한 연산

      • . .* :: ?: sizeof

 

  • 연산자 오버로딩에 있어서의 주의사항

    • 첫번째 : 본 의도를 벗어난 연산자 오버로딩은 좋지 않다.
    • 두번째 : 연산자 우선 순위와 결합성을 바꿀 수는 없다.

    • 세번째 : 디폴트 매개 변수 설정이 불가능하다.
    • 네번째 : 디폴트 연산자들의 기본 기능까지 빼앗을 수는 없다.

 

III. 단항 연산자의 오버로딩

  •  증가, 감소 연산자 오버로딩
  1.  #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     void ShowPosition();
     Point& operator++();
     friend Point& operator--(Point& p);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
    Point& Point::operator ++()
    {
     x++;
     y++;
     return *this;
    }
  4. Point& operator--(Point& p)
    {
     p.x--;
     p.y--;
     return p;
    }
  5. int main()
    {
     Point p(1,2);
     ++p;
     p.ShowPosition();
     --p;
     p.ShowPosition();
     ++(++p);
     p.ShowPosition();
     --(--p);
     p.ShowPosition();
  6.  return 0;
    }
  • *this가 의미하는 바는?

    • 자기 자신을 리턴하기 위해서
  • x와 y를 증가 시킨후에  자기자신을 리턴을 한 이유는?

    • ++p 연산 후에 그 다음 연산을 가능케 하기 위해서
  • 리턴 타입이 Point& 이유는?

    • 만약 리턴 타입이 Point 였다면 1만 증가 하게 된다. 이유는 ++p 연산에 의해서 리턴 되는 것은 p의 참조가 아닌  p의 복사본이기 때문이다.

       

  • 선 연산과 후 연산의 구분
  1. #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     void ShowPosition();
     Point& operator++();
     friend Point& operator--(Point& p);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
    Point& Point::operator ++()
    {
     x++;
     y++;
     return *this;
    }
  4. Point& operator--(Point& p)
    {
     p.x--;
     p.y--;
     return p;
    }
  5. int main()
    {
     Point p1(1,2);
     (p1++).ShowPosition();
     
     Point p2(1,2);
     (++p2).ShowPosition();
     return 0;
  6. }
  •  위의 소스의 결과값을 찍어보면 원래 나와야할 1,2가 아닌 2,3이 나와 버린다. 왜 그럴까?

    • 원래는 연산자의 위치에 따라 의미가 달라지는데 ++p와 p++가 동일하게 해석이 되어 버린다.
    • ++ 연산의 경우, 전위 증가와 후위 증가의 형태를 구분 짓기 어려워서, 후위 증가를 위한 함수를 오버로딩할 경우, 키워드 int를 매개변수로 선언을 하면 된다.
  1.  Point Point::operator ++(int)
    {
     Point temp(x,y);  // Point temp(*this)
     x++;                  // ++(*this)
     y++;                  //
     return temp;
    }
  • 위와 같이 정의를 하면 된다.

    •  Point temp(*this 이런 형태로 구현하는 것도 좋다. 이는 복사 생성자를 호출하는 형태로 객체를 복사하는 것이다.
    • 객체는 지역적으로 선언되어 있는 지역객체이다. 전역 객체가 아닌기 때문에 레퍼런스 형태로 리턴하면 안된다.

 

IV. 교환 법칙 해결하기

  •  교환 법칙의 적용

    • 앞에서 제시한  소스들에서 p+1은 되어도 1+p는 안되었다.
  1. #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x =0, int _y=0):x(_x),y(_y){}
     void ShowPosition();
     Point operator+(int val);
     friend Point operator+(int val,Point& p);
    };
  3. void Point::ShowPosition()
    {
     cout<<x<<" "<<y<<endl;
    }
  4. Point Point::operator +(int val)
    {
     Point temp(x+val,y+val);
     return temp;
    }
  5. Point operator+(int val,Point& p)
    {
     return p+val;
    }
  6. int main()
    {
     Point p1(1,2);
     Point p2 = p1+3;
     p2.ShowPosition();
  7.  Point p3 = 3+p2;
     p3.ShowPosition();
  8.  return 0;
    }
  •   이런식으로 소스를 추가하면 p+1 이나 1+p와 같은 교환법칙이 성립하게 된다.

 

  • 임시 객체의 생성
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
     char name[20];
    public:
     AAA(char* _name)
     {
      strcpy(name,_name);
      cout<<name<<" 객체 생성 "<<endl;
  3.  }
     ~AAA()
     {
      cout<<name<<"객체 소멸"<<endl;
     }
    };
  4. int main()
    {
     AAA aaa("aaaOBJ");
     cout<<" ---------------- 임시 --------------------"<<endl;
     AAA("temp");
     cout<<" ---------------- 임시 --------------------"<<endl;
     return 0;
    }
  •  실행을 해보면 임시 객체가 생성되었다가, 그 다음 줄로 넘어가면서 바로 소멸된다는 것을 보여주고 있다.

    • 이러한 임시 객체를 어디에 쓸까?
  1. Point Point::operator+(int val)         //Point Point::operator+(int val)
  2. {                                                //{
  3. Point temp(x+val,y+val);               // return Point(x+val,y+val);
  4. return temp;                                // }
  5. }
  • 원래 소스에서 주석쪽 소스로 바꾸면 (기능은 같다.) 주석쪽 소스가 임시 객체를 생성하자마자 바로리턴해 주고 있다.

 

V. cout, cin 그리고 endl의 비밀

  • cout, cin 그리고 endl의 구현 이해
  1. #include <stdio.h>
  2. namespace mystd
    {
     char* endl= "\n";
     class ostream
     {
     public:
      void operator<<(char*str)
      {
       printf("%s",str);
      }
      void operator<<(int i)
      {
       printf("%d",i);
      }
      void operator<<(double i)
      {
       printf("%e",i);
      }
     };
     ostream cout;
    }
  3. using namespace mystd;
  4. int main()
    {
     cout<<"Hello World\n";
     cout<<3.14;
     cout<<endl;
     cout<<1;
     cout<<endl;
     return 0;
    }
  •  위에서 만든 네임스페이스를 사용해서 출력이 잘된다.

    • 하지만 cout<<"hello"<<100<<3.12<<endl; 이렇게 쳐서 컴파일 하면 오류가 난다.
    • (((cout<<"hello")<<100) <<3.12)<<endl;이런 식으로 하면 잘 된다. 귀찮기 때문에 소스를 좀 변경해 보자.
  1.  ostream& operator<<(char*str)
      {
       printf("%s",str);
       return *this;
      }
      ostream& operator<<(int i)
      {
       printf("%d",i);
       return *this;
      }
      ostream& operator<<(double i)
      {
       printf("%e",i);
       return *this;
      }
  • 위에 처럼 연산자를 오버로딩하고 있는 operator<< 함수는 cout 객체를 반환하면 된다.

 

VI. 배열의 인덱스 연산자 오버로딩의 예

  • 기본 자료형 데이터를 저장할 수 있는 배열 클래스
  1.  #include <iostream>
  2. using namespace std;
  3. const int SIZE = 3;
  4. class Arr
    {
    private:
     int arr[SIZE];
     int idx;
    public:
     Arr():idx(0){}
     int GetElem(int i);
     void SetElen(int i,int elem);
     void AddElem(int elem);
     void ShowAllData();
  5. };
  6. int Arr::GetElem(int i)
    {
     return arr[i];
    }
  7. void Arr::SetElen(int i, int elem)
    {
     if (idx <=i)
     {
      cout<<"존재하지 않는 요소!"<<endl;
      return;
     }
     arr[i] = elem;
    }
    void Arr::AddElem(int elem)
    {
     if (idx>=SIZE)
     {
      cout<<" 용량 초과"<<endl;
      return ;
     }
     arr[idx++] = elem;
    }
    void Arr::ShowAllData()
    {
     for (int i=0;i<idx;i++)
     {
      cout<<"arrr ["<<i<<"] = "<<arr[i]<<endl;
     }
    }
  8. int main()
    {
     Arr arr;
     arr.AddElem(1);
     arr.AddElem(2);
     arr.AddElem(3);
     arr.ShowAllData();
  9.  arr.SetElen(0,10);
     arr.SetElen(1,20);
     arr.SetElen(2,30);
  10.  cout<<arr.GetElem(0)<<endl;
     cout<<arr.GetElem(1)<<endl;
     cout<<arr.GetElem(2)<<endl;
     return 0;
    }
  • 위에서 정의한 클래스가 보다 배열다워지려면 인덱스 연산자 [] 를 통한 참조가 가능해야 한다.
  1. int main()
    {
     Arr arr;
     arr.AddElem(1);
     arr.AddElem(2);
     arr.AddElem(3);
     arr.ShowAllData();
     arr[0]=10;
     arr[1]=20;
     arr[2]=30;
     for (int i;i<SIZE;i++)
     {
      cout<<arr[i]<<endl;
     }
     return 0;
    }
  • 위의 소스를 찍어 내려면
  1. int& Arr::operator[](int i)
    {
     return arr[i];
    }
  • 위의 함수를 추가해 주면 된다.

 

  • 객체를 저장할 수 있는 배열 클래스
  1.  #include <iostream>
    using namespace std;
  2. /******************* POINT Class*************************/
    class Point
    {
    private:
     int x,y;
    public:
     Point (int _x =0 , int _y=0):x(_x),y(_y){}
     friend ostream& operator<<(ostream& os, const Point& p);
    };
  3. ostream& operator<<(ostream& os, const Point& p)
    {
     os<<"["<<p.x<<" ," <<p.y<<"]";
     return os;
    }
  4. /****************** PointArr Class*********************/
  5. const int Size = 3;
    class PointArr
    {
    private:
     Point arr[Size];
     int idx;
    public:
     PointArr():idx(0){}
     void AddElem(const Point& elem);
     void ShowAllData();
     Point& operator[](int i);
    };
  6. void PointArr::AddElem(const Point &elem)
    {
     if (idx >= Size)
     {
      cout<<" 용량 초과"<<endl;
      return;
     }
     arr[idx++] = elem;
    }
    void PointArr::ShowAllData()
    {
     for (int i=0;i<idx;i++)
     {
      cout<<"arr["<<i<<"]="<<arr[i]<<endl;
     }
    }
    Point& PointArr::operator [](int i)
    {
     return arr[i];
    }
  7. int main()
    {
     PointArr arr;
     arr.AddElem(Point(1,1));
     arr.AddElem(Point(2,2));
     arr.AddElem(Point(3,3));
     arr.ShowAllData();
  8.  arr[0] = Point(10,10);
     arr[1] = Point(20,20);
     arr[2] = Point(30,30);
  9.  cout<<arr[0]<<endl;
     cout<<arr[1]<<endl;
     cout<<arr[2]<<endl;
    }
  • 메인에서 arr.AddElem(Point(1,1));줄에서 생성하는 Point 객체는 임시 객체의 형태는 띠고 있다.

 

VII. 반드시 해야만 하는 대입 연산자의 오버로딩

  •  오버로딩된 대입 연산자의 존재 : 디폴트 대입 연산자
  1. #include <iostream>
    using namespace std;
  2. class Point
    {
    private:
     int x,y;
    public:
     Point(int _x=0,int _y=0):x(_x),y(_y){}
     friend ostream& operator<<(ostream& os,const Point& p);
  3. };
  4. ostream& operator<<(ostream& os,const Point& p)
    {
     os<<"["<<p.x<<","<<p.y<<"]"<<endl;
     return os;
    }
  5. int main()
    {
     Point p1(1,3);
     Point p2(10,30);
     cout<<p1<<endl;
     cout<<p2<<endl;
     p1=p2;
     cout<<p1<<endl;
     return 0;
    }
  •  메인에서 보면 p1 = p2 이렇게 되어있는데 =연산자를 오버로딩하고 있는 함수는 보이지 않는다.

    • 디폴트 대입 연산자가 제공되기 때문이다. 멤버 변수 대 멤버 변수의 복사가 이뤄지고 있음알 수 있기 때문에 밑의 소스와 유사할 것이다.
    • 리턴 타입이 Point&형이고 자기자신을 리턴하는 이유는 p1=p2=p3 같은 연산을 허용하기 위해서이다

 

  • 디폴트 대입 연산자의 문제점
  1. #include <iostream>
    using namespace std;
  2. class Person
    {
    private:
     char* name;
    public:
     Person(char* _name);
     Person(const Person& p);
     ~Person();
     friend ostream& operator<<(ostream& os, const Person& p);
  3. };
    Person::Person(char* _name)
    {
     name = new char[strlen(_name)+1];
     strcpy(name,_name);
    }
    Person::Person(const Person &p)
    {
     name = new char[strlen(p.name)+1];
     strcpy(name,p.name);
    }
    Person::~Person()
    {
     delete[] name;
    }
  4. ostream& operator<<(ostream& os, const Person& p)
    {
     os<<p.name;
     return os;
    }
    int main()
    {
     Person p1("lee");
     Person p2("hone");
  5.  cout<<p1<<endl;
     cout<<p2<<endl;
     p1=p2;
     cout<<p1<<endl;
     return 0;
    }
  • 위의 소스를 컴파일후 실행 하면 실행중에 오류가 난다. 왜냐 하면 디폴트 대입 연산자 때문이다.

    • 메모리 공간을 동적 할당 했기 때문에 깊은 복사를 하도록 해야하는데 디폴트 대입 생성자는 얕은 복사를 하기 때문이다.
    • 또 하나의 문제는 메모리 유출에 관련이 있다.
  1.  Person& Person::operator=(const Person& p)
    {
     delete []name;
     name = new char[strlen(p.name)+1];
     strcpy(name,p.name);
     return *this;
    }
  • 위의 소스를 추가를 해주면 된다.

    • 생성자 내에서 메모리 공간을 동적 할당하게 되면, 할당된 메모리를 해제하는용도의 소멸자를 정의해야하며, 복사 생성자와 대입 연산자도 깊은 복사를 하도록 정의해야한다.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

012. 템플릿(Template)  (0) 2008.12.07
011. string 클래스 디자인  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
007. 상속(Inheritance)의 이해  (0) 2008.12.07
Posted by kid1412
,

009. virtual의 원리와 다중 상속

I. 클래스의 멤버 함수는 사실 어디에

  1.  #include <iostream>
  2. using namespace std;

  3. class Data
    {
     int data;
    public:
     Data(int num)
     {
      data = num;
     }
     void ShowData()
     {
      cout<<"data : "<<data<<endl;
     }
     void Add(int num)
     {
      data +=num;
     }
    };
  4. int main()
    {
     Data ddd(10);
     ddd.Add(10);
     ddd.ShowData();
     return 0;
    }
  •   구조체와 전역 함수로 위의 클래스를 흉내내 보자
  1. #include <iostream>
  2. using namespace std;
  3. struct Data
    {
     int data;
     void (*ShowData)(Data*);
     void (*Add)(Data*,int);
    };
  4. void ShowData(Data* THIS)
    {
     cout<<"Data : "<<THIS->data<<endl;
    }
    void Add(Data* THIS,int num)
    {
     THIS->data += num;
    }
  5. int main()
    {
     Data ddd1 = {10,ShowData,Add};
  6.  ddd1.Add(&ddd1,10);
  7.  ddd1.ShowData(&ddd1);
  8.  Data ddd2 = {1,ShowData,Add};
     ddd2.Add(&ddd2,1);
     ddd2.ShowData(&ddd2);
     return 0;
    }
  •  위의 소스를 실행하면 알수 있듯이 객체가 생성되면, 멤버 변수는 객체내에 존재하게된다. 그러나 멤버 함수는 메모리의 한 공간에 존재하면서, 모든 객체가 공유하는 형태를 취하게 된다.

 

II. 가상 함수가 동작하는 원리

  1.  #include <iostream>
  2. using namespace std;
  3.  class A
     {
      int a;
      int b;
  4.  public:
      virtual void fct1(){cout<<"fct1(...)"<<endl;}
      virtual void fct2(){cout<<"fct2(...)"<<endl;}
     };
  5.  class B : public A
     {
      int c;
      int d;
  6.  public:
      virtual void fct1(){cout<<"overriding fct1(...)"<<endl;}
      void fct3(){cout<<"fct3(...)"<<endl;}
     };
  7.  int main()
     {
      A* aaa = new A();
      aaa->fct1();
  8.   B* bbb = new B();
      bbb->fct1();
      return 0;
     }
  •   가상 함수가 하나 이상 있을 시 컴파일러는 가상함수 테이블이라는 것을 만들어 둔다.

    • 가상함수 테이블에서는 오버라이딩된 클래스의 가상 함수 fct1에 대한 정보는 포함되어있지 않다.
    • 즉, 하나 이상의 가상함수를 멤버로 지니는 클래스의 객체에는 가상함수테이블을 위한 포인터가 멤버로 추가된다.

 

 III. 다중 상속에 대한 이해

  • 다중 상속이란

    • 하나의 상속받는 클래스가 둘 이상의 기본 클래스를 상속하는 것을 말한다.
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void String1()
     {
      cout<<"AAA:String1"<<endl;
     }
    };
  4. class BBB
    {
    public:
     void String2()
     {
      cout<<"BBB:String2"<<endl;
     }
    };
  5. class CCC:public AAA,public BBB
    {
    public:
     void ShowString()
     {
      String1();
      String2();
     }
    };

  6. int main()
    {
     CCC ccc;
     ccc.ShowString();
     return 0;
    }
  • CCC에서 AAA와 BBB를 둘다 상속 받았기 때문에 String1과 String2를 호출 할수 있다.

 

IV. 다중 상속의 모호성

  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void String()
     {
      cout<<"AAA:String1"<<endl;
     }
    };
  4. class BBB
    {
    public:
     void String()
     {
      cout<<"BBB:String2"<<endl;
     }
    };
  5. class CCC:public AAA,public BBB
    {
    public:
     void ShowString()
     {
      String();
      String();
     }
    };

  6. int main()
    {
     CCC ccc;
     ccc.ShowString();
     return 0;
    }
  • 위와 같은 경우 오류가 나는데 AAA에서나 BBB에서 String이라는 함수를 썼기 때문이다.

    • 따라서 ShowString에서 AAA::String, BBB::String 이런식으로 바꾸어 주어야 한다.

 

V. virtual Base 클래스

  1.  #include <iostream>
  2. using namespace std;

  3. class AAA
    {
    public:
     void String1()
     {
      cout<<"AAA::String"<<endl;
     }
    };
  4. class BBB:public AAA
    {
    public:
     void String2()
     {
      cout<<"BBB::String"<<endl;
     }
    };
  5. class CCC : public AAA
    {
    public:
     void String3()
     {
      cout<<"CCC::String"<<endl;
     }
    };
  6. class DDD:public BBB,public CCC
    {
    public:
     void ShowString()
     {
      String1();
      String2();
      String3();
     }
    };
  7. int main()
    {
     DDD ddd;
     ddd.ShowString();
     return 0;
    }
  • 위의 소스를 컴파일 하면 String1에서 오류가 난다.

    • 이유는 DDD에서 String1을 호출 하려고 하지만 상속받은 BBB를 참조해야할지 CCC를 참조해야할지 애매해서 오류가 난다.
  1. class BBB:virtual public AAA
    {
    public:
     void String2()
     {
      cout<<"BBB::String"<<endl;
     }
    };
  2. class CCC : virtual public AAA
    {
    public:
     void String3()
     {
      cout<<"CCC::String"<<endl;
     }
    };

- 이런식으로 AAA를 virtual로 상속 받으면 된다. 따라서 AAA클래스 안에 존재하는 멤버는 한번만 상속받게 된다.

 

 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

011. string 클래스 디자인  (0) 2008.12.07
010. 연산자 오버로딩  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
007. 상속(Inheritance)의 이해  (0) 2008.12.07
006. static 멤버와 const 멤버  (0) 2008.12.07
Posted by kid1412
,

008. 상속과 다형성

I. 상속의 조건(미완성)

  • 상속은 만병통치약이 아니다.

    • 잘못된 상속의 경우 클래스의 관계를 복잡하게 만드는 단점이 있다.
  • IS-A 관계에 의한 상속

    • is a 관계라는 것은 ~은 ~이다. 라는 뜻으로 class Student : public Person 이면 Student class는 public Person 이다. 라는 것이다.
  •  Has-A 관계에 의한 상속! 그리고 대안

    • 상속은 소유를 표현하기 위해서도 사용된다.

 

II. 상속된 객체와 포인터의 관계

  • 객체 포인터 : 객체의 주소 값을 저장할 수 있는 포인터
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"Study"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work"<<endl;
     }
    };
  6. int main()
    {
     Person* p1 = new Person;
     Person* p2 = new Student;
     Person* p3 = new PartTimeStd;
  7.  p1->Sleep();
     p2->Sleep();
     p3->Sleep();
     return 0;
    }
  • 메인에서 p1은 Person을 p2는 Student를 p3는 PartTimestd를 가리킨다. 그렇기 때문에 sleep을 사용할 수 있다.

    • Person* p1 = new PartTimestd; 처럼 바꾸어도 컴파일이 된다.

      • 이유는 PartTimestd 클래스의 객체는 PartTimestd 객체이자, Student객체이면서 동시에 Person객체 이기도 하다.

 

  • Employee problem 해결의 첫번째 단계

    • 이부분은 책으로 봐주세요

 

  • 객체 포인터의 권한

    • 클래스의 객체 포인터는 가리키는 대상이 어떠한 객체이건, 클래스 타입 내에 선언된 멤버와 클래스가 상속한 클래스의 멤버에만 접근이 가능하다.

III. 상속된 객체와 참조의 관계

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

    • AA 클래스의 포인터는 *AAA 객체의 주소 값 뿐만 아니라, AAA 클래스를 상속받는 클래스 객체의 주소 값도 저장이 가능하다.
    • AAA 클래스의 레퍼런스는 AAA 객체 뿐만아니라, AAA 클래스를 상속받는 클래스의 객체도 참조 가능하다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep()"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"study()"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work()"<<endl;
     }
    };
  6. int main()
    {
     PartTimeStd p;
     Student& ref1 = p;
     Person& ref2 = p;
  7.  p.Sleep();
     ref1.Sleep();
     ref2.Sleep();
     
     return 0;
    }
  • 객체를 참조하는 레퍼런스의 특징을 보여주는 소스이다.

 

  • 객체 레퍼런스의 권한

    • AAA 클래스의 객체 포인터는 가리키는 대상이 어떠한 객체이건, AAA 클래스 타입 내에 선언된 멤버와 AAA클래스가 상속한 클래스의 멤버에만 접근이 가능하다.
    • AAA 클래스의 레퍼런스는 참조하는 대상이 어떠한 객체이건, AAA 클래스 타입 내에 선언된 멤버와 AAA ㅡㅋㄹ래스가 상속한 클래스의 멤버에만 접근이 가능하다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
    public:
     void Sleep()
     {
      cout<<"Sleep()"<<endl;
     }
    };
  4. class Student : public Person
    {
    public:
     void Study()
     {
      cout<<"study()"<<endl;
     }
    };
  5. class PartTimeStd : public Student
    {
    public:
     void Work()
     {
      cout<<"Work()"<<endl;
     }
    };
  6. int main()
    {
     PartTimeStd p;
     p.Sleep();
     p.Study();
     p.Work();
  7.  Person& ref = p;
     ref.Sleep();
     ref.Study();
     ref.Work();
     
     return 0;
    }
  • 위의 소스에서 오류가 나는데 메인에서 ref.Study()와 ref.Work()가 오류가 난다.

    • 이유는 ref에 p를 선언 하였지만 Person클래스 이기 때문에 Person클래스 안에 있는 Sleep만 불러 올수 있다.

 

IV. Static Binding & Dynamic Binding

  •  오버라이딩의 이해
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB b;
     b.fct();
     return 0;
    }
  •   class BBB나 class AAA 에서 fct 함수를 같이 썼는데 메인에서 BBB에 있는 fct를 호출하게 된다.

    • 이유는 AAA에 정의했던 fct는 BBB에서 정의했던 fct 함수에 가져진 것이다.

 

  • 오버라이딩된 함수를 호출 하는 방법
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new BBB;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  •  오버라이딩 된 함수를 호출 하는 방법은 포인터를 써서 하는 것이다.

    • AAA* a = b; 처럼 포인터 b가 지니고 있는 값을 a에 대입 시켜 주므로써 fct를 호출 하면 AAA에 있는 것이 호출 된다.

      • 여기는 설명이 좀 이상하니 나중에 보충하겠습니다.

 

  • 멤버 함수를 가상(virtual)으로 선언하기

    • 오버라이딩되는 함수를 가상으로 선언할수 있다. 즉, dynamic binding을 한다는 의미이다.
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new BBB;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  • 결과값을 보면 AAA 클래스 안에 fct가 없는 것처럼 나온다. 가상의 함수로 설정해 놨기 때문에 BBB AAA 가 나와야 할것이 BBB BBB가 나온 것이다.

 

  • 가상 함수의 특성은 상속 된다.
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
    class CCC : public BBB
    {
    public:
     void fct()
     {
      cout<<"CCC"<<endl;
     }
    };
  5. int main()
    {
     BBB *b = new CCC;
     b->fct();
  6.  AAA* a = b;
     a->fct();
  7.  delete b;
     return 0;
    }
  •  위의 결과를 찍어보면 CCC CCC가 나오는데 AAA에 있는 virtual 로 선언하여 상속 받은 BBB나 CCC가 fct 함수가 virtual이 선언 되어 있는 것과 같으나 최종적으로 오버라이딩한 함수를 제외한 나머지 함수는 가려지게 되어서 CCC에있는 fct 함수가 호출된다.

 

  • 무엇이 static binding이고, 무엇이 dynamic binding인가?
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      cout<<"BBB"<<endl;
     }
    };
    int main()
    {
     BBB b;
     b.fct();
  5.  AAA* a = new BBB;
     a->fct();
     return 0;
    }
  •  main을 보면 b.fct() 가 static binding이고 AAA* a = new BBB가 dynamic binding 이다.

    • 왜냐하면 b.fct는 컴파일하는 동안에 호출될 함수가 결정되기 때문이다. 즉 호출할 함수가 이미 고정되어 있다는 것이다
    • AAA* a = new BBB는 컴파일 동안이 아닌 실행하는 동안에 호출될 함수가 결정된다. 즉 포인터가 가리키는 객체가 무엇이냐에 따라서 그 문장이 호출되는 함수가 유동적이기 때문이다.

 

  • 오버라이딩된 함수 호출하기

    • 범위 지정 연산자를 통해서 오버라이딩된 함수를 호출 가능하다.
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     virtual void fct()
     {
      cout<<"AAA"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     void fct()
     {
      AAA::fct();
      cout<<"BBB"<<endl;
     }
    };
    int main()
    {
     AAA* a = new BBB;
     cout<<"1"<<endl;
     a->fct();
  5.  cout<<"2"<<endl;
     a->AAA::fct();
     return 0;
    }
  •  첫번째 방법은  BBB를 호출시 AAA쪽에 fct를 호출하라는 뜻이다.
  • 두번째 방법은 a를 AAA:fct()로 아예 경로를 지정해 주는 방법이다.

V. Employee Problem 완전 해결

  • 여기 부분은 책을 봐주세요
  •  순수 가상 함수와 추상 클래스

    • virtual int GetPay() = 0 이런식으로 선언 한것이 순수 가상 함수이다.

      • 컴파일러에게 이 함수가 호출될 일이 없거든 선언만 하고 정의를 안한 것이다. 라고 얘기 해주는 정도의 내용이다
      • 이러한 멤버함수가 하나 이상이 있으면 추상 클래스라고 한다.

 

VI. virtual 소멸자의 필요성

  1.  #include <iostream>
  2. using namespace std;
  3. 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<<' ';
     }
    };
  4. 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;
     }
    };
  5. int main()
    {
     AAA* a = new BBB("Good","123");
     BBB* b = new BBB("Good","345");
  6.  a->ShowString();
     b->ShowString();
  7.  cout<<"-----객체 소멸 직전-----"<<endl;
     delete a;
     delete b;
     return 0;
    }
  • 위의 소스에서 main 함수를 보면 delete a를 해줄때 포인터 a를 통한 BBB 객체의 소멸을 시도 하지만 AAA 타입이기 때문에 BBB객체를 AAA 객체로 인식한다. 그래서 AAA의 소멸자만 호출이 된다.

 

  • virtual 소멸자

    • 위에 해결책은 AAA 소멸자에 virtual을 해주면 된다.
  1.  virtual ~AAA()
     {
      cout<<"~AAA() call"<<endl;
      delete []str1;
     }
  •  위의 방식은 먼저 AAA클래스의 소멸자를 호출하려 할때 virtual 관계여서 같은 역할을 하는 BBB 소멸자를 호출한다. 그런데 BBB 클래스는 AAA클래스를 상속 받고 있기 때문에 BBB 클래스의 소멸자를 한 후에 AAA 클래스의 소멸자를 호출한다.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

010. 연산자 오버로딩  (0) 2008.12.07
009. virtual의 원리와 다중 상속  (0) 2008.12.07
007. 상속(Inheritance)의 이해  (0) 2008.12.07
006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
Posted by kid1412
,

007. 상속(Inheritance)의 이해

I. 상속의 기본 개념

  • 아래의 소스를 보자
  1.  class Person
    {
     int age;
     char name[20];
    public:
     int GetAge()const
     {
      return age;
     }
     const char* GetName() const
     {
      return name;
     }
     Person(int _age=1,char* _name = "noname")
     {
      age = _age;
      strcpy(name, _name);
     }
    };
  2. class Student : public Person
    {
     char major[20];
    public:
     Student(char* _major)
     {
      strcpy(major,_major);
     }
     const char* GetMajor()const
     {
      return major;
     }
     void ShowData() const
     {
      cout<<"이름 : "<<GetName()<<endl;
      cout<<"나이 : "<<GetAge()<<endl;
      cout<<"전공 : "<<GetMajor()<<endl;
     }
    };
  • 클래스들만 정의 한 것이다.

    • 위에 소스를 보면 class Person이랑 class Student 두개의 클래스가 선언 되어 있다. 그런데 Student 클래스를 보면 class Student : public Person 이라고 되어있는데 이것이 Person 클래스를 public으로 상속을 받겠다는 뜻이다.
    • 이제 풀 소스를 보자
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
     int age;
     char name[20];
    public:
     int GetAge()const
     {
      return age;
     }
     const char* GetName() const
     {
      return name;
     }
     Person(int _age=1,char* _name = "noname")
     {
      age = _age;
      strcpy(name, _name);
     }
    };
  4. class Student : public Person
    {
     char major[20];
    public:
     Student(char* _major)
     {
      strcpy(major,_major);
     }
     const char* GetMajor()const
     {
      return major;
     }
     void ShowData() const
     {
      cout<<"이름 : "<<GetName()<<endl;
      cout<<"나이 : "<<GetAge()<<endl;
      cout<<"전공 : "<<GetMajor()<<endl;
     }
    };
  5. int main()
    {
     Student Kim("Computer");
     Kim.ShowData();
     return 0;
    }
  • 결과값을 찍어보자. 다음과 같은 그림으로 되어 있다고 보면 된다.

    • 19.jpg  

II. 상속하는 클래스의 객체 생성 및 소멸 과정

  • 객체 생성 과정의 이해
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     AAA()
     {
      cout<<"AAA() Call!!"<<endl;
     }
     AAA(int i)
     {
      cout<<"AAA(int i) Call!!"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     BBB()
     {
      cout<<"BBB() call!"<<endl;
     }
     BBB(int j)
     {
      cout<<"BBB(int j) call!"<<endl;
     }
    };
  5. int main(void)
    {
     cout<<"객체 1 생성"<<endl;
     BBB bbb1;
     cout<<"객체 2 생성 "<<endl;
     BBB bbb2(10);
     return 0;
    }
  •  위 소스의 결과값을 보면  BBB(int j)call 는 떴는데 AAA(int i) call 은 왜 안뜨는지 이상하게 여길지도 모른다.

    • 이유는 메인에서 BBB bbb2(10)으로 불렀을 때, BBB(int j)로 가지만 상속을 받아서 AAA로 갈 때는 10도 같이 넘어 가는게 아니기 때문에 디폴트생성자인 AAA()를 부르기 때문에 AAA(int i)가 결과값에 안뜨는 것이다.

 

  • 객체 생성 과정의 적용
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
     int age;
     char name[20];
    public:
     int GetAge() const
     {
      return age;
     }
     const char* GetName()  const
     {
      return name;
     }
     Person(int _age = 1,char* _name = "noname")
     {
      age= _age;
      strcpy(name,_name);
     }
    };
  4. class Student : public Person
    {
     char major[20];
    public:
     Student(int _age,char* _name, char* _major)
     {
      age = _age;
      strcpy(name,_name);
  5.   strcpy(major,_major);
     }
     const char* GetMajor() const
     {
      return major;
     }
     void ShowData() const
     {
      cout<<"이름 : "<<GetName()<<endl;
      cout<<"나이 : "<<GetAge()<<endl;
      cout<<"전공 : "<<GetMajor()<<endl;
     }
    };
  6. int main()
    {
     Student Kim(20,"Hong Gil Dong", "computer");
     Kim.ShowData();
     return 0;
    }
  •  위의 소스를 컴파일 하면 오류가 난다 어디서 나는지 보자.

    • Student 클래스 안에 생성자 안에 age = _age와 strcpy(name, _name); 이 두개가 에러가 난다. 이유가 뭘까?
    • 이유는 age와 name이 Person클래스에서 보면 private이기 때문에 오류가 난다. public 일경우는 오류가 날 일이 없다. 그럼 어찌 해야할까? Student클래스안에 생성자만 좀 바꾸어 주면 된다.
  1.  Student(int _age,char* _name, char* _major) : Person(_age,_name)
     {
      strcpy(major,_major);
     }
  • 위의 소스를 보면 Student 생성자의 int _age, char* _name이 Person클래스의 생성자를 호출하는데 쓰인다.

 

  • 객체 소멸 과정의 이해
  1. #include <iostream>
  2. using namespace std;
  3. class AAA
    {
    public:
     AAA()
     {
      cout<<"AAA() call!"<<endl;
     }
     ~AAA()
     {
      cout<<"~AAA() call!"<<endl;
     }
    };
  4. class BBB : public AAA
    {
    public:
     BBB()
     {
      cout<<"BBB() call!"<<endl;
     }
     ~BBB()
     {
      cout<<"~BBB() call!"<<endl;
     }
    };
  5. int main()
    {
     BBB bbb;
     return 0;
    }
  •  위의 소스의 결과값을 보면 먼저 생성된것은 나중에 소멸된다.

    • 그리고 메인에서 BBB를 호출했는데 AAA가 먼저 찍힌 이유는 BBB를 호출할때 AAA에서 상속 받았기 때문에 AAA클래스로 이동에서 AAA생성자부터 호출한 다음 BBB의 생성자를 호출한다. 그리고 소멸은 BBB가 소멸되고 AAA가 소멸된다. AAA가 소멸을 먼저 되게 하면 오류는 안띄어도 실행시 에러메시지가 가볍게 나와줄 것이다.

III. protected 멤버

  •  protected 멤버의 특징이다.

    • protected 멤버는 외부에서 보면 private로 보이고, 상속 관계에서 보면 public으로 보인다.
    • protected 멤버는 private 멤버와 똑같다. 다만 private 멤버와 달리 상속 관계에 놓여 있는 경우에 접근을 허용할 뿐이다.
  1. #include <iostream>
  2. using namespace std;
  3. class Person
    {
    protected:
     int age;
     char name[20];
    public:
     int GetAge() const
     {
      return age;
     }
  4.  const char* GetName() const
     {
      return name;
     }
     Person(int _age = 1,char* _name = "noname")
     {
      age = _age;
      strcpy(name,_name);
     }
    };
  5. class Student : public Person
    {
     char major[20];
    public:
     Student(int _age,char* _name, char* _major) : Person(_age,_name)
     {
      strcpy(major,_major);
     }
     const char* GetMajor() const
     {
      return major;
     }
     void ShowData() const
     {
      cout<<"이름 : "<<name<<endl;
      cout<<"나이 : "<<age<<endl;
      cout<<"전공 : "<<major<<endl;
     }
  6. };
  7. int main()
    {
     Student Kim(20,"kim","po");
     Kim.ShowData();
     return 0;
    }
  •  위의 소스를 보면 age와 name이 protected로 되어 있다. 그렇기 때문에 class Student에서 생성자 초기화시 age와 name이 private처럼 해주면 된다. 그리고 보면 ShowData에서 name,age,major 처럼 상속관계일때 외부에서 접근이 가능하다.

IV. 세가지 형태의 상속

 

 상속형태 public protected private
 기본 클래스      
 public 멤버  public protected private
 protected 멤버  protected  protected  private
 private 멤버  접근불가 접근불가 접근불가
  • 개념 정도만 알자

    • public 상속은 public 보다 접근 권한이 넓은 것을 public으로 맞춰준다.
    • protected 상속은 protected보다 접근 권한이 넓은 것을 protected으로 맞춰 준다.
    • private 상속은 private보다 접근 권한이 넓은 것은 private으로 맞춰준다.

문제

 

 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

009. virtual의 원리와 다중 상속  (0) 2008.12.07
008. 상속과 다형성  (0) 2008.12.07
006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
Posted by kid1412
,

006. static 멤버와 const 멤버

I. 클래스와 const

  • const 키워드에 대한 복습

    • 첫째. const 키워드는 변수의 선언 앞에 붙어서 변수를 상수화한다.
    • 둘째. const 키워드는 포인터가 가리키는 데이터를 상수화 한다.
    • 셋째. const 키워드는 포인터 선언 시 이름 앞에 붙어서 포인터 자체를 상수화한다.
  • 멤버 변수의 상수화, 그리고 초기화
  1. #include <iostream>
  2. using namespace std;
  3. class Student
    {
     const int id;
     int age;
     char name[20];
     char major[30];
    public:
     Student(int _id, int _age, char* _name, char* _major)
     {
      id = _id;
      age = _age;
      strcpy(name,_name);
      strcpy(major,_major);
     }
  4.  void ShowData()
     {
      cout<<"이름 : "<<name <<endl;
      cout<<"나이 : "<<age<<endl;
      cout<<"학번 : "<<id<<endl;
      cout<<"학과 : "<<major<<endl;
     }
    };
  5. int main()
    {
     Student Kim(1234,23,"l","3");
     Student Ho(12315,34,"g","re");
  6.  Kim.ShowData();
     cout<<endl;
     Ho.ShowData();
     return 0;
    }
  •  위의 소스는 컴파일이 안된다. 이유는 id를 초기화 하려고 하는데  id는 const로 설정 되어 있어서 초기화가 되지 않는다.

    • 이럴때에는 멤버 이니셜라이저 라는 문법을 사용하면 된다.
  1.   public:
     Student(int _id, int _age, char* _name, char* _major) : id(_id)
     {
      age = _age;
      strcpy(name,_name);
      strcpy(major,_major);
     }
  • 위처럼 클래스를 바꿔주면 잘된다. : id(_id)는 멤버 변수 id를 매개 변수 _id로 초기화한다는 뜻이다.

 

  • const 멤버 함수

    • 멤버 함수가 상수화되면, 이 함수를 통해서 멤버 변수의값이 변경 되는 것은 허용되지 않는다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Student
    {
     const int id;
     int age;
     char name[20];
     char major[30];
    public:
     Student(int _id, int _age, char* _name, char* _major) : id(_id),age(_age)
     {
      strcpy(name,_name);
      strcpy(major,_major);
     }
  4.  void ShowData() const
     {
      cout<<"이름 : "<<name <<endl;
      cout<<"나이 : "<<age<<endl;
      cout<<"학번 : "<<id<<endl;
      cout<<"학과 : "<<major<<endl;
     }
    };
  5. int main()
    {
     Student Kim(1234,23,"l","3");
     Student Ho(12315,34,"g","re");
  6.  Kim.ShowData();
     cout<<endl;
     Ho.ShowData();
     return 0;
    }
  • ShowData 멤버함수를 const 하였다.
  1. #include <iostream>
  2. using namespace std;
  3. class Count
    {
     int cnt;
    public:
     Count() : cnt(0){}
     int* GetPtr() const{
      return &cnt;
     }
     void Increment()
     {
      cnt++;
     }
     void ShowData()const
     {
      ShowIntro();
      cout<<cnt<<endl;
     }
     void ShowIntro()
     {
      cout<<"현재 count의 값 : "<<endl;
     }
    };
  4.  int main()
    {
     Count count;
     count.Increment();
     count.ShowData();
     return 0;
    }
  • 위의 클래스부분쪽에 const를 쓴 부분을 보자 int *GetPtr() const 와 ShowData() const에서 오류가 나는데 이유는 const된 함수는 const화 되지 않은 함수의 호출을 허용하지 않을 뿐더러 멤버 변수의 포인터를 리턴하는 것도 허용하지 않는다.
  • 출력하려면 어떻게 해야할까? 리턴 받는 값이나 리턴 하는 곳에 const화를 시켜주면 된다.
  1. class Count
    {
     int cnt;
    public:
     Count() : cnt(0){}
    const int* GetPtr() const{
      return &cnt;
     }
     void Increment()
     {
      cnt++;
     }
     void ShowData() const
     {
      ShowIntro();
      cout<<cnt<<endl;
     }
     void ShowIntro() const
     {
      cout<<"현재 count의 값 : "<<endl;
     }
    };
  • 위의 소스 처럼 바꾸면 컴파일이 되긴한다. 저렇게 되는 소스 보다 차라리 const 안쓰는 쪽으로 하는 것이 좋을 것이다.

 

  • const와 함수 오버로딩

    • 상수 함수냐 아니냐에 따라서도 함수 오버로딩은 성립한다.
  1.  #include <iostream>
  2. using namespace std;
  3. class AAA
    {
     int num;
    public:
     AAA(int _num):num(_num){}
     void ShowData()
     {
      cout<<"void ShowData(0 호출"<<endl;
      cout<<num <<endl;
     }
     void ShowData() const
     {
      cout<<"void ShowData() const 호출"<<endl;
      cout<<num<<endl;
     }
    };
  4. int main()
    {
     const AAA aaa1(20);
     AAA aaa2(70);
     aaa1.ShowData();
     aaa2.ShowData();
     return 0;
    }
  •   const가 있고 없고의 차이로 함수가 달라지는 것을 알 수 있다.

II. 클래스와 static

  • 전역 변수가 필요한 상황
  1. #include <iostream>
  2. using namespace std;
  3. 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;
  4.  }
     void ShowData()
     {
      cout<<"이름 : "<<name;
      cout<<"나이 : "<<age;
     }
    };
  5. int main(void)
    {
     Person p1("Lee",13);
     Person p2("Jong",22);
     return 0;
    }
  •  위에서 int count =1; 처럼 전역변수로 선언 하면 값이 증가를 한다.

    • 완벽한 객체지향적 프로그래밍하기 위해서바꿔 보자.

 

  •  static 멤버의 특징

    • 첫째 main 함수가 호출되기도 전에 메모리 공간에 올라가서 초기화된다. 따라서 public으로 선언이 된다면, 객체 생성 이전에도 접근이 가능하다.
    • 둘째 객체의 멤버로 존재하는 것이 아니다. 다만 선언되어 있는 클래스 내에서 직접 접근할 수 있는 권한이 부여된 것이다.
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
    public:
     static int n;
    };
  3. int AAA::n =1;
  4. int main()
    {
     cout<<AAA::n<<endl;
     AAA::n++;
     cout<<AAA::n<<endl;
     return 0;
    }
  • static 멤버는 main 함수가 호출되기 이전에 이미 메모리 공간에 올라가서 초기화된다.

    • 초기화를 해주려면 생성자를 써주면 좋겠지만 객체 생성되기 전에 메모리에 올라가기 때문에 int AAA::n = 1; 이런식으로 초기화를 해줘야 한다.
  1. #include <iostream>
    using namespace std;
  2. class AAA
    {
     int val;
     static int n;
    public:
     AAA(int a =0)
     {
      val = a;
      n++;
     }
     void ShowData()
     {
      cout<<"val : "<<val<<endl;
      cout<<"n : "<<n<<endl;
     }
    };
  3. int AAA::n =1;
  4. int main()
    {
     AAA a1(10);
     a1.ShowData();
  5.  AAA a2(20);
     a2.ShowData();
     return 0;
    }
  •  위의 소스 결과를 보면  static int n이 private로 선언되어 있기 때문에 외부(main)에서는 허용이 안된다.

    •  변수 n은 객체 안에 존재 하지 않는다. 다만 바로 접근할 수 있는 권한이주어져있어서 ShowData에서 접근을 할 수 있는 것이다.
    • static 멤버 변수나 멤버함수는 객체의 멤버가 아닌 클래스 변수, 클래스함수라고 표현하는것이 더 정확하다.

 

  • 그 이외의 키워드 : explicit & mutable

    • 명시적인 것만 허용한다! : explicit
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
    public:
     explicit AAA(int n)
     {
      cout<<"explicit AAA(int n)"<<endl;
     }
    };
  3. int main()
    {
     AAA a1(10);
     AAA a2 = 10;
     return 0;
    }
  •  위의 소스에서 생성자 AAA 앞에 explicit 키워드를 써서 묵시적인 호출을 허용하지 않게 했다.

    • main 함수 안에 AAA a1(10)은 되지만 AAA a2 = 10 은 묵시적으로 AAA a2(10)으로 변환이 되는데 그것을 허용하지 않아서 오류가 난다.

 

  • 예외를 둔다! : mutable
  1. #include <iostream>
    using namespace std;
  2. class AAA
    {
    private:
     mutable int val1;
     int val2;
    public:
     void SetData(int a, int b) const
     {
      val1 = a;
      val2 = b;
      cout<<val1<<" "<<val2<<endl;
     }
    };
  3. int main()
    {
     AAA a1;
     a1.SetData(10,20);
     return 0;
    }
  •  public 에 있는 SetData를 const로 지정하여 값을 못 바꾸게 만들고 메인에서 값을 집어 넣으면 원래 val1,val2가 오류가 나야하나 val1에서는 오류가 나지 않는다.

    • val1에 mutable를 붙여서 예외로 오류가 안나게 된 것이다. (되도록 안쓰는 편이 좋다.) 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

008. 상속과 다형성  (0) 2008.12.07
007. 상속(Inheritance)의 이해  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
003. 클래스의 기본  (0) 2008.12.07
Posted by kid1412
,

005. 복사 생성자

I. C,C++ 스타일 초기화

  • C 에서의 스타일 초기화

    • int val = 10;
  • C++에서의 스타일 초기화

    • int val(10); or int val = 10;

II. 복사 생성자의 형태

  1. #include <iostream>
    using namespace std;
  2. class AAA
    {
    public:
     AAA()
     {
      cout<<"AAA() 호출"<<endl;
     }
     AAA(int i)
     {
      cout<<"AAA(int i) 호출"<<endl;
     }
     AAA(const AAA& a)
     {
      cout<<"AAA(const AAA& a) 호출"<<endl;
     }
    };
  3. int main()
    {
     AAA obj1;
     AAA obj2(10);
     AAA obj3(obj2);
     return 0;
    }
  •  위의 소스에 main안에 보면 AAA obj3(obj2); 이것이 복사 생성자이다. 복사 생성자를 선언 할 경우 &는 꼭 선언을 해줘야 한다.

III. 디폴트 복사 생성자

  •  디폴트 복사 생성자 형태

    • Point(const Point &p) 이런 식을 들어간다.
  1.  #include <iostream>
  2. using namespace std;
  3. class Point
    {
     int x,y;
    public:
     Point(int _x,int _y)
     {
      x = _x;
      y = _y;
     }
     /*
     Point(const Point &p)
     {
      x = p.x;
      y = p.y;
     }*/ //디폴트 복사 생성자
     void ShowData()
     {
      cout<<x<<' '<<y<<endl;
     }
    };
  4. int main()
    {
     Point p1(10,20);
     Point p2(p1);
  5.  p1.ShowData();
     p2.ShowData();
    }
  • 디폴트 복사 생성자는 맴버 변수 대 멤버 변수의 복사를 수행한다.

IV. 깊은 복사를 하는 복사 생성자

  • 디폴트 복사 생성자의 문제점
  1.  #include <iostream>
  2. using namespace std;
  3. class Person
    {
     char *name;
     char *phone;
     int age;
    public:
     Person(char* _name, char* _phone,int _age);
     ~Person();
     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;
    }
  7. Person::~Person()
    {
     delete []name;
     delete []phone;
    }
  8. void Person::ShowData()
    {
     cout<<"name : "<<name<<endl;
     cout<<"phone: "<<phone<<endl;
     cout<<"age : "<<age<<endl;
    }
  9. int main()
    {
     Person p1("KIM","123-123",23);
     Person p2 = p1;
     return 0;
    }
  • 위의 소스를 실행 시켜보면 오류가 나게 된다 그 이유는 얕은 복사를 하고 있기 때문이다.

15.jpg 

  • 위의 그림이 얕은 복사이다. 위쪽의 소스가 위의 그림 처럼 되어 있는데 오류 나는 이유는 잘 실행되다가 소멸 과정(소멸은 생성이 나중에 된것 부터 소멸된다.)에서 p2가 사라지면 가리키는 값은 메모리에서 해제 되고  p1도 소멸할 때 메모리를 해제하려고보니 해제할 메모리 공간이 없어서 오류가 발생하게 되는 것 이다.

    • 디폴트 복사 생성자는 얕은 복사를 하므로, 깊은 복사를 하도록 직접 복사 생성자를 제공해야 하낟.

 

  • 깊은 복사
  1. #include<iostream>
  2. using namespace std;
  3. class Person
    {
     char *name;
     char *phone;
     int age;
    public:
     Person(char* _name, char* _phone,int _age);
     Person(const Person& p);
     ~Person();
     void ShowData();
    };
  4. Person::Person(const Person &p)
    {
     name = new char[strlen(p.name)+1];
     strcpy(name,p.name);
  5.  phone = new char[strlen(p.phone)+1];
     strcpy(phone,p.phone);
     age = p.age;
    }
  6. 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,_phone);
     age = _age;
    }
    Person::~Person()
    {
     delete []name;
     delete []phone;
    }
  7. void Person::ShowData()
    {
     cout<<"name : "<<name<<endl;
     cout<<"phone : "<<phone<<endl;
     cout<<"age : "<<age<<endl;
  8. }
  9. int main()
    {
     Person p1 ("kim","123-123",23);
     Person p2 = p1;
  10.  p1.ShowData();
     p2.ShowData();
    }
  •   위의 소스가 깊은 복사를 한 것이다. 명시적으로 복사 생성자를 만들었다.

V. 복사 생성자가 호출되는 시점

  •  복사 생성자가 호출되는 시점은

    • 기존에 생성된 객체로 새로운 객체를 초기화 하는 경우
    • 함수 호출 시 객체를 값에 의해 전달하는 경우
    • 함수 내에서 객체를 값에 의해 리턴하는 경우
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
    public:
     AAA()
     {
      cout<<"AAA() 호출"<<endl;
     }
     AAA(const AAA& a)
     {
      cout<<"AAA(const AAA& a) 호출"<<endl;
     }
    };
  3. int main()
    {
     AAA obj1;
     AAA obj2 = obj1;
     return 0;
    }
  • 위의 소스는 호출되는 시점에서 첫번째인 기존에 생성된 객체로 새로운 객체를 초기화하는 경우이다.

    • obj1은 기존에 이미 생성된 객체이고 obj2는 새롭게 생성하고자 하는 객체이다.
  1.  void function(int a){}
    int main()
    {
     int n =10;
     function(n);
    }
  • 위의 같은 소느는 n을 10으로 초기화 하고나서 function으로 변수 n을 인자로 전달 할때 매개변수 a가 이 값을 복사하게 된다.

    • Call-By-Value에 의한 함수 호출 과정을 세분화 하면

      • 매개 변수를 위한 메모리 공간 할당
      • 전달 인자 값의 복사
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
     int val;
    public:
     AAA(int i)
     {
      val = i;
     }
     AAA(const AAA& a)
     {
      cout<<"AAA(const AAA& a)호출"<<endl;
      val =a.val;
     }
     void ShowData()
     {
      cout<<"val : "<<val<<endl;
     }
    };
    void funcion(AAA a)
    {
     a.ShowData();
    }
    int main()
    {
     AAA obj(30);
     funcion(obj);
     return 0;
    }
  • 위의 소스를 보면 function(obj)를 보면 obj가 가지고 있는 값을 a에 복사하는 것이다.

    • 매개변수로 생성되는 객체의 복사 생성자가 호출되는 것이다.
  1.  #include <iostream>
    using namespace std;
  2. class AAA
    {
     int val;
    public:
     AAA(int i)
     {
      val = i;
     }
     AAA(const AAA& a)
     {
      cout<<"AAA(const AAA& a)호출"<<endl;
      val =a.val;
     }
     void ShowData()
     {
      cout<<"val : "<<val<<endl;
     }
    };
    AAA function(void)
    {
     AAA a(10);
     return a;
    }
    int main()
    {
     function();
     return 0;
    }
  •  위의 소스는 main에서 function 함수를 호출 하고나서 function 함수 안에서 AAA a객체를 생성하고 나서 return a로 a객체 복사본을 리턴한다.

문제

5 -1

문제1

연습문제 4 -1의 3번 문제를 통해서 NameCard클래스를 정의하였다. 이 클래스도 생성자 내에서 메모리 공간을 동적 할당하고 있으며, 소멸자에서 이를 해제하고 있기 때문에 깊은 복사를 하는 복사 생성자가 필요한 상황이다. 이에 적절한 복사 생성자를 정의해 보기 바란다.

 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

007. 상속(Inheritance)의 이해  (0) 2008.12.07
006. static 멤버와 const 멤버  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
003. 클래스의 기본  (0) 2008.12.07
002. C 기반의 C++ 2  (0) 2008.12.07
Posted by kid1412
,

004. 클래스의 완성

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명의 데이터를 입력받기 위해서 배열을 선언 하되, 객체 포인터 배열을 선언하는 형태를 취하도록 하자. 

 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
003. 클래스의 기본  (0) 2008.12.07
002. C 기반의 C++ 2  (0) 2008.12.07
001. C 기반의 C++ 1  (0) 2008.12.07
Posted by kid1412
,

003. 클래스의 기본

I. 구조체와 클래스

  • 구조체의 등장 배경은 무엇인가?

    • 일단 구조체의 이점은 관련있는 데이터를 하나로 묶으면 관리하기에도 프로그래밍 하기에도 편리하다는 점이 있다.
    • 부류를 형성하는 데이터들을 하나의 자료형으로
    • 정의해서, 관리 및 프로그램 구현에 도움을 주겠다는 의도로 등장.
  • 구조체에 대한 불만과 개선
  1. #include <stdio.h>
  2. struct Person
    {
     int age;
     char name[10];
    };
  3. int main()
    {
     int a = 10;
     Person p;
     return 0;
    }
  • 위와 같이 소스를 짜면 맞을듯 하나 틀리다.

    • main함수 안에 Person p; 라고 쓴 부분이 있는데 C파일에서는 컴파일 오류가 난다. 오류를 없애려면 struct Person p; 로 써야 한다.
    • cpp로 확장자를 바꾸어서 하면 컴파일이 잘 된다. 이유는 C 언어와 달리 C++의 매커니즘에는 사용자 정의 자료형과 기본 자료형을 동일시하자는 철학이 있다고 한다.
  • 구조체에 대한 자세한건 C에서 참조 하세요
  • 구조체가 아니라 클래스(class)

    •  클래스 = 멤버 변수 + 멤버 함수
    • 다음 소스는 기본적인 클래스를 쓴 예제이다.
  1.  #include <iostream>
    using namespace std;
    class Account
    {
    public:
     char accID[20];
     char secId[20];
     char name[20];
     int balance;
     
     void Deposit(int money)
     {
      balance += money;
     }
     void Withdraw(int money)
     {
      balance -= money;
     }
    };
    int main()
    {
     Account yoon={"1234","2321","yoon",1000};
     yoon.Deposit(100);
     cout<<"잔    액 : "<<yoon.balance<<endl;
  2.  yoon.Withdraw(200);
     cout<<"잔    액 : "<<yoon.balance<<endl;
     return 0;
    }

II. 클래스와 객체

  • 데이터 추상화

    • 현실세계의 사물을 데이터적인 측면과 기능적 측면을 통해서 정의하는 것.
  • 클래스

    •  추상화 된 데이터를 가지고 사용자 정의 자료형을 정의하는 것.
  • 객체

    •  클래스를 이용해서 정의된 자료형의 변수

III. 클래스 멤버의 접근 제어

  • 클래스의 내부 접근과 외부 접근
  1.  #include <iostream>
    using namespace std;
    class Counter
    {
    public:
     int val;
     void Increment(void)
     {
      val++ //내부 접근
     }
    };
    int main()
    {
     Counter cnt;
     cnt.val = 0;   //외부접근
     cnt.Increment();  //외부접근
     cout<<cnt.val<<endl; //외부접근
     return 0;
    }
  • 위 소스를 보면 클래스 안에서 접근을 하면 내부 접근 그 이외는 외부 접근으로 친다.

 

  •  public & private

    • private : 내부 접근만 허용 되며, 외부 접근 시도시 컴파일 에러가 난다.
    • public : 어디서 접근 하든 상관 안한다.
  1.  #include <iostream>
    using namespace std;
  2. const int OPEN = 1;
    const int CLOSE = 2;
  3. class Door
    {
    private:
     int state;
    public:
     void Open()
     {
      state = OPEN;
     }
     void Close()
     {
      state=CLOSE;
     }
     void ShowState()
     {
      cout<<"현재 문의 상태 : ";
      cout<<((state == OPEN)? "OPEN" : "CLOSE")<<endl; //if문과 같은 것
     }
    };
    int main()
    {
     Door d;
     //d.state = OPEN;  <= 외부에서 state(private)로 접근 하려고 했지 때문에 오류
     d.Open();
     d.ShowState();
     return 0;
    }
  • 위의 소스를 보면 메인에서 주석처리 된 부부은 private로 접근 하려고 해서 오류가 난다. 나머지는 public으로 접근이라 상관 없다.

IV. 멤버 함수의 외부 정의

  • 멤버 함수를 클래스 외부에 정의하기
  1. #include <iostream>
    using namespace std;
  2. const int OPEN = 1;
    const int CLOSE = 2;
  3. class Door
    {
    private:
     int state;
    public:
     void Open();
     void Close();
     void ShowState();
    };
    void Door::Open()
     {
      state = OPEN;
     }
    void Door::Close()
     {
      state=CLOSE;
     }
    void Door::ShowState()
     {
      cout<<"현재 문의 상태 : ";
      cout<<((state == OPEN)? "OPEN" : "CLOSE")<<endl; //if문과 같은 것
     }
    int main()
    {
     Door d;
     //d.state = OPEN;  <= 외부에서 state(private)로 접근 하려고 했지 때문에 오류
     d.Open();
     d.ShowState();
     return 0;
    }
  • 위의 소스는 앞절에서 한 소스를 클래스 안에 있던 함수를 외부로 빼온거 말고는 없다.

    • 잘 보면 외부로 함수를 빼었을 때 void Door::Open 이런식으로 써있는데 Door:: 은 Door이라는 클래스 안에 있는 Open 함수를 쓰겠다는 의미이다.
  • 클래스 내부 정의의 의미와 inline

    • 클래스의 멤버 함수를 내부에 정의한다는 것은 외부에 정의한 것과 달리 inline으로 처리할것으로 요구한다는 의미를 지닌다.
  1. #include <iostream>
    using namespace std;
  2. const int OPEN = 1;
    const int CLOSE = 2;
  3. class Door
    {
    private:
     int state;
    public:
     void Open();
     void Close();
     void ShowState();
    };
    inline void Door::Open()
     {
      state = OPEN;
     }
    inline void Door::Close()
     {
      state=CLOSE;
     }
    inline void Door::ShowState()
     {
      cout<<"현재 문의 상태 : ";
      cout<<((state == OPEN)? "OPEN" : "CLOSE")<<endl; //if문과 같은 것
     }
    int main()
    {
     Door d;
     //d.state = OPEN;  <= 외부에서 state(private)로 접근 하려고 했지 때문에 오류
     d.Open();
     d.ShowState();
     return 0;
    }
  • 위의 소스처럼 외부로 정의된 함수 앞에 inline 함수를 쓰면 된다. 컴파일러는 최적화를 위해서 무시할 때도 있다.

 

  • 헤더 파일을 이용한 파일의 분할
  • Door.h
  1. #include <iostream>
    using namespace std;
  2. const int OPEN = 1;
    const int CLOSE = 2;
  3. class Door
    {
    private:
     int state;
    public:
     void Open();
     void Close();
     void ShowState();
    };
  • Door.cpp
  1.  #include <iostream>
    #include "Door.h"
    using namespace std;
  2. void Door::Open()
    {
     state = OPEN;
    }
    void Door::Close()
    {
     state=CLOSE;
    }
    void Door::ShowState()
    {
     cout<<"현재 문의 상태 : ";
     cout<<((state == OPEN)? "OPEN" : "CLOSE")<<endl; //if문과 같은 것
    }
  • main.cpp
  1. #include "Door.h"
    int main()
    {
     Door d;
     //d.state = OPEN;  <= 외부에서 state(private)로 접근 하려고 했지 때문에 오류
     d.Open();
     d.ShowState();
     return 0;
    }
  • 위의 소스들처럼 클래스를 헤더파일어 넣고 그 헤더를 사용하면서 쓰면 된다.

문제

3 -1

문제 1

계산기 기능을 하는 Calculator라는 이름의 클래스를 작성해 보자. 기본적으로 지니고 있는 기능은 덧셈, 뺄셈, 곱셈, 그리고 나눗셈이며, 연산을 할 때마다 어떠한 연산을 몇번했는지 기록되어야 한다 다음에 제공되는 main 함수와 출력 결과를 통해서 요구되는 Calculator 클래스를 디자인해보자

 

문제2

Printer라는 이름의 클래스를 디자인하자. Printer클래스는 두가지 기능을 지닌다. 그 중 첫번째는 문자열을 설정할 수 있는 기능이고, 두번째 기능은 설정되어 있는 문자열을 출력하는 기능이다. main함수와 출력 결과를 통해서 요구되는 Printer 클래스를 디자인 해보자.( 멤버 변수는 private로 멤버함수는 public으로 하자)

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
002. C 기반의 C++ 2  (0) 2008.12.07
001. C 기반의 C++ 1  (0) 2008.12.07
Posted by kid1412
,

002. C기반의 C++ 2

I. 들어가기에 앞서서

  • 강의에서 언급하는 내용이라 합니다. 혼자서 생각을 먼저 해보세요

    • const 키워드의 의미! 다음 문장은 어떤 의미를 지니는가?

      • const int n = 10;   //int n이 10으로 고정. 변수 상수화
      • const int *n;  //int *n을 고정 한다. (즉, 값은 고정 하나, 주소는 변경 가능하다.) 데이터 상수화
      • int* const n;   //*n을 고정합니다.(즉, 값은 변경 가능 하나, 주소가 고정이다.) 포인터 상수화
      • const int* const n;   //2번째, 3번째 줄 합친거와 같다.(즉, 값도 고정이고, 주소도 고정이다.)
    • 스택이라는 메모리 영역은 어떤 용도로 사용되는 메모리 공간이며, 그 특징은?

      •  지역변수, 매개변수 (컴파일 시 할당)
    • 힙이라는 메모리 영역은 어떤 용도로 사용되는 메모리 공간이며, 그 특징은?

      •  프로그래머 할당 (런타임 시 할당)
    • malloc과 free 함수를 사용해야만 하는 이유?(필요한 이유)

II. 새로운 형태의 자료형 bool

  • 전통적인 방법의 true와 false
  1. #include <stdio.h>
  2. const int TRUE = 1;
    const int FALSE = 0;
  3. int IsPositive(int i)
    {
     if(i<0)
      return FALSE;
     else
      return TRUE;
    }
  4. int main(void)
    {
     int num;
     int result;
  5.  printf("숫자 입력 : ");
     scanf("%d",&num);
  6.  result = IsPositive(num);
  7.  if(result == TRUE)
      printf("Positive numver \n");
     else
      printf("Negative number \n");
     return 0;
    }
  • bool형을 이용한 true와 false
  1. #include <iostream>
  2. using namespace std;
  3. bool IsPositive(int i)
    {
     if(i<0)
      return false;
     else
      return true;
    }
  4. int main(void)
    {
     int num;
     bool result;
  5.  cout <<"숫자 입력 : ";
     cin >> num;
  6.  result = IsPositive(num);
     if(result == true)
      cout <<"Positive number"<<endl;
     else
      cout <<"Negative number"<<endl;
    }
  • Bool형이 int 형으로 바뀌면 true 일때는 1 false 일때는 0가 나옵니다.

III. 레퍼런스의 이해

  • 레퍼런스 라는 것이 무엇 일까?

    • 이름을 지니고 있는 대상에게 지어주는 별명을 가리켜 레퍼런스라고 한다.
    • int &ref = val;

      • val이라는 int형 변수의 이름에 ref라는 별명을 붙인 것이다. C++에서 레퍼런스를 선언할 때에도 &를 쓰인다.
  • 레퍼런스의 특징
  1.  #include<iostream>
  2. using namespace std;
  3. int main(void)
    {
     int val = 10;
     int &ref = val;
     val++;
  4.  cout<<"ref : "<<ref<<endl;
     cout<<"val : "<<val<<endl;
     
     ref++;
  5.  cout<<"ref : "<<ref <<endl;
     cout <<"val : "<<val<<endl;
     return 0;
    }
  • ref는 val의 별명이기 때문에 둘다 같은 거라고 보면 된다. 따라서 ref가 증가하든 val가 증가하든 여튼 같은게 증가하는 것이다.

    • 레퍼런스를 가지고 있는 연산은 레퍼런스가 참조하는 변수의 이름을 가지고 하는 연산과 같은 효과
  • 레퍼런스의 제약

    • 초기화를 시켜주지 않는다던가
    • 상수가 오면 error

IV. 레퍼런스와 함수

  •  포인터를 이용한 Call-By-Reference
  1. #include <iostream>
  2. using namespace std;
  3. void swap(int *a,int *b)
    {
     int temp = *a;
     *a = *b;
     *b = temp;
    }
  4. int main()
    {
     int val1 = 10;
     int val2 = 20;
  5.  cout <<"val1 : "<<val1<<' ';
     cout<<"val2 : "<<val2 <<endl;
  6.  swap(&val1,&val2);
     cout <<"val1 : "<<val1<<' ';
     cout<<"val2 : "<<val2 <<endl;
    return 0;
    }
  •  레퍼런스를 이용한 Call-By-Reference
  1. #include <iostream>
  2. using namespace std;
  3. void swap(int &a,int &b)
    {
     int temp = a;
     a = b;
     b = temp;
    }
  4. int main(void)
    {
     int val1 = 10;
     int val2 = 20;
  5.  cout <<" val1 : "<<val1<<' ';
     cout<<"val2 : "<<val2 <<endl;
  6.  swap(val1,val2);
     cout <<" val1 : "<<val1<<' ';
     cout<<"val2 : "<<val2 <<endl;
    }
  •  레퍼런스를 이용한 성능의 향상

    • 개인정보를 입출력하는 예제이다.
  1. #include <iostream>
  2. using namespace std;
  3. struct _Person{
     int age;
     char name[20];
     char personalTD[20];
    };
    typedef struct _Person Person;
  4. void ShowData(Person p)
    {
     cout<<"******* 개인 정보 출력 ********"<<endl;
     cout<<"이    름 : "<<p.name<<endl;
     cout<<"주민번호 : "<<p.personalTD  <<endl;
     cout<<"나    이 : "<<p.age<<endl;
    }
  5. int main(void)
    {
     Person man;
  6.  cout<<"이    름 : ";
     cin>>man.name;
     cout<<"나    이 : ";
     cin>>man.age;
     cout<<"주민번호 : ";
     cin >> man.personalTD;
  7.  ShowData(man);
     return 0;
    }
  • 위의 소스는 Call-By-Value의 기본방식인 값의 복사를 이용해서 만든 것이다.

    • main 안에 ShowData에 man을 넘겨 주는데 man의 크기는 44바이트이다.(구조체 보면 int + 배열20+배열 20이다.)
    • 만약 인자로 전달하는 변수가 늘어나고 크기고 커지면 프로그램 자체가 부담스러워 진다.
  • 부담스러운 Call-By-Value를 대신하는 Call-By-Reference
  1. void ShowData(Person &p)
    {
     cout<<"******* 개인 정보 출력 ********"<<endl;
     cout<<"이    름 : "<<p.name<<endl;
     cout<<"주민번호 : "<<p.personalTD  <<endl;
     cout<<"나    이 : "<<p.age<<endl;
    }
  • 레퍼런스의 형태로 받게 되면, 이름만 하나 더 추가하는 것이므로 44바이트나 되는 크기의 복사는 발생하지 않는다.

    • 추가 설명 : Call-By-Value는 값을 복사 하는 거기 때문에 44바이트나 복사를 해야하지만 레퍼런스로 설정하면 복사가 아닌 가리키는 식으로 되어서 복사가 일어나지 않는다.
    • 그러나 ShowData는 그저 보여주는 함수이기 때문에 p값이 변경되면 안된다. 그것을 방지 하려면
  1.  void ShowData(const Person &p)
    {
     cout<<"******* 개인 정보 출력 ********"<<endl;
     cout<<"이    름 : "<<p.name<<endl;
     cout<<"주민번호 : "<<p.personalTD  <<endl;
     cout<<"나    이 : "<<p.age<<endl;
    }
  •  const를 붙여 주면 값을 변경 할수 없다.

    • 좀더 자세히 말하면 레퍼런스 p를 통한 데이터의 조작을 허용하지 않겠다는 것이다.

V. 레퍼런스를 리턴하는 함수의 정의

  • 레퍼런스를 리턴하는 적절한 형태의 함수와 그 의미

#include <iostream>

using namespace std;

int& increment(int &val)
{
 val++;
 return val;
}

int main(void)
{
 int n = 10;
 int &ref = increment(n);
 cout << "n : "<<n<<endl;
 cout<<"ref : "<<ref<<endl;
 return 0;
}

  • n이 넘어가 면서 reference로 val라는 이름이 추가 되고 리턴 되면서 val는 ref라는 이름이 생기고 호출이 완료되면 val는 사라진다.
  • 레퍼런스를 리턴하는 잘됫된 형태의 함수

    • 지역변수는 레퍼런스로 리턴할 수 없다.

#include <iostream>

using namespace std;

int& function(void)
{
 int val = 10;
 return val;
}

int main(void)
{
 int &ref = function();
 cout<<"ref : "<<ref<<endl;
 return 0;
}

  • 실행은 되지만(또는 값이 제대로 나오지만) 컴파일시 waring이 뜬다. 왜냐하면 지역변수는 함수 호출후 사라지기 때문이다.

    • waring을 뜨지 않게 하려면 static int val = 10; 를 치면 된다.

VI. new & delete

  • new와 delete는 C에서 malloc과 free랑 비슷하다.
  • 일단 malloc과 free에 대한 것이다.

#include <iostream>

using namespace std;

int main(void)
{
 int size;
 cout << "할당하고자 하는 배열의 크기 : ";
 cin >> size;

 int* arr = (int*)malloc(sizeof(int)*size);

 for(int i=0;i<size;i++)
  arr[i]  = i+10;
 for(int j=0;j<size;j++)
  cout<<"arr["<<j<<"] = "<<arr[j]<<endl;
 free(arr);
 return 0;
}

  • 위의 소스를 new와 delete로 바꾼다면 (별 다른 점은 없다. )

#include <iostream>

using namespace std;

int main(void)
{
 int size;
 cout << "할당하고자 하는 배열의 크기 : ";
 cin >> size;

 int *arr = new int[size];

 for(int i=0;i<size;i++)
  arr[i]  = i+10;
 for(int j=0;j<size;j++)
  cout<<"arr["<<j<<"] = "<<arr[j]<<endl;
 delete []arr;
 return 0;
}

  • 위에서 마지막에 delete []arr;이 있는데 delete[] arr; delete []arr; delete [] arr; 이런 식으로 쓰면 된다.

 

  •  NULL 포인터를 리턴하는 new 연산자

#include <iostream>

using namespace std;

int main(void)
{
 int size;
 cout << "할당하고자 하는 배열의 크기 : ";
 cin >> size;

 int *arr = new int[size];
 if (arr == NULL)
 {
  cout << "메모리 할당 실패"<<endl;
  return -1;
 }
 for(int i=0;i<size;i++)
  arr[i]  = i+10;
 for(int j=0;j<size;j++)
  cout<<"arr["<<j<<"] = "<<arr[j]<<endl;
 delete []arr;
 return 0;
}

  •  위 소스는 if문이 추가 되었는데 if문이 메모리가 제대로 할당이 되었는지 확인 하는 조건문이다.

    • 하지만 솔직히 if문 같은 비교문을 넣으면 성능만 저하 시킬수 있다고 판단을 한다.
    • 그래서 매크로를 사용하여 다르게 처리를 해보자

 #include <iostream>

using namespace std;
#define DEBUG 1 //테스트할 경우
//#define DEBUG 0 최종

int main(void)
{
 int size;
 cout << "할당하고자 하는 배열의 크기 : ";
 cin >> size;

 int *arr = new int[size];

 

#if DEBUG == 1
 if (arr == NULL)
 {
  cout << "메모리 할당 실패"<<endl;
  return -1;
 }
#endif

 

 for(int i=0;i<size;i++)
  arr[i]  = i+10;
 for(int j=0;j<size;j++)
  cout<<"arr["<<j<<"] = "<<arr[j]<<endl;
 delete []arr;
 return 0;
}

- 중간에 #if #endif 를 매크로와 함께 써서 테스트 일 경우에만  컴파일 되게 할 수도 있다.

 

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
003. 클래스의 기본  (0) 2008.12.07
001. C 기반의 C++ 1  (0) 2008.12.07
Posted by kid1412
,

001. C기반의 C++ 1

I. printf와 scanf를 대신하는 입-출력 방식

  • hello world의 출력 비교

    • C 버전
    1. #include<stdio.h>
    2. int main()
    3. {   
    4. printf("Hello World\n");
    5. return 0;
    6. }
    • C++ 버전
    1. #include<iostream.h>
    2. int main()
    3. {
    4. cout<<"hello wolrd"<<endl;
    5. return 0;
    6. }
    • C++ another 버전
    1. #include<iostream>
    2. int main()
    3. {
    4. std::cout<<"hello wolrd<<std::endl;
    5. return 0;
    6. }
    • cout의 쓰임새

      • cout << 출력대상 << 출력대상1<<출력대상 2 ..... ;
      • 위에 소스를 보다 보면 <<endl; 가 있는데 이건 C에서 \n 와 같은 역할을 한다.
      • 작은 따옴표 '' 는 문자를 큰 따옴표 " "는 문자열을 나타낸다.
  • 데이터 입력

    • C 버전
    1. #include <stdio.h>
    2. int main()
    3. {
    4. int a;
    5. printf(" a 에 숫자를 넣으시오 : ");
    6. scanf("%d",&a);
    7. return 0;
    8. }
    • C++ 버전
    1. #include<iostream>
    2. int main()
    3. {
    4. int a;
    5. std::cout<<"a에 숫자를 넣으시오";
    6. std::cin >> a;
    7. return 0;
    8. }
    • cin의 쓰임새

      • cin >> 입력 변수 1>> 입력변수 2;

II. 함수 오버로딩

  • C에서는 안되지만  C++에서 되는 오버로딩에 대해서 보자

    1. #include <iostream>
      int function()
      {
          return 10;
      }
      int function(int a, int b)
      {
          return a+b;
      }
      int main()
      {
          std::cout <<function()<<std::endl;
          std::cout <<function(12,13)<<std::endl;
          return 0;
      }
  • function()와 function(12,13)이 들어갈 곳을 정확히 알고 같은 이름의 함수여도 알아서 집어 넣는게 오버로딩이다.
  • 단, 함수의 이름은 같지만 매개변수의 타입이나 개수가 달아야만 한다.
  • 또, 리턴값이 더라도 매개변수의 타입이나 개수가 같다면 오버로딩이 될 수 없다.

III. 디폴트 매개변수

  1. int function(int a = 0)
  2. {
  3. return a+1;
  4. }
  5. int main()
  6. {
  7. std::cout<<function();
  8. }
  • 위와 같은 소스에서 매개 변수 int a = 0 에서 = 0 이 디폴트 매개 변수 이다.

    • 메인 함수 안에 function()이 아무 매개변수를 설정 안하고 함수를 부르면 위에 function(int a =0)을 불러오면서 a=0으로 초기화 하고 실행 한다. 즉 결과 값은 1 이 나오게 된다.
  • 디폴트 매개 변수와 함수의 오버로딩
  1. #include<iostream>
  2. int function(int a = 10){
  3. return a + 1;

  4. }
  5. int function(void){
  6. reutnr 10;

  7. }
  8. int main(void)
  9. {
  10. std::cout<<function(10)<<std::endl;

  11. std::cout<<function()<<std::endl;

  12. }
  • 위에 소스를 보고 과연 컴파일이 제대로 될까?

    • 답은 제대로 안된다. 여기서 약간 헷갈릴수도 있는데 function(10)을 부르면 처음에 나오는 function(int a = 10)을 호출하게 된다. (이유는 다 알꺼라 본다.)
    • 그런데 그 아래 function()는 어떤 함수를 불러야 할까? function()이어도 funcrion(int a =10)을 부를 수도 있고 function()을 부를 수도 있다. 그래서 컴파일시에 어떤 함수를 불러야 할지 몰라서 오류가 나게 된다.

IV. 인라인(inline) 함수

  • 매크로 함수의 정의와 장점

    • 일단 매크로 함수에 대해서 예시를 보자
    1. #include<iostream>
    2. #define a 100;
    3. int main()
    4. {
    5. std::cout << a + 1 <<std:;endl;
    6. }
    • 위에 #define이라고 되어 있는 부분이 있다. a를 100으로 치환 시킨다. 이것이 매크로 이다.
  • C++기반의 함수의 in-line화

    1. #include<iostream>
    2. inline int SQUARE(int x)
    3. {
    4. return x*x;
    5. }
    6. int main(void)
    7. {
    8. std::cout<<SQUARE(5)<<std::endl;
    9. return 0;
    10. }
    • C++ 에서는 함수 앞에 inline만 붙여주면 inline화 시킬수 있어서 매크로를 사용할 필요가 없다.

      • 참고 . 컴파일러에 따라서는 inline 선언이 오히려 성능 향상에 해가 되면 무시해 버리기도 한다.

V. 이름공간(namespace)에 대한 소개

  • namespace

    • 말그대로 이름공간 이다.
    • 쓰는 이유는 어느 두 소스 또는 모듈들을 하나의 완성된 프로그램으로 옮길때 이름 충돌이 생길 수도 있기에 쓰인다.
    • 예를 들어 살펴 보자
    1. #include<iostream>
    2. void function(void)
    3. {
    4. std::cout<<"A"<<std::endl;
    5. }
    6. void function(void)
    7. {
    8. std::cout<<"B"<<std::endl;
    9. }
    10. int main()
    11. {   
    12. function();
    13. return 0;
    14. }
    • 위와 같은 소스가 있을 때 메인에서 function()을 불러오려고 봤더니 함수가 겹쳐서 오류가 난다. 이럴때 이름공간을 정의해서 문제를 해결해 보자.
    1. #include<iostream>
    2. namespace a_com{
    3. void function(void)
    4. {

      std::cout<<"A"<<std::endl;

    5. }

    6. }   
    7. namespace b_com{
    8. void function(void)
    9. {
    10. std::cout<<"B"<<std::endl;
    11. }
    12. }
    13. int main()
    14. {
    15. a_com::function();
    16. b_com::function();
    17. return 0;
    18. }
    • 위와 같이 namespace를 지정해주고 메인에서 부를 때 이름공간의 이름(a_com, b_com)을 붙어서 불러오면 각각의 함수를 불러
      온다.
  • std::cout , std::cin, std::endl

    • 위에서 이렇게 써왔는데 std::는 무엇일까?

      • 여지껏 공부한 내용으로 보아 std라는 namespace에 cout을 쓰겠다는 뜻이 되지 않겠는가.
  • using

    • std::cout 같이 std:: 를 계속 붙여서 쓰기 귀찮아 지므로 우리는 좀더 편하게 해보자.
    1. #include<iostream>
    2. using std::cout;
    3. using std::endl;
    4. int main()
    5. {
    6. cout <<"hello"<<endl;
    7. }
    • using으로 std::cout, std::endl를 쓰겠다고 선언을 하고 그 다음 부터는 std 안붙이고도 쓸수 있게 된다.
    • 위에 using을 더 간단하게 써보자
    1. #include<iostream>
    2. using namespace std;
    3. int main()
    4. {
    5. cout <<"hello"<<endl;
    6. }
    • 위와 같은 소스를 쓰면 namespace std를 전부다 쓴다는 것이다. 처음 소스 처럼 using 2줄 쓰는 것 보다 한줄쓰는게 더 간단하지 않은가
    • 전역 변수에 있는 것으로 접근 하려면 ::<변수명> 이렇게 써주면 된다.

VI. 문제

1-1

문제 1

사용자로부터 총 10개의 정수를 입력받아서 그 합을 출력하는 프로그램을 작성해보자.

문제 2

사용자로부터 이름과 전화번호를 입력받아서 배열에 저장한 다음, 그대로 출력해 주는 프로그램을 작성해 보자

문제 3

숫자를 하나 입력받아서 그 숫자에 해당하는 구구단을 출력하는 프로그램을 작성 해 보자. 예를 들어서 사용자가 5를 입력한다면 구구단에서 5단을 출력해야 한다.

문제 4

판매원들의 급여 계산 프로그램을 작성해 보자. 이 회사는 모든 판매원에게 매달 50만원의 기본 급여와 물품 판매 가격의 12%에 해당하는 돈을 지급한다. 예를 들어서 민수라는 친구의 이번 달 물품 판매 금액이 100만원이라면, 50+100*0.12 = 62만원을 급여로 지급받는다.

1-2

문제 1

다음 main 함수에서 필요로 하는 swap 함수를 구현 하라.

이 글은 스프링노트에서 작성되었습니다.

'2. C/C++ > 02. C++' 카테고리의 다른 글

006. static 멤버와 const 멤버  (0) 2008.12.07
005. 복사생성자  (0) 2008.12.07
004. 클래스의 완성  (0) 2008.12.07
003. 클래스의 기본  (0) 2008.12.07
002. C 기반의 C++ 2  (0) 2008.12.07
Posted by kid1412
,