19. 특수 연산자

19.2 특수 연산자

19.2.1 첨자 연산자 []

  • operator[] 함수는 클래스 개체에 대한 의미를 부여하는데 사용된다.

  • operator[] 함수의 두 번째 인자는 어떤 타입이든 가능하다. → 따라서 vector, 연관 배열 같은 것들도 정의할 수 있다.

  • operator[] () 는 비 static 멤버 함수여야 한다.

struct Assoc {
	vector<pair<string, int>> vec;   // {name, value} 쌍의 배열 
	
	const int& operator[] (const string&) const;
	int& operator[] (const string&);
}

// ... 

// s 를 찾는다. 그 값이 발견되면 참조자를 반환한다. 
// 그렇지 않을 경우 새로운 쌍 {s, 0} 을 만들고, 그 값에 대한 참조자를 반환한다. 
int& Assoc::operator[](const string& s) 
{
	for(auto : vec)
		if(s==x.first) return x.second; 
	vec.push_back({s,0});
	return vec.back().second;
}

int main() 
{
	Assoc values; 
	string buf; 
	while(cin>>buf) ++values[buf];
	for(auto x : values.vec) 
		cout << '{' << x.first << ',' << x.second << "}\\n";
}

19.2.2 함수 호출 ()

  • 함수 호출 표기 () 는 다른 연산자와 동일하게 오버로딩 될 수 있고, 아래와 같이 사용될 수 있다.

  • () 연산자의 가장 중요한 용도는 어떤 측면에서 함수처럼 동작하는 개체를 위한 통상적인 함수 호출 문법을 제공하는 것이다.

  • 이러한 개체를 이용하면 매개변수를 받아들이는 연산을 수행할 수 있다.

  • 많은 경우 함수 개체가 연산을 수행하는 데 필요한 데이터를 갖고 있을 수 있느냐가 중요하다.

  • 예를 들어 아래와 같이 저장된 값을 인자로 추가해주는 opearotr() () 를 가진 함수를 정의할 수 있다.

  • 아래 코드는 complex{2,3}vec의 모든 원소에 추가하고 , complex{z}lst 의 모든 원소에 추가 할 것이다.

    • for_each 함수의 3번째 인자는 opearotr() () 를 오버로딩 한 개체여야만 한다.

  • 람다 표현식은 기본적으로 함수 개체를 정의하기 위한 문법이기에, 아래와 같이 작성할 수도 있다.

    • 이 경우 람다 표현식은 함수 개체 Add 와 똑같은 것을 생성한다.

    • operator() () 는 비 static 멤버 함수여야 한다.

19.2.3 역참조

  • 역참조 연산자는 클래스 개체는 포인터가 쓰이는 방식과 매우 유사한 방식으로 클래스 X 의 멤버 변수에 접근하는데 사용될 수 있다.

  • 예를 들면 다음과 같다.

    • 개체 p 를 포인터 p.operator→() 로 변환하는 것은 가리켜지는 멤버 m 에 의존하지 않는다.

  • 하지만, 새로운 문법이 도입되지 않은 관계로, 뒤에 여전히 멤버 이름이 필요하다.

  • → 오버로딩은 주로 ‘스마트 포인터’ 를 만드는 데 유용하다.

  • 이후 내용은 강의 후 ..

19.2.4 증가와 감소 ++ --

  • ‘스마트 포인터’ 가 등장하자 사람들은 증가 연산자 ++ 과 감소 연산자 -- 를 자주 사용하게 됐다. → 이 연산자들은 기본 제공 타입에 대해 쓰이는 용도를 흉내 내기 위해서다. → 이는 통상적인 포인터 타입을 ‘스마트 포인터’ 로 대체하는 것이 목적인 경우에 필요하다.

  • 스마트 포인터는 런타임 오류 체크가 추가된다는 점만 제외하면 통상적인 포인터와 동일한 의미 구조를 갖는다.

  • 다음 코드는 p 가 범위를 벗어난다는 문제가 있다.

  • 개선된 아래 코드를 살펴보자

    • 여기서는 *XPtr<X> 클래스의 개체로 대체하려고 하는데, 이 개체는 실제로 X 를 가리키는 경우에만 역참조 될 수 있다.

    • 또한 p 가 배열 내에서 가리키고 증가와 감소 연산의 결과로 해당 배열에 포함된 개체가 나올 떄만 p 가 증가되고 감소될 수 있게 보장하려고 한다.

  • 증가, 감소 연산자는 전위형 및 후위형 연산자로 모두 사용 가능하다는 점에서 C++ 연산자 중에서도 특이하다.

  • 따라서 Ptr<T> 에 대해서 전위형 및 후위형의 증가, 감소 연산자를 정의해야 한다.

    • 후위 연산자에 int 인자는 해당 함수가 ++ 의 후위형 적용에 대해 호출된다는 점을 나타내기 위해 사용된다. → 실제 int 는 사용되지 않는다.

19.2.5 할당 및 할당 해제 new delete

  • 일반적으로 new delete 연산자를 오버로딩 하는 것은 권장되지 않는다.. → 복잡도가 높아진다..

19.2.6 사용자 정의 리터럴

  • C++ 는 다양한 기본 제공 타입에 대한 리터럴을 제공한다.

  • 추가적으로 우리는 사용자 정의 타입에 대한 리터럴과 기본 제공 타입에 대한 리터럴의 새로운 형식을 정의할 수 있다.

  • 정의하는 방법은 다음과 같다. → 리터럴 연산자의 이름은 operaotr”” 에 접미사가 뒤따르는 것이다.

    • 아래 두 개 연산자는 각각 접미사 i, s 를 정의하고,

    • constexpr 을 통해 컴파일 타임 평가를 이용한다.

  • 기본적인 계획은 리터럴일 수 있는 것을 구문 분석한 후에 컴파일러가 항상 접미사를 체크하는 것이다.

  • 접미사를 달아 사용자 정의 리터럴을 만들 수 있는 리터럴에는 네 종류가 있다.

    • 정수 리터럴

    • 부동소수점 리터럴

    • 문자열 리터럴

    • 문자 리터럴

19.3 문자열 클래스

  • 표준 라이브러리 std::string 에 비해 간소화 된 String 클래스를 구현해보자

19.3.1 필수 연산

  • String 클래스는 통상적인 생성자, 소멸자, 대입 연산의 집합을 제공한다.

19.3.2 문자에 대한 접근

  • 문자에 대한 접근 연산자의 설계는 어려운 주제이다. → 이상적으로 접근은 관용적 표기법 []을 이용해야 하고, 최대한 효율적이고, 범위를 체크해야 하기 때문이다. → 하지만 이런 속성을 전부 수용할 수 있는 방법은 존재하지 않는다…

  • 여기서는 표준 라이브러리를 따라서 관용적인 [] 연산자와 범위 체크 at() 연산을 제공하고자 한다.

  • [] 를 통상적인 용도로 사용하자는 것이 기본 구상이다.

    • 여기서는 at() 의 사용이 불필요한데, s 에 대해 0에서 s.size()-1 까지만 접근하기 때문이다.

  • 실수의 가능성이 보이는 곳에서는 at() 을 사용할 수 있다.

    • 실수 가능성이 있는 곳에서도 [] 을 사용할 수 있기 때문에, std::string 표준 라이브러리의 일부 [] 구현에서는 범위를 체크한다.

    • 하지만, 범위 체크에는 오버헤드가 발생한다는 점을 생각하고 상황에 따라서 구현이 필요하다.

19.3.3 표현

  • String 에 대한 표현은 세 가지 목표를 충족하게 선택됐다.

    • C 스타일 문자열을 String 으로 변환하기 쉬고, C 스타일 문자열로서 String 문자에 쉽게 접근할 수 있게

    • 힙 공간의 사용을 최소화하게

    • String 끝에 문자를 효율적으로 추가할 수 있게

  • 결과는 복잡도가 올라갔지만, 효율적으로 동작한다.

  • 아래 코드는 두 개의 문자열 표현을 통해서 **짧은 문자열 최적화 기법(short string optimization)**을 구현한다.

    • sz≤short_max 라면 문자들이 String 개체 내 ch 란 이름의 배열로 저장된다.

    • !(sz≤short_max) 라면 문자들이 힙 영역에 저장되고 확장을 위한 추가 공간을 할당할 수 있다.

  • 두 경우 모두 ptr 은 원소를 가리킨다.

    • 접근 함수는 어떤 표현이 쓰이고 있는지 검사할 필요가 없다.

    • 그저 ptr 만 사용되면 되는 것이다.

  • sz≤short_max 인 경우에만 배열 ch 를 사용하고, !(sz≤short_max) 일 때는 정수 space 를 사용하기 때문에 chspace 두 개 모두에 대한 공간을 할당하는 것은 낭비가 될 것이다. → 이런 낭비를 피하기 위해서 union 을 사용한다.

19.3.4 멤버 함수

  • 기본 생성자는 String 이 비어 있게 정의한다.

  • copy_from(), move_from() 이 있으면 생성자, 이동, 대입의 구현이 상당히 간단해진다.

    • 아래 코드에서는 공간 체크, 새로운 공간 할당, 등 .. 상당히 많은 일들이 벌어지고 있다.

    • 이런 코드의 복잡성을 생각하고 싶지 않다면 std::string 을 사용하자. → 라이브러리를 사용하는 이유가 바로 이것이다!

19.3.5 보조 함수

  • 그 외 입출력, 연산자 오버로딩 등.. 수 많은 함수 ..

19.4 프렌드

  • 통상적인 멤버 함수 선언은 논리적으로 구분되는 3가지 속성을 지정한다.

    • 함수는 클래스 선언의 비공개 부분을 접근할 수 있어야 한다.

    • 함수는 클래스의 유효 범위 내에 있어야 한다.

    • 함수는 개체에 대해 호출될 수 있어야 한다. (this 포인터를 가진다)

  • friend 로 선언된 함수는 멤버 함수와 똑같이 클래스의 구현에 접근이 혀용되지만, 그렇지 않을 경우에 해당 클래스와 무관하다.

  • friend 선언은 클래스 선언의 private 부분이나 public 부분 중 어느 쪽에도 놓일 수 있다. → 어느 곳에 선언되는 것은 중요하지 않다.

  • 다음과 같이 함수에 friend 선언을 할수도 있고, 클래스에 friend 선언을 할수도 있다.

Last updated