Publish:

태그: , , ,

카테고리:


🤯 언리얼을 하기 위해 C++ 기억 되살리기 프로젝트

표준 템플릿 라이브러리 (STL) 1

  • STL 컨테이너의 목적
    • 템플릿 기반
      • c# 제네릭스와 비슷한 것
    • 모든 컨테이너에 적용되는 표준 인터페이스
    • 메모리 자동 관리

1. 벡터

  • 어떤 자료형도 넣을 수 있는 동적 배열
    • 기본(primitive) 데이터
    • 클래스
    • 포인터
  • 그 안에 저장된 모든 요소들이 연속된 메모리 공간에 위치
  • 요소 수가 증가함에 따라 자동으로 메모리를 관리해 줌
  • 어떤 요소에도 임의로 접근 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> scores;                // <- 템플릿! <int>. 이게 c#의 <T>.. 제네릭과 비슷한 것
    scores.reserve(2);                      // 2개 공간 잡아줘~ 공간 예약.

    scores.push_back(30);                   // 제일 마지막 위치에 요소 추가하기
    scores.push_back(50);

    scores.pop_back();                      // 맨 뒤의 요소 제거

    std::cout << "Current capacity : " << scores.capacity() << std::endl;
    std::cout << "Current size : " << scores.size() << std::endl;
}

1-1. reserve

  • vector의 용량을 늘린다
  • 용량이 증가해야 하면, 새로운 저장 공간을 재할당하고 기존 요소들을 모두 새 공간으로 복사
  • 느릴 수 밖에 없지만 이걸 사용하는 게 낫다 왜냐면!
  • 불필요한 재할당을 막기 위해! vector를 생성한 직후에 이 함수를 호출하자
  • scores.reserve(10);

1-2. 요소 하나에 접근하기

  • operator[](size_t n)
  • 지정된 위치(n)의 요소를 참조로 반환한다
    1
    2
    3
    
    scores[i] = 3;                // 그냥 값으로 반환한다면 대입이 안되었겠죠
                                  // 값은 임시적으로 반환된 값이니까, 대입해봐야 원본은 바뀌지 않을것
                                  // 하지만 바뀌죠? 그러니 참조로 반환되었다는 걸 알 수 있다!
    
  • 배열 처럼 쓸 수 있다. 연속된 메모리 공간에 있으니까.

💡 허나 이 방식은 벡터에서만 쓸 수 있음!!!

  • 맵(map) 에서는 인덱스로 operator[]를 쓸 수 없음.
  • 그래서 STL 컨테이너를 순회할 땐 반복자(iterator)를 쓰는 것이 표준 방식이다~

1-3. 반복자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> scores;
    scores.reserve(2);

    scores.push_back(30);
    scores.push_back(50);

    
    for (std::vector<int>::iterator iter = scores.begin(); iter != scores.end(); ++iter)
    {
        std::cout << *iter << " ";
    }
}

반복자

  • iterator
    • STL 컨테이너의 요소들을 순회하는 데에 사용
  • begin();
    • vector의 첫 번째 요소를 가리키는 반복자를 반환
  • end();
    • vector의 마지막 요소 바로 뒤의 요소를 가리키는 반복자를 반환

(10, 20) 이렇게 vector가 있다면,
begin()은 10을 가리키고, end()는 20의 뒤를 가리킨다.

역방향 반복자

  • end()에서 시작해서 begin()까지
  • rbegin()
    • reverse begin
    • 마지막 요소 가리킴
  • rend()
    • 시작 요소에서 하나 전에서부터 시작 (10, 20, 30 이렇게 있다면 10 이전것을 가리킴)

특정 위치에 삽입 : insert

1
2
std::vector<int>::iterator it = scores.begin();
it = scores.insert(it, 80);                     // 현재 iterator가 가리키는 위치(처음)에 80을 삽입

복사 문제

  • 배열의 고질적인 문제점
  • vector의 begin에 insert를 할 경우, 기존의 데이터들을 복사해서 뒤에 다 넣어준 다음 insert가 실행됨
  • 뒤에 다른 메모리가 잡혀 있다면, 비어있는 다음 메모리를 찾아 그 메모리에 재할당 및 복사를 해줌

특정 위치에 삭제 : erase

1
2
std::vector<int>::iterator it = scores.begin();
it = scores.erase(it, 80);                     // 현재 iterator가 가리키는 위치(처음)에 80을 삽입

삭제할 경우 복사했을까? 재할당 했을까?

  • 재할당은 없고 복사는 있다.
  • it이 가리키는 위치에 있는거 지우고, 그 뒤에 있는거 하나씩 복사해서 넣어준다.

벡터 교환하기 : swap

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.cpp
vector<int> scores;
scores.reserve(2);

scores.push_back(85);               // scores 벡터에 85
scores.push_back(73);               // scores 벡터에 85, 73

vector<int> anotherScores;
anotherScores.assign(7, 100);       // 100, 100, 100, 100, 100, 100, 100
// assign : 같은 숫자를 7번 반복해서 채워주는 함수

scores.swap(anotherScores);         // scores: 100, 100, 100, 100, 100, 100, 100
                                    // anotherScores: 85, 73
  • swap 은 빠른 연산임.
  • 가리키는 주소를 바꿔주기 때문.

벡터 크기 변경하기 : resize

  • reserve 랑은 다름. reserve는 공간 예약을 하는 것!
  • (10, 20, 30) 이 들어있는 벡터에 resize(2); 를 한다면 (10, 20) 만 남고 사이즈도 2로 줄어들게 됨.
  • 즉, 새 크기가 vector의 기존 크기보다 작으면, 초과분이 제거됨
  • 새 크기가 vector의 기존 용량보다 크면, 재할당이 일어남

벡터 초기화하기 : clear

  • 벡터 내의 요소들을 지우고, 용량은 변하지 않음.
  • 그래서 용량은 기존 담았던 갯수만큼 유지되어 있지만,
  • 크기는 0됨

1-4. 개체 벡터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Score.h
class Score
{
    public:
        // ...
    private:
        int mScore;
        string mClassName;
};

// Main.cpp
int main()
{
    vector<Score> scores;                   // 클래스로 구성된 벡터. 오브젝트를 넣을 거임.
    scores.reserve(4);

    scores.push_back(Score(30, "C++"));     // Score 개체를 스택에 임시로 만들었다가
                                            // push_back할 때 벡터에 복사되어 들어가진다.
    scores.push_back(Score(59, "Algorithm"));
    scores.push_back(Score(87, "Java"));
    scores.push_back(Score(41, "Android"));
}

1-5. 포인터 벡터

객체를 직접 보관하는 벡터의 문제점

1
2
3
4
5
6
7
8
9
10
vector<Score> scores;
scores.reserve(1);                              // 공간 1 예약
scores.push_back(Score(10, "C++"));

scores.push_back(Score(41, "Android"));         // 메모리 재할당 발생
                                                // 그런데 객체를 보관하는 vector 이므로
                                                // 오브젝트들을 다 복사해야 하는 상황이 발생...
                                                // capacity, size, 주소까지 합쳐서 12바이트 정도 복사가 되는 거임.
                                                
vector<Score> scores2 = scores;                 // 대입을 하여 사본이 생기는 것이니 또 복사가 일어나게 됨.

메모리 복사 양이 많아진다. 💡 그래서 포인터를 저장하는 것으로 해결하자!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
    vector<Score*> scores;                      //Score의 포인터를 저장하겠다는 변수
    scores.reserve(2);

    scores.push_back(new Score(30, "C++"));     // 객체를 힙에 만들어서 저장하고, 벡터는 스택에 만들어져 주소 저장
    scores.push_back(new Score(87, "Java"));    // 재할당 발생. 벡터 변수 4바이트를 8바이트로 늘리는 재할당일 뿐
    scores.push_back(new Score(41, "Android"));

    for (vector<Score*>::iterator iter = scores.begin(); iter != scores.end(); ++iter)
    {
        delete *iter;                           // delete 해주기!
    }

    scores.clear();                             // 객체를 생성했을 경우 clear 전에 delete 해주는 것 까지 필요함!

    return 0;
}

2. 벡터의 장점과 단점

  • 장점
    • 순서에 상관 없이 요소에 임의적으로 접근 가능 (인덱스만 알면)
    • 제일 마지막 위치에 요소를 빠르게 삽입 및 삭제
  • 단점
    • 중간에 요소 삽입 및 삭제는 느림
    • 재할당 및 요소 복사에 드는 비용이 있음

이슈 및 공부한 것을 기록해두는 개인 블로그 입니다. 댓글, 피드백 환영합니다 🙂

Update:

댓글남기기