[C++] 생성자와 소멸자(4-3-1)

서희찬·2021년 5월 16일
0
post-thumbnail

지금까지 객체를 생성하고 객체의 맴버변수 초기화를 목적으로 InitMember라는 이름의 함수를 정의하고 호출하였으나 이는 여간 불편한 게 아니다.
다행히 !
"생성자" 라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있다.

생성자의 이해


생성자 다음과 같은 형태를 띤다.

  • 클래스의 이름과 함수의 이름이 동일하다.
  • 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.

이는 다음과 같은 특징을 가진다.

객체 생성시 딱 한번 호출된다.

이전에는 다음과 같이 객체를 생성하였지만

SimpleClass sc; // 전역, 지역 및 매개변수의 형태
SimpleClass * ptr = new SimpleClass; // 동적할당의 형태 

그러나 !
생성자가 정의되었으니, 객체생성과정에서 자동으로 호출되는 생성자에게 전달할 인자의 정보를 다음과 같이 추가해야한다.

다음 예제를 통해

  • 생성자도 함수의 일종이니 오버로딩이 가능하다.
  • 생성자도 함수의 일종이니 매개변수에 디폴트값 설정이 가능하다.

를 알 수 있다.

#include <iostream>
using namespace std;

class SimpleClass
{
private:
    int num1;
    int num2;
public:
    SimpleClass()//생성자
    {
        num1 = 0;
        num2 = 0;
    }
    SimpleClass(int n)
    {
        num1 = n;
        num2 = 0;
    }
    SimpleClass(int n1, int n2)
    {
        num1 = n1;
        num2 = n2;
    }
//    SimpleClass(int n1 = 0, int n2=0)
//    {
//        num1 = n1;
//        num2 = n2;
//    }
    
    void ShowData() const
    {
        cout<<num1<<" "<<num2<<endl;
    }
};

int main(void)
{
    SimpleClass sc1;
    sc1.ShowData();
    
    SimpleClass sc2(100);
    sc2.ShowData();
    
    SimpleClass sc3(100,200);
    sc3.ShowData();
    
    return 0;
}


(2번째 생성자를 주석처리 하지 않고 다른 생성자의 주석을 풀면 둘다 호출이 가능하기 때문에 호출할 생성자를 결정하지 못해 에러가 발생한다.)

생성자를 하나씩보자 !
2번째와 3번째 생성자를 이용해서 객체를 생성하기 위해서는
다음과 같은 문장이 각각 필요하다.

SimpleClass sc2(100);
SimpleClass *ptr2 = new SimpleClass(100);

SimpleClass sc3(100,200);
SimpleClass * ptr3 = new SimpleClass(100,200);

그러나!!!
제일 첫 생성자를 이용해서 객체를 생성하기 위해서는 다음과 같은 문장을 구성하면 안된다.

SimpleClass sc1();

대신 다음과 같이 구성해야한다.

SimpleClass sc1;
SimpleClass * ptr1 = new SimpleClass;
SimpleClass * ptr1 = new SimpleClass();

다 이해 되는데
왜 sc1()은 안되는지 모르겠다.
왜 그럴것같은가?

바로 이는 함수의 원형 선언에 해당하기 때문이다 !

그래서 이것을 생성자의 호출문으로 인정해버리면, 컴파일러는 이러한 문장을 만났을 때, 이것이 객체생성문인지 함수의 원형선언인지를 구분하지 못한다.

이전 예제에 대한 생성자의 활용

#include <iostream>
using namespace std;

class FruiteSeller // 과일판매 아재
{
private:
    int APPLE_PRICE;
    int numOfApples;
    int myMoney;
    
public:
//    void InitMembers(int price,int num, int money)
//    {
//        APPLE_PRICE = price;
//        numOfApples = num;
//        myMoney = money;
//    }
    FruiteSeller(int price, int num, int money)
    {
        APPLE_PRICE = price;
        numOfApples = num;
        myMoney = money;
    }
    int SaleApples(int money)
    {
        if(money<0)
        {
            cout<<"잘못된 정보가 전달되어 판매를 취소합니다."<<endl;
            return 0;
        }
        
        int num = money/APPLE_PRICE;
        numOfApples -= num;
        myMoney+=money;
        return num;
    }
    void ShowSalesResult() const
    {
        cout<<"남은 사과 : "<<numOfApples<<endl;
        cout<<"판매 수익 : "<<myMoney<<endl<<endl;
    }
};

class FruiteBuyer
{
    int myMoney; // pirvate:
    int numOfApples; // pricate
    
public:
    FruiteBuyer(int money)
    {
        myMoney = money;
        numOfApples = 0;
    }
    void BuyApples(FruiteSeller &seller, int money)
    {
        if(money<0)
        {
            cout<<"잘못된 정보가 전달되어 구매를 취소합니다."<<endl;
            return;
        }
        numOfApples += seller.SaleApples(money);
        myMoney -= money;
    }
    void ShowBuyResult() const
    {
        cout<<"현재 잔액 : "<<myMoney<<endl;
        cout<<"사과 개수 : "<<numOfApples<<endl<<endl;
    }
};

int main(void)
{
    FruiteSeller seller(1000,20,0);
    FruiteBuyer buyer(5000);
    buyer.BuyApples(seller, 2000); // 과일 구매!
    
    cout<<" 과일 판매자의 현황 "<<endl;
    seller.ShowSalesResult();
    cout<<"과일 구매자의 현황 "<<endl;
    buyer.ShowBuyResult();
    
    return 0;
}
// 대화

이제 앞에 짠 다른 코드에서 생성자를 알아보자.

이를 보면
Rectangle 클래스는 두 개의 Point 객체를 맴버로 지니고 있어서 객체가 생성되면, 두 개의 Point 객체가 함께 생성된다.

따라서 다음과 같은 생각을 할 것이다.

"Rectangle 객체를 생성하는 과정에서 Point 클래스의 생성자를 통해서 Point 객체를 초기화 할 수 없을까?"

위와 같은 생각은 당연빠따로 하게 될것인데..
이것을 해주는게 바로 !
맴버 이니셜라이저 이다 !

맴버 이니셜라이저를 이용한 맴버 초기화



이제 맴버 이니셜라이저를 추가한 전체예제를 보자 .

Point.hpp,cpp
Rectangle.hpp,cpp
main.hpp
순이다.

//
//  Point.hpp
//  1_practice
//
//  Created by 서희찬 on 2021/05/16.
//

#ifndef Point_hpp
#define Point_hpp
#include <iostream>

class Point
{
private:
    int x;
    int y;
public:
    Point(const int &xpos, const int &ypos);
    int GetX() const;
    int GetY() const;
    bool SetX(int xpos);
    bool SetY(int ypos);
};
#endif /* Point_hpp */
//
//  Point.cpp
//  1_practice
//
//  Created by 서희찬 on 2021/05/16.
//
#include <iostream>
#include "Point.hpp"
using namespace std;

Point::Point(const int &xpos, const int &ypos)
{
    x = xpos;
    y = ypos;
}

int Point::GetX() const
{
    return x;
}

int Point::GetY() const
{
    return y;
}

bool Point::SetX(int xpos)
{
    if(0>xpos || xpos >100)
    {
        cout<<"벗어난 범위의  전달"<<endl;
        return false;
    }
    x = xpos;
    return true;
}

bool Point::SetY(int ypos)
{
    if(0>ypos || ypos >100)
    {
        cout<<"벗어난 범위의 값 전달"<<endl;
        return false;
    }
    y = ypos;
    return true;
}
//
//  Rectangle.hpp
//  1_practice
//
//  Created by 서희찬 on 2021/05/16.
//

#ifndef Rectangle_hpp
#define Rectangle_hpp

#include "Point.hpp"

class Rectangle
{
private:
    Point upLeft;
    Point lowRight; // 두개의 포인트
public:
    Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
    void ShowRecInfo() const;
};

#endif /* Rectangle_hpp */
//
//  Rectangle.cpp
//  1_practice
//
//  Created by 서희찬 on 2021/05/16.
//
#include <iostream>
#include "Rectangle.hpp"
using namespace std;

Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2)
:upLeft(x1, y1), lowRight(x2, y2)
{
    
}

void Rectangle::ShowRecInfo() const
{
    cout<<"좌 상단 : "<<"["<<upLeft.GetX()<<", ";
    cout<<upLeft.GetY()<<"]"<<endl;
    cout<<"우 하단 : "<<"["<<lowRight.GetX()<<", ";
    cout<<lowRight.GetY()<<"]"<<endl;
}
#include <iostream>
#include "Point.hpp"
#include "Rectangle.hpp"
using namespace std;

int main(void)
{
    
    Rectangle rec(1,1,5,5);
    rec.ShowRecInfo();
    return 0;
}

이제 제법 클래스 다운 모습을 갖추게 되었다.

이니셜라이저를 사용하다 보면 생성자의 몸체 부분이 비는 일이 종종 있다.

이니셜라이저는 선택적으로 존재하는 대상이여서 정의 되어 있지 않으면 빠이 ~!
인데
생성자는 그렇지않다.

디폴트 생성자라는 것이 자동적으로 삽입되어 호출 되기 때문이다.

맴버 이니셜라이저 를 이용한 변수 및 const 상수의 초기화

맴버 이니셜라이저는 객체가 아닌 맴버의 초기화에도 사용할 수 있다.

다음을 보자

이와 같이 이니셜라이저를 통해 맴버변수의 초기화도 가능하며, 이렇게 초기화 하는 경우 선언과 동시에 초기화되는 형태로 바이너리가 구성된다.
즉 다음의 형태로 맴버 변수가 선언과 동시에 초기화 되었다고 볼 수 있다.

int num1 = n1;

따라서 Const 선언된 맴버변수도 초기화가 가능하다.
(선언과 동시에 초기화 되어야하니 말이다.)


이렇게 재구성 할수 있다.

const 변수와 const 상수

  • const는 변수를 상수화시키는 키워드이다.
    따라서 const선언에 의해 상수화 된 변수를 두고 전자로 부르고 후자로 부르기도 한다.
    즉, 같은의미이다.

이니셜라이저의 이러한 특징은 맴버변수로 참조자를 선언할 수 있게 합니다.

const 변수와 마찬가지로 참조자도 선언과 동시에 초기화가 이뤄져야한다.
따라서 이니셜라이저를 이용하면 참조자도 맴버 변수로 선언될 수 있다.


참조자를 맴버 변수로 선언하는 경우가 흔한 것은 아니지만, 이와 유사한 코드를 보았을 때 이해하기위해서 언급하였다.

디폴트 생성자

메모리 공간의 할당 이후 생성자의 호출까지 완료되어야 "객체"라 할 수 있다.

객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다.


그래서 생성자를 정의하지 않는 클래스는 디폴트 생성자라는것이 위와 자동으로 삽입된다고 생각하면된다.

따라서!
모든 객체는 한번의 생성자 호출을 동반하는데 이는 new 연산자를 이용한 객체의 생성에도 해당 하는 이야기다.
즉,

AAA * ptr = new AAA;

도 말이다!
다만 !!!
malloc을 사용하면 클래스의 크기정보만 바이트 단위로 전달되기 때문에 생성자가 호출될 일이 전혀~ 없다!

그러니 동적할당을 이용할라고할 때에는 new 연산자를 이용하자.

생성자 불일치

생성자를 정의하였을때는 그에 맞게 만들어 줘야한다.

private 생성자


private 생성자는 클래스 내부에서 객체의 생성만 된다.

private생성자는 객체의 생성방법을 제한하고자 하는 경우에는 매우 유용하게 사용이 된다.

소멸자의 이해와 활용

객체 생성시 반드시 호출되는 것이 생성자라면, 객체 소멸시 반드시 호출되는 것은 소멸자이다.

소멸자는 다음의 형태를 갖는다.

  • 클래스의 이름 앞에 "~"가 붙은 형태의 이름을 갖는다.
  • 반환형이 선언되어 있지 않으며, 실제로 반화하지 않는다.
  • 매개변수는 void형으로 선언되어야 하기 때문에 오버로딩도, 디폴트 값 설정도 불가능하다.


이렇듯 적어두지 않으면 자동으로 소멸자가 삽입된다.

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
private:
    char * name;
    int age;
public:
    Person(char * myname,int myage)
    {
        int len = strlen(myname)+1;
        name = new char[len];
        strcpy(name, myname);
        age = myage;
    }
    void ShowPersonInfo() const
    {
        cout<<"이름 : "<<name<<endl;
        cout<<"나이 : "<<age<<endl;
    }
    ~Person()
    {
        delete [] name;
        cout<<"called destructor!"<<endl;
    }
};

int main(void)
{
    Person man1("hui chan",23);
    Person man2("Seo Chan", 41);
    man1.ShowPersonInfo();
    man2.ShowPersonInfo();
    return 0;
}

이 예제를 통해 소멸자를 통해서 객체소멸과정에서 처리해야할 ㅇㄹ들을 자동으로 처리할 수 있다.

문제 04-3[C++기반의 데이터 입출력]

문제 1

앞서 제시한 문제04-2를 해결하였는가?
당시만 해도 생성자를 설명하지 않은 상황이기 때문에 별도의 초기화 함수를 정의 및 호출해서 Point,Circle,Ring 객체를 초기화 했다.
이때 구현한 답에 대해서 모든 클래스에 생성자를 정의해 보자.

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos,ypos;
public:
//    void Init(int x,int y)
//    {
//        xpos = x;
//        ypos = y;
//    }
    Point(int x, int y):xpos(x),ypos(y)
    {
        
    }
    void showPointInfo() const // 이 함수내에서는 맴버변수에 저장된 값을 변경하지 않겠다.
    {
        cout<<"["<<xpos<<","<<ypos<<"]"<<endl;
    }
};

class Circle //원 ->원의 중심좌표, 반지름 길이 정보
{
private:
    int rad; // 반지름
    Point center; // 원의 중심
public:
//    void Init(int x,int y, int r)
//    {
//        rad =r;
//        center.Init(x, y);
//    }
    Circle(int x, int y, int r):center(x,y)
    {
        rad = r;
    }
    void ShowCircleInfo() const
    {
        cout<<"Radius : "<<rad<<endl;
        center.showPointInfo();
    }
};

class Ring
{
private:
    Circle inCircle;
    Circle outCircle;
public:
//    void Init(int inX,int inY, int inR, int x, int y, int r)
//    {
//        inCircle.Init(inX, inY, inR);
//        outCircle.Init(x, y, r);
//    }
    Ring(int inX,int inY, int inR, int x, int y, int r):inCircle(inX,inY,inR),outCircle(x,y,r)
    {
        
    }
    void ShowRingInfo() const
    {
        cout<<"Inner Circle Info ..."<<endl;
        inCircle.ShowCircleInfo();
        cout<<"Out Circle Info..."<<endl;
        outCircle.ShowCircleInfo();
    }
    
};

int main(void)
{
    Ring ring(1,1,4,2,2,9);
//    ring.Init(1,1,4,2,2,9);
    ring.ShowRingInfo();
    return 0;
}


성공 !

문제 2

명함을 의미하는 NameCard 클래스를 정의해보자.
이 클래스에는 다음의 정보가 저장되어야 한다.

  • 성명
  • 회사이름
  • 전화번호
  • 직급

단, 직급 정보를 제외한 나머지는 문자열의 형태로 저장을 하되, 길이에 딱 맞는 메모리 공간을 할당 받는 형태로 정의하자.(동적할당해라)
그리고 직급 정보는 int형 맴버변수를 선언해서 자장하되 아래의 enum선언을 활용해야한다.

enum(CLERK, SENIOR, ASSIST, MANAGER);

위의 enum 선언에서 정의된 상수는 순서대로 사원, 주임, 대리 , 과장을 뜻한다.
그럼 다음 main함수와 실행의 예를 참조하여, 이 문제에서 원하는 형태대로 NameCard 클래스를 완성해보자.

참고로 이 문제 해결을 위해서는 Cp 03예제 RacingCarEnum을 참고하면 도움이 된다.

int main(void)
{
    NameCard manClerk("Lee","ABEng","010-2342-1231",COMP_POS::CLERK);
    NameCard manSENIOR("Seo","APPLE","010-2342-1231",COMP_POS::SENIOR);
    NameCard manAssist("Kim","SAMSUNG","010-2342-1231",COMP_POS::ASSIST);
    manClerk.ShowNameCardInfo();
    manSENIOR.ShowNameCardInfo();
    manAssist.ShowNameCardInfo();
    return 0;
}

한번 짜보자 !


#include <iostream>
#include <cstring>

using namespace std;

namespace COMP_POS
{
    enum {CLERK, SENIOR, ASSIST, MANAGER}; // 열거형 상수

    void ShowPositionInfo(int pos)
{
        switch (pos) {
            case CLERK: //0
                cout<<"사원"<<endl;
                break;
            case SENIOR: // 1
                cout<<"주임"<<endl;
                break;
            case ASSIST: // 2
                cout<<"대리"<<endl;
                break;
            case MANAGER: // 3
                cout<<"과장"<<endl;
                break;
        }
    }
}

class NameCard
{
private:
    char * name; // 성명
    char * company; // 회사이름
    char * phone; // 폰번
    int position; // 직급
public:
    NameCard(char * _name, char * _company, char * _phone, int pos):position(pos)// 생성자와 이니셜라이저
    {
        name = new char[strlen(_name)+1]; // 크기만큼 동적할당
        company = new char[strlen(_company)+1];
        phone = new char[strlen(_phone)+1];
        strcpy(name, _name);
        strcpy(company, _company);
        strcpy(phone, _phone);
    }
    void ShowNameCardInfo() const
    {
        cout<<"name : "<<name<<endl;
        cout<<"company : "<<company<<endl;
        cout<<"phone : "<<phone<<endl;
        cout<<"position : "<<position<<endl;
        cout<<endl;
    }
    ~NameCard() // 소멸자
    {
        delete []name;
        delete [] company;
        delete [] phone;
    }
};

int main(void)
{
    NameCard manClerk("Lee","ABEng","010-2342-1231",COMP_POS::CLERK);
    NameCard manSENIOR("Seo","APPLE","010-2342-1231",COMP_POS::SENIOR);
    NameCard manAssist("Kim","SAMSUNG","010-2342-1231",COMP_POS::ASSIST);
    manClerk.ShowNameCardInfo();
    manSENIOR.ShowNameCardInfo();
    manAssist.ShowNameCardInfo();
    return 0;
}

간단하다.

열거ㅇ 상수에 직급정보를 스위치문을 사용하여 저장해주고, 명함 정보의 클래스를 하나씩 저장한다.
우선 이름,회사,휴대폰 번호, 직급
이렇게 저장하는데 직급은 int 형으로 받으므로 int로 초기화 시킨다.

그리고 문제에서 크기만큼 동적할당하라고 했으므로 name = new char[strlen(_name]+1)] 이런 형시으로 동적할당해준다.
그런데, 이러한 방법으로 하기위해 생성자와 이니셜라이저를 미리 만들어주었고,
동적할당후 strcpy 를 이용해서 문자열을 복사한다.

그 후 const 함수를 써서 출력을 돕고
소멸자를 통해서 소멸시켜주면 프로그램은 끝이난다.

main 은 명함을 각각 만들어준 후
순서대로 출력하는것을 적어줬다.

마지막이다!

클래스와 배열 그리고 this 포인터

객체배열

객체 기반의 배열은 다음의 형태로 선언한다.

SoSimple arr[10];

이를 동적으로 할당하는 경우에는 다음의 형태로 선언한다.

SoSimple * ptrArr = new SoSimple[10];

이러한 형태로 배열을 선언하면 열개의 SoSimple 객체가 모여 배열을 구성하는 형태가 된다.
이렇듯 구조체 배열의 선언과 차이가 없다.
하지만 !
배열을 선언하는 경우에도 생성자는 호출이 된다.
단, 배열의 선언과정에서는 호출할 생성자를 별도로 명시하지 못하낟.
즉, 위의 형태로 배열이 생성되려면 다음 형태의 생성자가 반드시 정의되어 있어야하낟.

SoSimple(){-------}

배열선언 이후에 각각의 요소를 여러분이 원하는 값으로 초기화 시키길 원한다면, 일일이 초기화의 과정을 별도로 거쳐야 한다!
그럼 다음예제를 보면서 객체배열을 조금 더 살펴보자.

#include <iostream>
#include <cstring>

using namespace std;

class Person
{
private:
    char * name;
    int age;
public:
    Person(char * myname, int myage):age(myage)
    {
        int len = strlen(myname)+1;
        name = new char[len];
        strcpy(name, myname);
    }
    Person()
    {
        name = NULL;
        age =0;
        cout<<"called Person()"<<endl;
    }
    void SetPersonInfo(char * myname, int myage)
    {
        name = myname;
        age = myage;
    }
    void ShowPersonInfo() const
    {
        cout<<"name : "<<name<<",  ";
        cout<<"age : "<<age<<endl;
    }
    ~Person()
    {
        delete [] name;
        cout<<"called destructor !"<<endl;
    }
};

int main(void)
{
    Person parr[3];
    char namestr[100];
    char * strptr;
    int age;
    int len;
    
    for(int i=0;i<3;i++)
    {
        cout<<"name : ";
        cin>>namestr;
        cout<<"age : ";
        cin>>age;
        len = strlen(namestr)+1;
        strptr = new char[len];
        strcpy(strptr, namestr);
        parr[i].SetPersonInfo(strptr, age);
        
    }
    parr[0].ShowPersonInfo();
    parr[1].ShowPersonInfo();
    parr[2].ShowPersonInfo();
    return 0;
}

움,,,,
이상하게 출력된다ㅎ
위의 실행결과를 통해서 객체 배열 생성시 void형 생성자가 호출됨을 확인할 수 있다.
그리고 배열 소멸시에도 그 배열을 구성하는 객체의 소멸자가 호출됨을 확인할 수 있다.

객체 포인터 배열

객체 배열이 객체로 이뤄진 배열이라면, 객체 포인터 배열은 객체의 주소 값 저장이 가능한 포인터 변수로 이뤄진 배열이다.

이를 알아보기 위해
앞선 예제를 객체 포인터 배열 기반으로 변경해보자.

#include <iostream>
#include <cstring>

using namespace std;

class Person
{
private:
    char * name;
    int age;
public:
    Person(char * myname, int myage):age(myage)
    {
        int len = strlen(myname)+1;
        name = new char[len];
        strcpy(name, myname);
    }
    Person()
    {
        name = NULL;
        age =0;
        cout<<"called Person()"<<endl;
    }
    void SetPersonInfo(char * myname, int myage)
    {
        name = myname;
        age = myage;
    }
    void ShowPersonInfo() const
    {
        cout<<"name : "<<name<<",  ";
        cout<<"age : "<<age<<endl;
    }
    ~Person()
    {
        delete [] name;
        cout<<"called destructor !"<<endl;
    }
};

int main(void)
{
    //Person parr[3];
    Person * parr[3];
    char namestr[100];
    int age;

    
    for(int i=0;i<3;i++)
    {
        cout<<"name : ";
        cin>>namestr;
        cout<<"age : ";
        cin>>age;
        parr[i] = new Person(namestr, age);
        
    }
    parr[0]->ShowPersonInfo();
    parr[1]->ShowPersonInfo();
    parr[2]->ShowPersonInfo();
    delete parr[0];
    delete parr[1];
    delete parr[2];
    
    return 0;
}

배열선언 대신 포인터 배열선언 해주었는데 이는 객체의 주소 값 3개를 저장할 수 있는 배열이다.

객체를 생성 후 이 객체의 주소 값을 배열에 저장하는것이 parr[i] = new Person(namestr,age);의 역할이다.

객체를 저장할 때에는 위의 예제에서 보인 두 가지 방법 중 하나를 택해야 한다.
즉, 저장의 대상을 객체로 하느냐 객체의 주소 값으로 하느냐를 결정해야 한다.

나머지는 다음 포스트에서 이어가겠다/

profile
Carnegie Mellon University Robotics Institute | Research Associate | Developing For Our Lives, 세상에 기여하는 삶을 살고자 개발하고 있습니다

0개의 댓글