- copy constructor
- move constructor
- perfect forwarding
- 임시객체
- explicit, friend, static, mutable
※ C++ 클래스의 생성자 종류
- 기본생성자 (Default Constructor): 기본적으로 컴파일러가 생성해주는 생성자.
- 복사생성자 (Copy Constructor): 포인터를 멤버로 갖는 클래스는 깊은 복사(deep copy)를 수행한다.
- 이동생성자 (Move Constructor): r-value를 parameter로 갖는 생성자. (C++ 11에서 추가된 타입의 생성자)
※ 복사생성자 (Copy Constructor)
- 자신과 같은 클래스 타입의 다른 객체에 대해 참조를 인수로 전달받아 그 참조를 이용해 자신을 초기화 하는 방법
- 복사생성자는 새롭게 생성되는 객체가 원본객체와 같지만 완전한 독립성을 갖게 해준다.
(복사생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값의 복사이기 때문.)
※ 복사생성자 형식: Add(const Add& copy) : num1(copy.num1), num2(copy.num2) { ... }
※ default 복사생성자
- 복사생성자 정의가 없을 때 자동 삽입되며 이로 인해 멤버 간 복사(얕은 복사)시 복사생성자를 직접 정의할 필요가 없다.
- 멤버 대 멤버의 단순복사를 진행한다.
- 다만 필수로 복사생성자를 정의해야하는 경우가 있다. (깊은 복사)
int main() {
Position xpos;
Position ypos(xpos); // default 복사생성자의 멤버 대 멤버 복사(얕은 복사)
}
Position ypos(xpos)는 Position ypos = xpos와 동일하게 사용될 수 있다.
※ 얕은 복사의 문제점
위의 Person p2(p1)에서 default 복사생성자의 멤버 대 멤버 복사(얕은 복사)가 진행된다.
이때, ~Person 즉, 소멸자에서 delete [ ]name;를 할 때 문제점이 발생한다.
위의 코드의 과정을 보면,
- 얕은 복사의 결과로 하나의 문자열을 2개의 객체가 동시에 참조하게 된다.
- 위의 예시에서 delete [ ]name;으로 p2의 소멸자가 호출되면서 문자열을 소멸시킨다.
- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.
★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.
※깊은복사
- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법
- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.
위의 코드에서 아래 코드를 추가해주면 된다.
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장
즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.
§ 또 다른 예제
DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
// 자신만의 동적데이터공간 할당
mArray = new int[mSize];
// 다른 객체로부터의 데이터 복사
for (int i = 0; i < mSize; i++) {
mArray[i] = other.mArray[i];
}
}
※ 복사생성자 호출 시점: 객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.
1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때. [Person p2 = p1;]
2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때
3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.
- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)
2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시
3. 객체를 반환하지만 참조형으로 반환하지 않는경우
cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!
Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?
A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.
예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.
{
Person p1(5);
cout << "부모 객체 p1 생성" << endl;
}
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;
만약 실행되었다면 다음과 같이 출력이될 것이다.
부모의 생성자
부모 객체 p1 생성
부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자
하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.
이해를 위해 또 다른 예시를 들어 보겠다.
{
Person p1(5);
}
cout << "부모 객체 p1 생성" << endl;
이렇게 된다면 출력은 어떻게 될까?
이는 아래와 같다.
부모의 생성자
부모의 소멸자
부모 객체 p1 생성
즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.
cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.
※ 임시객체를 이용한 obj.func().func(); 연산
※ 반환시 만들어진 (임시)객체의 소멸
1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.
2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
또 다른 예시)
※ r-value 참조
r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에 Double Ampersand(&&)를 붙여 표기한다.
이런 더블 엡퍼센드를 r-value 참조자라 부른다.
사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다
프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.
/*---------------일반 변수--------------- */
void f(){
int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸
/*---------------r value 참조 변수--------------- */
void f(){
int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸
위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다
?. r-value 참조는 목적이 무엇인가 ?
1. 클래스의 객체 생성시 사용하는 이동생성자와 이동대입연산자의 인수로 사용.
2. perfect forwarding이라는, 함수 오버로딩의 개수를 줄이는데 사용
https://chan4im.tistory.com/20
☆Prev. C++, Modern C++의 변화
※ C++의 버전변화 C++98, C++03 ==> C++11 ==> C++14 ... C++11: 기존 C++98에서 람다 표현식을 지원한 것 C++14: 일반화된 함수반환 형식 유추를 지원한 것 ※ C++11/14 문법적 변경 사항 1. 초기화 리스트 및..
chan4im.tistory.com
※ 이동생성자 (Move Constructor) _ (feat. &&의 사용)
- 이동생성자가 호출되면 얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move) 방식으로 객체를 생성.
- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.
Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?
A. 아래 코드를 통해 설명하겠다.
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class Animal {
private:
char *name;
int age;
public:
Animal(int age_, const char* name_) {
age = age_;
name = new char[strlen(name_) + 1];
strcpy(name, name_);
}
Animal(const Animal & a) { //복사생성자에 의한 깊은복사
age = a.age;
name = new char[strlen(a.name) + 1];
strcpy(name, a.name);
cout << "복사생성자" << endl;
}
Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
age = a.age;
name = a.name;
cout << "이동생성자" << endl;
a.name = nullptr;
}
~Animal(){
cout <<"Destructor!!" << endl;
if(name)
delete [] name;
}
void changeName(const char *newName) {
strcpy(name, newName);
}
void printAnimal() {
cout << "Name: " << name << " Age: " << age << endl;
}
};
int main() {
Animal A(10, "Jenny");
A.printAnimal();
vector<Animal> vec; // Animal 벡터 타입
// Animal 벡터 객체 삽입
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
A.printAnimal();
for (int i = 0; i < 5; i++) {
vec[i].printAnimal();
}
return 0;
}
복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.
이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.
[이동생성자 사용 시 주의할 점]
1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)
2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함
3, 소멸자에서 메모리할당 관련 변수가 nullptr인지 확인해 줘야함.
ex-2) 예제
또 다른 설명)
방법: 이동 생성자 정의 및 할당 연산자 이동(C++)
자세한 정보: 생성자 이동 및 할당 연산자 이동(C++)
learn.microsoft.com
※ Perfect Forwarding
#include <iostream>
using namespace std;
class Widget {
public:
Widget(int&, int&){ }
};
class X_Widget {
public:
X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
Z_Widget(const int&, const int&){ }
};
// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A& a, B& b) {
return new T(a, b);
}
int main() {
int a = 4, b = 5;
Widget *w = factory<Widget>(a,b);
// 아래부터는 모든 타입을 수용하는 함수템플릿을 만들지 않는 이상 Err 발생
X_Widget *x = factory<X_Widget>(2, b);
Y_Widget *y = factory<Y_Widget>(a, 2);
Z_Widget *z = factory<Z_Widget>(2, 2);
delete w; delete x; delete y; delete z;
}
따라서 결과적으로 참조타입별 함수오버로딩이 증가하는 문제를 해결하는 방법으로 다음과 같이 r value참조를 사용한다.
#include <iostream>
using namespace std;
class Widget {
public:
Widget(int&, int&){ }
};
class X_Widget {
public:
X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
Z_Widget(const int&, const int&){ }
};
// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A&& a, B&& b) {
return new T(a, b);
// return new T(std::forward<A>(a), std::forward<B>(b));
}
int main() {
int a = 4, b = 5;
Widget *w = factory<Widget>(a,b);
X_Widget *x = factory<X_Widget>(2, b);
Y_Widget *y = factory<Y_Widget>(a, 2);
Z_Widget *z = factory<Z_Widget>(2, 2);
delete w; delete x; delete y; delete z;
}
※ explicit: 복사생성자의 묵시적변환
- Add add2 = add1;을 Add add2(add1); 과 같은 방식을 허용하지 않는 방법
explicit Add(const Add& copy) : num1(copy.num1), num2(copy.num2){
//empty
}
그렇다면 이런 explicit의 이점은 무엇일까?
explicit을 사용함으로 인해 컴파일러의 자동 형변환을 막을 수 있다.
이는 내부 함수, 템플릿 구현에 탁월한데 사용자가 원하지 않는 형변환이 발생하는 등의
예상할 수 없는 버그가 발생할 수 있어서 explicit으로 사용자가 상황에 맞게 직접 형변환을 해주도록 한다.
※ friend 선언: private에 접근을 허용하는 선언(남용X)
1. A클래스가 B클래스를 대상으로 friend 선언을 하면 B는 A의 private멤버에 직접 접근이 가능
2. 단, A도 B의 private에 직접접근을 하려면 B가 A를 대상으로 friend선언을 해줘야 함.
※ static 멤버변수 (class 변수)
- 메모리 공간에 딱! 하나만 할당되며 모든 객체가 static 멤버변수를 공유할 수 있다.
- 단, 생성자에서 static 멤버변수를 초기화 할 수 없다! (객체 생성될 때 마다 초기화 되기 때문)
따라서 아래와 같이 static 변수 초기화 문법은 정해져 있다.
class Position{
private:
static int num;
public:
Position(){ num++; }
};
int Position::num = 0; //static변수 초기화
※ static 멤버함수
- 선언된 클래스의 모든 객체가 공유
- public 선언 시 클래스 이름으로 호출 가능
- static 멤버함수내에서는 static 멤버변수와 static 멤버함수만 호출 가능하다.
※ const static 멤버
- const 멤버변수의 초기화는 initializer를 이용해야 한다.
- const static 멤버변수의 초기화는 선언과 동시에 초기화가 가능하다!
class Country {
public:
const static int Korea = 82;
};
int main() {
cout << "국번: " << Country::Korea << endl;
}
※ mutable
- const 함수 내의 값 변경을 예외적으로 허용해주는 키워드.
※ [Effective C++_item 5]: C++이 은근슬쩍 만들어 호출해버리는 함수에 집중하자!
생성자: 새로운 객체를 메모리에 만들거나 객체의 초기화를 맡는 함수
소멸자: 객체를 없앰과 동시에 메모리에서 그 객체를 사라지게 하는 함수
대입 연산자: 기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수
직접 클래스안에 선언하지 않더라도 C++ 컴파일러가 저절로 선언해주는 멤버함수가 있다.
복사생성자, 복사대입연산자, 소멸자로 이때 컴파일러가 만드는 형태는 모두 기본형이다.
이들은 모두 public 멤버이며 inline 함수이다.(Effective C++ item 30 참조)
이때, 기본클래스의 소멸자가 가상소멸자로 되어있지 않으면 비가상 소멸자로 만들어진다.(Effective C++ item 7 참조)
컴파일러가 만들어낸 복사생성자와 복사대입연산자의 경우, 하는 일이 매우 단순하다.
-> 원본 객체의 비정적 데이터를 사본 객체로 그냥 복사하는 것이 전부이다.
복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법으로 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해주며
예를 들면, Book 클래스의 복사 생성자의 원형은 Book(const Book&) 와 같다.
※ [Effective C++_item 6]: 컴파일러가 만든 함수가 필요없으면 이들의 사용을 금하자!
컴파일러가 생성하는 함수(모두 public): 복사생성자 / 복사 대입 연산자 / 소멸자
객체의 사본을 만드는 것을 원천 차단하기 위한 방법(2가지)
방법1)
1. 컴파일러 생성함수는 public이기에 이를 private 선언 해준다.
2. 멤버함수, friend선언을 통해 접근이 가능 => 정의(define)를 하지 않는다.
방법 2)
1. 복사생성자와 복사대입연산자를 private으로 선언
2. Sale 자체가 아닌 별도의 기본 클래스에 넣고 이것으로 부터 Sale을 파생
(다만, Uncopyable클래스가 기본 클래스이기에 이 기법은 다중상속으로 갈 가능성이 존재한다.)
'C | C++ > C++' 카테고리의 다른 글
this.code(6)_ operator overloading_part1. (0) | 2022.10.25 |
---|---|
★★this.code(5)_ 상속, 가상함수(동적바인딩 / 오버라이딩 / 추상클래스), 다형성, 다중상속 (0) | 2022.10.25 |
this.code(3)_ class, 객체생성과 소멸, *this (0) | 2022.10.25 |
this.code(2)_ const, constexpr (0) | 2022.10.25 |
this.code(1)_ C++, based by C (0) | 2022.10.21 |