40-1-다.연산자

벡터에는 상식적으로 필요하다고 생각되는 대부분의 연산자들이 정의되어 있어 간단한 동작은 연산자만으로도 처리할 수 있다. C++의 연산자 오버로딩 기능을 아주 적절히 잘 활용하고 있는데 벡터뿐만 아니라 STL 컨테이너들은 모두 비슷한 방식으로 연산자를 오버로딩한다.

대입

벡터끼리 대입할 때는 간단하게 = 연산자를 사용하면 된다. 대입을 받는 좌변 벡터는 우변 벡터의 크기만큼 자동으로 크기가 늘어날 것이며 우변의 모든 요소가 좌변으로 대입된다. 벡터의 요소가 객체인 경우 개별 객체의 대입 연산자를 호출하여 깊은 복사를 하므로 요소들도 완전한 사본으로 생성될 것이다. 우변의 모든 요소가 좌변으로 복사되어 두 벡터가 완전히 같아지며 메모리 관리, 요소 개수 관리 등의 모든 처리는 대입 연산자 내부에서 알아서 처리할 것이다. 좌변 벡터에 원래 들어 있던 값은 당연히 파괴된다.

대입 연산자는 두 벡터를 완전히 똑같이 만드는데 만약 일부 구간만 복사하고 싶다면 assign 멤버 함수를 사용한다. 항상 이런 식인데 연산자는 전체에 대한 처리를 하며 일부분에 대한 처리는 별도의 멤버 함수가 준비되어 있다. 연산자는 전달받을 수 있는 피연산자 수가 제한되어 있어 부분에 대한 처리를 할만큼 충분한 정보를 제공받을 수 없기 때문이다. assign은 다음 두 개의 버전이 제공된다.

 

void assign(size_type count, const Type& val);

void assign(InIt first, InIt last);

 

첫 번째 버전은 val값 count개를 반복적으로 복사한다. 벡터를 특정값으로 가득 채우고 싶을 때 이 버전을 사용한다. 두 번째 버전은 반복자 구간을 받아들이는데 다른 컨테이너의 일부 요소를 벡터에 대입한다. 템플릿 함수로 정의되어 있으므로 입력 반복자 조건만 만족하면 벡터가 아닌 컨테이너의 구간도 대입할 수 있다.

 

: vectorassign

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

 

template<typename C> void dump(const char *desc, C c) { cout.width(12);cout << left << desc << "==> ";

      copy(c.begin(),c.end(),ostream_iterator<typename C::value_type>(cout," ")); cout << endl; }

 

void main()

{

     int ari[]={1,2,3,4,5};

     vector<int> vi(&ari[0],&ari[5]);

 

     vector<int>vi2;

     vi2=vi;

     dump("vi2",vi2);

 

     vector<int>vi3;

     vi3.assign(vi.begin()+1,vi.end()-1);

     dump("vi3",vi3);

}

 

세 개의 벡터를 생성하는데 vi를 vi2에 대입했으므로 vi2는 vi의 모든 요소에 대한 사본을 가진다. vi3는 vi2의 요소 중 전후 하나씩을 빼고 가운데 세 개만을 대입받았다. 실행 결과는 다음과 같다.

 

vi2         ==> 1 2 3 4 5

vi3         ==> 2 3 4

교환

swap멤버 함수는 두 벡터의 요소들을 통째로 교환한다. 교환하고자하는 대상 벡터를 인수로 전달하기만 하면 호출한 벡터와 인수로 전달된 벡터가 교환된다.

 

void swap(vector& Right);

 

멤버 함수 외에 모든 컨테이너에 대해 쓸 수 있는 일반적인 swap 알고리즘도 있다. 벡터끼리 교환하고 싶을 때는 다음 두 가지 방법 중 하나를 사용한다.

 

v1.swap(v2);     // 멤버 함수

swap(v1,v2);     // 알고리즘 함수

 

일반적인 알고리즘이 있는데도 불구하고 벡터가 특별히 swap 멤버 함수를 제공하는 이유는 일반적인 알고리즘이 요소를 직접 교환하도록 되어 있는데 비해 벡터끼리 교환할 때는 단순히 포인터만 교환하면 훨씬 더 빠르기 때문이다. 벡터가 요소들을 멤버로 가지고 있는 것이 아니라 내부적으로는 요소의 시작 포인터만을 가지므로 이 포인터와 크기 정보 등만 교환하면 된다.

그러나 실제로 두 개의 swap 함수는 완전히 동등한데 왜냐하면 swap 알고리즘 함수가 벡터에 대해 부분 특수화되어 있기 때문이다. swap은 두 개의 컨테이너를 교환하되 벡터나 리스트 등 더 빠르게 교환할 수 있는 컨테이너에 대해서는 멤버 함수 버전을 호출하도록 되어 있다.

즉 전역 swap 함수는 단순히 중계만 할 뿐이며 일관된 방법으로 컨테이너를 교환하는 인터페이스를 제공하는 역할을 한다. 그래서 어떤 함수를 사용하나 사실상 속도차는 없는 편이다. 간단하게 예제를 만들어 보자.

 

: vectorswap

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

 

template<typename C> void dump(const char *desc, C c) { cout.width(12);cout << left << desc << "==> ";

      copy(c.begin(),c.end(),ostream_iterator<typename C::value_type>(cout," ")); cout << endl; }

 

void main()

{

     const char str[]="abcdefghijklmnopqrstuvwxyz";

     vector<char> vc1(&str[0],&str[5]);

     vector<char> vc2(&str[5],&str[19]);

     dump("before vc1",vc1);

     dump("before vc2",vc2);

     vc1.swap(vc2); 

     dump("after vc1",vc1);

     dump("after vc2",vc2);

}

 

두 개의 문자 벡터를 만들어 놓고 swap 멤버 함수로 교환했다.

 

before vc1  ==> a b c d e

before vc2  ==> f g h i j k l m n o p q r s

after vc1   ==> f g h i j k l m n o p q r s

after vc2   ==> a b c d e

 

어차피 크기 정보까지 같이 교환되므로 두 벡터의 크기가 달라도 상관없다. 다음과 같이 교환해도 효과는 마찬가지이다.

 

vc2.swap(vc1); 

swap(vc1,vc2);

 

누가 교환의 주체가 되느냐만 다를 뿐인데 교환이란 어차피 양쪽을 모두 변경하는 연산이므로 똑같은 의미이다. 전역 swap 알고리즘 함수로 벡터를 교환해도 결국은 swap 멤버 함수가 교환을 처리할 것이다. 그러나 두 벡터의 타입이 달라서는 안되는데 vector<int>와 vector<char>는 서로 대입될 수 없는 대상이므로 교환도 불가능하다.

비교

벡터끼리 비교할 때는 ==, != 상등 연산자와 <, >, <=, >= 비교 연산자를 사용한다. 상등 비교는 요소의 개수와 모든 요소의 값이 일치할 때 같은 것으로 판단한다. 벡터가 생성되어 있는 메모리 위치나 추가로 할당되어 있는 여유분은 벡터의 실제 내용이 아니므로 상등 비교의 대상이 아니다. 들어 있는 내용만 같다면 같은 벡터로 취급된다.

대소를 비교할 때는 대응되는 각 요소들을 일대일로 비교하다가 최초로 다른 요소가 발견되었을 때 두 요소의 대소를 비교한 결과를 리턴한다. 만약 한쪽 벡터의 길이가 더 짧아 먼저 끝을 만났다면 아직 끝나지 않은 벡터가 더 큰 것으로 판별한다. 이런 식으로 비교하는 것을 사전식 비교라고 하는데 상식과도 일치한다.

 

: vectorcompare

#include <iostream>

#include <vector>

using namespace std;

 

void main()

{

     const char *str="0123456789";

     vector<char> vc1(&str[0],&str[10]);

     vector<char> vc2;

     vector<char> vc3;

 

     vc2=vc1;

     puts(vc1==vc2 ? "같다":"다르다");

     puts(vc1==vc3 ? "같다":"다르다");

     vc2.pop_back();

     puts(vc1 > vc2 ? "크다":"크지 않다");

}

 

세 개의 벡터를 생성해 놓고 상등 및 대소 비교를 했다. vc2는 vc1을 대입받았으므로 당연히 같을 것이고 vc3는 빈 벡터이므로 vc2와는 다르다. vc2에서 끝 요소를 빼고 vc1과 비교하면 긴 벡터가 더 큰 것으로 판단한다. 중간의 요소 하나가 다르면 최초로 달라진 요소를 기준으로 대소를 판단할 것이다.

 

같다

다르다

크다

 

연산자는 벡터 전체를 비교하며 일부 구간만 비교하고 싶을 때는 다음 장에서 배울 equal, mismatch 알고리즘 함수를 사용한다. 이 함수들을 사용하면 벡터뿐만 아니라 임의의 컨테이너 구간끼리도 비교할 수 있다.

요소 참조

벡터의 임의 요소를 읽을 때는 [ ] 연산자를 사용하며 괄호안에 부호없는 정수로 첨자를 지정한다. 벡터는 임의 접근이 가능하므로 첨자 번호로 요소를 빠르게 참조할 수 있다. vi 벡터의 크기가 1000일 때 900번째 요소를 읽고 싶다면 vi[900]을 참조하면 된다. 이때 리턴되는 값은 레퍼런스이므로 vi[900]=1234; 처럼 대입식의 좌변에 놓아 요소값을 변경하는 것도 가능하다. 벡터가 배열을 흉내내므로 마치 배열인 것처럼 사용하면 된다.

[ ] 연산자와 비슷하게 동작하는 at 함수도 정의되어 있는데 인수로 첨자를 지정한다. [ ] 연산자는 첨자가 무조건 유효하다고 가정하는 반면 at 함수는 벡터의 크기를 점검하여 무효한 첨자일 경우 out_of_range 예외를 발생시킨다는 점이 다르다. 그래서 배열 범위를 벗어나는 실수를 막을 수 있다. [ ] 연산자와 at 함수 모두 상수, 비상수 버전이 각각 정의되어 있다.

 

const_reference at(size_type pos) const;

reference at(size_type pos);

 

at 함수로 예외를 처리하는 간단한 예제를 만들어 보자.

 

: indexat

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

 

void main()

{

     int ari[]={1,2,3,4,5};

     vector<int> vi(&ari[0],&ari[5]);

 

     try {

          //cout << vi[10] << endl;

          cout << vi.at(10) << endl;

     }

     catch(out_of_range e) {

          cout << "벡터의 범위를 벗어났습니다." << endl;

     }

}

 

크기 5의 벡터 vi를 선언하고 vi[10]과 vi.at[10]으로 범위 바깥의 10번째 요소를 읽어 보았다. vi[10]은 예외를 일으키지 않고 무조건 10 번째 값을 읽으므로 다운되거나 아니면 운이 좋다 하더라도 쓰레기값을 돌려줄 것이다. 그러나 at 함수는 예외를 일으키므로 try 블록안에 넣어두면 안전하게 예외를 처리할 수 있다.

at 함수가 예외를 처리하므로 안전성이 좀 더 높지만 액세스할 때마다 첨자 범위를 일일이 점검해야 하므로 속도는 느리다. 또한 예외 처리를 위해 반드시 try, catch 블록을 구성해야 하므로 번거롭기도 하다. 일정한 범위에 대해서만 루프를 돌 때는 굳이 at 함수를 쓸 필요없이 [ ] 연산자를 사용하는 것이 더 효율적이다. 사용자가 첨자 번호를 직접 입력한다거나 할 때만 at 함수와 예외 처리를 사용하는 것이 좋다. 벡터는 무엇보다 빠른 요소 참조가 장점인데 읽을 때마다 범위를 점검하면 이 장점이 사라질 것이다.

이 외에 벡터의 요소를 읽는 방법에는 반복자와 * 연산자를 사용하는 방법이 있다. 단, 반복자는 가리키고 있는 위치만 읽을 수 있으므로 읽고자 하는 곳으로 먼저 이동해야 한다. 벡터의 반복자는 +n 연산을 지원하므로 *(vi.begin()+n) 연산문으로 n번째 요소를 읽을 수 있으며 vi.end를 기준으로 뺄셈을 하면 끝에서부터 n번째 요소를 읽을 수도 있다.