티스토리 뷰

복사(copy) 생성자

// vector.h
class Vector
{
public:
	Vector(const Vector& other);	
	
private:
	int mX;
	int mY;
};

// vector.cpp
Vector::Vector(const Vector& other)
	: mX(other.mX)
	, mY(other.mY)
{
}
  • 같은 클래스에 속한 다른 개체를 이용하여 새로운 개체를 초기화
Vector(const Vector& other);

Vector a;
Vector b(a); // 복사생성자 호출

암시적 복사 생성자

  • 코드에 복사 생성자가 없는 경우, 컴파일러가 암시적 복사 생성자를 자동 생성한다
Vector(const Vector& other)
	: mX(other.mX)
	, mY(other.mY)
{
}
  • 🔥 다만, 얕은 복사가 되므로, 주의하자
    • 클래스에 포인터 형 변수가 있을때, 얕은복사가 되면 문제가 발생할 수 있다

얕은 복사의 문제점

// ClassRecord.h
class ClassRecord
{
public:
	ClassRecord(const int* scores, int count);
	~ClassRecord();
	
private:
	int mCount;
	int* mScores; // 🚨
}
// ClassRecord.cpp
ClassRecord::ClassRecord(const int* scores, int count)
	: mCount(count)
{
	mScores = new int[mCount];
	memcpy(mScores, scores, mCount * sizeof(int));	
}

ClassRecord::~ClassRecord()
{
	delete[] mScores;
}
  • 위 코드에서는, 컴파일러가 암시적 복사 생성자를 자동으로 만들어준다
ClassRecord classRecord(scores, 5);
ClassRecord* classRecordCopy = new ClassRecord(classRecord);
delete classRecordCopy;
// 암시적 복사 생성자
ClassRecord::ClassRecord(const ClassRecord& other)
	: mCount(other.mCount)
	, mScores(other.mScores)
{
}
  • mScores 가 얕은 복사가 된다
    • 즉, 주소값을 공유한다
    • 🚨 delete 할때, 원본 개체에도 영향이 끼친다

🔥 직접 복사 생성자를 만들어서, 깊은 복사를 하자

  • 포인터 변수가 가리키는 실제 데이터까지 복사하는 것이다
ClassRecord::ClassRecord(const ClassRecord& other)
	: mCount(other.mCount)
{
	mScores = new int[mCount];
	memcpy(mScores, other.mScores, mCount * sizeof(int));
}

함수 오버로딩(overloading)

  • 매개변수 목록을 제외하고는 모든게 동일
  • 👁️ 반환형은 상관 없음
void Print(int score); // OK
void Print(const char* name); // OK
void Print(float gpa, const char* name); // OK
int Print(int score); // 컴파일 에러
int Print(float gpa); // OK

함수 오버로딩 매칭하기

  • 함수 매칭 결과는 3개가 있음
    1. 매칭되는 함수를 찾을 수 없음 : 컴파일 에러 🔴
    2. 매칭되는 함수 여러개 찾음(누굴 호출할지 모호) : 컴파일 에러 🔴
    3. 가장 적합한 함수를 하나 찾음 : OK 🟢

Q.

void Print(int score); // 1
void Print(const char* name); // 2
void Print(float gpa, const char* name); // 3

Print(100);
Print("jiggyjiggy");
Print(4.0f, "jiggyjiggy");
Print(77.5f);
Print("jiggyjiggy", 4.0f);

A.

void Print(int score); // 1
void Print(const char* name); // 2
void Print(float gpa, const char* name); // 3

Print(100); // 1
Print("jiggyjiggy"); // 2
Print(4.0f, "jiggyjiggy"); // 3
Print(77.5f); // 👁️ 1, 컴파일 경고가 나올 수 있음
Print("jiggyjiggy", 4.0f); // 컴파일 에러

함수 매칭 순서

int Max(int, int);
int Max(double, double);
int Max(const int a[], size_t);

int Min(int, int);
int Min(double, double);
int Min(const int a[], size_t);

int main()
{
	std::cout << Max(1, 3.14) << std::endl; // 결과를 써놓자면, 컴파일 에러 🚨
}
int Max(int, int);
int Max(double, double);
  • 1 : int
  • 3.14 : double

자동 형변환은 될 것 같은데, 어떻게 처리될까?

int Max(int, int);
// 1 : 정확한 매치
// 3.14 : 표준 변환(standard conversion)
int Max(double, double);
// 1 : 표준 변환(standard conversion)
// 3.14 : 정확한 매치
  • 이는 모호한 호출
  • 🚨 따라서, 컴파일 에러!

연산자(operator) 오버로딩

  • 연산자 : 함수처럼 작동하는 부호
  • C++ 에서는 프로그래머가 연산자를 오버로딩 할 수 있음

🔥 연산자를 함수로 어떻게 표현할지가 포인트

  • 연산자 오버로딩을 보자
// main.cpp
Vector v1(10, 20);
Vector v2(3, 17);
Vector sum = v1 + v2;

// Vector.h
class Vector
{
public:
	Vector operator+(const Vector& rhs) const;
private:
	int mX;
	int mY;
};

// Vector.cpp
Vector Vector::operator+(const Vector& rhs) const;
{
	Vector sum;
	sum.mX = mX + rhs.mX;
	sum.mY = mY + rhs.mY;
	
	return sum;
}
  • 부호는 같지만 여러가지 연산이 가능
int1 = int1 + int2; // 두 int형 변수를 더함
float1 = float1 + float2; // 두 float형 변수를 더함
name = firstName + lastName; // 두 string형 변수를 더함
  • 🔥 연산자는 오버로딩 하는 방법이 2가지 있다
    • 멤버 함수
    • 멤버가 아닌 함수 (friend 키워드)
  • 연산자 역시도 메서드이다
Vector sum = v1 + v2;
Vector sum = v1.operator+(v2);
  • 특정 연산자들은 멤버 함수를 이용해서만 오버로디이 가능하다
    • 👁️ =, (), [], →

멤버 함수(연산자) 오버로딩 : Vector의 operator+() 연산자를 오버로딩 해보자

Vector result = vector1 + vector2;
Vector result = vector1.operator+(vector2);
  • Vector : 반환형
  • operator+ : 함수 이름
  • vector2 : 인자

‘반환형’ ‘함수이름( 인자 ) const;

Vector operator+(const Vector& rhs) const;

  • 인자는 굳이 복사할 필요 없으니깐, 참조형으로 받아온것

friend 키워드

멤버가 아닌 함수를 이용한 연산자 오버로딩

  • 이런게 가능할까?
Vector vector1(10, 20);
std::cout << vector1;

위를 함수로 나타내면 아래와 같을 것이다

#include <iostream>

std::cout.operator<<(vector1); 
  • 🚨 근데 cout 은 내가 건들수있는게 아닌데?
  • 직접 iostream 클래스에 넣어야하나?
    • 말이 안됌!!
    • 내 소유가 아님

Vector의 operator<<() 연산자를 만들어보자

시도 1. 🔴

void operator<<(std::ostream&& os, const Vector& rhs)
{
	os << rhs.mX << ", " << rhs.mY;
}
  • 문제점
    • 이 전역 함수를 어디에 넣지?
    • 이 함수가 Vector 클래스의 private 멤버를 어떻게 읽지?

🔥 friend 키워드!

  • 클래스 정의 안에 friend 키워드 사용가능
    • 다른 클래스나 함수가, 나의 private 또는 protected 멤버에 접근할 수 있게 허용
  • friend 키워드를 쓰지 않았을때, 컴파일 에러 발생 예시
// X.h
class X
{
private:
	int mPrivateInt;
};

// Y.h
#include "x.h"
class Y
{
public:
	void Foo(X& x);
};

// Y.cpp
void Y::Foo(X& x)
{
	x.mPrivateInt += 10; // 컴파일 에러
}
  • friend 키워드 사용 예시
// X.h
class X
{
	friend class Y;
private:
	int mPrivateInt;
};

// Y.h
#include "x.h"
class Y
{
public:
	void Foo(X& x);
};

// Y.cpp
void Y::Foo(X& x)
{
	x.mPrivateInt += 10; // OK
}
  • OOP 에서 friend 키워드가 안티패턴이라고 불리는 이유가 이것임 ㅋ
    • 캡슐화 위배라는 관점
  • 근데, 이런 상황도 생각해볼 수 있음
    • 덩치가 커질때, 특정 클래스에만 제공하고싶은거다
    • getter로 모두에게 뚫어주는것이 아니라!
  • 본인 namespace 가 아닌, 다른 namespace에 있는 클래스에게까지 friend ship 을 줄 수 있다!

friend 함수

함수로도 줄 수 있음

// X.h
class X
{
private:
	int mPrivateInt;
};

// GlobalFunction.cpp
void Foo(X& x)
{
	x.PrivateInt += 10; // 컴파일 에러
}
// X.h
class X
{
	friend void Foo(X& x);
private:
	int mPrivateInt;
};

// GlobalFunction.cpp
void Foo(X& x)
{
	x.PrivateInt += 10; // OK
}

연산자 오버로딩에 필요한 friend 함수

  • friend 함수는 멤버 함수가 아님!
    • “전역 함수가, 나를 접근할 수 있도록, 권한을 줄게~!” 라는 뜻
  • 하지만 다른 클래스의 private 멤버에 접근할 수 있음!
class Vector
{
	friend void operator<<(std::ostream& os, const Vector& rhs);
};

void operator<<(std::ostream& os,const Vector& rhs) // 시그니처가 Vector::operator<< 가 아님! 
{
	os << rhs.mX << ", " << rhs.mY;
}

// Vector.h
class Vector
{
	friend void operator<<(const std::ostream& os, const Vector& rhs); // private, public 상관없으나 그냥 관습적으로 맨 위에 씀
public:
	// ...
private:
	// ...	
};

// Vector.cpp
void operator<<(std::ostream& os,const Vector& rhs) // 시그니처가 Vector::operator<< 가 아님! 
{
	os << rhs.mX << ", " << rhs.mY;
}

// main.cpp
Vector vector1(10, 20);
std::cout << vector1;

시그니처가 정말 올바른가?

void operator<<(std::ostream& os,const Vector& rhs) // 시그니처가 Vector::operator<< 가 아님! 

std::cout << vector1 << std::endl; // Q. 이게 제대로 동작하는가
  1. std::cout << vector1
    • operator<<(std::cout, vector1);
    • 🚨 근데 반환이 void
  2. vector1 << std::endl
    • ?
    • 뭐 할수가 없음
void operator<<(std::ostream& os,const Vector& rhs) // 시그니처가 Vector::operator<< 가 아님! 

std::cout << vector1 << std::endl; // A. 컴파일 에러 🔴

chaining이 될 수 있도록, 수정해보자

void operator<<(std::ostream& os, const Vector& rhs); 🔴

std::ostream& operator<<(std::ostream& os, const Vector& rhs); 🟢
std::ostream& operator<<(std::ostream& os, const Vector& rhs)
{
	os << rhs.mX << ", " << rhs.mY;
	return os;
}

완전히 수정한 operator<<()

// Vector.h
class Vector
{
	friend std::ostream& operator<<(const std::ostream& os, const Vector& rhs); // private, public 상관없으나 그냥 관습적으로 맨 위에 씀
public:
	// ...
private:
	// ...	
};

// Vector.cpp
std::ostream& operator<<(std::ostream& os, const Vector& rhs)
{
	os << rhs.mX << ", " << rhs.mY;
	return os;
}

// main.cpp
Vector vector1(10, 20);
std::cout << vector1 << std::endl;

연산자 오버로딩과 const

const를 쓰는 이유?

Vector operator+(const Vector& rhs) const;
  • 멤버 변수의 값이 바뀌는 것을 방지한다
  • 최대한 많은 곳에 const를 붙이자
  • 지역(local) 변수까지도
    • 모든 회사가 그렇게하지는 않지만,,,

const & 를 사용하는 이유?

Vector operator+(const Vector& rhs) const;

std::ostream& operator<<(const std::ostream& os, const Vector& rhs) const; 
// 🔴 이렇게쓰면, os에 쓸수 없다
// 하지만, const로 시작한 다음, 필요에 의해서 열어주자 🟢
	// std::ostream& operator<<(std::ostream& os, const Vector& rhs); 

  • 불필요한, 개체의 사본이 생기는 것을 방지
  • 멤버 변수가 바뀌는 것도 방지

연산자 오버로딩에 const를 사용하지 않는경우

어떨때 const를 쓰지 않을지 미리 고민해보자

vector1 += vector2;

vector1 += vector1.operator+=(vector2);
  • 반환값 : vector1
  • 함수 이름: operator+=
  • 인자 : vector2

초안 : Vector operator+=(const Vector& rhs) const ; 🔴

좌항(호출자)를 바꾸는 연산이므로, const 함수가 아니여야 한다

올바른 코드 : Vector operator+=(const Vector& rhs); 🟢

고민포인트 : 반환형

Vector operator+=(const Vector& rhs);

자기 개체를 반환해야하는데, Vector 를 반환하는게 최선일까?

체이닝을 하기 시작하면, 최선이 아닐 수 있다

Vector& operator+=(const Vector& rhs);

  • 개체 복사가 없음
  • 연산자 여럿을 연결해서 쓸 수 있음
vector1 += vector2 += vector3
// Vector.cpp
Vector& operator+=(const Vector& rhs)
{
	mX += rhs.mX;
	mY += rhs.mY;
	
	return *this;
}
  • this : 나 자신을 가리키는 포인터
  • *this : “나 자신을 가리키는 포인터” 에서 개체를 뽑는다 == “나”라는 개체

cf) 오버로딩 불가능한 연산자도 존재한다

  • ., .*, ::, ?:, 등등

연산자 오버로딩을 남용하지 말아라

  • 의미가 직관적이지 않는 경우가 많음
  • 차라리 함수로 만들어서, 의미를 명시해주자.

대입(assignment) 연산자

  • operator=
  • 복사생성자와 거의 동일
    • 대입 연산자로 할당해주는 경우는
      • 이미 오랫동안 존재해오던 개체인 상황도 존재
      • 따라서, 대입 연산자는 메모리를 해제해 줄 필요가 있을 수 있다.
  • 복사 생성자를 구현했다면, 일반적으로, 대입 연산자도 구현해야할 것이다

암시적 operator=

  • operator= 구현이 안되어 있다면, 컴파일러가 operator= 연산자를 자동으로 만들어준다
class Vector
{
public:
	Vector() {}
	Vector& operator=(const Vector& rhs)
	{
		mX = rhs.mX;
		mY = rhs.mY;
		
		return *this;
	}
};
  • 복사 생성자와 마찬가지로, 얕은복사 🔥

암시적 함수들을 제거하는 법

  • 언어차원에서 암시적으로 만드는 경우가 많다
    • 매개변수 없는 생성자
    • 복사 생성자
    • 소멸자
    • 대입(=) 연산자
  • 제거해보자

암시적 기본생성자를 “지우는” 법

  1. 다른 생성자를 작성
class Vector
{
public:
	Vector(const Vector& other);
};

b. private: 또는 protected: 에 작성

class Vector
{
public:
	
private:
	Vector() {};
};

암시적 복사 생성자를 “지우는” 법

class Vector
{
public:
	
private:
	Vector(const Vector& other) {};
};

암시적 소멸자를 “지우는” 법

class Vector
{
public:
	
private:
	~Vector() {};
};
  • 근데, 쓸일은 거의 없을 것. 아래의 코드를 보자
Vector v1; // 컴파일 에러 🚨
Vector* v2 = new Vector();
delete v2; // 컴파일 에러 🚨
  • 프로그램 종료시까지 살려야하는 개체는 그럴 수도? ㅎㅋ

암시적 operator= 를 “지우는” 법

class Vector
{
public:
	
private:
	const Vector& operator=(const Vector& rhs);
};

'C・C++ > OOP' 카테고리의 다른 글

[C++] low level 개체지향 프로그래밍 3  (1) 2024.10.06
[C++] low level 개체지향 프로그래밍 1  (2) 2024.10.06
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함