[UE5 TIL] Day 14 - C++ 학습/과제 작성

JungHoon Eum·2026년 3월 16일

키워드

STL, 컨테이너, vector, map, 알고리즘, sort, find, 반복자, begin/end, rbegin/rend range-based for, const auto&, distance, base(), SOLID, 인터페이스

복습 포인트

  • vector: 동적 배열, 끝에 넣고 빼는 건 빠름 / 중간 erase는 비쌈
  • map: 키-값 저장, 키 기준 자동 정렬, 중복 키 불가
  • pair.first, pair.second, insert
  • range-based for문: for(int num: vec)
  • const auto&: 읽기 전용 + 타입 자동 추론 + 복사 없이 참조
  • sort(시작반복자,끝반복자,비교함수)
  • find(): 찾으면 반복자 반환, 못 찾으면 end()
  • 반복자: 포인터처럼 동작하지만 컨테이너용 공통 접근 도구
  • base(): 역반복자를 정방향 반복자로 바꿀 때 씀 (-1 해야 함에 주의)
  • SRP: 한 클래스에 한 책임
  • OCP: 확장엔 열려있기, 수정엔 닫혀있기
  • LSP: 자식이 부모 대신 들가도 동작해야함
  • ISP: 각 클래스마다 필요한 기능만 구현하기
  • DIP: 구체 클래스 말고 추상화에 의존하기

개발자를 위한 C++ 문: STL 기초

  • STL: Standard Template Library
    • C++ 표준 라이브러리의 일부. 컨테이너, 알고리즘, 반복자 등 템플릿 기반 구성요소 포함
    • 다양한 자료구조와 알고리즘을 구현하지 않고 활용 가능

컨테이너

  • 데이터를 담는 자료구조
  1. 템플릿으로 구현 - 다양한 타입의 데이터 저장 가능
  2. 메모리 관리 내부적으로 해결 -> new, delete... 이런 거 필요없음.
  3. 반복자 제공: 내부 구현 방식 몰라도 동일한 방식으로 컨테이너 순회 가능

벡터

  • 배열과 유사한 컨테이너
  1. 템플릿 클래스로 구현 - 특정 타입에 종속되지 않음
  2. 삽입되는 원소 개수에 따라 내부 배열 크기가 자동으로 조정
  3. 임의 접근 가능(인덱스를 통해 특정 위치에 접근)
  4. 삽입/삭제는 맨 뒤에 하는 게 효율적(중간 삽입/삭제는 배열 복붙하는 과정 필요)

벡터의 선언

  • 타입만 명시해서 선언하는 방법/ 초기값까지 같이 선언하는 경우
#include <vector>
vector<int> vec1; //빈 벡터. 크기 0.
vector<int> vec2(5,10); //크기 5, 모든 원소 10으로 초기화
vector<int> vec3(vec2); //vec2 복붙하기
  • 초기화 리스트를 사용하여 벡터를 선언하는 코드
    vector<int> vec3 = {1, 2, 3, 4, 5};
  • 벡터를 2차원 배열처럼 사용하고 싶다면?
    vector<int> vec2D(3, vector<int>(4, 7));
    -> 3열 4행짜리 원소들 다 7인 2차원 배열

벡터의 동작

  1. push_back: 벡터의 맨 끝에 원소를 추가하는 메서드
    원소의 개수가 늘면서 크기는 자동으로 증가.
    vec.push_back(10); 이런 식으로 쓴다.
    원소 출력하는 방법은?
for(int num: vec) //num: vec 원소들 하나하나씩 꺼내오는 
//C++ 전용 "range-based for문"
//for(int i=0, i<vec.size()...int num=vec[i] 이런 걸 축약한 느낌
{
std::cout << num <<" ";
}
  1. pop_back: 벡터 맨 끝의 원소를 제거. 크기 자동으로 줄어듦.
    vec.pop_back() 이런 식으로 쓺. 원소 명시 필요 X.
  2. size 벡터의 크기를 확인하는 메서드
    vec_size()
  3. erase: 특정 위치의 원소 제거. 시간 복잡도 커서 엔간하면 사용 안하는 게 맞다.
    vec.erase(vec.begin()+1) = 두번째 원소 제거
    vec.erase(vec.begin()+1, vec.begin()+3) = 2~3번째 원소 제거: 끝쪽 위치는 제외
    erase 를 너무 많이 쓴다? 내가 컨테이너 잘못 골랐을 가능성 높다.

  • 특정 키를 사용하여 값을 검색하는 기능 제공하는 컨테이너
  • like 전화번호부 -> 이름을 이용하여 전화번호를 검색
  • 그래서 연관 컨테이너 라고 부름 -> 키와 값 쌍으로 이루어져 있음.
  1. 키-값 쌍은 pair<const Key, value> 형태로 저장
  2. 키의 값을 기준으로 내부 데이터 자동정렬
  3. 중복된 키값을 허용하지 않는다.

맵의 선언

#include <iostream>
#include <map>
//#include <string> -> 없어도 위에 헤더들 때매 돌아갈 순 있는데 
//엔간하면 추가하는 게 나음

map<int,string> studentMap; //맵 선언
//키는 정수, 값은 string
//요소 추가하는 방법은
studentMap[101] = "Alice"; //키는 101, 값은 Alice
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
//출력은
for(const auto& pair: studentMap)
{
	std::cout <<"ID: " << pair.first <<", Name: " <<pair.second<<std::endl;
}
  • const auto&: 읽기 전용으로(const) 타입은 컴파일러가 알아서(auto) 참조로 받아서(&) 복사 안하고 효율적으로 값 불러오기 (변수명 pair)
  • for(.....: studentMap): C++ range-based for문이다. 즉 studentMap의 모든 요소를 꺼내서 하나하나 돌리는 for문

맵의 동작

  1. mapkey 순으로 오름차순 정렬
    myMap[20] = "Banana" 식으로 원소 하나 넣으면 알아서 int 키 순으로 정렬해 줌

  2. insert() 를 이용해서 추가 가능
    - insert() - make_pair() 이용
    myMap.insert(make_pair(1, "Apple"));
    - myMap.insert({4, "Dog"}); 식으로도 가능: 앞 값이 key, 뒤가 값
    - myMap[7] = "Giraffe"; 도 가능: [] 안에 key

  3. find()

    • 특정 키가 map 에 존재하는지 확인 가능. 존재하면 해당 키의 iterator 반환, 존재하지 않으면 map.end()를 반환
    • int key=2, auto it = myMap.find(key);
    if (it != myMap.end()) {
        cout << "Found! Key: " << it->first << ", Value: " << it->second << endl;
    } else {
        cout << "Key " << key << " not found!" << endl;
    }
  4. size(): 맵의 키-값 쌍의 갯수 반환

  5. erase(): 특정 key 가진 요소 삭제

    • 존재하지 않는 키 삭제하면? 0 반환
  6. clear(): 모든 원소 삭제. 대부분의 컨테이너가 가지고 있는 메소드다

    • myMap.clear();

알고리즘

  • 컨테이너 한 두개만 배워도 대부분 이용 가능 -> 공용으로 쓰는 알고리즘, 반복자 적용 가능하기 때문.

sort

  • 컨테이너 내부의 데이터를 정렬하는 함수
  • 기본 타입(int, double 등) 은 사용자 정렬 함수가 없으면 오름차순으로 정렬
  • 사용자 정렬 함수: 인자 1개, 2개 받는 경우가 있으나, 이 수업에서는 2개 받는 경우만 다름
    • comp(a, b): 첫 번째 인자 a가 앞에 있는 원소를 의미
    • comp(a, b)의 반환타입은 bool.
      true면 순서 그대로 유지. false 면 a, b 순서 바꿈
  • 기본타입 배열 정렬하기(정렬 기준 X)
    sort(arr, arr + size); (size=sizeof(arr)/sizeof(arr[0]))
    -> 걍 내부 원소들 순서대로 정렬됨
  • comp(a, b) 이거 그냥 bool 문이다 -> comp 여도 되고 compare 여도 되고...왜 뭐 특별한거처럼 써놨지?
  • sort() 사용법: sort(시작 반복자, 끝 반복자, 비교함수)
    • 시작 반복자: 해당 컨테이너의 시작 주소. vec.begin() 일 수도 있고, 배열의 경우 그냥 arr 일 수 도 있고...
    • 끝 반복자: 마지막 주소. vec.end()arr+size
    • 비교함수: 앞, 뒤 원소 비교해서 bool 값 반환해 주는 함수면 뭐든 상관없음. comp 일 필요 X
  • 사용자 정의 클래스 정렬하기:
    • string name, int age 멤버변수 가지고 있다고 생각.
bool compareByAgeAndName(const Person& a, const Person& b) {
    if (a.getAge() == b.getAge()) {
        return a.getName() < b.getName(); // 이름 오름차순
    }
    else
    {
    	return a.getAge() < b.getAge();
    } // 나이 오름차순
}

find

  • 컨테이너 내부에서 특정 원소 찾아 해당 원소의 반복자 반환
  • find(first, last, 찾을 값)
    • first-last가 범위, 값 없으면 last 반환
    • 정확히는 last 직전까지 -> 그래서 vec.end() 그냥 넣어도 됨

반복자

  • 컨테이너들에서 알고리즘을 동작시키는 도구
  • 컨테이너들 구현 방식 몰라도 반복자를 통해 원하는 위치나 값을 받아올 수 있다.

순방향 반복자

  • begin() -> 첫 번째 원소.
  • end() 마지막 원소 다음을 가리킴.
    • 일관된 반복 구조, 탐색 실패 표현 용이

역방향 반복자

  • rbegin() -> 마지막 원소
  • rend() -> 첫 번째 원소 이전을 가리킴.
  • 벡터에서도 auto it = numbers.begin() 하고 *it 하면 it 주소의 값 불러올 수 있다 Like 포인터
  • 맵에서는 it->first 식으로. 클래스 포인터다
  • distance(a, b) -> a원소부터 b 원소까지 거리. b가 begin()index 겠지?
  • it.base() 하면 역방향으로 서치 중일때 해당 역방향 반복자 에서 앞의 값을 불러와 준다. why? 반복자는 메모리 '칸' 이 아니라 '칸막이' 를 가리키는 것에 가깝기 때문.
  • 역방향 반복자에서 정방향 반복자로 전환을 하려면
    distance(words.begin(), it.base()) - 1 이따구로 만들어야 함. 왜 설명을 제대로 못할까?

개발자를 위한 C++ 문법: 객체지향적 설계

  • 문법이 아닌 작법에 가까운 부분. 왜 중요한가
  1. 대부분 라이브러리/오픈소스는 객체지향적으로 설계. 이 방식을 이해하면 이해도가 높아짐
  2. 설계가 깔끔해지면 개발 시간 단축 가능
  3. 기능 변경에 유연하게 대응 가능

응집도

  • 클래스 또는 모듈 내부 구성 요소가 얼마나 밀접하게 관련되어 있는지: 응집도가 높을수록 좋은 설계
  • 클래스 내부에 관련 없는 기능이 포함되면 변경이 자주 발생, 확장도 쉽지 않다.

예시 - 피자 배달 과정 클래스

  1. 피자 배달
  2. 웹사이트 디자인
  3. 회사 마케팅
  4. 창고 관리
  • 이 기능들이 한 클래스에 모여 있다? 응집도 낮음

결합도

  • 한 모듈을 수정할 때 다른 모듈들이 얼마나 영향을 받는가
  • 낮을 수록 좋은 코드라 평가
  • class Car 가 멤버변수로 engine: Engine 을 그냥 받아오는 경우: 디젤엔진, 전기엔진 등등...에 대해 다 별개로 지정을 해줘야함
    • engine: unique_ptr<engine> 으로 받아오기. 자손 클래스로 업캐스팅

SOLID 원칙

단일 책임 원칙 (SRP)

  • 하나의 클래스에 하나의 책임
    • 학생 이름 받고 - 이름 출력 - 점수 받고 성적 계산
    • 각각 하나의 클래스로 처리

개방 폐쇄 원칙 (OCP)

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 한다
  • 새로운 코드 추가는 쉽지만, 그를 위해 기존 코드 수정할 필요는 최소화
  • 주로 순수가상함수-자손클래스에서 실제 작동 방식 구현으로 달성한다.
  • 인터페이스: "이 함수는 꼭 구현해라" 하고 정해두는 규칙 (주로 순수가상함수가 인터페이스다)

리스 코프 치환 원칙 (LSP)

  • 자식 클래스는 부모 클래스에서 기대되는 행동을 보장해야 한다
  • square-rectangle을 자손-부모 관계로 규정해 버리면?
  • width, height가 square엔 없고, 제대로 동작 안함

인터페이스 분리 원칙 (ISP)

  • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다
  • 역할별로 세분화된 인터페이스르 만들어, 필요한 기능만 구현 -> 부모-자손 관계만 만들 게 아니라, 포인터로 필요한 기능 구현한 클래스만 받는 관계도 고려해야.

의존 역전 원칙 (DIP)

  • 실제 구현을 담당하는 모듈을 직접 가져오는 게 아니라, 추상화된 클래스를 끼고 받아와라.
  • 구현 방식이 수정되거나 확장되어도 큰 클래스 전체가 영향받지 않는다.
profile
개발지망생

0개의 댓글