Post

2026-03-06 TIL (9일차)

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)을 가리키는지 이미 알고 있기 때문에 데이터 타입의 크기로 나누는거는 알아서 해준다

This post is licensed under CC BY 4.0 by the author.