Operator Overloading
Is a specific case of polymorphism in which some or all operators like+, = or == are treated as polymorphic functions and as such have different behaviors depending on the types of its arguments. Instead of writing:add (a, multiply (b,c));
You can simply writea + b * c;
The way to overaload a oprator (unary or binary) is (where @ represents a valid operator):return_type operator@(argument_list)
{
// ... definition
}
For example we have the following definition for our own array type.arr.set(5, 19); // Element 5 gets value 19
int a = arr.get(7);
And now we want the following usage that we are used to.arr[5] = 19;
int a = arr[7];
Then we would need to overload the []-operator like this:// DynArray.h
class DynArray
{
public:
double & operator[](int index) { return array[index]; }
double operator[](int index) const { return array[index]; }
private:
double * array;
int bufSize;
};
The reason for having two operator overloads is that we could create a method in our class DynArray later that would take another DynArray as parameter which is marked as const. Then we could not use methods that aren´t const methods. We would need this function then and the compiler would know we refer to this method when we index the const array.Binary operators
Ex:+, -, *, /, +=, *=, ==, `!= etc.DynArray operator+(const DynArray & arr) const;
Both the parameter and the object for the operation are declared as const. We make the addition to a third array and return that array. To save unecessary copies and memory we pass the parameter by "call-by-reference".DynArray a,b,c;
c = a + b;
c = a.operator+(b); // both are equivalent
If you don´t want to return a new object but modify the existing object (*this).const DynArray & operator*=(const double scale);
But why return a const DynArray? Well the reason could be that you simply dont want to enable modification of the result of the computation. With the declaration above the following is not possible. (v1 *= 3) = v2; // Not possible
v1 = (v2 *= 5); // Possible
A best practice when deciding wheather to define a function or method is if the operator is changing the current object or not.DynArray operator+=(const DynArray & arr) const; //method
DynArray operator+(const DynArray & arr1, //func
const DynArray & arr2);
friend functions are functions declared to be friends of the class and can therefore access private variables and so on, but their definitions would be outside of the class.class DynArray
{
public:
double & operator[](int index) { return array[index]; }
double operator[](int index) const { return array[index]; }
friend int sum (const DynArray&);
private:
double * array;
int bufSize;
};
//main.cpp
int main()
{
DynArray arr;
...
...
sum(arr);
}
int sum (const DynArray& param)
{
int sum = 0;
for(int i = 0; i < param.bufSize; i++) // bufsize is private
sum += param[i];
return sum;
}
Unary operator
Writing unary operators is, thankfully, a little bit easier. Here we use the increment and decrement functions as examples. If you have methods where order of execution is important like postfix or prefix the way of doing it is by adding a dummy parameter to the postfix method (int).DynArray operator++(); // prefix
DynArray operator++(int); // postfix
Assignment operator
This one is a little bit special. The reason is that sometimes the inner variables of the object cannot so easily be copied.DynArray objekt1, objekt2;
.....
objekt1 = objekt2; //makes a shallow copy
Here the inner array of object1 is a pointer to an array, this pointer will be copied over to object2. This is definitively not what we want. To make our own assignment operator we write:const DynArray & DynArray::operator=( const DynArray &a);
//implementation
const DynArray & DynArray::operator=(const DynArray &arr)
{
if (this != &arr) // allow self-assignment: a = a;
{
bufSize = arr.bufSize;
delete[] array;
array = new double[bufSize];
for(int i = 0; i < bufSize; i++)
{
array[i] = arr.array[i];
}
}
return *this;
}
The reason for returning *this is that you want to allow chained assignments objekt1 = objekt2 = objekt3;.The copy constructor
There is a built in copy constructor in C++ for all objects, but this too has the same defect as the assignment operator, its shallow. This is constructor is usually called when:- initilizing objects with an other intance of the same class.
- when passing arguments to functions without reference (call-by-value).
- when returning objects from functions.
int main()
{
string a = "Mickey", b = "Mouse";
string Mickey(a); // 1. copy constructor called
string Mouse = b; // assignment operator
a = add(Mickey, Mouse); // 2. copy constructor called
}
string add( string a, string b)
{
string temp = a+b;
return temp; // 3. copy constructor called
}
Resolve for our array.DynArray( const DynArray & a );
// defined copyconstructor (NOT "const DynArray a" => infinite loop)
int main()
{
...
DynArray arr1; // default.constr.
DynArray arr2( 2, -99 ); // overloaded.
DynArray arr3(arr2); // copy.constr.
DynArray arr4 = arr2; // copy.constr.
...
}
The problem of not having one copy constructor can be quite hideous. Look at the following example.int sum( DynArray a, int num)
{
int sum = 0;
for (int i=0; i < num; i++)
sum += a[i];
return sum;
}
int main()
{
DynArray array;
... // put values
cout << sum(array, nrOfElem);
... // do something else
}
Seems harmless, right. The parameter a in sum copies the array object. If you haven´t defined your own assignment operator then it will use the built in shallow copy operator which means that the private array pointer in the object will be copied too. Here comes the problem; the destructor of the copied object is called when sum ends at }. This means that everything will be destoryed and so the original array will not exist anymore either!When passing parameters by reference to functions or constructors, be VERY careful about const correctness. Pass by non-const reference ONLY if the function will modify the parameter and it is the intent to change the caller's copy of the data, otherwise pass by const reference. There is a small clause in the C++ standard that says that non-const references cannot bind to temporary objects. A temporary object is an instance of an object that does not have a variable name.
std::string( "temporary string" ); // temporary
std::string s("object string"); // non temporary
Usually when you dont want outer classes or functions to use the copy constructor or assignment operator you could declare these as private to simply avoid these issues.Streams
A 'stream' is internally nothing but a series of characters. Streams work with built-in data types, and you can make user-defined types work with streams by overloading the insertion operator<< to put objects into streams, and the extraction operator >> to read objects from streams. For example to output "hello world" you write:cout << "hello world" << endl;
Here cout is an instance of ostream. And for writing to a file you would use an ofstream:ofstream file( "test.txt" );
if ( !file ) {
cout << "An error occurred opening the file" << endl;
} else {
file << "text written to the file: test.txt";
}
And if you for example are reading input from a istream:int x;
if ( cin >> x ) {
cout << "Please enter a valid number" << endl;
}
And if you would like to copy the contents of a file to another:ifstream infile("src");
if (infile)
{
ofstream outfile("destination");
char c;
while (infile.get(c))
outfile << c;
}
And if you instead would like to read from a file you would instead use an ifstream and the >> operator. Here is a picture of the hierarchy.
The underlying low-level interface that corresponds to the actual medium is a character buffer (technically called the streambuf). Being a buffer, it does not hold the entire content of the stream (if the stream is large enough) so you can't use it for random access (but we will later see an example of an oddity of a stream that DO HAVE random access).
First, the stream is initialized with the appropriate type (like a std::string for a stringstream and the filename for an fstream) of values and suitable modes (like ios::in for input and ios::out for output and many more depending on the type of the stream). After that, you can specify where the I/O should occur, through the get and put pointers.
seekg(0); seekg(0,ios::beg)//sets the get pointer to the beginning.seekg(5,ios::beg)//sets the get pointer to 5 chars forward of the beginning.tellp(); tellg()//returns the current value of the put/get pointerseekp(-10,ios::end)//sets the put pointer to 10 chars before the endseekp(1,ios::cur)//proceeds to next char
So when you want your class to be able to read input streams or write to outputstreams you always do it the same way. This is the beuty of streams, its always the same interface. If you want for example a Loggclass to be able to output to whatever storage you would need to only overload the
<< operator and take a ostream as parameter.class LogStatement
{
public:
LogStatement(std::string s): data(s), time_string( timestamp() ) {};
//This method handles all the outputs.
friend ostream& operator<<(ostream&, const LogStatement&);
private:
std::string data;
std::string time_string;
};
ostream& operator<<(ostream& ost, const LogStatement& ls)
{
ost<<"~|"<<ls.time_string<<'|'<<ls.data<<"|~";
return ost;
}
A thing to notice is that the << operator takes in an ostream object, modifies it and then returns it. This is an expected behaviour so that all streams can be called and chained together:cout<<"Hey, I'm "<< n <<" years old.";
Remember we said that you can´t use streams for random access since the underlying buffer is hidden in the interface. Well there is a class of streams called random streams (rsteam) that allow the user to access this buffer. We will look at an example of rsteam called fstream that allow random access to a file. It is done by letting the fstream class be a hybrid between ifstream and ofstream and thus be able to do both input and output:#include <iostream>
class testclass {
double a[2];
int b;
public:
testclass( void ) { a[0] = a[1] = 0.0; b = 0; }
testclass( double a1, double a2, int i)
{ a[0] = a1; a[1] = a2; b = i; }
friend ostream& operator<< ( ostream &os, const testclass &tc)
{
return os << "{ " << tc.a[0] << ", " <<
tc.a[1] << ", " << tc.b << " }";
}
};
main()
{
fstream<testclass> f1("testclass.data");
fstream<int> f2("int.data");
testclass t1(0.0, 1.0, 1);
testclass t2(0.0, 2.0, 2);
testclass t3(0.0, 3.0, 3);
f1(2,t1); f1(1,t2); f1(0,t3); // index the underlying buffer
cout << f1(0) << endl << f1(1) << endl << f1(2) << endl;
f2(12,1200);
f2(6,600);
f2(0,0);
cout << f2(0) << endl << f2(6) << endl << f2(12) << endl;
}
The output of this program would be:{ 0, 3, 3 }
{ 0, 2, 2 }
{ 0, 1, 1 }
0
600
1200
Inga kommentarer:
Skicka en kommentar