코딩 학습/C와 C++

C++ 기초 - 복사 생성자와 오버로딩

이개 2026. 4. 7. 16:54

오버로딩

반환형은 오버로딩의 판단 기준이 안 된다. 이름과 매개변수가 기준이다.

 

복사 생성자

복사 생성자(Copy Constructor)

생성자의 오버로딩 중 하나.

매개변수 자료형이 같은 클래스의 참조 자료형인 생성자를 복사 생성자라고 부른다.

새 객체를 생성 할 때, 기존에 만들어져 있던 객체를 통해 생성하고자 할 때 호출됩니다. 다시 말해, 같은 클래스에 속한 다른 객체를 참조하여 새로운 객체를 초기화한다.

 

ThisClass(const ThisClass& InOther); 
	// 이런 형태의 생성자를 복사 생성자라 부릅니다.
	// 자기 자신 자료형의 참조를 인자로 받으면 복사 생성자라고 부릅니다.

 

 

// MyVector.h
MyVector(const MyVector& InOther) {};

// Main.cpp
MyVector A = MyVector(1, 7);

MyVector B(A);
	// "새 객체" B를 생성할 때, 기존에 만들어져 있던 객체 A를 통해 생성하고자 합니다. 
	// -> 복사 생성자 MyVector(const MyVector& InOther)가 호출됩니다.
MyVector C = MyVector(B); 
	// "새 객체" C를 생성할 때, 기존에 만들어져 있던 객체 B를 통해 생성하고자 합니다. 
	// -> 복사 생성자 MyVector(const MyVector& InOther)가 호출됩니다.

 

 

암시적 복사 생성자

코드에 복사 생성자가 없는 겨우, 컴파일러가 암시적으로 복사 생성자를 자동으로 생성.

// MyVector.h 

class MyVector
{
	// 분명 만들지 않았습니다.

private:
	float X;
    
	float Y;
}
// MyVector.obj

class MyVector
{
public:
	MyVector() {}                   // 컴파일러가 암시적으로 만들어준 기본 생성자
    
	MyVector(const MyVector& other) // 컴파일러가 암시적으로 만들어준 복사 생성자
		: X(other.X)
		, Y(other.Y)
	{
	}

private:
	float X;
    
	float Y;
    
}

 

암시적 복사 생성자를 컴파일러가 만들 수 있는 이유?

-> 모든 멤버별로 복사를 진행할 뿐이므로 자동 생성 가능.

암시적 복사 생성자는 얕은 복사를 수행하기 때문에, 포인터를 멤버 변수로 갖고 있다면 주의!

 

// Main.cpp

#include <iostream>

class Pet
{
public:
	Pet()
	{
		std::cout << "Pet Constructor" << std::endl;
	}

	Pet(const Pet& InOther)
		: Age(InOther.Age)
	{
		std::cout << "Pet Copy Constructor" << std::endl;
	}

public:
	int Age;

};

class Human
{
public:
	Human()
	{
		std::cout << "Person Constructor" << std::endl;
	}

	Human(const Human& InOther)
		: MyPet(InOther.MyPet) // 복사생성자의 호출
	{
		std::cout << "Person Copy Constructor" << std::endl;
	}

public:
	Pet MyPet; // 다른 클래스의 객체

};

int main(void)
{
	Human Me;
	Me.MyPet.Age = 10;

	Human You = Human(Me);

	return 0;
}

 

결과

 

 

연산자 오버로딩

C++에서는 프로그래머가 연산자를 오버로딩 할 수 있다.

 

연산자인 것과 연산자가 아닌 것

MyVector& Ref; // &는 연산자가 아니다.
MyVector* Ptr; // *도 연산자가 아니다.

 

Max(1, 3.14f) // ()는 함수 호출 연산자이다.
int Score = Scores[10]; // []도 배열 첨자 연산자이다.
MyVector* Vector01 = new MyVector[10]; // new도 연산자이다.
delete[] Vector01; // delete도 연산자이다.

 

 

연산자 오버로딩의 두 가지 방법

1. 멤버 함수로 정의할 수 있을 때

ThisClass operator+(const ThisClass& InOther) const;

ThisClass operator-(const ThisClass& InOther) const;

ThisClass operator*(const ThisClass& InOther) const;

ThisClass operator/(const ThisClass& InOther) const;

 

오버로딩 예시

// MyVector.h

#pragma once

class MyVector
{
public:
	MyVector();

	MyVector(float InX, float InY);

	MyVector(const MyVector& InOtherVector);

	~MyVector();

	void PrintX() const;

	void PrintY() const;

	float GetX() const;

	float GetY() const;

	void SetX(float InX);

	void SetY(float InY);

	//bool IsEqual(const MyVector& InOtherVector) const;
	bool operator==(const MyVector& InOtherVector) const;
		// 연산자 오버로딩

	MyVector Multiply(float InScalar) const;

	MyVector Multiply(const MyVector& InOtherVector) const;

private:
	float X;

	float Y;

};

 

// MyVector.cpp

#include "MyVector.h"

#include <iostream>

MyVector::MyVector()
	: X(0)
	, Y(0)
{
}

MyVector::MyVector(float InX, float InY)
	: X(InX)
	, Y(InY)
{
}

MyVector::MyVector(const MyVector& InOtherVector)
	: X(InOtherVector.X)
	, Y(InOtherVector.Y)
{
}

MyVector::~MyVector(void)
{
}

void MyVector::PrintX() const
{
	std::cout << X << std::endl;
}

void MyVector::PrintY() const
{
	std::cout << Y << std::endl;
}

float MyVector::GetX() const
{
	return X;
}

float MyVector::GetY() const
{
	return Y;
}

void MyVector::SetX(float InX)
{
	X = InX;
}

void MyVector::SetY(float InY)
{
	Y = InY;
}

/*
bool MyVector::IsEqual(const MyVector& InOtherVector) const
{
	if (X == InOtherVector.X && Y == InOtherVector.Y)
	{
		return true;
	}

	return false;
}
*/

bool MyVector::operator==(const MyVector& InOtherVector) const
{
	return X == InOtherVector.X && Y == InOtherVector.Y;
}

MyVector MyVector::Multiply(float InScalar) const
{
	MyVector Result = MyVector();
	Result.SetX(X * InScalar);
	Result.SetY(Y * InScalar);

	return Result;
}

MyVector MyVector::Multiply(const MyVector& InOtherVector) const
{
	MyVector Result = MyVector();
	Result.SetX(X * InOtherVector.GetX());
	Result.SetY(Y * InOtherVector.GetY());

	return Result;
}

 

// Main.cpp

#include <iostream>

#include "MyVector.h"

int main(void)
{
	MyVector Vector01(1, 2);
	MyVector Vector02(1, 2);
	MyVector Vector03;
	Vector03 = Vector01.Multiply(Vector02);

	std::cout << "Vector01 == Vector02: " << (Vector01 == Vector02) << std::endl;
	std::cout << "Vector01 == Vector03: " << (Vector01.operator==(Vector03)) << std::endl;
		// 연산자도 하나의 멤버 함수처럼 호출할 수 있습니다.

	return 0;
}

 

 

2. 멤버 함수로 정의할 수 없을 때, 즉 전역 함수로 정의해야 할 때

float Score;
std::cout << Score;

MyVector Vector01
std::cout << Vector01; 
	// 이런게 하고 싶습니다.
	// 그럼 화면에는 "(5, 2)"와 같이 출력되었으면 합니다.

MyVector 객체를 콘솔에 출력하려면?

 

목표 형태 : std::cout.operator << (Vector01);

cout은 iostream header 파일에 있는데, 이를 바꿀 수는 없다.

 

그럼 전역함수로 만든다면? 

// MyVector.cpp

...

void operator<<(std::ostream& InOutputStream, const MyVector& InPrintedVector)
{
	InOutputStream << '(' << InPrintedVector.X << ", " << InPrintedVector.Y << ')' << std::endl;
}

 

어디에 작성해야할까?

 

참고로 ostream은 Output Stream의 약자로, 출력을 담당하는 클래스다. cout도 ostream 타입이다.

ios
 └── ostream        ← 출력 담당
      ├── cout      ← 콘솔 출력
      ├── cerr      ← 에러 출력
      ├── clog      ← 로그 출력
      └── ofstream  ← 파일 출력

전부 ostream을 상속받기 때문에 ostream&으로 받으면 cout, 파일 출력 등 전부 대응 가능하다.

 

참조자로 받아야 정상적으로 사용 가능하다.

// & 없이 받으면?
void operator<<(std::ostream InOutputStream, ...)
//                            ↑ 복사본 생성
// cout을 복사? → 말이 안 됨! cout은 복사 불가능한 객체
// & 로 받으면?
void operator<<(std::ostream& InOutputStream, ...)
//                             ↑ 원본을 그대로 참조
// cout 자체를 가리킴 → 실제 콘솔에 출력됨 ✅

 

std::ostream& operator<<(std::ostream& os, const MyVector& v)
{
    os << '(' << v.X << ", " << v.Y << ')';
    return os; // os = cout 원본을 반환
}

// 사용
cout << vec1 << vec2;

// 내부적으로 이렇게 동작
operator<<(cout, vec1); // cout 반환
operator<<(cout, vec2); // cout 반환

 

 

또, 전역함수는 멤버 변수에 접근이 불가능하다. 그러므로 friend 키워드로 오버로딩을 해보자.

// MyVector.h

#include <iostream> // std::ostream

class MyVector
{
	friend void operator<<(std::ostream& InOStream, const MyVector& InVector);
	
	friend MyVector operator*(float InScalar, const MyVector& InVector);
	
	...
};

 

// MyVector.cpp

...

void operator<<(std::ostream& InOStream, const MyVector& InVector)
{
	...
}

MyVector operator*(float InScalar, const MyVector& InVector)
{
	...
}

 

 

friend 키워드를 활용한 전역함수 오버로딩 문법

// MyVector.h

#include <iostream> // std::ostream

class MyVector
{
	friend void operator<<(std::ostream& InOStream, const MyVector& InVector);
	
	friend MyVector operator*(float InScalar, const MyVector& InVector);
	
	...
};

 

// MyVector.cpp

...

void operator<<(std::ostream& InOStream, const MyVector& InVector)
{
	...
}

MyVector operator*(float InScalar, const MyVector& InVector)
{
	...
}

 

위와 같을 경우 반환 타입이 void라서 체이닝이 안 되는 문제 발생

cout << Vec1 << Vec2; // ❌ 에러!

 

체이닝하려면 

// void → std::ostream& 로 바꿔야 함
std::ostream& operator<<(std::ostream& InOutputStream, const MyVector& InPrintedVector)
{
    InOutputStream << '(' << InPrintedVector.X << ", " << InPrintedVector.Y << ')' << std::endl;
    return InOutputStream; // ← 스트림을 다시 반환
}

// 이제 체이닝 가능
cout << Vec1 << Vec2; // ✅ (3, 5)(1, 2)

 

 

 

 

다음과 같이 코드를 작성할 수도 있다.

cout << Vector01 << endl;
// 1. << 연산자의 결합법칙을 알아야 합니다.
// https://en.cppreference.com/w/cpp/language/operator_precedence?utm_source=chatgpt.com#:~:text=For%20example%2C%20the%20expressions,.
// 위 링크 내용에 따르면 좌측 결합성입니다. 따라서 아래와 같이 파싱됩니다. 즉, (cout << Vector01) 부분이 먼저 수행됩니다.

(cout << Vector01) << endl;
// cout << Vector01은 void operator<<(cout, Vector01) 함수를 호출합니다.
// 아래와 같이 평가됩니다.

void << endl; 
// 컴파일 에러.

 

 

// 만약 방어적인 프로그래밍을 하려고 InOStream 매개변수의 자료형을 const로 했다면 어떻게 될까요?
std::ostream& operator<<(const std::ostream& InOStream, const MyVector& InVector)
	// operator<<() 함수 내부에서 InOStream 매개변수의 수정이 필요하기 때문에 안됩니다. 출력이 불가능해집니다.
	// 더군다나, InOStream을 다시 반환 해야하는데, const로 받았다가 const가 안달린 채로 반환한다면 어떻게 될까요?
	// 이런 짓은 하면 안됩니다. const로 받았어야할 이유가 응당 있었을 텐데, 그걸 함부로 없애는 짓입니다.

// 그럼 반환 자료형에 const는?
const std::ostream& operator<<(std::ostream& InOStream, const MyVector& InVector)
	// cout << Vector01 << endl; 과 같은 코드에서 문제가 생깁니다. endl 출력 불가능.

 

 

연산자 +=의 오버로딩

Vector01 += Vector02;

이 코드는 Vector01 = Vector01 + Vector02와 내부 동작이 같지 않다.

Vector01.operator+=(Vector02); 와 동일한 코드.

// MyVector.cpp

#include "MyVector.h"


std::ostream& operator<<(std::ostream& InOStream, const MyVector& InVector)
{
	InOStream << '(' << InVector.X << ", " << InVector.Y << ')';

	return InOStream;
}

MyVector::MyVector()
	: X(0)
	, Y(0)
{
}

MyVector::MyVector(float InX, float InY)
	: X(InX)
	, Y(InY)
{
}

MyVector::MyVector(const MyVector& InOtherVector)
	: X(InOtherVector.X)
	, Y(InOtherVector.Y)
{
}

MyVector::~MyVector(void)
{
}

void MyVector::PrintX() const
{
	std::cout << X << std::endl;
}

void MyVector::PrintY() const
{
	std::cout << Y << std::endl;
}

float MyVector::GetX() const
{
	return X;
}

float MyVector::GetY() const
{
	return Y;
}

void MyVector::SetX(float InX)
{
	X = InX;
}

void MyVector::SetY(float InY)
{
	Y = InY;
}

bool MyVector::operator==(const MyVector& InOtherVector) const
{
	return X == InOtherVector.X && Y == InOtherVector.Y;
}

MyVector MyVector::Multiply(float InScalar) const
{
	MyVector Result = MyVector();
	Result.SetX(X * InScalar);
	Result.SetY(Y * InScalar);

	return Result;
}

MyVector MyVector::Multiply(const MyVector& InOtherVector) const
{
	MyVector Result = MyVector();
	Result.SetX(X * InOtherVector.GetX());
	Result.SetY(Y * InOtherVector.GetY());

	return Result;
}

/*
MyVector MyVector::operator+=(const MyVector& InOtherVector)
{
	MyVector Result;
	Result.SetX(X + InOtherVector.GetX());
	Result.SetY(Y + InOtherVector.GetY());

	return Result;
}
*/

MyVector MyVector::operator+=(const MyVector& InOtherVector)
{
	X = X + InOtherVector.GetX();
	Y = Y + InOtherVector.GetY();

	return *this;
}
// Main.cpp

#include <iostream>

#include "MyVector.h"

int main(void)
{
	MyVector Vector01 = MyVector(1, 2);
	MyVector Vector02 = MyVector(3, 4);
	Vector01 += Vector02;

	std::cout << Vector01 << std::endl;
	
	MyVector Vector03 = MyVector(5, 6);
	Vector03 += Vector01 += Vector02;

	std::cout << Vector03 << std::endl;

	return 0;
}

 

 

const 함수로 선언하지 않도록 주의

MyVector& operator+=(const MyVector& InOtherVector) const;
	// 이렇게 선언해버리면 함수 내부에서 this를 수정할 수 없습니다.

 

 

연산자 오버로딩 시 주의할 점

  1. 새로운 연산자 기호를 만들 수는 없다.
  2. 오버로딩 할 수 없는 연산자가 존재한다.
    1. .(멤버 접근 연산자)
    2. ::(범위 지정 연산자) 
    3. ?:(삼항 연산자)

 

대입 연산자

operator= 연산자는 복사 생성자가 하는 일과 비슷해보인다.

다만, 대입 연산자는 기존 메모리 해제가 필요할 수 있고, 자기 자신을 대입하는 경우에 대한 예외 처리가 필요하다.

 

// Main.cpp

#include <iostream>
#include <string>

int main(void)
{
	std::string String01 = "String01";
	std::string String02 = "String02";
	String02 = String01;
		// 1. 이미 생성되어 있던 객체 String02에 이미 생성되어 있던 객체 String01을 대입. 대입 연산자가 호출됩니다.
		// 2. String02는 기존에 가지고 있던 메모리를 해제하고, 
		//    필요하다면 String01에 맞게 새로운 메모리를 할당받고 데이터를 복제합니다.
	
	return 0;
}
// Main.cpp

#include <iostream>
#include <string>

int main(void)
{
	std::string String01 = "String01";
	std::string String02(String01);
		// String02는 이제막 생성되고 있는 새 객체. 그러니 복사 "생성자"가 호출됩니다.
	
	return 0;
}
// Main.cpp

#include <iostream>
#include <string>

int main(void)
{
	std::string String01 = "String01";
	std::string String02 = String01; 
		// Q. 이건 대입 연산자가 호출될까요, 복사 생성자가 호출될까요?
	
	return 0;
}

대입 연산자는 이미 존재하는 객체에 값을 덮어 씌울 때 사용하는 것이다.

= 기호가 있어도 선언과 동시에 초기화하면 복사생성자가 호출된다.

 

 

  복사 생성자 대입 연산자
호출 시점 객체 생성 시 이미 존재하는 객체에 대입 시
객체 상태 새로 태어남 이미 존재함
기호 = (선언과 동시에) = (선언 이후)

 

 

operator = 구현이 안 되어 있으면 컴파일러가 operator= 연산자를 자동으로 만들어준다.

컴파일러가 만들어준 대입 연산자는 기본적으로 멤버별 복사이고, 얕은 복사다.

따라서 포인터가 멤버변수로 있다면 문제가 된다.

 

MyString 클래스의 대입 연산자 구현

MyString& MyString::operator=(const MyString& InOther)
{
	// 대입연산자는 기존에 있던 객체에 가해지는 연산자.
	// 이때문에 기존 메모리를 먼저 해제해야할 수도 있습니다.

	std::cout << "operator=(const MyString& InOther) has been called." << std::endl;

	if (this == &InOther)
	{
		return *this; // 자기 자신 대입 방지
	}

	delete[] Data;  // 기존 메모리를 먼저 해제.
	Data = nullptr;
	
	Size = InOther.Size;
	Capacity = InOther.Capacity;

	Data = new char[Capacity];
	memcpy(Data, InOther.Data, sizeof(char) * Size);
	Data[Size] = '\0';

	return *this;
}

 

자기 자신의 메모리를 해제할 수는 없으므로 if문으로 방지해준다.

 

  • A = B 실행 시
    • 1. 자기 자신 대입인지 확인 (A = A 방지)
    • 2. A의 기존 메모리 해제 (메모리 누수 방지)
    • 3. B의 Size, Capacity 복사
    • 4. 새 메모리 할당
    • 5. memcpy로 B의 데이터 복사
    • 6. 문자열 끝에 '\0' 추가
    • 7. *this 반환 (체이닝 지원)

 

memcpy란

Memory Copy의 약자로, 메모리를 통째로 복사하는 C함수.

memcpy(목적지, 출처, 복사할_바이트_수);

memcpy(Data, InOther.Data, sizeof(char) * Size);
//     ↑목적지  ↑출처         ↑복사할 크기

 

for문으로 복사하는 것보다 빠르다.

// memcpy - 한 번에 복사 (빠름) ✅
memcpy(Data, InOther.Data, sizeof(char) * Size);

// for문 - 한 글자씩 복사 (느림)
for (int i = 0; i < Size; i++)
{
    Data[i] = InOther.Data[i];
}

 

암시적 함수의 호출 막기

  • 암시적 함수의 종류
    • 매개변수 없는 기본 생성자, 복사 생성자, 소멸자, 대입 연산자...

 

암시적 기본 생성자 호출 막기

// 방법 1. 생성자를 하나라도 정의
public:
	MyVector(const int InX, const int InY);
// 방법 2. private 접근 제어자에 생성자 구현
private:
	MyVector() {};

 

 

암시적 복사생성자 호출 막기

private:
	MyVector(const MyVector& InOtherVector) {}

 

암시적 소멸자 호출 막기

// MyVector.h

class MyVector
{
private:
	~MyVector() {};

};

// Main.cpp

#include "MyVector.h"

int main(void)
{
	MyVector Vector01; 
		// 위와 같이 private 접근 제어자에 소멸자가 작성되어 있다면
		// 스택에 객체를 만드는 것은 안됩니다. 함수 종료시 소멸자가 호출될 수 없기 때문입니다.
		
	MyVector* Vector02 = new MyVector();
		// 윗 줄은 컴파일 됩니다.
	delete Vector02;
		// 윗 줄은 컴파일 에러 납니다.
	Vector02 = nullptr;

	return 0;
}

 

암시적 소멸자 호출 막기

private:
    MyVector& operator=(const MyVector& InOtherVector);

 

 

private에 선언해주면 된다.