'2019/11/27'에 해당되는 글 2건

728x90

기존 객체와 같은 값을 가진 객체를 복사할 때 값 형식이냐 참조 형식이냐에 따라 얕은 복사와 깊은 복사의 개념이 나눠진다.


stack 영역
- 프로그램이 자동으로 사용하는 임시 메모리 영역이다.
- 지역 변수, 매개변수, 리턴 값 등 잠시 사용되었다가 사라지는 데이터를 저장하는 영역이다.
- 함수 호출 시 생성되고, 함수가 끝나면 시스템에 반환 된다.
- 스택 사이즈는 각 프로세스마다 할당 되지만 프로세스가 메모리에 로드 될 때 스택 사이즈가 고정되어 있어, 런타임 시에 스택 사이즈를 바꿀 수는 없다.
- 명령 실행시 자동 증가/감소 하기 때문에 보통 메모리의 마지막 번지를 지정 한다.


heap 영역
- 동적으로 메모리를 할당 하고자 할 때 위치하는 메모리 영역으로 동적 데이터 영역이라고 부르며, 메모리 주소 값에 의해서만 참조되고 사용되는 영역이다.
- 이 영역에 데이터를 저장 하기 위해서 C는 malloc(), C++은 new() 함수를 사용한다.

new int(3)로 객체를 생성하면 heap 공간에 메모리를 할당하고 값 3을 저장한다.  포인터 변수 a로 100번지 주소를 가리키게 한다.

new int(5)로 객체를 생성하면 heap 공간에 메모리를 할당하고 값 5를 저장한다.  포인터 변수 b로 200번지 주소를 가리키게 한다.


a = b 를 함으로써 a 는 b의 주소를 가리키게 된다. 즉, b주소가 가진 값 5를 출력한다.

동적으로 할당한 메모리는 포인터가 진입점인데 100번지 메모리 공간은 진입점을 잃어버려 Garbage가 된다.


복사가 발생하여

대상체는 1개인데, 대상체에 접근할 수 있는 포인터가 2개인 경우를 shallow copy(얕은 복사)라고 한다.

대상체가 2개인데, 대상체에 접근할 수 있는 포인터가 각각 1개인 경우를 deep copy(깊은 복사)라고 한다.


Java, C#에서는 Garbage Collector 가 삭제를 해주는데 C++에서는 없다.


#include <iostream>
using namespace std;

int main() {

    int* a = new int(3); // 메모리 공간 할당
    int* b = new int(5);

    cout << "a의 주소(복사전) : " << a << endl;
    cout << "b의 주소(복사전) : " << b << endl;   

 


    a = b; // 얕은 복사(참조 복사) ← b의 주소를 a에 복사 즉 a 는 b의 주소 200을 가리킨다.
    // *a = *b; // 깊은 복사(값 복사)

    *b = 10;


    cout << "a의 주소(복사후) : " << a << endl;
    cout << "b의 주소(복사후) : " << b << endl;

    cout << "a의 값 : " << *a << endl;
    cout << "b의 값 : " << *b << endl;

    delete a; // a가 가리키고 있는 메모리 공간 해제
    delete b;
}


얕은 복사를 했을 경우 아래와 같은 에러가 발생한다.

주소값을 참조하기 때문에 복사를 하였더라도 같은 주소를 참조하게 된다.

소멸자가 호출될 때 같은 메모리를 참조하기 때문에 한번 delete 된 메모리를 다시 delete 하기 때문에 오류 창이 뜬다.


깊은 복사를 할 경우에는 에러가 발생하지 않는다.

깊은 복사는 주소가 가리키는 값 즉, 그 메모리의 값을 복사하는 형태다.

새로운 메모리를 할당하기 때문에 원본과 복사본의 참조 공간이 다르므로 소멸자도 오류가 발생하지 않는다.


얕은 복사와 깊은 복사를 좀 더 이해하기 위해서

구글에서 영문으로 shallow copy vs deep copy c++ example 검색하여

https://owlcation.com/stem/Copy-Constructor-shallow-copy-vs-deep-copy 사이트를 찾아 읽어보고

Visual Studio 2019 Community 에서 테스트하고 적어둔다.


#include <iostream>
using namespace std;

class ShalloC {
private:
    int* x; //Sample 01: Private Data Member
    // This class contains only one integer pointer as private data member.
public:
    //Sample 02: Constructor with single parameter
    ShalloC(int m) {
        cout << "생성자 호출" << endl;
        x = new int;
        *x = m;
        // The constructor will create a memory location in a heap
        // and copy the passed in value m to the heap content.
    }

    //Sample 08: Introduce Copy Constructor and perform Deep Copy
    ShalloC(const ShalloC& obj)    {
        cout << "복사 생성자 호출" << endl;
        x = new int;
        *x = obj.GetX();
        // The statement x = new int; will create the new heap location
        // and then copies the value of obj content to new heap location.
    }

    //Sample 03: Get and Set Functions
    int GetX() const {
        return *x;
    }
    void SetX(int m) {
        cout << "SetX " << endl;
        *x = m;
    }

    //Sample 04: Print Function
    void PrintX() {
        cout << "Int X = " << *x << endl;
    }

    //Sample 05: DeAllocate the heap
    ~ShalloC() {
        cout << "소멸자 호출" << endl;
        delete x; // new 로 할당한 메모리 해제
    }
};

int main()
{
    //Sample 06: Create Object 1 and copy that to Object 2.
    //           Print the data member for both Object 1 & 2.
    ShalloC ob1(10);
    ob1.PrintX();
    ShalloC ob2 = ob1;
    ob2.PrintX();

    //Sample 07: Change the Data member value of Object 1
    //           And print both Object 1 and Object 2
    cout << endl;
    ob1.SetX(12); // ob1 객체 값 변경
    ob1.PrintX(); // ob1 객체의 값 출력
    ob2.PrintX(); // ob2 객체의 값 출력
}

In the Program main we created two Objects ob1 and ob2.

The object ob2 is created using the copy constructor. How? And where is the "copy constructor".?

If you look at the statement ShalloC ob2 = ob1 ; you clearly know that the ob2 is not yet created and in the mean time ob1 is already created.

Hence, a copy constructor is invoked. Even though the copy constructor not implemented, the compiler will provide default copy constructor. Once both the objects are created we print the values in ob1 and ob2.


실행결과

ob1 객체의 값을 변경하고 나서 ob1 의 값과 ob2의 값이 서로 달라진 걸 확인할 수 있다.

이렇게 값을 변경해 봄으로써 깊은 복사(deep copy)가 된 것인지 얕은 복사(shallow copy)가 된 것인지 명확히 알 수 있다.


복사 생성자 코드 부분을 주석처리하고 테스트 해보면 이해가 명확해진다.

default 복사 생성자가 자동으로 추가되어 얕은 복사를 수행했고, 소멸자는 같은 메모리 주소를 2번 삭제하려고 하다보니 에러 메시지를 출력한다.

블로그 이미지

Link2Me

,

C++ 복사 생성자

C++/C++ 문법 2019. 11. 27. 08:58
728x90

C++ 클래스에는 멤버 변수와 멤버 함수가 있다.

클래스 객체를 생성하면 복사 생성자를 별도로 선언하지 않아도 default 로 복사 생성자(Copy constructor)가 자동으로 생성된다.

만약 생성자 내에서 동적 할당을 한다면 반드시 구현해 주어야 할 것은 소멸자, 복사 생성자이다.

'얕은 복사'는 주소값만 복사하는 것이며, '깊은 복사'는 따로 메모리 할당까지 해주는 것이다.

default copy constructor 는 얕은 복사가 발생하여 소멸자에서 에러가 발생한다.

따라서 개발자가 직접 깊은 복사가 되도록 copy constructor를 구현해 주어야 한다.



C++ 복사 생성자 동영상 강좌를 따라하는데 에러가 발생하고 안된다.

동영상 강좌가 Visual Studio 2017 이전 버전인가 보다. 해결방법은 아래 설명되어 있다.

Person 클래스의 ① 복사 생성자를 구현한 상태로 실행해보고 ② 다시 주석처리하고 실행해보면, 결과가 다르게 나온다는 걸 확인할 수 있다.

예제1.

#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
public:
    Point(int _x=0, int _y=0) : x(_x), y(_y){}
    Point(const Point& p) { // 복사 생성자
        x = p.x;
        y = p.y;
    }
    // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다.
    void ShowPosition();
};

void Point::ShowPosition() {
    cout << "(" << x << ", " << y << ")" << endl;
}

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person(char* _name, char* _phone, int _age);
    Person(const Person &p) {
        name = new char[strlen(p.name) + 1]; // 문자열 할당. + 1 은 NULL 문자 고려
        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;
    }
    ~Person();
    void ShowData();
};

Person::Person(char* _name, char* _phone, int _age) {
    name = new char[strlen(_name) + 1];
    strcpy_s(name, strlen(_name) + 1,_name);

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

    age = _age;
}

Person::~Person() { // DeAllocate the heap
    delete []name;
    delete []phone;
}

void Person::ShowData() {
    cout << "name : " << name << endl;
    cout << "phone : " << phone << endl;
    cout << "age : " << age << endl;
}

int main() {
    Point p1(10, 20); // 객체가 생성될 때 메모리가 할당된다.
    Point p2(p1); // 복사 생성자 호출 발생

    p1.ShowPosition();
    cout << "p1의 주소 값 : " << &p1 << endl;
    p2.ShowPosition();
    cout << "p2의 주소 값 : " << &p2 << endl;

    Person p3("홍길동", "010-1234-5555", 30);
    Person p4(p3);

    p4.ShowData();
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
}

에러메시지 내용은

'Person::Person(const Person &)': 인수 1을(를) 'const char [4]'에서 'char *'(으)로 변환할 수 없습니다.


Visual Studio 2017 이상부터는 표준 문법을 엄격히 사용하도록 되어 있다.

Visual Studio 는 문자열 저장방식을 char * 가 아닌 string 사용을 권장하고 있다.

에러 메시지를 해결하는 방법으로 해당 파일에서 마우스 우클릭 누르면 속성이 나온다.

C/C++ → 언어 → 준수모드 : 아니오 로 변경하면 에러 메시지 없이 결과가 출력된다.

이것이 올바른 해결책은 아니다.

에러 메시지에 해당되는 https://docs.microsoft.com/ko-kr/cpp/error-messages/compiler-errors-2/compiler-error-c2664?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev16.query%3FappId%3DDev16IDEF1%26l%3DKO-KR%26k%3Dk(C2664)%26rd%3Dtrue&view=vs-2019 를 차분히 읽어보니 해결책은 따로 있다.


const char* _name, const char* _phone 로 const 를 붙이면 해결된다.


copy_constructor.cpp



다른 방법 : char* _name 을 const char _name[] 으로 변경해주고, 선언과 정의를 분리했다.

예제2에서 결과 출력이 잘 되는 걸 확인할 수 있다.

예제2.

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

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p);
    void ShowData(); // 선언
};

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << "생성자 호출" << endl;
    name = new char[strlen(_name) + 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 << endl;
    cout << "phone 주소 : " << (void*)phone << endl;
}

Person::~Person() {
    cout << "소멸자 호출" << endl;
    delete[]name; // 동적할당한 메모리 해제
    delete[]phone;
// 동적할당한 메모리 해제


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

Person::Person(const Person& p) : age(p.age) {
    cout << "복사 생성자 호출" << endl;
    name = new char[strlen(p.name) + 1]; // 문자열 할당. +1은 NULL문자(\0) 고려
    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);


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

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

int main() {
    Person p1("홍길동", "010-1234-5555", 30);
    p1.ShowData();

    Person p2(p1); // 복사 생성자 호출
    p2.ShowData();
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
}

생성자 호출이 되고, 복사 생성자가 호출된다.

소멸자가 두번 호출되는 걸 확인할 수 있고 소멸된 순서를 확인할 수 있다.



이번에는 char *name 대신에 string 으로 변경하여 #include <string> 를 추가하고 코드를 수정해서 테스트했다.

결과는 잘 출력된다.


예제3.

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

class Point {
private:
    int x, y;
public:
    Point(int _x=0, int _y=0) : x(_x), y(_y){}
    Point(const Point& p) { // 복사 생성자
        x = p.x;
        y = p.y;
    }
    // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다.
    void ShowPosition();
};

void Point::ShowPosition() {
    cout << "(" << x << ", " << y << ")" << endl;
}

class Person {
    string name;
    string phone;
    int age;
public:
    Person(string _name, string _phone, int _age);
    ~Person();
    Person(const Person& p); // Copy Constructor

    //Setter method
    void setName(string name) {    this->name = name; }
    void setPhone(string phone) { this->phone = phone; }
    void setAge(int age) { this->age = age; }

    // Getter method
    string getName() { return name; }
    string getPhone() { return phone; }
    int getAge() { return age; }

    void ShowData();
};

Person::Person(string _name, string _phone, int _age) {
    cout << "Person 생성자 호출- 메모리 주소 : " << this << endl;
    name = _name;
    phone = _phone;
    age = _age;
}

Person::~Person() {
    cout << "Person 소멸자 호출- 메모리 주소 : " << this << endl;
}

Person::Person(const Person& p) {
    cout << "Person 복사 생성자 호출 - 메모리 주소 : " << this << endl;
    name = p.name;
    phone = p.phone;
    age = p.age;
}

void Person::ShowData() {
    cout << "name : " << name << endl;
    cout << "phone : " << phone << endl;
    cout << "age : " << age << endl;
}

int main() {
    Point p1(10, 20); // 객체가 생성될 때 메모리가 할당된다.
    Point p2(p1);

    p1.ShowPosition();
    cout << "p1의 주소 값 : " << &p1 << endl;
    p2.ShowPosition();
    cout << "p2의 주소 값 : " << &p2 << endl << endl;

    Person p3("홍길동", "010-1234-5555", 30);
    Person p4(p3);

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


Person 클래스의 생성자 호출, 복사 생성자 호출, 소멸자 호출이 2번 되는 걸 확인할 수 있다.

'C++ > C++ 문법' 카테고리의 다른 글

C++ 복사 대입 연산자(copy assignment operator)  (0) 2019.11.28
C++ 얕은 복사, 깊은 복사(Shallow Copy vs. Deep Copy)  (0) 2019.11.27
C++ 연산자 오버로딩  (0) 2019.11.26
C++ static  (0) 2019.11.25
C++ 클래스 이해  (0) 2019.11.23
블로그 이미지

Link2Me

,