티스토리 뷰
복사(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개가 있음
- 매칭되는 함수를 찾을 수 없음 : 컴파일 에러 🔴
- 매칭되는 함수 여러개 찾음(누굴 호출할지 모호) : 컴파일 에러 🔴
- 가장 적합한 함수를 하나 찾음 : 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. 이게 제대로 동작하는가
- std::cout << vector1
- operator<<(std::cout, vector1);
- 🚨 근데 반환이 void
- 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;
}
};
- 복사 생성자와 마찬가지로, 얕은복사 🔥
암시적 함수들을 제거하는 법
- 언어차원에서 암시적으로 만드는 경우가 많다
- 매개변수 없는 생성자
- 복사 생성자
- 소멸자
- 대입(=) 연산자
- 제거해보자
암시적 기본생성자를 “지우는” 법
- 다른 생성자를 작성
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
링크
TAG
- 톰캣11
- 논문추천
- 엔티티 설계 주의점
- 연관관계 편의 메서드
- 이진탐색
- tomcat11
- core c++
- CPU
- Dispatcher Servlet
- sleep lock
- tree
- Java
- 백준
- servlet
- S4
- JPA
- C
- pocu
- 개발 공부 자료
- reader-writer lock
- Memory
- PS
- thread
- generic swap
- condition variable
- OOP
- Spring MVC
- S1
- generic sort
- 객체 변조 방어
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함