오버로딩
반환형은 오버로딩의 판단 기준이 안 된다. 이름과 매개변수가 기준이다.
복사 생성자
복사 생성자(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를 수정할 수 없습니다.
연산자 오버로딩 시 주의할 점
- 새로운 연산자 기호를 만들 수는 없다.
- 오버로딩 할 수 없는 연산자가 존재한다.
- .(멤버 접근 연산자)
- ::(범위 지정 연산자)
- ?:(삼항 연산자)
대입 연산자
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에 선언해주면 된다.
'코딩 학습 > C와 C++' 카테고리의 다른 글
| C++ 기초 - Casting, inline 키워드, static 키워드, 예외 처리 (1) | 2026.04.08 |
|---|---|
| C++ 기초 - string, File I/O (0) | 2026.04.08 |
| C++ 기초 - 생성자와 소멸자 (0) | 2026.04.06 |
| C++ 기초 - Console IO (0) | 2026.04.03 |
| C++ 배치고사 선택 문제 (0) | 2026.04.03 |