※ 복사함수

객체를 복사하는 함수로 복사생성자복사대입연산자가 있다.

- 복사생성자 (Copy Constructor): 포인터를 멤버로 갖는 클래스는 깊은 복사(deep copy)를 수행.

- 복사대입연산자 (operator=): 따로 선언하지 않을 경우 default로 얕은 복사를 하는 대입연산자를 컴파일러가 자동생성.

 

 주소 즉, 포인터를 갖는 경우,복사생성자(Copy Constructor)와

대입연산자(Copy Assignment Operator)를 반드시 정의! 

#include <iostream>
#include <cstring>
using namespace std;

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person();
    // VC++에서 char* _name은 문자열 상수를 직접 입력으로 받지 못함
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p);
    Person& operator=(const Person& p);
    void ShowData(); // 선언
};

Person::Person() {
    cout << " 매개변수 없는 생성자 호출" << endl;
    name = NULL;
    phone = NULL;
    age = 0;
}

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << " 매개변수 3개 생성자 호출" << endl;
    name = new char[strlen(_name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(_name) + 1, _name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1, _phone);

    age = _age;

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person::~Person() { // DeAllocate the heap
    cout << " 소멸자 호출" << endl;
    delete[]name;
    delete[]phone;

    cout << " name 주소 해제 : " << (void*)name << ", phone 주소 해제 : " << (void*)phone << endl;
}

Person::Person(const Person& p) : age(p.age) {
    cout << " 복사 생성자 호출" << endl;
    name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사
    // The statement name = new char; will create the new heap location 
    // and then copies the value of obj content to new heap location.

    phone = new char[strlen(p.phone) + 1]; 
    strcpy_s(phone, strlen(p.phone) + 1, p.phone);

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person& Person::operator=(const Person& p) {
    cout << " 복사 대입 연산자 호출" << endl;
    if (this != &p) {
        delete[]name;
        delete[]phone;

        name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
        strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

        phone = new char[strlen(p.phone) + 1]; 
        strcpy_s(phone, strlen(p.phone) + 1, p.phone);

        age = p.age;

        cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
    }
    return *this;
}

void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의
    cout << " name : " << name << ", phone : " << phone << ", age : " << age << endl;
}

int main() {
    cout << endl; // 출력 구분 목적
    Person p1("홍길동", "010-1234-5555", 34);
    p1.ShowData();

    cout << endl;
    Person p2(p1); // 복사 연산자 호출
    p2.ShowData();

    cout << endl;
    Person p3;
    p3 = p1; // 대입연산자 호출된다. p3.operator=(p1);
    p3.ShowData();

    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
    cout << endl;
}

출처: https://link2me.tistory.com/1755?category=1075719 [소소한 일상 및 업무TIP 다루기:티스토리]

 

 

 

 

 

 

[Effective C++ _item 5]에서 말했듯, 복사함수는 컴파일러가 생성해 주지만 

우리가 객체복사함수를 선언한다는 것은 컴파일러의 복사함수의 기본동작이 마음에 들지 않기 때문이다.

 

[Effective C++ _item 12]: 객체의 모든 부분을 빠짐없이 복사하자!

Ex-1) 문제되는 점이 없다.

 

Ex-2) 데이터 멤버 하나를 Custormer에 추가하게 되면서 문제가 발생한다.

=> 바로 복사함수가 완전복사가 아닌, 부분복사가 된다는 점이다!

따라서 이 문제를 해결하기 위해 복사함수는 물론 생성자도 모두 다시 갱신 및 작성해줘야 한다.

이 문제가 가장 문제가 되는 경우가 있다. 바로클래스의 상속이다!

아래의 예시에서 PriorityCustomer에 선언된 데이터 멤버를 모두 복사하고 있는 것은 사실이지만

Customer로 부터 상속된 즉, PriorityCustomer복사생성자에는 기본클래스 생성자에 넘길 인자들이 명시되지 않아서 기본 생성자에 의해 초기화가 되어 버린다.

또한, 복사대입 연산자의 경우 기본클래스의 데이터 멤버를 건드릴 시도도 하지 않기에 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.

 

따라서 아래 빨간색 글씨처럼 파생클래스의 복사함수 안에서 기본클래스의 복사함수를 호출하자!

 

 

 

 

 

 

[Effective C++ _item 11]: operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자!

자기대입이란?

어떤 객체자기자신에 대입연산자를 적용하는 것이다.

예를 들어 arr[i] = arr[j]; 라는 문장에서 ij값이 같다면, 자기대입문이 되는 것이다.

중복참조 때문에 이러한 현상이 발생하게 되는 것이다

 

중복참조란?

여러곳에서 하나의 객체를 참조하는 것

 

operator= 구현 시, 객체가 그 자신에 대입되는 경우를 제대로 처리하는기법

1. 원본 객체와 복사대상 객체의 주소를 비교 (위의 if를 이용한 코드)
2. 예외 안정성에 집중하기 (문장의 순서를 적절히 조정 or swap기법) – item 29 참조

- copy constructor

- move constructor

- perfect forwarding

- 임시객체

- explicit, friend, static, mutable

※ C++ 클래스의 생성자 종류

- 기본생성자 (Default Constructor): 기본적으로 컴파일러가 생성해주는 생성자.

- 복사생성자 (Copy Constructor): 포인터를 멤버로 갖는 클래스는 깊은 복사(deep copy)를 수행한다.

- 이동생성자 (Move Constructor): r-value를 parameter로 갖는 생성자. (C++ 11에서 추가된 타입의 생성자)

 

 

 

 

※ 복사생성자 (Copy Constructor)

- 자신과 같은 클래스 타입의 다른 객체에 대해 참조를 인수로 전달받아참조를 이용해 자신을 초기화 하는 방법

- 복사생성자는 새롭게 생성되는 객체가 원본객체와 같지만 완전한 독립성을 갖게 해준다.

  (복사생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값의 복사이기 때문.)

 

※ 복사생성자 형식: Add(const Add& copy) : num1(copy.num1), num2(copy.num2) { ... }

 

 

※ default 복사생성자

- 복사생성자 정의가 없을 때 자동 삽입되며 이로 인해 멤버 간 복사(얕은 복사)시 복사생성자를 직접 정의할 필요가 없다.

- 멤버 대 멤버의 단순복사를 진행한다.

- 다만 필수로 복사생성자를 정의해야하는 경우가 있다. (깊은 복사)

int main() {
    Position xpos;
    Position ypos(xpos); // default 복사생성자의 멤버 대 멤버 복사(얕은 복사)
}

 

Position ypos(xpos)는 Position ypos = xpos와 동일하게 사용될 수 있다.

 

 

※ 얕은 복사의 문제점

위의 Person p2(p1)에서 default 복사생성자의 멤버 대 멤버 복사(얕은 복사)가 진행된다.

이때, ~Person 즉, 소멸자에서 delete [ ]name;를 할 때 문제점이 발생한다. 

위의 코드의 과정을 보면, 

- 얕은 복사의 결과로 하나의 문자열을 2개의 객체가 동시에 참조하게 된다.

- 위의 예시에서 delete [ ]name;으로 p2의 소멸자가 호출되면서 문자열을 소멸시킨다.

- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.

 

★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.

※깊은복사

- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법

- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.

위의 코드에서 아래 코드를 추가해주면 된다.

- 멤버변수 age의 멤버 대 멤버 복사

- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장

즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.

 

 

 

§ 또 다른 예제

DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
    // 자신만의 동적데이터공간 할당
    mArray = new int[mSize];
    
    // 다른 객체로부터의 데이터 복사
    for (int i = 0; i < mSize; i++) {
        mArray[i] = other.mArray[i];
    }
}

 

 

 

※ 복사생성자 호출 시점: 객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.

1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때.  [Person p2 = p1;]

2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때

3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.

- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)

 

2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시

출처) 윤성우의 열혈C++프로그래밍

3. 객체를 반환하지만 참조형으로 반환하지 않는경우

출처) 윤성우의 열혈C++ 프로그래밍

 

 

 

 

cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!

Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?

A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.

예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.

{
 Person p1(5);
cout << "부모 객체 p1 생성" << endl;
 }
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;

만약 실행되었다면 다음과 같이 출력이될 것이다.

부모의 생성자
부모 객체 p1 생성

부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자

하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.

 

 

이해를 위해 또 다른 예시를 들어 보겠다.

 {
     Person p1(5);
 }
cout << "부모 객체 p1 생성" << endl;

이렇게 된다면 출력은 어떻게 될까?

이는 아래와 같다.

부모의 생성자

부모의 소멸자
부모 객체 p1 생성

즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.

 

 

cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 임시객체를 이용한 obj.func().func(); 연산

 

 

 

 

 

 

 

 

※ 반환시 만들어진 (임시)객체의 소멸

1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.

2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.

 

또 다른 예시)

 

 

 

 

 

 

 

 

※ r-value 참조

r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에  Double Ampersand(&&)를 붙여 표기한다.

이런 더블 엡퍼센드를 r-value 참조자라 부른다.

 

사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다

프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.

/*---------------일반 변수--------------- */
void f(){
    int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸


/*---------------r value 참조 변수--------------- */
void f(){
    int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸

위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다

 

 

?. r-value 참조는 목적이 무엇인가 ?

1. 클래스의 객체 생성시 사용하는 이동생성자 이동대입연산자의 인수로 사용.

2. perfect forwarding이라는, 함수 오버로딩의 개수를 줄이는데 사용

https://chan4im.tistory.com/20

 

☆Prev. C++, Modern C++의 변화

※ C++의 버전변화 C++98, C++03 ==> C++11 ==> C++14 ... C++11: 기존 C++98에서 람다 표현식을 지원한 것 C++14: 일반화된 함수반환 형식 유추를 지원한 것 ※ C++11/14 문법적 변경 사항 1. 초기화 리스트 및..

chan4im.tistory.com

 

 

 

 

 

 

 

이동생성자 (Move Constructor) _ (feat. &&의 사용)

- 이동생성자가 호출되면 얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move) 방식으로 객체를 생성.

- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.

 

Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?

A. 아래 코드를 통해 설명하겠다.

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class Animal {
private:
    char *name;
    int age;
public:
    Animal(int age_, const char* name_) {
        age = age_;
        name = new char[strlen(name_) + 1];
        strcpy(name, name_);
    }
    Animal(const Animal & a) { //복사생성자에 의한 깊은복사
        age = a.age;
        name = new char[strlen(a.name) + 1];
        strcpy(name, a.name);
        cout << "복사생성자" << endl;
    }


    Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
        age = a.age;
        name = a.name;
        cout << "이동생성자" << endl;
        a.name = nullptr;
    }
    ~Animal(){
        cout <<"Destructor!!" << endl;
        if(name)
            delete [] name;
    }
    void changeName(const char *newName) {
        strcpy(name, newName);
    }
    void printAnimal() {
        cout << "Name: " << name << " Age: " << age << endl;
    }
};

int main() {
    Animal A(10, "Jenny");
    A.printAnimal();

    vector<Animal> vec; // Animal 벡터 타입

    // Animal 벡터 객체 삽입
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);

    A.printAnimal();

    for (int i = 0; i < 5; i++) {
        vec[i].printAnimal();
    }
    return 0;
}

복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남 
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.

이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.

 

[이동생성자 사용 시 주의할 점]

1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)

2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함

3, 소멸자에서 메모리할당 관련 변수가 nullptr인지 확인해 줘야함.

 

 

ex-2) 예제

출처)&nbsp;https://link2me.tistory.com/1756

또 다른 설명)

https://learn.microsoft.com/ko-kr/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=msvc-170&viewFallbackFrom=vs-2019 

 

방법: 이동 생성자 정의 및 할당 연산자 이동(C++)

자세한 정보: 생성자 이동 및 할당 연산자 이동(C++)

learn.microsoft.com

 

 

※ Perfect Forwarding

#include <iostream>
using namespace std;

class Widget {
public:
    Widget(int&, int&){ }
};
class X_Widget {
public:
    X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
    Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
    Z_Widget(const int&, const int&){ }
};

// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A& a, B& b) {
    return new T(a, b);
}

int main() {
    int a = 4, b = 5;

    Widget *w = factory<Widget>(a,b);
    // 아래부터는 모든 타입을 수용하는 함수템플릿을 만들지 않는 이상 Err 발생
    X_Widget *x = factory<X_Widget>(2, b);
    Y_Widget *y = factory<Y_Widget>(a, 2);
    Z_Widget *z = factory<Z_Widget>(2, 2);

    delete w; delete x; delete y; delete z;
}

 

 

따라서 결과적으로 참조타입별 함수오버로딩이 증가하는 문제를 해결하는 방법으로 다음과 같이 r value참조를 사용한다.

#include <iostream>
using namespace std;

class Widget {
public:
    Widget(int&, int&){ }
};
class X_Widget {
public:
    X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
    Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
    Z_Widget(const int&, const int&){ }
};

// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A&& a, B&& b) {
    return new T(a, b);
 // return new T(std::forward<A>(a), std::forward<B>(b));
}

int main() {
    int a = 4, b = 5;

    Widget *w = factory<Widget>(a,b);
    X_Widget *x = factory<X_Widget>(2, b);
    Y_Widget *y = factory<Y_Widget>(a, 2);
    Z_Widget *z = factory<Z_Widget>(2, 2);

    delete w; delete x; delete y; delete z;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ explicit: 복사생성자의 묵시적변환

- Add add2 = add1;을 Add add2(add1); 과 같은 방식을 허용하지 않는 방법

explicit Add(const Add& copy) : num1(copy.num1), num2(copy.num2){
    //empty
}

그렇다면 이런 explicit의 이점은 무엇일까?

explicit을 사용함으로 인해 컴파일러의 자동 형변환을 막을 수 있다.

이는 내부 함수, 템플릿 구현에 탁월한데 사용자가 원하지 않는 형변환이 발생하는 등의

예상할 수 없는 버그가 발생할 수 있어서 explicit으로 사용자가 상황에 맞게 직접 형변환을 해주도록 한다.

 

 

 

 

※ friend 선언: private에 접근을 허용하는 선언(남용X)

1. A클래스가 B클래스를 대상으로 friend 선언을 하면 B는 A의 private멤버에 직접 접근이 가능

2. 단, A도 B의 private에 직접접근을 하려면 B가 A를 대상으로 friend선언을 해줘야 함.

 

 

※ static 멤버변수 (class 변수)

- 메모리 공간에 딱! 하나만 할당되며 모든 객체가 static 멤버변수를 공유할 수 있다.

- 단, 생성자에서 static 멤버변수를 초기화 할 수 없다! (객체 생성될 때 마다 초기화 되기 때문)

따라서 아래와 같이 static 변수 초기화 문법은 정해져 있다.

class Position{
private:
    static int num;
public:
    Position(){ num++; }
};

int Position::num = 0;  //static변수 초기화

 

※ static 멤버함수

- 선언된 클래스의 모든 객체가 공유

- public 선언 시 클래스 이름으로 호출 가능

- static 멤버함수내에서는 static 멤버변수와 static 멤버함수만 호출 가능하다.

 

※ const static 멤버

- const 멤버변수의 초기화는 initializer를 이용해야 한다.

- const static 멤버변수의 초기화는 선언과 동시에 초기화가 가능하다!

class Country {
public:
    const static int Korea = 82;
};

int main() {
    cout << "국번: " << Country::Korea << endl;
}

 

 

 

※ mutable

- const 함수 내의 값 변경을 예외적으로 허용해주는 키워드.

 

 

 

 

 

 

 

 

 

 

※ [Effective C++_item 5]: C++이 은근슬쩍 만들어 호출해버리는 함수에 집중하자!

생성자: 새로운 객체를 메모리에 만들거나 객체의 초기화를 맡는 함수

소멸자: 객체를 없앰과 동시에 메모리에서 그 객체를 사라지게 하는 함수

대입 연산자: 기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수

 

직접 클래스안에 선언하지 않더라도 C++ 컴파일러가 저절로 선언해주는 멤버함수가 있다.

복사생성자, 복사대입연산자, 소멸자 이때 컴파일러가 만드는 형태는 모두 기본형이다.

이들은 모두 public 멤버이며 inline 함수이다.(Effective C++ item 30 참조)

이때, 기본클래스의 소멸자가 가상소멸자로 되어있지 않으면 비가상 소멸자로 만들어진다.(Effective C++ item 7 참조)

 

컴파일러가 만들어낸 복사생성자와 복사대입연산자의 경우, 하는 일이 매우 단순하다.

-> 원본 객체의 비정적 데이터사본 객체로 그냥 복사하는 것이 전부이다.

 

복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법으로 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해주며

예를 들면, Book 클래스의 복사 생성자의 원형Book(const Book&) 와 같다.

 

 

 

 

※ [Effective C++_item 6]: 컴파일러가 만든 함수가 필요없으면 이들의 사용을 금하자!

컴파일러가 생성하는 함수(모두 public): 복사생성자 / 복사 대입 연산자 / 소멸자

 

객체의 사본을 만드는 것원천 차단하기 위한 방법(2가지)

 

방법1)

1. 컴파일러 생성함수는 public이기에 이를 private 선언 해준다.

2. 멤버함수, friend선언을 통해 접근이 가능 => 정의(define)를 하지 않는다.

 

방법 2)

1. 복사생성자와 복사대입연산자 private으로 선언

2. Sale 자체가 아닌 별도의 기본 클래스에 넣고 이것으로 부터 Sale을 파생

(다만, Uncopyable클래스가 기본 클래스이기에 이 기법은 다중상속으로 갈 가능성이 존재한다.)

+ Recent posts