map은 템플릿 클래스이기 때문에 선언할 때 <> 사이에 key 값의 타입, value 값의 타입을 순서대로 적어줘야 한다.
key 타입과 value 타입이 같지 않아도 된다.
map<int, string> m;
이렇게 타입이 달라도 상관없다.
이렇게 선언된 map에 데이터를 저장하고 싶다면
m[1] = "one";
이렇게 하면 된다.
크기가 확보된 것도 아닌데 빈 map 객체인데도 이렇게 인덱스로 저장이 가능하다.
벡터의 경우에 데이터를 추가하고 싶다면 push_back() 함수를 써야 하고 set의 경우에는 insert() 함수를 써서 데이터를 추가할 수 있었는데 map은 원래 1이라는 인덱스가 있는 것처럼 저렇게 key 값을 [ ] 사이에 적고 그 키 값에 해당하는 value 값을 대입해주면 된다.
다만 주의할 것은 map이라는 자료구조는 key를 통해서 원하는 값을 꺼내오는 것이기 때문에 key 값은 유일한 값이어야 한다. 즉 key 값들 끼리는 중복이 없어야 한다. 다음 코드를 보자.
함수들의 사용법은 매우 간단하기 때문에 이 예제 코드와 실행 결과만 봐도 알 수 있을 것이라 생각된다.
그럼 이제 map에 저장된 모든 데이터 쌍을 순회하는 법을 알아보자.
배열이나 벡터 같은 경우는 for(int i = 0; i < n; i++) 이 문장으로 모든 것이 가능했다. 이렇게 하고 for문 안에서 i를 인덱스로 해서 그 인덱스에 해당하는 원소에 접근할 수 있었다. 하지만 map은 인덱스로 접근이 불가능하고 key 값으로 접근이 가능한 자료구조이기 때문에 순회를 하려면 iterator를 사용해야 한다.
#include <iostream>
#include <string>
#include <map>
#include <tuple>
using namespace std;
int main() {
map<string, int> m;
m["김철수"] = 15;
m["김영희"] = 23;
m["홍길동"] = 40;
for (map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++) {
cout << "이름: " << iter->first << ", 나이: " << iter->second << endl;
}
return 0;
}
for(int i = 0; i < n; i++) 대신에 for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)를 쓰는데
for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)
이 코드 꼭 알아두자.
iterator는 주소이기 때문에 iter.first가 아니라 iter->first 또는 (*iter).first로 접근이 가능하다.
예를 들어 하나의 원소가 8바이트인 map 객체가 있는데 3개의 쌍이 들어있고 map의 시작 주소는 1000이라고 하자.
그러면 첫 번째 원소는 1000번지~1007번지 이렇게 8바이트에 걸쳐서 저장되어 있다.
두 번째 원소는 1008번지~1015번지에, 세 번째 원소 즉 마지막 원소는 1016번지~1023번지에 저장되어 있을 것이다.
iterator 변수인 iter를 iter++를 하면 iter가 가지고 있는 주소는 8바이트씩 증가한다.
즉 처음에 iter=m.begin() 이렇게 선언되었을 때 iter는 1000, for문을 한 번 돌고 나면 iter++에 의해 1008이 되고, 그 다음 for문을 돌면서 1016이 된다. 그리고 iter++에 의해서 1024가 된다. 이 1024가 바로 m.end()이다.
그렇게 for문을 돌면서 순회하다가 for문에서 조건 체크를 한다. iter != m.end()이어야 for문의 body 부분으로 들어갈 수 있는데 1024는 m.end()이다. 따라서 for문을 빠져나가게 되는 것이다.
배열의 크기를 나타내는 [] 안에는 const로 선언된 상수 변수 또는 리터럴 상수만 들어갈 수 있다.
따라서 이 경우 포인터를 이용한 동적할당을 해야 한다.
이렇게 동적 할당을 해주면 에러가 안 나고 크기가 n인 int 배열이 잘 선언되었다.
하지만 배열의 크기를 미리 알지 못 할수도 있다.
입력이 계속 들어오고 입력이 0일 경우 입력을 그만 받는 프로그램이 있을 수도 있다.
이 경우 배열의 크기를 얼마로 잡아야 할까?
물론 int arr[100000]; 이렇게 크게 잡아놓고 입력을 받을 수도 있지만 메모리 공간을 너무 낭비한다는 단점이 있고
입력으로 들어온 수의 개수를 초과하는 인덱스에 접근해도 IndexOutOfRange예외가 발생하지 않기 때문에 쓰레기값을 가지고 엉뚱한 계산을 할 수도 있다.
배열의 이러한 한계를 극복한 것이 vector 클래스이다. 벡터는 사용하다가 크기가 부족해도 크기를 늘릴 수도 있고 초기에 크기를 선언하지 않아도 되기 때문에 배열보다 훨씬 유연하다. 그리고 벡터는 배열과 다르게 클래스이기 때문에 유용한 함수들도 들어있어서 더욱 편하다.
C++에서 우선순위 큐를 구현하려면 <queue> 라이브러리를 사용하면 된다. 따라서 #include <queue> 코드를 써줘야 한다.
우선순위 큐를 선언하는 코드는 다음과 같다.
priority_queue<int, vector<int>> q;
이렇게 큐를 선언하면 숫자가 클 수록 우선순위가 높은 우선순위 큐가 만들어진다.
priority_queue<int, vector<int>> q;에서 이 안에서 int는 큐에 들어갈 데이터의 타입을 말한다.
priority_queue<int, vector<int>> q;에서 vector<int>는 데이터들이 들어갈 컨테이너이다. 실제로는 이 곳에 int 데이터들이 저장된다. 하지만 동작을 우선순위 큐 처럼 하게 하기 위해서 priorty_queue 타입으로 감싼 것이라고 보면 된다.
벡터든 배열이든 정렬을 하려면 <algorithm> 라이브러리의 sort() 함수를 쓰면 된다. 따라서 <algorithm> 헤더파일을 포함해줘야 한다. sort() 함수의 첫번째 두번째 매개변수는 iterator, 즉 포인터이다.
배열의 경우 (배열의 크기가 10인 경우)
sort(arr, arr + 10);
벡터의 경우
sort(v.begin(), v.end());
이렇게 하면 된다.
첫 번째 인자로는 정렬하고 싶은 데이터 집합이 배열이라면 배열의 이름을 넘겨주면 되고 (배열의 이름은 포인터니까) 벡터라면 v.begin()을 넘겨주면 된다. (벡터에서 v.begin()을 하면 벡터의 시작 주소를 반환한다.) 두 번째 인자로는 정렬하고 싶은 데이터 집합이 배열이라면 배열의 이름 + 배열의 크기을 넘겨주면 되고 벡터라면 v.begin()을 넘겨주면 된다. (벡터에서 v.end()을 하면 벡터의 마지막 주소를 반환한다.) 이렇게 매개변수 두 개를 모두 넣어주고 sort() 함수를 호출하면 오름차순으로 정렬된다.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 4, 7, 2, 5, 10, 8, 1, 6, 3 };
cout << "정렬 전: ";
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
sort(v.begin(), v.end());
cout << "정렬 후: ";
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
return 0;
}
이 코드의 실행 결과는 다음과 같다.
sort() 함수 호출 이후에는 오름차순으로 정렬되어 있는 것을 볼 수 있다.
배열로 해도 마찬가지이다.
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int arr[10] = { 9, 4, 7, 2, 5, 10, 8, 1, 6, 3 };
cout << "정렬 전: ";
for (int i = 0; i < 10; i++) {
cout << arr[i] << " ";
}
cout << endl;
sort(arr, arr + 10);
cout << "정렬 후: ";
for (int i = 0; i < 10; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
이 코드를 실행해도
배열의 원소들이 오름차순으로 정렬된 것을 볼 수 있다. 벡터를 정렬하려면 sort(v.begin(), v.end());이고 배열을 정렬하려면 sort(arr, arr + 10);라는 것 기억하자.
벡터를 내림차순으로 정렬하고 싶다면 sort(v.begin(), v.end()); 대신에 sort(v.rbegin(), v.rend());를 쓰면 된다.
sort(v.rbegin(), v.rend());
그런데 오름차순이 아니라 내가 원하는 정렬 기준을 세워서 정렬하고 싶다면? 정렬하고 싶은 대상이 기초 타입이 아니라면? sort() 함수의 세 번째 인자로 비교기준이 되는 함수포인터 또는 함수 객체를 넣어주면 된다. 그 함수는 반환타입이 bool 타입이어야 하고 매개변수는 두 개이며 두 매개변수의 타입은 정렬할 데이터의 타입과 일치해야 한다. 내림차순으로 정렬하고 싶다면 compare 함수를 이렇게 만들면 된다.
bool cmp(int a, int b) {
return a > b;
}
이 cmp 함수를 sort() 함수의 3번째 인자로 넣어서 아래 코드를 실행하면
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
bool cmp(int a, int b) {
return a > b;
}
int main() {
vector<int> v = { 4, 7, 2, 5, 10, 8, 1, 6, 3 };
cout << "정렬 전: ";
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
sort(v.begin(), v.end(), cmp);
cout << "정렬 후: ";
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
return 0;
}
이렇게 내림차순으로 정렬된 것을 볼 수 있다.
배열을 정렬할 때에도 똑같이 하면 된다.
#include <iostream>
#include <algorithm>
using namespace std;
bool cmp(int a, int b) {
return a > b;
}
int main() {
int arr[10] = { 9, 4, 7, 2, 5, 10, 8, 1, 6, 3 };
cout << "정렬 전: ";
for (int i = 0; i < 10; i++) {
cout << arr[i] << " ";
}
cout << endl;
sort(arr, arr + 10, cmp);
cout << "정렬 후: ";
for (int i = 0; i < 10; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
이렇게 배열과 벡터를 정렬하는 방법을 배워보았다. 만약에 정렬하고 싶은 자료들의 타입이 기초타입이 아니라 내가 정의한 클래스라고 해도 compare 함수만 구현해서 sort 함수의 인자로 넣어준다면 나만의 기준으로 정렬할 수 있다.
#include <iostream>
using namespace std;
int main()
{
int* p;
{ int a;
a = 2;
p = &a;
cout << *p << endl;
}
cout << *p << endl; //허상 참조 발생
return 0;
}
변수 a의 범위(scope)는 변수 a가 선언된 블럭 안에서만이다.
a가 선언된 그 중괄호를 벗어나면 a는 메모리의 스택에서 사라진다. 그리고 메모리의 그 공간을 안 쓰는 공간이라고 생각하게 된다.
따라서 메모리의 그 위치에 다른 데이터를 쓸 수도 있다.
그런데 포인터 p는 a보다 한 블럭 바깥 블럭에서 선언되었다.
(당연히 선언과 초기화를 따로 할 수도 있다.)
포인터 변수도 일반 변수와 마찬가지로
int* pa = &a;
이렇게 한 줄로 선언과 초기화를 동시에 할 수도 있는거고
int* pa;
pa = &a;
이렇게 선언과 초기화를 따로 두 문장으로 할 수도 있는 것이다.
아무튼 p가 존재하는 범위는 a보다 범위가 한 블럭 바깥 범위이다. 따라서 중괄호가 끝나 a는 메모리에서 사라졌지만 p는 아직도 살아있고 a가 저장되어 있었던 그 주소를 아직도 가리키고 있다.
이렇게 a의 scope은 끝났지만 p의 scope은 끝나지 않았을 때 p가 간접참조연산자 *을 이용해서 *p 이렇게 접근해서 출력하면 2가 아니라 다른 이상한 값을 출력할 수도 있다.
메모리의 그 주소에 마침 다른 데이터가 아직 써지지 않았다면 2를 출력할 것이고 아니라면 이상한 값을 출력할 것이다.
그런데 이렇게 읽어만 와서 출력하는는 것은 그나마 다행이지 만약에 *p = 100; 이런 코드를 a의 scope 바깥에서 쓴다면 정말 치명적인 오류가 발생할 수도 있다. 그 주소에 중요한 데이터가 있었을 지도 모르는데 그 데이터를 100으로 덮어쓰는 것이기 때문이다.
이렇게 scope이 끝난 변수에 접근하는 것을 허상 참조(dangling pointer)라고 한다.
이처럼 포인터는 주소를 가지고 메모리의 그 위치에 접근할 수 있다는 강력한 기능을 가지고 있으면서도
그 장점으로 인한 치명적인 오류가 발생할 수도 있다.
그러한 오류를 막기 위해서 포인터를 이용한 코드를 짤 때에는 이러한 습관을 들이는 것이 좋다.
포인터 변수를 선언하고 바로 초기화해주지 않을 때에는 NULL로 초기화해주는 것이 좋다.
int* pa = NULL;
이렇게 NULL로 초기화를 해준다면 적어도 아까와 같은 치명적인 오류는 발생하지 않는다.
(참고로 NULL은 포인터 변수에만 사용할 수 있다.)
예를 들어 int* pa; 이렇게 선언해놓고 '나중에 pa에 a의 주소를 담아야지' 하고 생각하고 있었는데 깜빡하고 pa = &a; 이 코드를 쓰지 않고 pa에는 a의 주소가 있겠거니 하고 *pa 이렇게 역참조하면 아까와 같은 치명적인 오류가 발생할 것이다. pa를 초기화하지 않았기 때문에 pa가 선언된 메모리의 주소에 있던 쓰레기 값 32비트를 주소라고 생각하고 억지로 읽어와서 그 32비트 주소 메모리 위치에 접근해서 그 곳의 데이터를 바꿀 수가 있다.
하지만 NULL로 초기화해놨다면
널포인터라며 예외(런타임에러)가 발생한다. 이것은 치명적인 오류가 아니라 이것을 보고 '아 내가 널 포인터를 썼구나' 하고 코드를 짠 사람이 알게 되고 코드를 고칠 것이다.
하지만 전혀 어렵지 않다는 것을 알게 되었고 다른 사람들에게도 포인터가 어렵지 않다는 것을 전파시키고 싶다.
일반적으로 변수는 정수, 실수, 문자 등 데이터 값을 저장한다.
하지만 포인터 변수는 위와 같은 데이터가 저장되어 있는 메모리 주소를 저장하는 특별한 변수이다.
변수 이름 앞에 &를 붙이면 그 변수가 저장된 메모리의 주소이다.
#include <iostream>
using namespace std;
int main() {
int a = 100;
cout << &a << endl; // a의 주소 출력
return 0;
}
위 코드를 실행하면 이런 결과가 나온다.
a가 저장된 메모리의 주소가 출력되는 것을 볼 수 있다. 이처럼 &a 라는 표현은 a의 주소라는 것이다.
잊지 말자 어떤 변수의 주소를 사용하고 싶다면
&변수이름
이렇게 쓰면 된다.
그런데 012FF71C와 같은 주소를 어떤 변수에 저장하고 싶다면? 그 변수의 타입은?
주소는 어떤 타입의 변수에 저장해야 하는걸까?
포인터 변수에 저장하면 된다. 포인터 변수는 무조건 4바이트이다. 왜냐하면 포인터 변수는 어떤 변수(그 변수 이름을 a라고 하자)의 주소를 담는 변수인데 a가 int 타입이건 double 타입이건 char 타입이건 메모리 주소는 32비트(4바이트)이기 때문이다. (32bit 시스템의 경우)
그럼 포인터 변수는 무슨 타입의 변수일까?
포인터에도 여러 타입이 있다.
int 타입 a의 주소를 저장하려면 int* 타입의 포인터 변수에 저장하면 되고
short 타입 변수인 b의 주소를 저장하려면 short* 타입의 포인터 변수에 저장하면 되고
char 타입 변수인 c의 주소를 저장하려면 char* 타입의 포인터 변수에 저장하면 된다.
포인터 변수도 다른 변수들과 마찬가지로 사용하기 전에 선언해야 한다.
예를 들어 a라는 int 타입 변수의 주소를 pa라는 포인터 변수에 저장하려면
int a = 100;
int* pa = &a; //포인터 변수 선언 및 a의 주소 대입
이렇게 하면 된다. 이제 pa에는 a의 주소 32비트(4바이트)가 저장되어 있다.
메모리 몇 번지인지 pa에 저장된 주소를 보고 싶다면 직접 cout << pa; 해 보면 된다.
pa에는 주소가 담겨 있다는 것을 알 수 있다.
그러니까 cout << &a;와 cout << pa;는 똑같이 a의 주소를 출력한다.
그럼 int 말고 다른 타입 변수들도 포인터 변수에 주소를 저장해 보자.
int count = 5;
short status = 2;
char letter = 'A';
int* pCount = &count;
short* pStatus = &status;
char* pLetter = &letter;
이 경우 메모리 상에는 이렇게 저장된다.
count는 int 타입이기 때문에 4칸에 걸쳐서 5가 저장되고 status는 short 타입이기 때문에 두 칸에 걸쳐서 2가 저장되고 letter은 char 타입이기 때문에 한 칸에 'A'의 아스키코드 값인 65(16진수로 41)가 저장된다. (메모리는 1바이트마다 주소가 부여되어 있기 때문에 메모리 한 칸은 1바이트라고 생각하면 된다.)
그리고 포인터 변수인 pCount는 4칸에 걸쳐서 count가 저장된 메모리의 주소를 저장하고 있고
pStatus도 4칸에 걸쳐서 status가 저장된 메모리의 주소를 저장하고 있고
pLetter도 4칸에 걸쳐서 letter가 저장된 메모리의 주소를 저장하고 있는 것을 볼 수 있다.
그러면 여기에서 의문이 들 것이다. 일반 변수를 선언할 때 타입을 명시하는 이유는
int 타입은 메모리 4바이트를 잡아야 하고 double 타입은 8바이트, char은 1바이트, ... 이런 식으로 타입마다 잡아야 하는 메모리의 크기가 다르니까 그러는 것이지만
포인터 변수는 어차피 다 4바이트인데 왜 선언할 때 int*, short*, char* 이런 식으로 타입을 다르게 선언하는 것일까?
어차피 다 4바이트인데?
그 이유는 여기에 있다.
포인터 변수는 간접 참조 연산자 *를 통해서 그 주소에 저장된 값을 읽어올 수 있다.
예를 들면 int*형 포인터 변수 pCount에 저장된 주소를 가지고 count에 저장된 값인 5를 읽어올 수 있다.
int count = 5;
int* pCount = &count;
cout << *pCount << endl; //5 출력
pCount에는 주소인 00B3FC00이 저장되어 있다. 그리고 그 주소 00B3FC00번지에는 5가 저장되어 있다.
*pCount는 pCount에 있는 그 주소(00B3FC00번지)에 가서 거기에 있는 값을 가져오라는 것이다.
따라서 cout << *pCount;나 cout << count;나 똑같이 5를 출력한다.
정리하면
*포인터변수
는 '그 주소에 가서 거기에 저장되어 있는 값을 가져와'라는 말이다.
그런데 문제가 있다. *pCount라고 해서 일단 00B3FC00번지에 가긴 했는데 여기에서 몇 바이트를 읽어와야 하지?
00B3FC00번지에는 00이 저장되어 있고 그 다음 번지에는 00, 그 다음 번지에는 00, 그 다음 번지에는 05, 그 다음 번지에는 00, 그 다음 번지에는 02가 저장되어 있는데 몇 칸을 읽어와야 하는 걸까?
여기에서 포인터 변수의 타입이 필요하다. pCount는 int* 타입의 포인터 변수이다. int는 4바이트이므로 00B3FC00번지부터 4바이트 만큼의 데이터를 읽어오면 된다. 즉 00000005를 읽어오는 것이다.
그러니까 포인터 변수의 타입은 * 연산자를 이용해서 그 주소에 있는 값을 읽어올 때 몇 칸을 읽어올지 알기 위해서 있는 것이다. int* 타입의 포인터 변수는 4바이트를 읽어오면 되고 char* 타입의 포인터 변수는 1바이트를 읽어오면 되고 double* 타입의 포인터 변수는 8바이트를 읽어오면 된다.
만약 변수의 타입과 포인터 변수의 타입이 다르다면
이렇게 컴파일 에러가 난다. 따라서 반드시 변수의 타입과 포인터의 타입을 일치시켜야 한다.
이제 포인터의 기본을 배웠으니 예제 코드를 살펴보자.
#include <iostream>
using namespace std;
int main() {
int count = 5;
int* pCount = &count;
cout << "The value of count is " << count << endl;
cout << "The address of count is " << &count << endl;
cout << "The address of count is " << pCount << endl;
cout << "The value of count is " << *pCount << endl;
return 0;
}
위의 코드의 실행 결과이다.
count와 *pCount는 5를, &count와 pCount는 주소를 출력하는 것을 볼 수 있다.
find() 함수는 최초 발견된 위치를 반환하기 때문에 문자열에서 'o'가 있는 모든 인덱스를 찾고 싶으면 이렇게 사용하면 된다.
string s1("Welcome to c++");
int idx = 0;
int count = 1;
while (s1.find('o', idx) != string::npos) {
idx = s1.find('o', idx);
cout << count << "번째로 발견된 o의 위치는 " << idx++ << "입니다." <<endl;
count++;
}
이렇게 find() 함수의 반환값 + 1을 find 함수의 두 번째 인자로 주면 계속 검색할 수 있다. (여기에서는 후위연산자로 idx를 1 증가시켜줬다)
만약에 찾지 못하면 string::npos를 반환하는데 npos는 const형 static 변수이다. (npos는 not position의 줄인 말이다.)
static 멤버를 사용할 때에는 클래스이름::을 앞에 붙여줘야 한다.
문자열 삽입과 교체
+insert(index: int, s: string)
문자열의 index 위치에 문자열 s 삽입
+insert(index: int, n: int, char: c)
문자열의 index 위치에 문자 c를 n개 삽입
+replace(index: int, n: int, s: string)
문자열의 index 위치부터 n개를 문자열 s로 교체
string s1("Welcome to HTML");
s1.insert(11, "C++ and "); // s1의 인덱스 11 위치에 문자열 삽입
cout << s1 << endl; // Welcome to C++ and HTML 출력
string s2("AA");
s2.insert(1, 4, 'B'); //s2의 인덱스 1 위치에 문자 4개 삽입
cout << s2 << endl; //ABBBBA 출력
string s3("Welcome to HTML");
s3.replace(11, 4, "C++"); // s3의 인덱스 11부터 4개를 문자열로 교체
cout << s3 << endl; // Welcome to C++ 출력