짧은 설명
- nullptr로 초기화를 한다면 반드시 문자열 생성때 null 체크를 해줘야한다.
- strcpy_s : 버퍼 오버플로우를 방지하는 문자열 복사 함수
- c_str() : 문자열 객체의 현재 값을 나타내는 null을 포함하는 배열의 포인터를 반환하는 함수
- [] 연산자는 경계검사를 하지 않는다.
객체 복사
- 이해가 안된다면 생성자를 만들때 기본생성자, 소멸자, 복사 생성자, 대입 연산자, 이동생성자, 이동 대입 연산자 모두 구현해라
- l-value , r-value는 오버로딩 조건이 될 수 있다.
이동 생성자와 이동 대입 연산자
int num = 10;
- l-value : 읽고 쓸 수 있는 변수
- r-value : 임시객체, 리터럴 상수, 반환값
- 메모리는 잡히지만 한번 사용하면 메모리가 버려진다.
- 값을 할당할 수 없기에 r-value이다.
- 반환값도 반환되는 값만 사용할 수 있기에 r-value 다.
class String {
public:
String() { /*...*/ }
String(const char* str) { /*...*/ }
String(const String& rhs) { /*...*/ } //복사 생성자
String(String&& rhs) noexcept // 이동 생성자 / r-value를 참조한것이다.
{
cout << "String(String&&) : " << this << endl; //이동대입 연산자 호출 확인용 출력
len = rhs.len;
strData = rhs.strData; // 포인터만 복사
rhs.strData = NULL; // 원본 무효화
}
~String() { /*...*/ }
String& operator=(const String& rhs) { /*...*/ }
String& operator=(String&& rhs) { // 이동 대입 연산자
cout << "String &operator=(String&&) : " << this << endl;
len = rhs.len;
strData = rhs.strData; // 포인터만 복사
rhs.strData = NULL; // 원본 무효화
rhs.len = 0 //모든 멤버를 초기화 시켜주는것이 좋다.
return *this;
}
char* GetStrData() const { /*...*/ }
int GetLen() const { /*...*/ }
private:
void alloc(int len) { /*...*/ }
void release() { /*...*/ }
char* strData;
int len;
};
String getName() {
cout << "===== 2 =====" << endl;
String res("Doodle");
cout << "===== 3 =====" << endl;
return res;
}
int main() {
String a;
cout << "===== 1 =====" << endl;
a = getName(); // 이동 대입 연산자 호출
cout << "===== 4 =====" << endl;
}
- r-value 참조 (&&)를 사용
- l-value는 & 하나를 참조로 사용한다.
- &&를 받는 복사 생성자도 오버로딩 조건이 된다.
- l-value가 정의 안되어있다면 r-value 참조 오버로딩 된 매개변수로 들어가게 된다.
- r-value 레퍼런스 변수에 l-value를 넣을 수 는 없다.
- r-value 즉, 이동 생성자의 매개변수에는 const를 붙이면 안된다.
- 초기화 후 임시 객체의 값은 0 또는 그에 준하는 수치로 초기화를 해줘야한다.
- 깊은 복사 대신 포인터만 이동
- 즉, 주소의 소유권을 이전하는 것이다.
- 메모리를 새로 할당 할 필요 없다.
- 원본 객체는 NULL로 설정하여 무효화
- 불필요한 메모리 할당/해제를 많이 줄일 수 있다.
메모리 관리 규칙
- 할당과 해제의 쌍: new와 delete, new[]와 delete[] 반드시 쌍으로 사용
- 소유권 명확화: 각 객체가 자신의 메모리에 대한 책임 명확히
- RAII 원칙: 생성자에서 자원 획득(즉, 생성), 소멸자에서 해제
- 예외 안전성: 생성자에서 예외 발생 시 메모리 누수 방지
동적 할당 Rule of Three/Five
동적 메모리를 관리하는 클래스는 다음을 구현해야 한다. : 4, 5는 r-value 참조 생성 및 대입이다.
- 소멸자: 동적 할당된 자원 해제
- 복사 생성자: 깊은 복사 구현
- 복사 대입 연산자: 자기 대입 검사와 깊은 복사
- 이동 생성자: 효율적인 자원 이동
- 이동 대입 연산자: 효율적인 자원 이동 대입
임시 객체
MyClass obj; //l-value
obj = MyClass(매개변수);
- MyClass(매개변수) : 잠시 메모리가 생겨났다가 대입이 끝나고 삭제되는 객체다.
- 대입이 끝나고 소멸자가 호출되는 것이다.
- 값을 할당할 수 없기에 r-value이다.
noexcept 키워드
- 최적화할때 도움을 주는 키워드다.
- 후에 예외처리에 사용된다.
- 예외처리를 하지 않아도 된다고 사용자에게 알려주는 키워드다.
- 컴파일러가 실행될때 예외처리에 관한 작동을 안해도 되도록 해주는 기능이다.
- 해당 키워드가 붙은 함수가 오류가 생기면 프로그램이 강제로 정지된다.
사용자 정의 변환
- 대체로 사용하지 않는 문법이기에 이런것이 있다고만 알아두면 좋다.
class Item {
public:
Item() {
cout << "Item()" << endl;
}
Item(int num) : num(num) { // int를 Item으로 변환
cout << "Item(int)" << endl;
}
Item(string name) : name(name) { // string을 Item으로 변환
cout << "Item(string)" << endl;
}
Item(int num, string name) : num(num), name(name) {
cout << "Item(int, string)" << endl;
}
private:
int num;
string name;
};
int main() {
cout << "===== A =====" << endl;
Item a1 = Item(1); // 명시적 변환
Item a2(2); // 직접 초기화
Item a3 = (Item)3; // C 스타일 캐스팅
Item a4 = 4; // 암시적 변환 / ㅁ4(4) 로 생각하면 이해될것이다.
Item a5, a6, a7;
a5 = Item(5);
a6 = 6; // 암시적 변환
a7 = (Item)7;
cout << "===== B =====" << endl;
Item b4 = string("Stone"); // string에서 Item으로 변환
cout << "===== C =====" << endl;
Item c1 = Item(1, "Stone");
Item c2(2, "Dirt");
Item c4 = { 3, "Wood" }; // 중괄호 초기화
Item c5, c6;
c5 = Item(4, "Grass");
c6 = { 5, "Water" }; // 중괄호 대입
}
- 이렇게도 동작이 되지만 클래스의 암시적 형변환같은 애매모호한 코드는 작성하지 않는게 좋다.
변환 연산자
class Item {
public:
Item() { }
Item(int num) : num(num) { }
Item(string name) : name(name) { }
Item(int num, string name) : num(num), name(name) { }
//operator int() : int형변환 연산자다.
operator int() const { // Item을 int로 변환
cout << "Item::operator int()" << endl;
return num;
}
//operator string() : string형변환 연산자다.
operator string() const { // Item을 string으로 변환
cout << "Item::operator string()" << endl;
return name;
}
private:
int num;
string name;
};
int main() {
Item i1(1, "Stone");
int inum = i1; // int inum = (int)i1;과 같음 / 역 형변환이다.
string iname = i1; // string iname = (string)i1;과 같음
cout << inum << endl; // 1
cout << iname << endl; // Stone
}
- operator 타입() 형식으로 정의
- 반환 타입을 명시하지 않음
- const 멤버 함수로 구현하는 것이 일반적
explicit 키워드
- 암시적 형변환을 막는 키워드다.
- 의도하지 않은 암시적 변환 방지
- 타입 안정성 향상
- 명시적 변환만 허용
class SafeNumber {
public:
explicit SafeNumber(int value) : value(value) {}
explicit operator int() const { return value; }
private:
int value;
};
// 사용 예
SafeNumber num1(10); // OK
SafeNumber num2 = 20; // 에러! explicit로 인해 암시적 변환 불가
SafeNumber num3 = SafeNumber(30); // OK, 명시적 변환
int x = static_cast<int>(num1); // OK, 명시적 변환
int y = num1; // 에러! explicit로 인해 암시적 변환 불가
사용자 정의 변환 주의사항
- 암시적 변환은 예상치 못한 동작 야기 가능
- 가능하면 explicit 사용으로 명시적 변환만 허용
- 변환의 의미가 명확한 경우에만 구현
상속
- 부모클래스의 멤버를 물려받는 것이다.
- 자식 클래스는 부모 클래스의 private에 접근할 수 없다.
- 간접 참조 : 포인터, 레퍼런스
- 변수들이다.
- 업캐스팅 / 다운캐스팅 : 변수들의 형변환이다.
- 절차 지향 : 순서대로 작업 처리
- ~한다. → 함수의 목적
- 함수들의 집합 처리 프로그래밍을 의미한다.
- 객체 지향 : 주체 우선 처리
- ~한다가 아니라 ‘누가’ 뭘 한다 처럼 주체가 중요한 프로그래밍이다.
- 클래스들의 상호작용이다.
상속 기본
- 상속은 기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 객체지향 프로그래밍의 핵심 개념이다.
- 코드 재사용성 향상: 공통 기능을 기본 클래스에 정의
- 계층적 관계 표현: 현실 세계의 계층 구조를 프로그램으로 모델링
- 계층 관계 : 파일시스템처럼 부모안에 자식안의 자식같이 계층이 나눠진것이다.
- 유지보수 용이: 공통 기능 수정 시 한 곳만 수정
파생 클래스
- 파생 클래스(Derived Class): 상속을 받는 클래스, 자식 클래스라고도 함
- 기본 클래스(Base Class): 상속을 제공하는 클래스, 부모 클래스라고도 함
class 파생클래스명 : 접근지정자 상속받을 기본클래스명 {
// 파생 클래스의 멤버들
};
class Animal {
public:
void Breathe() { cout << "숨을 쉰다." << endl; }
int age;
};
class Dog : public Animal {
public:
void Walk() { cout << "걷는다." << endl; }
};
class Sparrow : public Animal {
public:
void Fly() { cout << "난다." << endl; }
};
int main() {
Dog d;
d.age = 7;
d.Breathe(); // 상속받은 멤버 함수
d.Walk(); // 자신의 멤버 함수
cout << d.age << endl;
Sparrow s;
s.age = 2;
s.Breathe(); // 상속받은 멤버 함수
s.Fly(); // 자신의 멤버 함수
cout << s.age << endl;
}
- Animal 클래스는 모든 동물의 공통 특성인 Breathe() 메서드와 age 속성을 가짐
- Dog 클래스는 Animal을 상속받아 Walk() 메서드를 추가
- Sparrow 클래스는 Animal을 상속받아 Fly() 메서드를 추가
- 파생 클래스 객체는 기본 클래스의 모든 public 멤버에 접근 가능
생성자 호출 순서
- 상속 관계에서 객체 생성 시 생성자 호출 순서:
- 기본 클래스 생성자가 먼저 호출
- 파생 클래스 생성자가 나중에 호출
class Base {
public:
Base() {
cout << "Base()" << endl; // ------- 1
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived()" << endl; // -------- 2
}
};
int main() {
Derived d;
}
객체 생성 / 소멸 순서
- 생성 순서: 기본 클래스 멤버 → 기본 클래스 → 파생 클래스 멤버 → 파생 클래스
- 소멸 순서: 파생 클래스 → 파생 클래스 멤버 → 기본 클래스 → 기본 클래스 멤버
상속이 필요한 경우
코드 중복 제거
- 상속을 사용하지 않으면 여러 클래스에서 동일한 코드를 반복해야 함
- 자세히
- class Image { public: operator string() { return "사진"; } }; class Message { public: Message(int sendTime, string sendName) { this->sendTime = sendTime; this->sendName = sendName; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } private: // 자식 클래스에서도 이 멤버들에 직접적인 접근 불가 int sendTime; string sendName; }; class TextMessage : public Message { public: TextMessage(int sendTime, string sendName, string text) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->text = text; } string GetText() const { return text; } private: string text; }; class ImageMessage : public Message { public: ImageMessage(int sendTime, string sendName, Image* image) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->image = image; } Image* GetImage() const { return image; } private: Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
- 공통 멤버 변수와 메서드를 Message 클래스에 통합
- 코드 중복 제거
- 유지보수가 용이해짐
오버라이드
- 파생 클래스에서 기본 클래스와 동일한 이름의 멤버를 정의하면 기본 클래스의 멤버가 가려짐
- 부모 자식에 같은 이름의 멤버 함수가 있다면, 멤버 하이딩 또는 오버라이딩이 발생한다.
- 멤버 하이딩 :
- 오버 라이딩 :
class Base {
public:
int a = 10;
};
class Derived : public Base {
public:
int a = 20; // Base의 a를 가림 / 멤버 은닉 / 멤버 하이딩
};
int main() {
Base b;
Derived d;
cout << b.a << endl; // 10 출력
cout << d.a << endl; // 20 출력 (Derived의 a)
}
- 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
- 가려진 기본 클래스의 멤버에 접근할 때 유용
- class Base { public: void Print() { cout << "From Base!" << endl; } }; class Derived : public Base { public: void Print() { // Base의 Print()를 오버라이드 cout << "From Derived!" << endl; } }; int main() { Derived d; d.Print(); // Derived의 Print() 호출 d.Base::Print(); // 명시적으로 Base의 Print() 호출 d.Derived::Print(); // 명시적으로 Derived의 Print() 호출 }
- 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
- 가려진 기본 클래스의 멤버에 접근할 때 유용
가상 함수 / 오버라이드 / 동적 바인딩
- 가상 함수 : 멤버함수 앞에 virtual을 선언한다.
- 동작에는 차이가 없다.
- 차이 : 자식 함수에 같은 이름이 있다면 ‘오버라이드’ 되는 것을 의미한다.
- 오버 라이딩 : 동적 바인딩에 의해 호출되는것이다.
- 오버 로딩 : 정적 바인딩에 의해 호출되는 것이다.
- 정적 바인딩 : 컴파일 전에 참조를 결정하는 것이다.
- 바인딩 : 함수를 호출할때 같은 이름의 함수가 여러개일때 어떤 함수를 호출할지 참조를 확인하는 기능이다.
- 동적 바인딩 : 호출할 떄 참조를 확인하여 어떤 함수를 호출할 지 정하는 것이다.
- 부모 클래스의 간접 참조로 자기 자식 객체도 참조할 수 있다.
- 부모* = 자식 객체 주소
class Weapon {
public:
Weapon(int power) : power(power) {
cout << "Weapon(int)" << endl;
}
virtual void Use() { // 가상 함수!!
cout << "Weapon::Use()" << endl;
}
protected:
int power;
};
class Sword : public Weapon {
public:
Sword(int power) : Weapon(power) {
cout << "Sword(int)" << endl;
}
void Use() { // 오버라이드
cout << "Sword::Use()" << endl;
swing();
}
private:
void swing() {
cout << "Swing sword." << endl;
}
};
class Magic : public Weapon {
public:
Magic(int power, int manaCost) : Weapon(power), manaCost(manaCost) {
cout << "Magic(int, int)" << endl;
}
void Use() { // 오버라이드
cout << "Magic::Use()" << endl;
cast();
}
private:
void cast() {
cout << "Cast magic." << endl;
}
int manaCost;
};
int main() {
Sword mySword(10);
Magic myMagic(15, 7);
Weapon* currentWeapon; //현재 Weapon, Sword, Magic을 참조할 수 있다.
currentWeapon = &mySword; //Sword 자식 간접참조 / 동적 바인딩을 위한 참조 할당이다.
currentWeapon->Use(); // 동적 바인딩에 의해 Sword::Use() 호출
}
- virtual 키워드로 선언
- 기본 클래스 포인터로 파생 클래스 객체를 가리킬 때, 실제 객체의 타입에 따라 호출될 함수가 결정됨
- 동적 바인딩(Dynamic Binding): 실행 시간에 호출할 함수가 결정됨
가상 소멸자
- 소멸자 앞에 virtual을 붙인것이다.
- 앞으로 소멸자는 전부 가상 소멸자로 선언해라
- 부모 소멸자가 호출되면 자식의 소멸자가 호출이 안되는 경우가 있기 때문이다.
class Image {
public:
operator string() {
return "사진";
}
};
class Message {
public:
Message(int sendTime, string sendName) {
this->sendTime = sendTime;
this->sendName = sendName;
}
virtual ~Message() {} // 가상 소멸자
int GetSendTime() const { return sendTime; }
string GetSendName() const { return sendName; }
virtual string GetContent() const { return ""; }
private:
int sendTime;
string sendName;
};
class TextMessage : public Message {
public:
TextMessage(int sendTime, string sendName, string text)
: Message(sendTime, sendName) {
this->text = text;
}
string GetContent() const { return text; } // 오버라이드
private:
string text;
};
class ImageMessage : public Message {
public:
ImageMessage(int sendTime, string sendName, Image* image)
: Message(sendTime, sendName) {
this->image = image;
}
string GetContent() const { return (string)*image; } // 오버라이드
private:
Image* image;
};
void printMessage(Message* m) {
cout << "보낸 시간 : " << m->GetSendTime() << endl;
cout << "보낸 사람 : " << m->GetSendName() << endl;
cout << " 내 용 : " << m->GetContent() << endl; // 동적 바인딩
cout << endl;
}
int main() {
Image* dogImage = new Image;
TextMessage* hello = new TextMessage(10, "두들", "안녕");
ImageMessage* dog = new ImageMessage(20, "두들", dogImage);
printMessage(hello);
printMessage(dog);
delete dogImage;
delete hello;
delete dog;
}
- 기본 클래스 포인터로 파생 클래스 객체를 삭제할 때 올바른 소멸자가 호출되도록 보장
- 가상 함수를 가진 클래스는 항상 가상 소멸자를 가져야 함
참조를 사용한 다형성
class Image {
public:
operator string() {
return "사진";
}
};
class Message {
public:
Message(int sendTime, string sendName) {
this->sendTime = sendTime;
this->sendName = sendName;
}
virtual ~Message() {} // 가상 소멸자
int GetSendTime() const { return sendTime; }
string GetSendName() const { return sendName; }
virtual string GetContent() const { return ""; }
private:
int sendTime;
string sendName;
};
class TextMessage : public Message {
public:
TextMessage(int sendTime, string sendName, string text)
: Message(sendTime, sendName) {
this->text = text;
}
string GetContent() const { return text; } // 오버라이드
private:
string text;
};
class ImageMessage : public Message {
public:
ImageMessage(int sendTime, string sendName, Image* image)
: Message(sendTime, sendName) {
this->image = image;
}
string GetContent() const { return (string)*image; } // 오버라이드
private:
Image* image;
};
void printMessage(const Message& m) { // 참조로 받기
cout << "보낸 시간 : " << m.GetSendTime() << endl;
cout << "보낸 사람 : " << m.GetSendName() << endl;
cout << " 내 용 : " << m.GetContent() << endl;
cout << endl;
}
int main() {
Image* dogImage = new Image;
Message* messages[] = {
new TextMessage(10, "두들", "안녕"),
new ImageMessage(20, "두들", dogImage),
new TextMessage(30, "두들", "잘가")
};
// 범위 기반 for문
for (Message* m : messages) {
//실제로 참조하고 있는 클래스에 따라 다른 함수가 호출된다.
printMessage(*m);
}
// 배열의 각 칸마다 동적 할당된 객체를 가리키는 포인터가 들어있으므로 각각 삭제
for (Message* m : messages) {
delete m;
}
delete dogImage;
}