PHP 第10章 类和对象 PHP 第10章 类和对象

2016-07-09

一、基本概念

1.1、对象

统一管理属于同一事物的变量,这就是对象。

  • OOP(object oriented programming),即面向对象编程,其中两个最重要的概念就是类和对象。

  •  世间万物都具有自身的属性和方法,通过这些属性和方法可以区分出不同的物质。 

  • 属性和方法的集合就形成了类,类是面向对象编程的核心和基础, 通过类就将零散的用于实现某个功能的代码有效地管理起来了。

  •  类只是具备了某些功能和属性的抽象模型,而实际应用中需要一个一个实体,也就是需要对类进行实例化, 类在实例化之后就是对象。

1.2、分析对象在内存中的存在形式

class Person 

    public $name; 
    public $age; 
}
$a = new Person(); .
$a->name = 'luluyii'; 
$a->age = 20; 
$b = $a;

https://file.lulublog.cn/images/3/2022/08/Y2Q2U7IXK7499ID2ueSgeItd0TK8k2.png

二、$this —— 我的地址

2.1、案例

class Person
{
    public $name;
    public function _ _construct($name){
        $this->name = $name;
    }
    $p1 = new Person("luluyii");
    $p2 = new Person("lulubin"); 
}

https://file.lulublog.cn/images/3/2022/08/hohCPvCPvZ4zrQcacOHMoYrCPVRepQ.png

2.2、说明

(1)$this本质可理解成这个对象的地址

(2)哪个对象使用到$this,就是用哪个对象的地址

(3)$this 不能在类外部使用

三、构造方法

1、构造方法是类的一种特殊方法,它主要作用是完成对新对象的初始化。

2、特点:构造方法没有返回值,是自动被调用的。

3、构造方法的形式

php 4中构造方法的名字和类型一致 class Person{ public function Person(){} }

php5中一个类可以同时有两种形式的构造方法:_ _construct()和类名(),当两个方法同时存在时,优先调用_ _construct()

4、一个类有且只有一个构造方法,在php5后虽然_ _ construct()和类名()可以共存,但实际也只能使用一个

5、如果没有给类自定义构造方法,则该类默认使用系统的构造方法

6、构造方法的默认访问修饰符是public

四、析构方法

4.1、基本概念

(1)php5之后加入的,主要作用是释放资源,并不是销毁对象本身。

(2)在销毁对象前,系统会自动调用该类的析构方法

(3)一个类最多只有一个析构方法

4.2、说明

(1)析构方法调用顺序:先创建的对象资源后被释放

(2)析构方法被调用时间:当程序退出,即进程结束时;当一个对象成为垃圾对象时;

(3)垃圾对象是指没有任何变量再引用它

(4)因为 php 具有垃圾回收机制,能自动清除不再使用的对象,释放内存,一般情况下可以不手动创建析构方法。

4.3、案例1

$p1 = new Person("luluyii",1);
$p2 = new Person("lulubin",2);

https://file.lulublog.cn/images/3/2022/08/HPPOZj03EJPPXjiJJJ6piJsojOoS66.png

解释:

(1)栈的先入后出,由于$p1先进栈,故$p2先出栈

(2)垃圾对象:当指向堆区的箭头消失时,如①消失,则堆区中0x12成为垃圾对象

(3)让对象成为垃圾对象:$p1 = null;

(4)当0x12成为垃圾对象时,会先被立即调用,即先调用$p1再调用$p2

4.4、案例2

class Person
{
    public $name;
    public $age;
    //构造方法
    public function _ _construct($name,$age){
        $this->name = $name;
        $this->age = $age;
    }
    //析构方法
    public function _ _destruct(){
        echo $this->name."销毁资源";
    }
}
$p1= new Person("luluyii",1);
$p2= new Pserson("lulubin",2);
输出:luluyii销毁资源    lulubin销毁资源
$p1 = new Person("luluyii",1);
$p1 = null; 
$p2 = new Pserson("lulubin",2); 
输出:lulubin销毁资源     luluyii销毁资源

五、静态变量

5.1、全局变量

在程序中都可使用的变量 global

(1)案例1

global $a;
$a = 9;
function test()
{
    $a = 90;
}
test();
echo $a;

https://file.lulublog.cn/images/3/2022/08/bRYV5El8lWEyR6Z028Wa6Y2ItzIat5.png

解释:栈区中$a [0x1] [90] 与全局区中$a [0x2] [90]地址不同,echo $a 寻找global $a,输出 9

(2)案例2

global $a;
$a = 9;
function test()
{
     global $a;
     $a = 90;
}
test();
echo $a;

https://file.lulublog.cn/images/3/2022/08/B8775t5lLgGLcOSWgu50OBk9og4u4g.png

解释:栈区中$a [0x1] [90] 与全局区中$a 地址相同,则全局区中$a 的值被修改为90,输出90

5.2、静态变量

(1)案例

class Child
{
    public $name;
    public static $nums = 0;
    function _ _construct($name){
        $this->name = $name;
    }
    public function joinGame(){
        self::$num += 1;
        echo $this->name."加入游戏";
    }
}
$chilid1 = new Child("luluyii");
$child1->joinGame();
$child2 = new Chlid("lulubin");
$child2->joinGame();
echo Child::$nums;

https://file.lulublog.cn/images/3/2022/08/Xcqhpq2Qskk2c2oQckP3vHAAKpCLaF.png

输出:luluyii加入游戏 lulubin加入游戏 2

(2)静态变量的基本用法

① 在类中定义静态变量:[访问修饰符] static $变量名;

② 访问静态变量

在类中访问:类名/self::$静态变量名;在类外访问:类名/对象名::$静态变量名

(3)静态方法/类方法

① 形式:[访问修饰符] static 方法名(){ }

② 访问类方法

在类中访问:类名/self::方法名;在类外访问:类名::方法名、对象名->方法名

③ 特点

静态方法:操作静态变量,但是不能操作非静态变量;普通方法:既可以操作静态变量,也可以操作非静态变量

注意:当一个类中非静态方法被self::调用时,被调用的方法自动转为静态方法

(4)什么时候用静态变量

当变量用于所有对象共享时,则应当使用静态变量。

比如:定义学生类,统计学生共交多少钱?每个学生都是独立的对象,定义公共静态变量$fee,则可累加所有学生学费。

六、封装、继承、多态

6.1、封装:与访问修饰符挂钩

①、抽象

定义类时,实际上是把一类事物的共有属性和行为提取出来,形成一个物理模型(模板),这种研究问题的方法称为抽象。

封装性:也称为信息隐藏,就是将一个类的使用和实现分开,只保留部分接口和方法与外部联系,或者说只公开了一些供开发人员使用的方法。 

于是开发人员只需要关注这个类如何使用,而不用去关心其具体的实现过程,这样就能实现MVC分工合作,也能有效避免程序间相互依赖, 实现代码模块间松藕合。

②、php提供三种访问控制符号

(1)public:表示全局,可以在本类、子类、类外部中使用

(2)protected:表示受保护,可以在本类、子类中使用

(3)private:表示私有,只能在本类中使用

上述三个控制修饰符可以对属性和方法修饰,默认是public;php4中 var = public,后面不用var

③、访问protected、private变量

通常的做法是提交public函数去访问这些变量,形式如下:

public function setXxx($val){ }
public function getXxx(){ }
public function showInfo(){ }

6.2、继承 extends

①、定义

一个子类通过extends父类,然后把父类的public、protected属性和方法继承下来。

继承性:就是子类自动继承其父级类中的属性和方法,并可以添加新的属性和方法或者对部分属性和方法进行重写。继承增加了代码的可重用性。

②、基本语法

class 类名 extends 父类名
{
    //再写自己需要的属性和方法
}

③、继承的细节讨论

(1)一个类只能继承一个父类,若你希望继承多个类的属性和方法,则用多层继承

class A
{
    public $n1 = 90;
}
class B extends A
{
    public $n2 = "hello";
}
class C extends B { }
$c = new C();
echo $c->n1;

输出:90

证明C类通过多层继承方式继承了A类

(2)当创建子类对象时,默认情况下,不会自动调用父类的构造方法,这点与java不同

① 若子类没有定义构造方法,则会如同一个普通的类方法一样从父类继承(假设父类的构造方法没有被定义为private)

class A
{
    protected function _ _construct(){
        echo "A_ _construct";
    }
}
class B extends A { }
$b = new B();
输出:A_ _construct

② 要执行父类的构造方法,则需要在子类的构造方法中调用 parent::_ _construct() 或者 类名::_ _construct()

class A
{
    protected function _ _construct(){
        echo "A_ _construct";
    }
}
class B extends A 
{
    public function _ _construct(){
        parent::_ _construct();
    }
}
$b = new B();
输出:A_ _construct

6.3、多态

①、函数的重载

函数名一样,但是通过函数的参数个数或者类型不同,达到调用相同一个函数名,但是又可以区分不同的函数。

多态性:子类继承了来自父级类中的属性和方法,并对其中部分方法进行重写。 于是多个子类中虽然都具有同一个方法,但是这些子类实例化的对象调用这些相同的方法后却可以获得完全不同的结果,这种技术就是多态性。 多态性增强了软件的灵活性。

②、php5 通过魔术方法 _ _call 来实现方法重载的效果

(但是_ _call 官方不推荐使用,默认情况下“直接”支持重载)

class A
{
    public function test1($p){
        echo "接收一个参数";
    }
    public function test2($p){
        echo "接收两个参数";
    }
    public function _ _call($method,$p){
        if($method == 'test'){
            if(count($p) == 1){
                $this->test1($p);
            }
            if(count($p) == 2){
                $this->test2($p);
            }
        }
    }
}
$a = new A();
$a->test(1);
$a->test(3,4);

输出:接收一个参数 接收两个参数

分析:当执行$a->test(1),想调用test()却找不到该方法,则系统调用了_ _call()

③、方法的重写/覆盖

(1)在一个子类的方法和父类的方法完全一样时,我们称为方法的重写或方法的覆盖

(2)细节讨论

① 要实现重写,要求子类的方法名字和参数列表一模一样,但是并不要求参数的名称一样。

② 若子类要调用父类的某个方法,则可使用 parent/父类名::方法名(参数)

③ 实现方法覆盖的时候,访问修饰符可以不一样,但必须满足:子类的访问访问 >= 父类的访问范围(一代要比一代强哈)

(3)多态的体现

当子类没有覆盖父类的方法时,则可调用父类的方法;

当子类覆盖父类的方法时,则调用自己重写的方法,这就是多态的体现。

6.4、面向对象、面向服务、面向组件三种编程模式

①、背景

无论什么系统变大了之后,就会有一系列问题。

面向XX就是为了解决系统成长过程中遇到问题,而采用的一些范式。

②、举例

你开始给一个企业做MIS系统。

当这个企业来很小的时候,用简单的面向对象编程,数据库+服务端+浏览器已经满足需求。不需要考虑面向组件开发和 SOA。

慢慢的,这个企业长大了,当初简单的 mis 系统,变得越来越复杂和庞大。系统中有很多重复功能的代码。当这些功能模块的业务有变化时是你头疼的时候:代码中有很多地方要修改,遇到新员工,有时总是改不全。系统上线问题越来越多,需求响应时间也越来越长。经常被客户骂:他X的,这么简单的需求搞了半个月都上不了线。去年xxxxxxx两天就上线了。

此时,你会考虑怎么把系统中那些重复的代码统一起来。你会考虑到组件化,即“面向组件”。

你把一个个比较独立的业务模块约定好接口,开发成组件。以后再有类似的功能模块,直接调用这个组件,即节省开发成本,又容易维护。

后来,企业变成了集团公司。已经上线了很多套各种各样的mis系统。虽然大部分系统都实现了组件化。但做为一个集团公司,仍然有很多共同的业务,不同mis系统中有很多功能重复的模块。此时又面临业务升级困难,难以使用的问题:一个需求可能要涉及很多套mis系统的升级。同时每套系统都有独自的界面,客户录入一个数据,要打开N个页面,要登陆N次,叫苦不迭。各种数据不一致的问题接踵而来。

SOA 来啦。架构师把各个系统功能类似的模块抽象成服务,重复的模块再也没有了,不同系统间互相调用服务接口。以前要自己写一大堆代码,现在搞清楚接口,直接调用另一套Mis系统的服务接口就 OK了。

也有了单点登陆,有了portal,有了搜索服务,有了知识库等等。

但是问题又来了:

总有一些很重要的服务,所有的系统都会依赖它,它出一点问题,所有系统都停转。你开始考虑双机,热备,负载均衡。

以前用的IBM的主机+Oracle数据库+EMC的存储,再后来买更贵的性能更好的。慢慢的你发现,企业挣的钱都他妈的给了IOE。你开始考虑分布式,开始考虑使用开源产品。

③、用一张图形象解释 —— 面向对象过程中常见概念

https://file.lulublog.cn/images/3/2022/08/wwTWIPvB6cd1PNq1wiv1PP1dwiw1Xd.jpg

七、魔术方法

class Person
{
	private $name;
	private $sex;
	private $age;

	/**
	 * 构造方法在对象诞生时为成员属性赋初值
	 */
	function __construct($name="宋", $sex="", $age=1)
	{
		$this->name = $name;
		$this->sex = $sex;
		$this->age = $age;
	}

	/**
	 * 这个方法用来获取私有成员属性值的,有一个参数,参数传入你要获取的成员属性的名称,返回获取的属性值,
	 * 这个方法不用我们手工的去调用,因为我们也可以把这个方法做成私有的方法,是在直接获取私有属性的时候对象自动调用的。
	 */
	public function __get($propertyName)
	{
		if($propertyName=="sex") {
			return "保密";//不让别人获取到性别,以“保密”替代
		} else if($propertyName=="age") {
			if($this->age > 30){
				return $this->age-10;
			}else{
				return $this->$propertyName;
			}
		}else {
			return $this->$propertyName;
		}
	}

	/**
	 * 这个方法用来为私有成员属性设置值的,有两个参数,第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
	 * 这个方法同样不用我们手工去调用,它也可以做成私有的,是在直接设置私有属性值的时候自动调用的,同样属性私有的已经被封装上了,
	 * 如果没有__set()这个方法,是不允许的,比如:$this->name=‘zhangsan’,这样会出错,但是如果你在类里面加上了__set($property_name, $value)这个方法,
	 * 在直接给私有属性赋值的时候,就会自动调用它,把属性比如name传给$property_name,把要赋的值“zhangsan”传给$value,通过这个方法的执行,达到赋值的目的。
	 * 如果成员属性不封装成私有的,对象本身就不会去自动调用这个方法。为了不传入非法的值,还可以在这个方法给做一下判断。
	 */
	public function __set($propertyName, $propertyValue)
	{
		if($propertyName=="sex"){
			if(!($propertyValue == "男" || $propertyValue == "女"))  //第二个参数只能是男或女
				return; //如果是非法参数返回空,则结束方法执行
		}
		if($propertyName=="age"){
			if($propertyValue > 150 || $propertyValue <0) .="" this-="">$propertyName = $propertyValue;
	}
	
	/**
	 * isset()函数测定私有成员时,自动调用
	 */
	public function __isset($nm)
	{
		echo"isset()函数测定私有成员时,自动调用
"; return isset($this->$nm); } /**  * 当在类外部使用unset()函数来删除私有成员时自动调用  */ public function __unset($nm) { echo"当在类外部使用unset()函数来删除私有成员时自动调用的
"; unset($this->$nm); } } $person1=new Person("张三", "男", 40); echo "姓名:".$person1->name."
"; $person1->name="李四"; echo "姓名:".$person1->name."
"; echo isset($person1->name); 输出: 姓名:张三 姓名:李四 isset()函数测定私有成员时,自动调用 1

7.1、__call

  • 魔术方法__call() 的作用是当程序调用一个不存在或不可见的成员方法时,php 会先调用__call()方法, 将那个不存在的方法的方法名和参数都存储下来。 

  • __call()包含两个参数,第一个参数是那个不存在的方法的方法名,是个字符串类型; 第二个参数是那个不存在的方法的所有参数,是个数组类型。 

  • 本人认为__call()方法的意义更多在于调试,可以定位到错误。同时可以捕捉异常,如果某个方法不存在, 则执行其它可替代方法。

7.2、其他方法

__sleep 
__wakeup 
__toString 
__set_state 
__construct, 
__destruct 
__call, 
__get, 
__set, 
__isset, 
__unset 
__sleep, 
__wakeup, 
__toString, 
__set_state, 
__clone 
__autoload

八、抽象类和接口

8.1、类

  • 如何定义常量、如何类中调用常量、如何在类外调用常量

  • 类中的常量也就是成员常量,常量就是不会改变的量,是一个恒值。

  • 定义常量使用关键字const. 例如:const PI = 3.1415326;

  • 无论是类内还是类外,常量的访问和变量是不一样的,常量不需要实例化对象, 访问常量的格式都是类名加作用域操作符号(双冒号)来调用。 即:类名 :: 类常量名;

8.2、抽象类

①、为什么要设计抽象类这个技术

答:在实际开发中,我们可能有这样的一种类,是其它类的父类,但是本身并不需要实例化,主要用途是用于让子类继承,这样可以达到代码复用,同时利于项目设计者设计类。

②、基本语法

abstract class 类名
{
    //属性
    //方法
}

③、案例

abstract class Animal
{
    public $name;
    //这个方法没有方法体,主要为了让子类去实现
    abstract public function cry();
}
class Cat extends Animal
{
    public function cry(){
        echo "喵喵喵";
    }
}
$cat1 = new Cat();
$cat1->cry();

④、细节讨论

(1)若一个类使用 abstract 修饰,则该类就是抽象类;

若一个方法被 abstract 修饰,则该方法就是抽象方法,抽象方法不能有方法体。

(2)抽象类可以没有抽象方法,同时也可以有普通方法。

(3)一个类只要有一个抽象方法,则该类必须声明为 abstract

(4)若A类继承了一个抽象类B,则要求A类实现抽象类B的所有抽象方法,除非A类本身是一个抽象类

8.3、接口

—— 更抽象的抽象类

①、接口的作用

声明一些方法,供其他类实现,接口还体现了高类聚、低耦合的特点

②、基本语法

(1)接口的基本语法

interface 接口名
{
    //常量
    //方法
}

接口名:第一个字母小写,通常是“i”,如“iUsb”;

接口方法:不能有方法体,而抽象类的方法可以有方法体,故接口是更抽象的抽象类(重要)

(2)实现接口

class 类名 implements 接口1,接口2 { }

③、案例

interface iUsb
{
    public function start();
    public function stop();
}
class Camera implements iUsb
{
    public function start(){
        echo "相机开始工作";
    }
    public function stop(){
        echo "相机停止工作";
    }
}
$camera1 = new Camera();
$camera1->start();
$camera2->stop();

输出:相机开始工作 相机停止工作

④、接口高类聚、低耦合的分析

(1)高类聚:声明了共有的方法

(2)低耦合

当类1出现问题,则继承了类1的类2、类3均出现问题;

当类1出现问题,实现同一接口的类2、类3不会出现问题。

⑤、细节讨论

(1)什么情况下使用接口

A 定规范

B 定规范,让其他人实现

C 当多个类间是平级关系(即没有继承关系),但这些类都要去实现某个功能,只是实现的方式不一样

(2)注意事项

A 接口中的属性是 const 常量,接口的方法是public(默认)且不能有方法体

B 不能去实例化一个接口,一个类可以实现多个接口

C 当一个类实现了某个接口,要求该类实现这个接口的所有方法

D 一个类可以同时继承一个类和实现多个接口

class A extends B implements C,D

(3)类继承和实现接口的比较

A 可以认为实现接口是对类单一继承的补充

B 可以在不破坏层级关系的前提下对某个类的功能扩展

8.4、算法

写一个算法,使对象可以像数组一样进行foreach循环,要求属性必须是私有的(Interator模式的PHP5实现,写一类实现Interator接口)

  • 这就要讲到PHP的内置接口Iterator了,PHP5开始支持了接口, 并且内置了Iterator接口, 所以如果你定义了一个类,并实现了Iterator接口,那么你的这个类对象就是ZEND_ITER_OBJECT,否则就是ZEND_ITER_PLAIN_OBJECT.

  • 对于ZEND_ITER_PLAIN_OBJECT的类,foreach会通过HASH_OF获取该对象的默认属性数组,然后对该数组进行foreach. 而对于ZEND_ITER_OBJECT的类对象,则会通过调用对象实现的Iterator接口相关函数来进行foreach。

Iterator extends Traversable {
     /* 方法 */
    abstract public mixed current ( void )
    abstract public scalar key ( void )
    abstract public void next ( void )
    abstract public void rewind ( void )
    abstract public boolean valid ( void )
}
class sample implements Iterator
{
    private $_items = array(1,2,3,4,5,6,7);
    public function __construct() {
    }
    public function rewind() { reset($this->_items); }
    public function current() { return current($this->_items); }
    public function key() { return key($this->_items); }
    public function next() { return next($this->_items); }
    public function valid() { return ( $this->current() !== false ); }
}
$sa = new sample();
foreach($sa as $key => $val){
    print $key . "=>" .$val;
}
阅读 3218