Computer Science/C & C++

[C++] Class Constructor | 클래스 생성자

lww7438 2020. 2. 25. 23:01

Class Constructor

클래스 생성자

- 클래스에 반드시 포함되는 특별한 멤버 함수 중 하나이다.
- 해당 클래스를 통해 데이터 객체가 생성되면 프로그램에 의해 자동으로 호출되는 메서드이다.
- 새로운 데이터 객체를 생성하고 그들의 데이터 멤버에 값을 대입하는 등의 기본적인 연산을 수행한다.
(사용자가 어떻게 정의하느냐에 따라 다양한 작업들도 수행할 수 있다.)


Constructor Definition (생성자 정의)

- 생성자는 별도의 리턴형을 가지지 않는다.(\(\texttt{void}\)형에도 해당되지 않는다.)
- 생성자의 이름은 해당 클래스의 이름과 동일하다.
- 각각의 생성자들끼리는 함수 Signature(매개변수 정보)를 달리하여, 생성자 또한 오버로딩이 가능하다.
(즉, 각각의 생성자는 자신만의 유일한 함수 Signature를 가져야 한다.)
- 생성자는 객체를 만드는 메서드이므로, 생성자 호출이 진행중인 상태는 아직 데이터 객체가 완전히 만들어지지 않은 상태임을 의미한다. 따라서, 생성자는 데이터 객체에 대한 멤버 함수를 수행시킬 수 없다.

// Ex. 생성자 정의 예시

class Stock {
private:
    ...
public:
    Stock();
    Stock(std::string&, int);
};

Stock::Stock() {...}

Stock::Stock(std::string& co, int n) {...}

Using Constructor (생성자 호출)

- 생성자는 데이터 객체를 선언할 때, 명시적 또는 암시적 방법으로 호출할 수 있다.

// Ex. 생성자 사용 예시
class Stock {
private:
    string m_company;
    int m_shares;
    double m_share_val;
    ...
public:
    Stock(const string& c, long s, doubles_v);    // 3개의 매개변수를 가진 생성자
    ...
};

...

// 명시적인 생성자 호출
Stock food = Stock("World Cabbage", 250, 1.25);

// 암시적인 생성자 호출
Stock garment("Furry Mason", 50, 2.5);

// new를 이용한 생성자 호출
Stock *pstock = new Stock("Electroshock Games", 18, 19.0);

- 생성자 호출 구문은 다른 메서드들과 달리, 멤버 접근 연산자가 필요가 없다.
(생성자는 객체에 의해 호출되는 것이 아니라 객체를 생성하는 메서드이기 때문이다.)
- 새로 선언된 객체가 생성자를 호출하게 되면, 생성자가 건네받은 Argument들을 객체에 그대로 대입할 수도 있고,
임시 객체에 값들을 저장하고 그 임시 객체를 새롭게 선언된 객체에 대입하는 방식으로 초기화 할 수도 있다.
- 객체의 값을 초기화 과정을 통해 설정할 수 있고, 대입을 통해서 설정할 수도 있는 환경이라면, 가급적 초기화를 통해 설정하도록 한다.

- 객체가 새로 생성된 이후에도, 추가적으로 생성자를 명시적인 방법을 통해 재호출할 수 있다.
※ 이 때, 다시 호출된 생성자는 임시 객체를 새로 만들어 정의된 작업을 수행한 이후에 생성자를 다시 호출한 객체에 대입하는 작업을 수행한다. 이 과정에서 임시 객체가 생겨나고 파괴되므로 이 임시 객체에 대한 파괴자도 호출된다.
(이 임시 객체에 대한 파괴자가 호출되는 타이밍은 컴파일러마다 차이가 있다.)

// Ex. 생성자를 다시 호출하는 예시

Stock stock1("Nifty Foods", 10, 50.0);
...
stock1 = Stock("Boffo objects", 2, 2.0);    // 이 때는, 임시 객체에 Argument를 대입시킨 후, 그 임시 객체를 stock1 객체에 새롭게 대입한다.


- 동적으로 메모리를 할당받는 구문에서도 마찬가지로, 생성자가 호출된다.

// Ex. 메모리를 동적으로 할당받는 경우에 호출되는 생성자
// 여기서, value는 typeName형 값이다.

className* pClass = new className(value);

// 위 구문은 아래와 같은 생성자들 중 하나를 호출시킨다.

className (typeName);
className (const typeName&);

Default Constructor (디폴트 생성자)

- 매개변수를 갖지 않아, 초기화 값을 따로 제공하지 않고 객체를 생성시키는 생성자이다.
- 프로그래머가 생성자를 따로 정의하지 않으면, 컴파일러는 내용 없는 디폴트 생성자를 자동으로 만든다.

// Ex. Stock 클래스의 디폴트 생성자 예시

Stock::Stock() { }     // 디폴트 생성자 내부는 아무 내용도 없다.

※ 만일 프로그래머가 어떤 형태의 생성자이던, 생성자를 하나 이상 정의한다면 컴파일러는 따로 생성자를 자동으로 만들지 않는다. 따라서 어떠한 형태이든, 한 생성자를 정의했다면, 가능한 모든 경우에 대비해 다양한 Signature의 생성자를 만들어놔야 한다.

// Ex. 생성자 정의 시 유의사항 예시
Stock::Stock(const string& co, int n) {...}

Stock stock1;     // 디폴트 생성자를 요구하는 객체 생성을 시도한다.
// 생성자를 위와 같은 형태로 하나만 정의해놓았기 때문에 이 구문은 오류를 낳는다.

디폴트 생성자는 두 가지 방법으로 정의할 수 있다.

// Ex. 디폴트 생성자를 정의하는 두 가지 방법

// 1. 매개변수 없이 디폴트 생성자를 직접 정의
Stock::Stock() { }
// C++ 초기버전에서는 1번 방법만으로만 디폴트 생성자를 정의할 수 있었다.


// 2. 기 정의된 생성자의 모든 매개변수에 디폴트 값을 부여하여 디폴트 생성자를 정의
Stock::Stock(const string& co = "Error", int n = 0, double pr = 0.0);
// 이 방법은 매개변수를 받는 여러가지 버전도 생성하고, 디폴트 생성자도 만들게 되므로 유용하다.

- 사용자는 디폴트 생성자를 한 가지만 가질 수 있다. 따라서 위와 같이 두 경우를 동시에 정의하게 되면, 디폴트 생성자를 호출할 때 어느 버전을 골라야 할지에 대한 Ambiguity(모호성) 에러가 발생하게 된다.
(두 가지 경우 모두, \(\texttt{Stock();}\) 구문으로 호출할 수 있기 때문이다.)
- 따라서, 디폴트 생성자를 정의하고자 한다면, 위와 같은 두 가지 방법 중 하나만 선택해서 정의해야 한다.

// Ex. 디폴트 생성자 호출 예시

Stock first;                 // Implicit(암시적) 디폴트 생성자 호출
Stock *second = new Stock;   // Implicit(암시적) 디폴트 생성자 호출
Stock third = Stock();       // Explicit(명시적) 디폴트 생성자 호출

One-Argument Constructor (하나의 매개변수를 가진 생성자)

- 매개변수의 종류가 하나인 생성자에 한해서, \(\texttt{className objectName = value;}\) 형태의 구문을 허용한다.
- 이는, C++이 Built-in Data Type(내재 데이터형)과 다름없는 \(\texttt{class}\)에 지향점을 두는 것의 반증이다.
- 위와 같은 대입 구문의 형태는 사용자로 하여금 클래스 객체를 일반 데이터형을 다루는 만큼의 편리함을 제공한다.

// Ex. 하나의 매개변수를 가지는 생성자를 이용한 클래스 객체 선언 예시

class Bozo {
private:
    int m_val;
    ...
public:
    Bozo(int);      // 매개변수가 하나인 생성자
    ...
};

...

Bozo tubby = 32;     // Bozo tubby(32); 구문과 같다.

tubby = 12.25;       // 실제로는, tubby = Bozo((int)12.25); 구문으로 변환되어 실행된다.       
// Parameter와 Argument간 데이터형이 일치하지 않을 경우엔, 컴파일러가 암시적 형변환을 수행하여 값을 대입한다.
// 이 경우에는 double(혹은 float)형 Argument를 int형 Parameter에 대입시키고 있으므로, 컴파일러는 Argument를 int형으로 변환시키게 된다.
// 단, Bozo(double)이나 Bozo(float)와 같은 생성자가 정의되지 않은 상태이어야 Ambiguity 에러가 발생하지 않는다.

 


\(\texttt{explicit}\) Keyword (\(\texttt{explicit}\) 키워드)

- 하나의 매개변수를 가진 생성자에 한해서 선언할 수 있는 키워드이다.
- 하나의 매개변수를 가진 생성자는, 특별히 \(\texttt{objectName = value;}\) 형태의 구문과 같이 암시적 형변환을 허용하는데, \(\texttt{explicit}\) 키워드는 이 암시적 형변환을 불허하게 하는 키워드이다.
- 하지만, \(\texttt{objectName = Constructor(value);}\)와 같은 명시적 형변환은 허용한다.


Member Initializer List Syntax (멤버 초기자 리스트 문법)

- 생성자에서 멤버 초기화를 수행시키는 수단 중 하나이다.
- 일반적인 \(\texttt{this->member = value;}\) 구문에서는 복사 생성자가 실행되어 임시 데이터 객체에 \(\texttt{value}\)가 전달되고, 그 임시 객체가 \(\texttt{this->member}\)에 전달된다.
- 멤버 초기자 리스트에 있는 항목들은 순서대로 초기화되지 않고, 클래스 정의에 선언된 순서대로 초기화된다.
- 멤버 초기자 리스트는 위와 같은 이중적인 초기화 과정이 생략되고 더 빠른 시간내에 초기화를 가능하게 해준다.

// Ex. 멤버 초기자 리스트 구문 예시

class className {
private:
    string A;
    bool B;
    int C;
public:
    className(const string& a = "None", bool b = false, int c = 0);
    ...
};


className::className(const string& a, bool b, int c) : A(a), B(b), C(c) {}

// 위 정의 내용은 아래의 정의 내용과 같은 결과를 내지만, 수행시간에서 차이가 난다.

className::className(const string& a, bool b, int c) {
    A = a;
    B = b;
    C = c;
}

※ 멤버 초기자 리스트는 Constructor에만 허용되는 문법이다.

※ \(\texttt{const}\)형 멤버와 참조로 선언된 멤버의 경우,
   Constructor에서 반드시 멤버 초기자 리스트를 통해 초기화해야 하며,
   일반적인 대입 연산을 불가능하다.

// const형 멤버는 일반적인 대입 연산을 허용하지 않는다.

class className {
private:
    string A;
    bool B;
    const int C;
	int& D;
public:
    className(const string& a = "None", bool b = false, int c = 0, int d = 1);
    ...
};


className::className(const string& a, bool b, int c) {
    A = a;
    B = b;
    C = c;    // 허용되지 않는다!
    D = d;    // 허용되지 않는다!
}

// 해결책
className::className(const string& a, bool b, int c, int d) : C(c), D(d) {
    A = a;
    B = b;
}

List Initialization for Class Object (클래스 객체에 대한 리스트 초기화)

- C++11에서 클래스 객체에도 리스트 초기화를 수행할 수 있게 되었다.
- 리스트 초기화는 객체에 데이터를 대입할 때, \(\texttt{=}\) 연산자(대입 연산자)가 아닌, \(\texttt{{ }}\) 괄호(중괄호)를 이용하여 대입(초기화)를 한다.
- 또한 C++11부터는 \(\texttt{std::initializer_list}\) 클래스를 호출할 수 있는데, 이 클래스를 통해 임의의 길이를 가진 리스트를 표현할 수 있고, 모든 매개변수들을 같은 데이터 타입으로 변환시킬 수도 있다.
* Narrowing 측면에서 List Initialization의 장점

// Ex. 클래스 객체에 대한 리스트 초기화 예시

Stock jock {"Sport Age Storage, Inc"};
Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};

Member In-Class Initialization (멤버 In-Class 초기화)

- 클래스 정의부에서, 멤버 변수에 값을 대입하여 멤버 초기화 리스트를 통해 초기화한 것과 동일한 효과를 낼 수 있다.

class Classy {
private:
    int mem1 = 10;         // In-Class Initialization
    const int mem2 = 20;   // In-Class Initialization
    ...
};

// 위와 같이, 클래스 정의에서 초기화를 하여
// 멤버 초기화 리스트를 통해 mem1, mem2를 각각 10, 20으로 초기화하는 것과
// 같은 효과를 낸다.