|5.5| 복사 생성자

● 얕은 복사와 깊은 복사 (shallow copy & deep copy)

Person 객체의 멤버 변수로 

int id;
char *name;

이 있다고 가정하자.

얕은 복사의 경우, name 포인터로 문자열 배열을 공유하기 때문에 사본 객체에서 name 문자열을 변경하면,
원본 객체의 name 문자열이 변경되는 문제가 발생한다.

깊은 복사는 원본의 name 포인터가 가리키는 메모리까지 복사하여 원본과 사본의 name은 별개의 메모리를 가리킨다.

=> 가능하면 얕은 복사가 일어나지 않도록 해야 한다.


● 복사 생성 및 복사 생성자



복사 생성자 선언


1
2
3
4
class ClassName {
    ClassName(ClassName& c); // 복사 생성자
};
 
cs



※ 복사 생성자의 매개변수는 오직 하나이며,

자기 클래스에 대한 참조로 선언된다.

또한, 복사 생성자는 클래스에 오직 한 개만 선언이 가능하다.


복사 생성자 실행


1
2
3
4
Circle src(30); // 일반적인 생성자 호출
 
Circle dest(src); /* src 객체를 복사하여 dest 객체 생성
                  복사 생성자 Circle(Circle& c) 호출 */
cs



※ 예시 : Circle 크래스의 복사 생성자와 객체 복사


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class Circle {
private:
    int radius; 
public:
    Circle(Circle& c); // 복사 생성자 선언
    Circle() { radius = 1; }
    Circle(int radius) { this->radius = radius; }
    double getArea() { return 3.14*radius*radius; }
}; 
 
Circle::Circle(Circle& c) { // 복사 생성자 구현
    this->radius = c.radius;
    cout << "복사 생성자 실행 radius = " << radius << endl;
}
 
int main() {
    Circle src(30); // src 객체의  보통 생성자 호출
    Circle dest(src); // dest 객체의 복사 생성자 호출
 
    cout << "원본의 면적 = " << src.getArea() << endl;
    cout << "사본의 면적 = " << dest.getArea() << endl;
}
 
cs



실행 결과 :

복사 생성자 실행 radius = 30

원본의 면적 = 2826

사본의 면적 = 2826



※ 만약 복사 생성자를 따로 정의하지 않는다면?


=> 컴파일러가 디폴트 복사 생성자(default copy constructor)를 묵시적으로 삽입한다.


※   하지만 default copy constructor 코드는 shallow copy를 실행하도록 만들어진 코드이다.

포인터 타입 변수가 없는 클래스의 경우, 얕은 복사는 전혀 문제가 없다.

" 공유 " 의 문제가 발생하지 않기 때문이다.


※ 예제 : 얕은 복사 생성자를 사용하여 문제가 생기는 경우 ( 프로그램이 비정상 종료 )



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define _CRT_SECURE_NO_WARNINGS //비주얼 스튜디오에서 strcpy로 인한 오류를 막기 위한 선언문
#include <iostream>
#include <cstring>
using namespace std;
 
class Person { // 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::~Person() {// 소멸자
    if(name) // 만일 name에 동적 할당된 배열이 있으면
        delete [] name; // 동적 할당 메모리 소멸
}
 
void Person::changeName(const char* name) { // 이름 변경
    if(strlen(name) > strlen(this->name))
        return// 현재 name에 할당된 메모리보다 긴 이름으로 바꿀 수 없다.
    strcpy(this->name, name);
}
 
int main() {
    Person father(1"Kitae");            // (1) father 객체 생성
    Person daughter(father);            // (2) daughter 객체 복사 생성. 복사 생성자 호출
 
    cout << "daughter 객체 생성 직후 ----" << endl;
    father.show();                        // (3) father 객체 출력
    daughter.show();                    // (3) daughter 객체 출력
 
    daughter.changeName("Grace");        // (4)     daughter의 이름을 "Grace"로 변경
    cout << "daughter 이름을 Grace로 변경한 후 ----" << endl;
    father.show();                        // (5) father 객체 출력
    daughter.show();                    // (5) daughter 객체 출력
 
    return 0;                            // (6), (7) daughter, father 객체 소멸
}
cs

  





※    main 함수 종료 시, daughter 객체가 먼저 소멸되고, father 객체의 소멸자가 name에 할당된 메모리를 힙에 반환하려고 할 때!!!

이미 반환한 메모리를 반환하게 되므로, 오류가 발생하고 프로그램이 비정상 종료된다.



※ 예시 : 깊은 복사 생성자를 가진 정상적인 Person 클래스




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#define _CRT_SECURE_NO_WARNINGS //비주얼 스튜디오에서 strcpy로 인한 오류를 막기 위한 선언문
#include <iostream>
#include <cstring>
using namespace std;
 
class Person { // Person 클래스 선언
    char* name;
    int id;
public:
    Person(int id, const char* name); // 생성자
    Person(Person& person); // 복사 생성자
    ~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::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;
}
 
Person::~Person() {// 소멸자
    if(name) // 만일 name에 동적 할당된 배열이 있으면
        delete [] name; // 동적 할당 메모리 소멸
}
 
void Person::changeName(const char* name) { // 이름 변경
    if(strlen(name) > strlen(this->name))
        return// 현재 name에 할당된 메모리보다 긴 이름으로 바꿀 수 없다.
    strcpy(this->name, name);
}
 
int main() {
    Person father(1"Kitae");            // (1) father 객체 생성
    Person daughter(father);            // (2) daughter 객체 복사 생성. 복사 생성자 호출
 
    cout << "daughter 객체 생성 직후 ----" << endl;
    father.show();                        // (3) father 객체 출력
    daughter.show();                    // (3) daughter 객체 출력
 
    daughter.changeName("Grace"); // (4)     // daughter의 이름을 "Grace"로 변경
    cout << "daughter 이름을 Grace로 변경한 후 ----" << endl;
    father.show();                        // (5) father 객체 출력
    daughter.show();                    // (5) daughter 객체 출력
 
    return 0;                                // (6), (7) daughter, father 객체 소멸
}
cs




● 묵시적 복사 생성 주의 상황들


1
Person daughter(father); // 복사 생성자를 명시적으로 호출
cs




1. 객체로 초기화하여 객체가 생성될 때


1
2
3
4
Person son = father;
// Person son(father);
// 이 묵시적으로 호출된다
 
cs



1
2
3
// 위와 헷갈리지 말자
Person = son;
son = father; // 복사 생성자 호출되지 않음
cs


=> '복사 생성' 과 '복사'의 차이 ★★★★ (성돈이한테 질문함)


2. 'call by value' 로 객체가 전달될 때


=> 앞에서 call by value로 매개 변수 객체가 생성될 때, 생성자가 실행되지 않는다고 하였는데,

생성자 대신에 복사 생성자가 실행된다.



3. 함수가 객체를 리턴할 때



1
2
3
4
5
6
Person g() {
    Person mather(2"Jane");
    return mother; /* mother의 복사본을 생성하여 복사본 리턴
                   사본이 만들어 질 떄 복사 생성자 호출*/
}
g();
cs



=> g()가 mother 객체를 리턴할 때, mother 객체의 복사본을 만들어 넘겨준다. 복사본을 만들 때 복사 생성자가 호출된다.






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

#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#6 함수 중복과 static 멤버  (0) 2018.10.05
#5-A 함수와 참조  (0) 2018.10.04

|5.1| 함수의 인자 전달 방식


1) call by value

2) call by address

3) call by reference ★



|5.2| 함수 호출시 객체 전달


 객체 전달시, call by value


1
2
3
4
5
6
7
8
9
10
11
void increase(Circle c) {
    int r = c.getRadius();
    c.setRadius(r + 1);
}
 
int main() {
    Circle waffle(30);
    increase(waffle);
    cout << waffle.getRadius() << endl;
}
 
cs


=>객체 전달시 call by value 로 전달된다 (30이 출력)

※ call by value는 실인자 객체의 크기가 크면 객체를 복사하는 시간이 커지는 단점이 있다.



● 객체의 전달시, 생성자와 소멸자의 실행 여부

-> 소멸자만 실행된다. 

1
2
3
4
5
int main() {
    Circle waffle(30);
    increase(waffle);
    cout << waffle.getRadius() << endl;
}
cs

실행 결과




● call by address로 전달한다면

1
2
3
4
5
6
7
8
9
10
void increase(Circle *p){
    int r = p->getRadius();
    p->setRadius(r+1);
}
 
int main(){
    Circle waffle(30);
    increase(&waffle);
    cout << waffle.getRadius() ;
}
cs


 -> 예상대로 31이 출력된다



※ call by address를 사용하면, 매개 변수가 단순 포인터이므로, call by value 시에 발생하는 생성자 소멸의 비대칭 문제가 없고, 복사 시간 소모가 없다.


|5.3| 객체 치환 및 객체 리턴


● 객체 치환

1
2
3
4
5
Circle c1(5);
Circle c2(30);
c1 = c2;    // c1과 c2의 내용은 완전히 같아지나,
            // 내용물만 같을 뿐, 별도의 다른 객체이다.
            // "객체 치환 은 동일한 클래스 타입에서만 적용된다.
cs



|5.4| 참조와 함수


※ C++에서 참조는 다음과 같이 활용된다.

1) 참조 변수
2) 참조에 의한 호출
3) 함수의 참조 리턴

1) 참조 변수

1
2
3
4
5
6
7
int n = 2;
int &refn = n; /* refn은 n의 별명
               참조 변수는 별도의 공간을 갖지 않고,
               원본 변수의 공간을 공유한다.*/
 
Circle circle;
Circle &refc = circle /* refc는 circle의 별명 */
cs


※ 참조 사항



1
int &refn; // 초기화가 없으면 에러가 난다.
cs


1
2
3
int &refn = n;
int & refn = n;
int& refn = n; // &의 위치는 무관하다

cs



2) cal by reference 


※  참조는 call by reference 에서 많이 사용된다.

매개변수가 실인자를 참조하여 실인자와 공간을

공유하도록 하는 인자 전달 방식이다.

매개변수를 reference parameter 라고 부른다.


※ 참조 매개 변수는 이름만 생성되므로, 생성자와 소멸자는

    실행되지 않는다.





1
2
3
4
void swap(int &a, int &b);
 
int m=2, n=9;
swap(m, n); 
cs


swap 함수에 대한 호출은 call by reference 이다.

( 함수의 원형으로 구분한다. )


     ※ 예제 : 참조 매개 변수로 평균 리턴하기



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
bool average(int a[], int sizeint& avg) {
    if(size <= 0)
        return false;
    int sum = 0;
    for(int i=0; i<size; i++
        sum += a[i];
    avg = sum/size;
    return true;
}
 
int main() {
    int x[] = {0,1,2,3,4,5};
    int avg;
    if(average(x, 6, avg)) cout << "평균은 " << avg << endl;
    else cout << "매개 변수 오류" << endl;
 
    if(average(x, -2, avg)) cout << "평균은 " << avg << endl;
    else cout << "매개 변수 오류 " << endl;
}
cs



3) 참조 리턴


1
2
3
4
5
6
7
8
9
10
11
12
13
char c = 'a';
 
char& find() {    // char 타입의 참조 리턴
    return c;    //변수 c에 대한 참조 리턴
}
 
char x = find(); // x = 'a'가 됨
 
char &ref = find();
 
ref = 'M';    // c = 'M'
 
find() = 'b'// c = 'b' 가 됨
cs



※ 참조 리턴 예제



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
 
// 배열 s의 index 원소 공간에 대한 참조 리턴하는 함수
char& find(char s[], int index) {
    return s[index]; // 참조 리턴
}
 
int main() {
    char name[] = "Mike";
    cout << name << endl;
 
    find(name, 0= 'S'// name[0]='S'로 변경
    cout << name << endl;
 
    char& ref = find(name, 2);
    ref = 't'// name = "Site"
    cout << name << endl;
}
 
cs


★★★ 참조 리턴은 연산자 함수에서 대표적으로 사용된다.





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

#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#6 함수 중복과 static 멤버  (0) 2018.10.05
#5-B 복사 생성자  (0) 2018.10.05
#include
#include
#include
#include
using namespace std;

vector list[20001];
int color[20001];

void dfs(int node, int c) {
	color[node] = c;
	for (int i = 0; i < list[node].size(); i++) {
		int next = list[node][i]; // next는 node와 연결되어있는 정점 중 가장 앞 숫자
		if (color[next] == 0) { // next가 아직 A/B 중 어디도 포함되지 않을 경우
			dfs(next, 3 - c); // next를 node와 반대 팀으로 배정한 후, 이동!
		}
	}
}

int main() {
	int T;
	cin >> T;
	

	while (T--) {

		int node, edge;
		cin >> node >> edge;


		for (int i = 0; i < edge; i++) { // list 작성
			int u, v;
			cin >> u >> v;
			list[u].push_back(v);
			list[v].push_back(u);
		}

		for (int i = 1; i <= node; i++) { // 전체 노드를 탐색
			if (color[i] == 0) {	// 노드 i의 색이 배정되어 있지 않으면,
				dfs(i, 1);			// i번째노트부터 dfs 탐색
			}
		}

		bool ok = true;

		for (int i = 1; i <= node; i++) {	// 1부터 끝까지 노드 탐색
			for (int k = 0; k < list[i].size(); k++) {	// 해당 노드에 연결된 모든 정점 탐색
				int j = list[i][k];
				if (color[i] == color[j]) {		// 만약 i번째 노드와 연결된 노드가 색이 같으면
					ok = false;					// ok를 false 로!
				}
			}
		}
		
		if (ok == false) cout << "NO" << endl;
		else cout << "YES" << endl;

		for (int i = 0; i <= node; i++) { 			// ★★★계속 index실수함.. node는 1 <= i <= node 이렇게!!
			list[i].clear();
			color[i] = 0;
		}

	}
	return 0;

}


dfs자체를 bool을 리턴하게 하는 풀이도 참조하장☆