C++ 복습(5) - 복사생성자와 함수중복

C++의 객체 복사

  • 얇은 복사(shallow copy)
    • 객체 복사시, 객체의 멤버를 1:1로 복사
    • 객체의 사본은 원본 객체가 할당 받은 메모리를 공유함
      • 그러므로 원본과 사본중 하나만 수정해도 나머지 객체의 속성이 같이 바뀜
  • 깊은 복사(deep copy)
    • 객체 복사시, 객체의 멤버를 1:1로 복사
    • 객체를 그 메모리 크기만큼 아예 다른 객체로 복사하는 것이므로, 원본과 사본이 메모리를 공유하지 않음
      • 원본과 사본을 따로 수정 가능
    • shallow_deep_copy

복사 생성자(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); // 디폴트 매개변수를 사용하고 있는지 모호함, 컴파일 오류 발생
    }