클래스 설계에 있어 몇 가지의 설계 지침이 존재한다.

컴파일러가 자동으로 생성하는 멤버 함수들

오늘은 그거에 대해서 정리해 보자.

 

디폴트 생성자

 

매개변수를 전혀 사용하지 않거나, 모든 매개변수가 디폴트 매개변수를 사용하는 생성자다.

사용자가 생성자를 하나도 정의하지 않으면, 사용자를 대신하여 컴파일러는 디폴트 생성자를 정의한다.

디폴트 생성자의 존재는 사용자가 객체를 생성하는 것을 허용한다.

 

모든 기초 클래스를 위한 디폴트 생성자를 호출하는 것과, 다른 클래스의 객체인 멤버들을 호출하는 것도 수행한다.

멤버 초기자 리스트를 사용하여 기초 클래스 생성자를 명시적으로 호출하지 않고 파생 클래스 생성자를 작성하게 되면

컴파일러는 새 객체의 기초 클래스 부분을 생성하기 위해 기초 클래스 디폴트 생성자를 사용하게 된다.

 

사용자가 만약에 생성자를 정의하게 되면, 컴파일러는 디폴트 생성자를 정의하지 않음에 유의하자.

이때에는 디폴트 생성자를 제공하는 것이 프로그래머의 책임으로 된다.

 

생성자를 정의하는 이유는 객체가 항상 바르게 초기화되도록 하기 위한 것도 있다.

특히 클래스가 포인터 멤버를 가지고 있을 경우에는 확실하게 초기화되어야 한다.

모든 클래스 데이터 멤버를 의미 있는 값으로 초기화시키는, 명시적 디폴트 생성자를 제공하는 것이 바람직하다.

 

복사 생성자

 

클래스형의 객체를 매개변수로 사용하는 생성자.

  • 새 객체를 동일한 클래스의 다른 객체로 초기화할 때
  • 객체가 함수에 값으로 전달할 때
  • 함수가 객체를 값으로 리턴할 때
  • 컴파일러가 임시 객체를 생성할 때

프로그램이 복사 생성자를 (명시적 or 암시적) 사용하지 않으면, 컴파일러는 함수 정의가 아니라 원형만 제공한다.

그렇지 않으면, 프로그램은 멤버별 초기화를 수행하는 복사 생성자를 정의한다.

 

멤버별 초기화가 좋지 못한 상황이 존재하는데,

new에 의해 초기화되는 멤버 포인터들은, 일반적으로 깊은 복사를 요구한다.

이럴 때에는 복사 생성자를 직접 정의할 필요가 있다.

대입 연산자

 

디폴트 대입 연산자는 어떤 객체를 동일한 클래스의 다른 객체에 대입하는 것을 처리한다.

대입과 초기화를 혼동하면 안 된다!

구문이 새 객체를 생성한다면 그것은 초기화를 사용하는 것이고,

구문이 기존 객체의 값을 변경한다면, 그것은 대입이다.

Star sirius;
Star alpha = sirius; // 초기화

Star dogstar;
dogstar = sirius; // 대입

디폴트 대입은 멤버 간 대입을 사용한다.

어떤 멤버가 그 자체로 클래스 객체일 경우에는 디폴트 멤버간 대입은 해당 클래스에 정의된 대입 연산자를 사용하게 된다.

 

복사 생성자를 명시적으로 정의할 필요가 있다면, 같은 이유로 대입 연산자도 명시적으로 정의할 필요가 있다.

Star & Star::operator=(const Star&);

컴파일러는 하나의 데이터형을 다른 데이터형에 대입하는 대입 연산자는 생성하지 않는다.

 

클래스 메서드에 관련된 그 밖의 고려 사항

 

# 생성자 관련 사항

생성자들은 새로운 객체를 생성한다는 점에서 클래스의 다른 메서드들과 구별된다.

상속은 파생 클래스 객체가 기초 클래스 메서드를 사용할 수 있다는 것을 의미한다. 그런데 생성자의 경우에 그 객체는 생성자가 자신의 일을 끝낸 후까지 존재하지 않는다.

 

# 파괴자 관련 사항

클래스 생성자에서 new에 의해 대입되는 메모리를 해제하고, 클래스 객체의 파괴에 따르는 특별한 정리 작업을 처리하기 위해서

명시적 파괴자를 반드시 정의해야 한다는 것을 기억하자.

특히 그 클래스를 기초 클래스로 사용할 예정이라면, 가상 파괴자를 제공하자.

 

# 변환 관련 사항

정확히 하나의 매개변수를 사용하여 호출할 수 있는 생성자는, 그 매개변수의 데이터형을 클래스형으로 변환하는 것을 정의한다.

Star(const char *); // const char *를 Star로 변환
Star(const Spectral&, int members = 1); // Spectral을 Star로 변환

Star north;
north = "polaris"

1. Star::Star(const char *)를 사용해서 Star 객체를 생성하고,
2. 생성된 Star 객체를 매개변수로 사용하여 Star::operator=(const Star&) 메서드를 호출하게 된다.
단, 이것은 (char *)를 Star에 대입하는 대입 연산자를 정의하지 않았을 때에만 해당

만약에 explicit 키워드를 사용한다면

public:
	explicit Star(const char *);
    
    
north = "polaris";       // 허용되지 않고
north = Star("polaris"); // 허용된다.

 

클래스 객체를 다른 데이터형으로 변환하려면, 변환 함수를 정의해야 한다.

변환 함수는 매개변수를 사용하지 않고, 변환 결과 데이터형의 이름을 리턴형으로 선언하지 않는 클래스 멤버 함수다.

Star::Star double() const { ... } // Star를 double로 변환
Star::Star const char * () const {...} // Star를 const char *로 변환

변환 함수는 이치에 맞을 경우에만 신중하게 사용하는 것이 좋다. 그렇지 않으면 모호한 코드를 작성할 확률이 높아진다고 한다.

이때 explicit 키워드를 사용한다면 명시적 변환만 허용하기 때문에 조금 더 신중하게 작성될 수 있다.

 

값으로 전달, 참조로 전달

 

객체를 값으로 전달하지 않고 참조로 전달해야 한다. 이는 효율성

객체를 값으로 전달하면 임시 복사본이 생긴다. 복사 생성자를 호출하고, 나중에 파괴자를 호출한다는 것을 의미

큰 객체의 복사는 참조로 전달할 때보다 속도가 많이 느려질 수 있다.

그러므로 함수가 객체를 변경하지 않는다면, 그 함수의 매개변수를 const 참조로 선언해야 한다.

 

가상 함수를 사용하는 상속의 경우에, 기초 클래스 참조 매개변수를 받아들이도록 정의된 함수는,

파생 클래스에 대해서도 성공적으로 사용이 가능하다. (전정한 가상 함수의 힘 - 동적 결합이 실현된다.)

 

객체 리턴과 참조 리턴

 

참조를 리턴하는 것이 더 좋다.

객체를 리턴하게 되면 리턴된 객체의 임시 복사본을 생성하기 때문이다.

그 복사본은 호출한 프로그램에서 사용할 수 있는데, 객체를 리턴하면 복사 생성자를 호출하여 복사본을 생성하는 시간 부담과,

나중에 파괴자를 호출하여 그 복사본을 없애는 시간 부담이 따른다.

참조로 리턴하게 되면, 시간과 메모리가 절약된다.

 

객체를 직접 리턴하는 것은 객체를 값으로 전달하는 것과 비슷하다.

둘 다 임시 복사본을 생성하며, 참조를 리턴하는 것은 객체를 참조로 전달하는 것과 비슷하다.

 

주의할 점은

함수는 그 함수 안에서 생성된 임시 객체에 대한 참조를 리턴하면 안 된다.

함수가 종료되어 그 객체가 파괴되면 더 이상 참조가 유효하지 않기 때문이다.

이러한 코드의 경우 호출한 함수에서 사용할 수 있는 복사본을 생성할 수 있도록 객체를 리턴해야 옳다.

 

함수가 그 함수 안에서 생성된 임시 객체를 리턴한다면, 참조를 사용해서는 안 된다.

Vector Vector::operator+(const Vector& b) const
{
	return Vector(x + b.x, y + b.y);
}

 

참조나 포인터를 통해 전달된 객체를 함수가 리턴한다면, 그 객체를 참조로 리턴해야 한다.

const Stock& Stock::topval(const Stock& s) const
{
    if (s.total_val > total_val)
    	return s;     // 매개변수로 전달된 객체
    else
    	return *this; // 호출한 객체
}

 

const의 사용

 

const를 사용할 기회가 있을 때 신중해질 필요가 있다.

메서드가 매개변수를 변경하지 않는다는 확신이 있을 때, 그것을 사용할 수 있다.

Star::Star(const char * s) { ... } //s가 지시하는 문자열을 변경하지 않는다.

메서드가 그것을 호출한 객체를 변경하지 않는다는 확신이 있을 때, const를 사용할 수 있다.

void Star::show() const {...} // 호출한 객체를 변경하지 않는다.

여기서 사용된 const는 const Star * this를 의미하고, this는 호출한 객체를 지시한다.

일반적으로, 참조를 리턴하는 함수는 대입 구문의 왼쪽에 올 수 있다.

 

객체 안의 데이터를 변경하는 데 참조나 포인터 리턴값을 사용할 수 없도록 하기 위해서 const를 사용할 수 있다.

const Stock & Stock::topval(const Stock& s) const
{
}

함수의 매개변수를 const에 대한 참조나 const를 지시하는 포인터로 선언하면,

매개변수를 변경하지 않는다는 보장이 없는 경우에 그 매개변수를 다른 함수에 전달할 수 없다.

 

public 상속에 관련된 그 밖의 고려 사항들

 

# is - a 관계

파생 클래스가 기초 클래스의 특별한 종류가 아니면 public 파생을 사용하면 안 된다.

어떤 경우에는 순수 가상 함수를 가지고 있는 추상화 기초 클래스를 생성하고, 그것으로부터 다른 클래스를 파생시키는 것이 최선의 방법일 수 있다.

 

명시적 데이터형 변환 없이 기초 클래스 포인터가 파생 클래스 객체를 지시할 수 있고,

기초 클래스 참조가 파생 클래스 객체를 참조할 수 있다는 것이 is-a 관계의 특성이다.

다만 그 역은 성립되지 않는다.

명시적인 데이터형 변환 없이는 파생 클래스 포인터나 참조가 기초 클래스 객체를 참조할 수 없다.

다운캐스팅이 이치에 맞을 수도 있고 아닐 수 도 있다.

 

# 상속되지 않는 것

생성자는 상속되지 않는다.

일반적으로 파생 클래스 생성자는 파생 클래스 객체의 기초 클래스 부분을 생성하기 위해, 

멤버 초기자 리스트 문법을 사용하여 기초 클래스 생성자를 호출한다.

 

파생 클래스 생성자가 멤버 초기자 리스트 문법을 사용하여 기초 클래스 생성자를 명시적으로 호출하지 않는다면

기초 클래스의 디폴트 생성자를 사용하게 된다.

 

파괴자는 상속되지 않는다.

컴파일러는 디폴트 파생 클래스 파괴자를 생성한다.

일반적으로 어떤 클래스가 기초 클래스의 역할을 한다면, 그 파괴자는 가상이어야 한다.

 

대입 연산자는 상속되지 않는다.

상속받은 메서드는 기초 클래스와 파생 클래스에서 동일한 함수 시그내처를 사용한다.

그러나 대입 연산자는 그 클래스형의 형식 매개변수를 가지기 때문에, 각 클래스마다 다른 함수 시그내처를 가진다.

 

# 대입 연산자 관련 사항

한 객체를 동일한 클래스의 다른 객체에 대입하는 것을 컴파일러가 발견하면, 

컴파일러는 그 클래스에 대해 자동으로 대입 연산자를 제공한다. (멤버별 대입, 암시적 디폴트 버전)

 

기초 클래스를 위한 대입 연산자를 명시적으로 제공하지 않으면, 그 연산자가 사용된다.

같은 논리로, 어떤 클래스가 다른 클래스의 객체를 멤버로 가지고 있을 때, 그 멤버의 대입을 위해서 그 멤버 클래스의 대입 연산자가 사용된다.

 

중요. 클래스 생성가 new를 사용하여 포인터를 초기화하고 있다면, 명시적 대입 연산자를 제공해야 한다.

 

파생 클래스 객체를 기초 클래스 객체에 대입한다면?

대입 구문은 왼쪽에 있는 객체에 의해 호출되는 메서드로 번역된다는 사실을 기억하자.

 

역으로, 파생 클래스 참조는 기초 클래스 객체를 자동으로 참조할 수 없기 때문에,

변환 생성자가 준비되어 있지 않다면 코드를 실행되지 않는다.

 

# 가상 메서드

파생 클래스에서 메서드를 다시 정의하려면, 기초 클래스에서 그 메서드를 가상으로 정의하자.

이것은 동적 결합을 가능케 한다.

 

코드를 동적 결합하게 하려면, 객체를 참조나 포인터 형태로 전달해야 한다.

 

# 파괴자 관련 사항

기초 클래스 파괴자는 가상이어야 한다.

객체에 대한 기초 클래스 포인터나 참조를 통해 파생 클래스 객체를 파괴할 때, 프로그램은 기초 클래스 파괴자만 사용하는 것이 아니라,

파생 클래스 파괴자를 먼저 사용하고, 뒤이어 기초 클래스 파괴자를 사용한다.

 

# 프렌드 관련 사항

프렌드 함수는 클래스 멤버가 아니기 때문에 상속되지 않는다.

다만 파생 클래스의 프렌드를 기초 클래스의 프렌드로 사용하기를 원할 수 있는데,

이때에는 데이터형 캐스팅을 실시하고 사용하면 된다.

 

# 기초 클래스 메서드의 사용

  • 파생 클래스 객체는 그 파생 클래스가 메서드를 다시 정의하지 않는다면, 상속된 기초 클래스 메서드를 자동으로 사용한다.
  • 파생 클래스 파괴자는 기초 클래스 파괴자를 자동으로 호출한다.
  • 파생 클래스 생성자는 멤버 초기자 리스트에서 다른 생성자를 지정하지 않는다면 기초 클래스 디폴트 생성자를 자동으로 호출한다.
  • 파생 클래스 생성자는 멤버 초기자 리스트에 지정된 기초 클래스 생성자를 명시적으로 호출한다.
  • 파생 클래스 메서드들은 public, protected 기초 클래스 메서드들을 호출하기 위해 사용 범위 결정 연산자를 사용할 수 있다.
  • 파생 클래스의 프렌드는 파생 클래스 참조나 포인터를 기초 클래스 참조나 포인터로 데이터형을 캐스트 하고 그렇게 변환된 참조나 포인터를 사용하여 기초 클래스의 프렌드를 호출할 수 있다. 

'CS > C++' 카테고리의 다른 글

클래스 상속  (0) 2024.07.27
접두어 방식과 접미어 방식의 차이  (1) 2023.03.05

+ Recent posts