2022. 4. 16. 02:03ㆍC++
C++ 스타일의 초기화
C++에서는 다음과 같은 방식으로 선언 및 초기화가 가능하다.
변수와 참조자.
//변수, 참조자 초기화
//1.변수, 참조자 초기화
int num(20); // 메모리에 int만큼의 공간을 할당하고 그 안에 20을 넣는다.
int &ref(num); // num이라는 공간에 ref라는 새로운 별명을 지어준다.
//2.대입연산자를 이용
int num = 20;
int &ref = num;
객체 초기화
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1),num2(n2)
{ }
void ShowSimepleDate()
{
cout<<num1<<endl;
cout<<num2<<endl;
}
};
int main(void)
{
SoSimple sim1(15,20);
SoSimple sim2 = sim1; //어떠한 형태의 대입 연산이 진행되겠는가?
// 위의 문장은 객체의 생성 및 초기화를 연상시킨다.
//sim2 객체를 새로 생성해서, 객체 sim1과 sim2간이의 멤버 대 멤버 복사가 일어난다고 예상해볼 수 있다.
sim2.ShowSimepleDate(); //15, 20출력
return 0;
}
위의 main함수의 두 번째 문장에 해당하는 다음 문장은 객체의 생성 및 초기화를 연상시킨다.
SoSimple sim2 = sim1;
즉, sim2 객체를 새로 생성해서, 객체 sim1과 sim2간이의 멤버 대 멤버 복사가 일어난다고 예상해볼 수 있다.
실제로 아래의 그림에서 볼 수 있듯이 sim2 객체가 생성된 후 sim1가 sim2간의 멤버 대 멤버 복사가 일어난다.
따라서 다음 두 문장은 동일한 의미로 해석이된다.
SoSimple sim2 = sim1;
SoSimple sim2(sim1);
SoSimple sim2(sim1);
위 문장에 담겨져 있는 내용은 다음과 같다.
- SoSimple형 객체를 생성해라.
- 객체의 이름은 sim2로 정한다.
- sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체 생성을 완료한다.
위의 객체 생성문에서 호출하고자 하는 생성자는 다음과 같이 Sosimple 객체를 인자로 받을 수 있는 생성자이다.
SoSimpe(Sosimple ©)
{ . . . }
SoSimple sim2=sim1; // 이 문장이 아래의 문장으로
SoSimple sim2(sim1); // 묵시적변환(자동으로 변환)되어 객체가 생성된다.
하지만 앞서 정의한 SoSimple 클래스에는 이러한 유형의 생성자가 정의되어있지않다.
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1),num2(n2)
{ }
SoSimple(const SoSimple ©): num1(copy.num1), num2(copy.num2)
// 원본을 변경시키는 것을 방지하기위해 const를 삽입한다.
{
cout<<"Called SoSimple(SoSimple ©)"<<endl;
}
void ShowSimepleDate()
{
cout<<num1<<endl;
cout<<num2<<endl;
}
};
int main(void)
{
SoSimple sim1(15,20);
cout<<"생 성 및 초기화 직전"<<endl;
SoSimple sim2 = sim1; //SoSimple sim2(sim1); 으로 변환!
//Called SoSimple(SoSimple ©)
cout<<"생성 및 초기화 직후 "<<endl;
sim2.ShowSimepleDate();
return 0;
}
위의 코드는
SoSimple sim2 = sim1 가 SoSimple sim2(sim1)로 묵시적(자동)으로 변환된다는 사실만 제외하면 특별할 게 없다.
SoSimple(SoSimple ©) { }
위의 생성자를 가리켜 별도로 복사생성자라고 부르는 이유는 생성자가 호출되는 시점이 다른 일반 생성자와 차이가 있기 때문이다.
참고로 멤버 대 멤버의 복사에 사용되는 원본을 변경시키는 것은 복사의 개념을 무너뜨리는 행위가 발생하는 것이기 때문에 const를 삽입해 이러한 실수를 막는다.
디폴트 복사 생성자
복사 생성자를 정의하지 않으면, 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 삽입된다.
class SoSimple
{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1),num2(n2)
{ }
void ShowSimepleDate()
{
cout<<num1<<endl;
cout<<num2<<endl;
}
};
따라서 위의 클래스의 정의는 아래의 클래스 정의와 완전히 동일하다
class SoSimple
{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1),num2(n2)
{ }
SoSimple(SoSimple ©): num1(copy.num1), num2(copy.num2)
{
}
void ShowSimepleDate()
{
cout<<num1<<endl;
cout<<num2<<endl;
}
};
> 멤버 대 멤버의 복사가 진행되는 디폴트 복사 생성자가 자동으로 삽입되니까, 굳이 복사 생성자를 직접 정의할 필요는 없지않을까?
실제로, 많은 경우에 있어서 복사생성자를 직접 정의하지 않아도 된다. 그러나 반드시 복사 생성자를 정의해야 하는 경우가 있다.
디폴트 복사 생성자의 얕은복사!
디폴트 복사 생성자는 멤버 대 멤버의 복사를 진행하고, 이러한 방식의 복사를 가리켜 '얕은 복사'라 한다.
이는 멤버변수가 힙의 메모리 공간을 참조하는 경우에 문제가 된다.
디폴트 복사 생성자의 문제점
#include <iostream>
#include <cstring>
using namespace std;
class Person
{
public:
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<<"called destructor!"<<endl;
}
};
int main(void)
{
Person man1("Lee", 21);
Person man2 = man1; //묵시적 변환
man1.ShowPersonInfo();
man2.ShowPersonInfo();
//called destructor! 한번만 출력.
return 0;
}
위의 코드는 두 개의 객체를 생성했음에도 called destructor! 가 한번만 출력된다.
(물론 컴파일러의 종류, 설정 및 환경에 따라 아무런 문제도 일으키지 않고 두 번 출력될 수 있다. )
다음 문장에 의해 man2가 생성되면서 디폴트 복사 생성자가 호출된다.
Person man2=man1;
이는 다음과 같다.
우리는 아래와 같은 그림을 기대했을 것이다.
man2의 소멸자가 호출되면서 다음 문장도 실행이 된다.
delete[] name;
즉, 객체 man2의 소멸로 인해 다음과 같은 상황에 놓이게 된다.
man1 객체의 소별자에 포함되어 있는 다음 문장에서 문제가 된다.
delete[] name;
이미 지워진 문자열을 대상으로 delete 연산을 하기 때문에 문제가 된다.
복사생성자가 하는 일
Person(const Person ©):age(copy.age)
{
name = new char[strlen(copy.name)+1];
strcpy(name, copy.name);
}
따라서 위와 같은 복사생성자를 만든다고 가정한다면
복사생성자가 하는 일은 다음 두가지 이다.
- 멤버 변수 age의 멤버 대 멤버 복사를한다.
- 메모리 공간 할당 후 문자열 복사, 그리고 할당된 메모리 주소 값을 멤버 name에 저장한다.
복사 생성자 호출 시점
- 기존에 생성된 객체를 이용해 새로운 객체를 초기화하는 경우(앞서 말한 경우)
- Call-by-value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우
- 객체를 반환하되, 참조형으로 반환하지 않는 경우
메모리 공간의 할당과 초기화가 동시에 일어나는 상황
1.
int num1 = num2;
2.
int SimpleFunc(int n) //할당과 동시에 초기화가 이뤄진다.
{
}
int main(void)
{
int num=10;
SimpleFunc(num); //호출되는 순간 매개 변수 n이 할당과 동시에 초기화
}
3.
int SimpleFunc(int n)
{
return n; //반환하는 순간 메모리 공간이 할당되면서 동시에 초기화
}
int main(void)
{
int num=10;
cout<<SimpleFunc(num)<<endl;
}
> 함수가 값을 반환하면, 별도의 메모리 공간이 할당되고, 이 공간에 반환 값이 저장된다.
위의 상황은 객체를 대상으로 해도 달라지지 않는다.
1.
SoSimple obj2 = obj1; //ob1도 SoSimple 객체라 가정
//메모리 공간에 SoSimpe 만큼의 공간 할당, obj1으로 초기화
2.
SoSimple SimpleFuncObj(SoSimple ob)
{ }
int main(void)
{
SoSimple obj;
SimpleFuncObj(obj); //ob객체가 생성되면서(ob를 위한 메모리 공간이 할당되고)
} // 전달되는 인자로 초기화
3.
SoSimple SimpleFuncObj(SoSimple ob)
{
return ob; //반환하는 순간 SoSimple 객체를위한 메모리 공간이 할당되면서 동시에 객체 ob로 초기화!
}
할당 이후, 복사 생성자를 통한 초기화
- Call-by-value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우
객체의 초기화는 멤버 대 멤버가 복사되는 형태로 이뤄져야 한다.
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num1;
public:
SoSimple(int n1) : num1(n1)
{ }
SoSimple(const SoSimple ©): num1(copy.num1)
{
cout<<"Called SoSimple(SoSimple ©)"<<endl;
}
void ShowDate()
{
cout<<"num1: "<<num1<<endl;
}
};
void SimpleFuncObj(SoSimple ob)
{
ob.ShowDate();
}
int main(void)
{
SoSimple obj(21);
cout<<"함수호출 전"<<endl;
SimpleFuncObj(obj); //ob 객체가 생성되고 obj로 초기화
cout<<"함수호출 후"<<endl;
// 결과
// 함수호출 전
// Called SoSimple(SoSimple ©)
// num1: 21
// 함수호출 후
return 0;
}
아래는 복사 생성자의 호출 관계이다.
위의 그림에서 보이듯이 ob 객체가 obj 객체로 초기화 된다.
따라서 ob객체의 복사 생성자가 호출되면서 obj객체가 인자로 전달되어야 한다 .
- 객체를 반환하되, 참조형으로 반환하지 않는 경우
다음의 코드를 실행할때, 아래와 같이 나온다.
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num1;
public:
SoSimple(int n1) : num1(n1)
{ }
SoSimple(const SoSimple ©): num1(copy.num1)
{
cout<<"Called SoSimple(SoSimple ©)"<<endl;
}
SoSimple & AddNum(int n)
{
num1 += n;
return *this; //문장을 실행하는 객체자신을 반환하고 있다. 반환값이 참조형이니 참조 값이 반환된다.
}
void ShowDate()
{
cout<<"num1: "<<num1<<endl;
}
};
SoSimple SimpleFuncObj(SoSimple ob) // 인자 전달 과정에서 복사 생성자가 호출됨
{
cout<<"return 이전"<<endl;
return ob; //ob객체를 반환하고 있다. 반환형이 참조형이 아니므로 ob객체의 복사본이 만들어지면서 반환된다.
}
int main(void)
{
SoSimple obj(21);
SimpleFuncObj(obj).AddNum(30).ShowDate();
//SimpleFuncObj 함수가 반환한 객체를 대상으로 AddNum 함수를 호출하고
// 이어서 AddNum함수가 반환하는 참조 값을 대상으로 ShowDate함수를 호출하고 있다.
obj.ShowDate();
return 0;
}
결과
Called SoSimple(SoSimple ©)
return 이전
Called SoSimple(SoSimple ©)
num1: 51
num1: 21
'C++' 카테고리의 다른 글
깊은 복사 / 얕은 복사 (0) | 2022.04.18 |
---|---|
explicit (0) | 2022.04.18 |
학번, 중간성적, 기말성적 입력 프로그램 (0) | 2022.04.14 |
mutable (0) | 2022.04.10 |
static (0) | 2022.04.10 |