C++ 복습(5) - 복사생성자와 함수중복
C++의 객체 복사
- 얇은 복사(shallow copy)
- 객체 복사시, 객체의 멤버를 1:1로 복사
- 객체의 사본은 원본 객체가 할당 받은 메모리를 공유함
- 그러므로 원본과 사본중 하나만 수정해도 나머지 객체의 속성이 같이 바뀜
- 깊은 복사(deep copy)
- 객체 복사시, 객체의 멤버를 1:1로 복사
- 객체를 그 메모리 크기만큼 아예 다른 객체로 복사하는 것이므로, 원본과 사본이 메모리를 공유하지 않음
- 원본과 사본을 따로 수정 가능
복사 생성자(copy constructor)
- 객체를 복사 생성시 호출되는 특별한 생성자
- 특징
- 한 클래스에 한개만 선언 가능
- 자기 클래스에 대한 참조 매개변수(class&)를 가지는 독특한 생성자
- 복사 생성자 선언 예시
-
class Circle { ... Circle(Circle &c); // 복사 생성자 선언 ... } Circle::Circle(Circle& c){ // 복사 생성자 구현부 ... }
-
- 복사 생성자 사용 예시
-
int main() { Circle src; // 디폴트 생성자를 사용하여 Circle 객체를 만듦 Circle dest(src); // dest 객체의 복사 생성자를 호출하여 src객체의 속성을 복사 }
-
- 복사 생성자가 선언되지 않은 클래스의 경우, 컴파일러가 자동으로 디폴트 복사 생성자를 삽입
-
class Circle { int radius; public: Circle(int r); // 복사 생성자 없음 double getArea(); } Circle src(10); Circle dest(src); // 디폴트 복사 생성자 Circle(Circle&)를 호출 // 디폴트 복사 생성자 예시 Circle::Circle(Circle& c){ this->radius = c.radius; // 원본 객체 c의 각 멤버를 사본(this)에 복사한다. }
-
주의할 점: 디폴트 복사 생성자의 경우 얕은 복사로 인해 비정상 종료가 일어날 수 있음!!
얕은 복사로 인한 비정상 종료 예시
class Person {
char* name;
int id;
public:
Person(int id, const char* name); // 생성자
~Person(); // 소멸자
void changeName(const char *name);
void show(){ cout << id << ',' << name << endl;}
};
Person:: Person(int id, const char* name){ // 생성자
this->id = id;
int len = strlen(name); // name의 문자 개수
this->name = new char[len+1]; // name의 문자열 공간 동적할당
strcpy(this->name, name); // name에 문자열 복사
}
Person::~Pserson(){ // 소멸자
if(name) // 만일 name에 동적할당된 배열이 있으면
delete [] name; // 동적할당 메모리 소멸
}
void Person::changeName(const char* name){ // 이름 변경
if(strlen(name) > strlen(this->name))
return;
strcpy(this->name, name);
}
// show 객체 구현부는 생략
// 이 부분은 컴파일러에 의해 자동 삽입되는 디폴트 복사 생성자!!
Person::Person(Person& p){
this->id = p.id;
this->name = p.name;
}
int main() {
Person father(1, "Kitae"); // (1) father 객체 생성
Person daughter(father); // (2) daughter 객체 복사 생성, 복사생성자 호출
father.show(); // (3) father 객체 출력
daughter.show(); // (3) daughter 객체 출력
daughter.changeName("Grace"); // (4) daughter 이름을 "Grace"로 변경
father.show(); // (5) father 객체 출력
daughter.show(); // (5) daughter 객체 출력
return 0; // (6), (7) daughter, father 객체 소멸
}
깊은 복사 생성자로 수정하여 위에서 발생하는 오류를 해결할 수 있다.
class Person {
char* name;
int id;
public:
Person(int id, const char* name); // 생성자
Person(Person& person); // 복사 생성자
...
};
Person::Person(Person& person)
{
this->id = person.id; // id값 복사
// 깊은 복사가 이루어짐
int len = strlen(person.name); // name의 문자 개수
this-> name = new char[len + 1]; // name을 위한 공간 할당
strcpy(this->name, person.name); // name의 문자열 복사
cout << "복사 생성자 실행" << this->name << endl;
}
묵시적 복사 생성 예제
// 2. 값에 의한 호출로 객체가 전달될 때, person 객체의 복사 생성자 호출
void f(Person person){
person.changeName("dummy");
}
Person g(){
Person mother(2, "Jane");
// 3. 함수에서 객체를 리턴할 때, mother 객체의 복사본 생성, 복사본의 복사 생성차 오출
return mother;
}
int main() {
Person father(1, "Kitae");
// 1. 객체로 초기화하여 객체가 생성될 때, son 객체의 복사 생성자 호출
Person son = father;
f(father);
g();
}
함수 중복(Function Overloading)
C++에서 코드를 작성하다 보면, C언어와는 다르게 동일한 이름의 함수가 공존할 수 있다. namespace를 분리시키는 방법도 있지만, 여기서의 함수 중복은 그런 것이 아니다.
단, 함수 중복을 하려면 아래와 같은 조건들을 만족시켜야 한다.
- 중복된 함수들의 이름이 동일해야 한다.
- 중복된 함수들의 매개 변수 타입이 다르거나 개수가 달라야 한다.
- 리턴 타입은 함수 중복과 무관한다.
함수 중복을 하게 되면 함수의 이름을 구분하여 기억할 필요가 없고, 함수 호출을 잘못하는 실수를 줄일 수 있다.
성공적인 함수 중복의 예시는 아래와 같다.
int sum(int a, int b, int c) {
return a+b+c;
}
double sum(double a, double b) {
return a+b;
}
int sum(int a, int b) {
return a+b;
}
int main() {
// 첫 번째 sum 함수 호출
cout << sum(2, 5, 33);
// 두 번째 sum 함수 호출
cout << sum(12.5, 33.6);
// 세 번째 sum 함수 호출
cout << sum(2, 6);
}
함수 중복 실패 사례는 아래와 같다.
int sum(int a, b) {
return a+b;
}
double sum(int a, int b) {
return (double)(a+b);
}
int main() {
cout << sum(s, 5);
}
이 경우 컴파일러는 어떤 sum() 함수를 호출하는지 구분할 수가 없다. 즉, 위에서 언급 한 세 번째 조건에 걸리게 되어 함수 중복에 실패한다. 위 예시와 같은 경우는 차라리 나중에 설명할 generic template 문법을 사용하여 달성할 수 있다.
생성자 함수 중복
이전의 게시물에서도 사용한 Circle 예시를 들어서 생성자 함수를 이해해 보자
class Circle {
...
public:
Circle() { radius = 1; }
Circle(int r) { radius = r; }
...
}
int main() {
Circle donut; // Circle() 생성자 호출
Circle pizza(30); // Circle(int r) 생성자 호출
}
위 처럼 생성자 함수를 중복시킬 수도 있다.
디폴트 매개변수
매개 변수 값이 넘어오지 않는 경우, 디폴트값을 받도록 선언된 매개변수이다.
주의 할 점: 매개변수가 여러 개인 함수의 경우, 아래와 같이 디폴트값을 받는 매개변수를 함수 선언시 맨 뒤로 위치시켜야 오류가 발생하지 않는다.
void calc(int a, int b=5, int c, int d=0); // 이 경우 컴파일 오류 발생
void calc(int a, int c, int b=5, int d=0); // 이렇게 바꿔주면 컴파일 성공
함수 중복 간소화
위에서 설명한 디폴트 매개변수를 사용해서 함수의 중복을 간소화할 수 있다.
class Circle {
...
public:
Circle() { radius=1; }
Circle(int r) { radius=r; }
...
}
// 위의 중복되는 생성자 함수를 아래와 같이
// 디폴트 매개변수를 활용하여 간소화시킬 수 있다.
class Circle {
...
public:
Circle(int r=1) { radius=r; }
...
}
주의할 점: 디폴트 매개변수를 가진 함수는 (같은 이름을 가진) 다른 중복함수들과 같이 사용할 수 없다.
함수 중복의 모호성
함수를 중복할때, 이를 애매하게 작성하면 컴파일러가 어떤 함수를 호출하는지 판단하지 못한다.
- 형 변환으로 인한 모호성
-
float square(float a) { return a*a; } double square(double a) { return a*a; } int main() { cout << square(3.0); // 3.0은 double, 모호하지 않음 cout << square(3); // 3이 double인지, float인지 모호함, 컴파일 오류 발생 }
- 참조 매개 변수로 인한 모호성
int add(int a, int b) { return a+b; } int add(int a, int &b) { b = b+a; return b; } int main() { int s = 10, t = 20; cout << add(s, t); // 값에 의한 호출인지 참조에 의한 호출인지 애매함, 컴파일 오류 발생 }
- 디폴트 매개 변수로 인한 모호성
void msg(int id) { cout << id << endl; } void msg(int id, string s="") { cout << id << ":" << s << endl; } int main() { msg(5, "Good Morning"); // 정상 컴파일, 두 번째 msg() 호출 msg(6); // 디폴트 매개변수를 사용하고 있는지 모호함, 컴파일 오류 발생 }