36-2-바.비교와 검색

두 문자열 객체의 상등, 대소를 비교할 때는 기본 타입과 마찬가지로 관계 연산자를 사용한다. 문자열을 비교할 때는 _Traits 객체가 지정한 규칙에 따라 문자 코드의 순서에 따라 비교하는데 string 클래스에 적용된 디폴트 _Traits가 ASCII 코드를 기준으로 하므로 A보다 B가 크고 B보다 C가 더 크다. 관계 연산자 외에 compare라는 멤버 함수로 비교할 수도 있는데 compare는 연산자에 비해 문자열의 일부만을 비교할 수 있다는 점이 다르다. 연산자는 항상 전체를 대상으로 하며 함수는 일부에 대해서도 동작하는 식이다.

관계 연산자와 compare 함수 모두 다양한 타입에 대해 오버로딩되어 있으므로 string 객체끼리 비교할 수도 있고 string 객체와 문자열 상수를 비교할 수도 있다. 연산자의 경우 좌우변의 순서는 중요하지 않으므로 순서에 상관없이 비교 가능하다. 즉 if (s == "mo")로 비교하나 if ("mo" == s)로 비교하나 똑같다는 얘기다. 다음은 string 객체끼리 비교하는 compare 함수이며 이 외에 const char *를 인수로 취해 문자열 상수와 비교하는 compare 함수도 정의되어 있다.

 

int compare(const string& str) const;

int compare(size_t pos, size_t num, const string& str) const;

int compare(size_t pos, size_t num, const string& str, size_t off, size_t count) const;

 

첫 번째 원형은 두 문자열 객체 전체를 비교하며 두 번째 원형은 호출 객체의 일부를 str 전체와 비교하며 세 번째 원형은 호출 객체 일부와 str의 일부를 비교한다. 비교한 결과는 정수로 리턴되는데 두 문자열이 완전히 일치하면 0을 리턴하며 호출한 객체가 더 작으면 음수를 리턴하고 호출한 객체가 더 크면 양수를 리턴한다. 표준 strcmp 함수의 리턴값과 의미가 같으므로 외우기도 쉽다. 다음은 관계 연산자와 compare 함수를 사용하여 문자열끼리 비교하는 예제이다.

 

: compare

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s1("aaa");

     string s2("bbb");

 

     cout << (s1 == s1 ? "같다":"다르다") << endl;

     cout << (s1 == s2 ? "같다":"다르다") << endl;

     cout << (s1 > s2 ? "크다":"안크다") << endl;

 

     string s3("1234567");

     string s4("1234999");

     cout << (s3.compare(s4)==0 ? "같다":"다르다") << endl;

     cout << (s3.compare(0,4,s4,0,4)==0 ? "같다":"다르다") << endl;

 

     string s5("hongkildong");

     cout << (s5 == "hongkildong" ? "같다":"다르다") << endl;

}

 

여러 가지 문자열 객체들을 비교해 봤는데 상수나 마찬가지인 문자열을 비교했으므로 결과는 뻔하다.

 

같다

다르다

안크다

다르다

같다

같다

 

s1끼리는 당연히 같고 s1과 s2는 다르며 대소를 비교하면 'b'의 문자 코드가 'a'보다 더 크므로 s2가 더 크다. 마치 정수나 실수를 비교하는 것처럼 문자열 객체의 상등, 대소 비교를 할 수 있어 코드가 아주 상식적이다. s3, s4를 compare 함수로 비교할 때 전체를 다 비교하면 다른 것으로 판단하지만 앞 부분의 4글자만 비교하면 같다는 결과가 나온다. 이처럼 문자열의 일부분만 비교하고 싶을 때는 관계 연산자 대신 compare 멤버 함수를 사용해야 한다.

문자열 객체끼리뿐만 아니라 문자열 상수와도 비교할 수 있다. s5의 문자열이 특정 문자열인지 알고 싶을 때 예제에서와 같이 비교하고 싶은 문자열 상수를 바로 우변에 써도 상관없다. 뿐만 아니라 "hongkildong" == s5처럼 문자열 상수를 좌변에 쓰고 string 객체를 우변에 써도 잘 동작하는데 이렇게 되는 이유는 == 연산자가 string 클래스의 멤버 함수로 정의되어 있지 않고 전역 연산자 함수로 정의되어 있으며 (string &, const char *)와 (const char *, string &) 버전이 중복되어 있기 때문이다.

다음은 string 객체에서 부분 문자열이나 특정 문자가 어디에 있는지를 찾는 검색 함수에 대해 알아보자. 여러 가지 다양한 검색 함수가 준비되어 있는데 가장 기본적인 검색 함수는 find이다. find만 해도 다양한 타입에 대해 오버로딩되어 있다.

 

size_t find(char ch, size_t off=0) const;

size_t find(const char* ptr, size_t off=0) const;

size_t find(const char* ptr, size_t off=0, size_t count) const;

size_t find(const string& str, size_t off=0) const;

 

string 객체의 off위치에서 문자, 문자열, 다른 string 객체를 찾아 그 첨자 위치를 리턴한다. 발견되지 않을 경우 -1로 정의되어 있는 string::npos를 리턴하므로 이 값과 상등 연산해 보면 검색 대상의 존재 여부를 알 수 있다.

 

: stringfind

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s1("string class find function");

     string s2("func");

 

     cout << "i:" << s1.find('i') << "번째" << endl;

     cout << "i:" << s1.find('i',10) << "번째" << endl;

     cout << "ass:" << s1.find("ass") << "번째" << endl;

     cout << "finding의 앞 4:" << s1.find("finding",0,4) << "번째" << endl;

     cout << "kiss:" << s1.find("kiss") << "번째" << endl;

     cout << s2 << ':' << s1.find(s2) << "번째" << endl;

}

 

테스트 문자열을 하나 선언하고 이 문자열에 문자와 부분 문자열의 위치를 검색해 보았다.

 

i:3번째

i:14번째

ass:9번째

finding의 앞 4:13번째

kiss:4294967295번째

func:18번째

 

i문자를 문자열의 처음부터 검색하면 "string" 단어의 3번째 위치에서 검색된다. 단일 문자를 검색할 때는 두 번째 인수 off로 검색 시작 위치를 지정할 수 있는데 10번째에서부터 검색하면 "find" 단어의 2번째 위치에서 검색되는데 이 위치는 전체 문자열의 14번째 위치에 해당된다. 검색을 시작할 위치에 따라 결과 위치가 달라지는데 이런 식으로 검색을 반복하면 해당 문자가 있는 모든 위치를 알 수 있다.

부분 문자열도 검색할 수 있는데 "ass"라는 문자열은 "class" 단어의 뒤쪽에 발견된다. 부분 문자열의 일부도 검색할 수 있는데 "finding"이라는 단어는 없지만 이 단어의 앞쪽 4글자만 검색하면 검색된다. 지정한 단어가 발견되지 않을 경우 string::npos가 리턴되는데 이 값이 리턴되었다는 것은 해당 문자열이 없다는 뜻이다. string 객체를 검색하는 것도 물론 가능하다.

find는 가장 기본적인 검색 함수이며 이외에 검색 방향과 포함 문자 검색, 비포함 문자 검색 등의 다양한 함수들이 다양한 원형으로 제공된다. 함수들의 수가 많으므로 원형은 따로 제시하지 않고 예제만으로 함수를 소개하기로 한다.

 

: stringfind2

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s1("starcraft");

     string s2("123abc456");

     string moum("aeiou");

     string num("0123456789");

 

     cout << "순방향 t:" << s1.find('t') << "번째" << endl;

     cout << "역방향 t:" << s1.rfind('t') << "번째" << endl;

     cout << "역방향 cra:" << s1.rfind("cra") << "번째" << endl;

     cout << "최초의 모음" << s1.find_first_of(moum) << "번째" << endl;

     cout << "최후의 모음" << s1.find_last_of(moum) << "번째" << endl;

     cout << "최초의 비숫자" << s2.find_first_not_of(num) << "번째" << endl;

     cout << "최후의 비숫자" << s2.find_last_not_of(num) << "번째" << endl;

}

 

string 클래스가 제공하는 검색 함수들을 전부 한 번씩 호출해 보았다.

 

순방향 t:1번째

역방향 t:8번째

역방향 cra:4번째

최초의 모음2번째

최후의 모음6번째

최초의 비숫자3번째

최후의 비숫자5번째

 

rfind는 역방향으로 검색한다. 같은 문자나 부분 문자열이 두 번 이상 포함되어 있을 경우 순방향으로 찾을 때와 역방향으로 찾을 때의 결과가 다르다. C표준 함수 중에도 순방향으로 문자를 검색하는 strchr 함수와 역방향으로 검색하는 strrchr 함수가 있는데 find와 rfind의 차이가 바로 이 두 함수의 차이와 동일하다. C 표준 함수에는 부분 문자열을 역방향으로 검색하는 함수가 정의되어 있지 않지만 string 클래스의 rfind 함수는 이런 검색을 할 수 있다.

함수명에 first, last가 들어가는 함수들은 C 표준 함수의 strpbrk 함수와 유사하거나 약간 변형한 것들이다. 인수로 주어진 문자열을 구성하는 문자 중 하나를 순방향, 역방향으로 찾거나 아니면 구성 문자가 아닌 최초의 문자를 검색한다. 이 함수들의 동작을 말로 설명하는 것은 상당히 어려운데 strpbrk 함수를 먼저 이해하면 이 함수들도 유사한 방식으로 이해할 수 있다. 최초의 모음이나 숫자 또는 특정 문자군에 속한 문자를 찾고 싶을 때 이 함수들이 유용하다.

보다시피 string 클래스에는 별별 희한한 검색 함수가 다 마련되어 있는데 원래 검색이란 옵션이 많은 동작이라 멤버 함수들의 종류도 많을 수밖에 없다. 그러나 이런 다양한 함수가 준비되어 있음에도 불구하고 대소문자를 무시하고 검색하는 함수가 없어서 아쉽다. 함수란 아무리 많아도 특수한 응용에 두루 사용하기에는 역시 부족한데 이런 함수들이 필요하다면 개발자가 재량껏 래퍼 함수를 만들어 사용하는 수밖에 없다.