TIL_017: Iterator, string(), 객체지향적 설계

김펭귄·2025년 8월 20일

Today What I Learned (TIL)

목록 보기
17/93

오늘 학습 키워드

  • Iterator

  • string()

  • 객체지향적 코드 작성법

1. Iterator(반복자)

  • 컨테이너의 내부 구조는 서로 다르지만, Iterator를 통해 동일한 코드로 알고리즘 사용 가능

  • 반복자는 컨테이너의 요소에 대한 일관된 접근 방법을 제공하므로, 알고리즘이 특정 컨테이너의 내부 구현과 무관하게 동작할 수 있다

  • 반복자는 포인터처럼 동작하지만, 실제 타입은 컨테이너마다 별도로 정의된 "클래스(객체)"

  • 컨테이너 내부의 직접적인 주소(메모리 위치)가 아니라, 그 컨테이너의 특정 원소를 가리키는 "포인터 유사 객체"

  • 객체이므로 생성자와 같은 멤버함수도 존재

  • *it으로 값을 읽고, ++it으로 다음 요소의 위치로 이동

  • std::vector<int>::iterator 같이 자료형이 좀 기므로, 보통 auto it = ~~ 으로 사용

auto

  • 컴파일러에게 변수의 타입을 자동으로 추론하게 해주는 키워드

  • 컴파일러가 추론해야하므로 반드시 초기화해서 사용해야 함

  • 초기화 값을 바꾸어주면 그에 맞게 알아서 자료형도 바뀌는 편리함

auto x = 10;        // int
auto y = 3.14;      // double
auto z = "hello";   // const char*
auto flag = true;   // bool

std::vector v = {1,2,3};
// it은 std::vector<int>::iterator의 자료형을 가지게 됨
for (auto it = v.begin(); it != v.end(); ++it) { 
	// *it으로 값을 읽고, ++it으로 다음위치로 이동 (포인터와 비슷)
	std::cout << *it << std::endl;
}

begin(), end()

  • 해당 컨테이너의 순방향 반복자(iterator)를 반환

  • .begin(): 첫 번째 원소를 가리키는 반복자를 반환

  • .end(): 마지막 원소 "다음 위치"를 가리키는 반복자를 반환

    • 다음위치를 가리키기에 반복문에서 사용하기 쉬우며, 탐색 실패를 쉽게 표현 가능
map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};
    
// map의 iterator는 map<key자료형, value자료형>::iterator
// map<string, int>::iterator it
for (auto it = scores.begin(); it != scores.end(); ++it) {
    cout << it->first << ": " << it->second << endl;
}
// Alice: 90
// Bob: 85
// Charlie: 88

rbegin(), rend()

  • 역방향 반복자

  • .rbegin(): 컨테이너의 마지막 원소를 가리키는 역방향 반복자

  • .rend(): 컨테이너의 첫 번째 원소 이전을 가리키는 역방향 반복자

    • 마찬가지로 더 이전을 가리키기에 반복문과, 탐색 실패에 사용 용이
  • 반복문에 사용 예시

map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};

for (auto it = scores.rbegin(); it != scores.rend(); ++it) { // 역방향이지만 ++it 사용
    // code
}
vector<string> words = {"apple", "banana", "cherry", "date"};
string target = "banana";
auto it = find(words.rbegin(), words.rend(), target);

if (it != words.rend()) {
    cout << "역방향 인덱스: " << distance(words.rbegin(), it) << endl;
    // 순방향 반복자와 역방향 반복자끼리 계산 불가능
    cout << "정방향 인덱스: " << distance(words.begin(), it.base()) - 1 << endl;
} 
// 역방향 인덱스: 2
// 정방향 인덱스: 1

순방향 반복자 vs 역방향 반복자

순방향 반복자 (Forward Iterator)

  • 컨테이너의 첫 원소부터 마지막 원소까지 앞에서 뒤로 순회할 때 사용하는 기본 반복자

  • ++로 앞에서 뒤로 이동, *로 현재 가리키는 원소에 접근(--는 안 됨)

  • std::vector::iterator가 대표적인 순방향 반복자

역방향 반복자 (Reverse Iterator)

  • 컨테이너를 뒤에서 앞으로 순회할 때 사용하는 반복자 (순방향 반복자와 자료형이 다름)

  • ++를 통해 컨테이너 끝에서 앞으로 이동(반대방향), *로 원소에 접근

  • 순방향 반복자와 다르게-- 가능. 양방향반복자임

변환법

  • 역방향 반복자와 순방향 반복자는 타입이 다르기때문에, 직접 교환하거나 대입할 수 없으며, 변환이 필요

  • 변환 없이 사용하면 컴파일 에러

base()

    vector<int> a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	auto r_it = find(a.rbegin(), a.rend(), 5);
    cout << *r_it << *r_it.base();		//56
  • 역방향 반복자를 순방향 반복자로 변환시켜줌

  • 반복자가 가리키는 원소의 “바로 다음”(forward 방향 기준) 위치의 반복자를 반환

  • 그래서 distance(words.begin(), it.base())에서 1을 빼주는 것

std::make_reverse_iterator()

  • 순방향 반복자를 역방향 반복자로 변환

  • <iterator> 헤더에 정의

  • 근데 얘도 반복자가 가리키는 원소의 "바로 다음"(reverse 방향 기준) 위치의 반복자를 반환

    vector<int> a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	auto it = find(a.begin(), a.end(), 5);
    cout << *it << *make_reverse_iterator(it);	// 54

2. string()

  • string()은 객체 string의 생성자
using namespace std;
// 아래의 예시들은 다 string객체의 생성자 (string())
string s1;  						 // 빈 문자열 "" 생성
string s2(s1); 						 // s1문자열 복사해 생성
string s3("hello");					 // const char*형태의 C문자열을 받아 생성
string s4(s3, pos, count);  		 // s3의 pos 위치부터 count 개수까지 복사해 s4 생성
string s5(vec.begin(), vec.end()); 	 // 반복자 범위의 문자들을 복사해 생성
string s6(vec.rbegin(), vec.rend()); //	반복자 거꾸로 복사해 생성
string s7(5, 'A');  				 // "AAAAA"
string s1; 						  // ""
string s2("hello"); 			  // "hello"
string s3(s2); 					  // "hello"
string s4(s2, 1, 3); 			  // "ell"
vector v = {'w', 'o', 'r', 'l', 'd'};
string s5(v.begin(), v.end());    // "world"
string s6(v.rbegin(), v.rend());  // "dlrow"
string s7(4, '!'); 				  // "!!!!"

3. 객체지향적 코드 작성법

응집도

  • 관련성이 있는 모듈끼리 같은 클래스에 놓기

  • 계산하는 모듈은 계산 클래스에, 출력모듈은 출력 클래스에 몰아 놓기

결합도

  • 모듈 또는 클래스 간의 의존성을 나타내는 것으로, 결합도가 낮아야 좋은 코드

  • 결합도가 높으면 각 모듈 간 의존성이 강해져, 하나의 모듈이 변경될 때, 다른 모듈도 영향을 받게 됨

결합도 예시

  • 자동차 클래스에서 엔진 클래스를 포함하는 경우(결합도 높음), 아래와 같이 직접할 수 있지만, 새로운 다양한 종류의 엔진 클래스를 추가하게 된다면, 자동차 클래스도 함께 수정해야 함

  • 변경이 잦은 경우 수정 범위가 커지고 유지 보수가 어려워짐

  • 결합도를 낮추기 위해 엔진 인터페이스를 새로 추가하여 활용

  • 자동차 클래스는 엔진 인터페이스에만 의존하므로, 새로운 엔진을 추가해도 자동차 코드를 수정할 필요가 없다

  • 인터페이스는 A로 표시되며, 꼭 가상함수를 써 override 되도록 해야한다

// 엔진 인터페이스
class Engine {
public:
    virtual void start() = 0;	// 가상함수로 override 해야 하게끔 설정
};

class DieselEngine : public Engine {
public:
    void start() {}
};
class ElectricEngine : public Engine {
public:
    void start() {}
};

// Car 클래스는 엔진 인터페이스에만 의존
class Car {
private:
    unique_ptr<Engine> engine;

public:
    Car(unique_ptr<Engine> eng) : engine(move(eng)) {}

    void startCar() { engine->start(); }
};

int main() {
    // DieselEngine을 사용하는 경우
    auto dieselEngine = make_unique<DieselEngine>();
    Car dieselCar(move(dieselEngine));
    dieselCar.startCar();

    // ElectricEngine을 사용하는 경우
    auto electricEngine = make_unique<ElectricEngine>();
    Car electricCar(move(electricEngine));
    electricCar.startCar();
}

SOLID

SRP (단일 책임 원칙)

  • 각 클래스는 하나의 역할만을 해야함

  • student클래스는 학생의 정보를 저장하는 역할만 하면 되지, 이외의 함수는 학생의 역할이 아님

  • 따라서 점수 계산과 학생정보출력하는 부분은 따로 클래스를 만들어 각 클래스에서 student클래스를 받아서 사용

OCP (개방 폐쇄 원칙)

  • 확장에는 열려있고, 수정에는 닫혀있도록 코드 짜기

  • 기능이 변하거나 확장되는 것은 가능하지만 그 과정에서 기존의 코드가 수정되지 않아야 함

class ShapeManager {
public:
    void drawShape(int shapeType) {
        if (shapeType == 1) { /*원 그리기*/ } 
        else if (shapeType == 2) { /*사각형 그리기*/  }
    }
};
  • 위와 같이 짤 경우 새로운 도형을 추가할 때 drawShape함수도 수정을 해줘야함

  • 따라서 위와 같이 Shape라는 interface를 만들어서 새로운 도형 class가 추가되어도 다른 클래스에는 영향이 없게 만들기
class Shape {
public:
    virtual void draw() = 0; // 순수 가상 함수
};

class Circle : public Shape {
public:
    void draw() {/*원 그리기*/}
};
class Square : public Shape {
public:
    void draw() {/*사각형 그리기*/}
};
class Triangle : public Shape {
public:
    void draw() {/*삼각형 그리기*/}
};

class ShapeManager {
public:
    void drawShape(Shape& shape) { shape.draw(); }
};

LSP (리스 코프 치환 원칙)

  • 자식 클래스는 부모 클래스에서 기대되는 행동을 보장해야 함

  • 즉, 부모 클래스를 사용하는 코드가 자식 클래스로 대체되더라도 정상적으로, 일관적으로 동작해야 함

  • 예를 들어, 정사각형이 직사각형의 자식클래스일 때, 어차피 정사각형은 높이와 너비가 같다고 정사각형의 setWidth함수에서 heigt까지 설정하면 안 됨.
    원래 부모클래스에서의 setWidth함수는 너비만 설정하는 함수였으므로.

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

  • 자신이 사용하지 않는 기능은 따로 인터페이스로 분리한 후 인터페이스로 연결해 사용

  • 프린터기와 스캐너는 별개이기도 하고, 이들의 기능만 사용할 것이므로 분리해야 한다

  • 아래와 같이 인터페이스를 분리하여 사용하는 것이 좋다

DIP (의존 역전 원칙)

  • 상위 모듈이 하위 모듈에 의존해서는 안 됨

  • 아래 예시처럼, 상위 모듈인 Computer에서 하위 모듈인 Keyboard, Monitor를 직접적으로 타입을 받아서 사용하면 나중에 입출력 부품이 바뀌거나 하는 경우에 유지보수에 어려움이 생김

  • 따라서 마찬가지로 입출력 인터페이스 클래스를 따로 생성하고, 상위 모듈인 Computer에서는 이 인터페이스의 "포인터"를 받아서 결합력(의존성)을 낮춘다.

profile
반갑습니다

0개의 댓글