학습 개요
- 클래스에 포함할 수 있는 여러 가지 유형의 생성자에 대하여 학습함
- 기본적인 생성자의 용법을 설명하였는데, 이 밖에도 다양한 상황을 위한 여러 가지 생성자를 활용할 수 있음
- 특정 객체가 아닌 클래스에 속한 객체 전체에 대한 처리나 데이터를 정의하는
static
멤버에 대하여 학습함
학습 목표
- 디폴트 생성자를 정의하여 활용할 수 있음
- 복사 생성자 및 이동 생성자를 용도에 맞게 정의하여 활용할 수 있음
static
데이터 멤버 및static
멤버 함수를 활용할 수 있음
주요 용어
- 디폴트 생성자(default constructor)
- 매개 변수가 없거나 모든 매개 변수에 디폴트 인수가 지정된 생성자
- 복사 생성자(copy constructor)
- 동일 클래스의 객체를 복사하여 객체를 만드는 생성자
- 얕은 복사(shallow copy)
- 동객체를 복사할 때 객체의 데이터 멤버의 값을 그대로 복사하는 방식
- 깊은 복사(deep copy)
- 복사 된 객체가 소스 객체와 공유하는 자원이 없는 별개의 복사본이 될 수 있도록 복사하는 방식
- glvalue
- 객체 등의 아이덴티티를 결정하는 식
- prvalue
- 객체 등을 초기화하거나 연산자의 피연산자 값을 계산하는 식으로, 아이덴티티는 없음
- xvalue
- glvalue 중 보유하고 있는 자원을 재활용할 수 있는(일반적으로 생명 주기의 마지막에 도달하여 보유할 필요가 없게 되는 경우에 해당됨) 객체 등에 해당되는 식
- lvalue
- glvalue 중 xvalue를 제외한 것으로, 그 아이덴티티가 유지되어야 하는 식
- rvalue
- prvalue 또는 xvalue에 해당되는 식
- 이동 생성자(move constructor)
- rvalue 참조로 전달된 같은 클래스의 객체의 내용을 이동하여 객체를 만드는 생성자
강의록
디폴트 생성자
디폴트 생성자(default constructor)
- 매개 변수가 없는 생성자, 또는 모든 매개 변수에 디폴트 인수가 지정된 생성자
- 클래스를 선언할 때 생성자를 선언하지 않으면 컴파일러는 묵시적으로 디폴트 생성자를 정의함
- 묵시적 디폴트 생성자는 아무런 처리도 포함하지 않음
- 생성자를 하나라도 선언하면 컴파일러는 묵시적 디폴트 생성자를 정의하지 않음
디폴트 생성자의 활용
- 묵시적 디폴트 생성자
Counter.h 클래스 정의
1 2 3 4 5 6 7 8
class Counter { int value; public: // Counter() {} // 생성자를 선언하지 않으면 컴파일러가 디폴트 생성자가 하나 있다고 생각함 void reset() { value = 0; } void count() { ++value; } int getValue() const { return value; } };
main()
함수1 2 3
int main() { Counter cnt; // 초기화가 되지 않은 상태로 value 데이터 멤버 가지고 있는 객체 생성 }
생성자를 한 개라도 명시적으로 선언하면 묵시적 디폴트 생성자는 포함되지 않음
- 디폴트 생성자가 없는 클래스
CounterM.h 클래스 정의
1 2 3 4 5 6 7 8 9 10 11 12
class CounterM { const int maxValue; int value; public: CounterM(int mVal) : maxValue(mVal), value{0} {} // mVal 인수 이용해 초기화 void reset() { value = 0; } void count() { value = value < maxValue ? value + 1 : 0; } };
main()
함수1 2 3 4
int main() { CounterM cnt1(999); // 생성자에 해당하는 형태로 객체 생성해야 함 CounterM cnt2; // 에러 }
- 객체 배열의 선언
Counter.h 클래스 정의
1 2 3 4 5 6 7 8
class Counter { int Value; public: // Counter() { } // 묵시적 디폴트 생성자 void reset() { value = 0; } void count() { ++value; } int getValue() const { return value; } };
main()
함수1 2 3 4
int main() { Counter cntArr[4]; // OK Counter *pt = new Counter[10]; // OK -> 카운터 객체를 동적으로 10개 생성 }
CounterM.h 클래스 정의
1 2 3 4 5 6 7 8 9
class CounterM { const int maxValue; int value; public: // CounterM의 디폴트 생성자 없음 CounterM(int mVal) : maxValue(mVal), value{0} {} void reset() { value = 0; } };
main()
함수1 2 3 4 5
int main() { CounterM cntMArr1[3]; // 에러 CounterM cntMArr2[3] = {CounterM(99), CounterM(99), CounterM(999)}; // OK -> 인수 전달해 초기 값 제공 CounterM *pt = new CounterM[10]; // 에러 }
복사 생성자
복사 생성자의 개념
- 복사 생성자(copy constructor)
- 동일 클래스의 객체를 복사하여 객체를 만드는 생성자
- 반드시 자기 자신 타입의 참조를 매개 변수로 받아야 함
- 일반 생성자는 그 외의 인자를 받아서 객체를 생성함
- 묵시적 복사 생성자
- 객체의 데이터 멤버들을 그대로 복사하여 객체를 만들도록 묵시적으로 정의된 복사 생성자
- 동일 클래스의 객체를 복사하여 객체를 만드는 생성자
명시적 복사 생성자를 정의하는 형식
1 2 3 4 5 6 7
class ClassName { public: ClassName(const ClassName & obj){ // 생성되는 객체에 obj를 복사하는 처리 } };
- 묵시적 복사 생성자
CounterM.h 클래스 정의
1 2 3 4 5 6 7 8 9 10 11 12
class CounterM { const int maxValue; int value; public: CounterM(int mVal) : maxValue(mVal), value{0} {} // CounterM(const CounterM &c) // 묵시적인 복사 생성자 있다고 가정 // : maxValue(c.maxValue), // value(c.value) {} void reset() { value = 0; } };
main()
함수1 2 3 4 5
int main() { CounterM cnt4(99); // 생성자 이용해 객체 생성 CounterM cnt5(cnt4); CounterM cnt6 = cnt4; }
CounterM.h 클래스 정의 (명시적 복사 생성자)
1 2 3 4 5 6 7 8 9 10
class CounterM { const int maxValue; int value; public: CounterM(int mVal) : maxValue(mVal), value{0} {} CounterM(const CounterM &c) // 복사 생성자 : maxValue(c.maxValue), value(c.value) {} void reset() { value = 0; } };
main()
함수1 2 3 4 5
int main() { CounterM cnt4(99); CounterM cnt5(cnt4); CounterM cnt6 = cnt4; }
얕은 복사의 문제점 - VecF 클래스
- VecF 클래스
- 벡터 객체를 만들 수 있는 VecF 클래스를 정의하고자 한다.
- VecF 객체는 저장할 수 있는 float 값의 수를 인수로 지정하여 생성되며, 저장할 값의 배열이 제공될 경우 그 값으로 초기화한다.
- 인수로 전달된 VecF 객체와 덧셈한 결과를 반환할 수 있으며, 객체에 저장된 벡터를 출력할 수 있다.
행위
멤버 함수 비고 VecF(int d, const float* a)
생성자 ~VecF()
소멸자 VecF add(const VecF& fv)
fv
와 덧셈한 결과를 반환함void print()
벡터를 출력함 속성
데이터 멤버 비고 int n
벡터의 크기를 저장함 float* arr
벡터의 저장 공간 포인터 VecF.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
#include <iostream> #include <cstring> using namespace std; class VecF { int n; // private 멤버 float* arr; // private 멤버 public: VecF(int d, const float* a=nullptr) : n{d} { arr = new float[d]; if (a) memcpy(arr, a, sizeof(float) * n); // 배열이 전달 되었다면 메모리에 들어있는 내용을 다른 메모리 영역으로 복사함 } ~VecF() { // 소멸자 delete[] arr; // 할당 받은 메모리 시스템에 반납 } VecF add(const VecF& fv) const { VecF tmp(n); // 벡터의 덧셈 결과를 저장할 임시 객체 for (int i = 0; i < n; i++) tmp.arr[i] = arr[i] + fv.arr[i]; return tmp; // 덧셈 결과를 반환함 } void print() const { cout << "[ "; for (int i = 0; i < n; i++) cout << arr[i] << " "; cout << "]"; } };
VFMain1.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <iostream> using namespace std; #include "VecF.h" int main() { float a[3] = { 1, 2, 3 }; VecF v1(3, a); // 1, 2, 3을 저장하는 벡터 VecF v2(v1); // v1을 복사하여 v2를 만듦 -> 복사 생성자가 없기 때문에 묵시적인 복사 생성자가 생김 v1.print(); cout << endl; v2.print(); cout << endl; return 0; } // [ 1 2 3 ] // [ 1 2 3 ]
- 데이터 멤버는 별개지만 데이터 멤버 안에 있는 포인터가 가리키고 있는 메모리가 같은 자원을 가리킴
- 동일한 메모리를 가리킴 = shallow copy
main
함수가 끝나게 되면서 v1, v2, a 배열 사라지게 됨- v1, v2 소멸자 동작
- v1 객체의
delete [] arr;
실행- arr에 해당하는 메모리를 시스템에 반납
- v2 객체의
delete [] arr;
실행- 이미 반납 된 arr의 메모리를 반납하려고 해서 오류 발생
- 데이터 멤버는 별개지만 데이터 멤버 안에 있는 포인터가 가리키고 있는 메모리가 같은 자원을 가리킴
복사 생성자의 활용 - VecF.h 수정본
VecF.h 수정본
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class VecF { int n; float *arr; public: VecF(int d, const float* a=nullptr) : n{d} { arr = new float[d]; if (a) memcpy(arr, a, sizeof(float) * n); } VecF(const VecF& fv) : n(fv.n) { // 명시적인 복사 생성자 추가 arr = new float[n]; memcpy(arr, fv.arr, sizeof(float)*n); // 데이터를 저장할 수 있는 메모리 별도 할당 } };
VFMain1.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <iostream> using namespace std; #include "VecF.h" int main() { float a[3] = { 1, 2, 3 }; VecF v1(3, a); // 1, 2, 3을 저장하는 벡터 VecF v2(v1); // v1을 복사하여 v2를 만듦 -> 별도로 메모리 만들어서 데이터 복사 v1.print(); cout << endl; v2.print(); cout << endl; return 0; } // [ 1 2 3 ] // [ 1 2 3 ]
- deep copy
- 포인터가 있어 포인터에 동적으로 할당 받는 메모리가 있는 경우에 포인터 부분까지 그대로 저장 공간을 새로 할당 받아 그대로 복사
- 내용은 같지만 완전히 분리 된 새로운 객체가 만들어짐
- 포인터가 있어 포인터에 동적으로 할당 받는 메모리가 있는 경우에 포인터 부분까지 그대로 저장 공간을 새로 할당 받아 그대로 복사
main
함수가 끝나게 되면서 v1, v2, a 배열 사라지게 됨- v1, v2 소멸자 동작
- v1 객체의
delete [] arr;
실행- arr에 해당하는 메모리를 시스템에 반납
- v2 객체의
delete [] arr;
실행- 분리 된 객체에 저장 되기 때문에 소멸자 동작 시 오류 발생하지 않음
- deep copy
이동 생성자
불필요한 복사의 비효율성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class VecF {
VecF(int d, const float* a = nullptr) { ...... }
VecF(const VecF& fv) : n{fv.n} {
arr = new float[n];
memcpy(arr, fv.arr, sizeof(float)*n);
}
~VecF() { ...... }
VecF add(const VecF& fv) const {
VecF tmp(n); // 벡터의 덧셈 결과를 저장할 임시 객체
for (int i = 0; i < n; i++)
tmp.arr[i] = arr[i] + fv.arr[i];
return tmp; // 덧셈 결과를 반환함
}
};
1
2
3
4
5
6
7
int main() {
float a[3] = { 1, 2, 3 };
float b[3] = { 2, 4, 6 };
VecF v1(3, a);
VecF v2(3, b);
VecF v3(v1.add(v2)); // 반환 객체는 v3에 복사된 후 제거됨
}
- rvalue 참조를 이용한 이동 생성자로 효율 개선 가능
rvalue 참조
- 전통적 의미의 lvalue와 rvalue
a = b + 10;
a
- lvalue
- 값을 저장할 수 있는 실체가 있는 요소
b + 10
- rvalue
- 전달할 값이 만들어지기만 하면 됨
C++11 이후의 식의 분류
- rvalue 참조의 선언
- & 기호로 선언하는 lvalue 참조와 달리 rvalue 참조는 && 기호로 선언함
lvalue 참조와 rvalue 참조의 사용 예
1 2 3 4 5 6 7
VecF v1(3), v2(3); VecF& vLRef = v1; // lvalue 참조로 lvalue를 참조함 int& a = 10; // 오류: lvalue 참조로 rvalue를 참조할 수 없음 const int& b = 20; // 상수 lvalue 참조로는 rvalue를 참조할 수 있음 int&& c = 30; // rvalue는 rvalue 참조로 참조할 수 있음 VecF&& vRRef1 = v1.add(v2); // 함수의 반환 객체는 rvalue임 VecF&& vRRef2 = v2; // 오류: rvalue 참조로 lvalue를 참조할 수 없음
이동 생성자의 개념
- 이동 생성자(move constructor)
- rvalue 참조로 전달된 같은 클래스의 객체의 내용을 이동하여 객체를 만드는 생성자
이동 생성자의 선언 형식
1 2 3 4 5 6
class ClassName { public: ClassName(ClassName&& obj){ // r value 참조한 이동 생성자 // 생성되는 객체에 obj의 내용을 이동하는 처리 } };
이동 생성자의 활용 - VecF.h 수정본2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class VecF { int n; float *arr; public: // 복사 생성자 VecF(const VecF& fv) : n(fv.n) { arr = new float[n]; memcpy(arr, fv.arr, sizeof(float)*n); } // 이동 생성자 VecF(VecF&& fv) : n{fv.n}, arr(fv.arr) { // r value 참조 fv.arr = nullptr; // 자기 자신이 가지고 있던 메모리를 생성 된 객체에다가 옮겨준 다음에 객체에 해당되는 내용을 nullptr로 해 자원 이동 시킴 fv.n = 0; } ~VecF() { delete[] arr; // 가지고 있는 포인터가 nullptr일 때는 아무 일도 하지않음 } };
main()
함수1 2 3 4 5 6 7
int main() { float a[3] = { 1, 2, 3 }; float b[3] = { 2, 4, 6 }; VecF v1(3, a); VecF v2(3, b); VecF v3(v1.add(v2)); }
static
데이터 멤버와 static
멤버 함수
객체의 메모리 공간
객체를 저장하기 위한 메모리 공간
- 데이터 멤버들을 저장할 수 있는 메모리 공간이 각각의 객체마다 존재 해야함
static
데이터 멤버- 클래스에 속하는 모든 객체들이 공유하는 데이터 멤버
- 객체 생성과 관계 없이 프로그램이 시작되면
static
데이터 멤버를 위한 메모리 공간이 할당됨 - 일반 데이터 멤버와는 달리,
static
데이터 멤버는 클래스 선언문 내에서는 선언만 하고 클래스 외부에서 별도로 정의해야 함
static
멤버 함수- 특정 객체에 대한 처리를 하는 것이 아니라, 소속 클래스 단위의 작업을 수행하는 함수
static
멤버 함수는 객체가 정의되지 않아도 사용할 수 있음static
멤버 함수 안에서는 일반 멤버를 사용할 수 없으며,static
멤버만 사용할 수 있음
예제 : NamedObj 클래스
- 이름을 갖는 객체를 만들 수 있는 VecF 클래스를 정의하고자 한다.
- 객체가 생성될 때 고유 번호를 가지게 되는데, 이 번호는 NamedObj 객체가 생성됨에 따라 1번부터 시작하여 차례로 부여되는 일련번호이다.
- 객체는 자기 자신의 일련번호와 이름을 출력할 수 있으며, 현재 존재하는 NamedObj 클래스의 객체 수를 구할 수 있다.
행위
멤버함수 비고 NamedObj(const char* s)
생성자 - 이름을 s
로 초기화함~NamedObj()
소멸자 void display()
ID와 이름을 출력함 static int nObj()
현재 존재하는 객체의 수를 구함 속성
데이터 멤버 비고 char* name
이름을 저장함 int id
ID 번호를 저장함 static int nConstr
생성된 객체의 수 static int nDestr
소멸된 객체의 수
NamedObj 클래스
NamedObj.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
class NamedObj { char* name; int id; // static 데이터 멤버 - 클래스 전체에 하나씩만 만들어짐 static int nConste; // 생성된 객체 수 static int nDestr; // 소멸된 객체 수 public: NamedObj(const char* s); // 생성자 ~NamedObj(); // 소멸자 void display() const { // 객체의 속성 출력 cout << "ID : " << id << " 이름 : " << name << endl; } static int nObj() { // static 멤버함수: 존재하는 객체 수 반환 return nConste - nDestr; // 객체가 없는 상태에서도 동작 가능해야 함 (static 멤버만 사용해야 함) } };
NamedObj.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <cstring> #include "NamedObj.h" NamedObj::NamedObj(const char* s) { name = new char[strlen(s)+1]; // 문자열을 복사할 공간을 할당 strcpy(name, s); id = ++nConste; // 생성된 객체의 수를 증가시키고 이를 ID로 부여함 } NamedObj::~NamedObj() { ++nDestr; // 소멸된 객체의 수를 증가시킴 delete [] name; } // static 데이터 멤버의 정의 및 초기화 int NamedObj::nConste = 0; // static 데이터 멤버는 클래스 선언문 안에서 선언만 해주고 별도의 cpp 파일에서 정의를 해줘야 함 int NamedObj::nDestr = 0;
StaticDM.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
void f() { NamedObj x("Third"); // 세 번째 객체의 생성 x.display(); // 함수 반환 후 x는 소멸됨 } int main() { // 객체 생성 전이지만 static 멤버 함수이기 때문에 클래스 이름 이용해 호출 가능 cout << "NamedObj 클래스의 객체 수 : " << NamedObj::nObj() << endl; NamedObj a("First"); // 첫 번째 객체 생성 NamedObj b("Second"); // 두 번째 객체 생성 f(); NamedObj c("Fourth"); // 네 번째 객체 생성 c.display(); cout << "NamedObj 클래스의 객체 수 : " << NamedObj::nObj() << endl; return 0; }
연습 문제
디폴트 생성자에 대한 올바른 설명은?
a. 매개 변수가 없거나, 모든 매개 변수에 디폴트 인수가 지정된 생성자이다.
- 디폴트 생성자는 매개변수가 없거나, 모든 매개변수에 디폴트 인수가 지정된 생성자임
- 만일 클래스 선언문에 생성자가 없다면 컴파일러가 아무런 처리도 하지 않는 디폴트 생성자를 만듬
- 생성자를 하나라도 선언하였다면 컴파일러는 디폴트 생성자를 자동적으로 선언하지 않음
위 지문에서 클래스 Copycat의 ㈀에 넣을 복사 생성자의 머리부 내용은?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class Copycat { char* name; public: Copycat(const char* n) { name = new char[strlen(n)+1]; strcpy(name, n); } // (ㄱ) { name = new char[strlen(cc.name)+1]; strcpy(name, cc.name); } };
a.
Copycat(const Copycat& cc)
- 복사 생성자는 참조 호출을 해야 함
- 이렇게 전달 된 원본 객체인 실 매개 변수를 보호할 수 있도록
const
인수로 선언하는 것이 일반적임 Copycat(Copycat cc)
,Copycat(const Copycat cc)
와 같이 값 호출을 하면 객체를 복사해야 함- 복사 생성자는 복사를 담당하는 생성자이므로 복사가 필요한 값 호출을 할 수 없음
ClassA라는 클래스의 이동 생성자를 선언하기 위한 머리 부를 올바르게 작성한 것은? a.
ClassA(ClassA&& obj)
- 이동 생성자는 rvalue 참조를 받을 수 있도록 매개 변수를 선언해야 하며, 이동 후 매개변수로 전달된 객체의 내용이 이동 됨으로써 객체의 내용이 바뀌어야 하므로 복사 생성자처럼
const
매개 변수로 전달하는 것은 의미가 맞지 않음
- 이동 생성자는 rvalue 참조를 받을 수 있도록 매개 변수를 선언해야 하며, 이동 후 매개변수로 전달된 객체의 내용이 이동 됨으로써 객체의 내용이 바뀌어야 하므로 복사 생성자처럼
클래스의
static
멤버에 대한 올바른 설명은?a. 객체가 하나도 생성되지 않은 상태라도 클래스의 static 멤버 함수를 호출할 수 있다.
static
멤버는 특정 객체가 아닌 해당 클래스 전체를 대상으로 하는 데이터나 처리를 위해 선언 됨static
데이터 멤버는 특정 객체에 속하는 데이터가 아닌 클래스 전체에 대한 데이터이며, 객체 생성 여부와 관계없이 클래스에 대해 하나만 만들어짐static
멤버 함수는 클래스 전체를 대상으로 한 처리를 정의하며, 객체 생성 여부와 관계없이 호출될 수 있음- 따라서
static
멤버 함수 내에서 일반 멤버를 액세스할 수 없고 반드시static
멤버만 사용할 수 있음
정리 하기
- 디폴트 생성자는 인수를 지정하지 않고 객체를 생성할 수 있게 하며, 생성자를 명시적으로 선언하지 않으면 아무런 처리도 하지 않는 디폴트 생성자가 묵시적으로 정의 됨
- 복사 생성자는 기존의 동일 클래스 객체를 복사하여 새로운 객체를 만들 수 있게 함
- rvalue 참조는 이동을 할 수 있는 대상을 참조하는 용도로 사용함
- 이동 생성자는 매개 변수로 전달 된, 앞으로 더 이상 필요하지 않을 객체의 내용을 이동하여 새로운 객체를 생성하기 위해 사용함
- 클래스에 속하는 모든 객체가 공유하는 데이터 멤버는
static
멤버로 선언하며, 객체 생성과 무관하게 프로그램을 시작할 때 생성 됨 - 클래스의
static
멤버 함수는 특정 객체가 아닌 소속 클래스 단위의 작업을 수행하는 함수이며,static
멤버 함수 안에서는static
멤버만 사용할 수 있음