Day 21 - 我们这一班

Object-oriented programming

  • 在之前,我们使用的 programming 的方式会被称为:procedural programming。
  • 而在学会 objected-oriented programming 是一种基於 procedural programming 的延伸,但是两者的不同是「观点的不同」。
  • 在 C里面,通常会用 structure;而在 c++ 里面则是使用 classes
  • classes 就与 structure 一样,可以让我们自定义 data type,而在 class 里面的变数则会被称为 objects
  • 使用 class / OOP 可以让程序更好的模组化,在大型程序里面会使用的比较多。

Outline

  • Basic concepts
  • Constructors and the destructor
  • Friends and static members
  • Object pointers and the copy constructor

Basic concepts

之前有写过一个 Point 的 structure (其实就是二维阵列上的向量)

那我们今天来做一个 multi-dimensional vector

struct MyVector
{
	int n; 
	int* m; // 为了要动态的储存 因为是 n 维向量,你不知道会是多少
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

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

int main()
{
	MyVector v;
	v.init(3);
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m;
	return 0;
}

这样子写其实已经可以满足我们原本想要的需求,但是还是有几个缺点

  • 如果今天你是跟同学一起写这段程序的话,他们可能会忘记 initialize vector,这样我们就不知道 m 会指向哪里了。
  • 同学也可以不用你的 print ,自己再写一个 for loop 去 cout
  • n 跟 m 乱用
  • 忘记 delete

所以我们可能希望我们写的 structure 可以

  • initializer 可以自动地被呼叫
  • vector 只能用我们的方式印
  • n 、m 不能被分离式的修改(修 n 时 m 也会被修)
  • 动态配置的空间可以自动被释放掉

这时候 class 就可以派上用场了,因为它可以:

  • member function 一定会被自动呼叫
  • 可以 hide 一些 member ,还有放一些 public member
  • ...还有更多

在使用之前,我们必须先知道:

variable 分为两种

  • Instance variables (default)
  • Static variables

function 分为两种

  • Instance functions (default)
  • Static functions

Definition of class

  • class 可以说是把 struct 做一点改变而已。但是如果你直接把上面的程序中的 struct 改成 class
class MyVector
{
	int n; 
	int* m; // 为了要动态的储存 因为是 n 维向量,你不知道会是多少
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

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

int main()
{
	MyVector v;
	v.init(3);
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m;
	return 0;
}

这时候你会发现,好像没办法 compile

主要是因为 class 需要设定 visbility (class 与 struct 最大差别)

我们必须在 class 里面设定三种 member

  • Public : 可以在任何时候呼叫
  • Private : 只能在 class 里面呼叫
  • Protected : 未来可能会遇到

在预设值下,所有的 member 都是 private,所以我们要做打开或是关起来这件事情。

就像这样:

class MyVector
{
private:
	int n; 
	int* m; // 为了要动态的储存 因为是 n 维向量,你不知道会是多少
public:	
	void init(int dim);
	void print();
};

void MyVector::init(int dim) // dim = dimension
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; ++i) // initialization
		m[i] = 0;
}

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

int main()
{
	MyVector v;
	v.init(5); // 这时候就可以
	v.m[0] = 3;
	v.print(); // (3, 0, 0)
	delete [] v.m; // 这样不行
	return 0;
}

像是在 main function 里面,如果我们要存取 init 这个函数,因为我们已经把它改成 public ,因此如果你宣告他就可行,但是下面的 delete [] v.m ,因为我们把 m 设置成 private ,这时候就不能叫到他了(只能在 class 里面存取他)。

data hiding (Encapsulation 封包)**

  • 为什麽要设置 private ? 原因是因为我们想要保障 n、m 不会被乱动,程序才不会出错。
  • 同理,设置 public 的原因,也是想要限制使用者不要乱用其他的方式,像是用 cout << + for loop 而不是我们自己写出来的 print。
  • 在 99.9% 的情况下, instance variable 会被设为 private, instance function 会被设为 public。

简单说,我们把一坨东西封包起来(像是手机或是电视),再告诉使用者要怎麽使用(说明书),你没办法拿他做其他的事情(就像是电视就只能看电视)。

Instance function overloading: (函式多载)

也就是说可以传入多种情况,函式都可以运行,且可做不同结果

class MyVector
{
private:
	int n; 
	int* m; // 为了要动态的储存 因为是 n 维向量,你不知道会是多少
public:	
	void init();
	void init(int dim);
	void init(int dim, int value)
};

void MyVector::init()
{
	n = 0;
	m = nullptr;
}

void MyVector::init(int dim) // dim = dimension
{
	init(dim, 0);
}

void MyVector::init(int dim, int value)
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; i++)
		m[i] = value;
}

除此之外,class 也可以:

  • 把object 丢进去其他 function,或是做为回传值。
  • 在其他的 class 里面使用自定义的 class 作为 data type。(class 中的 class)

Constructors and the destructor

还记得我们刚刚想要完成的几件事吗?

  • [ ] initializer 可以自动地被呼叫
  • [x] vector 只能用我们的方式印
  • [x] n 、m 不能被分离式的修改(修 n 时 m 也会被修)
  • [ ] 动态配置的空间可以自动被释放掉

我们目前大概只完成了第2 3 项,1 4 则需要下面的方式来达成。

Constructors 建构子 :

他是一个在 class 中的 function,可以做到: (在物件被建立的时候)

  • 自动的呼叫
  • 不能重复呼叫
  • 不能被手动的呼叫

因此他可以达成我们 自动呼叫且初始化的功能。

特性:

  • Constructor 的名字就跟 class 一样。

    class MyVector
    {
    private:
    	int n; 
    	int* m; 
    public:	
    	MyVector(); // Constructor
    	MyVector(int dim);
    	MyVector(int dim, int value)
    };
    
  • 且他不会回传任何东西(连 void 都没有)。

  • 可以 overloading

  • 没有 parameter 的 constructor (Myvector )会被称为 default constructor,如果你没有建立一个 constructor的话,系统会自己帮你建立一个(也就是说 class 里面一定会做一个 constructor),且里面不会做任何事情

所以把 constructor 加到我们刚刚做的程序里:

class MyVector
{
private:
	int n; 
	int* m; 
public:	
	MyVector init(); 
	MyVector inti(int dim, int value = 0); // 如果只传一个 parameter -> value 用 0 来做
	void print;
};
MyVector::MyVector()
{
	n = 0;
	m = nullptr;
}

Myvector::Myvector(int dim, int value)
{
	n = dim;
	m = new int[n];
	for (int i = 0; i < n; i++)
		m[i] = value;
}

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

使用:

int main()
{
	Myvector v1(1);
	Myvector v2(3, 8); // 现在宣告的时候就可以顺便初始化
	v1.print();
	v2.print();
	return 0;
}

这样子我们就可以做到自动的初始化了

剩下的 release 动态配置的空间则会使用到 destructor →


Destructors

destructors:

  • (在物件被消灭的时候) 被呼叫
    • 被消灭的定义: variable 的生命周期结束
  • 需要他的原因是因为在 variable 生命周期结束的时候,我们不会有机会去对他做甚麽事情。
  • 跟 constructor 一样,destructor 也会自动地被系统呼叫(如果你没有定义 destructor)。

Define:

  • 在class() 前面加上 ~ 就可以宣告一个 destructor了。
class MyVector
{
private:
	int n; 
	int* m; 
public:	
	~MyVector();
};

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

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

int main()
{
	if (true)
		MyVector v1(1); 
	return 0;
}

如此一来,我们宣告一个 object 的时候,就不会发生 memory leak?

Order?

class A
{
public:
	A(){ cout << "A\n";}
	~A(){ cout << "a\n";}
};

class B
{
private:
	A a;
public:
	B(){ cout << "B\n";}
	~B(){ cout << "b\n";}
};

int main()
{
	B b;
	return 0;
}

萤幕会印出:

A
B
b
a

这就是如果有 class 包含在 class 里面的 constructor 和 destructor 呼叫的顺序。


Friends and static members

Getter and setter

在大多数的情况下,instance variable 会是 private 的,因此为了存取他们,我们必须使用 getter & setter 来对他们做一些事。

  • getter 会回传一个 private instance variable 的值
  • setter 则可以更改他们的值
class MyVector
{
private:
	int n;
	int* m;
public:
	int getN { return n; }
	void setN(int v){ n = v; }
};

friends

我们可以想像,如果今天我们想要开一个权限(像是你手机的密码),你不可能会跟陌生人说吧,所以一定是开给你感情比较好的朋友。

朋友可以是:

  • global function
  • class

像是下面这个 class

class MyVector
{
	//....
friend void test();
friend class Test;
};

我们可以知道 friend 的几个特性: (以上面为例):

  • 在 test() 和 class Test 里面,可以使用 MyVector 的 private members
  • MyVector 无法使用 Test 的 private members (单向)(就跟交朋友一样?)
  • friend 可以在 private 或是 public宣告(没差)

所以我们可以在 test 里面使用 n

void test{
	MyVector v;
	v.n = 100; // 因为是朋友!
	cout << v.n
}

在 Test 里面也可以使用:

class Test{
public:
	void test(MyVector){
		v.n = 203;
		cout << v.n;
	}
};

Static members

  • 在 class 里面,每一个 object 都拥有自己的 instance variables 和 functions。
    (这时候就会称这些 variables & functions 是 object-specific)

  • 但是反过来,member variable 或是 function 可以是一个 class 的属性( attribute) 或是 operation。

    (这个时候这些 variable 和 function 就会被称为 class specific)

    他们就会被称为 static members (静态成员: 保持不变)

举个例子: 像是 windows 中的视窗,每一个视窗都是一个 object。这些视窗都有自己的名称、自己专属的大小 这些特性就会被称为 object-specific attribute。而每一个视窗,都会有一些相同的东西,像是每个视窗都会有 title bar、这些 bar 都有一样的颜色、都会有 — [ ] X (缩小 / 全萤幕/ 关闭) 等按钮,这些他们拥有的相同属性,就可以称为一个 class-specific attribute。

class Window
{
private:
	int width;
	int height;
	int locationX;
	int locationY;
	int status; // 0: min, 1:usual, 2: max
	static int barColor; // 0: gray....
	//.....
public:
	static int getBarColor();
	static void setBarColor(int color);
	//....
};

另外,我们必须在 global 的环境下 初始化一个 static variable(因为全部都要用),像是这样:

int Window::barColor = 0; // default
int Window::getBarColor()
{
	return barColor; 
}
void Window::setBarColor(int color)
{
	barColor = color;
}

那如果我们要在 int main() 存取 static member要用

class name::member name

如果是要存取 instance 的 member:

object name.member name

使用 static member:

int main()
{
	Window w;
	cout << Window::getBarColor();
	cout << "\n";
	Window::setBarColor(1);
	return 0;
}

所以现在我们就有了四种 member type:

  • instance variable & instance functions
  • static variable & instance functions

他们俩者的关系是这样:

  • 在 instance function 里面,可以存取 static members
  • 但是在 static function 里面,不可以存取instance member
  • 我们也可以像: w.getBarColor() 这样子透过 object 存取 static member,但是 非常不建议 这麽使用,最好用 class 呼叫他

!!写程序好习惯!!

  • 如果你今天要写一个每一个 object 都要使用的特性或是 function,这时候就要使用 static member → 可以维持一致性,

  • 不要 object 来呼叫 static member

  • 尽量用 class 来呼叫:

    int Window::getBarColor()
    {
    	return Window::barColor;
    }
    

static members' function:

  1. 刚刚我们知道了 static members 可以维持 object 的一致性
  2. 他还有另一个功能就是可以 计算一个 object 被建立了几次

instance :

找出有几个人被 constuct

class A
{
private:
	static int count;
public:
	A() {A::count++; } // 因为被大家共用,所以只要有人被建立的时候,就++
	static int getCount(){ return A::count; }
};

int A::count = 0;

int main(int argc, char const *argv[])
{
	A a1, a2, a3;
	cout << A::getCount() << "n"; // 3 
	return 0;
}

instance:

找出有几个人目前还活着(alive)

class A
{
private:
	static int count;
public:
	A() {A::count++; }
	~A() {A::count--; }
	static int getCount(){ return A::count; }
};

int A::count = 0; 

int main(int argc, char const *argv[])
{
	if (true)
		A a1, a2, a3;
	cout << A::getCount() << "n"; // 0
	return 0;
}

另外,在中间的那一行就是前面所说的,static variable 要在 global 初始化。


Object pointer

  • 因为 class 是一种我们自定义的 data type

  • 且 pointer 可以指向任何一种 data type

    • 因此 pointer 是可以指向一个 object 的。 (store the address of an object)

    instnace:

    int main()
    {
    	MyVector v(5);
    	MyVector* ptrv = &v; // object pointer
    	return 0;
    
    }
    
  • object pointer 的使用?

    • 因为 pointer 就是存取 object (例如说 a ) 的位置(其实就是代表 a 的意思),所以我们可以用 *ptrA 去存取 a 中的 function,像是这样: (*ptrA).print()

    • 但是有另一个更简单存取的方式,就是直接用 ->

      所以上面的存取就可以写成 ptrA ->print();

  • WHY OBJECT POINTERS?

    1. 当我们要做一个 object array 的时候,可以用 pointer 来延迟 constructor 的呼叫,就会害我们失去初始化的机会。

      像是这个:

      int main()
      {
      	MyVector v[3]; // an object array
      	v[0].print(); // run-time error 因为 m 是 nullptr
      	return 0;
      }
      

      如果我们先呼叫了v[3],会因为我们没办法初始化,会导致 array 里面n = 0, m = nullptr。

      所以可以用 Dynamic object arrays 这个方法来解决:

      • object pointer可以帮我们做 动态记忆体配置
      int main()
      {
      	MyVector* ptrV = new MyVector(5); //呼叫 constructor
      	ptrV->print();
      	delete ptrV;
      	return 0;
      }
      
      • 也可以做出一个 动态的 array

      ❌ 因为我们宣告了 5 个 object,可是只叫了一个 pointer

      int main()
      {
      	MyVector* ptrV = new MyVector[5]; // 宣告动态 array
      	ptrV[0].print(); // run-time error 
      	delete [] ptrV;
      	return 0;
      }
      

      ✅ 我们宣告了 5 个 pointer,每一次都 create一个 object,再去做 constructor (中间的 for loop),就可以完成。

      int main()
      {
      	MyVector* ptrArray[5]; //no constructor invocation
      	for(int i = 0; i < 5; i++)
      		ptrArray[i] = new MyVector(i + 1); // constructor
      	ptrArray[0]->print();
      	// some delete statements
      	return 0;
      }
      
    2. Passing object into a function

      如果我们今天要写一个程序,让我们把每一个 vector 的质相加

      MyVector sum(MyVector v1, MyVector v2, MyVector v3 )
      {
      	// assume that their dimensions are identical
      	int n = v1.getN();
      	int* sov = new int [n]; //sov = sum of vectors
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1.getM(i) + v2.getM(i) + v3.getM(i); // 把他们相加
      	MyVector sumOfVec(n, sov); // constructor -> 後面要写
      	return sumOfVec;
      }
      
      int MyVector::getN() { return n; }
      int MyVector::getM(int i) { return m[i]; }
      MyVector::MyVector(int d, int v[]) // sov 是一个 array
      {
      	n = d;
      	for (int i = 0; i < n; ++i)
      		m[i] = v[i];
      }
      

      在这个程序里面,有 4 个 MyVector object 被创造,但是如果有更多的 object ,这时候就有点麻烦。

      所以可以改成用 pointer 来写:

      MyVector sum(MyVector* v1, MyVector* v2, MyVector* v3)
      {
      	int n = v1->getN();
      	int* sov = new int [n];
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1->getM[i] + v2->getM[i] + v3->getM[i];
      	MyVector sumOfVec(n, sov);
      	return sumOfVec;
      }
      

      如此一来,我们就只需要创造一个 object 就好了。

      但是很有可能因为 object 不够多,所以用 pointer 的时候花的时间会更多,这样就不太划算,因此建议 object 比较少的时候可以直接使用 object 来比较快。

    3. Passing object references

      MyVector sum(const MyVector& v1,const MyVector& v2,const MyVector& v3)
      {
      	int n = v1->getN();
      	int* sov = new int [n];
      	for (int i = 0; i < n; ++i)
      		sov[i] = v1.getM[i] + v2.getM[i] + v3.getM[i];
      	MyVector sumOfVec(n, sov);
      	return sumOfVec;
      }
      

      同样的,我们也可以传入 reference ,而下面就把 references 当作一般变数,直接用 .getM[i] 就可以了。

      而在 argument 的部分,因为我们不想要这些 reference 被更改,所以我们要用 const 来保护他们不被更改。

      很多时候我们写出来的程序,不是为了要做出甚麽功能,而是要避免一些事情的发生。


Copying an object

class A
{
private:
	int i;
public:
	A() { cout << "A"; }
};
void f(A a1, A a2, A a3)
{
	A a4;
}

int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n";
	f(a1, a2, a3); // A
	return 0;
}

这段程序会传出:

AAA
===
A

为什麽当我们呼叫 f 的时候,只会传出 A 而已? 而不是传出 4 个 A(4 次 constructor)

In general, when we pass by value, a local variable will be created.

  • when we pass by value for an object, a local object is created.
  • the constructor should be invoked.
int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n";
	A a4 = a1; // nothing
	return 0;
}

那如果我们把程序改成这样,就会甚麽东西都不会传出。

这是因为:

Creating an object by "copying" and object is a special operation. It happens:

  • when we pass an object into a function using call by value mechanism.

    f(a1, a2, a3);
    
  • when we assign an object to another object.

    A a4 = a1;
    
  • when we create an object with another object as the argument of the constructor.

    A a5(a1);
    

COPY CONSTRUCTOR

这一个机制会被称为 "copy constructor" (也就是用 copy object 的方式来建立 object)(这也是一个 default copy constructor),他甚麽事情都不会做。我们也可以手动的设定他要做甚麽事情:

  • 在 C++ 里面,copy constructor 的 parameter 一定要是 constant reference。
  • 如果 calling by value,copy constructor 会被宣告无数(parameter)次。
class A
{
private:
	int i;
public:
	A() { cout << "A"; }
	A( const A& a) { cout << "a"; }
};

void f(A a1, A a2, A a3)
{
	A a4;
}

int main()
{
	A a1, a2, a3; // AAA
	cout << "\n===\n; // ===
	f(a1, a2, a3); // aaaA
	A a4(a1); // a
	A a4 = a1; // a
	return 0;
}

像是在 f(a1, a2, a3); 的时候,就会因为 copy constructor 被启动,所以就会印出 a。而像下面的 A a4(a1);,也是会印出 aA a4 = a1; 的结果也是相同的。

WHY COPY CONSTRUCTORS?

我们自己也可以手动的写出 copy constructor,大概会长:

MyVector::MyVector(const MyVector& v)
{
	n = v.n;
	m = v.m;
}

Shallow copy :

这个就是一般的 default copy constructor 会做的事情(前提: member中没有 array 或是 pointer)。

那如果有 array 或是 pointer,因为 m 这个指标是指向一块空间,但是如果是一个 array 的话,就会产生不同的指标指向同样的空间的情况

int main()
{
	MyVector v1(5, 1);
	MyVector v2(v1); //??
}

这时候记忆体中会变成:

这时候如果我们改了 v1 的时候,v2 都会一起被改。

Deep copy:

为了避免我们上述讲的情形,我们就需要在 copy 的时候把一个一个 element 改掉。

首先我们要手动的宣告一个 dynamic array, 让 m 储存他的地址。最後在把 v.m[i] 的值取出来。

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

心得

以前学 python 的时候碰到 class 的机率很少,几乎没写过。
现在终於知道在干嘛了。


<<:  Day25 测试与评量 MMF

>>:  [13th][Day25] kubernetes & docker

Day 24 - 资料结构入门理解

前言 今天要来讨论一些更进阶的程序写法,比较偏向效能方面的优化,怎麽写可以让效能变好、扩充容易,而不...

LeetCode 896. Monotonic Array

题目 An array is monotonic if it is either monotone ...

[JS] You Don't Know JavaScript [this & Object Prototypes] - this All Makes Sense Now! [上]

前言 在this Or That?中提到了许多对於this的误解,并且也对於这些误解做了一些解释,我...

【Day 29】心法与招式并用 x AWS SDK x Python

tags: 铁人赛 SDK AWS Python 前言 杨过先背起全真教心法之後,才去练古墓派招式。...

2.4.16 Design System - Toasts / Snackbars

Toasts 有时又叫做 Snackbar 用来提醒用户一些小事的元件 也有几种不同的使用情境,在 ...