An interesting feature of the C++ language is that it supports operator overloading in addition to function overloading because operators are considered functions in C++. For example, an addition expression of a + b can also be expressed as a function: operator + (a, b). Operator + represents the addition function. Expressions in high-level languages are very similar to mathematical expressions, and to some extent it is easier to understand and read expressions by operators than by functions. In general, overloading the implementation of an operator should be similar to the mathematical representation of the operator itself, but you can also completely implement a function that has nothing to do with the operator itself or vice versa (such as subtracting a + operator). Operator functions can be classified as class operators and ordinary operators, just like the member functions of a class and ordinary functions. To define an operator function is always defined and declared in the following format:
Return type operator operator (parameter type 1 [, parameter type 2] [, parameter type 3] [, parameter type N]);Copy the code
Operator overloading requires that the operator be preceded by the keyword operator. In general, the number of arguments should not exceed two because operators are mostly unary or binary, and only the function operator () and the new and delete operators support more than two arguments.
The types of operators that can be overloaded
Not all operators in C++ support overloading, and we cannot create new operators (such as sigma). Some operators can only be overloaded as class member functions, while others can only be used as normal functions.
- The operators that cannot be overloaded are:.. * ::? : sizeof
- The operators that can only be overloaded as class member functions are :() [] -> =
I’ll go through the various methods of operator overloading in detail below. At the same time, in order to show more universality, THE definition of parameter types on my side adopts the form of template, and gives some general implementation logic of operators. In practice, overloading needs to be defined and declared according to the specific type.
1. Stream operators
describe | value |
---|---|
Operator type | >> << |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | binary |
The return type | Lvalue reference |
The stream operator is a C++ specific operator. The iostream class in the C++ standard library supports stream operators and provides two types of operators, read stream >> and insert stream <<. These operators are used for input and output operations respectively, and can be consecutively input and output. And for ordinary functions the first argument must also be a reference type. The following example illustrates how to declare and define a convection operator:
// The normal stream operator function template
template<class LeftType.class RightType>
LeftType& operator << (LeftType& left, const RightType& right)
{
//...
return left;
}
template<class LeftType, class RightType>
LeftType& operator >> (LeftType& left.RightType& right)
{
/ /...
return left;
}
// Class member functions
class CA
{
public:
template<class RightType>
CA& operator << (const RightType& right)
{
//...
return *this;
}
template<class RightType>
CA& operator >>(RightType& right)
{
/ /...
return *this; }};Copy the code
As can be seen from the above example:
- The return of a stream operator is always a reference type, so that the return value can be an lvalue and perform a continuous stream operation.
- For the input stream operator >> we require that the arguments on the right hand side be referential because the input stream modifies the contents of the argument variable on the right hand side. If the parameter on the right is a normal value type, the input will not be changed. Of course, the right parameter type can be set to a pointer in addition to a reference.
- In the case of the output stream operator <<, because it does not change the content of the right-hand argument, we recommend that the right-hand argument type be a constant reference type. This is to prevent internal modification of the right-hand argument and the creation of a copy of the data or redundant calls to construct the copy function.
- In general, the flow operator can be overloaded as a normal function or as a class member function. The difference is that ordinary functions cannot access the class’s private variables. Of course, the solution is to set the ordinary function as a friend function of the class.
2. Arithmetic expression operators
describe | value |
---|---|
Operator type | + – * / % ^ & | ~ > > < < |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | Everything is binary except ~ is one yuan |
The return type | Common value type |
Arithmetic expression is the most common mathematical symbols, respectively defined above is a plus (+) and minus (-), multiply (*), in addition to more than (/), take (%), or (^), and (&), or (|), not (~), arithmetic moves to the right (> >), logic shift to the left (< <) several operators. All operators except the ~ operator are binary operators, and the result of the operation is independent of the original value and cannot be an lvalue reference. Here is some example code for these operator overloads:
// Ordinary arithmetic operator function template
template<class ReturnType.class LeftType.class RightType>
ReturnType operator + (const LeftType& left.const RightType& right)
{
/ /...
returnReturn a value of type ReturnType}// The inverse operator is a unary operator.
template<class ReturnType, class LeftType>
ReturnType operator~ (const LeftType& left)
{
/ /...
returnReturn a value of type ReturnType}// Class member functions
class CA
{
public:
template<class ReturnType, class RightType>
ReturnType operator + (const RightType& right) const
{
/ /...
returnA new ReturnType object. }// The inverse operator is a unary operator.
template<class ReturnType>
ReturnType operator ~ () const
{
/ /...
returnA new ReturnType object. }};Copy the code
As can be seen from the above example:
- Functions return normal types instead of reference types because the result computed by these operators is not the same object as the input data but a temporary object, and therefore cannot return a reference type, that is, cannot be used as an lvalue.
- Because the returned value and the input parameter are different objects, the input parameters in the function are represented by constant references, so that the data cannot be modified and the construction of copies is reduced.
- The return type of a function can be different from the type of its input parameter, but in practice it is best to keep the types of all parameters the same.
- All are binary except the ~ operator, which is unary. You can see the difference between the unary and binary operators in the example above.
- The << and >> above denote displacement operations rather than flow operations, respectively. So you can see that we can completely customize the meaning of the operator, that is, the result of the implementation can be completely different from the meaning of the real mathematical operator.
3. Arithmetic assignment expression operators
describe | value |
---|---|
Operator type | + = – = * = / = % = ^ = & = | = > > < < = = |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | binary |
The return type | Lvalue reference |
In addition to the function of arithmetic operations mentioned above, arithmetic assignment expressions also have the function of saving results, that is, to save the results of operations. Therefore, the first argument to the operator function must be a reference type, not a constant, and the return type must be the same as that of the first argument. The following example illustrates how operators can be declared and defined:
// The normal operator function template
template<class LeftType.class RightType>
LeftType& operator+ = (LeftType& left.const RightType& right)
{
/ /...
return left;
}
// Class member functions
class CA
{
public:
template<class RightType>
CA& operator+ = (const RightType& right)
{
/ /...
return *this;
}
template<class RightType>
CA& operator+ = (RightType& right)
{
/ /...
return *this; }};Copy the code
As can be seen from the above example:
- The arithmetic assignment operator always returns a reference type, consistent with the type of the argument to the left of the operator.
- The right side of the function does not change the content of the right side of the parameter, so we recommend that the right side of the parameter type is constant reference type, in order to prevent the inside of the function to modify the right side of the parameter and create a copy of the data or create redundant calls to construct copy function.
4. Compare operators
describe | value |
---|---|
Operator type | = =! = < > < = > = && | |! |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | In addition to the! Everything else is binary |
The return type | bool |
The comparison operator is used primarily for logical judgment and returns a value of type bool. These operators do not change the content of the data, so the arguments are set to constant references. The following example illustrates how operators can be declared and defined:
// Ordinary arithmetic operator function template
template<class LeftType.class RightType>
bool operator= = (const LeftType& left.const RightType& right)
{
/ /...
return true or false
}
// The nonoperator is a unary operator.
template<class LeftType>
bool operator! (const LeftType& left)
{
/ /...
return true or false
}
// Class member functions
class CA
{
public:
template<class RightType>
bool operator= = (const RightType& right) const
{
/ /...
return true or false
}
// The inverse operator is a unary operator.
bool operator! (a)const
{
/ /...
return true or false}};Copy the code
As can be seen from the above example:
- Conditional operators generally return a fixed bool, and since they do not change the value of the data, both arguments and member functions are decorated with constants.
5. Increment and decrement operators
describe | value |
---|---|
Operator type | ++ — |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | One yuan |
The return type | Common types, and lvalue references |
The increment and decrement operators are unary and change their contents, so the left-hand argument cannot be a constant but only a reference type. And because increment comes in the form of suffix I ++ and prefix ++ I (the same goes for decrement, which is just an example of increment). The value returned by suffix increment cannot be an lvalue whereas the value returned by prefix increment can be an lvalue. To distinguish the prefix increment from the prefix increment, the system specifies that the prefix increment operator function is marked by an int. The following example illustrates how operators can be declared and defined:
// The normal function operator function template
//++i
template<class LeftType>
LeftType& operator+ + (LeftType& left.int)
{
/ /...
return left
}
//i++
template<class LeftType>
LeftType operator ++ (LeftType& left)
{
/ /...
returnNew LeftType value}// Class member functions
class CA
{
public:
CA& operator+ + (int)
{
/ /...
return *this;
}
CA operator+ + () {/ /...
returnNew CA type value}};Copy the code
As can be seen from the above function definition:
- Arguments to incrementing and decrement functions, as well as return values and function modifiers, cannot have const constant modifiers.
- Prefix increment returns a reference type that can be an lvalue, while suffix increment returns a value type that cannot be an lvalue.
- Arguments with an int declare prefix increment and those without an int declare suffix increment.
6. Assignment operators
describe | value |
---|---|
Operator type | = |
Whether class members are supported | YES |
Whether ordinary functions are supported | NO |
The operation unit | binary |
The return type | Lvalue reference |
Assignment operators can only be used in member functions of a class and not in ordinary functions. Assignment operator overloading is designed to solve the problem of deep copies of objects. We know that the default mechanism for object assignment in C++ is to make a byte by byte copy of the object’s memory data. This copy is fine for objects that have only value data members, but it may cause duplicate memory free problems if the object holds pointer data members. Such as the following code snippet:
class CA
{
public:
int *m_a;
~CA(){ delete m_a;}
};
void main(a)
{
CA a, b;
a.m_a = new int;
b = a; // Perform assignment here, but there is a danger!
}
Copy the code
As you can see from the above code, when the destructor of objects A and B ends its life cycle, the m_A of the data member is freed, but because of our default object assignment mechanism, the memory is freed twice, resulting in a crash. So in this case we need to overload the assignment operator of the class to solve the problem of shallow copy of the object. In the above case, in addition to overloading the assignment operator for a class, we also create a copy constructor for that class. There is a famous three-way rule for constructing classes:
If a class needs any of the following three member functions, it needs to implement all three: copy constructors, assignment operators, and destructors. In practice, many classes simply follow the “sophomore rule”, which means that assignment operators are fine as long as copy constructs are implemented, and destructors are not always required.
The purpose of implementing the principle of three is to solve the problem of deep copy and to solve the problem that the memory of some data members in the object is created by heap allocation. Copy constructor here the implementation of the general and the realization of the assignment operator, the difference is that a copy constructor usually used in the building of a object in the scene, the transfer function parameters such as the type of an object as well as the value of the type of object returned will be called copy structure, and the assignment operator is used for object to establish the assignment again after update. For example:
class CA
{
/ /...
};
CA foo(CA a)
{
return a;
}
void main(a)
{
CA a, c; // constructor
CA b = foo(a); // COPY construct will be called when A passes to Foo, and foo will call copy construct when it returns data to B, even if the assignment operator is present.
c = b; // The assignment operator
}
Copy the code
In the above code you can clearly see the timing and differences between constructor, copy constructor, assignment operator function calls. Let’s define the assignment operator and the rule of three:
class CA
{
public:
CA(){} // constructor
CA(const CA& other){} // Copy construct
CA& operator= (const CA& other) // Assignment operator overload
{
/ /..
return *this;
}
~CA(){} // destructor
}
Copy the code
As can be seen from the above definition:
- The assignment operator requires that the reference type of the class be returned, because the result of the assignment can be lvalue referenced.
- The assignment operator function argument is a constant reference indicating that the value of the input parameter is not modified.
7. Subscript index operator
describe | value |
---|---|
Operator type | [] |
Whether class members are supported | YES |
Whether ordinary functions are supported | NO |
The operation unit | binary |
The return type | reference |
We know that we can read and set the value of an element in an array by subscripting it. For example:
int array[10] = {0};
int a = array[0];
array[0] = 10;
Copy the code
In practice, some of our classes also have the property of collections, and we also want to get the data elements in this collection class by subscript. To solve this problem, we can implement the subscript index operator in the class. This operator can only be defined in a class, and the index subscript is usually an integer, but you can define it as a dictionary or mapping table. The specific code is as follows:
class CA { public: Template <class ReturnType, class IndexType> const ReturnType& operator [](IndexType index) const {returnareturnTemplate <class ReturnType, class IndexType> ReturnType& operator[](IndexType index) {returnareturnType reference}}Copy the code
As can be seen from the above code:
- Two functions are defined here: the former reads subscripts for constant collection objects and the latter reads and writes subscripts for nonconstant collection objects.
- The purpose of returning a reference type instead of a value type is to reduce unnecessary memory copying due to reading. Write operations must use reference types.
8. Type conversion operators
describe | value |
---|---|
Operator type | Various data types |
Whether class members are supported | YES |
Whether ordinary functions are supported | NO |
The operation unit | One yuan |
The return type | Various data types |
In practice, some of our methods or functions only accept certain types of arguments. For a class, if the object of the class is not of that particular type, the object cannot be passed as a parameter. To solve this problem, we must build a special type conversion function for the class to solve this problem. For example:
void foo(int a){
cout << a << endl;
}
class CA
{
private:
int m_a;
public:
CA(int a):m_a(a){}
int toInt(a)
{
returnm_a; }};void main(a)
{
CA a(10);
foo(a); // wrong!!! A is a CA type, not an integer.
foo(a.toInt()); // ok!!
}
Copy the code
As you can see, in order for argument passing to work, the CA class must create a new function toInt to get the integer and pass it to Foo. The type conversion operator can solve this problem in a more convenient and readable form. By overloading the type conversion operator, our code does not need to use extra functions to complete the parameter passing, but to pass the parameter directly. Conversion operator overloading is an adaptor pattern that allows conversion and transfer of different types of data in the form of conversion operators. Type conversion operator overloading is defined as follows:
class CA
{
public:
template<class Type>
operator Type()
{
returnType Indicates the Type of data. }};Copy the code
As you can see from the code above:
- Conversion operator overloading does not specify the return type, nor does it need to specify any other input parameter, but only the conversion type as the operator.
- Type conversion operator overloading can be used for any data type. It is easy to solve this type mismatch problem by using type conversion operator overloading.
class CA
{
private:
int m_a;
public:
CA(int a):m_a(a){}
operator int(a)
{
returnm_a; }};void main(a)
{
CA a(10);
foo(a); // ok! During parameter passing, A calls the conversion operator to convert the type.
}
Copy the code
9. Function operators
describe | value |
---|---|
Operator type | (a) |
Whether class members are supported | YES |
Whether ordinary functions are supported | NO |
The operation unit | N yuan |
The return type | any |
Function operators are used heavily in algorithms in the STL. Function operators can be understood as C++ support and implementation of closures. We can use an object as a normal function by using a function operator. This means that we can pass an object in some method that takes the address of the function as an argument, as long as the class implements the function operator and its parameter signature matches the signature of the received function parameter. Let’s start with the following code:
// Define a template fn that can accept normal functions as well as objects that implement function operators
template<class fn>
void foo2(int a.fn pfn)
{
int ret = pfn(a);
std: :cout << ret << std: :endl;
}
int foo1(int arg)
{
return arg + 1;
}
class CA
{
private:
int m_a;
public:
CA(int a):m_a(a){}
// Define a function operator
int operator(a)(int arg)
{
return arg + m_a;
}
// Define another function operator
void operator(a)(int arg1, int arg2)
{
std: :cout << arg1 + arg2 + m_a << std: :endl; }};void main(a)
{
foo2(10, &foo1); // Ordinary functions are passed as arguments.
CA a(20);
foo2(10, a); // Pass the object to foo2 as a normal function.
a(20.30); // The object is used as a normal function.
}
Copy the code
As you can see from the above code, since the CA class implements two function operators, we can use the CA object as a normal function, just like a normal function call. We call the object of a class that implements a function operator a function object. So why should objects provide the power of functions? The answer is that inside the function operator of an object we can access other properties or other member functions that the object itself has, whereas normal functions do not. As the above example illustrates, you can also use data members inside a class’s function operators. Overloading of multiple function operators can be used in a class, and the number of overloaded parameters and return types of the function operators can be fully customized. We know that closures are not supported in C++, but to some extent we can implement this closing-like capability in the form of function operator overloading.
10. Multiple reference operators, address operators, member access operators
describe | value |
---|---|
Operator type | * & – > |
Whether class members are supported | YES |
Whether ordinary functions are supported | Except for * & support, -> is not supported |
The operation unit | 1 yuan |
The return type | any |
In C++ I can use the * operator to evaluate a pointer object, that is, to get the object to which the pointer points. Use the & operator on an object to get the pointer address of the object; For a pointer object we can use the -> operator to access its data members. So the * operator represents the value operator (also known as the multiple reference operator, indirect reference operator), & represents the address-fetch operator, and -> represents the member access operator.
class CA
{
public:
int m_a;
};
void main(a)
{
CA a;
CA *p = &a; // Fetch the address operator
cout << *p << endl; // The value operator
p->m_a = 10; // Member access operator
}
Copy the code
You can see that the main purpose of the above three operators is for pointerdependent, or memory dependent, processing. The purpose of these three operator overloads is mainly for the implementation of smart Pointers and proxies. C++ is also the implementation of certain design patterns at the language level. In programming sometimes we construct a class to, is mainly used for the purpose of this class to another class, in addition to their own some of the ways, all of the other method calls will be delegated to the management class, so that we will implement all the management class in a management class methods, such as the following code example:
class CA
{
public:
void foo1(a);
void foo2(a);
void foo3(a);
};
class CB
{
private:
CA *m_p;
public:
CB(CA*p):m_p(p){}
~CB() { deletem_p; }// Be responsible for destroying objects
CA* getCA(a){ returnm_p; }void foo1(a){ m_p->foo1(); }void foo2(a){m_p->foo2(); }void foo3(a){m_p->foo3();}
};
void fn(CA*p)
{
p->foo1();
}
void main(a)
{
CB b(new CA);
b.foo1();
b.foo2();
b.foo3();
// Since fn only accepts CA types CB provides a method to convert to CA objects.
fn(b.getCA());
}
Copy the code
The above code can be seen that CB class is a CA class management class, he will be responsible for the CA class object life cycle management. In addition to this administration, the CB class implements all the methods of the CA class. This implementation is inefficient when there are many methods in the CA class. How can we solve this problem? The answer is the three operator overloads described in this section. Let’s look at how to implement overloading of these three operators:
class CA
{
public:
void foo1(a);
void foo2(a);
void foo3(a);
};
class CB
{
private:
CA *m_p;
public:
CB(CA*p):m_p(p){}
~CB() { deletem_p; }// Be responsible for destroying objects
public:
// Dereference and address operators are inverse operations
CA& operator* () {return*m_p; } CA*operator& () {returnm_p; }The implementation mechanism of the // member access operator is very similar to that of the & operator
CA* operator- > () {return m_p;}
};
void fn1(CA*p)
{
p->foo1();
}
void fn2(CA&r)
{
r.foo2();
}
void main(a)
{
CB b(new CA);
b->foo1();
b->foo2(); // These two call the -> operator overload
fn1(&b); // Call the & operator overload
fn2(*b); // Call the * operator overload
}
Copy the code
As you can see from the above code, the implementation of the three operators does not need to rewrite the implementation of foo1-foo3 in CB class, and we do not need to provide a special type conversion method, but can directly convert the CA object through the operator and use it. Of course, a complete smart pointer encapsulation is more than just overloading the three operators. We also need to deal with constructors, copy constructors, assignment operators, type-conversion operators, and destructors. If you want to learn more about smart Pointers, check out the AUTO_ptr class in STL
11. Memory allocation and destruction operators
describe | value |
---|---|
Operator type | new delete |
Whether class members are supported | YES |
Whether ordinary functions are supported | YES |
The operation unit | N yuan |
The return type | New returns a pointer, delete does not |
Yes, you read that correctly. C++ also supports overloading for memory allocation new and memory destruction delete, meaning that new and delete are also operators. By default, new and delete in C++ are allocated and destroyed in the heap. Sometimes we want to customize the allocation of memory for a class, so we need to overload new and delete. If new is overloaded, delete must be overloaded. I would like to have a separate article on allocating and destroying memory to cover this in detail. Here is a simple example of how to implement new and delete overloading:
class CA
{
public:
CA* operator new(size_t t){ returnmalloc(t); } void operator delete(void *p) { free(p); }};Copy the code
For a detailed description of the new and delete operators, see new and delete in C++