배움 저장소
[홍정모의 따라하며 배우는 C++] 9.연산자 오버로딩 본문
9.1 산술 연산자 오버로딩 하기
add 함수를 구현해보자. Parameter가 늘어날 때마다 오버로딩을 해주어야 한다
class Cents
{
private:
int m_cents;
public:
Cents(int cents=0){ m_cents = cents; }
int getCents() const { return m_cents; }
};
Cents add(const Cents& c1, const Cents& c2){
return Cents(c1.getCents() + c2.getCents());
}
int main(){
Cents wallet1(5);
Cents wallet2(13);
cout << add(wallet1, wallet2).getCents() << endl;
}
연산자 오버로딩으로 구현하면 간편하다
- 위 코드에서 add 대신에 operator +를 입력하면 '+' 연산자를 사용할 수 있다.
Cents operator + (const Cents& c1, const Cents& c2){
return Cents(c1.getCents() + c2.getCents());
}
int main(){
Cents wallet1(5);
Cents wallet2(13);
cout << (wallet1 + wallet2 + Cents(2)).getCents() << endl;
}
연산자 오버로딩을 멤버함수로 구현해도 된다
- " + " 연산자는 이항연산자로 연산자의 왼쪽과 오른쪽에 피연산자를 가진다. 이 때 멤버변수는 왼쪽 피연산자를 이미 가지게 되므로 오른쪽 피연산자만 있으면 된다. 그래서 Parameter를 하나 추가하면 에러가 발생한다.
class Cents
{
private:
int m_cents;
public:
Cents(int cents=0){ m_cents = cents; }
int getCents() const { return m_cents; }
Cents operator + (const Cents& c) {
return Cents(this->m_cents + c.m_cents);
}
};
int main(){
cout << (Cents(5) + Cents(13) + Cents(2)).getCents() << endl;
}
이 때 연산자 오버로딩의 매개변수에 const가 붙음을 확인하자. const 자료형은 인자로 R-value와 L-value 모두 가질 수 있다. const를 뺀 일반 자료형은 L-value만 가질 수 있다. 그래서 위 예제에서 const를 빼버리면 임시 객체는 사용할 수 없고 모두 L-vale만 사용해야 한다.
Cents wallet1(5);
Cents wallet2(13);
cout << (wallet1 + wallet2 ).getCents() << endl;
연산자 오버로딩 정보
꼭 외울 필요는 없다. 컴파일러가 경고,에러를 발생시키면 그때 구글링하자
(1) 다음 연산자 오버로딩은 반드시 멤버함수로 구현되어야 한다
assignment operator " = "
subscript " [ index ] "
function call " ( ) "
member selection " -> "
(2) " +, -, *, / " 모두 연산자 오버로딩이 가능하나 연산자의 우선순위는 바꿀 수 없다
(3) 아래 5개 연산자는 오버로딩이 불가능하다
sizeof operator(sizeof), Trenary conditional operator(?), Scope resolution operator(::)
member access operator(.), Pointer to member operator(.* or ->*),
(4) Bitwise XOR operator(^)는 연산자 우선순위가 낮아 괄호를 사용해주어야 한다. 이 연산자는 오버로딩하지 말자
9.2 입출력 연산자 오버로딩 하기
멤버함수로 입출력 연산자를 구현할 수 없다
- 입출력 연산자 << 와 >>는 두 개의 피연산자를 가진다. 멤버 함수는 왼쪽의 피연산자가 항상 this이다. 입출력 연산자를 오버로딩하려면 ostream객체 내부에서 해당 객체를 위한 멤버변수를 새로 만들어야한다. ostream 객체 내부를 수정하기는 어렵다.
입출력 연산자의 overloading은 friend로 처리하자
- 이 때 i/o stream의 reference를 반환해주자. 연쇄호출이 가능하다
class Point
{
private:
double m_x, m_y, m_z;
public:
Point(double x = 0.0, double y = 0.0, double z = 0.0):m_x(x), m_y(y), m_z(z){}
friend std::ostream& operator << (std::ostream& out, const Point& point){
out << point.m_x << " " << point.m_y << " " << point.m_z << endl;
return out; // for chaining member function
}
friend std::istream& operator >> (std::istream& in, Point& point) {
in >> point.m_x >> point.m_y >> point.m_z;
return in; // for chaining member function
}
};
int main(){
cout << Point(0.2, 0.1, 0.3) << " and " << Point(1.1, 2.2, 3.3) << endl;
Point p1, p2;
cin >> p1 >> p2;
cout << p1 << p2 << endl;
}
출력 연산자 overloading을 활용하여 파일 출력을 구현할 수 있다
#include <fstream>
int main(){
// Write File
ofstream output("out.txt");
output << Point(0.2, 0.1, 0.3) << Point(1.1, 2.2, 3.3) << endl;
output.close();
Point p1, p2;
// Read File
ifstream input("out.txt");
input >> p1 >> p2;
cout << p1 << p2;
input.close();
}
생성된 파일 : out.txt
0.2 0.1 0.3
1.1 2.2 3.3
출력값
0.2 0.1 0.3
1.1 2.2 3.3
9.3 단항 연산자 오버로딩 하기
class Cents
{
private:
int m_cents;
public:
Cents(int cents): m_cents(cents){}
friend std::ostream& operator << (std::ostream& out, const Cents& cents){
out << cents.m_cents;
return out; // for chaining member function
}
Cents operator - () const{ return Cents(-1 * m_cents); }
bool operator !() const { return m_cents == 0; }
};
int main(){
cout << -Cents(1) << endl; // >> -1
cout << -Cents(-1) << endl; // >> 1
cout << boolalpha;
cout << !Cents(1) << endl; // >> false
cout << !Cents(0) << endl; // >> true
}
9.4 비교 연산자 오버로딩 하기
"==", "!=" 모두 구현가능하다
- class 내부에 friend keyword가 입력된 함수를 선언해주자
bool operator !=(const Cents& left, const Cents& right){
return left.m_cents != right.m_cents;
}
int main(){
cout << boolalpha;
cout << (Cents(0) != Cents(1)) << endl;
cout << (Cents(1) != Cents(1)) << endl;
}
"<" 구현하기
- <algorithm>에 구현된 sort ( ) 함수를 사용하려면 less than operator "<"가 구현되어야 한다
#include <iostream>
#include <random>
#include <vector>
using namespace std;
class Cents
{
private:
int m_cents;
public:
Cents(int cents=0): m_cents(cents){}
int& getRef(){ return m_cents; }
friend std::ostream& operator << (std::ostream& out, const Cents& cents){
out << cents.m_cents;
return out; // for chaining member function
}
friend bool operator < (const Cents& left, const Cents& right){
return left.m_cents < right.m_cents;
}
};
int main(){
// init
vector<Cents> v(10);
for(int i=0; i<v.size(); ++i) { v[i].getRef() = i; }
// shuffle
std::random_device rd;
std::mt19937 engine(rd());
std::shuffle(v.begin(), v.end(), engine);
// before sort, print
for(Cents&c:v){ cout << c.getRef() << " "; }
cout << endl;
// sort
std::sort(v.begin(), v.end());
// after sort, print
for (Cents& c : v) { cout << c.getRef() << " "; }
cout << endl;
}
9.5 증감 연산자 오버로딩 하기
Prefix와 Postfix는 Parameter로 구분한다
- Prefix는 Parameter가 없다. Postfix는 dummy Parameter를 넣어준다
class Digit{
private:
int m_digit;
public:
Digit(int digit): m_digit(digit){}
friend std::ostream& operator <<(std::ostream& out, const Digit& digit){
out << digit.m_digit;
return out;
}
// Prefix
Digit& operator ++ () {
++this->m_digit;
return *this;
}
// Postfix
Digit operator ++ (int) { // must add dummy para
Digit temp = *this;
this ->m_digit++;
return temp;
}
};
int main(){
Digit ten(10);
cout << ++ten << endl;
cout << ten << endl;
Digit five(5);
cout << five++ << endl;
cout << five << endl;
}
9.6 첨자 연산자 오버로딩 하기
[ ] subscript operator
첨자 연산자 없이 멤버변수 배열에 접근해보자 : 불편하다
class IntList {
private:
int m_list[10];
public:
int* getList(){ return m_list; }
};
int main(){
IntList ilist;
for(int i=0; i<10; ++i){ ilist.getList()[i] = i; }
for (int i = 0; i < 10; ++i){ cout << ilist.getList()[i] << " "; }
}
subscript operator [ ] 첨자 연산자 구현하기
- 다음은 첨자 연산자에 정수형을 사용하는 예제이다. map을 사용할 때는 문자열로 구현한다.
- Instance에 const가 붙어있으면 별도의 함수를 호출해야 한다. 멤버함수를 const로 오버로딩 해주자.
class IntList {
private:
int m_list[10] = { 0, };
public:
//int& operator [](const int &index){ return *(m_list + index); }
int& operator [](const int &index) {
assert(0 <= index); assert(index < 10);
return m_list[index];
}
const int operator [] (const int &index) const {
assert(0 <= index); assert(index < 10);
return m_list[index];
}
};
int main() {
IntList ilist;
//for (int i = 0; i < 10; ++i) { ilist[i] = i; }
for (int i = 0; i < 10; ++i) { cout << ilist[i] << " "; }
cout << endl;
const IntList const_ilist;
for (int i = 0; i < 10; ++i) { cout << const_ilist[i] << " "; }
}
- assert는 inline으로 구현되어있다. if( expression ) 보다 훨씬 빠르다.
- Release 모드에서도 작동하지 않으므로 따로 코드를 수정하지 않아도 된다.
객체 포인터를 사용할 때 객체 배열과 subscription operator를 혼용하지 말자
IntList *ptr = new IntList;
ptr[1]; // access class array's next element
(*ptr)[1]; // use subscription operator, access to member variable
subscription operator로 map 구현하기
struct StudentGrade{
std::string name{};
char grade{};
};
class GradeMap{
private:
std::vector<StudentGrade> m_map{};
public:
char& operator[](const std::string& name);
};
char& GradeMap::operator[](const std::string& name)
{
auto found{ std::find_if(m_map.begin(), m_map.end(),
[&](const auto& student){ // this is a lambda
return (student.name == name);
}) };
if (found != m_map.end())
return found->grade;
// otherwise create a new StudentGrade for this student and add
// it to the end of our vector.
m_map.push_back({ name });
// and return the element
return m_map.back().grade;
}
9.7 괄호 연산자 오버로딩과 함수 객체
parenthesis and Function object(Functor)
클래스 이름을 함수처럼 활용할 수 있다
class Functor {
private:
int m_value;
public:
Functor(int value=0): m_value(value){}
int operator () (int added) { return (m_value += added); }
};
int main(){
Functor func(0);
cout << func(5) << endl; // >> 5
cout << func(5) << endl; // >> 10
}
9.8 형변환을 오버로딩 하기
클래스를 형변환 할 때 호출되는 함수를 구현해보자
- 아래 형변환 오버로딩 멤버함수는 해당 객체를 정수형으로 변환할 때 호출된다
class Cents {
private:
int m_cents;
public:
Cents(int cents=0):m_cents(cents){}
operator int() {
cout << "casting!" << endl;
return m_cents;
}
};
아래를 실행하면
Cents cent(5);
int value = int(cent);
(int)cent;
다음이 출력된다
casting!
casting!
달러 클래스를 센트 클래스로 형변환하는 오버로딩 멤버함수를 구현해보자
- main에서 Cents Instance가 출력되는 이유는 int로 형변환되기 때문이다.
- 함수명이 자료형을 알려주기 때문에 형변환 오버로딩에 반환 자료형이 없다.
class Cents {
private:
int m_cents;
public:
Cents(int cents=0):m_cents(cents){}
operator int() {
cout << "casting!" << endl;
return m_cents;
}
};
class Dollar{
private:
public:
int m_dollars = 0;
public:
Dollar(const int& dollars) : m_dollars(dollars) {}
// Type Conversion for class Cents
operator Cents() { return Cents(m_dollars * 100); }
};
int main(){
Cents wallet = Dollar(1);
cout << wallet << endl;
}
- 위에서 Cent 인스턴스를 바로 출력할수도 있고 함수를 구현하여 출력할 수도 있다. 함수를 따로 구현한다면 매개변수의 자료형은 const int&여야 한다. Cents의 형변환 오버로딩의 반환값이 R-Value이기 때문이다. const 자료형은 L-Value, R-Value 모두 복사할 수 있다.
void printCent(const int& value){ cout << value << endl; }
int main() {
Cents wallet;
printCent(wallet);
}
9.9 복사 생성자, 복사 초기화, 반환값 최적화
Return Value Optimization
Copy Constructor 복사 생성자를 사용한 예제이다
- 보안을 위해 복사생성자를 private sector에 넣을 수 있다. 그러면 외부에서 복사생성자를 사용할 수 없다.
class Fraction {
private:
int m_numerator;
int m_denominator;
public:
Fraction(int num = 0, int den = 1) : m_numerator(num), m_denominator(den)
{ assert(den != 0); }
// Copy Constructor
Fraction(const Fraction& fraction):m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
{ cout << "Copy Initialization is Called!" << endl; }
friend std::ostream& operator << (std::ostream& out, const Fraction &fraction) {
out << fraction.m_numerator << " / " << fraction.m_denominator;
return out;
}
};
int main(){
Fraction f1(3,5);
Fraction copied(f1);
cout << copied << endl; // >> Copy Initialization is Called
Fraction copied_2 = f1;
cout << copied_2 << endl; // >> Copy Initialization is Called
}
Return Value Optimization(반환값 최적화)
- 아래 예제를 debug 모드에서 실행하면 복사생성자가 호출된다. release 모드에서 실행하면 복사생성자가 호출되지 않는다. 컴파일러가 최적화를 해주었다. 복사생성자 호출은 컴파일러가 생략할 수 있다
Fraction ReturnValueOptimization(){
Fraction temp(1,2);
cout << &temp << endl;
return temp;
}
int main(){
Fraction result = ReturnValueOptimization();
cout << &result << endl;
cout << result << endl;
}
Output
-Debug- -Release Mode-
00CFF900 00D5FAE4
Copy Initialization is Called! 00D5FAE4
00CFF9F0 1 / 2
1 / 2
9.10 변환 생성자, explicit, delete
converting constructor
변환 생성자는 생성자를 변환시켜 준다
explicit keyword는 변환 생성자를 금지한다
delete는 특정 생성자를 지운다. 이때 동적할당에서 사용한 메모리 해제와 다른 의미이다
Converting Constructor(변환생성자)의 활용
- 클래스가 매개변수로 사용되었을 때, 매개변수의 값이 클래스의 생성자 매개변수 포맷과 일치한다면 자동으로 생성자를 호출해준다. 이를 변환생성자라 한다.
void ConversionPrint(Fraction frac){
cout << frac << endl;
}
int main(){
ConversionPrint(7); // >> 7 / 1
}
객체의 생성자에 explicit keyword를 추가하면 변환생성자를 사용할 수 없다
class Fraction {
//...
public:
explicit Fraction(int num = 0, int den = 1) : m_numerator(num), m_denominator(den)
{ assert(den != 0); }
//...
}
다음과 같은 에러가 발생한다. no suitable constructor exists to convert from "int" to "Fraction
int main(){
ConversionPrint(7); // Error!
}
생성자 지우기, delete keyword 활용하기
(1) 업데이트 된 프로그램에서 특정 생성자를 금지할 때 delete keyword를 사용한다.
(2) 클래스 내부에서 의도하지 않은 conversion constructor가 실행되서 원하지 않는 결과값을 얻을 수 있다. 이를 방지하기 위하여 해당 conversion constructor를 지워버릴 수 있다.
의도하지 않은 변환생성자가 호출되는 사례를 확인해보자
(1) 아래 예제에서 int 매개변수로 string 자료형의 길이를 정한다. 문자 매개변수가 들어오면 자동으로 int 자료형으로 변환되어 사용한다.
(2) 문자를 넣어 해당 인스턴스를 초기화 시도할 수 있다. 원하지 않는 결과가 나타난다.
(3) 따라서 문자형 인수를 사용했을 때 Error를 발생시켜야 한다.
- explicit keyword를 사용하면 conversion constructor는 실행되지 않는다.
- delete keyword를 사용하면 함수 내부에서도 conversion constructor를 사용할 수 없다.
class MyString {
private:
std::string m_string;
public:
//MyString(char) = delete;
//explicit MyString(int x) { m_string.resize(x); }
MyString(int x) { m_string.resize(x); }// allocate string of size x
MyString(const char* string) { m_string = string; } // allocate string to hold string value
friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};
std::ostream& operator<<(std::ostream& out, const MyString& s) {
out << s.m_string;
return out;
}
void printString(const MyString& s) { std::cout << s; }
int main() {
MyString mine = 'x';
std::cout << mine;
printString('x');
}
9.11 대입 연산자 오버로딩, 깊은 복사, 얕은 복사
멤버변수가 동적할당메모리를 사용하고 있을 때 대입연산자는 깊은 복사와 얕은 복사를 할 수 있다
다음은 얕은 복사 예제이다. 기본 copy constructor(복사연산자)를 사용하면 얕은 복사가 실행된다
- 얕은 복사 이후 복사본은 원본과 동적할당메모리를 공유한다.
- 복사본이 소멸자를 호출하여 동적할당 메모리를 해제시키면 원본은 더이상 동적할당메모리를 사용할 수 없다.
- 아래 예제를 실행시켜보자. 해제된 동적할당 메모리를 다시 해제하여 에러가 발생한다.
#include <iostream>
#include <cassert>
using namespace std;
class MyString {
private:
char* m_data = nullptr;
int m_length = 0;
public:
MyString(const char* source = "") {
assert(source);
m_length = std::strlen(source) + 1;// for adding null character
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i) { m_data[i] = source[i]; }
m_data[m_length - 1] = '\0';
}
~MyString() {
delete[] m_data;
}
char* getData() { return m_data; }
};
int main() {
MyString hello("Hello");
cout << (int*)hello.getData() << endl;
cout << hello.getData() << endl;
{
MyString copied = hello; // call default copy constructor
//copied = hello // call assignment operator
cout << (int*)copied.getData() << endl;
cout << copied.getData() << endl;
}
cout << hello.getData() << endl;
}
0083E178
Hello
0083E178
Hello
硼硼硼硼硼
깊은 복사이다. deep copy constructor(복사연산자)를 구현했다. 동적할당메모리를 새로 요청하여 값을 복사한다
class MyString {
private:
char* m_data = nullptr;
int m_length = 0;
public:
MyString(const char* source = "") {
assert(source);
m_length = std::strlen(source) + 1;// for adding null character
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i) { m_data[i] = source[i]; }
m_data[m_length - 1] = '\0';
}
~MyString() {
delete[] m_data;
}
char* getData() { return m_data; }
// Deep Copy Constructor
MyString(const MyString& source) {
cout << "Copy constructor " << endl;
m_length = source.m_length;
if (source.m_data != nullptr) {
m_data = new char[m_length];
for(int i=0; i<m_length; ++i)
m_data[i] = source.m_data[i];
} else {
m_data = nullptr;
}
}
};
00A9C898
Hello
Copy constructor
00A9C668
Hello
Hello
Copy Constructor VS Assignmnet Operator
- 초기화 때 사용하는 assignment operator는 복사 생성자를 호출한다.
- 인스턴스에 다른 인스턴스 값을 넣을 때는 오버로딩된 연산자가 호출된다.
헷갈린다면 객체를 초기화할 때 = 연산자를 피하고 { } 혹은 ( )를 사용하자
MyString hello("Hello");
MyString Copied = hello; // Copy constructor
MyString Assigned;
Assigned = hello; // Assignmnet Operator
Assignmnet Operator를 사용하려면 객체 멤버함수에 다음을 추가해주자
MyString& operator = (const MyString& source) {
cout << "Assignmnet Operator " << endl;
if (this == &source) return *this; // Instance = Instance Makes Error
// Prevent self-assignmnet
delete[] m_data; // free used allocated memory.
m_length = source.m_length;
if (source.m_data != nullptr) {
m_data = new char[m_length];
for (int i = 0; i < m_length; ++i)
m_data[i] = source.m_data[i];
}
else {
m_data = nullptr;
}
return *this;
}
기본으로 구현되는 얕은 복사생성자를 delete keyword로 금지할 수 있다
- 얕은 복사생성자를 허용하면 동적할당 메모리 관리가 어려워질 수 있다. 복사를 금지해보자.
class MyString {
public:
// ...
MyString(const MyString& source) = delete;
// ...
};
9.12 이니셜라이져 리스트 initializer list
initializer list는 배열 자료형을 한꺼번에 초기화해준다. 클래스에도 사용해보자
<initializer_list> 라이브러리를 포함시켜주자
#include <iostream>
#include <initializer_list>
#include <cassert>
using namespace std;
class IntArray {
private:
unsigned m_length = 0;
int *m_data = nullptr;
public:
IntArray(unsigned length) :m_length(length){
m_data = new int[length];
}
~IntArray() {
delete[] this->m_data;
}
// take Initializer list parameters for it
IntArray(const std::initializer_list<int>& list) : IntArray(list.size()) {
int i = 0; // call another constructor
for (auto& e : list) { // only iterator is available
m_data[i] = e;
++i;
}
//for(unsigned i = 0; i<list.size(); ++i)
// m_data[i] = list[i]; // Error! initzer list didn't support []
}
friend ostream& operator << (ostream& out, IntArray& arr) {
for(unsigned i=0; i< arr.m_length; ++i)
out << arr.m_data[i] << " ";
return out;
}
};
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };// initializer list
int *dy_arr = new int[5] { 1, 2, 3, 4, 5 };// initializer list
auto il = { 10, 20, 30 }; // auto is initializer_list<int>
IntArray int_array { 1, 2, 3, 4, 5 };
IntArray int_array2 = { 6, 7, 8, 9, 10 };
cout << int_array << endl;
}
initializer list를 대입연산자에 구현하기: 멤버함수로 구현해보자
IntArray& operator = (const std::initializer_list<int>& list) {
cout << "Assignment operator is executed" << endl;
delete[] m_data;
m_length = list.size();
m_data = new int[m_length];
int i = 0;
for (auto& e : list) {
this->m_data[i] = e;
i++;
}
return *this;
}
int main() {
IntArray int_array{ 1, 2, 3, 4, 5 };
IntArray int_array2(5);
int_array2 = {11, 12, 13};
cout << int_array2 << endl;
}
Assignment operator is executed
11 12 13
'Programming Language > C++' 카테고리의 다른 글
[홍정모의 따라하며 배우는 C++] 11. 상속 (0) | 2021.12.27 |
---|---|
[홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해 (0) | 2021.12.26 |
[홍정모의 따라하며 배우는 C++] 8. 객체지향의 기초 (0) | 2021.12.24 |
[홍정모의 따라하며 배우는 C++] 7. 함수 (0) | 2021.12.23 |
[홍정모의 따라하며 배우는 C++] 6. 행렬, 문자열, 포인터, 참조 (0) | 2021.12.19 |