STL은 "Standard Template Library"의 약자로, C++ 프로그래밍 언어에서 제공하는 템플릿 기반의 라이브러리이다.
C++에서는 구조체와 객체를 사용해서 여러 데이터를 포괄하는 개념을 이름으로 제시하고, 해당 이름을 호출하면 사전에 정해둔 과정에 따라 좀 더 복잡한 방법으로 데이터를 다룰 수 있게 조치하는 것이 관건.
그래서 나온 것이 "수정이 가능한 문자열", "크기가 바뀌는 배열", "메모리 관리에 적합한 최적화 데이터 집합" 등을 만들게 되는데 이 추가 기능들이 흔히 STL이라고 불리는 기능의 실체이다. 따라서 STL은 데이터 자료구조와 알고리즘을 효율적으로 사용할 수 있도록 다양한 기능을 제공한다.
문자열
#include <string>
int main()
{
std::string str = "가나다라마바사"; // <- 문자 배열을 응용한 클래스와 객체
// 문자열의 특징
str = "computer"; // 수정 가능
// 자주 쓰는 문자열 기능과 호출법
str += " is not so dumb"; // 문자열을 연산으로 추가 가능
//str -= " is not so dumb"; // 삭제는 연산으로 안 됨
str.append("!"); // append : 추가
const char* cPtr = str.c_str(); // c_str() : 문자열 (상수 포인터) 변환
std::cout << cPtr << std::endl; // 포인터 (주소가 나와야 했지만 C++라서 문자)
str.at(0); // at : n번째 글자
str.begin(); // begin() : '첫 데이터', 단순 값이 아니라 주소도 포함된 값
std::cout << *str.begin() << std::endl; // 사용시엔 역참조로
str.erase(str.begin()); // erase : 지우기 (글자x 데이터o)
}
벡터(vector)
데이터가 한 줄로 이어서 작성된 모습을 열차(vector)에 빗대에 이름이 벡터라고 붙여졌다고 한다.
C++에서 배열을 만들면, 변수 0번(배열의 첫 원소) 데이터를 메모리 어딘가에 만들고, 바로 그 뒤에 1번, 다시 그 뒤에 2번..
이렇게 끝까지 한줄로 이어붙여서 만든다. 배열에서 사용하는 []는 결국 최초 뎅리터 발생 주소로부터 변수 몇개 만큼의 자리가 떨어져 있는지 나타내는 "포인터"이다. 또한 배열이 바뀔 때 (원소가 바뀌든, 크기가 바뀌든...) 계속 새로운 주소를 제시해주면 배열 전체도 자유롭게 바뀔 수 있다
#include <vector>
int main()
{
// 벡터는 범용이기 때문에, 역설적으로 사용할 때
// "무엇의" 벡터인지 매우 정확히 제시해줘야 한다.
std::vector<int> vec; // <> 안에 쓰고 싶은 자료형을 넣는다
// 만들어진 벡터에 원소 추가하는 법
vec.push_back(1); // push_back(x) : 벡터의 맨 마지막 순서에 x라는 데이터를 (자료형에 맞게) 추가
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
// 벡터 출력 : 배열처럼 출력
for (int i = 0; i < 5; ++i)
{
std::cout << vec[i] << std::endl;
}
vec.pop_back(); // pop_back() : 제일 마지막 데이터 삭제
for (int i = 0; i < 4; ++i)
{
std::cout << vec.at(i) << std::endl; // at(n) : n번째 원소
// [] 와 at의 차이 : []는 포인터. at은 함수
// []는 빠르다. 원하면 허용된 범위 이상도
// 보려고 시도해볼 수 있다 (오류 100%지만)
// at은 느린 대신, 안정적이다. 다른 말로
// 잘못된 시도가 나오면 100% 튕김 아니면
// 오류 데이터 반환만을 한다. = 내 의도 100% 가능
}
// 벡터에서 중간에 추가나 삭제를 할 때
vec.insert(vec.begin() + 3, 5); // 추가
// 매개변수 : 추가 위치, 추가 데이터
// 추가 위치에서 위치는 순번x (정수로 표현된 인덱스)
// vec.begin()에서 시작된 '메모리 주소'
// 여기에 +n을 해서 결과적으로 n번 원소 자리에
// 데이터 추가 가능
// * vec.begin()의 자료형 : 'iterator' 혹은 반복 순찰자
// (줄여서 그냥 '반복자'라고 더 많이 부른다)
// 반복자를 사용한 반복문을 써보면
for (std::vector<int>::iterator iter = vec.begin();
iter != vec.end(); // 반복자가 벡터 밖으로 나가기 전까지
++iter)
{
// 반복자 역참조하고 출력하기
std::cout << *iter << std::endl;
}
// 벡터의 중간에서 데이터 빼기
// 위치를 먼저 변수로 파악
std::vector<int>::iterator it = vec.begin();
vec.erase(it + 4); // erase() : 중간에 지우기
vec.insert(vec.begin() + 2, 10); // 직접 시작점 지정
//벡터에서 지워진 데이터를 확인
for (it = vec.begin(); // it 리셋
it != vec.end();
++it)
{
std::cout << *it << std::endl;
}
// 최종 출력
for (int i = 0;
i < vec.size(); // size() : 벡터의 현재 원소 개수
++i)
{
//size()를 사용하면 벡터가 몇 개든 유연하게 반복문 가능
std::cout << vec[i] << std::endl;
}
}
벡터에서 반복자 iterator를 별도로 사용하는 이유는 다음과 같다.
1. 벡터가 가진 클래스로서의 데이터를 다 쓰려고
2. 연산 시에 메모리의 시작점을 정확히 알기 위해
3. 모든 자료구조가 일렬 배치인 것이 아니라서 (각 원소의 메모리 주소와 관계를 정확히 알기 위해)
벡터를 사용할 때 주의할 점: 반복자를 설정하고, 벡터에 insert, erase를 반복 사용하면 점점 반복자의 데이터가 사용 불능이 될 확률이 높아진다. 그 이유는 벡터의 메모리 일렬 정렬 때문. 따라서, 벡터의 원소를 중간에 추가, 삭제한 경우 그 뒤에 반복자를 다시 설정해주어야 한다.
리스트
리스트는 무작위로 배치된데이터에 연관성이 있을 때 그 데이터를 서로 묶어서 하나의 목록으로 쓰게 만든 것이다. 리스트는 배열과 달리, 원소 사이에 주소 연관성이 없다.
#include <list>
// 리스트 작성
std::list<int> lst; // 벡터와 마찬가지로, 무엇의 리스트인지 제시
// 원소 추가
lst.push_back(1);
lst.push_back(2);
lst.push_back(3);
lst.push_back(4);
lst.push_back(5);
lst.push_back(10);
lst.pop_back(); // 삭제도 됨
for (int i = 0; i < lst.size(); ++i)
{
//std::cout << lst[i] << std::endl; // 인덱스 사용불가
//std::cout << lst.at(0) << std::endl; //at 사용불가
}
// 반복자를 이용해 리스트 출력
std::list<int>::iterator itList = lst.begin();
for (itList;
itList != lst.end();
++itList)
{
std::cout << *itList << std::endl;
}
리스트의 각 원소는 메모리 위치가 모두 제각각이라 포인터를 사용할 때 활용한 인덱스 접근으로는 볼 수 없다
리스트는 각 원소는 "내 다음 원소는 주소가 지금 어디인지" 에 대한 데이터를 갖는다. 그리고 이 데이터를 전부 끌어낼 수 있는 반복자도 있다. 리스트는 이 반복자가 전체 데이터 파악에 필수다.
추가로 리스트에서 중간에 원소를 넣거나 지우는 함수를 다음과 같이 작성해보았다.
#include <iostream>
#include <list>
using namespace std;
// 리스트의 a번째에 b값을 넣는 함수
void list_insert(list<int>& mylist, int a, int b)
{
list<int>::iterator itlist = mylist.begin();
for (int i = 0; i < a-1; i++)
{
itlist++;
}
mylist.insert(itlist, b); //itlist 반복자를 이용해 a번째로 옮긴 후 b를 삽입
}
// 리스트의 a번째 값을 삭제하는 경우
void list_delete(list<int>& mylist,int a)
{
list<int>::iterator itlist = mylist.begin();
for (int i = 0; i < a - 1; i++)
{
itlist++;
}
mylist.erase(itlist); // 마찬가지로 반복자를 이용해 a번째 값 삭제
}
int main()
{
list<int> lst;
// 임의로 리스트 생성
lst.push_back(1);
lst.push_back(2);
lst.push_back(3);
lst.push_back(4);
lst.push_back(5);
lst.push_back(10);
std::list<int>::iterator itList = lst.begin();
list_insert(lst, 3, 999);
list_delete(lst, 2);
// 출력
for (itList;
itList != lst.end();
++itList)
{
std::cout << *itList << std::endl;
}
}
큐(Queue, 디큐(Dequeue) : 벡터의 추가, 삭제 규칙을 용도에 맞게 변형한 자료구조이다.
#include <queue>
// 큐
std::queue<int> q; // 정수의 큐
q.push(1); // 원소 추가 함수에 _back 철자가 없어졌다.
q.push(2);
q.push(3);
q.push(4);
q.push(5);
q.pop(); // pop도 마찬가지. _back이 없다.
q.pop();
// 출력
//for (int i = 0; i < q.size(); ++i)
//for (std::queue<int>::iterator it) // 반복자 불가
{
//std::cout << q[i] << std::endl; // <- [] 안 먹힌다.
}
// 출력
for (int i = 0; i < 3; ++i)
{
std::cout << q.front() << std::endl;
// front() : 자료구조의 0번 원소의 값
q.pop(); //삭제
}
큐의 특징은 다음과 같다.
1. 데이터는 기본 일렬 배치를 선호하지만 거기에 목숨 걸진 않는다.
2. 먼저 들어온 데이터가 먼저 삭제된다('선입, 선출' 혹은 단순히 '선착순'이라고도 한다)
3. 인덱스(주소에 의한 접근) 혹은 반복자(기준에 의한 데이터)가 큐에서는 의미가 없다. 선착순 처리가 무엇보다 필요하고, 나머지 계산이 불필요할 때 자주 사용되는 자료 구조이다.
디큐는 큐의 전후 확장 버전이다.
#include <deque>
std::deque<int> dq; // 디큐 작성
dq.push_back(1); // push_back사용가능
dq.push_back(2);
dq.push_back(3);
dq.push_back(4);
dq.push_back(5);
dq.push_front(6); // push_front(x) : 자료의 맨 처음에 x의 데이터를 추가하는 함수
dq.push_front(7);
dq.push_front(8);
dq.push_front(9);
dq.push_front(10);
for (int i = 0; i < 10; ++i)
{
std::cout << dq[i] << std::endl; // 인덱스 연산자 부활
}
디큐는 결과적으로 앞뒤로 넣고 뺄 수 있는 큐이다.
'C++' 카테고리의 다른 글
C++) 클래스(Class) (0) | 2025.01.08 |
---|---|
C++) 동적 할당 (0) | 2025.01.06 |
C++) 포인터(Pointer) (0) | 2025.01.06 |
C++) 인터페이스(Interface) (0) | 2024.12.26 |
C++) 템플릿(Template) (0) | 2024.12.26 |