언어 자료구조 알고리즘/Escort C++

[C++] 생성자

언제나휴일 2016. 4. 15. 12:56
반응형

- 생성자

 

  C++에서 특정 클래스 형식의 개체 인스턴스를 생성할 때 new 연산자를 사용합니다. new 연산자에서는 요청하는 형식의 개체를 위해 메모리를 할당하고 가상 함수 테이블을 형성하는 등의 초기 작업을 수행한 후에 생성자 메서드를 수행하고 생성된 개체의 메모리 주소를 반환합니다. 만약, 사용자가 생성자 메서드를 정의하지 않는다면 개체의 메모리를 할당하고 가상 함수 테이블을 형성하는 등의 초기 작업을 수행한 후 해당 개체의 메모리 주소를 반환하는데 이러한 작업을 수행하는 것을 디폴트 기본 생성자라고 합니다. 하지만 사용자가 생성자 메서드를 정의하면 디폴트 기본 생성자는 형성되지 않게 됩니다. 이러한 이유로 사용자가 입력 매개 변수가 있는 생성자를 정의했을 때 입력 인자를 전달하지 않고 개체를 생성하려고 하면 컴파일 에러가 발생합니다.

 

Stu.h - 사용자가 생성자를 명시하지 않음

#pragma once

#include <iostream>

using std::cout;

using std::endl;

class Stu

{

public:

    void Study();

};

 

Stu.cpp - Stu 클래스 정의와 동일하게 구체적인 구현에서도 생성자와 메서드를 명시하면 안 됨

#include "Stu.h"

void Stu::Study()

{

    cout<<"Stu::Study()"<<endl;

}

 

Program.cpp - 사용 예

#include "Stu.h"

void main()

{

    Stu *stu = new Stu();

    stu->Study();

    delete stu;

}

 

 예제와 같이 사용자가 생성자를 정의하지 않으면 컴파일러에 의해 자동으로 디폴트 기본 생성자가 만들어지게 됩니다. 하지만 사용자가 생성자 메서드를 하나로도 정의하면 디폴트 기본 생성자는 만들어지지 않습니다. 예를 들어, 입력 매개 변수가 있는 생성자를 정의를 하였을 때 인자를 전달하지 않고 개체를 생성하려고 하면 오류가 발생됩니다. 사용자에 의해 생성자를 정의하면 디폴트 기본 생성자는 자동으로 만들어지지 않기 때문입니다.

 

Stu.h - 입력 매개 변수가 string인 생성자를 명시

#pragma once

#include <string>

using std::string;

class Stu

{

    string name;

public:

    Stu(string _name);

};

 

Stu.cpp - Stu 클래스 정의와 동일하게 매개 변수가 string인 생성자 구현

#include "Stu.h"

Stu::Stu(string _name)

{

    name = _name;

}

 

사용할 수 있는 적절한 기본 생성자가 없습니다.


[그림 2.2]

 

 [그림 2.2]를 보면 인자를 넣지 않고 개체를 생성하려고 할 때 기본 생성자가 없다는 오류를 발생합니다. 이미 Stu에는 사용자가 정의한 생성자가 있어서 컴파일러에 의해 디폴트 기본 생성자를 자동으로 만들어주지 않기 때문입니다.

  

 그리고 생성자는 중복 정의가 가능한 메서드입니다. 다음의 예를 살펴보세요.

 

Stu.h 생성자를 중복 정의

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Stu

{

    int num;

    string name;

    string addr;

public:

    Stu(int _num, string _name);

    Stu(int _num, string _name, string _addr);

    void View();

};

 

Stu.cpp - 구현

#include "Stu.h"

Stu::Stu(int _num, string _name)

{

    num = _num;

    name = _name;

    addr = "입력을 하지 않았음";

}

Stu::Stu(int _num, string _name, string _addr)

{

    num = _num;

    name = _name;

    addr = _addr;

}

void Stu::View()

{

    cout<<"번호:"<<num<<" 이름:"<<name<<endl;

    cout<<"주소:"<<addr<<endl;

}

 

Demo.cpp - 데모 소스

#include "Stu.h"

void main()

{

    Stu *s1 = new Stu(3,"홍길동");

    Stu *s2 = new Stu(1,"강감찬","애월읍 고내리");

    s1->View();

    s2->View();

    delete s2;

    delete s1;

}


생성자 중복 정의

[그림 2.3]

 

[그림 2.3]을 보시면 생성자를 중복 정의하였을 때 사용자가 전달한 인수에 적절한 생성자 메서드가 호출되는 것을 확인할 수 있습니다.

 

 매개 변수가 있는 생성자 중에서 다른 개체를 인자로 전달받아 복사된 개체를 생성하는 복사 생성자가 있습니다. 개발자가 복사 생성자를 정의하지 않으면 컴파일러는 디폴트 복사 생성자를 만들어줍니다. 디폴트 복사 생성자에서는 입력 인자로 전달된 개체의 메모리를 덤핑하여 생성하는 개체의 메모리에 복사하는 작업을 수행합니다. 즉 새로 생성되는 개체는 입력 개체와 같은 값들을 갖는 개체가 생성되는 것입니다.

 

Stu.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Stu

{

    string name;

public:

    Stu(string _name);

    void View();

};

 

Stu.cpp - 구현

#include "Stu.h"

Stu::Stu(string _name)

{

    name = _name;   

}

void Stu::View()

{

    cout<<" 이름:"<<name<<endl;

}

  

Demo.cpp - 데모 소스

#include "Stu.h"

void main()

{

    Stu s1("홍길동");

    Stu s2(s1);

    Stu *s3 = new Stu(s1);

    s2.View();

    s3.View();

}

 

 데모 소스에서 Stu s2(s1); Stu *s3= new Stu(s1);처럼 다른 개체를 입력인자로 전달하여 개체를 생성하면 복사 생성자가 호출됩니다. 예제에서 Stu 클래스에 복사 생성자를 정의하지 않았으므로 컴파일러는 디폴트 복사 생성자를 만들어줍니다. [그림 2.4]를 보면 디폴트 복사 생성자에서는 생성되는 개체의 멤버 필드의 값을 입력 인자로 전달된 개체와 같게 만들어 주는 것을 알 수 있습니다.


복사 생성

[그림 2.4]

 

 개발자가 복사 생성자를 정의를 할 경우에는 입력 인자의 형식은 const 클래스명 & 로 하시면 됩니다. 입력 인자로 전달된 개체정보를 변경하지 말아야 하므로 const로 지정한 것입니다. 그리고 입력 인자로 전달된 개체 자체가 전달되어야 하므로 레퍼런스 변수로 받습니다.

 

Stu.h - 개발자가 복사 생성자를 정의

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Stu

{

    int num;

    string name;

public:

    Stu(int _num, string _name);

    Stu(const Stu &stu);

    void View();

};

 

Stu.cpp - 구현

#include "Stu.h"

Stu::Stu(int _num, string _name)

{

    num = _num;

    name = _name;

}

Stu::Stu(const Stu &stu)

{

    cout<<"복사 생성자 수행됨"<<endl;

    num = stu.num;

    name = stu.name;

}

void Stu::View()

{

    cout<<"번호:"<<num<<" 이름:"<<name<<endl;

}

 

Demo.cpp - 데모 소스

#include "Stu.h"

void main()

{

    Stu s1("홍길동");

    Stu s2(s1);

    s2.View();

}


[그림 2.5]

  

 이번에는 어떠한 경우에 복사 생성자가 수행되는지에 대해 살펴봅시다. 복사 생성자는 다른 개체를 인자로 전달받아 개체를 복사하여 생성하는 경우에 수행이 됩니다. 좀 더 구체적으로 살펴보면 new 연산자를 통해 개체를 생성할 때 입력 인자로 생성할 개체와 동일한 형식의 개체를 전달할 경우, 변수 선언 시에 입력 인자로 변수의 형식과 동일한 형식으로 초기화할 경우, 함수 호출 시에 개체를 전달할 경우, 함수 반환 시에 개체를 반환할 경우를 들 수 있습니다.

 

 다음의 데모 소스는 앞에서 사용한 데모 소스만 변경한 것입니다. 의도적으로 Test함수의 리턴 결과를 받지 않았습니다. 리턴 결과를 받는 과정에서 복사 생성자가 호출되는 것이 아니라 리턴하는 과정에서 복사 생성자가 호출되는 것을 보여주기 위해서입니다. 수행 결과인 [그림 2.6]을 함께 보시기 바랍니다.

 

Demo.cpp - 데모 소스

#include "Stu.h"

Stu Test(Stu s);

void main()

{

    Stu s1(2,"홍길동");

    cout<<"main-Stu s2(s1);"<<endl;

    Stu s2(s1);

    cout<<"main-Stu *s3 = new Stu(s1);"<<endl;

    Stu *s3 = new Stu(s1);

    cout<<"main-Test(s1);"<<endl;                 

    Test(s1);

    cout<<"main - }"<<endl;

}

Stu Test(Stu s)

{

    cout<<"Test - return s;"<<endl;

    return s;

}


복사 생성자 수행

[그림 2.6]

 

 이번에는 어떠한 경우에 개발자가 복사 생성자를 정의를 해야 하는지에 대해 알아보기로 합시다. 디폴트 생성자에서는 단순히 메모리를 덤핑하여 복사한다는 것은 앞에서 설명을 드렸습니다. 그럼에도 불구하고 개발자가 복사 생성자를 생성해야 하는 경우가 발생합니다. 여러가지 경우가 있지만 대표적인 경우가 개체 내부에 동적으로 다른 개체를 가지고 있을 경우입니다. 예를 들어 학생 개체가 책을 소유할 수 있게 정의한다고 가정합시다. 학생 A가 책1을 가지고 있고 학생 A를 입력 인자로 학생 B를 복사 생성한다고 하면 학생 B도 같은 책1을 가지게 됩니다. 큰 문제가 없을 것이라 생각이 되지만 두 학생이 가지고 있는 책은 다른 책이 아니라 같은 책으로 실제로 하나의 책만 있는 것입니다. 이 경우에 학생 A가 자신의 책의 내용을 변경하였을 경우에 학생 B가 가지고 있는 책의 내용도 변경이 됩니다. 두 명의 학생이 가지고 있는 책은 다른 개체가 아니라 같은 개체이기 때문입니다. 그리고 학생 A에서 책을 소멸시키면 학생 B에서는 이미 소멸된 책을 갖고 있게 됩니다. 이미 메모리에서 해제된 개체를 접근하는 것이기 때문에 심각한 버그가 될 수 있겠죠.

 

 다음은 이러한 문제점에 대해 살펴볼 수 있는 예제 코드입니다. Stu 클래스에 개발자가 복사 생성자를 정의하지 않았기 때문에 디폴트 복사 생성자가 만들어 질 것입니다. 이 경우에 어떠한 문제점이 있는지 살펴보시기 바랍니다.

 

Book.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Book

{

    string title;

    string memo;

public:

    Book(string _title);

    void SetMemo(string _memo);

    void View();

};

  

Book.cpp

#include "Book.h"

Book::Book(string _title)

{

    title = _title;

    memo = "";

}

void Book::SetMemo(string _memo)

{

    memo = _memo;

}

void Book::View()

{

    cout<<"제목:"<<title<<" 메모:"<<memo<<endl;

}

 

Stu.h

#pragma once

#include "Book.h"

class Stu

{

    string name;

    Book *book;

public:

    Stu(string _name);

    void SetBook(Book *_book);

    void Study();

    void View();

};

  

Stu.cpp

#include "Stu.h"

Stu::Stu(string _name)

{

    name = _name;

    book = 0;

}

void Stu::SetBook(Book *_book)

{

    book = _book;

}

void Stu::Study()

{

    if(book)    {    book->SetMemo("끄적끄적");    }

}

void Stu::View()

{

    cout<<"이름:"<<name<<endl;

    if(book)    {    book->View();    }

    else    {         cout<<"소유한 책이 없음"<<endl;    }

}

 

Demo.cpp - 데모 소스

#include "Stu.h"

void main()

{

    Stu s1("홍길동");

    s1.SetBook(new Book("Escort C++"));

    Stu s2(s1);

    s2.Study();

    s1.View();

}

 

[그림 2.7]



2장 캡슐화 Part1 

2장 캡슐화 Part2 

(모든 동영상 강의는 무료입니다.)

반응형