2026-03-06 TIL (9일차)
C++ 공부
1. override 사용 이유
virtual함수가 구현될 때 자식 클래스에서 virtual함수 구현할 때 실수 방지용으 사용된다.
2. 오버로딩 (Overloading) vs 오버라이딩 (Overriding)
● 오버로딩: 같은 이름의 함수를 매개변수의 타입이나 개수만 다르게 하여 중복 정의
● 오버라이딩: 상속 관계에서 부모의 가상 함수를 자식 클래스에서 똑같은 형태로 재정의
| 구분 | 오버로딩 | 오버라이딩 |
|---|---|---|
| 핵심 의미 | 중복 정의 | 덮어쓰기 |
| 함수 이름 | 같음 | 같음 |
| 매개변수 | 다름 (개수나 타입) | 완전히 같음 |
| 상속 관계 | 관계없음 (같은 클래스 내) | 필수 (상속 관계에서 발생) |
| 결정 시점 | 컴파일 시 | 실행 시 |
3. virtual 함수 호출 범위
A<-B<-C<-D식 상속을 받는 기준에서 A 부모클래스가 virtual함수를 구현하면
B 자식클래스 포인터로 D 자식클래스로 동적할당받고 함수를 불러도 D 클래스 함수가 출력된다.
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
31
32
33
34
35
36
class A {
public:
// 최상위에서 딱 한 번 virtual을 선언합니다.
virtual void fun() { std::cout << "A의 함수 호출" << std::endl; }
virtual ~A() {} // 가상 소멸자는 필수!
};
class B : public A {
public:
// virtual 키워드를 쓰지 않아도 A의 fun이 가상 함수이므로 자동 virtual입니다.
void fun() { std::cout << "B의 함수 호출" << std::endl; }
};
class C : public B {
public:
// 여기서도 생략해 보겠습니다.
void fun() { std::cout << "C의 함수 호출" << std::endl; }
};
class D : public C {
public:
void fun() { std::cout << "D의 함수 호출" << std::endl; }
};
int main() {
std::cout << "--- [테스트 시작] ---" << std::endl;
// 1. 실체는 D인데, B의 안경(포인터)으로 봅니다.
B* ptrB = new D();
// 2. 호출 결과는 과연?
ptrB->fun();
delete ptrB;
return 0;
}
4. 순수 가상 함수
1
순수 가상 함수가 class에 있으면 절대 객체 생성을 불가하게 만든다.
5. 가상 소멸자
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
31
32
33
34
35
가상 함수와 같은 개념으로 자식 클래스에서 동적 할당을 받는 기능이 있으면 해제를 해줘야하는데
가상 소멸자로 선언을 안하면 동적 할당 받을 때 자료형 클래스의 소멸자가 발생하므로 메모리 누수가 일어난다.
```cpp
class Parent {
public:
Parent() { std::cout << "Parent 생성자\n"; }
// ⚠️ virtual이 없습니다!
~Parent() { std::cout << "Parent 소멸자 (여기서 끝남)\n"; }
};
class Child : public Parent {
private:
int* data; // 동적 할당용 포인터
public:
Child() {
data = new int[100]; // 메모리 할당
std::cout << "Child 생성자 (100칸 할당)\n";
}
~Child() {
delete[] data; // 메모리 해제
std::cout << "Child 소멸자 (메모리 해제 완료)\n";
}
};
int main() {
std::cout << "--- 객체 생성 ---\n";
Parent* p = new Child();
std::cout << "\n--- 객체 소멸 (delete) ---\n";
delete p; // 문제 발생!!
return 0;
}
```
6. friend 키워드
연산자 오버로딩 때 주로 사용한다.
호출형태가 함수가 중심으로 하기 위해서 객체 내부의 행동의 정의가 해버리면 유연성이 없어진다.
7. 연산자 중복 정의하는 방법
멤버 함수 vs 전역 함수
| 구분 | 멤버 함수 구현(A.operator+(B)) | 전역 함수(friend) 구현 (operator+(A, B)) |
|---|---|---|
| 호출 구조 | obj1 + obj2 → obj1.operator+(obj2) | obj1 + obj2 → operator+(obj1, obj2) |
| 호출 형태 | 객체가 중심 (A가 주인, B는 손님) | 무엇이든 올 수 있음 (int, double, 다른 객체 등) |
| 좌항 | 반드시 해당 클래스 객체여야 함 | 무엇이든 올 수 있음 (int, double, 다른 객체 등) |
| 의미 | “A야, B를 가지고 이 일을 처리해라” | “A와 B를 가지고 이 연산을 수행해라” |
| 주요 용도 | +=, [], (), -> (필수 멤버 함수) | +, -, *, / (대칭적 산술 연산) |
| 주요 용도2 | 객체 상태를 직접 바꾸는 연산 | «, » (입출력 라이브러리 연동) |
| 캡슐화 | 멤버이므로 private에 직접 접근 | friend 선언을 통해 private 접근권 획득 |
| 특징 | this 포인터를 사용하여 명확함 | 대칭성 유지 가능 (10 + A 가능) |
8. 형변환 연산자
| 형변환 연산자 | 의미 |
|---|---|
| static_cast | 기본 타입의 변환이나 상속 관계에 있는 클래스 포인터를 변환할 때 사용 |
| dynamic_cast | 상속 관계에 있는 클래스 포인터를 안전하게 변환할 때 사용 |
| const_cast | 상수 속성을 변경 |
| reinterpret_cast | 관련 없는 포인터 사이의 무조건 변환 (정수형과 포인터 사이의 변환) |
9. 함수 템플릿vs 템플릿 함수
1
2
● 함수 템플릿: 함수를 만드는데 사용되는 템플릿
● 템플릿 함수: 템플릿을 기반으로 만들어진 함수
10. 일반 오버로딩 vs 완전 특수화
| 구분 | 일반 함수 오버로딩 | 완전 특수화 (‘template <>’) |
|---|---|---|
| 인식 우선순위 | 가장 높음 (1순위) | 중간 (2순위) |
| 형변환 | 암시적 형변환 허용 | 절대 허용 안 함 |
| 권장 사항 | 함수일 때 강력 추천 | 클래스 템플릿일 때 주로 사용 |
| 정의 | 독립적인 새 함수 | 기존 설계도의 변종 |
● 템플릿으로 함수를 정의 할 때 예외로 처리해야 매개변수들은 완전 특수화를 사용해도 되는데 대부분 오버로딩을 사용함
● 완전 특수화는 보통 템플릿 클래스때 사용
11. 템플릿을 관리하는 파일분할
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
템플릿을 헤더파일에 선언하고 cpp파일에 정의를 하게되괴 main에서 사용하게 되면 에러가 발생한다.
● 이유: 템플릿은 설계도라서 자료형이 들어가면 그 자료형 함수/클래스인지 식별을 못한다.
> ● 해결방법
> 1) 템플릿 헤더파일에 정의도 같이 다 해준다.
> 2) 템플릿을 사용하는 파일에 템플릿을 정의한 cpp를 include한다.
> 3) cpp로 사용하지 않고 tpp/ipp파일를 만들어 정의를 선언해주고 템플릿 헤더 마지막부분에 include 해준다.
{: .prompt-tip }
> ❗ 나는 3번 방법처럼 그냥 cpp로 만들고 마지막부분에 include 해주면 되는거 아니야? ➡️ 하게되면 오류가 발생한다.
> ● 이유
> 1) 컴파일러는 모든 .cpp는 독립적으로 빌드를 하게된다. 그럼 템플릿 정의한 cpp도 빌드를 하게된다.
> 2) 그럼 main.cpp에서 헤더를 불렀고, 그 헤더가 다시 템플릿를 정의한 cpp를 #include했다면 main 에도 똑같은 통째로 복사되어 들어간다.
> 3) 그럼 링커에서는 똑같은게 있다고 링커에러를 발생시킨다.
{: .prompt-danger }
**하지만 .tpp나 .ipp라는 확장자는 컴파일러에게 무시당하고 포함될 때만 작동해서 헤더 파일 일부로 작동된다.**
C++강의 3번 과제
1. 벡터
선언
● vector<자료형> 함수명 (사이즈, 초기값); -> 사이즈만큼 벡터를 만드는데 사이즈만큼 초기값을 넣겠다.자료형>● vector<자료형> 함수명 = { } -> 초기화 리스트를 줘서 선언하는 법자료형>
● vector<자료형> 함수명 (다른 벡터) -> 다른 벡터의 복사하거나 대입자료형>
● vector<자료형> 함수명 (행 사이즈, vector<자료형>(열 사이즈, 초기값)) -> 2차원 배열처럼 벡터를 사용하려면, 벡터의 타입을 벡터자료형>자료형>
벡터의 erase는 벡터의 성능을 저하시킨다.
● 중간에 삭제가 일어나면 삭제가 일어난 기준으로 부터 뒤에 있는 저장된 값들을 다 앞으로 땡겨와야한다.
● 그러므로 추가적인 연산이 필요해져서 비횽율적인 연산이 생겨 성능을 떨어트린다.
ex) vec.erase(vec.begin() + 1, vec.begin() + 3); -> 2~3번째 제거, erase의 2번째 매개변수는 그 위치 전까지 삭제하는거 같다.
2. 맵
선언
● map<키(자료형),값(자료형)> == std::pair<const 키(자료형), 값(자료형)>들이 줄지어 서 있는 구조
키값을 기준으로 오름차순으로 계속 정렬이 된다.
pair.first: pair의 첫 번째 데이터 (맵에서는 Key)
pair.second: pair의 두 번째 데이터 (맵에서는 Value)
3. 범위 기반 for 루프
for ( 컨테이너 요소의 자료형 변수명 : 컨테이너) == for (auto 변수명 = 컨테이너.begin(); 변수명 != 컨테이너.end(); ++변수명)
컨테이너 요소의 자료형: 컨테이너 선언을 할 때 할당하는 자료형(auto는 자동으로 컨테이너 요소의 자료형이 할당받는다.)
컨테이너.begin(): 컨테이너의 시작 원소 반복자(주소)
컨테이너.end(): 컨테이너의 마지막 원소의 바로 다음 반복자(주소)
4. 정렬
● sort (a(주소),b(주소));
sort는 매개변수 2개를 받아서 오름차순으로 정렬을 한다.
● sort(a(주소),b((주소),compare(함수));
하지만 세번째 매개변수에 직접 자신이 만든 사용자 정렬 함수를 넣으면 그 기준으로 정렬을 한다.
5. find
find(first(주소), last(주소), 찾을 값)
● find(first, last)가 탐색 대상
● 원소를 찾은 경우 해당 원소의 반복자(주소)를 반환
● 원소를 찾지 못한 경우 last 반복자(주소)를 반환
❗ (vector, string, deque)반복자-시작 반복자를 하면 위치가 나온다.
반복자가 가리키는 곳의 주소를 확인한다.
시작 반복자가 가리키는 시작 주소를 확인한다.
두 주소의 차이를 구한 뒤, 데이터 타입의 크기로 나눈다.
그 결과값(인덱스 번호와 동일)을 반환
반복자(it)가 자신이 어떤 타입(T)을 가리키는지 이미 알고 있기 때문에 데이터 타입의 크기로 나누는거는 알아서 해준다