⊙ 얕은 복사(Shallow Copy)가 일어나는 상황과 문제가 되는 상황을 이해한다.
⊙ 깊은 복사(Deep Copy)가 일어나는 상황을 이해한다.
⊙ 얕은 복사와 깊은 복사의 차이를 이해한다.
⊙ 깊은 복사를 구현한다.
복사 생성자
클래스에서 다른 객체의 데이터로 초기화된 객체를 생성하는 생성자이다. 모든 클래스는 기본 복사 생성자를 가지지만 개발자가 복사 생성자를 개발할 수도 있다.
복사 생성자는 대입에 경우에는 호출되지 않으며 객체의 초기화의 경우에만 호출된다. 아래 코드의 경우 마지막 줄은 복사 생성자가 호출되지 않는다.
Circle c1;
Circle c2(c1); // 복사 생성자 호출 O
Circle c1;
Circle c2=c1 // 복사 생성자 호출 O
c2=c1; // 복사 생성자 호출 X
얕은 복사(Shallow Copy)
앝은 복사는 한 객체에서의 각 데이터 필드를 다른 객체의 대응부분으로 간단히 복사 하는 것을 말한다.
얕은 복사가 일어나는 상황은 아래와 같다.
- 기본 복사 생성자를 사용할 경우
- 대입에 의한 복사가 이루어질 경우
결국 우리가 복사 생성자를 따로 개발하지 않고 객체를 생성할 때 복사해서 생성하는 경우, 그리고 평소에 우리가 아주 많이 사용하는 대입 연산을 사용할 때마다 얕은 복사가 일어나고 있다는 뜻이다. 그래서 "이 얕은 복사가 문제가 있는, 일어나서는 안되는 복사인가?"라고 한다면 그건 아니다.
우리가 주목해야하는 부분은 기본 복사 생성자를 이용해 객체가 생성되려고 하는데 데이터 필드에 주소값을 가진 데이터가 있는 경우이다. 즉 동적 할당을 받은 변수가 멤버 변수로 포함되어 있는 객체를 복사하려고 하면 문제가 생기는 것이다.
동적 할당 받은 변수와 일반 변수들을 포함한 객체가 기본 복사 연산자를 사용해 객체를 복사하여 얕은 복사가 되었을 때의 모습을 살펴보자.
(그림)
일반적인 변수를 복사하는 경우에는 아무 문제 없는 것을 볼 수 있다. 하지만 객체의 경우 주소를 복사하면 그 주소 안에 값이 아니라 주소를 그대로 복사해 온다는 것을 알 수 있다. 즉 두 곳에서 하나의 주소를 참조하고 있는 것이다. 같은 공간을 공유하고 있는 것이다. 만약 한 곳에서 값 변경을 한다면 다른 곳의 값도 똑같이 변경된다는 뜻이다. 이렇게 되면 값을 변경할 때 문제가 생기게 된다.
얕은 복사 예제
주소값을 가진 멤버 변수가 없다면 아무 문제가 없다. 그래서 아래의 예제에는 아무 문제가 없는 것이다.
Circle circle1(15);
Circle circle2(circle1); // 얕은 복사 발생
cout<<circle1.getRadius()<<"is"<<circle2.getRadius()<<endl;
circle2.setRadius(10.5); // 멤버 변수 변경
circle1.setRadius(20.5); // 멤버 변수 변경
cout<<circle1.getRadius()<<"is"<<circle2.getRadius()<<endl;
Circle은 radius라는 일반 변수만 가지고 있기 때문에 값 변경이 일어나도 서로 따로따로 변경이 일어날 뿐 값을 공유하지 않는다. 따라서 setRadius로 멤버 변수 값을 변경하여도 아무 이상 없이 둘다 값이 각각 10.5, 20.5로 변경될 것이다.
하지만 주소값을 가진 멤버 변수가 발생한다면 문제가 발생한다. 아래의 경우 얕은 복사가 일어나 문제가 발생하는 예제이다.
// [Course.h]
...
// 생성자
Course::Course(const string& courseName, int capacity)
{
numberOfStudent=0;
this->CourseName=courseName;
this->capacity=capacity;
students=new string[capacity]; // 동적 할당된 변수 존재
}
// 학생 추가 함수
void Course::addStudent(const string& name)
{
students[numberOfStudents]=name;
numberOfStudents++;
}
...
// [Main.cpp]
...
Course course1("C++", 10);
Course course2(course1); // 기본 복사 생성자 호출(얕은 복사)
// 값 변경
course1.addStudent("Peter");
course2.addStudent("Lisa");
cout<<course1.getStudents()[0]<<endl; // Lisa
cout<<course2.getStudents()[0]<<endl; // Lisa
Course 클래스에는 students라는 동적할당된 배열 변수가 존재한다. 그리고 따로 복사 생성자를 구현해주지 않았기 때문에 만약 복사 생성자를 호출해야할 때는 기본 복사 생성자가 호출될 것이다. 그러면 students는 얕은 복사가 일어나게 된다.
course2를 course1을 복사해 생성하였다. 분명 나는 course1에는 "Peter"를 추가하였고 course2에는 "Lisa"를 추가하였다. 그런데 결과값을 출력하면 둘다 Lisa가 출력된 것을 볼 수 있다. 이건 내가 의도한게 아닌데 말이다.
무슨 일이 일어난건지 확인해보자. 학생을 추가하면 students 배열에 numberOfStudents번째 요소로 학생을 추가한다. 그리고 numberOfStudents를 1 증가시켜 업데이트 시킨다. course1에서 "Peter"를 추가했을 때도 분명 그대로 동작했을 것이다. students[0]에 "Peter"가 추기되고 0이었던 NumberOfStudents는 1이 되었을 것이다.
이제 course2에 "Lisa"를 추가하는 상황을 보자. course2에는 아직 한명도 학생을 추가한 적이 없기 때문에 NumberOfStudents는 0일 것이다. 헷갈리며 안된다. NumberOfStudents는 얕은 복사가 일어나도 아무 상관없는 일반 int형 변수이다. 그럼 course2에서도 "Lisa"는 students[0]에 추가될 것이다. 그리고 마찬가지로 NumberOfStudents도 0에서 1로 증가할 것이다. 자, 이미 문제는 발생했다. course1과 course2는 students를 공유해서 사용하고 있다. 얕은 복사가 일어났기 때문이다. 그래서 course1에 추가되어 있던 "Peter"는 course2가 "Lisa"를 추가하면서 덮어 쓰여 버렸다. 그래서 둘다 0번째 학생을 출력했을 때 "Lisa"를 출력한 것이다.
깊은 복사(Deep Copy)
깊은 복사는 참조되는 객체를 따로 복사한 후 복사된 객체의 참조값(주소값)을 저장하는 방식이다. 동일한 내용의 2개의 객체를 따로 참조하는 것이다.
기본 사용자를 사용하면 얕은 복사가 일어나 버리는데 깊은 복사가 일어나게 하려면 어떻게 해야할까? 누가 해주지 않는다. 내가 만들어야 한다. 깊은 복사가 일어나는 상황은 아래와 같다.
- 사용자 정의 복사 생성자로 구현해 놓은 경우
사용자가 직접 참조값(주소값)을 가진 멤버 변수를 복사하는 방법에 대해 재정의하여 깊은 복사가 일어나도록 해주면 된다. 동적 할당된 멤버 변수에 그냥 대입 연산자(=)를 이용하여 복사하는 것이 아니라 new를 이용하여 새로 동적 할당한 변수를 만들어 준 뒤에 복사는 그 안에 내용만 가져가도록 만들어 준다.
이런 방법으로 참조값(주소값)을 가진 멤버 변수들을 복사한다면 아래와 같은 모습으로 복사를 할 수 있다.
(그림)
이렇게 하면 한 쪽에서 값의 변경이 일어나도 서로 아무런 영향을 주지 않는다. 둘은 독립적인 공간에 값을 가지고 있기 때문이다.
깊은 복사 예제
얕은 복사에서 예제로 활용했던 Course를 고쳐서 객체를 복사 생성할 때 문제가 없도록 만들어보자. Course에 없던 복사 생성자를 직접 만들어주면 된다.
// [Course.h]
...
Course::Course(const Course& course)
{
courseName=course.courseName;
numberOfStudents=course.numberOfStudents;
capacity=course.capacity;
students=new string[capacity]
for(int i=0; i<numberOfStudents; i++)
{
students[i]=course.students[i];
}
}
// [Main.cpp]
...
Course course1("C++", 10);
Course course2(course1); // 복사 생성자 호출(깊은 복사)
// 값 변경
course1.addStudent("Peter");
course2.addStudent("Lisa");
cout<<course1.getStudents()[0]<<endl; // Peter
cout<<course2.getStudents()[0]<<endl; // Lisa
아래 Main은 아까와 똑같은 코드를 그대로 가지고 온 것이다. 하지만 이 코드에서는 문제 없이 course1에는 "Peter", course2에는 "Lisa"가 출력된다. 이제 두 students를 완전히 독립된 공간에 할당해 주었기 때문에 문제가 발생하지 않는 것이다.
students에 new를 이용해 다시 다른 공간에 동적 할당 해준다. 그리고 numberOfStudents만큼 반복문을 돌리며 혹시 원본에 들어있던 students가 있다면 그 "값만" 그대로 새로 할당한 공간에 복사해온다. 이렇게하면 둘은 완전 다른 공간을 갖게 되는 것이다.
배운 내용 정리
- 복사 생성자는 클래스에서 다른 객체의 데이터로 초기화를 할 때 사용되는 생성자이다.
- 복사 생성자를 따로 정의하지 않으면 기본 복사 생성자를 가지며 얕은 복사로 데이터를 복사한다.
- 얕은 복사는 일반 변수를 복사하는데는 아무 문제 없지만 주소값을 가진 멤버 변수를 복사할 때는 주소값만 복사되어 원본 변수와 복사된 변수 모두 같은 곳을 참조하게 되어 문제가 발생할 수 있다.
- 깊은 복사는 개발자가 직접 복사 생성자를 만들어 주면 일어나게 할 수 있다.
- 사용자 정의 복사 생성자는 주소값을 가진 변수를 새로 동적 할당을 해주고 값만 복사해서 가져오게 한다.
'C++ > 게임 개발자를 위한 C++ 문법' 카테고리의 다른 글
스마트 포인터(Smart Pointer) (0) | 2025.01.03 |
---|---|
선형 탐색과 이진 탐색 (0) | 2025.01.02 |
배열 자체의 정적 선언과 요소의 개별적 동적 할당 (0) | 2024.12.26 |
다중 포함 방지 (0) | 2024.12.26 |
포인터의 개념과 사용 (0) | 2024.12.26 |