마지막 업데이트: 12.08.2018

메서드와 함께 연산자를 오버로드할 수도 있습니다. 예를 들어 다음 Counter 클래스가 있다고 가정해 보겠습니다.

클래스 카운터 ( public int Value ( get; set; ) )

이 클래스는 값이 Value 속성에 저장되는 일부 카운터를 나타냅니다.

그리고 Counter 클래스의 두 객체가 있다고 가정해 보겠습니다. 두 개의 카운터는 표준 비교 및 ​​더하기 작업을 사용하여 Value 속성을 기반으로 비교하거나 추가하려는 것입니다.

카운터 c1 = 새로운 카운터 ( 값 = 23 ); 카운터 c2 = 새로운 카운터( 값 = 45 ); 부울 결과 = c1 > c2; 카운터 c3 = c1 + c2;

그러나 현재 Counter 개체에 대해서는 비교 연산이나 더하기 연산을 모두 사용할 수 없습니다. 이러한 작업은 여러 기본 유형에서 사용할 수 있습니다. 예를 들어 기본적으로 숫자 값을 추가할 수 있지만 컴파일러는 클래스 및 구조와 같은 복잡한 유형의 개체를 추가하는 방법을 모릅니다. 그리고 이를 위해 필요한 연산자를 오버로드해야 합니다.

연산자 오버로딩은 우리가 연산자를 정의하고자 하는 객체의 클래스에서 특별한 메소드를 정의하는 것으로 구성됩니다.

public static return_type 연산자 연산자(매개변수)( )

오버로드된 연산자가 이 클래스의 모든 개체에 사용되므로 이 메서드에는 공용 정적 수정자가 있어야 합니다. 다음은 반환 유형의 이름입니다. 반환 유형은 검색하려는 개체의 유형을 나타냅니다. 예를 들어, 두 개의 Counter 객체를 추가함으로써 우리는 새로운 Counter 객체를 얻을 것으로 기대합니다. 그리고 둘을 비교한 결과, 조건식이 참인지 거짓인지를 나타내는 bool 유형의 객체를 얻고 싶습니다. 그러나 작업에 따라 반환 유형은 무엇이든 될 수 있습니다.

그런 다음 메서드 이름 대신 키워드 연산자와 연산자 자체가 있습니다. 그런 다음 매개변수가 괄호 안에 나열됩니다. 이항 연산자는 두 개의 매개변수를 사용하고 단항 연산자는 하나의 매개변수를 사용합니다. 그리고 어떤 경우에도 매개변수 중 하나는 연산자가 정의된 유형(클래스 또는 구조)을 나타내야 합니다.

예를 들어 Counter 클래스에 대해 여러 연산자를 오버로드해 보겠습니다.

클래스 Counter ( public int Value ( get; set; ) public static Counter operator +(Counter c1, Counter c2) ( return new Counter ( Value = c1.Value + c2.Value ); ) public static bool operator >(Counter c1, 카운터 c2) ( return c1.Value > c2.Value; ) public static bool 연산자<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

오버로드된 모든 연산자는 이진법이므로 두 개체에서 수행되므로 각 오버로드에 대해 두 개의 매개변수가 있습니다.

더하기 연산의 경우 Counter 클래스의 두 객체를 추가하고 싶기 때문에 연산자는 이 클래스의 두 객체를 받습니다. 그리고 덧셈의 결과로 새로운 Counter 객체를 얻고 싶기 때문에 이 클래스는 리턴 타입으로도 사용됩니다. 이 연산자의 모든 작업은 두 매개 변수의 Value 속성 값을 결합한 Value 속성을 사용하여 새 개체를 만드는 것입니다.

공용 정적 카운터 연산자 +(카운터 c1, 카운터 c2) ( 새 카운터 반환( 값 = c1.Value + c2.Value ); )

두 개의 비교 연산자도 재정의되었습니다. 이러한 비교 연산자 중 하나를 재정의하면 두 번째 비교 연산자도 재정의해야 합니다. 비교 연산자 자체는 Value 속성의 값을 비교하고 비교 결과에 따라 true 또는 false를 반환합니다.

이제 프로그램에서 오버로드된 연산자를 사용합니다.

Static void Main(string args) ( Counter c1 = new Counter ( Value = 23 ); Counter c2 = new Counter ( Value = 45 ); bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2;Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); )

연산자 정의는 본질적으로 메서드이기 때문에 이 메서드를 오버로드할 수도 있습니다. 즉, 다른 버전을 만들 수도 있습니다. 예를 들어 Counter 클래스에 다른 연산자를 추가해 보겠습니다.

공용 정적 int 연산자 +(카운터 c1, int val) ( return c1.Value + val; )

이 메서드는 Value 속성의 값과 일부 숫자를 더하여 합계를 반환합니다. 또한 이 연산자를 적용할 수도 있습니다.

카운터 c1 = 새로운 카운터 ( 값 = 23 ); 정수 d = c1 + 27; // 50 Console.WriteLine(d);

오버로딩으로 인해 매개변수를 통해 연산자에 전달되는 개체가 변경되어서는 안 됩니다. 예를 들어 Counter 클래스에 대해 증가 연산자를 정의할 수 있습니다.

공용 정적 카운터 연산자 ++(카운터 c1) ( c1.Value += 10; 반환 c1; )

연산자는 단항이므로 이 연산자가 정의된 클래스의 개체인 하나의 매개변수만 사용합니다. 그러나 이것은 연산자가 매개변수 값을 변경해서는 안 되므로 증분에 대한 잘못된 정의입니다.

증가 연산자의 더 정확한 오버로드는 다음과 같습니다.

공용 정적 카운터 연산자 ++(카운터 c1) ( 새 카운터 반환 ( 값 = c1.Value + 10 ); )

즉, Value 속성에 증가된 값이 포함된 새 개체가 반환됩니다.

동시에 하나의 구현이 두 경우 모두에서 작동하기 때문에 접두사 및 접미사 증가(및 감소)에 대해 별도의 연산자를 정의할 필요가 없습니다.

예를 들어 접두사 증가 작업을 사용합니다.

카운터 카운터 = new Counter() ( 값 = 10 ); Console.WriteLine($"(counter.값)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(counter.Value)"); // 이십

콘솔 출력:

이제 우리는 후위 증분을 사용합니다:

카운터 카운터 = new Counter() ( 값 = 10 ); Console.WriteLine($"(counter.값)"); // 10 Console.WriteLine($"((counter++).Value)"); // 10 Console.WriteLine($"(counter.Value)"); // 이십

콘솔 출력:

true 및 false 연산자를 재정의할 수 있다는 점도 주목할 가치가 있습니다. 예를 들어 Counter 클래스에서 정의해 보겠습니다.

클래스 Counter ( public int Value ( get; set; ) public static bool operator true(Counter c1) ( return c1.Value != 0; ) public static bool operator false(Counter c1) ( return c1.Value == 0; ) // 나머지 클래스 내용 )

이러한 연산자는 유형의 개체를 조건으로 사용하려는 경우 오버로드됩니다. 예를 들어:

카운터 카운터 = new Counter() ( 값 = 0 ); if (카운터) Console.WriteLine(true); 그렇지 않으면 Console.WriteLine(거짓);

연산자를 오버로드할 때 모든 연산자가 오버로드될 수 있는 것은 아닙니다. 특히 다음 연산자를 오버로드할 수 있습니다.

    단항 연산자 +, -, !, ~, ++, --

    이항 연산자 +, -, *, /, %

    비교 연산 ==, !=,<, >, <=, >=

    논리 연산자 &&, ||

    대입 연산자 +=, -=, *=, /=, %=

그리고 등호 연산자 = 또는 삼항 연산자 ?: 와 같이 오버로드될 수 없는 많은 연산자가 있으며 기타 여러 가지가 있습니다.

오버로드된 연산자의 전체 목록은 msdn 설명서에서 찾을 수 있습니다.

연산자를 오버로드할 때 연산자 우선 순위 또는 연결성을 변경할 수 없으며 새 연산자를 만들거나 .NET 기본 형식의 연산자 논리를 변경할 수 없습니다.

좋은 날!

글을 읽고 나서 이 글을 쓰고 싶다는 생각이 들게 된 이유는 글에 중요한 주제가 많이 공개되지 않았기 때문입니다.

기억해야 할 가장 중요한 점은 연산자 오버로딩은 함수를 호출하는 더 편리한 방법일 뿐이므로 연산자 오버로딩에 현혹되지 않는다는 것입니다. 코드 작성을 더 쉽게 만드는 경우에만 사용해야 합니다. 하지만 읽기에 부담이 될 정도는 아닙니다. 결국, 아시다시피 코드는 작성된 것보다 훨씬 더 자주 읽습니다. 기본 제공 유형과 함께 연산자를 오버로드하는 것은 절대 허용되지 않으며 사용자 정의 유형/클래스만 오버로드될 수 있음을 잊지 마십시오.

오버로드 구문

연산자 오버로딩 구문은 다음과 같은 함수를 정의하는 것과 매우 유사합니다. [이메일 보호됨], 여기서 @는 연산자의 식별자입니다(예: +, -,<<, >>). 고려하다 가장 간단한 예:
class Integer ( private: int value; public: Integer(int i): value(i) () const Integer operator+(const Integer& rv) const ( return (value + rv.value); ) );
이 경우 연산자는 클래스의 구성원으로 프레임되고 인수는 연산자의 오른쪽에 있는 값을 지정합니다. 일반적으로 연산자를 오버로드하는 두 가지 주요 방법이 있습니다. 클래스 친화적 전역 함수 또는 클래스 자체의 인라인 함수입니다. 어떤 연산자가 더 나은지 주제의 끝에서 고려할 것입니다.

대부분의 경우 연산자(조건부 제외)는 개체 또는 해당 인수가 참조하는 형식에 대한 참조를 반환합니다(형식이 다른 경우 연산자 평가 결과 해석 방법 결정).

단항 연산자 오버로딩

위에 정의된 Integer 클래스에 대해 단항 연산자를 오버로드하는 예를 고려하십시오. 동시에, 우리는 그것들을 friend 함수로 정의하고 감소 및 증가 연산자를 고려할 것입니다.
class Integer ( private: int value; public: Integer(int i): value(i) () //unary + friend const Integer& operator+(const Integer& i); //unary - friend const 정수 연산자-(const Integer& i) ; //접두사 증가 friend const Integer& operator++(Integer& i); //후위 증가 friend const 정수 operator++(Integer& i, int); //접두사 감소 friend const Integer& operator--(Integer& i); //후위 감소 friend const 정수 연산자--(Integer& i, int); ); // 단항 더하기는 아무 것도 하지 않습니다. const Integer& operator+(const Integer& i) ( return i.value; ) const Integer operator-(const Integer& i) ( return Integer(-i.value); ) // 접두사 버전은 증가 후 값을 반환합니다. const Integer& operator++(Integer& i) ( i.value++; return i; ) //후위 버전은 증분 전의 값을 반환합니다. const Integer operator++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //접두사 버전은 반환 감소 후의 값 const Integer& operator--(Integer& i) ( i.value--; return i; ) // 후위 버전은 감소하기 전의 값을 반환합니다. const Integer operator--(Integer& i, int) ( Integer oldValue(i.value) ); i .value--; oldValue 반환; )
이제 컴파일러가 감소 및 증가의 접두사와 후위 버전을 구별하는 방법을 알게 되었습니다. 그가 표현식 ++i를 보는 경우 operator++(a) 함수가 호출됩니다. i++가 보이면 operator++(a, int)가 호출됩니다. 즉, 오버로드된 operator++ 함수가 호출되며, 이것이 postfix 버전에서 dummy int 매개변수가 사용되는 것입니다.

이진 연산자

이항 연산자를 오버로드하는 구문을 고려하십시오. l-값을 반환하는 하나의 연산자를 오버로드해 보겠습니다. 조건 연산자그리고 새로운 가치를 생성하는 하나의 문장(우리는 그것들을 전역적으로 정의합니다):
class Integer ( private: int value; public: Integer(int i): value(i) () friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend 부울 연산자==(const 정수& 왼쪽, const 정수& 오른쪽); ); const Integer operator+(const Integer& 왼쪽, const Integer& right) ( return Integer(left.value + right.value); ) Integer& operator+=(Integer& left, const Integer& right) ( left.value += right.value; return 왼쪽; ) bool operator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
이러한 모든 예에서 연산자는 동일한 유형에 대해 오버로드되지만 필수는 아닙니다. 예를 들어 Integer 유형과 유사하게 정의된 Float 추가를 오버로드하는 것이 가능합니다.

인수 및 반환 값

보시다시피 예제는 다양한 방법함수에 인수를 전달하고 연산자 값을 반환합니다.
  • 예를 들어 단항 플러스의 경우 인수가 연산자에 의해 수정되지 않으면 상수에 대한 참조로 전달되어야 합니다. 일반적으로 이것은 거의 모든 사람에게 해당됩니다. 산술 연산자(덧셈, 뺄셈, 곱셈...)
  • 반환된 값의 유형은 연산자의 특성에 따라 다릅니다. 연산자가 새 값을 반환해야 하는 경우 새 개체를 만들어야 합니다(바이너리 플러스의 경우와 같이). 객체를 l-value로 변경하는 것을 방지하려면 해당 객체를 const로 반환해야 합니다.
  • 할당 연산자는 변경된 요소에 대한 참조를 반환해야 합니다. 또한 (x=y).f()와 같은 구성에서 할당 연산자를 사용하려면 변수 x에 대해 함수 f()가 호출되고 y를 할당한 후 참조를 반환하지 마십시오. 상수에 대한 참조를 반환합니다.
  • 논리 연산자는 최악의 경우 int를 반환하고 기껏해야 bool을 반환해야 합니다.

반환 값 최적화

새 객체를 만들고 함수에서 반환할 때 위에서 설명한 이진 더하기 연산자의 예와 같이 표기법을 사용해야 합니다.
반환 정수(왼쪽.값 + 오른쪽.값);
솔직히 말해서 C++11과 어떤 상황이 관련이 있는지 모르겠습니다. 아래의 모든 인수는 C++98에 유효합니다.
언뜻 보면 임시 객체를 생성하기 위한 구문처럼 보입니다. 즉, 위의 코드와 다음 코드 간에 차이가 없습니다.
정수 temp(left.value + right.value); 반환 온도;
그러나 실제로 이 경우 첫 번째 줄에서 생성자가 호출된 다음 개체를 복사하는 복사 생성자가 호출되고 스택이 해제되면 소멸자가 호출됩니다. 첫 번째 항목을 사용할 때 컴파일러는 처음에 복사해야 하는 메모리에 개체를 생성하여 복사 생성자와 소멸자에 대한 호출을 저장합니다.

특수 연산자

C++에는 특정 구문과 오버로딩 방법이 있는 연산자가 있습니다. 예를 들어, 인덱스 연산자 . 항상 클래스의 멤버로 정의되며, 인덱스된 객체가 배열로 동작하는 것이 목적이므로 참조를 반환해야 합니다.
쉼표 연산자
"특수" 연산자에는 쉼표 연산자도 포함됩니다. 옆에 쉼표가 있는 객체에 대해 호출됩니다(그러나 함수 인수 목록에서는 호출되지 않음). 이 연산자를 사용하는 의미 있는 예를 생각해 내는 것은 그리 쉬운 일이 아닙니다. 오버로딩에 대한 이전 기사에 대한 주석의 Habrauser .
포인터 역참조 연산자
이러한 연산자를 오버로딩하는 것은 스마트 포인터 클래스에 대해 정당화될 수 있습니다. 이 연산자는 반드시 클래스 함수로 정의되며 몇 가지 제한 사항이 적용됩니다. 이 연산자는 개체(또는 참조) 또는 개체에 액세스할 수 있도록 하는 포인터를 반환해야 합니다.
할당 연산자
할당 연산자는 "="의 왼쪽에 있는 객체와 떼려야 뗄 수 없는 관계에 있기 때문에 반드시 클래스 함수로 정의됩니다. 할당 연산자를 전역적으로 정의하면 "=" 연산자의 표준 동작을 재정의할 수 있습니다. 예시:
class Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( // 자체 할당 확인 if (this == &right) ( return *this; ) 값 = right.value; 반환 *this; ) );

보시다시피 함수의 시작 부분에서 자체 할당 확인이 이루어집니다. 일반적으로 이 경우 자체 할당은 무해하지만 상황이 항상 그렇게 간단하지는 않습니다. 예를 들어 개체가 크면 불필요한 복사를 하거나 포인터로 작업할 때 많은 시간을 할애할 수 있습니다.

오버로딩이 아닌 연산자
C++의 일부 연산자는 전혀 오버로드되지 않습니다. 분명히 이것은 보안상의 이유로 수행됩니다.
  • 클래스 멤버 선택 연산자 ".".
  • 클래스 멤버 역참조 연산자 ".*"에 대한 포인터
  • C++(Fortran에서와 같이) "**"에는 지수 연산자가 없습니다.
  • 연산자를 정의하는 것은 금지되어 있습니다(우선순위 문제가 있을 수 있음).
  • 연산자 우선 순위는 변경할 수 없습니다.
우리가 이미 알아냈듯이, 연산자에는 두 가지 방법이 있습니다. 클래스 함수 형태와 friend 전역 함수 형태입니다.
Rob Murray는 그의 책 C++ Strategies and Tactics에서 연산자 형식을 선택하기 위한 다음 지침을 확인했습니다.

왜 그런 겁니까? 첫째, 일부 운영자는 초기에 제한됩니다. 일반적으로 연산자를 정의하는 방법에 의미상 차이가 없다면 연결을 강조하기 위해 클래스 함수로 배열하는 것이 좋으며 또한 함수는 인라인(inline)이 됩니다. 또한 왼쪽 피연산자를 다른 클래스의 개체로 나타내야 하는 경우가 있습니다. 아마도 가장 눈에 띄는 예는 재정의<< и >> I/O 스트림용.

15장에서 우리는 두 종류의 특수 함수, 즉 오버로드된 연산자와 사용자 정의 변환을 살펴볼 것입니다. 내장 유형의 객체와 동일한 직관적인 방식으로 표현식에서 클래스 객체를 사용할 수 있습니다. 이 장에서는 먼저 오버로드된 연산자에 대한 일반적인 설계 개념을 간략하게 설명합니다. 그런 다음 특수 액세스 권한이 있는 클래스 친구의 개념을 소개하고 할당, 아래 첨자, 호출, 클래스 멤버 액세스 화살표, 증가 및 감소, 클래스 연산자에 특화된 일부 오버로드 연산자가 구현되는 방식에 초점을 맞춰 사용되는 이유를 논의합니다. 새로 만들고 삭제합니다. 이 장에서 다루는 또 다른 특수 함수 범주는 클래스 유형에 대한 표준 변환 집합인 멤버 변환 함수(변환기)입니다. 클래스 개체가 실제 함수 인수로 사용되거나 내장 또는 오버로드된 연산자의 피연산자로 사용될 때 컴파일러에서 암시적으로 적용합니다. 이 장은 객체를 인수, 클래스 멤버 함수 및 오버로드된 연산자로 전달하는 것을 고려하여 함수 오버로딩을 해결하기 위한 규칙에 대한 자세한 요약으로 끝납니다.

15.1. 연산자 오버로딩

우리는 이전 장에서 이미 연산자 오버로딩을 통해 프로그래머가 클래스 유형 피연산자에 대해 미리 정의된 연산자의 자체 버전(4장 참조)을 도입할 수 있음을 보여주었습니다. 예를 들어 섹션 3.15의 String 클래스에는 오버로드된 연산자가 많이 있습니다. 아래는 그 정의입니다.

#포함 클래스 문자열; istream& 연산자 >>(istream &, const 문자열 &); 스트림 및 연산자<<(ostream &, const String &); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String(const char* = 0); String(const String &); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=(const String &); String& operator=(const char *); // перегруженный оператор взятия индекса char& operator(int); // набор перегруженных операторов равенства // str1 == str2; bool operator==(const char *); bool operator==(const String &); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; };

String 클래스에는 세 가지 오버로드된 연산자 집합이 있습니다. 첫 번째는 할당 연산자 집합입니다.

먼저 복사 할당 연산자가 나옵니다. (14.7절에서 자세히 논의했습니다.) 다음 명령문은 객체에 C-문자열을 할당하는 것을 지원합니다. 유형 문자열:

문자열 이름; 이름="셜록"; // 연산자 사용 operator=(char *)

(복사 할당 이외의 할당 연산자는 섹션 15.3에서 설명합니다.)

두 번째 세트에는 인덱스를 사용하는 연산자가 하나만 있습니다.

// 오버로드된 인덱스 연산자 char& operator(int);

프로그램이 내장 유형의 객체 배열과 같은 방식으로 String 클래스의 객체를 인덱싱할 수 있습니다.

If (이름 != "S") cout<<"увы, что-то не так\n";

(이 연산자는 섹션 15.4에 자세히 설명되어 있습니다.)

세 번째 집합은 String 클래스의 개체에 대해 오버로드된 동등 연산자를 정의합니다. 프로그램은 이러한 두 객체가 같은지 또는 객체와 C-문자열이 같은지 여부를 테스트할 수 있습니다.

// 오버로드된 항등 연산자 세트 // str1 == str2; 부울 연산자==(const char *); 부울 연산자==(const 문자열 &);

오버로드된 연산자를 사용하면 클래스 유형의 개체를 4장에서 정의된 연산자와 함께 사용하고 내장 유형의 개체처럼 직관적으로 조작할 수 있습니다. 예를 들어, 두 개의 String 객체를 연결하는 작업을 정의하려는 경우 concat() 멤버 함수로 구현할 수 있습니다. 그러나 왜 concat()이 아니라 append()입니까? 우리가 선택한 이름은 논리적이고 기억하기 쉽지만 사용자는 여전히 우리가 함수라고 부르는 것을 잊을 수 있습니다. 오버로드된 연산자를 정의하면 이름을 더 쉽게 기억할 수 있습니다. 예를 들어 concat() 대신 새로운 연산 operator+=()를 호출합니다. 이러한 연산자는 다음과 같이 사용됩니다.

#include "String.h" int main() ( 문자열 name1 "Sherlock"; 문자열 name2 "Holmes"; name1 += " "; name1 += name2; if (! (name1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

오버로드된 연산자는 이름이 키워드 operator와 그 뒤에 오는 많은 C++ 사전 정의 연산자 중 하나로 구성된다는 점을 제외하고는 일반 멤버 함수와 마찬가지로 클래스 본문에 선언됩니다(표 15.1 참조). 다음은 String 클래스에서 operator+=()를 선언하는 방법입니다.

Class String ( public: // 오버로드된 연산자 세트 += String& operator+=(const String &); String& operator+=(const char *); // ... private: // ... );

다음과 같이 정의하십시오.

#포함 inline String& String::operator+=(const String &rhs) ( // rhs에 의해 참조된 문자열이 비어 있지 않은 경우 if (rhs._string) ( String tmp(*this); // 다음을 연결한 상태를 유지하기에 충분한 메모리를 할당합니다. string _size += rhs._size; delete _string; _string = new char[ _size + 1 ]; // 먼저 원래 문자열을 선택한 영역에 복사한 다음 // rhs가 참조하는 문자열을 추가합니다 strcpy(_string, tmp._string) ; strcpy(_string + tmp._size, rhs._string); ) return *this; ) inline String& String::operator+=(const char *s) ( // 포인터 s가 0이 아닌 경우 if (s) ( String tmp(*this) ); // 연결된 문자열을 저장하기에 충분한 메모리 영역을 할당합니다. _size += strlen(s); delete _string; _string = new char[ _size + 1 ]; // 먼저 소스 문자열을 할당된 영역에 복사합니다. // 그런 다음 s strcpy(_string, tmp._string); strcpy(_string + tmp._size, s); )가 참조하는 C 문자열의 끝에 추가합니다.

15.1.1. 클래스의 회원 및 비회원

String 클래스의 같음 연산자를 자세히 살펴보겠습니다. 첫 번째 연산자를 사용하면 두 개체의 동등성을 설정할 수 있고 두 번째 연산자는 개체와 C-문자열을 설정할 수 있습니다.

#include "String.h" int main() ( String flower; // 꽃 변수에 무엇인가 쓰기 if (flower == "lily") // 정답 // ... else if ("tulip" == flower ) // 오류 // ... )

main()에서 등호 연산자를 처음 사용할 때 String 클래스의 오버로드된 operator==(const char *)가 호출됩니다. 그러나 두 번째 if 문에서 컴파일러는 오류 메시지를 표시합니다. 무슨 일이야?

클래스의 멤버인 오버로드된 연산자는 왼쪽 피연산자가 해당 클래스의 개체인 경우에만 적용됩니다. 두 번째 경우 왼쪽 피연산자는 String 클래스에 속하지 않기 때문에 컴파일러는 왼쪽 피연산자가 C 문자열이 될 수 있고 오른쪽 피연산자가 String 개체가 될 수 있는 내장 연산자를 찾으려고 합니다. 물론 존재하지 않으므로 컴파일러는 오류를 말합니다.

그러나 클래스 생성자를 사용하여 C 문자열에서 클래스 String의 개체를 만들 수도 있습니다. 컴파일러가 암시적으로 이 변환을 수행하지 않는 이유는 다음과 같습니다.

If (String("tulip") == flower) //correct: 멤버 연산자가 호출됨

그 이유는 비효율성 때문입니다. 오버로드된 연산자는 두 피연산자가 같은 유형일 필요가 없습니다. 예를 들어, Text 클래스는 다음과 같은 항등 연산자를 정의합니다.

Class Text ( public: Text(const char * = 0); Text(const Text &); // 오버로드된 등호 연산자 세트 bool operator==(const char *) const; bool operator==(const String &) const; 부울 연산자==(const 텍스트 &) const; // ... );

main()의 표현식은 다음과 같이 다시 작성할 수 있습니다.

If (Text("tulip") == flower) // Text::operator==() 호출

따라서 비교에 적합한 같음 연산자를 찾기 위해 컴파일러는 왼쪽 피연산자를 일부 클래스 유형으로 캐스팅할 수 있는 생성자를 검색하여 모든 클래스 정의를 살펴봐야 합니다. 그런 다음 이러한 각 유형에 대해 연관된 오버로드된 같음 연산자를 모두 검사하여 비교를 수행할 수 있는 것이 있는지 확인해야 합니다. 그런 다음 컴파일러는 발견된 생성자와 등호 연산자(있는 경우)의 조합 중 오른쪽에 있는 피연산자와 가장 일치하는 조합을 결정해야 합니다! 컴파일러가 이러한 모든 작업을 수행해야 하는 경우 C ++ 프로그램의 번역 시간이 크게 증가합니다. 대신 컴파일러는 왼쪽 피연산자 클래스(및 19장에서 볼 기본 클래스)의 멤버로 정의된 오버로드된 연산자만 찾습니다.

그러나 클래스의 구성원이 아닌 오버로드된 연산자를 정의하는 것은 허용됩니다. 컴파일 오류를 일으킨 main() 행을 구문 분석할 때 이러한 명령문이 고려되었습니다. 따라서 C-문자열이 왼쪽에 있는 비교는 String 클래스의 구성원인 등호 연산자를 네임스페이스 범위에 선언된 등호 연산자로 대체하여 올바르게 만들 수 있습니다.

부울 연산자==(const 문자열 &, const 문자열 &); 부울 연산자==(const 문자열 &, const char *);

이러한 전역 오버로드된 연산자에는 멤버 연산자보다 매개 변수가 하나 더 있습니다. 연산자가 클래스의 멤버인 경우 this 포인터는 암시적으로 첫 번째 매개변수로 전달됩니다. 즉, 멤버 연산자의 경우 표현식

꽃 == "백합"

컴파일러에서 다음과 같이 다시 작성합니다.

flower.operator==("백합")

오버로드된 멤버 연산자의 정의에서 flower의 왼쪽 피연산자는 이것으로 참조할 수 있습니다. (this 포인터는 13.4절에서 도입되었습니다.) 전역 오버로드된 연산자의 경우 왼쪽 피연산자를 나타내는 매개변수를 명시적으로 지정해야 합니다.

그런 다음 표현

꽃 == "백합"

통화 교환원

부울 연산자==(const 문자열 &, const char *);

항등 연산자의 두 번째 사용 사례에 대해 어떤 연산자가 호출되는지는 분명하지 않습니다.

"튤립" == 꽃

우리는 이러한 오버로드된 연산자를 정의하지 않았습니다.

부울 연산자==(const char *, const String &);

그러나 이것은 선택 사항입니다. 오버로드된 연산자가 네임스페이스의 함수인 경우 첫 번째 매개변수와 두 번째 매개변수(왼쪽 및 오른쪽 피연산자) 모두 가능한 변환으로 간주됩니다. 컴파일러는 같음 연산자의 두 번째 사용을 다음과 같이 해석합니다.

연산자==(String("튤립"), 꽃);

다음 오버로드된 연산자를 호출하여 비교를 수행합니다. bool operator==(const String &, const String &);

그런데 두 번째 오버로드된 연산자를 제공한 이유는 다음과 같습니다. bool operator==(const String &, const char *);

C-string에서 String 클래스로의 유형 변환은 오른쪽 피연산자에도 적용할 수 있습니다. 두 개의 String 피연산자를 사용하는 네임스페이스에 오버로드된 연산자를 정의하기만 하면 main() 함수는 오류 없이 컴파일됩니다.

부울 연산자==(const 문자열 &, const 문자열 &);

이 문장만 제공할지 아니면 두 개를 더 제공할지 여부:

부울 연산자==(const char *, const String &); 부울 연산자==(const 문자열 &, const char *);

런타임에 C 문자열에서 문자열로 변환하는 비용이 얼마나 큰지, 즉 String 클래스를 사용하는 프로그램에서 추가 생성자 호출의 "비용"에 따라 달라집니다. 항등 연산자를 자주 사용하여 C 문자열과 개체를 비교하는 경우 세 가지 옵션을 모두 제공하는 것이 가장 좋습니다. (우리는 친구에 관한 섹션에서 효율성 문제로 돌아갈 것입니다.

생성자를 사용하여 클래스 유형으로 캐스팅하는 방법은 15.9절에서 더 자세히 다룰 것입니다. 섹션 15.10은 설명된 변환을 사용하여 함수 오버로딩을 해결하는 것을 다루고 섹션 15.12는 연산자 오버로딩을 해결하는 것을 다룹니다.)

그렇다면 연산자를 클래스의 구성원으로 만들지 네임스페이스의 구성원으로 만들지 결정하는 기준은 무엇입니까? 어떤 경우에는 프로그래머에게 선택의 여지가 없습니다.

  • 오버로드된 연산자가 클래스의 구성원이면 왼쪽 피연산자가 해당 클래스의 구성원인 경우에만 호출됩니다. 왼쪽 피연산자의 형식이 다른 경우 연산자는 네임스페이스의 구성원이어야 합니다.
  • 언어에서는 할당("="), 아래 첨자(""), 호출("()") 및 멤버 액세스("->") 연산자를 클래스의 멤버로 정의해야 합니다. 그렇지 않으면 컴파일 오류 메시지가 발행됩니다.
// 오류: 클래스의 멤버여야 합니다. operator(String &, int ix);

(할당 연산자는 섹션 15.3에서 더 자세히 논의되고, 섹션 15.4에서 인덱스 가져오기, 섹션 15.5에서 호출, 섹션 15.6에서 화살표 멤버 액세스 연산자)

그렇지 않으면 클래스 디자이너가 결정합니다. 같음 연산자와 같은 대칭 연산자는 피연산자가 클래스의 구성원이 될 수 있는 경우(String에서와 같이) 네임스페이스에서 가장 잘 정의됩니다.

이 하위 섹션을 끝내기 전에 네임스페이스의 String 클래스에 대한 동등 연산자를 정의해 보겠습니다.

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true ; ) 인라인 부울 연산자==(const String &str, const char *s) ( return strcmp(str.c_str(), s) ? false: true ; )

15.1.2. 오버로드된 연산자의 이름

미리 정의된 C++ 언어 연산자만 오버로드할 수 있습니다(표 15.1 참조).

표 15.1. 오버로드 가능한 연산자

+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <= >>= () -> ->* 새 새 삭제 삭제

클래스 디자이너는 오버로드된 다른 이름의 연산자를 선언할 수 없습니다. 예를 들어 지수를 위해 ** 연산자를 선언하려고 하면 컴파일러에서 오류 메시지를 생성합니다.

다음 네 가지 C++ 언어 연산자는 오버로드할 수 없습니다.

// 오버로드 불가 연산자:: .* . ?:

사전 정의된 연산자 할당은 기본 제공 유형에 대해 변경할 수 없습니다. 예를 들어, 오버플로에 대한 결과를 확인하기 위해 내장 정수 덧셈 연산자를 재정의하는 것은 허용되지 않습니다.

// 오류: 내장 덧셈 연산자를 재정의할 수 없습니다. int int operator+(int, int);

또한 두 개의 배열을 추가하기 위해 기본 제공 작업 세트에 operator+를 추가하는 것과 같이 기본 제공 데이터 유형에 대한 추가 연산자를 정의할 수 없습니다.

오버로드된 연산자는 클래스 또는 열거형 형식의 피연산자에만 정의되며, 하나 이상의 클래스 또는 열거형 형식 매개 변수(값 또는 참조로 전달됨)를 사용하여 클래스 또는 네임스페이스의 멤버로만 선언할 수 있습니다.

미리 정의된 연산자 우선 순위(섹션 4.13 참조)는 변경할 수 없습니다. 명령문에서 클래스 유형 및 연산자 구현에 관계없이

X == y + z;

operator+가 항상 먼저 실행되고 operator==가 뒤따릅니다. 그러나 괄호를 사용하여 순서를 변경할 수 있습니다.

연산자의 미리 정의된 arity도 보존되어야 합니다. 예를 들어, 단항 논리 연산자 NOT은 String 클래스의 두 객체에 대한 이항 연산자로 정의할 수 없습니다. 다음 구현은 올바르지 않으며 컴파일 오류가 발생합니다.

// 잘못된: ! 단항 부울 연산자입니다!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

기본 제공 유형의 경우 미리 정의된 네 가지 연산자("+", "-", "*" 및 "&")가 단항 또는 이진 연산자로 사용됩니다. 이러한 자질 중 하나에서 압도 될 수 있습니다.

operator()를 제외한 모든 오버로드된 연산자에는 잘못된 기본 인수가 있습니다.

15.1.3. 과부하 연산자의 개발

피연산자가 클래스 유형의 개체인 경우 할당 및 주소 연산자와 쉼표 연산자는 미리 정의된 의미를 갖습니다. 그러나 과부하가 걸릴 수도 있습니다. 다른 모든 연산자의 의미는 이러한 피연산자에 적용될 때 개발자가 명시적으로 지정해야 합니다. 제공할 연산자의 선택은 클래스의 예상 사용에 따라 다릅니다.

먼저 공개 인터페이스를 정의해야 합니다. 공용 멤버 함수 집합은 클래스가 사용자에게 노출해야 하는 작업을 기반으로 구성됩니다. 그런 다음 오버로드된 연산자로 구현되어야 하는 기능이 결정됩니다.

클래스의 공용 인터페이스를 정의한 후 작업과 연산자 간에 논리적 대응이 있는지 확인합니다.

  • isEmpty()는 LOGICAL NOT 연산자, operator!()가 됩니다.
  • isEqual()은 항등 연산자 operator==()가 됩니다.
  • copy()는 대입 연산자 operator=()가 됩니다.

모든 연산자에는 몇 가지 자연스러운 의미가 있습니다. 따라서 이진 +는 항상 더하기와 연관되며 클래스를 사용하여 유사한 작업에 매핑하는 것은 편리하고 간결한 표기법이 될 수 있습니다. 예를 들어, 행렬 유형의 경우 두 개의 행렬을 추가하는 것은 이진 플러스의 완벽하게 적절한 확장입니다.

연산자 오버로딩의 오용의 예는 operator+()를 빼기 연산으로 정의하는 것인데, 이는 무의미합니다. 직관적이지 않은 의미 체계는 위험합니다.

이러한 연산자는 여러 가지 다른 해석을 동등하게 잘 지원합니다. operator+()가 하는 일에 대한 흠잡을 데 없이 명확하고 근거가 있는 설명은 문자열 연결에 사용된다고 가정하는 String 클래스 사용자를 기쁘게 하지 않을 것입니다. 오버로드된 연산자의 의미가 명확하지 않은 경우 제공하지 않는 것이 좋습니다.

복합 연산자의 의미 체계와 기본 제공 유형에 대한 해당하는 단순 연산자 시퀀스(예: + 다음에 = 및 += 복합 연산자의 등가)는 클래스에 대해서도 명시적으로 유지되어야 합니다. 연결 및 멤버별 복사 작업을 지원하기 위해 String에 대해 operator+() 및 operator=()가 모두 정의되어 있다고 가정합니다.

문자열 s1("C"); 문자열 s2("++"); s1 = s1 + s2; // s1 == "C++"

그러나 이것은 복합 할당 연산자를 지원하기에 충분하지 않습니다.

S1 += s2;

예상되는 의미를 유지하도록 명시적으로 정의해야 합니다.

연습 15.1

다음 비교에서 오버로드된 연산자 operator==(const String&, const String&)를 호출하지 않는 이유는 다음과 같습니다.

"자갈" == "돌"

연습 15.2

이러한 비교에 사용할 수 있는 오버로드된 부등식 연산자를 작성하십시오.

문자열 != 문자열 문자열 != C-문자열 C-문자열 != 문자열

하나 이상의 명령문을 구현하기로 선택한 이유를 설명하십시오.

연습 15.3

오버로드될 수 있는 13장(섹션 13.3, 13.4 및 13.6)에서 구현된 Screen 클래스의 멤버 함수를 식별합니다.

연습 15.4

섹션 3.15에서 String 클래스에 대해 정의된 오버로드된 입력 및 출력 연산자가 멤버 함수가 아닌 전역 함수로 선언된 이유를 설명합니다.

운동 15.5

13장의 Screen 클래스에 대해 오버로드된 입력 및 출력 연산자를 구현합니다.

15.2. 친구들

네임스페이스 범위에서 정의된 String 클래스에 대해 오버로드된 동등 연산자를 다시 고려하십시오. 두 String 객체에 대한 동등 연산자는 다음과 같습니다.

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: 진실; )

이 정의를 멤버 함수와 동일한 연산자의 정의와 비교하십시오.

Bool String::operator==(const String &rhs) const ( if (_size != rhs._size) return false; return strcmp(_string, rhs._string) ? false: true; )

String 클래스의 private 멤버에 액세스하는 방법을 수정해야 했습니다. 새로운 항등 연산자는 전역 함수, 멤버 함수가 아니라 String 클래스의 개인 멤버에 액세스할 수 없습니다. size() 및 c_str() 멤버 함수는 String 개체의 크기와 기본 C 문자열 문자를 얻는 데 사용됩니다.

대체 구현은 전역 평등 연산자를 String 클래스의 친구로 선언하는 것입니다. 함수나 연산자가 이런 방식으로 선언되면 public이 아닌 멤버에 대한 액세스 권한이 부여됩니다.

(키워드 friend로 시작하는) friend 선언은 클래스 정의 내에서만 발생합니다. 친구는 친구 관계를 선언하는 클래스의 구성원이 아니므로 어느 섹션(공개, 비공개 또는 보호)에서 선언되는지는 중요하지 않습니다. 아래 예제에서는 이러한 모든 선언을 클래스 헤더 바로 뒤에 배치하기로 결정했습니다.

클래스 문자열(friend bool operator==(const String &, const String &); friend bool operator==(const char *, const String &); friend bool operator==(const String &, const char *), public: // ... 나머지 String 클래스);

이 세 줄에서 전역 범위에 속하는 세 개의 오버로드된 비교 연산자는 String 클래스의 친구로 선언되므로 해당 정의에서 이 클래스의 private 멤버에 직접 액세스할 수 있습니다.

// 친구 연산자는 String 클래스의 // 개인 멤버에 직접 액세스합니다. bool operator==(const String &str1, const String &str2) ( if (str1._size != str2._size) return false; return strcmp(str1._string, str2 . _string) ?false: true; ) 인라인 부울 operator==(const String &str, const char *s) ( return strcmp(str._string, s) ?false: true; ) // 등

이 경우 내장 함수 c_str() 및 size()가 효율적이고 여전히 캡슐화를 유지하므로 _size 및 _string 멤버에 대한 직접 액세스가 필요하지 않다고 주장할 수 있습니다. String 클래스에 대한 동등 연산자를 친구로 선언합니다. .

비멤버 연산자를 클래스의 친구로 만들어야 하는지 아니면 접근자 함수를 사용해야 하는지 어떻게 알 수 있습니까? 일반적으로 개발자는 클래스의 내부 표현에 접근할 수 있는 선언된 함수와 연산자의 수를 최소한으로 유지해야 합니다. 동일한 효율성을 제공하는 접근자 함수가 있는 경우 이를 선호해야 하므로 다른 함수에 대해 수행되는 것처럼 클래스 표현의 변경에서 네임스페이스의 연산자를 격리해야 합니다. 클래스 개발자가 일부 멤버에 대한 액세스 기능을 제공하지 않고 네임스페이스에 선언된 연산자가 이러한 멤버에 액세스해야 하는 경우 friend 메커니즘의 사용이 불가피합니다.

이 메커니즘의 가장 일반적인 용도는 클래스의 구성원이 아닌 오버로드된 연산자가 해당 개인 구성원에 액세스할 수 있도록 하는 것입니다. 왼쪽과 오른쪽 피연산자 사이의 대칭을 유지할 필요가 없다면 오버로드된 연산자는 전체 액세스 권한을 가진 멤버 함수가 됩니다.

친구 선언은 일반적으로 연산자와 관련하여 사용되지만 네임스페이스의 함수, 다른 클래스의 멤버 함수 또는 심지어 전체 수업이렇게 선언해야 합니다. 한 클래스가 두 번째 클래스의 친구로 선언되면 첫 번째 클래스의 모든 멤버 함수는 다른 클래스의 비공개 멤버에 액세스할 수 있습니다. 연산자가 아닌 함수의 예를 사용하여 이것을 고려합시다.

클래스는 무제한 액세스 권한을 부여하려는 여러 오버로드된 함수 각각을 친구로 선언해야 합니다.

extern ostream& storeOn(ostream &, 화면 &); extern BitMap& storeOn(BitMap &, 화면 &); // ... class Screen ( friend ostream& storeOn(ostream &, Screen &); friend BitMap& storeOn(BitMap &, Screen &); // ... );

함수가 두 개의 다른 클래스의 개체를 조작하고 비공개 멤버에 액세스해야 하는 경우 이러한 함수는 두 클래스의 친구로 선언되거나 한 클래스의 멤버와 두 번째 클래스의 친구로 만들 수 있습니다.

함수를 두 클래스의 친구로 선언하는 것은 다음과 같아야 합니다.

클래스 창; // 이것은 Screen 클래스의 선언일 뿐입니다. ( friend bool is_equal(Screen &, Window &); // ... ); class Window ( friend bool is_equal(Screen &, Window &); // ... );

함수를 한 클래스의 멤버로 만들고 두 번째 클래스의 친구로 만들기로 결정하면 선언은 다음과 같이 빌드됩니다.

클래스 창; class Screen ( // copy()는 Screen의 멤버입니다. class Screen& copy(Window &); // ... ); class Window ( // Screen::copy() 는 Window 클래스 친구입니다 Screen& Screen::copy(Window &); // ... ); Screen& Screen::copy(창 &) ( /* ... */ )

한 클래스의 멤버 함수는 컴파일러가 자체 클래스의 정의를 볼 때까지 다른 클래스의 친구로 선언될 수 없습니다. 항상 가능한 것은 아닙니다. Screen이 Window의 일부 멤버 함수를 친구로 선언해야 하고 Window도 같은 방식으로 Screen의 일부 멤버 함수를 선언해야 한다고 가정합니다. 이 경우 전체 Window 클래스가 Screen의 친구로 선언됩니다.

클래스 창; class Screen (친구 class Window; // ... );

Screen 클래스의 private 멤버는 이제 모든 Window 멤버 함수에서 액세스할 수 있습니다.

운동 15.6

연습 15.5에서 Screen 클래스에 대해 정의된 입력 및 출력 연산자를 친구로 구현하고 private 멤버에 직접 액세스하도록 정의를 수정합니다. 어떤 구현이 더 낫습니까? 이유를 설명해라.

15.3. 연산자 =

한 객체를 같은 클래스의 다른 객체에 할당하는 것은 복사 할당 연산자를 사용하여 수행됩니다. (이 특별한 경우는 섹션 14.7에서 논의되었습니다.)

클래스에 대해 다른 할당 연산자를 정의할 수 있습니다. 클래스의 객체에 이 클래스와 다른 유형의 값을 할당해야 하는 경우 유사한 매개변수를 사용하는 연산자를 정의할 수 있습니다. 예를 들어, C-string을 String 객체에 할당하는 것을 지원하려면:

Stringcar("볼크스"); 자동차 = "스튜드베이커";

const char* 유형의 매개변수를 사용하는 연산자를 제공합니다. 이 작업은 이미 클래스에서 선언되었습니다.

Class String ( public: // char*에 대한 할당 연산자 String& operator=(const char *); // ... private: int _size; char *string; );

이러한 연산자는 다음과 같이 구현됩니다. null 포인터가 String 개체에 할당되면 "비어 있음"이 됩니다. 그렇지 않으면 C 문자열의 복사본이 할당됩니다.

String& String::operator=(const char *sobj) ( // sobj는 null 포인터 if (! sobj) ( _size = 0; delete _string; _string = 0; ) else ( _size = strlen(sobj); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, sobj); ) return *this; )

문자열은 sobj가 가리키는 C 문자열의 복사본을 참조합니다. 왜 사본입니까? _string 멤버에 직접 sobj를 할당할 수 없기 때문에:

문자열 = sobj; // 오류: 유형 불일치

sobj는 const에 대한 포인터이므로 "비 const"에 대한 포인터에 할당할 수 없습니다(섹션 3.5 참조). 할당 연산자의 정의를 변경해 보겠습니다.

String& String::operator=(const *sobj) ( // ... )

이제 _string은 sobj로 주소가 지정된 C 문자열을 직접 참조합니다. 그러나 이것은 다른 문제를 야기합니다. C 문자열이 const char* 유형임을 상기하십시오. 매개변수를 const가 아닌 포인터로 정의하면 할당이 불가능합니다.

자동차 = "Studebaker"; // operator=(char *)에서는 유효하지 않습니다!

그래서 선택의 여지가 없습니다. C 문자열을 String 유형의 개체에 할당하려면 매개변수가 const char* 유형이어야 합니다.

_string에 저장하면 sobj가 처리하는 C 문자열에 대한 직접 참조가 다른 문제를 야기합니다. 우리는 sobj가 정확히 무엇을 가리키는지 모릅니다. 이것은 String 객체에 알려지지 않은 방식으로 수정된 문자 배열일 수 있습니다. 예를 들어:

문자 ia = ( "d", "a", "n", "c", "e", "r"); 문자열 트랩 = ia; // trap._string은 ia를 참조합니다. ia = "g"; // 하지만 우리는 이것을 필요로 하지 않습니다: // ia와 trap._string 둘 다 수정됩니다

trap._string이 ia를 직접 참조하는 경우 trap 개체는 독특한 동작을 보일 것입니다. String 클래스의 멤버 함수를 호출하지 않고도 해당 값이 변경될 수 있습니다. 그러므로 우리는 C-string 값의 복사본을 저장하기 위해 메모리 영역을 할당하는 것이 덜 위험하다고 믿습니다.

할당 연산자는 삭제를 사용합니다. _string 멤버는 힙에 있는 문자 배열에 대한 참조를 포함합니다. 누수를 방지하기 위해 새 문자열에 메모리가 할당되기 전에 이전 문자열에 할당된 메모리가 삭제로 해제됩니다. _string은 문자 배열을 다루기 때문에 배열 버전의 삭제를 사용해야 합니다(섹션 8.4 참조).

할당 연산자에 대한 마지막 참고 사항입니다. 반환 유형은 String 클래스에 대한 참조입니다. 링크를 왜? 사실 내장 유형의 경우 할당 연산자를 연결할 수 있습니다.

// 할당 연산자의 연결 int iobj, jobj; iobj = 작업j = 63;

그들은 오른쪽에서 왼쪽으로 연결됩니다. 이전 예에서 할당은 다음과 같이 수행됩니다.

iobj = (작업j = 63);

이는 String 클래스의 객체로 작업할 때도 편리합니다. 예를 들어 다음 구성이 지원됩니다.

문자열 버전, 명사; 동사 = 명사 = "카운트";

이 체인의 첫 번째 할당은 const char*에 대해 이전에 정의된 연산자를 호출합니다. 결과 형식은 String 클래스의 복사 할당 연산자에 대한 인수로 사용할 수 있는 형식이어야 합니다. 따라서 매개변수가 주어진 연산자형식이 const char *이고 String에 대한 참조는 여전히 반환됩니다.

할당 연산자가 오버로드되었습니다. 예를 들어 String 클래스에는 다음 세트가 있습니다.

// 오버로드된 할당 연산자 집합 String& operator=(const String &); 문자열& 연산자=(const char *);

String 개체에 할당할 수 있는 각 유형에 대해 별도의 할당 연산자가 존재할 수 있습니다. 그러나 이러한 모든 연산자는 클래스의 멤버 함수로 정의되어야 합니다.

15.4. 인덱스 테이크 연산자

인덱스 테이커 operator()는 개별 요소가 검색되는 컨테이너의 추상화를 나타내는 클래스에서 정의할 수 있습니다. 이러한 컨테이너의 예로는 String 클래스, 2장에서 소개한 IntArray 클래스 또는 C++ 표준 라이브러리에 정의된 벡터 클래스 템플릿이 있습니다. 인덱스 테이크 연산자는 클래스의 멤버 함수여야 합니다.

String 사용자는 _string 멤버의 개별 문자를 읽고 쓸 수 있어야 합니다. 이 클래스의 객체를 사용하는 다음 방법을 지원하고자 합니다.

문자열 항목("사치스러운"); 문자열 마이카피; (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

아래 첨자 연산자는 할당 연산자의 왼쪽이나 오른쪽에 나타날 수 있습니다. 왼쪽에 있으려면 인덱싱된 요소의 l-값을 반환해야 합니다. 이를 위해 참조를 반환합니다.

#포함 inine char& String::operator(int elem) const ( assert(elem >= 0 && elem< _size); return _string[ elem ]; }

다음 조각에서 색상 배열의 null 요소에는 "V" 문자가 할당됩니다.

Stringcolor("보라색"); 색상[ 0 ] = "V";

연산자 정의는 인덱스가 배열의 범위를 벗어났는지 확인합니다. 이를 위해 C 라이브러리 함수 assert()가 사용됩니다. elem의 값이 0보다 작거나 _string에서 참조하는 C-string의 길이보다 크다는 것을 나타내는 예외를 던질 수도 있습니다. (예외 발생 및 처리는 11장에서 논의되었습니다.)

15.5. 함수 호출 연산자

함수 호출 연산자는 클래스 유형의 개체에 대해 오버로드될 수 있습니다. (우리는 12.3절에서 함수 객체를 논의할 때 그것이 어떻게 사용되는지 이미 보았습니다.) 연산을 나타내는 클래스가 정의되면 해당 연산자는 그것을 호출하기 위해 오버로드됩니다. 예를 들어 int 유형의 절대값을 취하려면 absInt 클래스를 정의할 수 있습니다.

클래스 absInt ( public: int operator()(int val) ( int 결과 = val< 0 ? -val: val; return result; } };

오버로드된 operator()는 임의의 수의 매개 변수가 있는 멤버 함수로 선언되어야 합니다. 매개변수와 반환 값은 함수에 허용되는 모든 유형이 될 수 있습니다(섹션 7.2, 7.3 및 7.4 참조). operator()는 그것이 정의된 클래스의 객체에 인수 목록을 적용하여 호출됩니다. 이 장에서 설명한 일반화된 알고리즘 중 하나가 어떻게 사용되는지 살펴보겠습니다. 다음 예에서는 absInt에 정의된 연산을 ivec 벡터의 각 요소에 적용하기 위해 일반 transform() 알고리즘이 호출됩니다. 요소를 절대 값으로 대체합니다.

#포함 #포함 int main() ( int ia = ( -0, 1, -1, -2, 3, 5, -5, 8 ), 벡터 ivec(ia, ia+8); // 각 요소를 절대값으로 바꿉니다. transform(ivec.begin(), ivec.end(), ivec.begin(), absInt()); // ... )

transform()에 대한 첫 번째 및 두 번째 인수는 absInt 연산이 적용되는 요소의 범위를 제한합니다. 세 번째 것은 연산을 적용한 결과가 저장될 벡터의 시작 부분을 가리킵니다.

네 번째 인수는 기본 생성자를 사용하여 만든 absInt 클래스의 임시 개체입니다. main()에서 호출된 일반화된 transform() 알고리즘의 인스턴스화는 다음과 같습니다.

typedef 벡터 ::반복자 iter_type; // transform()의 인스턴스화 // 벡터 요소에 absInt 연산을 적용합니다. int iter_type transform(iter_type iter, iter_type last, iter_type result, absInt func) ( while (iter != last) *result++ = func(*iter++) ; // absInt::operator()가 호출됨 return iter; )

func는 int를 절대값으로 바꾸는 absInt 연산을 제공하는 클래스 객체입니다. absInt 클래스의 오버로드된 operator()를 호출하는 데 사용됩니다. *iter 인수는 절대값을 얻고자 하는 벡터의 요소를 가리키는 이 연산자에 전달됩니다.

15.6. 화살표 연산자

멤버 액세스를 허용하는 화살표 연산자는 클래스 개체에 대해 오버로드될 수 있습니다. 멤버 함수로 정의되고 포인터 의미 체계를 제공해야 합니다. 이 연산자의 가장 일반적인 용도는 내장된 것과 유사하게 동작하지만 몇 가지 추가 기능을 제공하는 "스마트 포인터"를 제공하는 클래스에서입니다.

Screen 객체에 대한 포인터를 나타내기 위해 클래스 유형을 정의하고 싶다고 가정해 봅시다(13장 참조):

Class ScreenPtr ( // ... private: Screen *ptr; );

ScreenPtr의 정의는 이 클래스의 객체가 Screen 객체를 가리키도록 보장되어야 합니다. 내장 포인터와 달리 null일 수 없습니다. 그런 다음 응용 프로그램은 ScreenPtr 유형의 개체가 Screen 개체를 가리키는지 여부를 확인하지 않고 사용할 수 있습니다. 이렇게 하려면 기본 생성자가 없는 ScreenPtr 클래스를 생성자로 정의해야 합니다(생성자는 섹션 14.2에서 자세히 설명했습니다).

클래스 ScreenPtr ( public: ScreenPtr(const Screen &s) : ptr(&s) ( ) // ... );

ScreenPtr 클래스의 개체에 대한 모든 정의는 초기화를 포함해야 합니다. ScreenPtr 개체가 참조할 Screen 클래스의 개체입니다.

화면Ptr p1; // 오류: ScreenPtr 클래스에는 기본 생성자가 없습니다. Screen myScreen(4, 4); ScreenPtr ps(myScreen); // 오른쪽

ScreenPtr 클래스가 내장 포인터처럼 작동하도록 하려면 오버로드된 연산자(역참조(*) 및 멤버 액세스를 위한 "화살표")를 정의해야 합니다.

// 포인터 동작을 지원하는 오버로드된 연산자 class ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); 멤버 액세스 연산자는 단항이므로 매개변수가 전달되지 않습니다. 표현식의 일부로 사용될 때 그 결과는 왼쪽 피연산자의 유형에만 의존합니다. 예를 들어 point->action(); 포인트 유형을 검토합니다. 클래스 유형에 대한 포인터인 경우 내장 멤버 액세스 연산자의 의미가 적용됩니다. 객체 또는 객체에 대한 참조인 경우 이 클래스에 오버로드된 액세스 연산자가 있는지 확인합니다. 오버로드된 화살표 연산자가 정의되면 점 개체에서 호출됩니다. 그렇지 않으면 점 연산자를 사용하여 개체 자체의 멤버를 참조해야 하므로(참조에 의한 경우 포함) 명령문이 유효하지 않습니다. 오버로드된 화살표 연산자는 클래스 유형에 대한 포인터나 정의된 클래스의 개체를 반환해야 합니다. 포인터가 반환되면 내장 화살표 연산자의 의미 체계가 포인터에 적용됩니다. 그렇지 않으면 포인터를 얻거나 오류가 감지될 때까지 프로세스가 재귀적으로 계속됩니다. 예를 들어 ScreenPtr 클래스의 ps 개체를 사용하여 Screen 멤버에 액세스하는 방법은 다음과 같습니다. ps->move(2, 3); "화살표" 연산자 왼쪽에 ScreenPtr 유형의 개체가 있으므로 이 클래스의 오버로드된 연산자가 사용되어 Screen 개체에 대한 포인터를 반환합니다. 그런 다음 내장 화살표 연산자가 검색된 값에 적용되어 move() 멤버 함수를 호출합니다. 다음은 작은 프로그램 ScreenPtr 클래스를 테스트합니다. ScreenPtr 유형의 객체는 Screen* 유형의 객체와 동일한 방식으로 사용됩니다. #include #포함 #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->키()<< ", " << ps->너비()<< ")\n\n"; for (int ix = 1; ix <= ps->키(); ++ix) ( for (int iy = 1; iy<= ps->너비(); ++iy) 커트<get(ix, iy); 쫓다<< "\n"; } } int main() { Screen sobj(2, 5); string init("HelloWorld"); ScreenPtr ps(sobj); // Установить содержимое экрана string::size_type initpos = 0; for (int ix = 1; ix <= ps->키(); ++ix) for (int iy = 1; iy<= ps->너비(); ++iy) ( ps->move(ix, iy); ps->set(init[ initpos++ ]); ) // 화면 내용 인쇄 printScreen(ps); 반환 0; )

물론 클래스 개체에 대한 포인터를 사용한 이러한 조작은 내장 포인터로 작업하는 것만큼 효율적이지 않습니다. 따라서 스마트 포인터는 사용의 복잡성을 정당화하기 위해 응용 프로그램에 중요한 추가 기능을 제공해야 합니다.

15.7. 증가 및 감소 연산자

이전 섹션에서 소개한 ScreenPtr 클래스의 구현을 계속 개발하면서 내장 포인터에 대해 지원되고 스마트 포인터가 갖고 싶어하는 두 가지 연산자를 더 살펴보겠습니다. 증가(++) 및 감소(--) . ScreenPtr 클래스를 사용하여 Screen 개체 배열의 요소를 참조하려면 몇 가지 추가 멤버를 추가해야 합니다.

먼저 0(ScreenPtr 개체가 단일 개체를 가리킴) 또는 ScreenPtr 개체가 가리키는 배열의 크기인 새 멤버 size를 정의합니다. 또한 주어진 배열의 시작 부분에서 오프셋을 기억하는 오프셋 멤버가 필요합니다.

Class ScreenPtr ( public: // ... private: int size; // 배열 크기: 유일한 객체가 int인 경우 0 offset; // 배열의 시작 부분에서 ptr의 오프셋 Screen *ptr; );

새로운 기능과 추가 멤버를 반영하도록 ScreenPtr 클래스 생성자를 수정합니다. 생성되는 객체가 배열을 가리키는 경우 클래스 사용자는 추가 인수를 생성자에 전달해야 합니다.

ScreenPtr 클래스 ( public: ScreenPtr(Screen &s , int arraySize = 0) : ptr(&s), 크기(arraySize), offset(0) ( ) private: int 크기; int 오프셋; Screen *ptr; );

이 인수는 배열의 크기를 지정합니다. 동일한 기능을 유지하기 위해 기본값 0을 제공하겠습니다. 따라서 생성자에 대한 두 번째 인수가 생략되면 크기 멤버는 0이 되므로 이러한 개체는 단일 Screen 개체를 가리킵니다. 새 ScreenPtr 클래스의 개체는 다음과 같이 정의할 수 있습니다.

화면 myScreen(4, 4); ScreenPtr pobj(myScreen); // 정답: 하나의 객체를 가리킴 const int arrSize = 10; 화면 *parray = 새 화면[arrSize]; ScreenPtr parr(*parray, arrSize); // 정답: 배열을 가리킴

이제 ScreenPtr에서 오버로드된 증가 및 감소 연산자를 정의할 준비가 되었습니다. 그러나 접두사와 접미사의 두 가지 유형이 있습니다. 다행히 두 옵션을 모두 정의할 수 있습니다. 접두사 연산자의 경우 선언에는 예기치 않은 내용이 포함되어 있지 않습니다.

Class ScreenPtr ( public: Screen& operator++(); Screen& operator--(); // ... );

이러한 연산자는 단항 연산자 함수로 정의됩니다. 예를 들어 다음과 같이 접두사 증가 연산자를 사용할 수 있습니다. const int arrSize = 10; 화면 *parray = 새 화면[arrSize]; ScreenPtr parr(*parray, arrSize); (int ix = 0; ix

이러한 오버로드된 연산자의 정의는 다음과 같습니다.

Screen& ScreenPtr::operator++() ( if (크기 == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= 크기 - 1) ( cerr<< "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset <= 0) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; }

접두사 연산자와 후위 연산자를 구별하기 위해 후자의 선언에는 int 유형의 추가 매개변수가 있습니다. 다음 조각은 ScreenPtr 클래스에 대한 증가 및 감소 연산자의 접두사 및 접미사 버전을 선언합니다.

Class ScreenPtr ( public: Screen& operator++(); // 접두사 연산자 Screen& operator--(); Screen& operator++(int); // 후위 연산자 Screen& operator--(int); // ... );

다음은 후위 연산자의 가능한 구현입니다.

Screen& ScreenPtr::operator++(int) ( if (크기 == 0) ( cerr<< "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset == size) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset == -1) { cerr <<"уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; }

두 번째 매개변수는 연산자 정의 내에서 사용되지 않기 때문에 이름을 지정할 필요가 없습니다. 컴파일러 자체는 무시할 수 있는 기본값을 대체합니다. 다음은 후위 연산자를 사용하는 예입니다.

Const int arrSize = 10; 화면 *parray = 새 화면[arrSize]; ScreenPtr parr(*parray, arrSize); (int ix = 0; ix

명시적으로 호출하는 경우 두 번째 정수 인수의 값을 전달해야 합니다. ScreenPtr 클래스의 경우 이 값은 무시되므로 무엇이든 될 수 있습니다.

parr.operator++(1024); // 접미사 연산자 호출++

오버로드된 증가 및 감소 연산자는 friend 함수로 선언할 수 있습니다. 이에 따라 ScreenPtr 클래스의 정의를 변경합니다.

Class ScreenPtr ( // 비멤버 선언 friend Screen& operator++(Screen &); // 접두사 연산자 friend Screen& operator--(Screen &); friend Screen& operator++(Screen &, int); // 후위 연산자 friend Screen& 연산자-- ( Screen &, int); public: // 멤버 정의 );

운동 15.7

ScreenPtr 클래스의 오버로드된 증가 및 감소 연산자에 대한 정의를 작성합니다. 이 연산자가 클래스의 친구로 선언되었다고 가정합니다.

운동 15.8

ScreenPtr은 Screen 클래스의 객체 배열에 대한 포인터를 나타내는 데 사용할 수 있습니다. 오버로드된 operator*() 및 operator >()(섹션 15.6 참조)를 수정하여 포인터가 어떤 상황에서도 배열 끝 앞이나 뒤에 요소를 지정하지 않도록 합니다. 팁: 이러한 연산자는 새 크기 및 오프셋 멤버를 사용해야 합니다.

15.8. 신규 및 삭제 연산자

기본적으로 힙에서 클래스 개체를 할당하고 클래스 개체가 차지하는 메모리를 해제하는 작업은 C++ 표준 라이브러리에 정의된 전역 new() 및 delete() 연산자를 사용하여 수행됩니다. (섹션 8.4에서 이러한 연산자에 대해 논의했습니다.) 그러나 클래스는 동일한 이름의 멤버 연산자를 제공하여 자체 메모리 관리 전략을 구현할 수도 있습니다. 클래스에 정의된 경우 전역 연산자 대신 호출되어 해당 클래스의 개체에 대한 메모리를 할당 및 할당 해제합니다.

Screen 클래스에서 new() 및 delete() 연산자를 정의해 보겠습니다.

new() 멤버 연산자는 void* 유형의 값을 반환하고 첫 번째 매개변수로 size_t 유형의 값을 취해야 합니다. 여기서 size_t는 시스템 헤더 파일에 정의된 typedef입니다. 다음은 그의 발표입니다.

new()를 사용하여 클래스 유형의 개체를 만들 때 컴파일러는 클래스에 이러한 연산자가 정의되어 있는지 확인합니다. 그렇다면 객체에 대한 메모리를 할당하기 위해 호출되는 것은 객체이고, 그렇지 않으면 전역 연산자 new()가 호출됩니다. 예를 들어, 다음 문장

화면 *ps = 새 화면;

힙에 Screen 객체를 생성하고 이 클래스에 new() 연산자가 있으므로 호출됩니다. 연산자의 size_t 매개변수는 화면 크기(바이트)로 자동 초기화됩니다.

클래스에 new()를 추가하거나 제거해도 사용자 코드에는 영향을 미치지 않습니다. new를 호출하는 것은 전역 연산자와 멤버 연산자 모두에 대해 동일하게 보입니다. Screen 클래스에 자체 new()가 없으면 호출은 올바른 상태로 유지되고 멤버 연산자 대신 전역 연산자만 호출됩니다.

전역 범위 확인 연산자를 사용하면 Screen 클래스에 자체 버전이 정의되어 있어도 전역 new()를 호출할 수 있습니다.

화면 *ps = ::새 화면;

삭제 피연산자가 클래스 유형의 개체에 대한 포인터인 경우 컴파일러는 해당 클래스에 delete() 연산자가 정의되어 있는지 확인합니다. 그렇다면 메모리를 해제하기 위해 호출되는 사람은 그 사람이고 그렇지 않으면 연산자의 전역 버전입니다. 다음 지시

삭제 ps;

ps가 가리키는 Screen 객체가 차지하는 메모리를 해제합니다. Screen에는 delete() 멤버 연산자가 있으므로 적용되는 연산자입니다. void* 유형의 연산자 매개변수는 자동으로 ps로 초기화됩니다. delete()를 클래스에 추가하거나 클래스에서 제거해도 사용자 코드에는 영향을 미치지 않습니다. 삭제 호출은 전역 연산자와 멤버 연산자 모두에 대해 동일하게 보입니다. Screen 클래스에 자체 delete() 연산자가 없으면 호출이 올바른 상태로 유지되고 멤버 연산자 대신 전역 연산자만 호출됩니다.

전역 범위 확인 연산자를 사용하면 Screen에 자체 버전이 정의되어 있어도 전역 delete()를 호출할 수 있습니다.

:: 삭제 ps;

일반적으로 사용되는 delete() 연산자는 메모리를 할당한 new() 연산자와 일치해야 합니다. 예를 들어, ps가 전역 new()에 의해 할당된 메모리 영역을 가리키는 경우 전역 delete()를 사용하여 해제해야 합니다.

클래스 유형에 대해 정의된 delete() 연산자는 하나가 아닌 두 개의 매개변수를 가질 수 있습니다. 첫 번째 매개변수는 여전히 void* 유형이어야 하고 두 번째 매개변수는 여전히 사전 정의된 size_t 유형이어야 합니다(헤더 파일을 포함하는 것을 잊지 마십시오).

Class Screen ( public: // // void operator delete(void *); void operator delete(void *, size_t); );

두 번째 매개변수가 있는 경우 컴파일러는 첫 번째 매개변수로 주소가 지정된 개체의 크기(바이트)와 동일한 값으로 이를 자동으로 초기화합니다. (이 옵션은 delete() 연산자가 파생 클래스에 상속될 수 있는 경우 클래스 계층 구조에서 중요합니다. 상속에 대한 자세한 내용은 이 장에서 설명합니다.)

Screen 클래스에서 new() 및 delete() 연산자의 구현을 더 자세히 살펴보겠습니다. 우리의 메모리 할당 전략은 freeStore 멤버가 가리키는 Screen 개체의 연결 목록을 기반으로 합니다. new() 멤버 연산자가 호출될 때마다 목록의 다음 개체가 반환됩니다. delete()가 호출되면 객체가 목록으로 반환됩니다. 새 객체가 생성될 때 freeStore로 지정된 목록이 비어 있으면 전역 연산자 new()가 호출되어 Screen 클래스의 screenChunk 객체를 저장할 수 있을 만큼 큰 메모리 블록을 얻습니다.

screenChunk와 freeStore는 모두 Screen에만 관심이 있으므로 비공개 멤버로 만들겠습니다. 또한 우리 클래스에서 생성된 모든 객체에 대해 이러한 멤버의 값은 동일해야 하므로 static으로 선언해야 합니다. Screen 객체의 연결 목록 구조를 지원하려면 세 번째 다음 멤버가 필요합니다.

Class Screen ( public: void *operator new(size_t); void operator delete(void *, size_t); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; );

다음은 Screen 클래스에 대한 new() 연산자의 가능한 구현입니다.

#include "Screen.h" #include // 정적 멤버는 // 헤더 파일이 아닌 프로그램 소스 파일에서 초기화됩니다. Screen *Screen::freeStore = 0; const int 화면::screenChunk = 24; void *Screen::operator new(size_t size) ( Screen *p; if (!freeStore) ( // 연결 리스트가 비어 있음: get new block // 전역 연산자가 호출됨 new size_t chunk = screenChunk * size; freeStore = p = 재해석_캐스트< Screen* >(새 문자[청크]); // 결과 블록을 목록에 포함 for (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1; p->다음 = 0; ) p = 무료 저장소; freeStore = freeStore->다음; 반환 p; ) 그리고 다음은 delete() 연산자의 구현입니다. void Screen::operator delete(void *p, size_t) ( // "삭제된" 객체를 다시 삽입합니다. // 자유 목록(static_cast< Screen* >(p))->다음 = freeStore; freeStore = static_cast< Screen* >(피); )

new() 연산자는 해당 delete() 없이 클래스에서 선언할 수 있습니다. 이 경우 객체는 같은 이름의 전역 연산자를 사용하여 해제됩니다. new() 없이 delete() 연산자를 선언하는 것도 허용됩니다. 객체는 같은 이름의 전역 연산자를 사용하여 생성됩니다. 그러나 이러한 연산자는 일반적으로 위의 예와 같이 동시에 구현됩니다. 클래스 디자이너는 일반적으로 두 가지가 모두 필요하기 때문입니다.

프로그래머가 명시적으로 선언하지 않더라도 클래스의 정적 멤버이며 이러한 멤버 함수에 대한 일반적인 제한 사항이 적용됩니다. this 포인터가 전달되지 않으므로 직접 액세스할 수만 있습니다. 정적 멤버. (섹션 13.5의 정적 멤버 함수에 대한 논의를 참조하십시오.) 이러한 연산자가 정적이 되는 이유는 클래스 객체가 생성되기 전(new()) 또는 소멸된 후에(delete()) 호출되기 때문입니다.

new() 연산자를 사용하여 메모리를 할당합니다. 예를 들면 다음과 같습니다.

화면 *ptr = 새로운 화면(10, 20);

// C++ 의사 코드 ptr = Screen::operator new(sizeof(Screen)); 화면::화면(ptr, 10, 20);

즉, 클래스에 정의된 new() 연산자를 먼저 호출하여 객체에 대한 메모리를 할당한 다음 해당 객체를 생성자로 초기화합니다. new()가 실패하면 bad_alloc 유형의 예외가 발생하고 생성자가 호출되지 않습니다.

예를 들어 delete() 연산자를 사용하여 메모리 해제:

삭제 포인트;

다음 명령을 순서대로 실행하는 것과 같습니다.

// C++ 의사코드 Screen::~Screen(ptr); 화면::연산자 삭제(ptr, sizeof(*ptr));

따라서 객체가 소멸되면 클래스 소멸자가 먼저 호출되고 클래스에 정의된 delete() 연산자가 호출되어 메모리를 해제합니다. ptr이 0이면 소멸자도 delete()도 호출되지 않습니다.

15.8.1. 신규 및 삭제 연산자

이전 하위 섹션에서 정의된 new() 연산자는 단일 개체에 대해 메모리가 할당된 경우에만 호출됩니다. 따라서 이 명령에서는 Screen 클래스의 new()가 호출됩니다.

// Screen::operator new() 호출 Screen *ps = new Screen(24, 80);

아래에서 전역 연산자 new()가 호출되어 Screen 유형의 객체 배열에 대해 힙에서 메모리를 할당합니다.

// Screen::operator new()를 Screen이라고 합니다. *psa = new Screen;

클래스는 배열과 함께 작동하도록 new() 및 delete() 연산자를 선언할 수도 있습니다.

new() 멤버 연산자는 void* 유형의 값을 반환하고 첫 번째 매개변수로 size_t 유형의 값을 취해야 합니다. 다음은 Screen에 대한 그의 선언입니다.

클래스 화면 ( public: void *operator new(size_t); // ... );

new를 사용하여 클래스 유형의 객체 배열을 만들 때 컴파일러는 new() 연산자가 클래스에 정의되어 있는지 확인합니다. 그렇다면 배열에 메모리를 할당하기 위해 호출되는 것은 배열이고, 그렇지 않으면 전역 new()가 호출됩니다. 다음 문은 엉덩이에 10개의 Screen 개체 배열을 만듭니다.

화면 *ps = 새 화면;

이 클래스에는 new() 연산자가 있으므로 메모리를 할당하기 위해 호출됩니다. size_t 매개변수는 10개의 Screen 객체를 보유하는 데 필요한 메모리 양(바이트)으로 자동 초기화됩니다.

클래스에 new() 멤버 연산자가 있더라도 프로그래머는 전역 new()를 호출하여 전역 범위 확인 연산자를 사용하여 배열을 만들 수 있습니다.

화면 *ps = ::새 화면;

클래스의 멤버인 delete() 연산자는 void 유형이어야 하며 void*를 첫 번째 매개변수로 취해야 합니다. Screen에 대한 그의 선언은 다음과 같습니다.

클래스 화면(공개: void 연산자 delete(void *); );

클래스 객체의 배열을 삭제하려면 다음과 같이 delete를 호출해야 합니다.

삭제 ps;

삭제 피연산자가 클래스 유형의 개체에 대한 포인터인 경우 컴파일러는 해당 클래스에 delete() 연산자가 정의되어 있는지 확인합니다. 그렇다면 메모리를 해제하도록 부름받은 사람이 그 사람이고 그렇지 않으면 그의 전역 버전입니다. void* 유형 매개변수는 배열이 위치한 메모리 영역의 시작 주소로 자동 초기화됩니다.

클래스에 delete() 멤버 연산자가 있더라도 프로그래머는 전역 범위 확인 연산자를 사용하여 전역 delete()를 호출할 수 있습니다.

:: 삭제 ps;

new() 또는 delete() 연산자를 클래스에 추가하거나 클래스에서 제거해도 사용자 코드에는 영향을 주지 않습니다. 전역 연산자와 멤버 연산자에 대한 호출은 모두 동일하게 보입니다.

배열이 생성되면 new()가 먼저 호출되어 필요한 메모리를 할당하고 각 요소는 기본 생성자로 초기화됩니다. 클래스에 생성자가 하나 이상 있지만 기본 생성자가 없으면 new() 연산자를 호출하면 오류로 간주됩니다. 이러한 방식으로 배열을 생성할 때 배열 요소 이니셜라이저 또는 클래스 생성자 인수를 지정하는 구문은 없습니다.

배열이 파괴되면 클래스 소멸자가 먼저 요소를 파괴하기 위해 호출된 다음 delete() 연산자가 호출되어 모든 메모리를 해제합니다. 이 작업을 수행할 때 올바른 구문을 사용하는 것이 중요합니다. 지시사항이 있는 경우

삭제 ps;

ps는 클래스 객체의 배열을 가리키고 대괄호가 없으면 메모리가 완전히 해제되지만 소멸자가 첫 번째 요소에 대해서만 호출됩니다.

delete() 멤버 연산자는 하나가 아니라 두 개의 매개변수를 가질 수 있으며 두 번째 매개변수는 size_t 유형입니다.

Class Screen ( public: // // void 연산자를 대체합니다. delete(void*); void operator delete(void*, size_t); );

두 번째 매개변수가 있는 경우 컴파일러는 배열에 할당된 메모리 양(바이트)과 동일한 값으로 이를 자동으로 초기화합니다.

15.8.2. 배치 연산자 new() 및 연산자 delete()

모든 선언에 다른 매개 변수 목록이 있는 경우 new() 멤버 연산자를 오버로드할 수 있습니다. 첫 번째 매개변수는 size_t 유형이어야 합니다.

화면 클래스 ( 공개: void *operator new(size_t); void *operator new(size_t, Screen *); // ... );

나머지 매개변수는 new를 호출할 때 제공된 배치 인수로 초기화됩니다.

무효 func(화면 *시작) ( // ... )

new 키워드 다음에 오고 괄호로 묶인 표현식 부분은 배치 인수를 나타냅니다. 위의 예는 두 개의 매개변수를 사용하는 new() 연산자를 호출합니다. 첫 번째는 바이트 단위의 Screen 클래스 크기로 자동 초기화되고 두 번째는 시작 배치 인수 값으로 초기화됩니다.

delete() 멤버 연산자를 오버로드할 수도 있습니다. 그러나 이러한 연산자는 삭제 식에서 호출되지 않습니다. 오버로드된 delete()는 new 연산자(오타가 아니라 실제로 new를 의미함)를 실행할 때 호출된 생성자가 예외를 throw하는 경우 컴파일러에 의해 암시적으로 호출됩니다. delete() 사용에 대해 자세히 살펴보겠습니다.

표현식을 평가할 때의 작업 순서

화면 *ps = 새(시작) 화면;

  1. 클래스에 정의된 new(size_t, Screen*) 연산자가 호출됩니다.
  2. Screen 클래스의 기본 생성자는 생성된 개체를 초기화하기 위해 호출됩니다.

ps 변수는 새 Screen 개체의 주소로 초기화됩니다.

클래스 연산자 new(size_t, Screen*)가 전역 new()를 사용하여 메모리를 할당한다고 가정합니다. 2단계에서 호출된 생성자가 예외를 throw하는 경우 개발자는 메모리가 해제되었는지 어떻게 확인할 수 있습니까? 메모리 누수로부터 사용자 코드를 보호하려면 이 상황에서만 호출되는 오버로드된 delete() 연산자를 제공해야 합니다.

클래스에 유형이 new() 매개변수의 유형과 일치하는 매개변수가 있는 오버로드된 연산자가 있는 경우 컴파일러는 이를 자동으로 호출하여 메모리를 해제합니다. 배치 연산자 new를 사용하여 다음 표현식이 있다고 가정합니다.

화면 *ps = 새(시작) 화면;

Screen 클래스의 기본 생성자가 예외를 throw하면 컴파일러는 Screen 범위에서 delete()를 찾습니다. 그러한 연산자를 찾으려면 해당 매개변수의 유형이 호출된 new()의 매개변수 유형과 일치해야 합니다. new()의 첫 번째 매개변수는 항상 size_t 유형이고 delete() 연산자는 항상 void*이므로 첫 번째 매개변수는 비교할 때 고려되지 않습니다. 컴파일러는 Screen 클래스에서 다음 형식의 delete() 연산자를 찾습니다.

무효 연산자 delete(void*, Screen*);

그러한 연산자가 발견되면 new()가 예외를 throw하는 경우 메모리를 해제하기 위해 호출됩니다. (그렇지 않으면 호출되지 않습니다.)

클래스 디자이너는 이 new() 연산자가 메모리 자체를 할당하는지 또는 이미 할당된 메모리를 사용하는지에 따라 일부 new()에 해당하는 delete()를 제공할지 여부를 결정합니다. 첫 번째 경우 생성자가 예외를 throw하는 경우 메모리 할당을 해제하려면 delete()를 포함해야 합니다. 그렇지 않으면 필요하지 않습니다.

배열에 대해 new() 할당 연산자와 delete() 연산자를 오버로드할 수도 있습니다.

클래스 Screen ( public: void *operator new(size_t); void *operator new(size_t, Screen*); void operator delete(void*, size_t); void operator delete(void*, Screen*); // ... );

new() 연산자는 배열을 할당하기 위해 new를 포함하는 표현식에 적절한 할당 인수가 있을 때 사용됩니다.

Void func(Screen *start) ( // Screen::operator new(size_t, Screen*) Screen 호출 *ps = new (start) Screen; // ... )

new 연산자를 실행하는 동안 생성자가 예외를 throw하면 해당 delete()가 자동으로 호출됩니다.

운동 15.9

다음 초기화 중 잘못된 것을 설명하십시오.

클래스 iStack ( public: iStack(int capacity) : _stack(capacity), _top(0) () // ... private: int _top; vatcor< int>_스택; ); (a) iStack *ps = 새로운 iStack(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = 새로운 iStack[ 100 ];

운동 15.10

new 및 delete를 포함하는 다음 표현식에서는 어떻게 됩니까?

클래스 연습( 공개: Exercise(); ~Exercise(); ); 운동 *pe = 새로운 운동; 삭제 PS;

전역 연산자 new() 및 delete()가 호출되도록 이러한 표현식을 수정합니다.

운동 15.11

클래스 디자이너가 delete() 연산자를 제공해야 하는 이유를 설명하십시오.

15.9. 사용자 정의 전환

우리는 이미 타입 변환이 내장 타입의 피연산자에 적용되는 방법을 보았습니다: 섹션 4.14에서 이 문제는 내장 연산자의 피연산자의 예를 사용하여 논의되었고 섹션 9.3에서는 함수를 호출하여 형식 매개변수 유형으로 변환합니다. 이 관점에서 다음 여섯 가지 추가 작업을 고려하십시오.

차치; 짧은 sh;, int ival; /* 연산당 하나의 피연산자 * 유형 변환이 필요합니다. */ ch + ival; 이발 + ch; ch+sh; 채널 + 채널; 이발 + 쉬; 쉬 + 아이발;

피연산자 ch 및 sh는 int 유형으로 확장됩니다. 작업을 수행할 때 int 유형의 두 값이 추가됩니다. 유형 확장은 컴파일러에 의해 암시적으로 수행되며 사용자에게 투명합니다.

이 섹션에서는 개발자가 클래스 유형의 개체에 대한 사용자 지정 변환을 정의하는 방법을 살펴보겠습니다. 이러한 사용자 정의 변환은 필요에 따라 컴파일러에서도 자동으로 호출됩니다. 왜 필요한지 보여주기 위해 섹션 10.9에서 소개된 SmallInt 클래스를 다시 살펴보겠습니다.

SmallInt를 사용하면 unsigned char와 동일한 범위의 값을 저장할 수 있는 개체를 정의할 수 있습니다. 0에서 255까지이며 범위를 벗어난 오류를 포착합니다. 다른 모든 측면에서 이 클래스는 unsigned char와 똑같이 동작합니다.

SmallInt 개체를 같은 클래스의 다른 개체나 내장 유형의 값에 더하거나 뺄 수 있도록 6가지 연산자 함수를 구현합니다.

SmallInt 클래스(친구 연산자+(const SmallInt &, int), 친구 연산자-(const SmallInt &, int), 친구 연산자-(int, const SmallInt &), 친구 연산자+(int, const SmallInt &), 공용: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operator-(const SmallInt &); // ... private: int 값; );

멤버 연산자는 두 개의 SmallInt 개체를 더하거나 빼는 기능을 제공합니다. 전역 친구 연산자를 사용하면 주어진 클래스의 객체와 내장 산술 유형의 객체에 대해 이러한 연산을 수행할 수 있습니다. 내장 산술 유형을 int로 변환할 수 있으므로 6개의 연산자만 필요합니다. 예를 들어, 표현식

두 단계로 해결:

  1. 이중 상수 3.14159는 정수 3으로 변환됩니다.
  2. operator+(const SmallInt &,int)가 호출되어 값 6을 반환합니다.

비교 및 복합 할당 연산자뿐만 아니라 비트 및 논리 연산을 지원하려면 얼마나 많은 연산자를 오버로드해야 할까요? 당신은 즉시 계산하지 않습니다. SmallInt 클래스의 개체를 int 유형의 개체로 자동 변환하는 것이 훨씬 더 편리합니다.

C++ 언어에는 모든 클래스가 해당 개체에 적용할 수 있는 변환 집합을 지정할 수 있는 메커니즘이 있습니다. SmallInt의 경우 int로 캐스팅된 개체를 정의합니다. 구현은 다음과 같습니다.

Class SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // 변환기 // SmallInt ==> int operator int() ( return value; ) // 오버로드된 연산자는 필요하지 않습니다 private: int value; );

int() 연산자는 사용자 정의 변환을 구현하는 변환기입니다. 이 경우 클래스 유형을 지정된 int 유형으로 캐스팅합니다. 변환기 정의는 변환의 의미와 컴파일러가 변환을 적용하기 위해 취해야 하는 조치를 설명합니다. SmallInt 개체의 경우 int로 변환하는 시점은 값 멤버에 저장된 int 형식의 수를 반환하는 것입니다.

이제 SmallInt 클래스의 개체는 int가 허용되는 모든 곳에서 사용할 수 있습니다. 더 이상 오버로드된 연산자가 없고 SmallInt에 int로의 변환기가 있다고 가정하면 더하기 연산은

SmallInt si(3); 시+3.14159

두 단계로 해결:

  1. SmallInt 클래스 변환기가 호출되고 정수 3을 반환합니다.
  2. 정수 3은 3.0으로 확장되고 배정밀도 상수 3.14159에 추가되어 6.14159가 됩니다.

이 동작은 이전에 정의된 오버로드된 연산자와 비교하여 기본 제공 형식 피연산자의 동작과 더 일치합니다. int가 double에 추가되면 두 개의 double이 추가되고(int가 double로 확장되기 때문에) 결과는 동일한 유형의 숫자입니다.

이 프로그램은 SmallInt 클래스의 사용을 보여줍니다.

#포함 #include "SmallInt.h" int main() ( cout<< "Введите SmallInt, пожалуйста: "; while (cin >> si1) ( cout<< "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ((si1 >127)? "보다 큼" : ((si1< 127) ? "меньше, чем " : "равно ")) <<"127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; }

컴파일된 프로그램은 다음과 같은 결과를 생성합니다.

SmallInt 입력: 127

읽기 값 127

127과 같습니다.

SmallInt를 입력하십시오(종료하려면 ctrl-d): 126

127미만

SmallInt를 입력하십시오(종료하려면 ctrl-d): 128

127이상입니다

SmallInt를 입력하십시오(종료하려면 ctrl-d): 256

*** SmallInt 범위 오류: 256 ***

#포함 클래스 SmallInt (친구 istream& 연산자>(istream &is, SmallInt &s); 친구 ostream& 연산자<<(ostream &is, const SmallInt &s) { return os << s.value; } public: SmallInt(int i=0) : value(rangeCheck(i)){} int operator=(int i) { return(value = rangeCheck(i)); } operator int() { return value; } private: int rangeCheck(int); int value; };

다음은 클래스 본문 외부의 멤버 함수 정의입니다.

Istream& operator>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* 처음 8비트 이외의 적어도 하나의 비트가 설정되면 * 값이 너무 크면 보고하고 즉시 종료 */ if (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. 변환기

변환기는 개체를 다른 형식으로 사용자 정의 변환을 구현하는 클래스 멤버 함수의 특별한 경우입니다. 변환기는 키워드 연산자 다음에 변환의 대상 유형을 지정하여 클래스 본문에 선언됩니다.

키워드 뒤에 오는 이름은 기본 제공 유형 중 하나의 이름일 필요는 없습니다. 아래 표시된 Token 클래스는 여러 변환기를 정의합니다. 하나는 typedef tName을 사용하여 유형 이름을 지정하고 다른 하나는 SmallInt 클래스 유형을 사용합니다.

#include "SmallInt.h" typedef char *tName; class Token ( public: Token(char *, int); operator SmallInt() ( return val; ) operator tName() ( return name; ) operator int() ( return val; ) // 다른 공개 멤버 private: SmallInt val; 문자*이름;);

SmallInt 및 int에 대한 변환기의 정의는 동일합니다. Token::operator int() 변환기는 val 멤버의 값을 반환합니다. val은 SmallInt 유형이므로 SmallInt::operator int()는 val을 int로 변환하는 데 암시적으로 사용됩니다. Token::operator int() 자체는 컴파일러에서 암시적으로 Token 유형의 개체를 int 유형의 값으로 변환하는 데 사용됩니다. 예를 들어, 이 변환기는 Token 유형의 실제 인수 t1 및 t2를 암시적으로 print() 함수의 형식 매개변수의 int 유형으로 캐스팅하는 데 사용됩니다.

#include "Token.h" void print(int i) ( cout< < "print(int) : " < < i < < endl; } Token t1("integer constant", 127); Token t2("friend", 255); int main() { print(t1); // t1.operator int() print(t2); // t2.operator int() return 0; }

프로그램을 컴파일하고 실행하면 다음 줄이 표시됩니다.

인쇄(int) : 127 인쇄(int) : 255

변환기의 일반적인 보기는 다음과 같습니다.

연산자 유형();

여기서 type은 내장 유형, 클래스 유형 또는 typedef 이름이 될 수 있습니다. 유형이 배열 또는 함수 유형인 변환기는 허용되지 않습니다. 변환기는 멤버 함수여야 합니다. 선언은 반환 유형이나 매개변수 목록을 지정해서는 안 됩니다.

연산자 int(SmallInt &); // 오류: SmallInt 클래스의 멤버가 아님( public: int operator int(); // 오류: 반환 유형 지정 operator int(int = 0); // 오류: 매개변수 목록 지정 // ... );

변환기는 명시적 형식 변환의 결과로 호출됩니다. 변환되는 값이 변환기가 있는 클래스 유형이고 해당 변환기의 유형이 캐스트 작업에서 지정되면 다음과 같이 호출됩니다.

#include "Token.h" 토큰 tok("function", 78); // 기능적 표기법: Token::operator SmallInt()는 SmallInt로 호출됩니다. tokVal = SmallInt(tok); // static_cast: Token::operator tName() 호출 char *tokName = static_cast< char * >(톡);

Token::operator tName() 변환기에 원치 않는 부작용이 있을 수 있습니다. 개인 멤버 Token::name에 직접 액세스하려는 시도는 컴파일러에 의해 오류 플래그가 지정됩니다.

문자 *tokName = tok.name; // 오류: Token::name은 비공개 멤버입니다.

그러나 사용자가 Token::name을 직접 변경할 수 있도록 하는 변환기는 우리가 보호하고자 하는 것과 정확히 일치합니다. 대부분 작동하지 않을 것입니다. 다음은 이러한 수정이 발생할 수 있는 방법의 예입니다.

#include "Token.h" 토큰 tok("function", 78); char *tok이름 = 톡; // 정답: 암시적 변환 *tokname = "P"; // 하지만 이제 name 멤버에 Punction이 있습니다!

Token 클래스의 변환된 개체에 대한 읽기 전용 액세스를 허용하려고 합니다. 따라서 변환기는 const char* 유형을 반환해야 합니다.

형식 정의 const char *cchar; class Token ( public: operator cchar() ( return name; ) // ... ); // 오류: char*에서 const char*로 변환할 수 없음 char *pn = tok; const char *pn2 = 톡; // 오른쪽

또 다른 솔루션은 Token 정의의 char* 유형을 C++ 표준 라이브러리의 문자열 유형으로 바꾸는 것입니다.

Class Token ( public: Token(string, int); operator SmallInt() ( return val; ) operator string() ( return name; ) operator int() ( return val; ) // 다른 공개 멤버 private: SmallInt val; string 이름; );

Token::operator string() 변환기의 의미는 토큰 이름을 나타내는 문자열의 값(값에 대한 포인터가 아니라)의 복사본을 반환하는 것입니다. 이렇게 하면 Token 클래스의 개인 이름 멤버가 실수로 수정되는 것을 방지할 수 있습니다.

대상 유형이 변환기 유형과 정확히 일치해야 합니까? 예를 들어 다음 코드는 Token 클래스에 정의된 int() 변환기를 호출합니까?

외부 무효 계산(이중); 토큰 토큰("상수", 44); // int() 연산자가 호출되었습니까? 예 // 표준 변환이 적용됩니다. int --> double calc(tok);

대상 유형(이 경우 double)이 변환기 유형(이 경우 int)과 정확히 일치하지 않으면 대상으로 이어지는 표준 변환 시퀀스가 ​​있는 경우 변환기가 계속 호출됩니다. 변환기 유형에서 유형. (이 시퀀스는 섹션 9.3에 설명되어 있습니다.) calc() 함수를 호출하면 Token::operator int()가 호출되어 tok 유형을 Token 유형에서 int 유형으로 변환합니다. 그런 다음 표준 변환이 적용되어 결과를 int에서 double로 캐스팅합니다.

사용자 정의 변환 후에는 표준 변환만 허용됩니다. 대상 유형에 도달하기 위해 다른 사용자 정의 변환이 필요한 경우 컴파일러는 변환을 적용하지 않습니다. Token 클래스에 정의된 연산자 int()가 없다고 가정하면 다음 호출은 오류가 발생합니다.

외부 무효 계산(int); 토큰 tok("포인터", 37); // Token::operator int()가 정의되지 않은 경우 // 이 호출은 컴파일 오류를 발생시킵니다. calc(tok);

Token::operator int() 변환기가 정의되지 않은 경우 tok를 int로 캐스팅하려면 두 개의 사용자 정의 변환기를 호출해야 합니다. 첫째, 실제 인수 tok는 변환기를 사용하여 Token 유형에서 SmallInt 유형으로 변환되어야 합니다.

토큰::연산자 SmallInt()

그런 다음 결과를 int 유형으로 캐스팅합니다. 또한 사용자 지정 변환기를 사용합니다.

토큰::연산자 int()

Token 유형에서 int 유형으로의 암시적 변환이 없기 때문에 calc(tok)에 대한 호출은 컴파일러에 의해 오류로 플래그 지정됩니다.

변환기 유형과 클래스 유형 사이에 논리적 대응이 없는 경우 변환기의 목적이 프로그램 독자에게 명확하지 않을 수 있습니다.

Class Date ( public: // 반환된 멤버를 추측해 봅니다! operator int(); private: int 월, 일, 년; );

Date 클래스의 int() 변환기는 어떤 값을 반환해야 합니까? 이 결정이나 저 결정에 대한 이유가 아무리 훌륭하다 해도 Date 클래스의 개체와 정수 사이에 명백한 논리적 대응이 없기 때문에 독자는 Date 클래스의 개체를 사용하는 방법에 대해 길을 잃을 것입니다. 이러한 경우 변환기를 전혀 정의하지 않는 것이 좋습니다.

15.9.2. 변환기로서의 생성자

SmallInt 클래스의 SmallInt(int)와 같이 단일 매개 변수를 사용하는 클래스 생성자 집합은 SmallInt 값에 대한 암시적 변환 집합을 정의합니다. 예를 들어 SmallInt(int) 생성자는 int 값을 SmallInt 값으로 변환합니다.

외부 무효 계산(SmallInt); 정수 나; // i를 SmallInt 값으로 변환해야 합니다. // 이것은 SmallInt(int)를 사용하여 수행됩니다. calc(i); calc(i)가 호출되면 i는 컴파일러에서 호출한 SmallInt(int) 생성자를 사용하여 원하는 유형의 임시 개체를 생성하는 SmallInt 값으로 변환됩니다. 그런 다음 이 객체의 복사본이 함수 호출이 다음 형식인 것처럼 calc()에 전달됩니다. // C++ 의사 코드 // 임시 SmallInt 객체가 생성됩니다( SmallInt temp = SmallInt(i); calc(temp); )

이 예에서 중괄호는 이 객체의 수명을 나타냅니다. 함수가 종료되면 소멸됩니다.

생성자 매개변수의 유형은 일부 클래스의 유형일 수 있습니다.

Class Number ( public: // SmallInt 유형의 값에서 Number 유형의 값 생성하기 Number(const SmallInt &); // ... );

이 경우 SmallInt 유형의 값은 Number 유형의 값이 허용되는 모든 곳에서 사용할 수 있습니다.

외부 무효 함수(숫자); SmallInt si(87); int main() ( // Number(const SmallInt &) func(si) 호출; // ... )

생성자가 암시적 변환을 수행하는 데 사용되는 경우 해당 매개변수의 유형이 변환할 값의 유형과 정확히 일치해야 합니까? 예를 들어 다음 코드는 SmallInt 클래스에 정의된 SmallInt(int)를 호출하여 dobj를 SmallInt 유형으로 캐스트합니까?

외부 무효 계산(SmallInt); 더블 도비; // SmallInt(int)가 호출되고 있습니까? 예 // dobj는 표준 변환에 의해 // double에서 int로 변환됩니다. calc(dobj);

필요한 경우 사용자 정의 변환을 수행하기 위해 생성자가 호출되기 전에 일련의 표준 변환이 실제 인수에 적용됩니다. calc() 함수를 호출할 때 double에서 int로의 표준 dobj 변환이 사용됩니다. 그런 다음 SmallInt(int)가 결과를 SmallInt로 캐스팅하기 위해 호출됩니다.

컴파일러는 암시적으로 단일 매개변수가 있는 생성자를 사용하여 해당 유형을 생성자가 속한 클래스 유형으로 변환합니다. 그러나 때로는 Number(const SmallInt&) 생성자를 호출하여 Number 유형의 개체를 SmallInt 유형의 값으로 초기화하고 암시적 변환을 수행하지 않는 것이 더 편리합니다. 생성자의 이러한 사용을 피하기 위해 명시적으로 선언해 보겠습니다.

Class Number ( public: // 명시적 Number(const SmallInt &); // ... );

컴파일러는 암시적 형식 변환을 수행하기 위해 명시적 생성자를 사용하지 않습니다.

외부 무효 함수(숫자); SmallInt si(87); int main() ( // 오류: SmallInt에서 Number로의 암시적 변환이 없습니다 func(si); // ... )

그러나 이러한 생성자는 캐스트 연산자의 형태로 명시적으로 요청되는 경우 형식 변환에 계속 사용할 수 있습니다.

SmallInt si(87); int main() ( // 오류: SmallInt에서 Number로의 암시적 변환이 없습니다. func(si); func(Number(si)); // 정답: 캐스트 func(static_cast< Number >(시)); // 정답: 캐스트 )

15.10. 변환 A의 선택

사용자 정의 변환은 변환기 또는 생성자로 구현됩니다. 이미 언급했듯이 변환기가 변환을 수행한 후 표준 변환을 사용하여 반환된 값을 대상 유형으로 캐스팅할 수 있습니다. 생성자에 의해 수행되는 변환은 인수 유형을 생성자의 형식 매개변수 유형으로 캐스트하는 표준 변환이 선행될 수도 있습니다.

사용자 정의 변환 시퀀스는 조합입니다. 사용자 정의값을 대상 유형으로 캐스팅하는 데 필요한 표준 변환. 이러한 시퀀스는 다음과 같습니다.

표준 변환 순서 ->

사용자 정의 변환 ->

표준 변환 순서

여기서 사용자 정의 변환은 변환기 또는 생성자에 의해 구현됩니다.

소스 값을 대상 유형으로 변환하기 위해 두 가지 다른 시퀀스의 사용자 정의 변환이 있을 수 있으며 컴파일러는 그 중에서 가장 좋은 것을 선택해야 합니다. 어떻게 되었는지 봅시다.

클래스에 많은 변환기를 정의할 수 있습니다. 예를 들어, Number 클래스에는 두 가지가 있습니다. operator int() 및 operator float() 둘 다 Number 객체를 float 값으로 변환할 수 있습니다. 당연히 직접 변환을 위해 Token::operator float() 변환기를 사용할 수 있습니다. 그러나 Token::operator int()도 작동합니다. 결과가 int 유형이므로 표준 변환을 사용하여 float로 변환할 수 있기 때문입니다. 그러한 시퀀스가 ​​여러 개 있는 경우 변환이 모호합니까? 아니면 그들 중 하나가 다른 것보다 낫습니까?

클래스 번호 ( public: operator float(); operator int(); // ... ); 번호 번호; floatff = 숫자; // 어떤 변환기? 뜨다()

이러한 경우 사용자 정의 변환의 최상의 순서는 변환기 이후에 적용되는 변환 순서의 분석을 기반으로 선택됩니다. 이전 예에서 다음 두 시퀀스를 적용할 수 있습니다.

  1. 연산자 float() -> 정확히 일치
  2. 연산자 int() -> 표준 변환

9.3절에서 논의한 바와 같이 표준 변환보다 정확한 일치가 더 좋습니다. 따라서 첫 번째 시퀀스가 ​​두 번째 시퀀스보다 낫습니다. 즉, Token::operator float() 변환기가 선택됩니다.

값을 대상 유형으로 변환하기 위해 두 개의 다른 생성자를 적용할 수 있습니다. 이 경우 생성자 호출 이전의 표준 변환 시퀀스가 ​​분석됩니다.

SmallInt 클래스 ( public: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dwal)); ( ) ); extern 무효 manip(const SmallInt &); int main() ( double dobj; manip(dobj); // 정답: SmallInt(double) )

여기에서 SmallInt 클래스는 Double 값을 SmallInt 개체로 변경하는 데 사용할 수 있는 두 개의 생성자 SmallInt(int) 및 SmallInt(double)을 정의합니다. SmallInt(double)은 double을 SmallInt로 직접 변환하고 SmallInt(int) double을 int로 변환하는 표준의 결과에 대해 작동합니다. 따라서 두 가지 사용자 정의 변환 시퀀스가 ​​있습니다.

  1. 정확히 일치 -> SmallInt(이중)
  2. 표준 변환 -> SmallInt(int)

정확한 일치가 표준 변환보다 낫기 때문에 SmallInt(double) 생성자가 선택됩니다.

어떤 순서가 더 나은지 결정하는 것이 항상 가능한 것은 아닙니다. 그것들이 모두 똑같이 좋은 경우가 있을 수 있으며, 이 경우 변환이 모호하다고 말합니다. 이 경우 컴파일러는 암시적 변환을 적용하지 않습니다. 예를 들어 Number 클래스에 두 개의 변환기가 있는 경우:

클래스 번호 ( public: operator float(); operator int(); // ... );

그러면 암시적으로 Number 유형의 개체를 long 유형으로 변환할 수 없습니다. 다음 명령문은 사용자 정의 변환 순서가 모호하기 때문에 컴파일 오류를 일으킵니다.

// 오류: float() 및 int() 둘 다 사용할 수 있습니다. long lval = num;

num을 long 유형의 값으로 변환하려면 다음과 같은 두 가지 시퀀스를 적용할 수 있습니다.

  1. 연산자 float() -> 표준 변환
  2. 연산자 int() -> 표준 변환

두 경우 모두 변환기를 사용한 후 표준 변환을 적용하므로 두 시퀀스 모두 동일하게 양호하며 컴파일러는 둘 중 하나를 선택할 수 없습니다.

명시적 유형 캐스팅을 통해 프로그래머는 원하는 변경 사항을 지정할 수 있습니다.

// 정답: 명시적 캐스트 long lval = static_cast (숫자);

이 사양의 결과로 Token::operator int() 변환기가 선택되고 표준 변환이 뒤따릅니다.

변환 시퀀스 선택의 모호성은 두 클래스가 서로 변환을 정의할 때도 발생할 수 있습니다. 예를 들어:

클래스 SmallInt ( 공개: SmallInt(const 번호 &); // ... ); 클래스 번호 ( 공개: 연산자 SmallInt(); // ... ); 외부 무효 계산(SmallInt); 외부 번호 번호; 계산(숫자); // 오류: 두 가지 변환 가능

num 인수는 2만큼 SmallInt로 변환됩니다. 다른 방법들: SmallInt::SmallInt(const Number&) 생성자 또는 Number::operator SmallInt() 변환기를 사용합니다. 두 변경 사항 모두 동일하므로 호출은 오류로 간주됩니다.

모호성을 해결하기 위해 프로그래머는 명시적으로 Number 클래스 변환기를 호출할 수 있습니다.

// 정확함: 명시적 호출이 명확하게 함 compute(num.operator SmallInt());

그러나 형식 캐스팅에 적합한 변환을 선택할 때 변환기와 생성자가 모두 고려되므로 명시적 캐스트를 사용하여 모호성을 해결해서는 안 됩니다.

계산(SmallInt(숫자)); // 오류: 여전히 모호함

보시다시피 존재감은 큰 수이러한 변환기 및 생성자는 안전하지 않으므로 해당합니다. 주의해서 사용해야 합니다. 암시적 변환을 수행할 때 생성자의 사용을 제한할 수 있습니다. 따라서 생성자를 명시적으로 만들어 예기치 않은 결과가 발생할 가능성을 줄일 수 있습니다.

15.10.1. 함수 과부하 해결 재검토

오버로드된 함수 호출을 해결하는 방법은 9장에서 자세히 설명합니다. 호출될 때 실제 인수가 클래스 유형, 클래스 유형에 대한 포인터 또는 클래스 멤버에 대한 포인터인 경우 더 많은 함수가 가능한 후보를 놓고 경쟁하고 있습니다. 따라서 이러한 인수의 존재는 과부하 해결 절차의 첫 번째 단계인 후보 함수 집합의 선택에 영향을 줍니다.

이 절차의 세 번째 단계에서 가장 잘 일치하는 항목이 선택됩니다. 이 경우 실제 인수 유형을 함수의 형식 매개변수 유형으로 변환하는 순위가 매겨집니다. 인수와 매개변수가 클래스 유형인 경우 가능한 변환 세트에는 사용자 정의 변환의 순서도 포함되어야 하며 순위도 지정해야 합니다.

이 섹션에서는 실제 인수와 형식 클래스 유형 매개변수가 후보 함수 선택에 미치는 영향과 사용자 정의 변환 시퀀스가 ​​가장 잘 설정된 함수 선택에 미치는 영향에 대해 자세히 살펴보겠습니다.

15.10.2. 후보자 기능

후보 함수는 호출된 함수와 이름이 같은 함수입니다. 다음과 같은 호출이 있다고 가정합니다.

SmallInt si(15); 추가(si, 566);

후보 함수의 이름은 add여야 합니다. add() 선언 중 어떤 것이 고려됩니까? 콜 포인트에서 볼 수 있는 것들.

예를 들어 전역 범위에 선언된 두 add() 함수는 다음 호출의 후보가 됩니다.

const 행렬& add(const 행렬 &, int); 이중 더하기(이중, 이중); int main() ( SmallInt si(15); add(si, 566); // ... )

호출 지점에서 선언을 볼 수 있는 함수에 대한 고려는 클래스 유형 인수가 있는 호출에 국한되지 않습니다. 그러나 선언 검색은 다음 두 가지 범위에서 더 수행됩니다.

  • 실제 인수가 클래스 유형의 개체, 클래스 유형에 대한 포인터 또는 참조, 또는 클래스 멤버에 대한 포인터이고 유형이 사용자 정의 네임스페이스에 선언된 경우 해당 공간에서 선언되고 동일한 이름이 후보 함수 세트에 추가되고 다음과 같이 호출됩니다.
namespace NS ( class SmallInt ( /* ... */ ); class String ( /* ... */ ); String add(const String &, const String &); ) int main() ( // si는 type class SmallInt: // NS 네임스페이스에 클래스가 선언됨 NS::SmallInt si(15); add(si, 566); // NS::add()는 후보 함수입니다. return 0; )

si 인수는 SmallInt 유형입니다. NS 네임스페이스에 선언된 클래스의 유형입니다. 따라서 이 네임스페이스에 선언된 add(const String &, const String &)는 후보 함수 집합에 추가됩니다.

  • 실제 인수가 클래스 유형의 개체, 클래스에 대한 포인터 또는 참조, 또는 클래스 멤버에 대한 포인터이고 해당 클래스에 호출된 함수와 이름이 같은 친구가 있으면 집합에 추가됩니다. 후보 기능:
  • namespace NS ( class SmallInt ( friend SmallInt add(SmallInt, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); add(si, 566); // 함수 - friend add() - 후보자 반환 0; )

    si 함수 인수는 SmallInt 유형입니다. SmallInt 클래스 친구 함수 add(SmallInt, int)는 NS 네임스페이스의 멤버이지만 해당 공간에서 직접 선언되지는 않습니다. NS에서 일반적인 검색은 친구 기능을 찾지 않습니다. 그러나 SmallInt 클래스 유형 인수를 사용하여 추가()를 호출하면 멤버 목록에 선언된 클래스의 친구도 고려되어 후보 집합에 추가됩니다.

    따라서 실제 함수 인수 목록에 객체, 클래스에 대한 포인터 또는 참조, 클래스 멤버에 대한 포인터가 포함된 경우 후보 함수 집합은 호출 지점에서 볼 수 있거나 동일한 항목에서 선언된 함수 집합으로 구성됩니다. 정의된 네임스페이스 클래스 유형 또는 이 클래스의 선언된 친구.

    다음 예를 고려하십시오.

    네임스페이스 NS ( class SmallInt ( friend SmallInt add(SmallInt, int) ( /* ... */ ) ); class String ( /* ... */ ); String add(const String &, const String &); ) const 행렬& add(const 행렬 &, int); 이중 더하기(이중, 이중); int main() ( // si는 클래스 SmallInt 유형입니다. // 클래스는 NS 네임스페이스에 선언됨 NS::SmallInt si(15); add(si, 566); // friend 함수는 return 0; )

    후보자는 다음과 같습니다.

    • 전역 함수:
    const matrix& add(const matrix &, int) double add(double, double)
  • 네임스페이스의 함수:
  • NS::add(const 문자열 &, const 문자열 &)
  • 친구 기능:
  • NS::add(SmallInt, int)

    과부하 해결은 SmallInt 클래스 친구 함수 NS::add(SmallInt, int)를 최적으로 선택합니다. 두 실제 인수는 모두 주어진 형식 매개변수와 정확히 일치합니다.

    물론 호출된 함수는 여러 클래스 유형 인수, 클래스에 대한 포인터 또는 참조 또는 클래스 멤버에 대한 포인터를 가질 수 있습니다. 이러한 각 인수에 대해 다른 클래스 유형이 허용됩니다. 이에 대한 후보 함수 검색은 해당 클래스가 정의된 네임스페이스 및 해당 클래스의 friend 함수 중에서 수행됩니다. 따라서 이러한 인수를 사용하여 함수를 호출하기 위한 결과 후보 집합에는 다른 클래스에서 선언된 다른 네임스페이스 및 친구 함수의 함수가 포함됩니다.

    15.10.3. 클래스 범위에서 함수를 호출할 후보 함수

    형식의 함수를 호출할 때

    클래스의 범위(예: 멤버 함수 내부)에서 발생하는 경우 이전 하위 섹션에서 설명한 후보 집합의 첫 번째 부분(즉, 호출 지점에서 볼 수 있는 함수 선언을 포함하는 집합)에 다음 항목 이상이 포함될 수 있습니다. 클래스의 멤버 함수일 뿐입니다. 이름 확인은 이러한 집합을 구성하는 데 사용됩니다. (이 주제는 섹션 13.9 - 13.12에서 자세히 논의되었습니다.)

    예를 고려하십시오.

    네임스페이스 NS( struct myClass( void k(int); 정적 void k(char*); void mf(); ); int k(double); ); 무효 h(문자); void NS::myClass::mf() ( h("a"); // 전역 h(char) k(4) 호출; // myClass::k(int) 호출 )

    섹션 13.11에서 언급했듯이 NS::myClass:: 한정자는 역순으로 조회됩니다. 먼저 mf() 멤버 함수 정의에 사용된 이름에 대한 표시 선언을 myClass 클래스에서 조회한 다음 NS 네임스페이스. 첫 번째 호출을 고려하십시오.

    mf() 멤버 함수 정의에서 h() 이름을 확인할 때 myClass 멤버 함수가 먼저 조회됩니다. 이 클래스의 범위에 이 이름을 가진 멤버 함수가 없기 때문에 검색은 NS 네임스페이스에서 계속됩니다. h() 함수도 거기에 없으므로 전역 범위로 이동합니다. 결과는 호출 지점에서 볼 수 있는 유일한 후보 함수인 전역 함수 h(char)입니다.

    적합한 광고가 발견되는 즉시 검색이 중지됩니다. 따라서 집합에는 이름 확인이 성공한 범위에 선언된 함수만 포함됩니다. 이것은 호출 후보 세트를 구성하는 예에서 볼 수 있습니다.

    먼저 myClass 클래스의 범위에서 검색을 수행합니다. 이것은 두 개의 멤버 함수 k(int) 및 k(char*)를 찾았습니다. 후보 집합은 해결이 성공한 범위에서 선언된 함수만 포함하므로 NS 네임스페이스를 조회하지 않고 함수 k(double)은 이 집합에 포함되지 않습니다.

    집합에 가장 적합한 함수가 없기 때문에 호출이 모호한 것으로 확인되면 컴파일러는 오류 메시지를 발행합니다. 실제 인수와 더 잘 일치하는 후보는 둘러싸는 범위에서 검색되지 않습니다.

    15.10.4. 사용자 정의 변환의 순위 지정 시퀀스

    실제 함수 인수는 일련의 사용자 정의 변환을 사용하여 형식 매개변수 유형으로 암시적으로 캐스트될 수 있습니다. 이것이 과부하 해결에 어떤 영향을 줍니까? 예를 들어, calc()에 대한 다음 호출이 있는 경우 어떤 함수가 호출됩니까?

    SmallInt 클래스(공개: SmallInt(int); ); 외부 무효 계산(이중); 외부 무효 계산(SmallInt); int ival; int main() ( calc(ival); // 어떤 calc()가 호출됩니까? )

    형식 매개변수가 실제 인수의 유형과 가장 잘 일치하는 함수가 선택됩니다. 최적의 맞춤 또는 최적의 기능이라고 합니다. 이러한 함수를 선택하기 위해 실제 인수에 적용된 암시적 변환의 순위가 매겨집니다. 가장 잘 살아남은 것은 인수에 적용된 변경 사항이 다른 살아남은 함수보다 나쁘지 않고 적어도 하나의 인수에 대해 다른 모든 함수보다 나은 경우입니다.

    표준 변환 시퀀스는 사용자 정의 변환 시퀀스보다 항상 좋습니다. 따라서 위의 예에서 calc()를 호출할 때 두 calc() 함수 모두 잘 설정됩니다. calc(double)은 실제 인수 int에서 형식 매개변수 유형 double로의 표준 변환이 있기 때문에 살아남았고, calc(SmallInt)는 SmallInt(int) 생성자를 사용하는 int에서 SmallInt로의 사용자 정의 변환이 있기 때문에 살아남았습니다. 따라서 최고의 서있는 함수는 calc(double)입니다.

    사용자 정의 변환의 두 시퀀스를 어떻게 비교합니까? 다른 변환기나 다른 생성자를 사용하는 경우 이러한 시퀀스는 모두 동일한 것으로 간주됩니다.

    클래스 번호 ( public: operator SmallInt(); operator int(); // ... ); 외부 무효 계산(int); 외부 무효 계산(SmallInt); 외부 번호 번호; 계산(숫자); // 오류: 모호성

    calc(int)와 calc(SmallInt)는 모두 살아남을 것입니다. 첫 번째는 Number::operator int() 변환기가 실제 Number 형식 인수를 형식 int 형식 매개 변수로 변환하기 때문이고 두 번째는 Number::operator SmallInt() 변환기가 실제 Number 형식 인수를 형식 SmallInt 형식으로 변환하기 때문입니다. 매개변수. 사용자 정의 변환 시퀀스는 항상 동일한 순위를 가지므로 컴파일러는 어느 것이 더 나은지 선택할 수 없습니다. 따라서 이 함수 호출은 모호하고 컴파일 오류가 발생합니다.

    변환을 명시적으로 지정하여 모호성을 해결하는 방법이 있습니다.

    // 명시적 캐스팅은 calc(static_cast)를 명확하게 합니다.< int >(숫자)));

    명시적 캐스트는 컴파일러가 Number::operator int() 변환기를 사용하여 num 인수를 int로 변환하도록 합니다. 그러면 실제 인수는 int 유형이 되며, 이는 가장 좋은 것으로 선택된 calc(int) 함수와 정확히 일치합니다.

    Number::operator int() 변환기가 Number 클래스에 정의되어 있지 않다고 가정해 보겠습니다. 그럼 도전이 될까요

    // Number::operator SmallInt()만 정의됨 calc(num); // 여전히 모호합니까?

    여전히 모호한가? SmallInt에는 SmallInt 값을 int로 변환할 수 있는 변환기도 있습니다.

    클래스 SmallInt ( public: operator int(); // ... );

    먼저 Number::operator SmallInt() 변환기를 사용하여 실제 인수 num을 유형 Number에서 SmallInt 유형으로 변환한 다음 SmallInt::operator SmallInt()를 사용하여 결과를 int로 캐스팅하여 calc() 함수가 호출된다고 가정할 수 있습니다. . 그러나 그렇지 않습니다. 일련의 사용자 정의 변환에는 여러 표준 변환이 포함될 수 있지만 사용자 정의 변환은 하나만 포함할 수 있습니다. Number::operator int() 변환기가 정의되지 않은 경우 실제 인수 num 유형에서 형식 매개변수 int 유형으로의 암시적 변환이 없기 때문에 calc(int) 함수는 안정적인 것으로 간주되지 않습니다.

    따라서 Number::operator int() 변환기가 없으면 나머지 함수는 호출이 허용되는 calc(SmallInt)뿐입니다.

    동일한 변환기가 두 개의 사용자 정의 변환 시퀀스에 사용되는 경우 최상의 변환기 선택은 호출 후 수행되는 표준 변환 시퀀스에 따라 다릅니다.

    클래스 SmallInt ( public: operator int(); // ... ); 무효 manip(int); 무효 manip(char); SmallInt si(68); main() ( manip(si); // manip(int) 호출)

    manip(int) 및 manip(char) 둘 다 설정된 함수입니다. 첫 번째는 SmallInt::operator int() 변환기가 SmallInt 유형의 실제 인수를 형식 매개변수 int 유형으로 변환하기 때문이고 두 번째는 동일한 변환기가 SmallInt를 int로 변환한 후 결과가 char로 변환되기 때문입니다. 표준 변환을 사용합니다. 사용자 정의 변환 시퀀스는 다음과 같습니다.

    Manip(int) : 연산자 int()->정확히 일치 manip(int) : 연산자 int()->표준 변환

    두 시퀀스 모두에서 동일한 변환기가 사용되기 때문에 표준 변환 시퀀스의 순위를 분석하여 최상의 변환을 결정합니다. 정확히 일치하는 것이 변환보다 낫기 때문에 manip(int)가 가장 잘 확립된 함수입니다.

    이러한 선택 기준은 사용자 정의 변환의 두 시퀀스 모두에서 동일한 변환기가 사용되는 경우에만 허용된다는 점을 강조합니다. 이것이 우리의 예제가 섹션 15.9의 끝 부분에 있는 예제와 다른 점입니다. 여기서 컴파일러가 일부 값에서 주어진 대상 유형으로의 사용자 정의 변환을 선택하는 방법을 보여주었습니다. 소스 및 대상 유형은 고정되었고 컴파일러는 선택해야 했습니다. 한 유형에서 다른 유형으로의 서로 다른 사용자 정의 변환 사이. 여기서 우리는 형식 매개변수의 유형이 다른 두 가지 다른 함수를 고려하고 대상 유형이 다릅니다. 2인 경우 다른 유형매개변수에는 서로 다른 사용자 정의 변환이 필요하며 동일한 변환기가 두 시퀀스에서 모두 사용되는 경우에만 한 유형을 다른 유형보다 선호하는 것이 가능합니다. 그렇지 않은 경우 변환기 적용 후 표준 변환을 평가하여 최상의 대상 유형을 선택합니다. 예를 들어:

    클래스 SmallInt ( public: operator int(); operator float(); // ... ); 무효 계산(float); 무효 계산(문자); SmallInt si(68); main() ( compute(si); // 모호성 )

    compute(float) 및 compute(int) 둘 다 확립된 함수입니다. compute(float)는 SmallInt::operator float() 변환기가 SmallInt 유형 인수를 float 매개변수 유형으로 변환하기 때문이고, compute(char)는 SmallInt::operator int()가 SmallInt 유형 인수를 int 유형으로 변환하기 때문입니다. 결과는 표준적으로 char 유형으로 캐스트됩니다. 따라서 다음과 같은 시퀀스가 ​​있습니다.

    Compute(float) : 연산자 float()->정확히 일치 compute(char) : 연산자 char()->표준 변환

    서로 다른 변환기를 사용하기 때문에 호출과 더 잘 일치하는 형식 매개변수가 있는 함수를 결정하는 것은 불가능합니다. 둘 중 가장 좋은 것을 선택하기 위해 표준 변환 시퀀스의 순위는 사용되지 않습니다. 호출은 컴파일러에 의해 모호한 것으로 표시됩니다.

    연습 15.12

    C++ 표준 라이브러리 클래스에는 변환기 정의가 없으며 하나의 매개 변수를 사용하는 대부분의 생성자는 명시적으로 선언됩니다. 그러나 많은 오버로드된 연산자가 정의됩니다. 디자인 과정에서 왜 이런 결정을 했다고 생각하는가?

    연습 15.13

    이 섹션의 시작 부분에 정의된 SmallInt 클래스의 오버로드된 입력 연산자가 다음과 같이 구현되지 않은 이유는 다음과 같습니다.

    Istream& operator>>(istream &is, SmallInt &si) ( return (is >> is.value); )

    연습 15.14

    다음 초기화에 대한 사용자 정의 변환의 가능한 시퀀스를 제공하십시오. 각 초기화의 결과는 무엇입니까?

    LongDouble 클래스(연산자 double(); 연산자 float(); ); extern LongDouble ldObj; (a) 정수 ex1 = ldObj; (b) float ex2 = ldObj;

    운동 15.15

    인수 중 하나 이상이 클래스 유형일 때 함수 오버로드를 해결할 때 고려되는 후보 함수의 세 가지 집합을 말하십시오.

    운동 15.16

    이 경우 calc() 함수 중 어떤 것이 가장 좋은 것으로 선택됩니까? 각 함수를 호출하는 데 필요한 일련의 변환을 보여주고 하나가 다른 함수보다 나은 이유를 설명하십시오.

    클래스 LongDouble( public: LongDouble(double); // ... ); 외부 무효 계산(int); 외부 무효 계산(LongDouble); 이중 dval; int main() ( calc(dval); // 어떤 함수인가요? )

    15.11. 과부하 해결 및 멤버 함수 A

    멤버 함수도 오버로드될 수 있으며, 이 경우에도 오버로드 해결 절차가 적용되어 가장 적합한 것을 선택합니다. 이 해결 방법은 일반 기능의 절차와 매우 유사하며 동일한 세 단계로 구성됩니다.

    1. 후보 기능 선택.
    2. 확립된 기능의 선택.

    그러나 후보 집합을 생성하고 안정적인 멤버 함수를 선택하는 알고리즘에는 약간의 차이가 있습니다. 이 섹션에서는 이러한 차이점을 고려할 것입니다.

    15.11.1. 오버로드된 멤버 함수 선언

    클래스 멤버 함수는 다음과 같이 오버로드될 수 있습니다.

    Class myClass ( public: void f(double); char f(char, char); // 오버로드 myClass::f(double) // ... );

    네임스페이스에 선언된 함수와 마찬가지로 멤버 함수는 매개변수 목록이 매개변수의 수나 유형이 다른 경우 동일한 이름을 가질 수 있습니다. 두 멤버 함수의 선언이 반환 형식만 다른 경우 두 번째 선언은 컴파일 오류로 간주됩니다.

    Class myClass ( public: void mf(); double mf(); // 오류: 오버로드할 수 없습니다 // ... );

    네임스페이스의 함수와 달리 멤버 함수는 한 번만 선언하면 됩니다. 두 멤버 함수의 반환 형식과 매개 변수 목록이 같더라도 컴파일러는 두 번째 선언을 잘못된 재선언으로 해석합니다.

    클래스 myClass ( public: void mf(); void mf(); // 오류: 재선언 // ... );

    오버로드된 모든 함수는 동일한 범위에서 선언되어야 합니다. 따라서 멤버 함수는 네임스페이스에 선언된 함수를 오버로드하지 않습니다. 또한 각 클래스에는 고유한 범위가 있으므로 다른 클래스의 멤버인 함수는 서로 오버로드되지 않습니다.

    오버로드된 멤버 함수 집합에는 정적 및 비정적 함수가 모두 포함될 수 있습니다.

    Class myClass ( public: void mcf(double); static void mcf(int*); // 오버로드 myClass::mcf(double) // ... );

    정적 또는 비정적 중 어떤 멤버 함수가 호출되는지는 오버로드 해결 결과에 따라 다릅니다. 정적 및 비정적 구성원이 모두 살아남은 상황에서의 해결 프로세스는 다음 섹션에서 자세히 설명합니다.

    15.11.2. 후보자 기능

    두 가지 종류의 멤버 함수 호출을 고려하십시오.

    Mc.mf(인수); pmc->mf(인수);

    여기서 mc는 myClass 유형의 표현식이고 pmc는 "myClass 유형에 대한 포인터" 유형의 표현식입니다. 두 호출에 대한 후보 세트는 mf() 선언을 찾을 때 myClass 범위에서 찾은 함수로 구성됩니다.

    마찬가지로 형식의 함수를 호출하는 경우

    마이클래스::mf(인수);

    후보 세트는 또한 mf() 선언을 찾을 때 myClass 클래스의 범위에서 발견되는 함수로 구성됩니다. 예를 들어:

    클래스 myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); )

    main()의 함수 호출 후보는 모두 myClass에 선언된 세 개의 mf() 멤버 함수입니다.

    무효 mf(더블); 무효 mf(문자, 문자 = "\n"); 정적 무효 mf(int*);

    mf()라는 멤버 함수가 myClass에 선언되지 않은 경우 후보 집합이 비어 있습니다. (사실, 기본 클래스의 함수도 고려됩니다. 우리는 19.3절에서 그것들이 어떻게 이 집합에 속하는지 논의할 것입니다.) 함수 호출에 대한 후보가 없으면 컴파일러는 오류 메시지를 발행합니다.

    15.11.3. 확립된 기능

    잘 확립된 함수는 주어진 실제 인수로 호출할 수 있는 후보 집합의 함수입니다. 그것이 살아남으려면 실제 인수의 유형과 형식 매개변수 사이에 암시적 변환이 있어야 합니다. 예: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // 어떤 mf() 멤버 함수입니까? 모호한 )

    이 스니펫에는 main()에서 mf()를 호출하기 위한 두 가지 잘 정립된 함수가 있습니다.

    무효 mf(더블); 무효 mf(문자, 문자 = "\n");

    • mf(double)은 매개변수가 하나만 있고 int 인수 iobj를 double 매개변수로 표준 변환하기 때문에 살아남았습니다.
    • mf(char,char)는 두 번째 매개변수에 대한 기본값이 있고 int 인수 iobj를 첫 번째 형식 매개변수의 char 유형으로 표준 변환하기 때문에 살아남았습니다.

    각 실제 인수에 적용된 잘 정립된 유형 변환 함수 중 가장 좋은 것을 선택할 때 순위가 매겨집니다. 가장 좋은 것은 사용된 모든 변환이 잘 확립된 다른 어떤 함수보다 나쁘지 않고 적어도 하나의 인수에 대해 그러한 변환이 다른 모든 함수보다 나은 것입니다.

    이전 예에서 설정된 두 함수 각각은 표준 변환을 사용하여 실제 인수의 유형을 형식 매개변수의 유형으로 캐스트합니다. 두 멤버 함수가 똑같이 잘 해결하기 때문에 호출이 모호한 것으로 간주됩니다.

    함수 호출의 유형에 관계없이 정적 및 비정적 멤버 모두 남아 있는 멤버 집합에 포함될 수 있습니다.

    클래스 myClass ( 공개: 정적 무효 mf(int); char mf(char); ); int main() ( char cobj; myClass::mf(cobj); // 어떤 멤버 함수인가요? )

    여기에서 mf() 멤버 함수는 클래스 이름과 범위 확인 연산자 myClass::mf()로 호출됩니다. 그러나 개체("점" 연산자 사용)도 개체에 대한 포인터("화살표" 연산자 사용)도 지정되지 않습니다. 그럼에도 불구하고, 비정적 멤버 함수 mf(char)는 여전히 정적 멤버 mf(int)와 함께 생존 집합에 포함됩니다.

    그런 다음 과부하 해결 프로세스가 계속됩니다. 실제 인수에 적용된 유형 변환의 순위를 기반으로 최상의 기능을 선택합니다. char 유형의 cobj 인수는 형식 매개변수 mf(char)와 정확히 일치하며 형식 매개변수 mf(int)의 형식으로 확장될 수 있습니다. 정확히 일치하는 순위가 더 높기 때문에 mf(char) 함수가 선택됩니다.

    그러나 이 멤버 함수는 정적이 아니므로 접근자 중 하나를 사용하여 myClass 클래스의 개체 또는 개체에 대한 포인터를 통해서만 호출됩니다. 이러한 상황에서 개체가 지정되지 않아 함수 호출이 불가능하면(우리의 경우에만 해당) 컴파일러는 이를 오류로 간주합니다.

    설정된 함수 집합을 구성할 때 고려해야 하는 멤버 함수의 또 다른 기능은 비정적 멤버에 const 또는 volatile 지정자가 있다는 것입니다. (섹션 13.3에서 논의되었습니다.) 과부하 해결 프로세스에 어떤 영향을 줍니까? 클래스 myClass가 다음과 같은 멤버 함수를 갖도록 하십시오.

    클래스 myClass ( 공개: 정적 무효 mf(int*); 무효 mf(더블); 무효 mf(int) const; // ... );

    그런 다음 정적 멤버 함수 mf(int*), 상수 함수 mf(int) 및 비 const 함수 mf(double) 모두 아래 표시된 호출에 대한 후보 집합에 포함됩니다. 그러나 그들 중 어느 것이 생존자 세트에 포함될 것입니까?

    int main() ( const myClass mc; double dobj; mc.mf(dobj); // 어떤 mf() 멤버 함수? )

    실제 인수에 적용할 변환을 조사하면서 mf(double) 및 mf(int) 함수가 살아남았다는 것을 알 수 있습니다. 실제 인수 dobj의 double 유형은 형식 매개변수 mf(double)의 유형과 정확히 일치하며 표준 변환을 사용하여 mf(int) 매개변수의 유형으로 캐스트될 수 있습니다.

    멤버 함수 호출이 점 또는 화살표 액세스 연산자를 사용하는 경우 정체된 함수 집합으로 함수를 선택할 때 함수가 호출되는 객체 또는 포인터의 유형이 고려됩니다.

    mc는 비정적 const 멤버 함수만 호출할 수 있는 const 개체입니다. 따라서 상수가 아닌 멤버 함수 mf(double)은 살아남은 함수 집합에서 제외되고 유일한 함수 mf(int)만 남아 있으며 호출됩니다.

    const 객체를 사용하여 정적 멤버 함수를 호출하면 어떻게 될까요? 결국, 그러한 함수에 대해 const 또는 volatile 지정자를 설정하는 것이 불가능하므로 const 객체를 통해 호출할 수 있습니까?

    클래스 myClass ( 공개: 정적 무효 mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // 정적 멤버 함수를 호출할 수 있습니까? )

    정적 멤버 함수는 동일한 클래스의 모든 개체에 공통입니다. 클래스의 정적 멤버에만 직접 액세스할 수 있습니다. 따라서 상수 객체 mc의 비정적 멤버는 정적 mf(int)에서 사용할 수 없습니다. 이러한 이유로 점 또는 화살표 연산자를 사용하여 const 개체에서 정적 멤버 함수를 호출하는 것이 허용됩니다.

    따라서 정적 멤버 함수는 호출되는 개체에 const 또는 volatile 지정자가 있는 경우에도 살아남은 함수 집합에서 제외되지 않습니다. 정적 멤버 함수는 해당 클래스의 개체에 대한 포인터 또는 개체에 해당하는 것으로 처리됩니다.

    위의 예에서 mc는 const 객체이므로 mf(char) 멤버 함수는 살아남은 집합에서 제외됩니다. 그러나 멤버 함수 mf(int)는 정적이기 때문에 그 안에 남아 있습니다. 이것이 유일하게 안정적인 기능이기 때문에 가장 좋은 것으로 판명되었습니다.

    15.12. 과부하 해결 및 A 연산자

    클래스는 오버로드된 연산자와 변환기를 선언할 수 있습니다. 초기화 중에 더하기 연산자가 발생했다고 가정합니다.

    SomeClass sc; int iobj = sc + 3;

    컴파일러는 SomeClass에서 오버로드된 연산자를 호출할지 아니면 sc 피연산자를 기본 제공 유형으로 변환한 다음 기본 제공 연산자를 사용할지 여부를 어떻게 결정합니까?

    대답은 SomeClass에 정의된 많은 오버로드된 연산자와 변환기에 따라 다릅니다. 덧셈을 수행할 연산자를 선택하면 함수 과부하 해결 프로세스가 적용됩니다. 에 이번 장피연산자가 클래스 유형의 개체일 때 이 프로세스를 통해 원하는 연산자를 선택하는 방법을 설명합니다.

    과부하 해결은 섹션 9.2에 제시된 것과 동일한 3단계 절차를 사용합니다.

    • 후보 기능 선택.
    • 확립된 기능의 선택.
    • 확립된 기능 중 최고의 선택.
    • 이 단계를 더 자세히 살펴보겠습니다.

      모든 피연산자가 기본 제공 형식인 경우 함수 오버로드 해결이 적용되지 않습니다. 이 경우 내장 연산자 사용이 보장됩니다. (내장형 피연산자와 함께 연산자를 사용하는 것은 4장에서 다룹니다.) 예를 들면:

    클래스 SmallInt ( 공개: SmallInt(int); ); SmallInt 연산자+ (const SmallInt &, const SmallInt &); 무효 func() ( int i1, i2; int i3 = i1 + i2; )

    피연산자 i1 및 i2는 클래스 유형이 아닌 int 유형이므로 내장 + 연산자가 추가로 사용됩니다. 오버로드된 operator+(const SmallInt &, const SmallInt &)는 무시되지만 SmallInt(int) 생성자의 형태로 사용자 정의 변환을 통해 피연산자를 SmallInt로 캐스팅할 수 있습니다. 아래에 설명된 과부하 해결 프로세스는 이러한 상황에 적용되지 않습니다.

    또한 연산자에 대한 오버로드 해결은 연산자 구문을 사용할 때만 사용됩니다.

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // 사용된 연산자 구문)

    대신 함수 호출 구문을 사용하는 경우 int res = operator+(si, iobj); // 함수 호출 구문

    그러면 네임스페이스의 함수에 대한 과부하 해결 절차가 적용됩니다(섹션 15.10 참조). 멤버 함수를 호출하는 구문이 사용되는 경우:

    // 멤버 함수 호출 구문 int res = si.operator+(iobj);

    그러면 멤버 함수에 대한 해당 절차가 작동합니다(섹션 15.11 참조).

    15.12.1. 후보 연산자 기능

    연산자 함수는 호출된 함수와 이름이 같은 경우 후보입니다. 다음 덧셈 연산자를 사용할 때

    SmallInt si(98); intiobj = 65; 정수 res = si + iobj;

    후보 연산자 함수는 operator+입니다. 어떤 operator+ 선언이 고려됩니까?

    잠재적으로 클래스 유형 피연산자와 함께 연산자 구문을 사용하는 경우 5개의 후보 세트가 구성됩니다. 처음 세 가지는 클래스 유형 인수를 사용하여 일반 함수를 호출할 때와 동일합니다.

    • 호출 지점에서 볼 수 있는 연산자 집합입니다. 연산자가 사용된 지점에서 볼 수 있는 operator+() 함수 선언이 후보입니다. 예를 들어, 전역 범위에서 선언된 operator+()는 operator+()가 main() 내부에서 사용되는 경우 후보입니다.
    SmallInt 연산자+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+()는 후보 함수임)
  • 피연산자의 유형이 정의된 네임스페이스에 선언된 연산자 집합입니다. 피연산자가 클래스 유형이고 해당 유형이 사용자 정의 네임스페이스에 선언된 경우 동일한 공간에서 선언되고 사용된 연산자와 이름이 같은 연산자 함수가 후보로 간주됩니다.
  • namespace NS ( class SmallInt ( /* ... */ ); SmallInt operator+ (const SmallInt&, double); ) int main() ( // si는 SmallInt 유형입니다: // 이 클래스는 NS 네임스페이스 NS에서 선언됩니다. :SmallInt si(15); // NS::operator+() - 후보 함수 int res = si + 566; return 0; )

    피연산자 si는 NS 네임스페이스에 선언된 SmallInt 클래스 유형입니다. 따라서 동일한 공간에 선언된 오버로드된 operator+(const SmallInt, double)가 후보 집합에 추가됩니다.

  • 피연산자가 속한 클래스의 친구로 선언된 연산자 집합입니다. 피연산자가 클래스 유형에 속하고 이 클래스의 정의에 적용된 연산자에 대해 동일한 이름의 친구 함수가 있는 경우 후보 집합에 추가됩니다.
  • namespace NS ( class SmallInt ( friend SmallInt operator+(const SmallInt&, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); // 친구 함수 operator+() - 후보 정수 res = si + 566, 반환 0, )

    si 피연산자는 SmallInt 유형입니다. 이 클래스의 친구인 operator+(const SmallInt&, int) 연산자 함수는 NS 네임스페이스의 멤버이지만 해당 공간에서 직접 선언되지는 않는다. NS에서의 일반적인 검색은 이 연산자 기능을 찾지 못할 것입니다. 그러나 SmallInt 유형의 인수와 함께 operator+()를 사용할 때 해당 클래스의 범위에서 선언된 friend 함수는 고려 사항에 포함되고 후보 집합에 추가됩니다. 이 세 가지 후보 연산자 함수 세트는 클래스 유형 인수를 사용하는 일반 함수 호출과 동일한 방식으로 형성됩니다. 그러나 연산자 구문을 사용하면 두 개의 집합이 더 생성됩니다.

    • 왼쪽 피연산자의 클래스에 선언된 멤버 연산자 집합입니다. operator+()의 피연산자가 클래스 유형이면 해당 클래스의 구성원인 operator+() 선언이 후보 함수 집합에 포함됩니다.
    클래스 myFloat( myFloat(더블); ); 클래스 SmallInt(공개: SmallInt(int); SmallInt 연산자+(const myFloat &); ); int main() ( SmallInt si(15); int res = si + 5.66; // 후보 멤버 연산자+() )

    SmallInt에 정의된 SmallInt::operator+(const myFloat &) 멤버 연산자는 main()에서 operator+()에 대한 호출을 해결하기 위해 후보 함수 집합에 포함됩니다.

  • 많은 내장 연산자. 기본 제공 operator+()와 함께 사용할 수 있는 유형이 주어지면 후보도 다음과 같습니다.
  • 정수 연산자+(int, 정수); 이중 연산자+(이중, 이중); T* 연산자+(T*, I); T* 연산자+(I, T*);

    첫 번째 선언은 두 개의 정수 유형 값을 추가하기 위한 내장 연산자에 대한 것이고 두 번째 선언은 부동 소수점 유형 값을 추가하기 위한 연산자에 대한 것입니다. 세 번째와 네 번째는 포인터에 정수를 추가하는 데 사용되는 내장 포인터 유형 덧셈 연산자에 해당합니다. 마지막 두 선언은 상징적이며 덧셈 연산을 처리할 때 컴파일러가 후보로 선택할 수 있는 내장 연산자의 전체 제품군을 설명합니다.

    처음 4개 세트 중 하나가 비어 있을 수 있습니다. 예를 들어 SmallInt 클래스의 멤버 중 operator+()라는 함수가 없으면 네 번째 집합은 비어 있습니다.

    후보 연산자 함수의 전체 집합은 위에서 설명한 5가지 하위 집합의 합집합입니다.

    네임스페이스 NS ( class myFloat ( myFloat(double); ); class SmallInt ( friend SmallInt operator+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operator int(); SmallInt operator+ ( const myFloat &); // ... ); SmallInt operator+ (const SmallInt &, double); ) int main() ( // type si - class SmallInt: // 이 클래스는 NS 네임스페이스 NS::SmallInt에 선언됨 si (15); int res = si + 5.66; // 어떤 연산자+()를 반환합니까? return 0; )

    이 5개 세트에는 main()에서 operator+()의 역할에 대한 7개의 후보 연산자 함수가 포함되어 있습니다.

      첫 번째 세트는 비어 있습니다. 전역 범위, 즉 main() 함수에서 operator+()가 사용되는 범위에서는 오버로드된 operator+() 연산자의 선언이 없습니다.
    • 두 번째 집합에는 SmallInt 클래스가 정의된 NS 네임스페이스에 선언된 연산자가 포함되어 있습니다. 이 공간에는 하나의 연산자가 있습니다. NS::SmallInt NS::operator+(const SmallInt &, double);
    • 세 번째 집합에는 SmallInt 클래스의 친구로 선언된 연산자가 포함되어 있습니다. 여기에는 NS::SmallInt NS::operator+(const SmallInt &, int);
    • 네 번째 집합에는 SmallInt의 멤버로 선언된 연산자가 포함되어 있습니다. NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • 다섯 번째 세트에는 내장 이항 연산자가 포함되어 있습니다.
    정수 연산자+(int, 정수); 이중 연산자+(이중, 이중); T* 연산자+(T*, I); T* 연산자+(I, T*);

    예, 연산자 구문과 함께 사용되는 연산자의 해결을 위한 후보 집합을 생성하는 것은 지루합니다. 그러나 구성된 후에는 선택된 후보의 피연산자에 적용된 변환을 분석하여 이전과 같이 지속 기능과 그 중 가장 좋은 기능을 찾습니다.

    15.12.2. 확립된 기능

    설정된 연산자 함수 집합은 주어진 피연산자로 호출할 수 있는 연산자만 선택하여 후보 집합에서 구성됩니다. 예를 들어, 위에서 찾은 7명의 후보자 중 누가 출마할 것입니까? 연산자는 다음 컨텍스트에서 사용됩니다.

    NS::SmallInt si(15); 시 + 5.66;

    왼쪽 피연산자는 SmallInt 유형이고 오른쪽 피연산자는 double입니다.

    첫 번째 후보는 잘 정립된 기능입니다. 이 용도연산자+():

    이니셜라이저로 SmallInt 형식의 왼쪽 피연산자는 이 오버로드된 연산자의 형식 참조 매개변수와 정확히 일치합니다. double 유형의 오른쪽 매개변수도 두 번째 형식 매개변수와 정확히 일치합니다.

    다음 후보 기능도 유지됩니다.

    NS::SmallInt NS::operator+(const SmallInt &, int);

    SmallInt 형식의 왼쪽 피연산자 si는 초기화로 오버로드된 연산자의 형식 참조 매개변수와 정확히 일치합니다. 오른쪽은 int 유형이며 표준 변환을 사용하여 두 번째 형식 매개변수 유형으로 캐스트할 수 있습니다.

    세 번째 후보 기능도 다음과 같이 유지됩니다.

    NS::SmallInt NS::SmallInt::operator+(const myFloat &);

    왼쪽 피연산자 si는 SmallInt 유형입니다. 오버로드된 연산자가 구성원인 클래스의 형식입니다. 오른쪽은 int 유형이고 myFloat(double) 생성자의 형태로 사용자 정의 변환을 사용하여 myFloat 클래스 유형으로 캐스트됩니다.

    네 번째 및 다섯 번째 설정된 기능은 내장 연산자입니다.

    정수 연산자+(int, 정수); 이중 연산자+(이중, 이중);

    SmallInt 클래스에는 SmallInt 값을 int로 변환할 수 있는 변환기가 포함되어 있습니다. 이 변환기는 첫 번째 내장 연산자와 함께 왼쪽 피연산자를 int로 변환하는 데 사용됩니다. double 형식의 두 번째 피연산자는 표준 변환을 사용하여 int 형식으로 변환됩니다. 두 번째 기본 제공 연산자의 경우 변환기는 왼쪽 피연산자를 SmallInt 형식에서 int 형식으로 변환한 후 결과가 표준적으로 double로 변환됩니다. double 유형의 두 번째 피연산자는 두 번째 매개변수와 정확히 일치합니다.

    이 다섯 가지 지속적인 함수 중 가장 좋은 것은 NS 네임스페이스에 선언된 첫 번째 함수 operator+()입니다.

    NS::SmallInt NS::operator+(const SmallInt &, 이중);

    두 피연산자는 모두 매개변수와 정확히 일치합니다.

    15.12.3. 모호

    기본 제공 형식 및 오버로드된 연산자에 대한 암시적 변환을 수행하는 변환기의 동일한 클래스에 있으면 둘 사이를 선택할 때 모호성이 발생할 수 있습니다. 예를 들어, 비교 함수가 있는 다음과 같은 String 클래스 정의가 있습니다.

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; // 연산자 없음 operator== (const char *) );

    이 operator== 사용:

    문자열 꽃("튤립"); void foo(const char *pf) ( // 오버로드된 연산자 호출 String::operator==() if (flower == pf) cout<< pf <<" is a flower!\en"; // ... }

    그럼 비교할 때

    꽃 == pf

    String 클래스의 항등 연산자는 다음과 같이 호출됩니다.

    pf의 오른쪽 피연산자를 const char* 유형에서 operator==() 매개변수의 String 유형으로 변환하기 위해 생성자를 호출하는 사용자 정의 변환이 적용됩니다.

    문자열(const char *)

    String 클래스 정의에 const char* 유형의 변환기를 추가하면:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operator const char*(); // 새 변환기 );

    operator==() 의 표시된 사용법이 모호해집니다.

    // 동등성 검사는 더 이상 컴파일되지 않습니다! if (꽃 == pf)

    변환기 연산자 const char*() 내장 비교 연산자 추가로 인해

    또한 안정적인 기능으로 간주됩니다. 이를 통해 String 유형의 왼쪽 피연산자 꽃을 const char * 유형으로 변환할 수 있습니다.

    이제 foo()에서 operator==()를 사용하기 위해 두 개의 설정된 연산자 함수가 있습니다. 첫번째

    문자열::연산자==(const 문자열 &) const;

    pf의 오른쪽 피연산자를 const char*에서 String으로 사용자 정의 변환해야 합니다. 초

    부울 연산자==(const char *, const char *)

    string에서 const char*로 flower의 왼쪽 피연산자를 사용자 지정 변환해야 합니다.

    따라서 첫 번째 잘 확립된 함수는 왼쪽 피연산자에 더 좋고 두 번째 함수는 오른쪽 피연산자에 더 좋습니다. 최상의 기능이 없기 때문에 호출은 컴파일러에 의해 모호한 것으로 표시됩니다.

    오버로드된 연산자, 생성자 및 변환기의 선언을 포함하는 클래스 인터페이스를 디자인할 때는 매우 주의해야 합니다. 사용자 정의 변환은 컴파일러에 의해 암시적으로 적용됩니다. 이로 인해 기본 제공 연산자는 클래스 유형 피연산자가 있는 연산자의 오버로드 해결에서 탄력적일 수 있습니다.

    운동 15.17

    클래스 유형의 피연산자로 연산자 오버로딩을 해결하는 데 고려되는 후보 함수 집합 5개를 말하십시오.

    운동 15.18

    어떤 operator+()가 main()에서 가장 성능이 좋은 덧셈 연산자로 선택됩니까? 모든 후보 함수, 설정된 모든 함수, 설정된 각 함수의 인수에 적용할 유형 변환을 나열합니다.

    네임스페이스 NS ( class complex ( complex(double); // ... ); class LongDouble ( friend LongDouble operator+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); operator double() ; LongDouble operator+(const complex &); // ... ); LongDouble 연산자

    연산자 오버로딩 기본

    다른 프로그래밍 언어와 마찬가지로 C#에는 기본 제공 형식에 대한 기본 작업을 수행하는 데 사용되는 토큰 집합이 있습니다. 예를 들어, + 연산을 두 정수에 적용하여 합을 제공할 수 있음을 알고 있습니다.

    // 정수로 + 연산. 정수 = 100; 정수 b = 240; 정수 c = a + b; //s는 이제 340입니다.

    여기에 새로운 것은 없지만 대부분의 기본 제공 C# 데이터 형식에 동일한 + 연산을 적용할 수 있다고 생각한 적이 있습니까? 예를 들어 다음 코드를 고려하십시오.

    // 문자열을 사용한 + 연산. 문자열 si = "안녕하세요"; 문자열 s2 = "세계!"; 문자열 s3 = si + s2; // s3에는 이제 "Hello world!"가 포함됩니다.

    기본적으로 + 연산의 기능은 표시된 데이터 유형(이 경우 문자열 또는 정수)을 기반으로 합니다. + 연산자가 숫자 유형에 적용되면 피연산자의 산술 합계를 얻습니다. 그러나 문자열 유형에 동일한 작업을 적용하면 문자열 연결이 발생합니다.

    C# 언어는 동일한 기본 토큰 집합(예: + 연산자)에도 고유하게 응답하는 특수 클래스 및 구조를 빌드하는 기능을 제공합니다. 절대적으로 모든 기본 제공 C# 연산자는 오버로드될 수 없습니다. 다음 표에서는 주요 작업의 오버로딩 기능에 대해 설명합니다.

    작업 C# 과부하 기능
    +, -, !, ++, --, 참, 거짓 이 단항 연산 집합은 오버로드될 수 있습니다.
    +, -, *, /, %, &, |, ^, > 이러한 이진 연산은 오버로드될 수 있습니다.
    ==, !=, <, >, <=, >= 이러한 비교 연산자는 오버로드될 수 있습니다. C#에서는 "like" 연산자(예:< и >, <= и >=, == 및 !=)
    작업을 오버로드할 수 없습니다. 그러나 인덱서는 유사한 기능을 제공합니다.
    () () 연산자는 오버로드할 수 없습니다. 단, 특별한 변환 방법을 통해 동일한 기능을 제공합니다.
    +=, -=, *=, /=, %=, &=, |=, ^=, >= 약식 할당 연산자는 오버로드할 수 없습니다. 그러나 적절한 이진 연산을 오버로드하여 자동으로 가져옵니다.

    연산자 오버로딩은 메서드 오버로딩과 밀접한 관련이 있습니다. 연산자는 키워드로 오버로드됩니다. 운영자연산자 메서드를 정의하고 해당 클래스에 대한 연산자 동작을 정의합니다. 연산자 메서드(연산자)에는 두 가지 형식이 있습니다. 하나는 단항 연산자용이고 다른 하나는 이진 연산자용입니다. 다음은 이러한 방법의 각 변형에 대한 일반적인 형식입니다.

    // 단항 연산자 오버로드의 일반적인 형태. public static return_type operator op(operand parameter_type) ( // operations ) // 이진 연산자 오버로딩의 일반적인 형태. public static return_type operator op(param_type1 operand1, parameter_type2 operand2) ( // operations )

    여기서 op는 + 또는 /와 같은 오버로드된 연산자로 대체되며, 반환 유형지정된 작업에서 반환된 특정 유형의 값을 나타냅니다. 이 값은 모든 유형이 될 수 있지만 연산자가 오버로드되는 클래스와 동일한 유형으로 지정되는 경우가 많습니다. 이 상관 관계를 통해 표현식에서 오버로드된 연산자를 더 쉽게 사용할 수 있습니다. 단항 연산자의 경우 피연산자전송된 피연산자를 나타내며 이항 연산자의 경우 동일하게 표시됩니다. 피연산자1그리고 피연산자2. 연산자 메서드에는 공용 및 정적 형식 지정자가 모두 있어야 합니다.

    이진 연산자 오버로딩

    가장 간단한 예를 사용하여 이항 연산자 오버로딩의 사용을 살펴보겠습니다.

    시스템 사용; System.Collections.Generic 사용; System.Linq를 사용하여; System.Text 사용; namespace ConsoleApplication1 ( class MyArr ( // 3차원 공간에서 한 점의 좌표 public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.x = x; y = y; this.z = z; ) // 이진 연산자 오버로드 + 공용 정적 MyArr 연산자 +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // 이항 연산자 오버로드 - public static MyArr operator -(MyArr obj1, MyArr obj2) ( MyArr arr = 새로운 MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; 반환 arr; ) ) 클래스 프로그램(정적 void Main(문자열 인수) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("첫 번째 점 좌표: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("두 번째 점 좌표: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); ) ) )

    단항 연산자 오버로딩

    단항 연산자는 이진 연산자와 같은 방식으로 오버로드됩니다. 주요 차이점은 물론 피연산자가 하나만 있다는 것입니다. ++, --, - 연산자의 오버로드를 추가하여 이전 예제를 현대화해 보겠습니다.

    시스템 사용; System.Collections.Generic 사용; System.Linq를 사용하여; System.Text 사용; namespace ConsoleApplication1 ( class MyArr ( // 3차원 공간에서 한 점의 좌표 public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.x = x; y = y; this.z = z; ) // 이진 연산자 오버로드 + 공용 정적 MyArr 연산자 +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // 이항 연산자 오버로드 - public static MyArr operator -(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; ) // 단항 오버로드 연산자 - 공개 정적 MyArr 연산자 -(MyArr obj1) ( MyArr arr = new MyArr(), arr.x = -obj1.x, arr.y = -obj1.y, arr.z = -obj1.z, 반환 arr, ) // 단항 연산자 오버로딩 ++ public static MyArr operator ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // 단항 오버로딩 연산자 -- 공개 c 정적 MyArr 연산자 --(MyArr obj1) ( obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; obj1을 반환합니다. ) ) 클래스 Program ( static void Main(string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("첫 번째 점 좌표: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("두 번째 점의 좌표: " + Point2.x + " " + Point2.y + " " + Point2. z + "\n"), MyArr Point3 = Point1 + Point2, Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z), Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z), Point2++, Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z), Point2--; 콘솔.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); ) ) )

    많은 프로그래밍 언어는 연산자를 사용합니다. 최소한 할당(= , := 또는 유사) 및 산술 연산자(+ , - , * 및 /). 대부분의 정적으로 유형이 지정된 언어에서 이러한 연산자는 유형에 바인딩됩니다. 예를 들어, Java에서 + 연산자를 사용한 추가는 정수, 부동 소수점 숫자 및 문자열에만 가능합니다. 행렬과 같은 수학적 객체에 대해 자체 클래스를 정의하면 이를 추가하는 메서드를 구현할 수 있지만 다음과 같이 호출할 수만 있습니다. a = b.add(c) .

    C++에는 그러한 제한이 없습니다. 거의 모든 알려진 연산자를 오버로드할 수 있습니다. 가능성은 무한합니다. 피연산자 유형의 조합을 선택할 수 있지만 유일한 제한은 사용자 정의 유형 피연산자가 하나 이상 있어야 한다는 것입니다. 즉, 기본 제공 유형에 대해 새 연산자를 정의하거나 기존 유형을 다시 작성합니다. 그것은 금지되어 있습니다.

    연산자를 언제 오버로드해야 합니까?

    중요한 사실을 기억하십시오. 의미가 있는 경우에만 연산자를 오버로드하십시오. 즉, 과부하의 의미가 분명하고 숨겨진 놀라움을 수반하지 않는 경우입니다. 오버로드된 연산자는 기본 버전과 동일하게 작동해야 합니다. 당연히 예외는 허용되지만 이해할 수있는 설명이 수반되는 경우에만 허용됩니다. 좋은 예는 연산자입니다.<< и >> 표준 라이브러리 iostream , 분명히 정상 과 다르게 작동 합니다 .

    다음은 연산자 오버로딩의 좋은 예와 나쁜 예입니다. 위의 행렬 추가는 예시적인 경우입니다. 여기서 더하기 연산자를 오버로드하는 것은 직관적이며 올바르게 구현된 경우 자명합니다.

    행렬 a, b; 행렬 c = a + b;

    잘못된 덧셈 연산자 오버로드의 예는 게임에 두 개의 플레이어 개체를 추가하는 것입니다. 클래스 작성자는 무엇을 의미 했습니까? 결과는 어떻게 될까요? 우리는 그 연산이 무엇을 하는지 모르기 때문에 이 연산자를 사용하는 것은 위험합니다.

    연산자를 어떻게 오버로드합니까?

    연산자 오버로딩은 특수 이름을 사용한 함수 오버로딩과 유사합니다. 실제로 컴파일러는 연산자와 사용자 정의 형식이 포함된 식을 보면 해당 식을 오버로드된 연산자의 적절한 함수에 대한 호출로 바꿉니다. 대부분의 이름은 키워드 operator로 시작하고 그 뒤에 연산자 이름이 옵니다. 지정이 특수 문자로 구성되지 않은 경우, 예를 들어 캐스트 연산자 또는 메모리 관리(new, delete 등)의 경우, 단어 operator와 연산자 지정은 공백(연산자 new)으로 구분되어야 합니다. 그렇지 않으면 공백을 무시할 수 있습니다(operator+ ).

    대부분의 연산자는 클래스 메서드와 간단한 기능, 하지만 몇 가지 예외가 있습니다. 오버로드된 연산자가 클래스 메서드인 경우 첫 번째 피연산자의 형식은 해당 클래스(항상 *this)여야 하고 두 번째 피연산자는 매개변수 목록에 선언되어야 합니다. 또한 메서드 문은 메모리 관리 문을 제외하고 정적이 아닙니다.

    클래스 메서드에서 연산자를 오버로드하면 클래스의 private 필드에 액세스할 수 있지만 첫 번째 인수의 숨겨진 변환은 사용할 수 없습니다. 따라서 이진 함수는 일반적으로 자유 함수로 오버로드됩니다. 예시:

    클래스 Rational ( public: //생성자는 int에서 암시적 변환에 사용할 수 있습니다. Rational(int numerator, int denominator = 1); Rational operator+(Rational const& rhs) const; ); int main() ( 유리 a, b, c; int i; a = b + c; //ok, 변환이 필요하지 않음 a = b + i; //ok, 두 번째 인수의 암시적 변환 a = i + c; //오류: 첫 번째 인수는 암시적으로 변환할 수 없음)

    단항 연산자가 자유 함수로 오버로드되면 숨겨진 인수 변환을 사용할 수 있지만 일반적으로 사용되지 않습니다. 반면에 이 속성은 이항 연산자에 필요합니다. 따라서 주요 조언은 다음과 같습니다.

    "와 같은 단항 연산자와 이항 연산자를 구현하십시오. 엑스="는 클래스 메서드로, 다른 이진 연산자는 자유 함수로 사용합니다.

    어떤 연산자가 오버로드될 수 있습니까?

    다음 예외 및 제한 사항에 따라 거의 모든 C++ 연산자를 오버로드할 수 있습니다.

    • operator** 와 같은 새 연산자는 정의할 수 없습니다.
    • 다음 연산자는 오버로드할 수 없습니다.
      1. ?: (삼항 연산자);
      2. ::(중첩된 이름에 대한 액세스);
      3. . (필드에 대한 액세스);
      4. .* (포인터에 의한 필드 접근);
      5. sizeof , typeid 및 캐스트 연산자.
    • 다음 연산자는 메서드로만 오버로드할 수 있습니다.
      1. = (과제);
      2. -> (포인터에 의한 필드 접근);
      3. () (함수 호출);
      4. (인덱스에 의한 접근);
      5. ->* (포인터에 의해 필드에 대한 포인터 접근);
      6. 변환 및 메모리 관리 연산자.
    • 피연산자의 수, 실행 순서 및 연산자의 연관성은 표준 버전에 따라 결정됩니다.
    • 최소한 하나의 피연산자는 사용자 정의 유형이어야 합니다. Typedef는 계산되지 않습니다.

    다음 부분에서는 그룹 및 개별적으로 C++ 오버로드된 연산자를 소개합니다. 각 섹션은 의미론으로 특징지어집니다. 예상되는 행동. 또한 연산자를 선언하고 구현하는 일반적인 방법을 보여줍니다.