복사 생성자(Copy Constructor)
-
동일한 클래스 타입의 객체를 매개변수로 받아 현재 객체에 덮어씌우는 생성자.
-
매개변수 타입은 const 레퍼런스 변수로, 객체 복사로 인한 오버로드를 피하고 안전성을 높일 수 있음.
-
실제로 만들지 않아도 Person p1; Person p2(p1); 처럼 사용할 수 있는데, 디폴트 생성자처럼 명시적으로 복사 생성자를 만들지 않으면 컴파일러가 자동으로 만들어 줌.
-
복사 생성자를 구현하는 방법은 멤버 변수들을 전부 복사하는 것으로, 멤버 이니셜라이저를 이용하는 것과 생성자 본문을 이용하는 것이 있음.
-
Person(const Person& src) : height(src.height), weight(src.weight) { }
-
Person(const Person& src) { height = src.height; weight = src.weight; }
-
-
대부분은 복사 생성자를 직접 만들 필요는 없음.
#include <iostream>
class Person {
private:
double height;
double weight;
public:
Person(const Person& src);
};
Person(const Person& src)
: height(src.height), weight(src.weight) { }
Person(const Person& src) {
height = src.height;
weight = src.weight;
}
int main() {
Person p1(183.4, 78.5);
Person p2(p1);
return 0;
}
복사 대입 연산자(Copy Assignment Operator)
-
동일한 클래스 타입의 객체를 현재 객체에 대입하는 연산자.
-
복사 대입 연산자는 = 연산자를 각 클래스에서 오버로딩해서 구현함.
-
Person& operator=(const Person& rhs);
-
-
복사 대입 연산자도 명시적으로 선언하지 않으면 객체 간 대입이 가능하도록 컴파일러가 자동으로 만들어 줌.
-
복사 생성자와 달리 복사 대입 연산자는 Person의 참조 객체를 반환하는데, 그 이유는 대입 연산이 중첩되어 수행될 수 있기 때문임.
-
예를 들어, obj1 = obj2 = obj3; 라는 코드가 있을 때, 먼저 obj2의 대입 연산자가 obj3를 우변 항목 인자로 호출함.
-
그 다음, obj1의 대입 연산자가 호출되는데 이 때 우변 항목 인자는 obj2가 아니며, obj1의 대입 연산자는 obj2의 대입 연산자가 obj3를 인자로 해 실행된 반환값을 우변 항목 인자로 취함.
-
만약 대입 연산이 실패해서 반환값이 없다면, obj1으로 전달할 인자가 없어지므로 오류가 발생함.
-
obj1의 대입 연산자가 그냥 obj2를 인자로 취할 경우, =기호가 멤버 함수 호출을 래핑(Wrapping)만 하기 때문에 obj1 = obj2 = obj3; 는 사실상 obj1.operator=(obj2.operator=(obj3)); 가 실행되는 것과 같으므로 결국 obj2.operator=의 올바른 반환값은 obj2 그 자체가 되어야 함.
-
결국 객체 반환에 따른 임시 객체로의 복제 오버로드를 피하려면 참조형으로 반환되는 것이 바람직함.
-
-
복사 대입 연산자의 구현 방법은 복사 생성자의 구현 방법과 비슷하나, 다음과 같은 중요한 차이점이 존재함.
-
복사 생성자는 객체 초기화 시점에만 호출되기 때문에 대상 객체들의 멤버들이 아직 유효하지 않음.
-
복사 대입 연산자는 이미 생성된 객체를 대상으로 하기 때문에 멤버들의 메모리 할당 완료 여부에 신경쓰지 않고도 값을 덮어 쓸 수 있음.
-
C++에서는 객체가 자기 스스로 대입하는 것이 문법적으로 가능하나, 문제가 발생할 수 있음을 유의해야 함.
-
복사 대입 연산자가 실행되면 가장 먼저 인자가 자기 자신인지 검사하고, 그렇다면 복제 작업을 하지 않고 그대로 반환하게 만드는 형태가 가장 적절함.
-
#include <iostream>
using namespace std;
class Person {
private:
double height;
double weight;
public:
Person& operator=(const Person& rhs);
void print() {
cout << "H = " << height;
cout << ", W = " << weight << endl;
}
};
Person& operator=(const Person& rhs)
{
if (this == &rhs) return *this;
height = rhs.height;
weight = rhs.weight;
return *this;
}
int main() {
Person p1(183.4, 78.5), p2(175.6, 68.3);
p1.print(); // H = 183.4, W = 78.5
p1 = p2;
p1.print(); // H = 175.6, W = 68.3
return 0;
}
얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)
-
복사 생성자, 복사 대입 연산자의 경우 컴파일러가 자동으로 만들어 주기 때문에 대부분 직접 구현할 필요가 없음.
-
컴파일러가 자동으로 생성한 멤버 함수들은 멤버 변수들에 대해 재귀적으로 복사 생성자 또는 복사 대입 연산자를 호출함.
-
단, int나 double, 포인터와 같이 기본 데이터 타입에 대해서는 복사 생성자나 복사 대입 연산자 대신 얕은 복사가 일어남.
-
얕은 복사(shallow copy): 포인터가 가리키는 데이터는 빼놓고 주소 값만 복사하는 방식
-
깊은 복사(deep copy): 포인터만 복사하지 않고 변수의 맥락에 맞게 연관된 데이터까지 재귀적으로 복사하는 방식
-
그러나, 얕은 복사는 객체가 동적으로 할당받은 메모리를 가지고 있을 경우 문제가 됨.
-
예를 들어, Person 클래스에서 사람이 가지고 있는 방의 개수(int numRooms;)를 받아 각 방의 면적을 나타내는 배열(int *roomWidth;)을 동적으로 생성한다고 가정했을 때, p1 = p2; 이후 소멸자가 호출될 때 p2의 소멸자가 먼저 호출되기 때문에 p1의 소멸자가 호출되어 roomWidth 을 delete할 때 이미 해제된 메모리를 다시 해제하므로 오류가 발생함.
-
또한, p1이 참조하던 원래 메모리는 가리키는 포인터가 없어짐. 이를 댕글링 포인터(dangling pointer)가 되며, 이는 심각한 메모리 누수 문제로 이어질 수 있음.
-
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
int numRooms;
int* roomWidth;
string name;
public:
Person(int numRooms, string name);
~Person();
int getNumRooms() const;
int* getRoomWidth() const;
void setName(double _name);
string getName() const;
};
Person::Person(int numRooms, string name) {
this->numRooms = numRooms;
this->name = name;
this->roomWidth = new int[numRooms];
for (int i = 0; i < this->numRooms; ++i) {
this->roomWidth[i] = (i + 1) * 10;
}
}
Person::~Person() {
delete[] roomWidth;
cout << "Destroyed!!" << endl;
roomWidth = nullptr;
}
int Person::getNumRooms() const { return numRooms; }
int* Person::getRoomWidth() const { return roomWidth; }
void Person::setName(double _name) { name = _name; }
string Person::getName() const { return name; }
void printRoom(const Person& p) {
string name = p.getName();
cout << p.getName() << "'s room width:" << endl;
const int n = p.getNumRooms();
const int* roomWidth = p.getRoomWidth();
for (int i = 0; i < n; ++i) {
cout << "[Room" << i + 1 << "] width = " << roomWidth[i] << endl;
}
}
int main() {
Person p1(3, "Person 1");
Person p2(5, "Person 2");
printRoom(p1);
printRoom(p2);
p1 = p2;
printRoom(p1);
return 0;
}
[결과]
./test
Person 1's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Destroyed!! <---- p2 의 것만 해제되고, 이후 p1의 소멸자에서 오류 발생.
test(3798,0x110f29dc0) malloc: *** error for object 0x7fecd3405810: pointer being freed was not allocated
test(3798,0x110f29dc0) malloc: *** set a breakpoint in malloc_error_break to debug
[1] 3798 abort ./test
-
위와 같은 문제를 피하려면 대입 대상인 좌변항의 객체가 참조하고 있는 메모리를 반환한 후에 새로 메모리를 준비해서 복사하는 깊은 복사(deep copy)를 해야 함.
// Copy Constructor
Person::Person(const Person& src)
: numRooms(src.numRooms), name(src.name)
{
this->roomWidth = new int[numRooms];
for (int i = 0; i < this->numRooms; ++i) {
this->roomWidth[i] = src.roomWidth[i];
}
}
// Copy Assignment Operator
Person& Person::operator=(const Person& rhs)
{
if (this == &rhs) return *this;
delete[] this->roomWidth;
this->roomWidth = nullptr;
this->numRooms = rhs.numRooms;
this->name = rhs.name;
this->roomWidth = new int[this->numRooms];
for (int i = 0; i < this->numRooms; ++i) {
this->roomWidth[i] = rhs.roomWidth[i];
}
return *this;
}
[결과]
Person 1's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Destroyed!!
Destroyed!! <---- 정상적으로 p1의 메모리를 해제하였음.
0의 법칙, 그리고 3의 법칙
-
Rule of Zero: 소멸자, 복사 생성자, 복사 할당 연산자 모두 명시적으로 만들지 않으면 컴파일러가 모두 자동으로 만들어준다는 법칙
-
Rule of Three: 소멸자, 복사 생성자, 복사 할당 연산자 중 하나를 명시적으로 만들면, 나머지 모두 명시적으로 만들어야한다는 법칙
멤버 변수 (Member Variable)
-
static
-
C언어에서 전역 변수와 유사하지만 클래스에 종속된다는 점이 다름.
-
일반적으로 객체별로 변수를 가지기보다 모든 객체가 함께 사용하는 하나의 변수가 필요할 때 사용함.
-
예를 들어, 하나의 변수가 바뀔 때마다 모든 객체의 멤버 변수를 동기화하는 작업이 비효율적일 때 사용됨.
-
"클래스명::변수명"으로 접근할 수 있음. 범위 지정 연산자 사용에 유의할 것.
-
-
const
-
생성 시점에 초기값을 부여한 뒤, 더 이상 수정할 수 없는 변수.
-
클래스가 아닌 객체에 종속됨.
-
객체 수준에서 상수값을 보유하는 것은 대부분 메모리 낭비이지만, static const 멤버 변수를 이용해 객체 간에 상수값을 공유할 수 있음.
-
예를 들어, GUI 프로그램의 초기 창 크기(가로, 세로 길이)는 static const 로 선언되는 것이 편리함.
-
-
reference (&)
-
메모리를 참조할 때 사용하는 멤버 변수로, 생성과 동시에 다른 객체를 참조하도록 초기화 되어야 함.
-
특정 클래스에서 다른 클래스를 참조할 때 포인터 또는 레퍼런스형을 사용할 수 있는데, 포인터보다는 레퍼런스를 사용하는 것이 바람직함.
-
포인터와 달리 레퍼런스 타입은 적합한 객체로 초기화되어야만 존재할 수 있기 때문에 훨씬 안전함.
-
-
const reference
-
레퍼런스형 멤버 변수는 const 객체를 참조할 수 있음.
-
const 객체를 참조하는 경우 const 객체의 const 멤버 함수만 호출 가능.
-
#include <iostream>
using namespace std;
class Vault {
private:
int money;
public:
Vault(int _memory) : money(_memory) { }
}
class Bank {
private:
static double interestRate;
const Vault& vault;
int width, height;
public:
static const int maxWidth = 300;
static const int maxHeight = 300;
Bank(const Vault& _vault, int _width, int _height);
Bank(const Bank& src);
};
// Initialize static member variable.
double Bank::interestRate = 3.5;
// Constructor with Member Initializer
Bank::Bank(const Vault& _vault, int _width, int _height)
: vault(_vault), width(_width), height(_height)
{ }
// Copy constructor
Bank::Bank(const Bank& src)
: vault(src.vault), width(src.width), height(src.height)
{ }
int main() {
Vault hanaVault(200'000'000);
Bank hanaBank(hanaVault, 50, 50);
cout << "Bank::maxWidth = " << Bank::maxWidth << endl;
cout << "Bank::maxHeight = " << Bank::maxHeight << endl;
cout << "hanaBank.vault = " << hanaBank.vault << endl;
// Error
// hanaBank.vault = 0;
return 0;
}
멤버 함수 (Member Function)
-
멤버 함수는 다른 말로 메서드(Method)라고도 함.
-
static
-
객체에 종속되는 부분이 없다면 static 멤버 함수로 선언 가능.
-
예를 들어, 소수값을 버리는 함수는 객체 종속되지 않으므로 static 멤버 함수로 선언하는 것이 바람직함.
-
멤버 함수에서는 static 과 const 를 함께 사용할 수 없음.
-
선언 시 static 을 맨 앞에 추가하지만, 구현 부분에서는 추가할 필요 없음.
-
static 멤버 함수는 연결된 객체가 없기 때문에 코드 구현부에서 this 포인터를 이용 불가.
-
static 멤버 변수만 이용 가능.
-
클래스 내부의 멤버 함수들은 static 멤버 함수를 보통 멤버 함수처럼 호출 가능.
-
클래스 외부에서는 static 멤버 함수 호출 시 "클래스명::함수명()"처럼 범위 지정 연산자(::)를 이용 해야 함.
-
-
const
-
멤버 함수가 객체의 멤버 변수 값을 바꾸지 않는다고 보장하기 위해 사용함.
-
const 멤버 함수 안에서 객체의 멤버 변수를 변경하면 컴파일 오류 발생.
-
static 멤버 함수에는 const 제한자 적용 불가. 클래스 공통으로서 연계되는 객체가 없으므로 const 선언이 무의미하기 때문.
-
const가 아닌 객체에 대해서는 모든 메서드(const 포함) 호출 가능.
-
const 객체에 대해서는 const 메서드만 가능.
-
가능하면 객체를 변경하지 않는 모든 멤버 함수에 const 적용을 권장함. 이유는 const 객체에서도 호출할 수 있도록 하기 위함.
-
const 제한자는 함수 선언에 포함되기 때문에 구현부에서도 똑같이 적용해야 함.
-
"반환형 클래스명::함수명() const { ... }"
-
-
default parameter
-
디폴트 매개변수는 멤버 함수의 프로토타입을 선언할 때 각 매개변수에 디폴트 값을 지정하는 것을 의미.
-
만약 사용자가 해당 인자를 직접 제공하면 디폴트 값은 무시됨.
-
만약 해당 인자를 공란으로 해서 호출하면 디폴트 값이 자동으로 적용됨.
-
디폴트 매개변수는 가장 마지막(오른쪽) 매개변수부터 시작해 파라미터 건너뛰지 않고 연속적으로만 적용 가능. 예를 들어, void func(int param1, int defaultParam1 = 200, int param2); 은 불가능. 반대로 void func2(int param1, int param2, int defaultParam1 = 200, int defaultParam2 = 400); 은 가능
-
그렇게 하지 않으면 멤버 함수가 호출될 때 어느 매개변수에 디폴트 값을 적용할지 컴파일러가 판단할 수 없음.
-
매개변수에 디폴트 값은 지정해도 구현부는 바뀌지 않음. 즉, 함수 프로토타입 선언할 때만 지정 가능하고 구현부 정의에서는 지정할 수 없음.
-
생성자가 디폴트 매개변수를 가진다면, 하나의 생성자로 인자가 여러 개인 경우 모두 이용 가능.
-
디폴트 매개변수로 할 수 있는 일은 멤버 함수 오버로딩으로도 할 수 있음. (선택적)
-
#include <iostream>
using namespace std;
class Vault {
private:
int money;
public:
Vault(int _memory) : money(_memory) { }
}
class Bank {
private:
static double interestRate;
const Vault& vault;
int width, height;
// new member variable
string branchName;
public:
static const int maxWidth = 300;
static const int maxHeight = 300;
Bank(const Vault& _vault, int _width, int _height);
Bank(const Bank& src);
// new member function
static int roundDown(double val);
// getter & setter
string getBranchName() const;
void setBranchName(string _bName);
};
// Initialize static member variable.
double Bank::interestRate = 3.5;
// Constructor with Member Initializer
/*
Bank::Bank(const Vault& _vault, int _width, int _height)
: vault(_vault), width(_width), height(_height)
{ }
*/
// using default parameters
Bank::Bank(const Vault& _vault,
int _width = maxWidth,
int _height = maxHeight)
: vault(_vault), width(_width), height(_height)
{ }
// Copy constructor
Bank::Bank(const Bank& src)
: vault(src.vault), width(src.width), height(src.height)
{ }
int Bank::roundDown(double val) {
return static_cast<int>(val);
}
string Bank::getBranchName() const {
return branchName;
}
void Bank::setBranchName(string _bName) {
branchName = _bName;
}
int main() {
Vault hanaVault(200'000'000);
Bank b1(hanaVault);
Bank b2(hanaVault, 50);
Bank b3(hanaVault, 50, 50);
Bank hanaBank(hanaVault, 50, 50);
hanaBank.setBranchName("Hana");
cout << hanaBank.getBranchName() << endl; // Hana
cout << Bank::roundDown(3.4) << endl; // 3
return 0;
}
'Programming Language > C++' 카테고리의 다른 글
예외 처리(Exception Handling) (0) | 2020.11.16 |
---|---|
객체 지향 프로그래밍(Object-Oriented Programming) - 4 (0) | 2020.11.15 |
객체 지향 프로그래밍(Object-Oriented Programming) - 3 (0) | 2020.11.10 |
객체 지향 프로그래밍(Object-Oriented Programming) - 1 (1) | 2020.11.06 |
From C to C++ (0) | 2020.11.04 |