728x90

프로그램이 실행되다가 실행이 끝나면 프로그램이 종료되기 때문에 실행창이 바로 꺼지는 것이다.

이 때 return 문 앞에 system("pause"); 이 코드는 실행이 끝나도 창이 바로 꺼지지 않고 멈춰있게 하는 것이다.

system("pause");
return 0; //리턴문 앞에
728x90
728x90

map은 데이터를 key-value 형태로 저장하는 자료구조이다.

http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

 

map을 선언하는 방법은 다음과 같다.

map<int, int> m;

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 값들 끼리는 중복이 없어야 한다. 다음 코드를 보자.

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	return 0;
}

이런 코드가 있을 때 이 코드를 실행하면

이렇게 철수와 영희의 나이가 제대로 출력된 것을 볼 수 있다.

 

하지만 10살짜리 김철수라는 동명이인이 있어서 map에 이 김철수의 데이터를 추가하면 어떻게 될까?

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	m["김철수"] = 10; //새로 추가된 10살짜리 김철수
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	cout << "철수의 나이: " << m["김철수"] << endl;
	return 0;
}

 

새로운 김철수가 추가된 것이 아니라 원래 있던 15짜리 철수의 나이가 10살로 바뀐 것을 볼 수 있다.

따라서 map을 사용할 때 key는 중복될 수도 있는 데이터가 아니라 unique한 데이터를 key 값으로 하는 것이 좋다. 만약 이 예제처럼 했다면 나도 모르게 이런 실수를 하고는 이상한 결과가 나온다고 코드를 붙잡고 끙끙댈 수가 있다.

 

이제 map 객체 선언 방법과 map에 데이터를 추가하는 방법을 알았으니 유용한 함수들을 알아보자.

+size(): size_type map에 저장된 쌍의 개수를 반환한다.
+empty(): bool map이 비어있는지 아닌지 반환한다.
+find(k: key_type): iterator 키값인 key가 있으면 그 iterator 반환, 없으면 end() 반환
+count(k: key_type): size_type k라는 키값을 가진 key의 개수를 반환한다. (0또는 1 반환)
+insert(pair<key_type, value_type>) pair의 first 값을 key로, second 값을 value로 가지는 쌍을 map 객체에 추가
+erase(k: key_type): size_type 해당 키 값을 가진 데이터를 삭제한다.
+erase(position: iterator): iterator 해당 iterator 위치에 있는 원소를 삭제한다.
+begin(): iterator map 객체의 시작 주소를 반환한다.
+end(): iterator map 객체의 가장 끝 원소의 다음 주소를 반환한다.

count() 함수는 사실 find() 함수로 대체할 수 있다. count() 함수는 해당 키 값을 가진 key의 개수를 반환한다고는 하지만 key 값들은 서로 중복이 없는 유일한 값이기 때문에 많아봤자 최대 1을 반환한다.

#include <iostream>
#include <string>
#include <map>
#include <tuple>
using namespace std;


int main() {
	map<string, int> m;
	cout << "m의 크기: " << m.size() << endl; // 0 출력
	cout << "m은 비어있습니까? " << (m.empty() ? "true" : "false") << endl; //true 출력
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "m의 크기: " << m.size() << endl; //2 출력
	m.insert(make_pair("홍길동", 40));
	cout << "홍길동의 나이: " << m["홍길동"] << endl; //40 출력
	cout << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	map<string, int>::iterator iter = m.find("김철수");
	if (iter != m.end()) cout << "김철수의 나이: " << iter->second << endl; // 주소는 -> 연산자를 통해 접근. (*iter).second도 가능
	cout << ((m.find("김민지") != m.end()) ? "m에 김민지 있습니다." : "m에 김민지 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	m.erase(iter); //iterator를 이용해 삭제
	cout << "삭제 후: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	m.erase("홍길동"); //key 값으로 삭제
	cout << "삭제 후: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	return 0;
}

이 코드를 실행하면 다음과 같은 결과가 나온다.

 

함수들의 사용법은 매우 간단하기 때문에 이 예제 코드와 실행 결과만 봐도 알 수 있을 것이라 생각된다.

 

그럼 이제 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문을 빠져나가게 되는 것이다.

포인터에 대해 익숙치 않은 분이라면 https://breakcoding.tistory.com/81

 

[C++] 포인터

포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다. 하지만 전혀 어렵지 않다는 것을 알게 되었고 다른 사람들에게도 포인터가 어렵지 않다는 것을 알려주고 싶다. 일반적으로 변수는 정수, 실수,..

breakcoding.tistory.com

이 글을 먼저 보고 오시는 것을 추천드린다.

 

그래서 아까 find() 함수를 사용할 때에도

if(m.find("홍길동") != m.end())

이런 식으로 사용했던 것이다. find() 함수는 iterator가 map을 순회하면서 find()의 매개변수로 들어온 그 key 값을 가진 원소가 있는지 찾는 것인데 m.end()까지 탐색해보고 없으면 멈추는 것이기 때문이다.

(선형 탐색을 하는 것처럼 말했지만 사실 map은 항상 정렬되어 있기 때문에 선형탐색이 아니라 이진탐색을 해서 find() 함수의 시간 복잡도는 로그 복잡도를 가진다.)

 

STL 라이브러리를 알아두면 굉장히 유용하므로 자주 쓰이는 함수, 순회하는 방법은 꼭 알아두자.

더 많은 함수를 알고 싶다면 http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

이 사이트에 직접 들어가서 보는 것을 추천한다.

728x90
728x90

만약에 입력으로 n을 입력받아 크기가 n인 배열을 만들고 싶다면 어떻게 해야 할까?

int n;
cin >> n;
int arr[n];

이렇게 하면 될까?

이렇게 선언하면 컴파일 에러가 나는 것을 볼 수 있다.

배열의 크기를 나타내는 [] 안에는 const로 선언된 상수 변수 또는 리터럴 상수만 들어갈 수 있다.

따라서 이 경우 포인터를 이용한 동적할당을 해야 한다.

이렇게 동적 할당을 해주면 에러가 안 나고 크기가 n인 int 배열이 잘 선언되었다.

 

하지만 배열의 크기를 미리 알지 못 할수도 있다.

입력이 계속 들어오고 입력이 0일 경우 입력을 그만 받는 프로그램이 있을 수도 있다.

 

이 경우 배열의 크기를 얼마로 잡아야 할까?

물론 int arr[100000]; 이렇게 크게 잡아놓고 입력을 받을 수도 있지만 메모리 공간을 너무 낭비한다는 단점이 있고 

입력으로 들어온 수의 개수를 초과하는 인덱스에 접근해도 IndexOutOfRange예외가 발생하지 않기 때문에 쓰레기값을 가지고 엉뚱한 계산을 할 수도 있다.

 

배열의 이러한 한계를 극복한 것이 vector 클래스이다. 벡터는 사용하다가 크기가 부족해도 크기를 늘릴 수도 있고 초기에 크기를 선언하지 않아도 되기 때문에 배열보다 훨씬 유연하다. 그리고 벡터는 배열과 다르게 클래스이기 때문에 유용한 함수들도 들어있어서 더욱 편하다.

http://www.cplusplus.com/reference/vector/vector/

 

vector - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

 

벡터의 선언 방법은 다음과 같다.

#include <vector>

일단 <vector> 헤더파일을 포함시켜주고

vector<int> v;

이렇게 선언한다. 그러면 v라는 빈 벡터가 생성된다.

벡터는 vector<T> 이렇게 만들어진 템플릿 클래스이기 때문에 벡터에 들어갈 타입을 <>이 사에에 적어줘야 한다.

 

이렇게 선언한 벡터 v에 1이라는 정수를 집어넣고 싶다면?

v.push_back(1);

이렇게 하면 된다.

 

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(7);
	v.push_back(15);
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
	return 0;
}

이 코드의 실행 결과는 다음과 같다.

벡터의 모든 원소를 for문을 돌리면서 출력하는데 v.size()를 사용했다.

v.size()를 하면 벡터에 있는 원소의 개수를 반환한다. 지금 이 코드의 경우 v.size()를 하면 3을 반환한다.

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v;
	v.push_back(1);
	v.push_back(7);
	v.push_back(15);
	cout << "벡터의 크기: " << v.size() << endl;
	return 0;
}

위 코드의 실행 결과

지금 우리가 벡터를 사용한 방법은 빈 벡터를 선언하고 push_back() 함수를 사용하여 값을 넣어주었다.

하지만 벡터의 크기를 처음부터 잡고 싶을 때엔 어떻게 해야 할까?

vector<int> v(3);

이렇게 선언하면 크기가 3인 벡터가 생성된다.

그럼 이렇게 크기가 3인 벡터를 선언하고 아까와 같이 push_back 함수를 통해 1, 7, 15를 벡터에 넣으면 어떻게 될까?

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v(3);
	v.push_back(1);
	v.push_back(7);
	v.push_back(15);
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
	return 0;
}

위 코드의 실행 결과

크기가 3인 벡터를 선언하고 push_back()으로 원소를 넣으면 이렇게 뒤에 원소가 추가되는 것을 볼 수 있다.

즉 push_back() 함수를 호출하면 반드시 벡터의 크기가 늘어난다는 것을 알 수 있다.

그리고 벡터의 크기를 정해서 선언하면 초기값 0으로 채워진 벡터가 생성된다는 것도 알 수 있다.

 

따라서 이렇게 일정 크기의 벡터를 선언하면 push_back()이 아니라

v[0] = 1;
v[1] = 7;
v[2] = 15;

이렇게 인덱스로 접근할 수 있다.

 

그러면 초기값을 0이 아니라 5로 주고 싶으면 어떻게 해야 할까?

vector<int> v(3, 5); //모든 원소가 5인 크기가 3인 벡터 생성.

이렇게 선언하면 초기값 5로 채워진 크기가 3인 벡터가 생성된다.

 

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v(3, 5);
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
	return 0;
}

위 코드의 실행 결과

이 코드를 실행하면 이렇게 5로 초기화되어 있는 것을 볼 수 있다.

 

또 내가 원하는 초기값이 다 다르다면 이렇게도 가능하다.

vector<int> v = {1, 7, 15};
#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v = { 1, 7, 15 };
	cout << "벡터의 크기: " << v.size() << endl;
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
	return 0;
}

위 코드의 실행 결과

 

벡터 클래스의 대표적인 생성자를 다시 살펴보자면

+vector() T 타입의 빈 벡터 생성
+vector(size: int) 숫자의 경우 0으로, bool의 경우 false로 초기화된 size 크기의 벡터 생성
+vector(size: int, defaultValue: T) defaultValue로 초기화된 size 크기의 벡터 생성

자주 쓰이는 대표적인 생성자는 이렇게 3개가 있다.

 

그러면 이제 vector 클래스의 유용한 함수들을 살펴보자.

+push_back(element: T): void 벡터에 element를 원소로 추가
+pop_back(): void 벡터의 마지막 원소를 삭제
+size(): unsigned int const 벡터의 크기(원소 개수) 반환
+at(index: int): T const index 위치에 있는 원소 반환. v[index]로 접근하는 것과 같다.
+empty(): bool const 벡터의 크기가 0인지 아닌지 반환
+clear(): void 벡터의 모든 원소를 삭제
+swap(v2: vector): void 벡터의 내용을 다른 벡터 v2와 교환. 같은 타입의 벡터만 가능
+erase(iterator position): iterator position 위치에 있는 원소를 삭제
#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v;
	cout << "벡터의 크기: " << v.size() << endl;
	cout << "벡터가 비어있나요? " << (v.empty() ? "true" : "false") << endl;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	cout << "벡터가 비어있나요? " << (v.empty() ? "true" : "false") << endl;
	cout << "벡터의 크기: " << v.size() << endl;
	cout << "0번째 원소: " << v[0] << endl; //이렇게 []로도 접근 가능하고
	cout << "3번째 원소: " << v.at(3) << endl; //이렇게 at() 함수로도 인덱스에 접근 가능
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << endl;
	v.pop_back();
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " "; //마지막 원소였던 5가 삭제된 것을 볼 수 있음
	}
	cout << endl;
	v.clear(); //벡터의 모든 원소 삭제
	cout << "벡터의 크기: " << v.size() << endl;
	cout << "벡터가 비어있나요? " << (v.empty() ? "true" : "false") << endl;
	cout << endl;
	return 0;
}

위 코드의 실행 결과

이 코드와 실행 결과만 보면 함수 사용법은 쉽게 알 수 있으리라 생각된다.

 

erase 함수 사용법은 조금 어려울 수도 있기 때문에 따로 빼서 설명하자면 예를 들어 i번째 원소를 벡터에서 삭제하고 싶다면 다음과 같이 쓰면 된다.

v.erase(v.begin() + i);

이렇게 하면 된다. erase 함수의 인자는 인덱스가 아니라 주소이기 때문에 저렇게 써야 한다. v.begin()은 벡터 v의 시작 주소이다.

전체 코드로 실습을 해보자면

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<int> v(10);
	for (int i = 0; i < 10; i++) {
		v[i] = i;
	}
	cout << "지우기 전:          ";
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << endl;
	v.erase(v.begin() + 5);
	cout << "5번 인덱스 지운 후: ";
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << endl;
	return 0;
}

위 코드의 실행 결과

v.erase(v.begin() + 5);로 5번 인덱스에 있는 원소가 삭제된 것을 볼 수 있다.

 

그러면 이차원 배열이 필요하면 어떻게 해야 할까?

벡터의 벡터로 이차원 배열을 구현할 수 있다. 5행 3열짜리 이차원 배열을 선언하고 싶다면 다음과 같이 하면 된다.

vector<vector<int>> v(5);
for (int i = 0; i < 5; i++) {
	v[i] = vector<int>(3);
}

코드를 보면 크기가 5인 벡터를 선언하는데 벡터의 원소로 벡터를 가지는 벡터이다.

즉, 빈 벡터 5개를 원소로 가지는 벡터이다.

그러고 나서 for문을 돌면서 각각의 빈 벡터를 크기가 3인 벡터로 할당해준다.

그러면 5x3짜리 이차원 배열과 같은 벡터가 만들어진다.

#include <iostream>
#include <vector>
using namespace std;

int main() {
	vector<vector<int>> v(5);
	for (int i = 0; i < 5; i++) {
		v[i] = vector<int>(3);
	}
	for (int i = 0; i < v.size(); i++) {
		for (int j = 0; j < v[i].size(); j++) {
			cout << v[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

위 코드의 실행 결과

이 코드를 줄여서 한 줄에 처리하고 싶다면

vector<vector<int>> w(5, vector<int>(3));

이렇게 하면 된다.

728x90
728x90

C++에서 우선순위 큐를 구현하려면 <queue> 라이브러리를 사용하면 된다. 따라서 #include <queue> 코드를 써줘야 한다.

 

priority_queue - C++ Reference

container_typeThe second template parameter (Container)Type of the underlying container

www.cplusplus.com

우선순위 큐를 선언하는 코드는 다음과 같다.

priority_queue<int, vector<int>> q;

이렇게 큐를 선언하면 숫자가 클 수록 우선순위가 높은 우선순위 큐가 만들어진다.

priority_queue<int, vector<int>> q;에서 이 안에서 int는 큐에 들어갈 데이터의 타입을 말한다.

priority_queue<int, vector<int>> q;에서 vector<int>는 데이터들이 들어갈 컨테이너이다. 실제로는 이 곳에 int 데이터들이 저장된다. 하지만 동작을 우선순위 큐 처럼 하게 하기 위해서 priorty_queue 타입으로 감싼 것이라고 보면 된다.

이렇게 선언하고 나서 우선순위 큐에 데이터를 집어넣으려면

q.push(3);

이렇게 push() 함수를 이용하면 된다.

 

q.push(4);
q.push(9);
q.push(1);
q.push(7);
q.push(10);
q.push(2);
q.push(3);

이렇게 7개의 int형 숫자 데이터를 우선순위 큐에 집어넣은 다음

 

while (!q.empty()) {
	cout << q.top() << endl;
	q.pop();
}

큐가 빌 때까지 pop 하면서 출력해보면 결과는 다음과 같다.

전체 코드는 아래와 같다.

#include <iostream>
#include <queue>
using namespace std;

int main() {
	priority_queue<int, vector<int>> q;
	q.push(4);
	q.push(9);
	q.push(1);
	q.push(7);
	q.push(10);
	q.push(2);
	q.push(3);
	while (!q.empty()) {
		cout << q.top() << endl;
		q.pop();
	}
	return 0;
}

 

우선순위 큐는 내부적으로 내림차순으로 정렬되어 있는 것을 볼 수 있다. 따라서 이렇게 pop 할 때마다 가장 큰 값을 반환한다. pop 할 때마다 가장 큰 값을 반환하는 것은 최대 힙(heap)의 성질과 같다.

따라서 priority_queue만으로 힙의 구현이 가능하다.

 

작은 데이터일수록 우선순위가 높은 우선순위 큐를 만들고 싶다면? 최소 힙을 만들고 싶다면?

priority_queue<int, vector<int>, greater<int>> q;

<> 사이에 vector<int> 뒤에 콤마를 찍고 세 번째 인자(?)로 greater<int>를 넣어주면 된다.

greater라는 정렬 기준을 추가해준 것이다. greater는 오름차순 정렬이라는 소리이다.

최소힙인데 왜 오름차순이지? 할 수도 있는데 잘 생각해보면 큐는 front에서 원소를 빼는데 front는 인덱스가 0이므로 맨 앞에 가장 작은 값이 와야 한다. 따라서 오름차순으로 정렬해야

값이 작을 수록 우선순위가 높은 우선순위 큐 또는 최소힙을 만들 수 있다.

#include <iostream>
#include <queue>
using namespace std;

int main() {
	priority_queue<int, vector<int>, greater<int>> q;
	q.push(4);
	q.push(9);
	q.push(1);
	q.push(7);
	q.push(10);
	q.push(2);
	q.push(3);
	while (!q.empty()) {
		cout << q.top() << endl;
		q.pop();
	}
	return 0;
}

이 코드를 실행하면

이렇게 작은 값부터 나오는 것을 볼 수 있다.

 

최대힙을 만들 때에는 아까처럼 정렬 기준을 정해주지 않아도 디폴트가 내림차순 정렬이고

priority_queue<int, vector<int>, less<int>> q;

이렇게 정렬기준으로 less를 넣어줄 수도 있다. less는 내림차순으로 정렬하라는 것이다.

728x90
728x90

벡터든 배열이든 정렬을 하려면 <algorithm> 라이브러리의 sort() 함수를 쓰면 된다. 따라서 <algorithm> 헤더파일을 포함해줘야 한다.
sort() 함수의 첫번째 두번째 매개변수는 iterator, 즉 포인터이다.

 

sort - C++ Reference

custom (2)template void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

www.cplusplus.com

배열의 경우 (배열의 크기가 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 함수의 인자로 넣어준다면 나만의 기준으로 정렬할 수 있다.



728x90
728x90

set은 중복없이 저장하는 자료구조이다. 집합이라고 생각하면 된다.

set을 사용하려면 #include <set>으로 <set> 헤더파일을 포함시켜야 한다.

set은 템플릿 클래스이기 때문에 set 객체를 선언할 때에는 set에 들어갈 원소들의 타입을 적어줘야 한다.

선언 방법은 다음과 같다.

set<int> s;

<> 안에는 set 원소의 타입을 적으면 된다.

double 타입의 원소를 가지는 set을 만들고 싶다면

set<double> s;

하면 된다.

그리고 이 set 객체에 원소를 집어넣고 싶으면

s.insert(3);

이렇게 하면 된다.

 

int arr[10] = { 1, 1, 2, 3, 4, 4, 5, 5, 6, 7 };

이렇게 1, 4, 5가 각각 2개씩 중복되서 있는 배열을 set에 넣어보자.

 

#include <iostream>
#include <set>
using namespace std;

int main() {
	int arr[10] = { 1, 1, 2, 3, 4, 4, 5, 5, 6, 7 };
	set<int> s;
	for (int i = 0; i < 10; i++) {
		s.insert(arr[i]);
	}
	cout << "set s의 크기: " << s.size() << endl;
	return 0;
}

이 코드의 실행 결과는 다음과 같다.

배열 arr의 원소 10개를 set에 집어넣었지만 set에는 7개의 원소 밖에 없다.

중복된 원소는 집어넣지 않기 때문이다.

 

set은 배열처럼 s[3] 이런 식으로 인덱스로 접근할 수 없다.

set을 순회하고 싶다면 다음과 같이 해야 한다.

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

iterator는 포인터이다. 포인터는 주소를 담고 있기 때문에 그 주소에 있는 을 출력해보려면 * 연산자로 접근해야 한다. *iter 이렇게 말이다.

s.begin()은 set의 시작 주소이다. for문 헤더에서 iterator를 s.begin()으로 초기화한다. 그러면 iter는 set의 시작 주소를 가리키게(저장하게) 된다.

그리고 for문으로 진입한다.

for문에 진입해서는 역참조연산자 *를 이용해서 set의 원소의 값을 출력한다.

그리고 iter++을 통해 그 주소를 4바이트만큼(int 타입이므로) 증가한다. 이 과정을 set의 마지막 주소에 가기 전까지 반복하면 된다.

 

이 코드의 실행 결과는 다음과 같다.

arr의 원소가 중복 없이 들어가 있는 것을 볼 수 있다.

 

set에 있는 원소를 삭제하고 싶으면

s.erase(1);

이렇게 해주면 된다.

 

이렇게 1을 삭제하고 다시 s를 출력해보면

#include <iostream>
#include <set>
using namespace std;

int main() {
	int arr[10] = { 1, 1, 2, 3, 4, 4, 5, 5, 6, 7 };
	set<int> s;
	for (int i = 0; i < 10; i++) {
		s.insert(arr[i]);
	}
	s.erase(1);
	for (set<int>::iterator iter = s.begin(); iter != s.end(); iter++) {
		cout << *iter << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

위 코드의 실행 결과

1이 사라진 것을 볼 수 있다.

 

set 객체가 비어있는지 확인하려면 s.empty()를 해보면 된다. s.size()가 0이면 비어있는 것이므로 1(true)을 리턴하고 아니라면 0(false)을 리턴한다.

728x90
728x90

코딩을 하다가 (x, y) 이런 순서쌍이 필요할 수도 있고 세 가지 정보를 묶어서 저장해야 할 수도 있다.

이럴 때 <tuple> 라이브러리를 사용하면 편리하다.

2-tuple은 주로 pair라고 부른다. 튜플에는 2-tuple(pair), 3-tuple, 4-tuple, 5-tuple, 6-tuple 등이 있는데 튜플이란 여러가지를 묶은 거라고 보면 된다.

 

튜플을 사용하려면 #include <tuple>을 써서 <tuple> 헤더파일을 포함해야 한다.

 

pair(2-tuple)를 만들고 싶으면 make_pair(a, b) 이런 식으로 사용하면 된다.

그리고 make_pair의 반환 타입은 pair<T1, T2> 템플릿(제네릭) 클래스이다.

2-tuple인 pair 생성 및 선언 방법은 다음과 같다.

pair<int, int> p = make_pair(1, 3);

두 개의 타입이 모두 같은 타입이 아니여도 된다.

pair<int, double> p = make_pair(1, 3.5);

템플릿이 T1, T2로 다르기 때문에 이렇게 서로 다른 타입의 데이터를 하나의 pair로 묶는 것도 가능하다.

 

그러면 p 객체에서 첫번째 요소인 1을 읽어오거나 두 번째 요소인 3의 값을 변경하고 싶을 때에는 어떻게 할까?

저장된 pair 객체에 저장된 요소 각각에 접근하고 싶을 때에는

p.first
p.second

이렇게 사용하면 된다.

 

#include <iostream>
#include <tuple>
using namespace std;

int main(){
	pair<int, int> p = make_pair(1, 3);
	cout << p.first << endl;
	cout << p.second << endl;
	return 0;
}

이 코드의 실행 결과는 다음과 같다.

정리하자면 pair를 만들 때에는 make_pair() 함수를 쓰면 되고 저장할 때에는 pair<T1, T2> 타입으로 저장한다.

저장한 요소에 접근할 때에는 pair 객체.first, pair 객체.second 이렇게 읽어올 수 있다.

 

그럼 3-tuple을 만드는 방법을 알아보자.

 

3-tuple 생성 및 선언 방법은 다음과 같다.

tuple<int, int, int> t = make_tuple(1, 2, 3);

pair와 마찬가지로 튜플도 3개의 타입이 모두 같지 않아도 된다.

 

그럼 저장한 1, 2, 3 요소 각각에 접근하고 싶을 때에는 어떻게 할까?

아까 pair는 first, second를 이용했지만 tuple은 get<>() 함수를 이용한다.

사용 방법은

get<접근할 요소의 인덱스>(튜플 객체)

이다.

#include <iostream>
#include <tuple>
using namespace std;

int main(){
	tuple<int, int, int> t = make_tuple(1, 2, 3);
	cout << get<0>(t) << endl;
	cout << get<1>(t) << endl;
	cout << get<2>(t) << endl;
	return 0;
}

이 코드를 실행하면

이런 결과가 나온다.

 

4-tuple, 5-tuple, 6-tuple도 마찬가지로 하면 된다. make_tuple() 함수를 이용해서 만들면 되고 get<>() 함수를 통해서 요소에 접근하는 것도 똑같다.

728x90
728x90

C언어와 C++은 포인터를 사용해서 다른 언어에 비해 강력한 기능을 수행할 수 있다.

주소에 직접 접근해서 데이터를 가져오는 등의 일을 할 수 있기 때문이다.

자바나 파이썬 등 기타 언어에서는 불가능한 일이다.

이렇게 강력한 기능을 가지고 있는 장점도 있는가 하면 단점도 있다.

포인터만 있으면 그 주소에 접근해서 그 주소에 있는 값을 읽어올 수 있다.

이것이 바로 강력한 기능이기도 하면서 치명적인 오류를 발생할 수 있는 부분이다.

잘못하면 중요한 데이터가 변경될 수도 있다.

 

예를 들어 이런 코드를 작성했다고 하자.

#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로 초기화해놨다면

널포인터라며 예외(런타임에러)가 발생한다. 이것은 치명적인 오류가 아니라 이것을 보고 '아 내가 널 포인터를 썼구나' 하고 코드를 짠 사람이 알게 되고 코드를 고칠 것이다.

따라서 처음에는 포인터를 NULL로 초기화 하는 것이 좋다.

728x90
728x90

포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다.

하지만 전혀 어렵지 않다는 것을 알게 되었고 다른 사람들에게도 포인터가 어렵지 않다는 것을 전파시키고 싶다.

 

일반적으로 변수는 정수, 실수, 문자 등 데이터 을 저장한다.

하지만 포인터 변수는 위와 같은 데이터가 저장되어 있는 메모리 주소를 저장하는 특별한 변수이다.

 

변수 이름 앞에 &를 붙이면 그 변수가 저장된 메모리의 주소이다.

#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는 주소를 출력하는 것을 볼 수 있다.

 

다음 글은 여기에서 이어집니다. ↓

breakcoding.tistory.com/294

 

[C++] 포인터와 배열 (포인터 심화)

지난 번에 올린 이 글에 이어지는 내용이다. (링크↓) https://breakcoding.tistory.com/81 [C++] 포인터 포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다. 하지만 전혀 어렵지 않다는 것을

breakcoding.tistory.com

 

728x90
728x90

c언어에서 쓰는 cstring은 문자열이 아니라 문자(char)의 배열인데 끝에 '\0'(널)로 막아줘야 하는 등 불편하고 문제가 많아서 C++에서는 string 클래스를 사용해서 문자열을 처리한다.

string은 int, double과 같은 기초타입(basic type)이 아니라 클래스이다.

따라서 string 클래스를 사용하고 싶으면 #include <string> 문장을 써서 <string> 헤더파일을 포함해야 한다.

string 클래스는 문자의 배열을 멤버변수로 가지며, 그것을 처리하는 함수들을 멤버함수로 가지고 있다.


아래의 두 문장은 결과적으로는 같지만 첫 번째 문장이 더 빠르다.

string s("Welcome to c++");
string s = "Welcome to c++";

첫 번째 문장은 바로 문자열 객체를 생성하는 것이지만

두 번째 문장이 동작하는 과정은 다음과 같다.

string은 클래스이기 때문에 생성자가 있다. 두 번째 문장의 경우 string s; 이렇게 선언한 것과 같으므로 string 클래스의 인자 없는 생성자가 호출된다. 따라서 일단 빈 문자열을 가진 객체가 생성된다.

그러고 나서 "Welcome to c++"라는 리터럴을 리터럴 저장소에서 복사해서 가져오는 것이다.

따라서 두 문장은 결과적으로는 같지만 내부적으로 동작되는 것은 다르다.

 

하지만 요즘은 내부적으로 optimizing(최적화) 시키기 때문에 시간 차이가 거의 없다고 한다.


문자열 추가

(참고로 UML 클래스 다이어그램에서 +는 public을, -는 private을 뜻한다.)

+append(s :string): string 문자열 s를 string 객체에 추가
+append(s: string, index: int, n: int): string 문자열 s를 index 위치에서 n개의 문자를 string에 추가
+append(s: string, n: int): string 문자열 s의 처음부터 n개의 문자를 string에 추가
+append(n: int, ch: char): string n개의 문자 ch를 string에 추가

①append(s :string)

string s1("Welcome");
s1.append(" to C++");
cout << s1 << endl; // Welcome to C++ 출력

②append(s: string, index: int, n: int)

string s2("Welcome");
s2.append(" to C and C++", 0, 5);
cout << s2 << endl; // Welcome to C 출력

여기서 헷갈리지 말아야 할 것은

s2.append(문자열, 0, 5");는 0번째 인덱스에서 시작해서 5개를 추가한다. ("0번째 인덱스부터 5번째 인덱스까지"가 아님)

 

③append(s: string, n: int)

string s3("Welcome");
s3.append(" to C and C++", 5);
cout << s3 << endl;

s3.append(문자열, 5);는 처음부터 5개를 s3에 추가하는 것이다. 즉 s3.append(문자열, 0, 5);와 같다.

 

④append(n: int, ch: char)

string s4("Welcome");
s4.append(4, 'G');
cout << s4 << endl;

이 함수에서 두 번째 인자는 char 타입이어야 한다. 'G'는 char 타입의 문자이지만 "G"는 string 클래스의 문자열이다.

작은따옴표는 char(문자), 큰 따옴표는 string(문자열)이라는 것 잊지 말자.


문자열 대입

+assign(s[]: char): string 문자 배열 또는 문자열 s를 string 객체에 대입
+assign(s: string, index: int, n: int): string 문자열 s의 index 위치에서 n개의 문자를 string 객체에 대입
+assign(s: string, n: int): string 문자열 s의 처음부터 n개의 문자를 string 객체에 대입
+assign(n: int, ch: char): string n개의 문자 ch를 string에 대입

①assign(s[]: char)

string s1("Welcome");
s1.assign("Dallas"); 
cout << s1 << endl; // Dallas 출력

②assign(s: string, index: int, n: int)

string s2("Welcome");
s2.assign("Dallas, Texas", 0, 5); 
cout << s2 << endl; // Dalla 출력

③assign(s: string, n: int)

string s3("Welcome");
s3.assign("Dallas, Texas", 5); 
cout << s3 << endl; // Dalla 출력

④assign(n: int, ch: char)

string s4("Welcome");
s4.assign(4, 'G'); 
cout << s4 << endl; // GGGG 출력

at, clear, erase, empty 함수

+at(index: int): char 문자열로부터 index 위치의 문자를 반환
+clear(): void 문자열의 모든 문자 제거
+erase(index: int, n: int): string 문자열의 index 위치에서 시작해서 n개의 문자 제거
+empty(): bool 문자열이 비어있으면 true 반환, 아니면 false 반환
string s1("Welcome");
cout << s1.at(3) << endl;         // c 출력
cout << s1.erase(2, 3) << endl ;  // Weme 출력
s1.clear();                	// s1은 빈 문자열
cout << s1.empty() << endl ;     // s1은 비어있으므로 1(true)을 출력한다.                                    

length, size, capacity 함수

+length(): int 문자열에서 문자의 개수를 반환
+size(): int length()와 같음
+capacity(): int 문자열에 할당된 저장 공간의 크기를 반환
+c_str(): char 문자열에 대한 c문자열을 반환
+data(): char c_str()과 같음
string s1("Welcome");
cout << s1.length() << endl;       // 길이는 7
cout << s1.size() <<endl;          // 크기는 7
cout << s1.capacity() << endl;     // 용량은 15

s1.erase(1, 2);                    //s1은 Wcome가 됨
cout << s1.length() << endl;       // 길이는 5
cout << s1.size() << endl;         // 크기는 5
cout << s1.capacity() << endl;     // 길이는 줄었지만 용량은 여전히 15

length()나 size()는 우리가 직접 글자 수를 세어보면 알 수 있지만 capacity()의 경우 직접 출력해보지 않으면 알 수 없다.

이 경우 운영체제가 15byte를 잡고 시작한 것이다. 규칙은 없고 OS가 여유있게 잡아놓고 나중에 필요하면 용량을 증가시킨다. 운영체제가 몇 바이트를 잡았는지는 문자열객체.capacity()로 직접 찍어봐야 알 수 있다.


문자열 비교 함수

+compare(s: string): int 문자열 객체와 s를 비교해서 정수(-1, 0, 1)를 반환한다.

두 개의 문자열 내용을 비교해야 할 때에는 compare() 함수를 사용한다.

두 개의 문자열을 비교하는데 왜 인자가 한 개일까? 

문자열 객체 자기 자신매개변수로 들어온 문자열을 비교한다.

string s1("Welcome");
string s2("Welcomg");

cout << s1.compare(s2) << endl; 	// 앞이 작으므로 -1 반환
cout << s2.compare(s1) << endl; 	// 앞이 크므로 1 반환
cout << s1.compare("Welcome") << endl; 	// 같으므로 0 반환

문자열을 비교하는 방법은 앞에서부터 한 글자씩 비교하는 것이다.

s1.compare(s2);의 경우를 예로 살펴보자.

일단 s1의 0번 인덱스에 해당하는 문자인 'W'와 s2의 0번 인덱스에 해당하는 문자 'W'를 비교한다. 같다.

그러면 s1의 1번 인덱스의 'e'와 s2의 1번 인덱스 'e'를 비교한다. 같다.

그러면 또 다음 인덱스인 2번 인덱스를 비교한다.

그렇게 하다가 6번 인덱스를 비교할 차례이다.

s1의 6번 인덱스인 'e'와 s2의 6번 인덱스인 'g'를 비교한다. 'e'의 아스키코드 값은 101, 'g'의 아스키코드 값은 103이다.

101 - 103은 음수이다. 이렇게 객체 자신(이 경우 s1)이 인자로 들어온 문자열(이 경우 s2)보다 작을 경우 -1을 반환한다.

 

s2.compare(s1); 처럼 객체 자신이 인자로 들어온 문자열보다 클 경우 1을 반환한다.

s1.compare("Welcome"); 처럼 문자열 객체 자신과 인자로 들어온 문자열이 일치하면 0을 반환한다.

 

만약 헷갈리고 외우기가 어렵다면 앞의 것에서 뒤의 것을 뺀다고 생각하면 된다.

앞의 것이 더 크면 1, 뒤의 것이 더 크면 -1, 같으면 0.


부분 문자열 구하기

+substr(index: int, n: int): string index 위치부터 n개의 문자열을 반환
+substr(index: int): string index 위치부터 끝까지의 문자열을 반환
string s1("Welcome");
cout << s1.substr(0, 1) << endl;  // W 출력(0위치부터 1개)
cout << s1.substr(5) << endl;    // me 출력(5위치부터 끝까지)
cout << s1.substr(3, 3) << endl; // com 출력(3위치부터 3개)

여기서 헷갈리지 말아야 할 것은 s1.substr(5);은 처음부터 5까지도 아니고, 처음부터 5개도 아니고 인덱스 5 위치부터 끝까지이다.

아까 위에서 append() 함수의 경우 s1.append(" to C and C++", 5);는 처음부터 5개이기 때문에 이것과 헷갈릴 수 있다.

append() 함수는 처음부터 5개, substr() 함수는 5부터 끝까지라는 것 꼭 기억하자.

또 하나 기억해야 할 것은 substr() 함수는 원본을 바꾸지 않는다는 것이다. 단지 부분문자열을 반환만 할 뿐이다.

s1.substr(0, 1)을 출력하면 W를 출력하겠지만 s1이 W로 바뀌는 것은 아니다. s1은 "Welcome" 그대로이다.


문자열 검색

+find(c: char): int 문자열에서 문자 c가 발견되는 최초 인덱스를 반환
+find(c: char, index: int): int index부터 찾아서 문자 c가 발견되는 최초 인덱스를 반환
+find(s: string): int 문자열에서 문자열 s가 발견되는 최초 인덱스를 반환
+find(s: string, index: int): int index부터 찾아서 문자열 s가 발견되는 최초 인덱스를 반환
//문자 검색
cout << s1.find('o') << endl;      // 4 출력(처음위치부터 찾음)
cout << s1.find('o', 6) << endl;   // 9 출력(인덱스 6 부터 찾음)

//문자열 검색
string s1("Welcome to HTML");
cout << s1.find("co") << endl;     // 3 출력(처음위치부터 찾음)

if (s1.find("co", 6) == string::npos)//찾지 못하면(인덱스 6부터 찾음) string::npos 반환
	cout << "co는 없습니다" << endl;

 

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++ 출력

문자열 연산자

[] 배열 첨자 연산자. 첨자 안의 인덱스에 해당하는 문자에 접근
= 한 문자열의 내용을 다른 문자열로 복사
+ 두 개의 문자열을 새로운 하나의 문자열로 연결
+= 하나의 문자열 내용을 다른 문자열에 추가
<< 문자열을 스트림에 삽입
>> 스트림으로부터 공백이나 NULL 문자에 의해 구분되는 문자열 추출
==, !=, <, <=, >, >= 문자열 비교를 위한 관계연산자
string s1="ABC";
string s2 = s1;
cout << s1[0];  // A 출력

string s3 = s1 + "DEFGH";  //s3는 ABCDEFGH
s1 += "ABC";  //s1은 ABCABC
cout << (s1<=s3) << endl ;  // 1(true) 출력

s1[0]은 s1.at(0)과 똑같고 s1 += "ABC"는 s1.append("ABC")와 똑같다.

+=라는 연산자를 그런 역할을 하도록 내부적으로 그렇게 정의해놓은 것이다.

그런데 +=보다는 append() 함수를 사용하는 것이 문자열을 더 정교하게 컨트롤할 수 있다.

물론 단순히 문자열을 하나의 문자열 뒤에 통째로 추가하고 싶을 때에는 +=와 append()나 똑같기 때문에 아무거나 사용해도 된다.

또 주의해야 할 것은 <= 연산자는 참일 때 1(true)를 리턴하지만 compare() 함수는 두 문자열이 같을 때 1을 리턴한다.

728x90

+ Recent posts