728x90
728x90
결과적으로 같은 초기화 방식
//C스타일
int num=20;
int &ref=num;
//C++스타일
int num(20);
int &ref(num);

 

객체의 생성
class SomSimple
{
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2):num1(n1), num(n2){}
	void ShowSimpleData()
	{
		cout<<num1<<endl;
		cout<<num2<<endl;
	}
};
int main(void)
{
	SoSimple sim1(15, 20);
	SoSimple sim2=sim1; 
    //sim2 객체를 새로 생성하여 객체 sim1과 sim2간의 멤버 대 멤버 복사가 일어난다.
	sim2.ShowSimpleData();
	return 0;
}

 

C++의 모든 객체는 생성자의 호출을 동반한다. 

SoSimple sim2(sim1);


SomSimple형 객체를 생성 -> 객체의 이름을 sim2로 지정 ->
sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체 생성을 완료 

 

복사 생성자 예제

복사생성자의 호출시점을 잘 이해해야 한다.
멤버 대 멤버 복사에 사용되는 원본의 변형 방지를 위해 키워드 const를 삽입한다.

// ClassInit.cpp
#include <iostream>
using namespace std;

class SoSimple{
private:
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2)
        :num1(n1), num2(n2)
    {
        // empty
    }
    
    // 복사생성자
    SoSimple(SoSimple &copy) 
        :num1(copy.num1), num2(copy.num2) 
    {
        // SoSimple객체를 인자로 받는 생성자
        // 이니셜라이저를 이용해 멤버 대 멤버 복사 
        cout<<"Called SomSimple(SoSimple &copy)"<<endl;
    }
    void ShowSimpleData(){
        cout<<num1<<endl;
        cout<<num2<<endl;
    }
};

int main()
{
    SoSimple sim1(15, 30);
    cout<<"생성 및 초기화 직전"<<endl;
    SoSimple sim2=sim1; //SoSimple sim2(sim1);으로 변환
    cout<<"생성 및 초기화 직후"<<endl;
    sim2.ShowSimpleData(); 
    return 0;
}

 

 

디폴트 복사 생성자

복사 생성자 없이도 오류가 일어나지 않는다. 
복사 생성자를 정의하지 않으면 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 들어간다.

아래 두 클래스 정의는 동일하다.

class SoSimple{
private:
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2)
        :num1(n1), num2(n2)
    {     }
};
class SoSimple{
private:
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2)
        :num1(n1), num2(n2)
    {     }
};    
     
    SoSimple(SoSimple &copy) 
        :num1(copy.num1), num2(copy.num2) 
    {     }
};

 

반드시 복사생성자를 정의해야 하는 경우도 있다.(나중에 설명)

 

 

변환에 의한 초기화 키워드 explicit로 막기

묵시적변환이 일어나서 복사생성자가 호출되는데, 복사생성자의 묵시적 호출을 막는 방법이다.
연산자를 이용한 객체의 생성 및 초기화가 불가능하다.

 explicit SoSimple(const SoSimple &copy)
 	:num1(copy.num1), num2(copy.num2)
{     }

묵시적변환이 많이 발생하면 코드의 결과를 예측하기 어렵기 때문에 explicit 키워드로 명확하게 코드를 하기 위해 자주 사용한다.

복사생성자 외에도 전달 인자가 하나인 생성자가 있어도 묵시적변환이 발생한다.

class A{
private:
	int num;
public:
	AAA(int n):num(n){    }
	. . . . . .
};

explicit 키워드가 생성자에 선언되면, 다음의 형태로 객체를 생성할 수 밖에 없다.

AAA obj(3);

복사생성자의 매개변수 선언에 const는 필수가 아니지만 참조형의 선언을 의미하는 &는 반드시 삽입해야 한다.
& 선언이 없다면 복사생성자 호출은 무한루프에 빠져버려 컴파일러가 미리 에러를 출력한다.

 

 

깊은 복사, 얕은 복사

디폴트 복사생성자에 의한 멤버 대 멤버 복사는 얕은 복사(Shallow copy)로 멤버변수가 힙에 메모리 공간을 참조하는 경우 문제가 된다.

디폴트 복사 생성자의 문제점
// ShallowCopyError.cpp
// 디버그 모드로 컴파일, 실행해야 함
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;
class Person
{
private:
	char* name;
	int age;
public:
	Person(const char* myname, int myage) {
		int len = strlen(myname) + 1;
		name = new char[len];
		strcpy(name, myname);
		age = myage;
	}
	void ShowPersonInfo() const {
		cout << "이름 : " << name << endl;
		cout << "나이 : " << age<< endl;
	}
	~Person() {
		delete []name;
		cout << "소멸자 호출!" << age << endl;
	}
};
int main(void) {
	Person man1("Lee", 29);
	Person man2 = man1;
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

(좌) 경고문 (우) 얕은복사의 결과

 

man2가 생성되면서 디폴트 복사 생성자 호출되고, 디폴트 복사 생성자는 멤버 대 멤버의 단순 복사를 하여 하나의 문자열을 두 객체가 참조하는 형태이기 때문에 객체 소멸과정에서 문제 발생

delete []name;

man1객체의 소멸자가 실행되어 소멸되어야 하는데 참조문자열이 man2 소멸자가 호출되며 소멸되어 문제가 된다.

 

깊은 복사(deep copy)를 위한 복사생성자의 정의

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

Person(const Person& copy):age(copy.age)
{
	name=new char[strlen(copy.name)+1];
	strcpy(name, copy.name);
}

위 생성자를 얕은복사 예제에 추가해준다.
멤버변수 age의 멤버 대 멤버 복사,메모리 공간 할당 후 문자열 복사, 할당된 메모리의 주소값을 멤버 name에 저장한다.

복사생성자를 별도로 정의하는 경우 (깊은 복사)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <memory>

using namespace std;
class Person
{
private:
	char* name;
	int age;
public:
	Person(const char* myname, int myage)
		: name(NULL)
		, age(0)
	{
		int len = strlen(myname) + 1;
		name = new char[len];
		memset(name,len,NULL);
		strcpy(name, myname);
		age = myage;
	}
	// deep copy
	Person(const Person& copy) :age(copy.age)
	{
		name = new char[strlen(copy.name) + 1];
		strcpy(name, copy.name);
	}
	void ShowPersonInfo() const {
		cout << "이름 : " << name << endl;
		cout << "나이 : " << age<< endl;
	}
	~Person() {
		delete []name;
		name = nullptr;
		cout << "소멸자 호출!" << age << endl;
	}
};
int main(void) {
	Person man1("Lee", 29);
	Person man2 = man1;
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

 

복사 생성자의 호출시점 (3가지)

1. 기존에 생성된 객체를 이용해서 새로운 객체를 초기화
2. Call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우
3. 객체를 반환하되, 참조형으로 반환하지 않는 경우

* 공통점 : 객체를 새로 생성하면서 동시에 동일한 자료형의 객체로 초기화해야한다.

복사 생성자의 호출횟수는 프로그램의 성능과도 관련있다.


1. 기존에 생성된 객체를 이용해서 새로운 객체를 초기화

메모리공간이 할당과 동시에 초기화되는 대표적인 상황은 다음과 같다.

int num1=num2 //num1라는 이름의 메모리공간을 할당과 동시에 num2에 저장된 값으로 초기화

 

2. Call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우

다음에서도 할당과 동시에 초기화가 일어난다.

int SimpleFunc(int n)
{
	.....
}
int main(void)
{
	int num=10;
	SimpleFunc(num); //호출되는 순간 매개변수 n이 할당과 동시에 초기화
}

SimpleFunc 함수가 호출되는 순간에 매개변수 n이 할당과 동시에 변수 num에 저장되어 있는 값으로 초기화된다.
매개변수도 함수가 호출되는 순간에 할당되므로 메모리공간의 할당과 초기화가 동시에 일어나는 상황이다.

 

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

다음의 경우에도 메모리 공간이 할당되면서 동시에 초기화가 이뤄진다. 
값을 반환하면 반환된 값은 별도의 메모리 공간이 할당되어서 저장된다.

int SimpleFunc(int n)
{
	.....
	return n; //반환하는 순간 메모리공간이 할당되면서 동시에 초기화
}
int main(void)
{
	int num=10;
	cout<<SimpleFunc(num)<<endl; //반환되는 값을 메모리 공간에 저장해뒀기에 출력도 가능하다.
	.....
}

출력을 위해서는 값을 참조할 수 있어야 하고, 참조가 가능하려면 메모리 공간에 저장되어있어야 된다.
함수가 값을 반환하면 별도의 메모리 공간이 할당되고, 이 공간에 반환값이 저장된다. (반환값으로 초기화된다.)

 

* 변수 말고 객체, 함수도 마찬가지

객체도 마찬가지로 객체가 생성되면서 (메모리공간이 할당되면서)초기화가 이뤄진다.
함수호출도 마찬가지로 객체가 생성되면서 전달되는 인자로 초기화된다.

SoSimple obj2=obj1;
SoSimple SimpleFuncObj(SoSimple ob)
{
 .....
}
int main(void)
{
	SoSimple obj;
	SimpleFuncObj(obj);
	.....
}

SimpleFuncObjc 함수가 호출되는 순간, 매개변수로 선언된 ob 객체가 생성되고(메모리공간이 할당) 
인자로 전달된 obj 객체로 초기화된다.(메모리 공간이 할당되면서 동시에 초기화)

SoSimple SimpleFuncObj(SoSimple ob)
{
 	.....
	return ob; //반환하는 순간 메모리 공간이 할당되면서 동시에 초기화
	// return문이 실행되는 순간 SoSimple 객체를 위한 메모리공간이 할당되고,
	// 이 공간에 할당된 객체는 반환되는 객체 ob의 내용으로 초기화
}

 

 

할당 이후, 복사생성자를 통한 초기화

객체가 생성 및 초기화될 때 초기화는 멤버 대 멤버가 복사되는 형태로 이뤄져야 하므로 복사생성자를 호출하는 방식으로 초기화를 진행한다.

#include <iostream>
using namespace std;

class SoSimple
{
private:
    int num;
public:
    SoSimple(int n)
        :num(n)
    { }
    SoSimple(const SoSimple& copy) 
        : num(copy.num)
    { 
        cout<<"Called SoSimple(const SoSimple& copy)"<<endl; 
    }
    void ShowData()
    {
        cout<<"num: "<<num<<endl;
    }
};
void SimpleFuncObj(SoSimple ob)
{
    ob.ShowData();
}
int main()
{
    SoSimple obj(7);
    cout<<"함수 호출 전"<<endl;
    SimpleFuncObj(obj); //함수를 호출하면서 객체 obj를 인자로 전달
    // 매개변수 ob의 복사 생성자가 호출되면서 인자로 obj가 전달된다
    
    cout<<"함수 호출 후"<<endl;
    return 0;
}

 

Q. 복사 생성자의 호출주체는 어느쪽일까?
obj 객체의 복사 생성자 호출 vs ob 객체의 복사 생성자 호출
A. ob객체는 obj 객체로 초기화된다. ob객체의 복사 생성자가 호출되면서 obj 객체가 인자로 전달되어야 한다.

 

 

복사생성자가 호출되는 세 번째 경우

* Point : 임시객체의 존재 알아보기

#include <iostream>
using namespace std;

class SoSimple
{
private:
    int num;
public:
    SoSimple(int n)
        :num(n)
    { }
    SoSimple(const SoSimple& copy) 
        : num(copy.num)
    { 
        cout<<"Called SoSimple(const SoSimple& copy)"<<endl; 
    }
    SoSimple& AddNum(int n) //참조형을 반환하는 함수
    {
        num+=n;
        return *this; //문장을 실행하는 객체 자신을 반환, 참조값을 반환
    }
    void ShowData()
    {
        cout<<"num: "<<num<<endl;
    }
};

SoSimple SimpleFuncObj(SoSimple ob) 
{ 
	cout<<"return 이전"<<endl;
	return ob; // 반환형이 참조형이 아니어서 객체의 복사본이 만들어지면서 반환이 진행	
	
	// 지역적으로 선언된 객체 ob는 소멸되고 obj객체와 임시객체만 남는다
	// obj객체가 전달되며 ob의 복사생성자가 호출되어 ob객체가 생기고
	// ob객체가 전달되며 임시객체의 복사생성자가 호출된다
} 

/** 
* @mainpage 메인페이지
* @brief 232쪽 프로그램 실행
* @details 임시객체의 존재 확인하기
*/
int main() {
	SoSimple obj(7); 
	SimpleFuncObj(obj).AddNum(30).ShowData(); //임시객체에 저장한 값 
	obj.ShowData(); //obj 객체에 저장된 값

	return 0; 
}

 

반환형이 참조형이 아니어서 객체의 복사본이 만들어지면서 반환이 진행되는데
지역적으로 선언된 객체 ob는 소멸되고 obj객체와 임시객체만 남는다.

obj객체가 전달되며 ob의 복사생성자가 호출되어 ob객체가 생기고
ob객체가 전달되며 임시객체의 복사생성자가 호출된다

호출결과로 반환된 임시객체를 대상으로 AddNum함수를 호출하고, (임시객체의 num은 37이 된다)
ShowData함수를 호출한다.

 

 

# 임시 변수 (출처)
 c++에서는 리턴값이 있을때 리턴값을 저장하기 위해 메모리 상에 임시 변수가 생성된다.
 단! 리턴값이 없다면 임시변수는 생성되지 않는다.
 또한! 함수 선언이 끝나게 된다면 임시변수 역시 메모리 상에서 사라진다.  

 

 

반환할 때 만들어진 객체가 사라지는 시점

임시객체도 임시변수처럼 임시로 생성됐다 소멸되는 객체이다.
임시객체는 임의로 만들 수 있다.

#include <iostream>
using namespace std;

class Temporary
{
private:
    int num;
public:
    Temporary(int n)
        :num(n){
        cout<<"create obj:"<<num<<endl;
    }
    ~Temporary()
    {
        cout<<"destroy obj:"<<num<<endl;
    }
    void ShowTempInfo()
    {
        cout<<"My num is:"<<num<<endl;
    }
};

int main()
{
    Temporary(100);//임시객체를 직접 생성
    // 100으로 초기화된 Temporary 임시객체가 생성된다.
    cout << "********* after make!" << endl << endl;

    Temporary(200).ShowTempInfo();// 임시객체를 생성하고, 임시객체를 대상으로 ShowTempInfo 함수를 호출한다
    // 객체가 생성 및 반환되면 생성 및 반환된 위치에 객체를 참조할 수 있는 참조 값이 반환되기 때문에 이런 문장이 가능 (후에 이야기)
    cout << "********* after make2!" << endl << endl;

    const Temporary& ref = Temporary(300); // 임시객체를 생성
    // 참조자 ref로 임시객체를 참조하고 있다
    cout << "********* end of main!" << endl << endl;
    
    return 0;
}

 

 

클래스 외부에서 객체의 멤버함수를 호출하기 위해 필요한 것은 3가지 중 하나이다.

1) 객체에 붙여진 이름
2) 객체의 참조 값 (객체 참조에 사용되는 정보)
3) 객체의 주소 값

임시객체가 생성된 위치에는 임시객체의 참조 값이 반환된다.
Temporary(200).ShowTempInfo(); 는 (임시객체의 참조값).ShowTempInfo(); 로 변환되는 것이다.
참조값이 반환되기 때문에 멤버함수의 호출이 가능하며 다음 문장도 가능하다.

 

 const Temporary& ref = Temporary(300); // 임시객체를 생성

 

임시객체 생성 시 반환되는 참조값이 참조자 ref에 전달되어 ref가 임시객체를 참조하게된다
임시객체는 메모리 공간에 있고, 임시객체의 참조값이 반환되어 일을 처리한다.

(접근이 불가능하게 된)임시객체는 다음 행으로 넘어가면 바로 소멸된다. 소멸자를 호출한다.
참조자에 참조되는 임시객체는 바로 소멸되지 않는다. 

 

반환할 때 만들어지는 임시객체의 소멸시기 확인 예제

(분리컴파일을 익숙하게 하려고 예제를 분리해서 작업하고 있습니다.)

// SoSimple.h
#pragma once
#include <iostream>
using namespace std;

class SoSimple
{
public:
	SoSimple(int n);
	SoSimple(const SoSimple& copy);
	~SoSimple();
private:
	int num;
};
// SoSimple.cpp
#include "SoSImple.h"

SoSimple::SoSimple(int n) : num(n)
{
	cout << "New OBject: " << this << endl;
}
SoSimple::SoSimple(const SoSimple& copy) : num(copy.num)
{
	cout << "New Copy obj: " << this << endl;
}
SoSimple::~SoSimple()
{
	cout << "Destoy obj: " << this << endl;
}
// ReturnObjDeadTime.cpp 
#include <iostream>
#include "SoSImple.h"
using namespace std;

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout << "Param ADR: " << &ob << endl;
	return ob;
}

int main()
{
	SoSimple obj(7);
	SimpleFuncObj(obj);

	cout << endl;
	SoSimple tempRef = SimpleFuncObj(obj); 
	//추가로 새 객체를 생성하지 않고
	//반환되는 임시객체에 tempRef이름을 할당

	cout << "Return obj " << &tempRef << endl; 
	return 0;
}

 

 

obj 생성
함수호출로 매개변수 ob 생성
Param ADR 출력 - ob의 주소
반환으로 인한 임시객체 생성
매개변수 ob 소멸
반환으로 생성된 임시객체 소멸

함수호출로 매개변수 ob 생성
Param ADR 출력 - ob의 주소
반환으로 인한 임시객체 생성
매개변수 ob 소멸
새 객체가 생성되지 않고 임시객체에 할당됨을 알 수 있는 결과. 임시객체의 주소값과 같음
임시객체소멸
obj 소멸

cout << "Return obj " << &tempRef << endl; //임시객체의 주소값이 반환된다

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

v