面向对象编程(Object Oriented Programming, OOP, 面向对象程序设计)是一种计算机编程架构,OOP的一条基本原则是计算机程序是由单个能够起到子程序作用的单元或对象组合而成,OOP达到了软件工程的三个目标:重用性、灵活性和扩展性。为了实现整体运算,每个对象都能够接收信息、处理数据和向其它对象发送信息。首先,面向对象符合人类看待事物的一般规律。其次,采用面向对象方法可以使系统各部分各司其职、各尽所能。为编程人员敞开了一扇大门,使其编程的代码 更简洁、更易于维护,并且具有更强的可重用性。
1.类与对象类是生成对象的模板,对象是根据类中定义的模板所 构造的数据,即对象是类的实例。
类:是具有相同属性和方法的一组对象的集合。它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和方法两个主要部分。在面向对象的编程语言中,类是一个独立的程序单元,它应该有一个类名并包括属性说明和方法说明两个主要部分。
对象:是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位。一个对象由一组属性和对这组属性进行操作的一组服务组成。
class ShopProduct{ //类体 }
生成ShopProduct对象:$product1是类的实例。
$product1 = new ShopProduct(); $product2 = new ShopProduct();
代码就是通过类产生实例对象的过程,$product1就是我们实例出来的对象名称, 同理,$product2也是我们实例出来的对象名称,一个类可以实例出多个对象,每个对象都是独立的,实例化出来的对象里面就包含了这些属性和方法。
对象在PHP里面和整型、浮点型一样,也是一种数据类,都是存储不同类型数据用的,在运行的时候都要加载到内存中去用, 那么对象在内存里面是怎么体现的呢?内存从罗辑上说大体上是分为4段, 栈空间段, 堆空间段,代码段, 初使化静态段,
程序里面不同的声明放在不同的内存段里面,栈空间段是存储占用相同空间长度并且占用空间小的数据类型的地方,比如说整型1, 10, 100,
1000, 10000, 100000等等,在内存里面占用空间是等长的,都是64位4个字节。
那么数据长度不定长,而且占有空间很大的数据类型的数据放在那内存的那个段里面呢?这样的数据是放在堆内存里面的。栈内存是可以直接存取的,而堆内存是不
可以直接存取的内存。对于我们的对象来说就是一种大的数据类型而且是占用空间不定长的类型,所以说对象是放在堆里面的,但对象名称是放在栈里面的,这样通
过对象名称就可以使用对象了。
$product1 = new ShopProduct();
对于这个条代码, $product1是对象名称在栈内存里面,new ShopProduct();是真正的对象是在堆内存里面的,所以$product1是存储对象首地址的变量,$product1放在栈内存里边,$product1相当于一个指针指向堆里面的对象, 所以我们可以通过$product1这个引用变量来操作对象。
2.对象属性和方法的使用:对象里面的成员使用对象->属性 、对象->方法 形式访问,没有第二种方法来访问对象中的成员。
3.$this:现在我们知道了如何访问对象中的成员,是通过”对象->成员”的方式访问的,这是在对象的外部去访问对象中成员的形式,那么如果我想在对象的内部,让对象里的方法访问本对象的属性。因为对象里面的所有的成员都要用对象来调用,包括对象的内部成员之间的调用,PHP提供了一个本对象的引用$this, 每个对象里面都有一个对象的引用$this来代表这个对象,完成对象内部成员的调用, this的本意就是“这个”的意思, 上面的实例里面,我们实例化三个实例对象$product1、 $product2,这二个对象里面各自存在一个$this分别代表对象。
4.__construct()与__destruct:
构造函数:在创建对象时,构造方法会被自动调用。构造方法可以用来确保必要的属性被设置。
class ShopProduct{ public $title; public $producerMainName; public $producerFirstName; public $price=0; //构造方法,确保必要的属性被设置,完成任何需要准备的工作,可以传参数进去 function __construct($title,$producerMainName,$producerFirstName,$price){ //通过构造方法传进来的$title给成员属性$this->title赋初使值 $this->title = $title; $this->producerMainName = $producerMainName; $this->producerFirstName = $producerFirstName; $this->price= $price; } function getProducer(){ return "{$this->producerMainName}"."{$this->producerFirstName}"; } } $product1 = new ShopProduct('书','电子书','eee','$20.00');
析构函数:
与构造函数相对的就是析构函数。析构函数允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件,
释放结果集等,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行,也就是对象在内存中被销毁前调用析构函数。与构造函数的名称类似,
一个类的析构函数名称必须是__destruct( )。析构函数不能带有任何参数。
<? //创建一个人类 class Person { //下面是人的成员属性 var $name; //人的名子 var $sex; //人的性别 var $age; //人的年龄 //定义一个构造方法参数为姓名$name、性别$sex和年龄$age function __construct($name, $sex, $age) { //通过构造方法传进来的$name给成员属性$this->name赋初使值 $this->name=$name; //通过构造方法传进来的$sex给成员属性$this->sex赋初使值 $this->sex=$sex; //通过构造方法传进来的$age给成员属性$this->age赋初使值 $this->age=$age; } //这个人的说话方法 function say() { echo "我的名子叫:".$this->name." 性别:".$this->sex." 我的年龄是:".$this->age; } //这是一个析构函数,在对象销毁前调用 function __destruct() { echo “再见”.$this->name; } } //通过构造方法创建3个对象$p1、p2、$p3,分别传入三个不同的实参为姓名、性别和年龄 $p1=new Person(“张三”,”男”, 20); $p2=new Person(“李四”,”女”, 30); $p3=new Person(“王五”,”男”, 40); //下面访问$p1对象中的说话方法 $p1->say(); //下面访问$p2对象中的说话方法 $p2->say(); //下面访问$p3对象中的说话方法 $p3->say(); ?>
输出结果为:我的名子叫:张三 性别:男 我的年龄是:20我的名子叫:李四 性别:女 我的年龄是:30我的名子叫:王五 性别:男 我的年龄是:40
再见王五
再见李四
再见张三
因为是栈内存中,先进后出。
5.public、private、protected:管理类的访问
1>>public:在任何地方都可以访问public属性和方法。
2>>private:只能在当前类中才能访问private方法或属性,即使在子类中也不能访问。
3>>protected:可以在当前类或子类中访问protested方法或属性,其他外部代码无权访问。
6.封装性:是面象对象编程中的三大特性之一,封装性就是把对象的属性和服务结合成一个独立的相同单位,并尽可能隐蔽对象的内部细节,包含两个含义:
1.把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位(即对象)。
2.信息隐蔽,即尽可能隐蔽对象的内部细节,对外形成一个边界〔或者说形成一道屏障〕,只保留有限的对外接口使之与外部发生联系。
封装的原则在软件上的反映是:要求使对象以外的部分不能随意存取对象的内部数据(属性),从而有效的避免了外部错误对它的"交叉感染",使软件错误能够局部化,大大减少查错和排错的难度。
class Person { //下面是人的成员属性 private $name; //人的名子,被private封装上了 private $sex; //人的性别, 被private封装上了 private $age; //人的年龄, 被private封装上了 //定义一个构造方法参数为私有的属性姓名$name、性别$sex和年龄$age进行赋值 function __construct($name, $sex, $age) { //通过构造方法传进来的$name给私有成员属性$this->name赋初使值 $this->name=$name; //通过构造方法传进来的$sex给私有成员属性$this->sex赋初使值 $this->sex=$sex; //通过构造方法传进来的$age给私有成员属性$this->age赋初使值 $this->age=$age; } //这个人可以说话的方法, 说出自己的私有属性,在这里也可以访问私有方法 function say() { echo "我的名子叫:".$this->name." 性别:".$this->sex." 我的年龄是:".$this->age; } } //通过构造方法创建3个对象$p1、p2、$p3,分别传入三个不同的实参为姓名、性别和年龄 $p1=new Person(“张三”,”男”, 20); $p2=new Person(“李四”,”女”, 30); $p3=new Person(“王五”,”男”, 40); //下面访问$p1对象中的说话方法 $p1->say(); //下面访问$p2对象中的说话方法 $p2->say(); //下面访问$p3对象中的说话方法 $p3->say();
没有加任何访问控制,默认的是public的,任何地方都可以访问
私有的成员只能在类的内部使用, 不能被类外部直接来存取, 但是在类的内部是有权限访问的, 所以有时候我们需要在类的外面给私有属性赋值和读取出来,也就是给类的外部提供一些可以存取的接口,上例中构造方法就是一种赋值的形式, 但是构造方法只是在创建对象的时候赋值,如果我们已经有一个存在的对象了,想对这个存在的对象赋值, 这个时候,如果你还使用构造方法传值的形式传值, 那么就创建了一个新的对象,并不是这个已存在的对象了。所以我们要对私有的属性做一些可以被外部存取的接口,目的就是可以在对象存在的情况下,改变和存取属性的值,但要注意,只有需要让外部改变的属性才这样做,不想让外面访问的属性是不做这样的接口的,这样就能达到封装的目的,所有的功能都是对象自己来完成,给外面提供尽量少的操作。
如果给类外部提供接口,可以为私有属性在类外部提供设置方法和获取方法,来操作私有属性.例如:
prvate $age; //私有的属性年龄 function setAge($age) //为外部提供一个公有设置年龄的方法 { if($age<0 || $age>130) //在给属性赋值的时候,为了避免非法值设置给属性 return; $this->age=$age; } function getAge() //为外部提供一个公有获取年龄的方法 { return($this->age); }
上面的方法是为一个成员属性设置和获取值, 当然你也可以为每个属性用同样的方法对其进行赋值和取值的操作,完成在类外部的存取工作。
7.要引用一个类而不是对象的方法,用::符号,应用对象的方法,用->符号,如:
一种是使用父类的“类名::“来调用父类中被覆盖的方法;
一种是使用“parent::”的方试来调用父类中被覆盖的方法;
parent::__construct()
8.继承:extends,最佳实践:
基类:商品基础属性
class ShopProduct{//基类 private $title; private $producerMainName; private $producerFirstName; protected $price; private $discount = 0; //构造方法,确保必要的属性被设置,完成任何需要准备的工作。 public function __construct($title,$firstName,$mainName,$price){//初始化所有的公用参数 $this->title = $title; $this->producerFirstName= $firstName; $this->producerMainName= $mainName; $this->price= $price; } //初始化所有的公用方法 public function getproducerFirstName(){ return $this->producerFirstName; } public function getproducerFirstName(){ return $this->producerMainName; } public function setDiscount($num){ return $this->discount=$num; } public function getDiscount(){ return $this->discount; } public function getTitle(){ return $this->title; } public function getPrice(){ return ($this->price - $this->discount); } }
子类:CD类
class CdProduct extends ShopProduct{ private $playLength = 0; public function __construct($title,$firstName,$mainName,$price,$playLength){ //获取父类的初始化参数 parent::__construct($title,$firstName,$mainName,$price); //私有的初始化参数 $this->playLength = $playLength; } public function getSummaryLine(){ return $this->playLength = $playLength; } public function getSummaryLine(){ $base = parent::getSummaryLine(); $base .= ":playing time -{$this->palyLength}"; return $base; } }
子类:BookProduct,parent::__construct()获取父类的参数。
class BookProduct extends ShopProduct{ private $numPages= 0; public function __construct($title,$firstName,$mainName,$price,$numPages){ //获取父类的初始化参数 parent::__construct($title,$firstName,$mainName,$price); //私有的初始化参数 $this->numPages= $numPages; } public function getNumbersOfPages(){ return $this->numPages= $numPages; } public function getSummaryLine(){ $base = parent::getSummaryLine(); $base .= ":page count -{$this->numPages}"; return $base; } }
9.final:关键字可以终止类的继承。final类不能有子类,final方法不能被覆盖。这个关键字只能用来定义类和定义方法, 不能使用final这个关键字来定义成员属性,因为final是常量的意思,我们在PHP里定义常量使用的是define()函数,所以不能使用final来定义成员属性。
使用final关键标记的类不能被继承:
final class Checkout{ }
这是一个final类,下面尝试继承:
class IllegalCheckout extends Checkout{ }
会产生致命错误.
final方法不能被继承:
class Checkout{ final function totalize(){ } }
继承:
class IllegalCheckout extends Checkout{ final function totalize(){ //重载这个方法 } }
也会导致错误。
10.__toString()
如果你打印一个对象,PHP就会把对象解析成一个字符串来输出。“__toString()”方法也是一样自动被调用的,是在直接输出对象引用时自动调用的, 前面我们讲过对象引用是一个指针,比如 说:“$p=new StringThing()“中,$p就是一个引用,我们不能使用echo 直接输出$p, 这样会输 出”Catchable fatal error: Object of class StringThing could not be converted to string “这样的错误。
class StringThing{} $st = new StringThing(); print $st;
这样毁产生上面的错误,但是通过实现自己的__toString()方法,可以控制输出字符串的格式,__toString()方法返回字符串。
class Person{ function getName(){return "Bob";} function getAge(){return 44;} function __toString(){ $desc = $this->getName(); $desc .= " (age ".$this->getAge().")"; return $desc; } }
此时,打印返回出自定义的内容
$person = new Person(); print $person;//Bob (age 44)
11.__clone():
复杂对象只是简单地将一个变量赋值给另一个变量。
class CopyMe{} $first = new CopyMe(); $second = $first;
在PHP4中,$second和$first是两个完全不同的对象。我们无法检查两个变量是否指向相同的对象。
在PHP5中,$second和$first指向同一个对象。运行PHP5的代码时,两个变量指向同一个引用,没有各自保留一份相同的副本,有时候是需要的。
PHP5中提供了clone关键字:
class CopyMe{} $first = new CopyMe(); $second = clone $first;//现在的$second和$first是不同的两个对象了
但是此时还有问题,每一个Person对象都有一个标示$id,如果这个对象$id对应的是数据库中的一条信息,可能出现的情况是,两个完全不同的对象指向数据库中的一条记录,此时,对一个对象的操作就会印象到另一个对象了。如果我们可以控制对象复制的内容,那么久可以避免上面的问题了。这就是__clone()的用途:
浅复制:
Person{ private $name; private $age; private $id; function __construct($name,$age){ $this->name = $name; $this->age = $age; } function setId($id){ $this->id = $id; } //这个函数在赋值的对象上运行 function __clone(){//将复制的对象id设置为0 $this->id = 0; } }
当在一个Person对象上调用clone时,产生一个新的副本,并且新副本的__clone()方法会被调用:
$person = new Person("bob",44); $person->setId(343); $person2 = clone $person;//此时的person2:name:bob age:44 id:0
深复制:以上的浅复制,可以保证所有基本数据类型的属性被完全赋值。在复制对象属性时只复制引用,并不复制引用的对象。如果对象有一个$balance属性,可以将该属性也复制给新副本,那么此时就有两个Person对象同时引用一个$balance。对一个操作,就以为着另一个也改变了。
class Account{//账单 public $balance;//余额 function __construct($balance){ $this->balance = $balance; } }
下面是Person对象,且引用了Account对象
Person{ private $name; private $age; private $id; function __construct($name,$age,Account $account){ $this->name = $name; $this->age = $age; $this->account = $account; } function setId($id){ $this->id = $id; } //这个函数在赋值的对象上运行 function __clone(){//将复制的对象id设置为0 $this->id = 0; } }
当复制这个对象是:
$person = new Person("bob",44,new Accoun(200)); $person->setId(343); $person2 = clone $person; //给person充一些钱 $person->account->balance +=20; //结果是$person2也被充了钱 print $person2->account->balance;//220
解决方法:
//这个函数在赋值的对象上运行 function __clone(){//将复制的对象id设置为0 $this->id = 0; $this->account = clone $account;//深度clone }