복사생성자
// C
없음
-------------------------------------------------------------------------
// JAVA
없음
--------------------------------------------------------------------------
// C++
// Vector.h
class Vector
{
pubilc:
Vector(const Vector& other);
private:
int mX;
int mY;
};
// Vector.cpp
Vector::Vector(const Vector& other)
: mX(other.mX)
, mY(other.mY)
{
}
// other 는 같은 클래스로 구현된 다른 개체로, 동일한 클래스에 있기 때문에 private 멤버에 접근 가능
여러 인스턴스로 개체를 복사할테니 이 부분을 생성자에서 처리하도록 유도한게 복사 생성자
생성자 구문이
Vector(const Vector& other);
이처럼 생성자의 파라미터가 나 자신일 경우 복사 생성자로 취급할 수 있다.
복사생성자:
다른 개체를 이용하여 새로운 개체를 초기화
Vector(const Vector& other);
Vector a; // 매개변수 없는 생성자를 호출
Vector b(a); // 복사 생성자를 호출
암시적 (implicit) 복사 생성자
코드에 복사 새성자가 없는 경우, 컴파일러가 암시적 복사 생성자를 자동 생성
// Vector.h
class Vector
{
private:
int mX;
int mY;
};
|
|---------------|
| 컴파일러 |
|---------------|
|
V
//Vector.obj
class Vector
{
public:
Vector() {}
Vector(const Vector& other)
: mX(other.mX)
, mY(other.mY)
{
}
private:
// ...
}
컴파일러는 Vector 클래스의 멤버 변수를 알기 때문에, 복사 생성자에서 생성되는 개체의 멤버 변수를 초기화 시킬 수 있다.
: mX(other.mX)
, mY(other.mY)
암시적 복사 생성자는 얕은 복사(shallow copy)를 수행한다
- 멤버 별 복사
- 각 멤버의 값을 복사
- 개체인 멤버 변수를 그 개체의 복사 생성자가 호출됨(재귀적 호출)
얕은 복사 생성자가 클래스에 포인터 형 변수가 존재한다면 어떻게 될까?
// ClassRecord.h
class ClassRecord
{
public:
classRecord(const int* scores, int count);
~ClassRecord();
private:
int mCount;
int* mScores;
};
// ClassRecore.cpp
ClassRecord::ClassRecord(const int* scores, int count)
: mCount(count)
{
mScores = new int[mCount];
memcpy(mScores, scores, mCount * sizeof(int));
}
ClassRecord::~ClassRecord()
{
delete[] mScores;
}
여기서 컴파일러가 암시적 복사 생성자를 추가 해준다면
// ClassRecore.cpp
ClassRecord::ClassRecord(const int* scores, int count)
: mCount(count)
{
mScores = new int[mCount];
memcpy(mScores, scores, mCount * sizeof(int));
}
//*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
ClassRecord::ClassRecord(const Class& other)
:mCount(other.mCount)
,mScores(othet.mScores)
{
}
//*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
ClassRecord::~ClassRecord()
{
delete[] mScores;
}
이 상태에서
//Main.cpp
ClassRecord classRecord(scores, 5);
ClassRecord* classRecordCopy = new ClassRecord(classRecord);
delete classRecordCopy;
개체 하나를 생성하고, 해당 개테를 통해 다른 개체를 생성할 때,
이 포인트형 멤버 변수가 문제를 일으킨다
(예시는 메모리 구조 이해를 위해서 일부러 heap 영역에 복사 개체를 생성하게 작성)
위에서 언급한대로 암시적 복사 생성자는 얕은 복사를 하기 때문에,
포인터형 변수의 주소를 그대로 새로운 개체에게 복사해준다.
따라서 처음 생성한
classRecord.mScore
와
classRecorcCopy.mScore
는
동일한 주소를 가르키고 있기 때문에
delete classRecordCopy 에서 해당 개체가 사라질 때, 동시에 mScore 의 데이터와 메모리가 해제되고
결과적으로 classRecord.mScore 도 가리키던 데이터와 메모리가 해제됨으로 인해
유효하지 않은 주소를 가리키는 Dangling pointer(댕글링 포인터) 가 되는 셈이다
그럼으로 멤버 변수에 heap 영역을 할당했을 경우 암시적 복사 생성자에 의존하지 말고, 프로그램 작성시 직접 명시를 통해 처리해주는 것이 안전한 프로그램을 만드는 방향이다
ClassRecord::ClassRecord(const ClassRecord& other)
:mCount(otehr.mCount)
{
mScores = new int[Count];
memcpy(mScores, other.mScores, mCount * sizeof(int));
}
직접 복사 생성자를 만들어서 깊은 복사(deep copy) 를 할 수 있도록 명시해준다
(포인터 변수가 가리키는 실제 데이터까지도 복사)
함수 오버로딩
메서드 오버로딩 언어별 비교
// C
없음
--------------------------------------------------------------------------------
// JAVA
public class X
{
public void Print(int score);
public void Print(String name);
public void Print(double gpa, String name);
}
---------------------------------------------------------------------------------
// C++
class X
{
public:
void Print(int score);
void Print(const char* name);
void Print(float gpa, const char* name);
};
오버로딩은 매개변수 목록을 제외하고는 모든 게 동일
반환형은 상과없음
void Print(int score);
void Print(const char* name)
void Print(float gpa, const char* name);
int Print(int score);
int Print(float gpa);
위 처럼 함수 오버로딩을 작성해 나갈 때의 결과는 다음과 같다
1. int 파라미터 함수 생성
2. char* 파라미터로 함수 오버로딩 유효
3. float 과 char* 로 두 개의 파리미터로 함수 오버로딩 유효
4. int 파라미터로 함수 오버로딩 무효(컴파일 에러, 반환형 차이는 오버로딩에 영향을 주지 않는다)
5. float 파리미터로 함수 오버로딩 유효
함수 오버로딩 매칭
함수 오버로딩 매칭에는 크게 3가지 경우가 존재한다.
- 가장 적함한 함수 하나 찾음 - 매칭 성공
- 매칭되는 함수를 찾을 수 없음 - 컴파일 에러
- 가장 적함한 함수를 여러 개 찾음(어떤 것을 호출해야할 지 모호) - 컴파일 에러
void Print(int Score);
void Print(cosnt char* name);
void Print(float apg, const char* name);
----------------------------------------------
Print(100);
Print("Ruby");
Print(4.0f, "Ruby");
Print(77.5f);
Print("Ruby", 4.0f);
1. 1번 함수 호출
2. 2번 함수 호출
3. 3번 함수 호출
4. 1번 함수 호출(암시적 형변환으로 호출 가능, 컴파일에 따라 경고 문구 발생)
5. 컴파일 에러
함수 매칭 순서
함수 매칭 순서를 살짝만 본다면
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;
}
이렇게 구성되어 있을 떄, main 에서 Max 를 호출했기 때문에 Min 을 제외하고 매칭을 해본다
Max() 중에서도 배열을 받는 함수는 매칭이 되는 값이 없기 때문에 제외한다면
int Max(int, int) 와 int Max(double, double) 이 남게 된다.
그럼 이제 인자와 파라미터의 매칭을 해보는데
(1, 3.14) <-> (int, int)
1 <-> int : 정확한 매칭
3.14 <-> in t : 표준 변환
--------------------------------
(1, 3.14) <-> (double, double)
1 <-> double : 표준변환
3.14 <-> double : 정확한 매칭
이렇게 둘 다 하나씩 매칭과 변환으로 인해 구분이 어렵다면 모호성으로 인해 컴파일 에러가 발생한다.
한 쪽이 전부 정확한 매칭을 이룰 경우엔 매칭이 더 잘된 것을 호출하게 되어있다
(인자가 3개, 4개 일 경우에도 마찬가지로 정확한 매칭이 많은 쪽의 함수를 호출한다는 것)
연산자(operator) 오버로딩
연산자 : 함수처럼 작동하는 부호
C++ 에서는 프로그래머가 연산자를 오버로딩 할 수 있다
//Vector.h
class Vector
{
public:
Vector operator+(const Vector& rhs) const;
private:
int mX;
int mY;
};
// Vector.cpp
Vectoer Vector::operator+(const Vector& rhs) const
// Vector 를 반환하는 생성된 Vector + const Vector& rhs 라고 이해하는 게 빠르다
{
Vector sum;
sum.mx = mX + rhs.mX;
sum.mY = mY + rhs.mY;
return sum;
}
// main.cpp
Vector v1(10, 20);
Vector v2(3, 17);
Vector sum = v1 + v2;
여기서 마지막 v1 과 v2 가 + 에 오버로딩된 연산자 오버로딩에 의해 계산값으로 주어지고, 반환된 결과룰 sum 으로 받게 된다.
연산자 오버로딩은 부호는 같지만 여러가지 연산을 가능하게 해준다
int1 = int1 + int2 >> 두 int 형 변수 더하기
float1 = float1 + float2 >> 두 float 형 변수 더하기
name = firstName + lastName >> 두 string 형 변수 더하기
연산자 오버로딩은 방법은 두 가지가 존재
- 멤버 함수
- Vector sum = v1 + v2;
- Vector sum = v1.operator+(v2); ---- 위와 동일한 방식
- std::cout << number;
- std::cout.operator<<(number); --- 위와 동일한 방식
- 특정 연산자들은 멤버 함수를 이용해서만 오버로딩 가능하다 (=, (), [], ->)
- 멤버 아닌 함수
연산자 오버로딩 구체적 분석
std::cout << vector1; 구현하기
생성 시
Vector vector1(10, 20)
기대 결과
10, 20
std::cout.operator<<(vector1)
이 방법으로 하면 되지 않을까? 하는 생각을 해보지만
cout 은 표준 헤더에 있는 라이브러리로 우리가 임의로 연산자 오버로딩을 삽입할 수 없다! (해서도 안된다)
이렇게 좌항이 되는 개체에 접근할 수 없을 때, 일반 함수 즉, 전역 함수를 통해서 구현해 볼 수 있다.
그럼 다음과 같이 작성하면 되는건가?
void operator<<(std::ostream& os, const Vector& rhs)
// 전역 함수이기 때문에 좌항으로 사용할 개체를 파라미터로 받는다
{
os << rhs.mX << ", " << rhs.mY;
}
눈썰미가 좋은 사람들은 이 코드는 잘못된 코드라는 걸 알 수 있다.
개체가 아닌 전역 함수에선 Vector 개체의 멤버 변수(rhs.mX, rhs.mY) 접근은 불가능하기 때문이다.
이 문제를 해결하기 위해 frined 키워드가 존재한다.
( 윤성우의 열혈 C++ chpt9. 여기서 조금이나마 frined 키워드를 다루었으니 같이 참고해도 좋다)
freind 키워드는 일방적으로 해당 개체가 friend 키워드로 선언한 함수 또는 개체에게
자신의 prinvate 영역의 멤버 변수의 접근에 허용해주는 키워드이다.
//X.h
class X
{
private:
int mPrivateInt;
};
// Y.h
#include "x.h"
class Y
{
pulic:
void foo(X& x);
};
// Y.cpp
void Y::Foo(X& x)
{
x.mPrinvaInt += 0;
}
이 코드에선 C++ 을 조금 아는 사람들은 Y,cpp 파일에서 x 의 멤버 변수에 값을 증가하는 행위는 컴파일 에러가 발생하는 것을 알 수 있다.
private 영역의 멤버 변수는 외부에서 접근한 수 없기 때문이다.
여기서 X 클래스가 Y 클래스에게 friend 키워드를 사용함으로써 접근을 허용할 수 있게 해줄 수 있다
(키워드 이름은 freind 이지만 사실상 친구비를 자처하는 셈이 아닌가 싶다)
//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(X7 x)
{
x.mprivateInt += 10;
}
X 클래스가 Y 클래스에게 friend 키워드를 통해 일방적 자신의 멤버 변수의 접근을 허용했기 때문에
Y.cpp 에선 원래 컴파일 에러가 발생해야 할 문구가 유효 판정으로 에러없이 구동한다.
(여기서 나처럼 헷갈리는 사람이 있을까봐 살짝 적어본다면, X 헤더에서 Y 클래스를 명시하는데 있어 Y 헤더를 include 해야되는 것이 아닐까? 하는 의문이 있었다.
X 클래스가 Y 클래스에게 자신을 받친 것이지, Y 클래스 자체를 다룰 것이 아니기 때문에 Y 헤더를 부를 필요가 없고,
Y 클래스에선 X 의 멤버 변수를 다뤄야 하기 때문에 X 의 헤더를 include 해줘야 된다.
이런 이유로 X 에서 friend 로 선언된 개체나 함수는 X 클래스에서 사용되는 것이 아니기 때문에
멤버 변수 또는 멤버 함수라고 할 수 없다)
이번엔 Y 개체 없이 Foo 함수에 적용 시키는 코드도 확인해보자
//X.h
class X
{
friend void Foo(X& x);
private:
int mPrivateInt;
};
// GlobalFunction.cpp
void Foo(X& x)
{
x.mPrivateInt += 10;
}
이렇게 전연 함수에도 freind 키워드를 통해서 private 영역에 접근할 수 있다.
그럼, 아까 Vector 를 통한 연산자 오버로딩을 작성한다면 어떻게 하면 될까?
class Vector
{
friend void operator<<(std::ostream& os, const Vector& rhs);
}
void operator<<(std::ostream& os, const Vector& rhs)
{
os << rhs.mX << ", " << rhs.mY;
}
같은 방식으로 Vector 에서 operator<< 함수를 friend 키워드로 정의해주면,
해당 operator 함수에서 멤버 변수에 접근하여 우리가 하고자 했던
Vector vector1(10, 20);
std::cout << vector1;
------------------------------------------
// 결과
10, 20
그런데 이렇게만 작성했다면 된걸까?
다음과 같이 작성을 한다면?
std::cout << Vector1 << std::endl;
<< 이 원래 기능으로 돌아가기 때문에 문제가 없을까?
아님
<< 가 계속 유지 되는 바람에 에러가 입력 파라미터가 매칭되지 않아서 문제가 생길까?
답은 전혀 다른 내용으로
연산자 오버로딩에 반환값이 없어서 에러가 뜬다
두 번쨰 << 연산자는 오리지널 << 연산자로 사용되지만, 이 연산자도 파라미터가 존재하는 연산자 함수이다
iostream 에서 제공하는 오리지널 << 연산자는 다음과 같이 정의되어 있다
std::ostream& operator<<(std::ostream& os, const T& val);
반환값이 ostream 인것을 확인할 수 있다.
즉
std::cout << vector1 << std::endl;
이 구문은
operator<<(operator<<(std::cout, vector1), std::endl);
이렇게 사용되는 것이고,
반환값을 기대하는 << 연산자 함수에게 오버로딩된 << 연산자 함수가 반환값이 void 이라 반환 값이 없어서,
전달해 줄 인자가 없는 관계로 에러가 발생하는것
그렇기 떄문에
std::ostream& operator<<(std::ostream& os, const Vector& rhs)
{
os << rhs.mX << ", " << rhs.mY;
return os;
}
다음과 같이 반환값을 설정해줌으로써 연속된 << 연산자에서도 문제가 발생하지 않게 할 수 있다.
연산자 오버로딩시에는 해당 연산자가 표준적으로 어떤 타입을 반환하는지 확인하고 따라야
사용자가 기대하는 방식대로 작동하고, 연쇄 호출도 가능하며, 컴파일 에러도 피할 수 있다.
연산자 오버로딩과 const
Vector operator+(const Vector& rhs) const;
const 를 사용하는 이유
- 멤버 변수의 값이 바뀌는 것을 방지
- 최대한 많은 곳에 const 를 분일 것
- 지역(local) 변수 까지도
const & 를 사용하는 이유
Vector operator+(const Vector& rhs) const;
std::ostream operator<<(const std::ostream& os, const Vector& rhs)
이렇게 const& 를 사용함으로써
- 불필요한 개체의 사본이 생기는것을 방지
- 파라미터가 값을 복사하기 위해 복사 생성자를 생성하는 것을 const & 를 통해 참조를 활용하게 유도
- 멤버 변수가 바뀌는 것도 방지
사실 위의 두 번째 코드의
const std::ostream& os
이 부분은 잘못된 const 의 사용법이다
os 를 통해서 출력할 건데, ostream 은 내부의 버퍼나 상태 플래그 등이 출력하고자 하는 형태에 따라 달라지고,
그에 따라 내부적으로 상황에 맞게 write 작업을 하도록 구현되어 있는데
이를 const 로 고정시켜버리면 내부에서 write 동작이 금지 됨으로써
read-only 에 write 작업을 하려는 행위이기 때문에 컴파일 에러가 난다
또한 반환 ostream 이 non-const 인데 os 를 반환 한다면 이는 const 를 non-const 로 변환 시키는 행위라 컴파일 에러가 난다
그래서 위 같은 경우는
std::ostream operator<<(std::ostream& os, const Vector& rhs)
이렇게 필요한 부분에만 const 를 붙혀주는 게 좋다.
이 잘못된 예시를 넣은 이유는 어찌되었든 const 를 조금 남발을 하더라고, 안전하게 코드를 작성하는 것이 좋고,
설사 const 를 잘못 붙혔다 하더라도 컴파일에서 잡아 주기 때문에
의도치 않은 변경 값이 발생하는 것보단, 확실한 곳의 에러 발생이 프로그램 구현에 안정성을 조금 더 기여하기 때문이다.
위와 비슷하게 연산자 오버로딩에 const 를 사용하지 않는 경우는 다음과 같은 경우도 있다
( += 연산자 오버로딩으로 예시 )
vector1 += vector2;
==
vector1 = vector1.operator+=(vector2);
-----------------------------------------------
Vector operator+=(const Vector& rhs) [ ] <- const? X
이 케이스는 좌항인 vector1 을 바꾸는 것이기 때문에 함수에 const 를 사용할 수 없다.
함수에 const 를 사용하면 해당 함수는 개제 자신(*this) 을 수정하지 않겠다는 의미라
+= 연산자 오버로딩의 수정하려는 행위와 충돌이 생기기 때문이다.
다음과 같이 참조를 통한 연산자 오버로딩 시
Vectore& opearotr+=(const Vector& rhs)
개체 복사가 없는 장점이 있음
----------------------------------------------------
vector1 += vector2 += vector3
그러나 이런식으로 사용할 시 가독성이 떨어짐
(연산의 우선순위를 파악하기 어려움)
복사 없이 체이닝을 막으려면 다음과 같이 좌항에 const 를 붙이면 된다.
const Vectore& opearotr+=(const Vector& rhs)
반환 객체가 const 인지라 read_only 가 되기 때문에 write 가 목적인 연산자 오버로딩은 non-const 라서 컴파일러에서 에러를 띄우게 된다.
연산자 오버로딩 제한사항
연산자 오버로딩의 남용 지양
Vector vector = vector1 << vector2;
Vector Vector::operator<<(const Vector& rhs) const
{
Vector cross;
corss.mX = mY * rhs.mZ - mZ * rhs.mY;
cross.mY = mZ * rhs.mX - mX * rhs.mZ;
cross.mZ = mX * rhs.mY 0 mY * rhs.mX;
return cross;
}
이와 같인 연산자만으론 알 수 없는 과한 오버로딩은 지양해야 할 부분이다
또는 다음처럼
Vector vector = vector1 * vector2;
연산의 의도는 이해할 수 있으나, 본질 적으로 무엇을 연산 할 것인지 뚜렷하지 않은(외적? 멤버 변수별 연산?) 것도 지양해야 한다.
대입(assignment) 연산자
- operator=
- 값을 대입해주는 연산자
- 복사 생성자와 거의 동일
- 대입 연산자는 대입이 되는 메모리의 해제 과정이 필요할 수가 있다는 부분의 차이가 있음
- 복사 생성자를 구현했다면 대입 연산자도 구현해야 한다
암시적 operator=
operator= 구현이 안 되어 있으면 컴파일러가 operator= 연산자를 자동으로 생성시켜 준다
//Vector.h
Class Vector
{
private:
int mX;
int mY;
};
|
|---------------|
| 컴파일러 |
|---------------|
|
V
//Vector.obj
class Vector
{
public:
Vector() {}
Vector& operator=(cosnt Vector& rhs)
{
mX = rhs.mX;
mY = rhs.mY;
return *this;
}
};
암시적 opearto= 연산자 오버로딩 또한 얕은 복사이기 때문에 이전 복사 생성자에서의 유의점이 그대로 반영된다
암시적 함수들을 제거하는 방법
클래스에 따라오는 기본 함수들
- 매개변수 없는 생성자
- 복사 생성자
- 소멸자
- 대입(=) 연산자
[ 암시적 생성자를 지우는 방법 ]
명시적으로 복사생성자를 작성하는 방법이 있으며,
다음과 같은 방법도 있다.
//Vector.h
class Vector
{
public:
private:
Vector() {};
int mX;
int mY;
};
//main.cpp
Vector v1; // 컴파일 에러
생성자를 private 에 생성함으로써 암시적 기본 생성자의 생성을 못하게 막을 수 있다
[ 암시적 소멸자를 지우는 방법 ]
//Vector.h
class Vector
{
public:
private:
~Vector() {}
int mX;
int mY;
};
//main.cpp
Vector v1; // 컴파일 에러
Vector* V2 = new Vector();
delete v2; // 컴파일 에러
Vector v1은 스택에 생성된 객체이므로, 스코프가 닫힐 때 호출할 소멸자 코드를 컴파일 타임에 생성해야 한다.
그런데 소멸자가 private이면, 컴파일러는 그 소멸자에 접근할 수 없어 호출 코드를 생성하지 못하고 컴파일 에러를 발생시킨다.
반면 Vector* v2 = new Vector();처럼 힙에 생성된 객체는 delete를 통해 명시적으로 소멸자를 호출할 수 있지만,
이 역시 소멸자가 private이면 delete 시점에 에러가 발생한다.
그러나 개체를 프로그램 종료시 까지 유지 할 것이 아닌 이상 기본 소멸자 생성을 제하는 방법을 사용할 필요성은 거의 없다고 본다.
[ 암시적 operator= 지우는 방법 ]
//Vector.h
class Vector
{
public:
private:
const Vecto& opeartor=(const Vector& rhs); //별도로 함수를 정의하지 않아도 유효함
int mX;
int mY;
};
//Main.cpp
Vector v1;
Vector v2;
v2 = v1; // 컴파일 에러
함수를 정의하지 않아도, 컴파일러가 해당 opeartor= 가 private 에 존재하기 때문에
정의 오류가 아닌 접근 불가 오류를 뱉게 된다.
'C++ > FOCU_C++' 카테고리의 다른 글
C++ chpt8. (1) | 2025.07.21 |
---|---|
C++ chpt7. (2) | 2025.07.01 |
C++ chpt6. (1) | 2025.05.16 |
C++ chpt5. (0) | 2025.05.04 |
C++ chpt4. (0) | 2025.05.03 |