8.1 스프레드시트 예제
이 장과 다음 장에서는 실제로 실행할 수 있는 간단한 스프레드시트 예제를 이용하여 여러 가지 개념을 소개한다. 여기서 만들 스프레드시트는 셀(cell)이란 단위로 구성된 2차원 격자로서, 각 셀은 숫자나 스트링(string)을 담을 수 있다. 스프레드시트 예제 애플리케이션은 Spreadsheet와 SpreadsheetCell이란 기본 클래스를 사용한다. Spreadsheet 객체마다 SpreadsheetCell 객체를 가진다. 그리고 이러한 Spreadsheet를 관리하는 SpreadsheetApplication이란 클래스도 정의한다. 이 장에서는 SpreadsheetCell을 중심으로 소개한다.
8.2 클래스 작성 방법
클래스를 작성하려면 그 클래스의 모든 객체에 적용할 동작(메서드, method)과 각 객체마다 가질 속성(데이터 멤버, data member)을 지정한다. 클래스를 작성하는 과정은 클래스를 정의하는 단계와 클래스를 구현하는(클래스의 메서드를 정의하는) 단계로 구성된다.
8.2.1 클래스 정의
SpreadsheetCell 클래스의 첫 버전을 작성해보자. 여기서는 각 셀마다 숫자 하나만 저장하도록 정의한다.
class SpreadsheetCell {
public:
void setValue(double inValue);
double getValue() const;
private:
double mValue;
};
클래스 정의는 항상 class 키워드와 클래스 이름으로 시작한다. C++에서 클래스 정의(class definition)는 문장(statement)이다. 따라서 반드시 세미콜론(;)으로 끝나야 한다. 클래스 정의를 작성한 파일의 이름은 주로 클래스 이름과 똑같이 짓는다. 예를 들어 SpreadsheetCell 클래스 정의 파일은 SpreadsheetCell.h 파일에 저장한다. 반드시 지켜야 할 규칙은 아니므로 원하는 이름으로 얼마든지 지정할 수 있다.
■ 8.2.1.1 클래스 멤버
클래스는 여러 개의 멤버(member)를 가질 수 있다. 멤버는 메서드, 생성자, 소멸자 같은 멤버 함수(member function)일 수도 있고 열거형, 타입 앨리어스, 중첩 클래스 같은 멤버 변수(member variable)일 수도 있다. 멤버 변수를 데이터 멤버라고도 부른다. 앞의 코드에서 SpreadsheetCell 클래스에서 지원하는 메서드와 데이터 멤버는 다음과 같이 선언했다.
/* 메서드입니다. */
void setValue(double inValue);
double getValue() const;
/* 데이터 멤버입니다. */
double mValue;
C++에서 struct는 디폴트 접근 제한자가 public이란 점을 제외하면 class와 같다. 따라서 C와 달리 클래스처럼 메서드를 가질 수 있다.
struct SpreadsheetCell {
/* public이 적용됩니다. */
void setValue(double inValue);
double getValue() const;
private:
double mValue;
};
■ 8.2.1.2 접근 제어
클래스의 각 멤버는 세 가지 접근 제어 제한자(access control specifier)인 public, protected, private 중 하나로 지정된다. 한 번 지정된 접근 제한자는 다른 지정자로 변경하지 전까지 모든 멤버에 적용된다. 클래스에 접근 제한자를 따로 명시하지 않으면 private가 적용된다. 즉, 접근 제한자를 따로 지정하지 않고 선언한 모든 멤버의 접근 범위는 private가 적용된다.
class SpreadsheetCell {
/* private가 적용됩니다. */
void setValue(double inValue);
public:
double getValue() const;
private:
double mValue;
};
세 가지 접근 제한자의 의미를 정의하면 다음 표와 같다.
접근자 | 의미 |
public | public 멤버 함수와 데이터 멤버는 어디에서나 접근할 수 있다. |
protected | protected 멤버 함수와 데이터 멤버는 같은 클래스로 된 객체의 멤버 함수로 접근할 수 있다. 또한, 파생 클래스의 멤버 함수는 베이스 클래스의 protected 멤버에 접근할 수 있다. |
private | protected 메서드와 데이터 멤버는 같은 클래스로 된 객체의 멤버 함수로만 접근할 수 있다. 파생 클래스의 멤버 함수는 베이스 클래스의 private 멤버 에 접근할 수 없다. |
■ 8.2.1.3 선언 순서
C++에서는 멤버와 접근 제한자를 선언하는 순서를 따로 정해두지 않았다. 데이터 멤버 앞에 멤버 함수를 선언해도 되고, private 뒤에 public을 선언해도 된다. 게다가 접근 제한자를 반복해서 지정해도 된다. 물론 가독성을 위해 public, protected, private 멤버를 함께 묶어서 선언하는 것이 좋다.
■ 8.2.1.4 클래스 내부의 멤버 이니셜라이저
클래스를 정의할 때는 멤버 변수를 선언하는 동시에 초기화할 수 있다. 예를 들어 다음과 같이 SpreadsheetCell 클래스에서 mValue 멤버의 기본값을 0으로 초기화할 수 있다.
/* 클래스 내부 이니셜라이저 */
class SpreadsheetCell {
private:
double mValue = 0;
};
8.2.2 메서드 정의 방법
함수를 만들 때 프로토타입뿐만 아니라 함수를 구현하는 정의 코드를 함께 작성하듯이 메서드도 프로토타입뿐만 아니라 메서드를 구현하는 정의 코드도 반드시 작성해야 한다. 이때 메서드 정의 코드보다 클래스 정의 코드가 먼저 나와야 한다. SpreadsheetCell 클래스의 두 메서드는 다음과 같이 정의한다.
#include "SpreadsheetCell.h"
void SpreadsheetCell::setValue(double inValue) {
mValue = inValue;
}
double SpreadsheetCell::getValue() const {
return mValue;
}
이 코드를 잘 보면 메서드 이름 앞에 클래스 이름과 콜론 두 개(::)가 붙어있다. 여기서 ::를 스코프 지정 연산자라 부른다. 컴파일러가 이 코드를 보면 여기 나온 setValue 메서드는 SpreadsheetCell 클래스에 속한다는 것을 알 수 있다.
■ 8.2.2.1 데이터 멤버 접근 방법
setValue나 getValue 메서드 같은 비정적 멤버는 항상 객체에 대해 실행된다. 다시 말해 이러한 메서드는 클래스에 정의된 데이터 멤버 중 현재 객체에 속한 멤버에 대해 접근한다. 서로 다른 두 객체에 대해 각각 한 번씩 setValue를 호출하면 실행되는 코드는 같지만 각자 속한 객체의 변숫값을 변경한다.
■ 8.2.2.2 다른 메서드 호출하기
메서드는 같은 클래스에 정의된 다른 메서드도 호출할 수 있다. SpreadsheetCell 클래스에 텍스트 데이터를 지원하도록 수정한 첫 번째 버전은 다음과 같다.
#include <string>
#include <string_view>
class SpreadsheetCell {
public:
void setValue(double inValue);
double getValue() const;
void setString(std::string_view inString);
std::string getString() const;
private:
/* 헬퍼 메서드입니다. */
std::string doubleToString(double inValue) const;
double stringToDouble(std::string_view inString) const;
/* 데이터 멤버입니다. */
double mValue;
};
이 버전의 클래스 코드는 셀에 텍스트 값을 설정하고 가져오는 메서드 두 개, double과 string 값을 상호 변환하는 private 헬퍼 메서드 두 개를 추가로 정의했다. 네 메서드의 구현 코드는 다음과 같다.
#include "SpreadsheetCell.h"
/* setString 멤버 함수가 stringToDouble 헬퍼 함수를 호출합니다. */
void SpreadsheetCell::setString(string_view inString) {
mValue = stringToDouble(inString);
}
/* getString 멤버 함수가 doubleToString 헬퍼 함수를 호출합니다. */
string SpreadsheetCell::getString() const {
return doubleToString(mValue);
}
string SpreadsheetCell::doubleToString(double inValue) const {
return to_string(inValue);
}
double SpreadsheetCell::stringToDouble(string_view inString) const {
return strtod(inString.data(), nullptr);
}
■ 8.2.2.3 this 포인터
일반 메서드를 호출하면 항상 메서드가 속한 객체의 포인터인 this가 숨겨진 매개변수 형태로 전달된다. this 포인터로 해당 객체의 데이터 멤버나 메서드에 접근할 수 있으며, 다른 메서드나 함수에 매개변수로 전달할 수도 있다. 때로는 이름을 명확히 구분하는 용도로도 사용된다.
8.2.3 객체 사용법
SpreadsheetCell 클래스 정의에 따라 SpreadsheetCell 객체를 생성하려면 SpreadsheetCell 타입의 변수를 따로 선언해야 한다. 객체를 생성해서 사용하는 방법은 크게 두 가지가 있다. 하나는 스택에 생성하는 것이고 다른 하나는 힙에 생성하는 것이다.
■ 8.2.3.1 스택에 생성한 객체
SpreadsheetCell 객체를 스택에 생성해서 사용하는 예를 살펴보자.
SpreadsheetCell myCell, anotherCell;
myCell.setValue(6);
anotherCell.setString("3.2");
객체를 생성하는 방법은 변수를 선언하는 방법과 같다. 변수 타입이 클래스 이름이라는 점만 다르다. myCell.setValue(6);에 나온 .을 도트(dot) 연산자라 부른다. 이 연산자로 객체에 속한 메서드를 호출한다.
■ 8.2.3.2 힙에 생성한 객체
다음과 같이 new를 사용해서 객체를 동적으로 생성할 수도 있다.
SpreadsheetCell* myCellp = new SpreadsheetCell();
myCellp->setValue(3.7);
delete myCellp;
myCellp = nullptr;
힙에 생성한 객체는 화살표 연산자(->)로 멤버에 접근한다. 화살표 연산자는 역참조 연산자(*)와 멤버 접근 연산자(.)를 합친 것이다. 힙에 할당한 메모리를 항상 해제해야 하듯이 힙에 할당한 객체 메모리도 반드시 delete로 해제해야 한다. 메모리 관련 문제가 발생하지 않게 하려면 다음과 같이 스마트 포인터를 사용한다. 스마트 포인터를 사용하면 메모리를 자동으로 해제하기 때문에 직접 해제하는 문장을 작성할 필요가 없다.
SpreadsheetCell* myCellp = make_unique<SpreadsheetCell>();
또는
unique_ptr<SpreadsheetCell> myCellp(new SpreadsheetCell());
NOTE 스마트 포인터를 사용하지 않는다면 포인터가 가리키는 객체를 삭제한 후 항상 포인터 값을 널 포인터(nullptr)로 리셋하는 것이 좋다. 이렇게 하면 객체를 삭제한 후, 실수로 그 포인터에 접근하더라도 문제를 쉽게 찾을 수 있다.
8.3 객체의 라이프 사이클
객체의 라이프 사이클은 생성(creation), 소멸(destruction), 대입(assignment)의 세 단계로 구성된다. 객체가 생성되고 소멸되고 대입되는 시점과 방법뿐만 아니라 이러한 동작을 원하는 방식으로 변경하는 방법도 정확히 이해할 필요가 있다.
8.3.1 객체 생성
스택에 생성되는 객체는 선언하는 시점에 생성되고, 스마트 포인터나 new, new[]를 사용할 때는 직접 공간을 할당해야 생성된다. 객체가 생성되면 그 안에 담긴 객체도 함께 생성된다. 객체는 선언과 동시에 초깃값을 설정하는 것이 좋다. 이 작업은 생성자(constructor, ctor)라 부르는 특수한 메서드에 객체를 초기화하는 코드를 작성하는 방식으로 처리할 수 있다.
■ 8.3.1.1 생성자 작성 방법
생성자 이름은 클래스 이름과 똑같이 지정한다. 생성자는 반환 값이 없으며 필요에 따라 매개변수를 받을 수 있다. 아무런 인수를 주지 않고 호출하는 생성자를 디폴트 생성자(default constructor)라 부른다. SpreadsheetCell 클래스에 생성자를 추가한 첫 번째 버전을 만들어보자.
/* 생성자 */
class SpreadsheetCell {
public:
SpreadsheetCell(double initValue);
};
SpreadsheetCell::SpreadsheetCell(double initValue) {
setValue(initValue);
}
SpreadsheetCell 생성자도 일종의 SpreadsheetCell 클래스 멤버이다. 따라서 생성자 이름 앞에 SpreadsheetCell::이라는 스코프 지정 연산자를 붙여야 한다.
■ 8.3.1.2 생성자 사용 방법
객체는 생성자를 통해 생성되고 그 객체의 값을 초기화할 수 있다. 스택 객체와 힙 객체 모두 생성자를 사용할 수 있다.
① 스택 객체 생성자
스택에 할당한 SpreadsheetCell 객체의 생성자를 호출하는 방법은 다음과 같다.
SpreadsheetCell myCell(5), anotherCell(10);
cout << "cell 1: " << myCell.getValue() << endl;
cout << "cell 2: " << anotherCell.getValue() << endl;
② 힙 객체 생성자
SpreadsheetCell 객체를 동적으로 할당할 때 생성자를 호출하는 방법은 다음과 같다.
/* 스마트 포인터 */
auto smartCellp = make_unique<SpreadsheetCell>(4);
/* 일반 포인터 */
SpreadsheetCell* myCellp = new SpreadsheetCell(5);
SpreadsheetCell* anotherCellp = nullptr;
anotherCellp = new SpreadsheetCell(10);
delete myCellp;
myCellp = nullptr;
delete anotherCellp;
anotherCellp = nullptr;
함수 안에서 스택에 포인터를 선언하거나 클래스의 데이터 멤버로 포인터를 선언할 때 그 포인터를 곧바로 초기화하지 않을 경우, 여기 나온 anotherCellp처럼 반드시 nullptr로 초기화한다. 또한, new로 객체를 동적으로 할당했다면 반드시 그 객체를 delete로 해제해야 한다.
■ 8.3.1.3 생성자 오버로딩
클래스에 생성자를 여러 개 만들 수 있다. 생성자가 여러 개이더라도 이름은 모두 똑같이 클래스 이름으로 정하고, 인수의 개수나 타입만 서로 다르게 한다. 이름이 같은 함수가 여러 개 있으면 컴파일러는 호출하는 시점에 매개변수의 개수와 타입이 일치하는 함수를 선택한다. 이를 오버로딩(overloading)이라고 부른다. 앞에서 본 SpreadsheetCell 클래스를 생성자를 두 개 갖도록 수정해보자. 하나는 double 타입의 초깃값을 받고, 다른 하나는 string 타입의 초깃값을 받는다.
/* 생성자 오버로딩 */
class SpreadsheetCell {
public:
SpreadsheetCell(double initValue);
SpreadsheetCell(string_view initString);
};
SpreadsheetCell::SpreadsheetCell(double initValue) {
setValue(initValue);
}
SpreadsheetCell::SpreadsheetCell(string_view initString) {
setString(initString);
}
■ 8.3.1.4 디폴트 생성자
디폴트 생성자는 아무런 인수도 받지 않는 생성자다. 영인수 생성자(0-argument constructor)라고도 부른다. 디폴트 생성자를 이용하면 직접 값을 지정하지 않고도 데이터 멤버를 초기화할 수 있다.
① 디폴트 생성자 작성 방법
앞에서 본 SpreadsheetCell 클래스에 디폴트 생성자를 정의하는 방법과 이렇게 정의한 디폴트 생성자의 구현 코드에 대한 첫 번째 버전은 다음과 같다. mValue에 클래스 내부의 멤버 이니셜라이저를 적용하면 생성자 구현 코드에 유일하게 작성한 문장마저 생략할 수 있다.
/* 디폴트 생성자 */
class SpreadsheetCell {
public:
SpreadsheetCell();
private:
double mValue = 0.0;
};
SpreadsheetCell::SpreadsheetCell() { }
스택 객체의 디폴트 생성자를 호출하는 방법은 다음과 같다.
SpreadsheetCell myCell(); /* 컴파일러는 myCell 함수를 선언하였다고 여깁니다.*/
myCell.setValue(6); /* 컴파일 오류가 발생합니다. */
SpreadsheetCell anotherCell; /* 소괄호를 반드시 생략합니다. */
anotherCell.setValue(6); /* 컴파일 오류가 발생하지 않습니다. */
스택 객체의 다른 생성자와 달리 디폴트 생성자는 함수 호출의 형식을 따르지 않는다. 컴파일러는 첫 문장을 인수를 받지 않고 리턴 타입이 SpreadsheetCell인 myCell이란 이름의 함수를 새로 선언한다고 여긴다. 그런 다음 두 번째 문장을 보고 함수 이름을 객체처럼 사용하는 실수를 저질렀다고 착각한다.
CAUTION 스택 객체를 생성할 때는 디폴트 생성자에 소괄호를 반드시 생략한다.
힙 객체의 디폴트 생성자를 호출하는 방법은 다음과 같다.
/* 스마트 포인터 */
auto smartCellp = make_unique<SpreadsheetCell>();
/* 일반 포인터 */
SpreadsheetCell* myCellp = new SpreadsheetCell();
delete myCellp;
myCellp = nullptr;
② 컴파일러에서 생성한 디폴트 생성자
생성자를 하나도 지정하지 않으면 인수를 받지 않는 디폴트 생성자를 컴파일러가 대신 만들어준다. 이렇게 컴파일러에서 생성한 디폴트 생성자는 해당 클래스의 객체 멤버에 대해서도 디폴트 생성자를 호출해준다. 하지만 int나 double 같은 기본 타입에 대해서는 초기화하지 않는다. 이렇게 해서 클래스의 객체를 생성할 수는 있지만 디폴트 생성자나 다른 생성자를 하나라도 선언하면 컴파일러가 디폴트 생성자를 자동으로 만들어 주지 않는다.
③ 명시적으로 생성한 디폴트 생성자
C++에서는 명시적으로 생성한 디폴트 생성자를 제공한다. 이를 이용하면 다음과 같이 클래스 구현 코드에 디폴트 생성자를 작성하지 않아도 된다.
class SpeadsheetCell {
public:
/* 명시적으로 생성한 디폴트 생성자 */
SpreadsheetCell() = default;
SpreadsheetCell(double initValue);
SpreadsheetCell(std::string_view initValue);
}
④ 명시적으로 삭제한 디폴트 생성자
C++는 명시적으로 삭제한 생성자도 지원한다. 예를 들어 정적(static) 메서드로만 구성된 클래스를 정의하면 생성자를 작성할 필요가 없을 뿐만 아니라 컴파일러가 생성자를 만들면 안 된다. 이럴 때는 다음과 같이 디폴트 생성자를 명시적으로 삭제해야 한다.
class SpeadsheetCell {
public:
/* 명시적으로 삭제한 디폴트 생성자 */
SpreadsheetCell() = delete;
}
■ 8.3.1.5 생성자 이니셜라이저
C++는 생성자에서 데이터 멤버를 초기화하기 위한 생성자 이니셜라이저(ctor initializer, member initializer list)를 제공한다. 앞에서 본 SpreadsheetCell 생성자에 이 방식을 적용해서 다시 작성하면 다음과 같다. 생성자 이니셜라이저는 생성자 인수 리스트와 생성자 본문을 시작하는 첫 중괄호 사이에 나온다. 이 구문은 콜론으로 시작하며 각 항목을 쉼표로 구분한다.
SpreadsheetCell::SpreadsheetCell(double initValue): mValue(initValue) { }
생성자 이니셜라이저로 데이터 멤버를 초기화하는 방식은 생성자 구문 안에서 데이터 멤버를 초기화할 때와는 다르다. C++에서 객체를 생성하려면 생성자를 호출하기 전에 그 객체를 구성하는 모든 데이터 멤버를 생성해야 한다. 생성자 구문 안에서 객체에 값을 대입하는 시점에는 객체가 이미 생성된 상태로 여기서는 단지 값을 변경할 뿐이다. 생성자 이니셜라이저를 이용하면 데이터 멤버를 생성하는 동시에 초깃값을 설정할 수 있는데, 이렇게 하는 것이 나중에 따로 값을 대입하는 것보다 훨씬 효율적이다.
NOTE 생성자 이니셜라이저를 이용하면 객체를 생성하는 시점에 데이터 멤버를 초기화할 수 있다.
어떤 데이터 멤버는 반드시 생성자 이니셜라이저나 클래스 내부 생성자 구문으로 초기화해야 한다. 다음 표에 이러한 타입을 정리했다.
데이터 타입 | 설명 |
const 데이터 멤버 | const 변수는 생성된 후 정상적인 방식으로 값을 대입할 수 없다. 반드시 생성 시점에 값을 지정해야 한다. |
레퍼런스(&) 데이터 멤버 | 레퍼런스는 가리키는 대상없이 존재할 수 없다. |
디폴트 생성자가 없는 객체 데이터 멤버 | C++에서는 객체 멤버를 디폴트 생성자로 초기화한다. |
디폴트 생성자가 없는 베이스 클래스 | 10장에서 자세히 소개한다. |
생성자 이니셜라이저를 사용할 때 한 가지 주의할 점이 있다. 데이터 멤버는 생성자 이니셜라이저에서 나열한 순서가 아닌 클래스 정의에 작성한 순서대로 초기화된다는 것이다.
CAUTION 생성자 이니셜라이저는 클래스 정의에 선언된 순서대로 데이터 멤버를 초기화한다. 생성자 이니셜라이저에 작성한 순서가 아니다.
■ 8.3.1.6 복사 생성자
C++에서는 복사 생성자(copy constructor)라는 특수한 생성자를 제공한다. 다른 객체와 똑같은 객체를 생성할 때 복사 생성자를 사용한다. 복사 생성자는 직접 작성하지 않으면 컴파일러가 디폴트 복사 생성자를 대신 만들어준다. SpreadsheetCell 클래스에 복사 생성자를 추가하면 다음과 같다.
class SpeadsheetCell {
public:
/* 복사 생성자 */
SpreadsheetCell(const SpreadsheetCell& src);
}
SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src): mValue(src.mValue) { } /* const 레퍼런스를 인수로 받습니다. */
복사 생성자는 원본 객체에 대한 const 레퍼런스를 인수로 받는다. 다른 생성자와 마찬가지로 리턴 값은 없다. 생성자 안에서 새로 만들 객체의 데이터 멤버를 모두 기존 객체의 데이터 멤버로 초기화한다.
함수나 메서드에 객체를 레퍼런스로 전달하면 복제 연산으로 인한 오버헤드를 줄일 수 있다. 객체에 있는 내용 전체가 아닌 객체의 주소만 복사하기 때문에 객체를 레퍼런스로 전달하는 방식이 값으로 전달하는 것보다 대체로 효율적이다. 한편, 객체를 레퍼런스로 전달할 때 그 값을 사용하는 함수나 메서드는 원본 객체를 변경할 소지가 있다. 성능이 이유로 레퍼런스 전달 방식을 사용한다면 객체가 변경되지 않도록 객체 앞에 const를 붙여야 한다. 이를 const 레퍼런스 객체를 전달한다고 표현한다.
NOTE 성능을 높이려면 객체를 값이 아닌 const 레퍼런스로 전달하는 것이 가장 좋다.
① 복사 생성자를 명시적으로 호출하기
복사 생성자를 명시적으로 호출할 수도 있다. 주로 다른 객체를 똑같이 복사하는 방식으로 객체를 만들 때 이 방식을 사용한다. 예를 들어 SpreadsheetCell 객체의 복사본을 만들려면 다음과 같이 작성한다.
SpreadsheetCell myCell(4);
SpreadsheetCell anotherCell(myCell); /* 복사 생성자가 호출됩니다. */
② 명시적으로 생성한 복사 생성자와 명시적으로 삭제한 복사 생성자
컴파일러가 생성한 복사 생성자를 명시적으로 생성하거나 삭제할 수 있다.
SpreadsheetCell(const SpreadsheetCell& src) = default;
또는
SpreadsheetCell(const SpreadsheetCell& src) = delete;
■ 8.3.1.7 위임 생성자
위임 생성자를 사용하면 같은 클래스의 다른 생성자를 생성자 안에서 호출할 수 있다. 위임 생성자는 반드시 생성자 이니셜라이저에서 호출해야 하며, 멤버 이니셜라이저 리스트에 이것만 적어야 한다. 예를 들면 다음과 같다.
SpreadsheetCell::SpreadsheetCell(string_view initValue): SpreadsheetCell(stringToDouble(initValue)) { }
여기서 string_view 타입 위임 생성자가 호출되면 이를 타깃 생성자에 위임한다. 타깃 생성자가 리턴하면 생성자의 코드가 실행된다.
■ 8.3.1.8 컴파일러가 생성하는 생성자에 대한 정리
컴파일러는 모든 클래스에 디폴트 생성자와 디폴트 복사 생성자를 자동으로 만들어준다. 그런데 프로그래머가 직접 작성한 생성자에 따라 컴파일러에서 자동으로 만들어주는 생성자가 달라질 수 있다. 여기에 적용되는 규칙은 다음과 같다.
직접 정의한 생성자 | 컴파일러가 만들어주는 생성자 |
없음 | 디폴트 생성자, 복사 생성자 |
디폴트 생성자 | 복사 생성자 |
인수를 받는 생성자 | 복사 생성자 |
복사 생성자 | 없음 |
8.3.2 객체 대입
C++ 코드에서 객체의 값을 다른 객체에 대입할 수 있다. 예를 들어 anotherCell 객체에 myCell 객체의 값을 대입하려면 다음과 같이 작성한다.
SpreadsheetCell myCell(5), anotherCell(10);
anotherCell = myCell; /* 객체를 대입합니다. */
SpreadsheetCell otherCell(myCell); /* 객체를 복사합니다. */
SpreadsheetCell otherCell = myCell; /* 객체를 복사합니다. */
C++에서 복사(copy)는 객체를 초기화할 때 적용되는 표현이고, 이미 값이 할당된 객체를 덮어쓸 때는 대입(assign)이라고 표현한다. C++ 복사 기능은 복사 생성자에서 제공한다. 일종의 생성자이기 때문에 객체를 생성하는데만 사용할 수 있다. 이 때문에 C++는 클래스마다 대입을 수행하는 메서드를 따로 제공하는데, 이 메서드를 복사 대입 연산자(copy assignment operator)라고 한다.
■ 8.3.2.1 복사 대입 연산자 선언 방법
SpreadsheetCell 클래스의 복사 대입 연산자(copy assignment operator)는 다음과 같다. 대입 연산자는 복사 생성자처럼 원본 객체에 대한 const 레퍼런스를 받는다.
class SpreadsheetCell {
public:
SpreadsheetCell& operator=(const SpreadsheetCell& rhs);
}
그런데 복사 대입 연산자는 복사 연산자와 달리 SpreadsheetCell 객체에 대한 레퍼런스를 반환한다. 그 이유는 여러 개의 대입 연산이 연달아 있을 수 있기 때문이다.
■ 8.3.2.2 복사 대입 연산자 정의 방법
대입 연산자를 구현하는 방법은 복사 생성자와 비슷하지만 몇 가지 중요한 차이점이 있다. 첫째, 복사 생성자는 객체를 초기화할 때 단 한 번만 호출된다. 그 시점에는 타깃 객체가 유효한 값을 갖고 있지 않다. 둘째, C++는 객체에 자기 자신을 대입할 수 있다. 따라서 대입 연산자를 구현할 때 자기 자신을 대입하는 경우를 반드시 고려해야 한다. SpreadsheetCell 클래스에 정의한 대입 연산자는 다음과 같다.
SpreadsheetCell& SpreadsheetCell::operator=(const SpreadsheetCell& rhs) {
if (this == &rhs) {
return *this;
}
mValue = rhs.mValue;
return *this;
}
■ 8.3.2.3 명시적으로 생성한 복사 대입 연산자와 명시적으로 삭제한 복사 대입 연산자
컴파일러가 자동으로 생성한 복사 대입 연산자를 다음과 같이 명시적으로 생성하거나 삭제할 수 있다.
SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = default;
또는
SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = delete;
■ 8.3.2.4 컴파일러가 만들어주는 복사 생성자와 복사 대입 연산자
C++11부터 클래스에 사용자가 선언한 복사 대입 연산자나 소멸자가 있으면 복사 생성자를 생성해주는 기능을, 그리고 복사 생성자나 소멸자가 있으면 복사 대입 연산자를 생성해주는 기능을 더 이상 지원하지 않는다. 이 기능을 사용하고 싶다면 다음과 같이 명시적으로 디폴트로 지정한다.
/* 명시적으로 생성한 복사 생성자, 복사 대입 연산자 */
SpreadsheetCell(const SpreadsheetCell& src) = default;
SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = default;
8.3.3 객체 소멸
객체가 제거되는 과정은 두 단계로 구성된다. 먼저 객체의 소멸자를 호출한 다음, 할당받은 메모리를 반환한다. 소멸자를 선언하지 않으면 컴파일러가 만들어주는데, 이를 이용해 멤버를 따라 재귀적으로 소멸자를 호출하면서 객체를 삭제할 수 있다.
스택 객체는 현재 실행하던 함수, 메서드, 그리고 코드 블록이 끝날 때와 같이 유효 범위(scope)를 벗어날 때 자동으로 삭제된다. 스택 객체가 삭제되는 순서는 선언 및 생성 순서와 반대이다. 이러한 순서는 객체로 된 데이터 멤버에 대해서도 똑같이 적용된다.
/* anotherCell 객체가 먼저 삭제되고 myCell 객체가 삭제됩니다. */
SpreadsheetCell myCell(5);
SpreadsheetCell anotherCell(10);
스마트 포인터를 사용하지 않은 힙 객체는 자동으로 삭제되지 않는다. 객체 포인터에 대해 delete를 명시적으로 호출해서 그 객체의 소멸자를 호출하고 메모리를 해제해야 한다. 예를 들면 다음과 같다.
SpreadsheetCell* myCellp = new SpreadsheetCell(5);
delete myCellp;
myCellp = nullptr;
'Object Oriented Programming(C++) > 전문가를 위한 C++' 카테고리의 다른 글
전문가를 위한 C++ | Chapter 10 상속 활용하기(상) (0) | 2022.01.30 |
---|---|
전문가를 위한 C++ | Chapter 07 메모리 관리(하) (0) | 2022.01.29 |
전문가를 위한 C++ | Chapter 07 메모리 관리(상) (0) | 2022.01.20 |
전문가를 위한 C++ | Chapter 09 클래스와 객체 마스터하기(하) (0) | 2022.01.16 |
전문가를 위한 C++ | Chapter 09 클래스와 객체 마스터하기(상) (0) | 2022.01.14 |
댓글