Day 22 - 运算过载,warning ! warning !

Outline

  • Motivations(为什麽要做 operation overloading) and prerequisites (基本知识)

    ( overloading 的情境) :

  • Overloading comparison and indexing operators

  • Overloading assignment and self-assignment operators

  • Overloading addition operators


Motivations and prerequisites

Recall MyVector class

class MyVector
{
private:
	int n;
	double* m;
public:
	// constructors
	MyVector();
	MyVector(int dim, double v[]);
	// copy default constructor
	MyVector(const MyVector& v);
	// destructor
	~MyVector();
	// print function
	void print();
};
MyVector::MyVector()
{
	n = 0;
	m = nullptr;
}

MyVector::MyVector(int dim, double v[])
{
	n = dim;
	m = new double[dim];
	for (int i = 0; i < dim; i++)
		m[i] = v[i];

}

MyVector::MyVector(const MyVector& v) // called by reference
{
	n = v.n;
	m = new double[n];
	for (int i = 0; i < n; i++)
		m[i] = v.m[i];
}

MyVector::~MyVector()
{
	delete[] m;
}

void MyVector::print()
{
	cout << "(";
	for (int i = 0; i < n - 1; i++)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

可加入一些 member function:

  • 我们使用一个 member function 来比较 MyVector 中的 objects (也就是向量): (当他们的 dimensions 都相同时)
    • u = v : 每一个 element 都一样
    • u < v : 每一个 element 都符合 (ui < vi) (每一个 u 的 element 都比 v 小)
    • u ≤ v : 每一个 element 都符合 (ui ≤ vi)

在这个例子里,因为是比较两个 vector 之间的大小,所以理论上应该就是 instance function。

class MyVector
{
private:
	int n;
	double* m;
public:
	// constructors
	MyVector();
	MyVector(int dim, double v[]);
	// copy default constructor
	MyVector(const MyVector& v);
	// destructor
	~MyVector();
	// print function
	void print();
	bool isEqual(const MyVector& v); // 传reference 会比较省时 const 要记得
};

bool MyVector::isEqual(const MyVector& v)
{
	if (n != v.n)
		return false;
	else
	{
		for (int i = 0; i < n; i++)
		{
			if (m[i] != v.m[i])
				return false;
		}
	}
	return true;
}

所以可以理解等等 function 的使用就会是 u.isEqual(v) 这样就可以比较两个 vector 了。

使用:

int main() 
{
	double d1[5] = { 1, 2, 3, 4, 5 };
	MyVector a1(5, d1); // (1)

	double d2[4] = { 1, 2, 3, 4 };
	MyVector a2(4, d1); // (2)
	MyVector a3(a1); // (3)

	cout << (a1.isEqual(a2)? "Y" : "N"); // N
	cout << "\n";
	cout << (a1.isEqual(a3) ? "Y" : "N"); // Y
	cout << "\n";
}

在这边,可以看到 isEqual 成功的判断了 a1, a2不同;a1, a3 相同(因为基本上就是直接 deep copy)。

虽然 isEqual 很方便,但是在比较两个变数是不是相等时,我们很常会使用 if(a1 == a2) 这样子,不但方便又快速。

但是问题是,因为 MyVector 是我们自己创造的type,电脑并不知道这个==对於两个 MyVector 要做甚麽事。

因此 ! 我们需要对这个 == 去 define 当他遇到两个 MyVector 时该做甚麽事(这也就叫做 overloading)。

前情提要 Restriction of Overloading :

  • 不是全部的 operator 都可以被 overloaded
  • 不能改变 operand 的个数
  • 不能创造新的 operator

this

当你建立一个 object 时,势必会占据一段 memory space。

而每个 object 都拥有他们自己的位置。

this 是一个指标,储存 object 的地址。

#include<iostream>
using namespace std;

class A
{
private:
	int a;
public:
	void f() { cout << this << "\n"; }
	A* g() { return this; }
};

int main()
{
	A obj;
	cout << &obj << "\n"; // 0053FD0C
	obj.f(); //0053FD0C
	cout << (&obj == obj.g()) << "\n"; // 1 (true)
	return 0;
}

那 this 可以拿来干嘛?

print()

我们原本写的 print() 是这样:

void MyVector::print()
{
	cout << "(";
	for (int i = 0; i < n - 1; i++)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

如果用了 this 可以写成:

void MyVector::print()
{
	cout << "(";
	for (int i = 0; i < this->n - 1; i++)
		cout << this->m[i] << ", ";
	cout << this->m[this->n - 1] << ")\n";
}

看起来好像没差,this-> 的概念,就是去那块记录的地址取出来,其实等同於(*this).n

说了这麽多好像还是没甚麽优点,this 的其中一个好处 :

在一个函数中,你可以同时使用名字相同的 variable & argument

没有 this 时:

MyVector::MyVector(int d, int v[])
{
	n = d;
	for (int i = 0; i < n; i++)
		m[i] = v[i];
}

有了 this 之後:

MyVector::MyVector(int n, int m[])
{
	this->n = n;
	for (int i = 0; i < n; i++)
		this->m[i] = m[i];
}

这样就可以让程序变得更乾净,更容易理解那个 n, m是谁,不会在多很多变数。

!!写程序好习惯!!

  • 只要你写到 instance variables 或是 function 时,一律使用 this->
  • this-> 会让程序变得更乾净

Constant object

变数有 constants,object 也可以有:

  • constant object:
double d[3] = {0, 0, 0};
const MyVector ORIGIN_3D(3, d);
// This object represent the original point

我们可以在初始化这个 object 的时候,对她做 const,这时候这个 object 就不能被改动了。

  • instance function:
class MyVector
{
private: 
	int n;
	int* m;
public:
	MyVector();
	MyVector(int dim, int v[]);
	void print() const;
}

如果你的 function 里面,会改动 object 的值,这时候就不能使用 constant。反之,像是 print 只是把他们的值印出,并没有改动任何东西,这时候就可以在他们的 header 後面加上一个 const。

!!写程序好习惯!!

const function 只能呼叫 const function,因此要养成好习惯,如果那个 function 是 const,就在後面加上 const。

constant instance variables:

在 class 里面,instance variable 也可以指派为 const。

class MyVector
{
private: 
	int n;
	int* m;
public:
	MyVector();
	MyVector(int dim, int v[]);
	MyVector(const MyVector& v);
	void print() const;
}

但是如果当 constructor 要对这个 variable 做事的时候,就会产生 compilation error。

MyVector::MyVector()
{
	n = 0; // error
	m = nullptr; 
}

这个时候,我们就要使用到 member initializer。

MyVector() : n(0)
{
	m = nullptr;
}
MyVector(int dim, int v[]) : n(dim)
{
	for(int i = 0; i < n; i++)
		m[i] = v[i];
}
MyVector(const MyVector& v) : n[v.n]
{
	m = new double[n];
	for(int i = 0; i < n; i++)
		m[i] = v.m[i];
}

在 function 的後面,加上: n(括号里面的东西就是要 assign 给 n 的咚咚)

因此,这个 member initializer 也可以对其他的 variable 做 initialize。大概像这样:

MyVector::MyVector(): n(0), m(NULL)
{
}

instance:

class MyVector
{
private:
	const	int n;
	double* m;
public:
	// constructors
	MyVector();
	MyVector(int dim, double v[]);
	// copy default constructor
	MyVector(const MyVector& v);
	// destructor
	~MyVector();
	// print function
	void print() const;
	bool isEqual(const MyVector& v) const; // 传reference 会比较省时 const 要记得
};

MyVector::MyVector(): n(0), m(NULL) 
{
}
MyVector::MyVector(int n, double m[])
{
  this->n = n;
  this->m = new double[n];
  for(int i = 0; i < n; i++)
    this->m[i] = m[i];
}
MyVector::MyVector(const MyVector& v)
{
  this->n = v.n;
  this->m = new double[n];
  for(int i = 0; i < n; i++)
    this->m[i] = v.m[i];	
}
MyVector::~MyVector() 
{ 
  delete [] m; 
}
void MyVector::print() const 
{
  cout << "(";
  for(int i = 0; i < n - 1; i++)
    cout << m[i] << ", ";
  cout << m[n-1] << ")\n";
}

Overloading comparison and indexing operator

An operator is overloaded by "implementing a special instance function".

要怎麽做 overload 呢? 公式是这样的:

operatorop

op 是那个 operator。

Comparison

我们来 overload "==" 这个 operator 吧 !

class MyVector
{
////.......
bool operator==(const MyVector& v) const;
}

bool MyVector::operator==(const MyVector& v) const
{
  if(this->n != v.n)
    return false;
  else
  {
    for(int i = 0; i < n; i++)
    {
      if(this->m[i] != v.m[i])
      return false;
    }
  }	
  return true;
}

这时候你就可以透过 "==" 来呼叫 isEqual,像是这样。

int main() 
{
	double d1[5] = { 1, 2, 3, 4, 5 };
	MyVector a1(5, d1); // (1)

	double d2[4] = { 1, 2, 3, 4 };
	MyVector a2(4, d1); // (2)
	MyVector a3(a1); // (3)

	cout << (a1 == (a2)? "Y" : "N"); // N
	cout << "\n";
	cout << (a1 == (a3) ? "Y" : "N"); // Y
	cout << "\n";
}

这个时候就可以把程序变得更简洁,更美观 & 直观!

或甚至可以这样写:

	cout << (a1.operator== (a2)? "Y" : "N"); // N
	cout << "\n";
	cout << (a1.operator== (a3) ? "Y" : "N"); // Y
	cout << "\n";

那我们就可以来如法炮制一个 operator< 的 function 了!

bool operator< (const MyVector& v) const
{
	if (this->n != v.n)
		return false;
	else
	{
		for(int i = 0; i < n; i++)
			{
				if (this->m[i] >= v.m[i])
					return false;
			}	
	}
	return true
}

或是 overloading != 。由於 != 就是 == 的反面,也就是说我们可以利用 == 来做 != 的 overloading。

bool MyVector::operator!=(const MyVector& v) const
{
	if(*this == v) // *this 就是我自己!
		return false;
	else
		return true;
	// or return !(*this == v)
}

这时候我们就可以看到 this 独特的使用地方了!

Restriction

在 overloaded 的 operator function 里面

  • 传入的 parameter 会被限制(像是 == 的 parameter不能超过一个)
  • 传入的 type 不会限制
  • 回传的 type 不会限制
  • 做甚麽事情也不会限制

Indexing operator

简单说就是给程序一个 index,程序会回传 vector v 的element v_i 值或是更改他的值。

就像是 array 一样, 这边也可以使用indexing operator: []

int main()
{
	double d1[5] = { 1, 2, 3, 4, 5 }; 
	MyVector a1(5, d1);
	cout << a1[3] << endl; // 其实 endl 也是一个object
	a1[1] = 4;
	
	return 0;
}

我们设想可以这样子使用 MyVector 作为提取他 v_i 的方式

class MyVector
{
	//...
double	operator[](int i) const;
};
double MyVector::operator[](int i) const
{
	if (i < 0 || i >= n)
		exit(1); // terminate the program (in <cstdlib>)
	return m[i];
}

exit() 是个新朋友,他可以瞬间终止这个程序。(帮助我们做 i 发生在我们不预期的时候)

()里面的1,是 exit() 会回传 1 给 operating system。

回传 0 : Normal terminal

回传其他数字: DIFFERENT ERROR

如果我们写好了上面那段,准备来跑的时候会发现:

int main()
{
	double d1[5] = { 1, 2, 3, 4, 5 }; 
	MyVector a1(5, d1);
	cout << a1[3] << endl; // 成功传出来
	a1[1] = 4; // error!!!
	
	return 0;
}

这是因为a1[1]回传的是一个 value,所以当我们写a1[1] = 4;这件事情的时候,就像是在写4 = 3一样了 ! 当然是不行的。

所以要这样子写:

class MyVector
{
	//...
double operator[](int i) const;
double& operator[](int i);
};

double MyVector::operator[](int i) const
{
  if(i < 0 || i >= n)
    exit(1);
  return m[i];
}
double& MyVector::operator[](int i)  // 回传 reference!!
{
  if(i < 0 || i >= n) // same
    exit(1); 
  return m[i];
}

我们再多做一次 overloading,这样就可以让a1[1]的意义变成回传她的地址,也就是代表那个变数。

这两个 overloading,可以用是否是 const 来区别,如果当我们传入的值是 constant,程序就会传入有 const 的 function 里,若不是的话就会传入 non-const function 里面。

implements:

#include <iostream>
#include <cstdlib>
using namespace std;

// class definition of MyVector
class MyVector
{
friend const MyVector operator+(const MyVector& v, double d);
private:
  int n; 
  double* m; 
public:
  MyVector();
  MyVector(int n, double m[]);  
  MyVector(const MyVector& v);
  ~MyVector();
  void print() const; 
  
  bool operator==(const MyVector& v) const;
  bool operator!=(const MyVector& v) const;
  bool operator<(const MyVector& v) const;
  double operator[](int i) const;
  double& operator[](int i);
  const MyVector& operator=(const MyVector& v);
  const MyVector& operator+=(const MyVector& v);
};

//instance functions
MyVector::MyVector(): n(0), m(NULL) 
{
}
MyVector::MyVector(int n, double m[])
{
  this->n = n;
  this->m = new double[n];
  for(int i = 0; i < n; i++)
    this->m[i] = m[i];
}
MyVector::MyVector(const MyVector& v)
{
  this->n = v.n;
  this->m = new double[n];
  for(int i = 0; i < n; i++)
    this->m[i] = v.m[i];	
}
MyVector::~MyVector() 
{ 
  delete [] m; 
}
void MyVector::print() const 
{
  cout << "(";
  for(int i = 0; i < n - 1; i++)
    cout << m[i] << ", ";
  cout << m[n-1] << ")\n";
}
// end of MyVector's instance functions

// MyVector's overloaded operators
bool MyVector::operator==(const MyVector& v) const
{
  if(this->n != v.n)
    return false;
  else
  {
    for(int i = 0; i < n; i++)
    {
      if(this->m[i] != v.m[i])
      return false;
    }
  }	
  return true;
}
bool MyVector::operator!=(const MyVector& v) const
{
  return !(*this == v);
}
bool MyVector::operator<(const MyVector& v) const
{
  if(this->n != v.n)
    return false;
  else
  {
  	for(int i = 0; i < n; i++)
  	{
  	  if(this->m[i] >= v.m[i])
  	    return false;
  	}
  }	
  return true;
}
double MyVector::operator[](int i) const
{
  if(i < 0 || i >= n)
    exit(1);
  return m[i];
}
double& MyVector::operator[](int i) 
{
  if(i < 0 || i >= n)
    exit(1);
  return m[i];
}
const MyVector& MyVector::operator=(const MyVector& v)
{
  if(this != &v)
  {
    if(this->n != v.n)
    {
      delete [] this->m;
      this->n = v.n;
      this->m = new double[this->n];
    }
    for(int i = 0; i < n; i++)
      this->m[i] = v.m[i];
  }  
  return *this;
}
const MyVector& MyVector::operator+=(const MyVector& v)
{
  if(this->n == v.n)
  {
    for(int i = 0; i < n; i++)
      this->m[i] += v.m[i]; 
  }
  return *this;
}
// main function
int main()
{
    double d1[5] = { 1, 2, 3, 4, 5 };
    MyVector a1(5, d1); // (1)

    double d2[4] = { 1, 2, 3, 4 };
    MyVector a2(5, d2);
    const MyVector a3(a1);

    a2[0] = 999; 
    if (a1 == a3)
        cout << a2[0] << " " << a3[0];

    return 0;
}

如果我们跑了这段程序,会跑出 999 1 ,但是如果我们把这段程序

double& MyVector::operator[](int i) 
{
  if(i < 0 || i >= n)
    exit(1);
  return m[i];
}

删掉,就会发现在 a2[0] = 999 那一行发生了 compilation error。

这时候可以用这样的方式验证一下这段程序到底在我们的 main function 里面做了甚麽?

double& MyVector::operator[](int i) 
{
	COUT << "..."; 
  if(i < 0 || i >= n)
    exit(1);
  return m[i];
}

就会发现,最後这段 ... 印了两次,结果像是这样:

......999 1

因此也可以证明,这两个const 与 non-const 的函式,当你要做 assignment (a2[0] = 999)的时候,这两者是缺一不可的。

Overloading assignment and self-assignment operators

上面的 comparison 和 indexing operation ,皆不会使 calling object 改变,但是有些 operation 则会使 calling object 改变,最简单的就像是 "="。

int main()
{
    double d1[3] = { 4, 8 ,7 };
    double d2[4] = { 1, 2, 3, 4 };
    MyVector a1(3, d1);
    MyVector a2(4, d2);

    a2.print();
    a2 = a1; // dangerous 
             // syntax error if n = constant
   // a2.print();
   // a2[0] = 9;
   // a.print();
    return 0;
}

像是上面这边的程序,我们本来就不用写任何东西就可以完成 a2 = a1 这件 assignment,但是这麽做会有一点危险,若 n 为 constant 的时候,就会出现 syntax error。

如果你再写入下面那三个注解掉的三行程序,结果会显示:

(1, 2, 3, 4)

(4, 8, 7)

(9, 8, 7)

你会发现 a1 原本是 4 8 7,却因为 a2[0] 被改成 9 而变成了 9 8 7。这种情形其实就长得像我们之前遇到过的 deep copy & shallow copy。

Default assignment:

= 在预设中其实长这样:

MyVector& MyVector::operator=(const MyVector& v)
{ // default
	this->n = v.n;
	this->m = v.m;	
}

因此,在 a1 = a2,的时候就会发生下面这件事(红色为 assign 後发生的事情)。

原本 a1 的 n 被改成 a2 的 n (也就是 4) ,而 m 这个指标则会改成 a2 的 m,所以 a1 就会指向原本 a2 指向的那个空间了。

那接下来,我们来手动制作这个 overload operator:

const MyVector& MyVector::operator=(const MyVector& v)
{
  if(this != &v) // avoid self-assignment
  {
    if(this->n != v.n)
    {
      delete [] this->m;
      this->n = v.n;
      this->m = new double[this->n];
    }
    for(int i = 0; i < n; i++) // 每个 element copy
      this->m[i] = v.m[i];
  }  
  return *this;
}
  1. 先把 m 所指到的空间 release
  2. 再把 n assign 到 v.n
  3. 最後再把 m 指到新的 n 的这段空间

这麽一来,我们就可以解决原本指向 m 那段空间的 bug

而一开始包着大家的 if 指的就是,如果你传进来的 parameter 是你自己的话,就不做任何事情,这样就可以避免当有猪队友写 a1 = a1; 时可能会发生的问题了。

那如果有人做了这件事: a1 = a2 = a3; (可以把他想成 a3 assign 给 a2,再把 a2 assign 给 a3)

为了避免这种情况,我们可以使用 const 来避免 a3 被 assign 给其他人。

另外,可以注意到我们回传的值是*this ,这是因为我们想要 return 的是一个 reference。

Preventing assignments and copying

  • 有时候,我们想要避免 object 的 assignment,以防止使用者乱 assignment,这时候只要把 assignment operator 放入 private member 里面就可以了。
  • 同样的,要避免 copy 这件事情,也是把它放到 private member 里面就好了。
  • 而我们在前面遇到的 copy constructor、assignment operator、还有 destructor这三者的使用时机就是:
    • 当 class 里面没有用到 pointer → 全部都不需要
    • 当 class 里面有用到 pointer → 全部都需要

Self-assignment operators

在向量里面,我们可能会想要加减他们,像是有两个向量u,v。而我们可能也会想要做一个 operation u += v 使 u_i 会变成 u_i + v_i (for all i)。

const MyVector& MyVector::operator+=(const MyVector& v)
{
  if(this->n == v.n)
  {
    for(int i = 0; i < n; i++)
      this->m[i] += v.m[i]; 
  }
  return *this;
}

Overloading addition operators

例如我们要做一个 加法 的 overloading,我们可能要做这几件事情:

  • parameter 使用 const MyVector&
  • 每一对 element 要一个一个的加 (u_i + v_i)
  • 不能更改 calling 和 parameter object
  • Return const MyVectora1 + a2 + a3 可以运作;但是可以避免 (a1 + a2) = a3

实作:

const MyVector operator+(const MyVector& v, double d)
{
  MyVector sum(*this); // creating a local variable
  sum += v; // using the overloaded +=
  return sum;
}

为什麽是 return 一个 object?

sum 在这个 function 里面,在函式结果的时候会被release(因为她是 local variable),所以回传 object的效果就是创立一个新的 object 储存结果,否则就会被 memory released。

且,我们甚至也可以让 + 做出不同的变化:

int main()
{
    double d1[5] = { 1, 2, 3 };
    MyVector a1(3, d1);
    MyVector a2(3, d1);

    a1 = a1 + a2;
    a1.print();
    a1 = a2 + 4.2;
    a1.print();

    return 0;
}

这时候 function 就必须写成:

class MyVector
{
//...
const MyVector operator+(double d);
const MyVector operator+(const MyVector& v, double d);
}
const MyVector operator+(const MyVector& v, double d)
{
  MyVector sum(v);
  for(int i = 0; i < v.n; i++)
    sum[i] += d;
  return sum;
}

Instance function vs. global function

因为加法有互换率(也就是 a+b = b+a),这时候如果我们写成:

a1 = 4.2 + a1; 

就会变得很奇怪(因为在原本的 function 没有这样的定义),而且我们也不能对 double 里面的函式去做改变。

因此这时候就必须使用 global function 来做 operator overloading。

const MyVector operator+(const MyVector& v, double d)
{ // need  to be friend of MyVector
  MyVector sum(v);
  for(int i = 0; i < v.n; i++)
    sum[i] += d; // pairwise addition
  return sum;
}
const MyVector operator+(double d, const MyVector& v)
{
  return v + d; // using the previous definition
}
const MyVector operator+(const MyVector& v1, const MyVector& v2)
{
  MyVector sum(v1); 
  sum += v2; // using overloaded +=
  return sum;
}

这样子就可以处理 double 在前,object 在後的情况了!

这时候你就可以写出像是这种的式子:

a3 = 3 + a1 + 4 + a3;

心得

这次说的东西,真的是完全没有想像过 !
C++ 也太酷。


<<:  Day 22 : 决策树

>>:  Day19 X Application Shell Architecture

Linkedin Java 技术认证题库分享- List排序

前言 在更新Linkedkin 个人档案的时候 偶然发现他有技术检定测验 如果总成绩在前30%,会发...

第30天 致谢与心得分享

铁人30天,实际在不简单,在这30天的过程,中间一度想要放弃、不知道要写什麽。 能持续产出优质文章的...

28/AWS SSA面试经验分享(上)

虽然现在欧洲还在work from home的期间,AWS ML Specialist Soluti...

What are the solid methods to solve Outlook Send Receive Error?

While working on Microsoft outlook account, I am n...

Powershell 入门之基本运算符

今天我们来一起看看 Powershell 中的运算符。 首先,我们来一起看看,Powershell ...