- 유도 클래스의 대입 연산자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다.
유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연산자 호출문을 삽입하지 않으면, 기초 클래스의 대입 연산자는 호출되지 않아서, 기초 클래스의 멤버변수는 멤버 대 멤버의 복사 대상에서 제외된다.
class First
{
private:
int num1, num2;
public:
. . .
First& operator=(const First& ref)
{
num1 = ref.num1;
num2 = ref.num2;
return *this;
}
. . .
}
class Second : public First
{
private:
int num3, num4;
Second& operator=(const Second& ref)
{
First::operator=(ref); // 기초 클래스의 대입 연산자 호출을 명령
num3 = ref.num3;
num4 = ref.num4;
return *this;
}
}
- 이니셜라이저를 이용하면, 선언과 동시에 초기화가 이루어지는 형태로 바이너리 코드가 생성된다. 이 과정에서 초기화의 과정을 단순화 시키고, 성능향상을 기대할 수 있다.
- C, C++의 기본 배열은 '경계검사를 하지 않는다'라는 단점이 있다. 이러한 단점 또한, 연산자 오버로딩을 통해, 어느정도 해결이 가능하다. 즉, [] 또한 연산자이고, 이 [] 연산자의 오버로딩을 통해 안전하지 않은 코드는 사전에 차단할 수 있다.
int& operator[] (int idx)
{
// 조건문
}
- const의 선언유무도 함수 오버로딩의 조건에 해당한다. 가령, 위와 같이 배열 오버로딩을 한 경우, 만일 클래스에 ShowAllData와 같이 배열 내의 모든 숫자를 보여주는 멤버함수가 있다고 가정하자,
그 경우, 해당 함수는 const로 선언 될 것이다. (const로 선언하지 말아야 할 이유가 없다.) 이 경우 배열 오버로딩이 된 객체는 const를 참조할 수 없고, 따라서 ShowAllData를 호출하면 컴파일 에러가 발생한다. - 이러한 경우를 방지하기 위해, 객체함수로 const 선언을 한 연산자 오버로딩 함수를 다시금 오버로딩 하여 호출하면, 컴파일 에러가 발생하지 않는다.
- 추가적으로, 배열을 오버로딩 할 때, 주소 값을 저장하는 경우 깊은 복사 혹은 얕은 복사를 신경쓰지 않아도 된다.
New/Delete
- New와 Delete는 엄연한 연산자이다. 따라서 둘 모두 연산자 오버로딩이 가능하다.
- New의 역할은 다음과 같다.
- 메모리 공간의 할당
- 생성자의 호출
- 할당하고자 하는 자료형에 맞게 반환된 주소 값의 형변환
- 따라서, 자료형을 사전에 정의해야 하는 malloc와 달리, new는 이러한 과정을 알아서 처리해준다.
또한 이 과정에서, 메모리 공간의 할당만 오버로딩이 가능하다.
- New의 역할은 다음과 같다.
void * operator new (size_t size) { ... }
- 할당 형식은 위와 같고, 조건은 다음과 같다.
- 반환형이 반드시 void 형일 것
- 매개변수형은 size_t일 것, 이때 크기정보는 바이트 단위로 계산되어야 한다.
- delete 연산자는 다음과 같이 오버로딩이 가능하다.
void operator delete(void * adr)
{
delete adr;
}
- operator new 와 operator delete는 static으로 선언된 함수이다. 따라서 이들은 다음과 같이 객체 생성 과정에서 호출이 가능하다.
Point * ptr = new Point(10,20);
포인터 연산자
- 포인터를 기반으로 하는 모든 연산자를 포인터 연산자라 하며, 대표적인 포인터 연산자는 다음과 같다.\
- -> 포인터가 가리키는 객체의 멤버에 접근
- * 포인터가 가리키는 객체에 접근
- 이러한 연산자는 일반적인 연산자의 오버로딩과 크게 차이는 없지만, 한가지 차이점이 있다.
그것은 둘 다 피 연산자가 하나인 단항 연산자의 형태로 오버로딩 된다는 특징을 가진다.
ClassName * operator->()
{
return this;
}
ClassName & operator*()
{
return *this;
}
- 만일 ClassName이라는 class 내부에, ShowData라는 멤버함수가 있다고 가정하자.
ClassName tmp(20);
tmp.ShowData();
(*tmp)=30;
tmp->ShowData();
(*tmp).ShowData();
return 0;
- 그렇다면 위 코드에서 (* tmp) = 30과 (* tmp).ShowData() 는 다음과 같이 해석된다.
- (tmp.operator*()) = 30
- (tmp.operator*()).ShowData()
- 다만, 다음 포인터 연산자의 경우 위와 같이 해석되면
- (tmp.operator*())->() ShowData;
- 와 같이 해석이 될 것이다. 하지만 operator()->() 가 반환하는 것은 주소값이므로, 문법적으로 성립하지 않는다.
따라서, 다음과 같이 해석되는 것이 올바르다.- (tmp.operator*())->()->ShowData();
스마트 포인터(smartPointer)
- 스마트 포인터는 객체이다. 즉, 포인터의 역할을 하는 객체인 것이다.
- 역할은 간단하다. new 키워드를 사용하여 동적으로 할당받은 메모리는 당연히 delete 키워드를 사용하여 해제하여야 한다. 그렇지 않으면 메모리 누수가 발생하여 시스템에 악영향을 끼친다. 동작은 다음과 같다.
- 먼저 new 연산자가 다음과 같은 과정을 통해 포인터를 생성한다.
- 메모리 공간의 할당 -> raw pointer 가 실제 메모리를 가리키도록 초기화한다.
- 생성자의 호출
- 할당하고자 하는 자료형에 맞게 반환된 주소 값의 형변환
- 후에, 기본 포인터에 스마트 포인터를 대입하여 사용한다.
- 물론, 이러한 과정을 기본 클래스 생성 후 해당 클래스의 상속을 받는 스마트포인터 유도클래스를 생성하여 사용하는 방법도 있지만, C++에서는 다음과 같은 스마트 포인터를 제공한다.
- unique_ptr
- 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록 객체에 소유권 개념을 도입한 스마트 포인터
- unique_ptr
unique_ptr<Song> SongFactory(const std::wstring& artist, const std::wstring& title)
{
// Implicit move operation into the variable that stores the result.
return make_unique<Song>(artist, title);
}
void MakeSongs()
{
// Create a new unique_ptr with a new object.
auto song = make_unique<Song>(L"Mr. Children", L"Namonaki Uta");
// Use the unique_ptr.
vector<wstring> titles = { song->title };
// Move raw pointer from one unique_ptr to another.
unique_ptr<Song> song2 = std::move(song);
// Obtain unique_ptr from function that returns by value.
auto song3 = SongFactory(L"Michael Jackson", L"Beat It");
}
void SongVector()
{
vector<unique_ptr<Song>> songs;
// Create a few new unique_ptr<Song> instances
// and add them to vector using implicit move semantics.
songs.push_back(make_unique<Song>(L"B'z", L"Juice"));
songs.push_back(make_unique<Song>(L"Namie Amuro", L"Funky Town"));
songs.push_back(make_unique<Song>(L"Kome Kome Club", L"Kimi ga Iru Dake de"));
songs.push_back(make_unique<Song>(L"Ayumi Hamasaki", L"Poker Face"));
// Pass by const reference when possible to avoid copying.
for (const auto& song : songs)
{
wcout << L"Artist: " << song->artist << L" Title: " << song->title << endl;
}
}
- shared_ptr
- 둘 이상의 소유자가 메모리에 있는 개체의 수명을 관리하는 시나리오를 위해 디자인된 스마트포인터
// shared_ptr-examples.cpp
// The following examples assume these declarations:
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct MediaAsset
{
virtual ~MediaAsset() = default; // make it polymorphic
};
struct Song : public MediaAsset
{
std::wstring artist;
std::wstring title;
Song(const std::wstring& artist_, const std::wstring& title_) :
artist{ artist_ }, title{ title_ } {}
};
struct Photo : public MediaAsset
{
std::wstring date;
std::wstring location;
std::wstring subject;
Photo(
const std::wstring& date_,
const std::wstring& location_,
const std::wstring& subject_) :
date{ date_ }, location{ location_ }, subject{ subject_ } {}
};
using namespace std;
int main()
{
// The examples go here, in order:
// Example 1
// Example 2
}
// Use make_shared function when possible.
auto sp1 = make_shared<Song>(L"The Beatles", L"Im Happy Just to Dance With You");
// Ok, but slightly less efficient.
// Note: Using new expression as constructor argument
// creates no named variable for other code to access.
shared_ptr<Song> sp2(new Song(L"Lady Gaga", L"Just Dance"));
// When initialization must be separate from declaration, e.g. class members,
// initialize with nullptr to make your programming intent explicit.
shared_ptr<Song> sp5(nullptr);
//Equivalent to: shared_ptr<Song> sp5;
//...
sp5 = make_shared<Song>(L"Elton John", L"I'm Still Standing");
//Initialize with copy constructor. Increments ref count.
auto sp3(sp2);
//Initialize via assignment. Increments ref count.
auto sp4 = sp2;
//Initialize with nullptr. sp7 is empty.
shared_ptr<Song> sp7(nullptr);
// Initialize with another shared_ptr. sp1 and sp2
// swap pointers as well as ref counts.
sp1.swap(sp2);
- weak_ptr
- 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근은 허용, 하지만 소유자에는 포함되지 않는 스마트 포인터이다.
- 이는 참조횟수를 기반으로 동작하기에, 위와 같은 동작이 가능하다.
펑터(functor)
- () 연산자를 이용하면, 객체를 함수처럼 사용할 수 있다. 이러한 클래스를 펑터 혹은 함수 오브젝트라고 한다.
Class Adder
{
public:
int operator()(const int& n1, cosnt int& n2)
{
return n1 + n2;
}
}
- 펑터의 경우, 함수 또는 객체의 동작방식에 유연함을 제공할 때 주로 사용된다.
형 변환 연산자
- 형 변환 연산자는 반환형을 명시하지 않는다. 하지만, return문에 의한 값의 변환은 얼마든지 가능하다.
operator int ()
{
return num;
}
'Study > C++' 카테고리의 다른 글
[C++] Exception (0) | 2023.10.15 |
---|---|
[C++] Template (0) | 2023.10.15 |
[C++] Operator Overloading (0) | 2023.10.13 |
[C++] Virtual principle and Multiple Inheritance (0) | 2023.10.10 |
[C++] Polymorphism (0) | 2023.10.09 |