PHP 面试专栏正式起更,每周一、三、五更新,提供最好最优质的 PHP 面试内容。继上一篇“PHP 面试常考内容之面向对象(1)”发表后,今天更新(2),需要(1)的可以直接点击文字进行跳转获取。整个面向对象文章的结构涉及的内容模块有:
一、面向对象与面向过程有什么区别?二、面向对象有什么特征?三、什么是构造函数和析构函数?四、面向对象的作用域范围有哪几种?五、PHP 中魔术方法有哪些?六、什么是对象克隆?七、this、self 和 parent 的区别是什么?八、抽象类与接口有什么区别与联系?九、PHP 面向对象的常考面试题讲解
关于 PHP 面向对象的内容将会被分为三篇文章进行讲解完整块内容,第一篇主要讲解一到四点内容,第二篇主要讲解五到八的内容,第三篇围绕第九点进行讲解。
以下正文的内容都来自《PHP 程序员面试笔试宝典》书籍,如果转载请保留出处:
五、PHP 种魔术方法有哪些?
在 PHP 中,把所有以__(两个下画线)开头的类方法保留为魔术方法。所以在定义类方法时,不建议使用 __ 作为方法的前缀。下面分别介绍每个魔术方法的作用。
1.__get、__set、__isset、__unset
这四个方法是为在类和它们的父类中没有声明的属性而设计的。1)在访问类属性的时候,若属性可以访问,则直接返回;若不可以被访问,则调用__get 函数。方法签名为:public mixed __get (string $name)2)在设置一个对象的属性时,若属性可以访问,则直接赋值;若不可以被访问,则调用__set 函数。方法签名为:public void __set (string $name , mixed $value)3)当对不可访问的属性调用 isset() 或 empty() 时,__isset() 会被调用。方法签名为:public bool __isset ( string $name)4)当对不可访问属性调用 unset() 时,__unset() 会被调用。方法签名为:public bool _unset (string $name) 需要注意的是,以上存在的不可访问包括属性没有定义,或者属性的访问控制为 proteced 或 private(没有访问权限的属性)。下面通过一个例子把对象变量保存在另外一个数组中。
<?php
class Test
{
/* 保存未定义的对象变量 */
private $data = array();
public function __set($name, $value){
$this->data[$name] = $value;
}
public function __get($name){
if(array_key_exists($name, $this->data))
return $this->data[$name];
return NULL;
}
public function __isset($name){
return isset($this->data[$name]);
}
public function __unset($name){
unset($this->data[$name]);
}
}
$obj = new Test;
$obj->a = 1;
echo $obj->a . “\n”;
?>
程序的运行结果为
1
2.__construct、__destruct
1)__construct 构造函数,实例化对象时被调用。2)__destruct 析构函数,当对象被销毁时调用。通常情况下,PHP 只会释放对象所占有的内存和相关的资源,对于程序员自己申请的资源,需要显式地去释放。通常可以把需要释放资源的操作放在析构方法中,这样可以保证在对象被释放的时候,程序员自己申请的资源也能被释放。例如,可以在构造函数中打开一个文件,然后在析构函数中关闭文件。
<?php
class Test
{
protected $file = NULL;
function __construct(){
$this->file = fopen(“test”,”r”);
}
function __destruct(){
fclose($this->file);
}
}
?>
3.__call() 和__callStatic()
1)__call($method, $arg_array):当调用一个不可访问的方法时会调用这个方法。2)__callStatic 的工作方式与 __call() 类似,当调用的静态方法不存在或权限不足时,会自动调用__callStatic()。使用示例如下:
<?php
class Test
{
public function __call ($name, $arguments) {
echo “ 调用对象方法 ‘$name’ “. implode(‘, ‘, $arguments). “\n”;
}
public static function __callStatic ($name, $arguments) {
echo “ 调用静态方法 ‘$name’ “. implode(‘, ‘, $arguments). “\n”;
}
}
$obj = new Test;
$obj->method1(‘ 参数 1 ’);
Test::method2(‘ 参数 2 ’);
?>
程序的运行结果为
调用对象方法 ‘method1’ 参数 1 调用静态方法 ‘method2’ 参数 2
4.__sleep() 和__wakeup()
1)__sleep 串行化的时候调用。2)__wakeup 反串行化的时候调用。也就是说,在执行 serialize() 和 unserialize() 时,会先调用这两个函数。例如,在序列化一个对象时,如果这个对象有一个数据库连接,想要在反序列化中恢复这个连接的状态,那么就可以通过重载这两个方法来实现。示例代码如下:
<?php
class Test
{
public $conn;
private $server, $user, $pwd, $db;
public function __construct($server, $user, $pwd, $db)
{
$this->server = $server;
$this->user = $user;
$this->pwd = $pwd;
$this->db = $db;
$this->connect();
}
private function connect()
{
$this->conn = mysql_connect($this->server, $this->user, $this->pwd);
mysql_select_db($this->db, $this->conn);
}
public function __sleep()
{
return array(‘server’, ‘user’, ‘pwd’, ‘db’);
}
public function __wakeup()
{
$this->connect();
}
public function __destruct(){
mysql_close($conn);
}
}
?>
5.__toString()
__toString 在打印一个对象时被调用,可以在这个方法中实现想要打印的对象的信息,使用示例如下:
<?php
class Test
{
public $age;
public function __toString() {
return “age:$this->age”;
}
}
$obj = new Test();
$obj->age=20;
echo $obj;
?>
程序的运行结果为
age:20
6.__invoke()
在引入这个魔术方法后,可以把对象名当作方法直接调用,它会间接调用这个方法,使用示例如下:
<?php
class Test
{
public function __invoke()
{
print “hello world”;
}
}
$obj = new Test;
$obj();
?>
程序的运行结果为
hello world
7.__set_state()
调用 var_export 时被调用,用__set_state 的返回值作为 var_export 的返回值。使用示例如下:
<?php
class People
{
public $name;
public $age;
public static function __set_state ($arr) {
$obj = new People;
$obj->name = $arr[‘name’];
$obj->age = $arr[‘aage’];
return $obj;
}
}
$p = new People;
$p->age = 20;
$p->name = ‘James’;
var_dump(var_export($p));
?>
程序的运行结果为
People::__set_state(array(
‘name’ => ‘James’,
‘age’ => 20,
)) NULL
8.__clone()
这个方法在对象克隆的时候被调用,php 提供的__clone() 方法对一个对象实例进行浅拷贝,也就是说,对对象内的基本数值类型通过值传递完成拷贝,当对象内部有对象成员变量的时候,最好重写__clone 方法来实现对这个对象变量的深拷贝。使用示例如下:
<?php
class People
{
public $age;
public function __toString() {
return “age:$this->age \n”;
}
}
class MyCloneable
{
public $people;
function __clone()
{
$this->people = clone $this->people; // 实现对象的深拷贝
}
}
$obj1 = new MyCloneable();
$obj1->people = new People();
$obj1->people->age=20;
$obj2 = clone $obj1;
$obj2->people->age=30;
echo $obj1->people;
echo $obj2->people;
?>
程序的运行结果为
age:20 age:30
由此可见,通过对象拷贝后,对其中一个对象值的修改不影响另外一个对象。
9.__autoload()
当实例化一个对象时,如果对应的类不存在,则该方法被调用。这个方法经常的使用方法为:在方法体中根据类名,找出类文件,然后 require_one 导入这个文件。由此,就可以成功地创建对象了,使用示例如下:Test.php:
<?php
class Test {
function hello() {
echo ‘Hello world’;
}
}
?>
index.php:
<?php
function __autoload($class) {
$file = $class . ‘.php’;
if (is_file($file) ) {
require_once($file); // 导入文件
}
}
$obj = new Test();
$obj->hello();
?>
程序的运行结果为
Hello world
在 index.php 中,由于没有包含 Test.php,在实例化 Test 对象的时候会自动调用__autoload 方法,参数 $class 的值即为类名 Test,这个函数中会把 Test.php 引进来,由此 Test 对象可以被正确地实例化。这种方法的缺点是需要在代码中文件路径做硬编码,当修改文件结构的时候,代码也要跟着修改。另一方面,当多个项目之间需要相互引用代码的时候,每个项目中可能都有自己的__autoload,这样会导致两个__autoload 冲突。当然可以把__autoload 修改成一个。这会导致代码的可扩展性和可维护性降低。由此从 PHP5.1 开始引入了 spl_autoload,可以通过 spl_autoload_register 注册多个自定义的 autoload 方法,使用示例如下:index.php
<?php
function loadprint($class) {
$file = $class . ‘.php’;
if (is_file($file)) {
require_once($file);
}
}
spl_autoload_register(‘loadprint’); // 注册自定义的 autoload 方法从而避免冲突
$obj = new Test();
$obj->hello();
?>
spl_autoload 是_autoload() 的默认实现,它会去 include_path 中寻找 $class_name(.php/.inc)。除了常用的 spl_autoload_register 外,还有如下几个方法:1)spl_autoload:_autoload() 的默认实现。2)spl_autoload_call:这个方法会尝试调用所有已经注册的__autoload 方法来加载请求的类。3)spl_autoload_functions:获取所有被注册的__autoload 方法。4)spl_autoload_register:注册__autoload 方法。5)spl_autoload_unregister:注销已经注册的__autoload 方法。6)spl_autoload_extensions:注册并且返回 spl_autoload 方法使用的默认文件的扩展名。
引申:PHP 有哪些魔术常量?
除了魔术变量外,PHP 还定义了如下几个常用的魔术常量。1)__LINE__:返回文件中当前的行号。2)__FILE__:返回当前文件的完整路径。3)__FUNCTION__:返回所在函数名字。4)__CLASS__:返回所在类的名字。5)__METHOD__:返回所在类方法的名称。与__FUNCTION__不同的是,__METHOD__返回的是“class::function”的形式,而__FUNCTION__返回“function”的形式。6)__DIR__:返回文件所在的目录。如果用在被包括文件中,则返回被包括的文件所在的目录(PHP 5.3.0 中新增)。7)__NAMESPACE__:返回当前命名空间的名称(区分大小写)。此常量是在编译时定义的(PHP 5.3.0 新增)。8)__TRAIT__:返回 Trait 被定义时的名字。Trait 名包括其被声明的作用区域(PHP 5.4.0 新增)。
六、什么是对象克隆?
对于对象而言,PHP 用的是引用传递,也就是说,对象间的赋值操作只是赋值了一个引用的值,而不是整个对象的内容,下面通过一个例子来说明引用传递存在的问题:
<?php
class My_Class {
public $color;
}
$obj1 = new My_Class ();
$obj1->color = “Red”;
$obj2 = $obj1;
$obj2->color =”Blue”; //$obj1->color 的值也会变成 ”Blue”
?>
因为 PHP 使用的是引用传递,所以在执行 $obj2 = $obj1 后,$obj1 和 $obj2 都是指向同一个内存区(它们在内存中的关系如下图所示),任何一个对象属性的修改对另外一个对象也是可见的。
在很多情况下,希望通过一个对象复制出一个一样的但是独立的对象。PHP 提供了 clone 关键字来实现对象的复制。如下例所示:
<?php
class My_Class {
public $color;
}
$obj1 = new My_Class ();
$obj1->color = “Red”;
$obj2 = clone $obj1;
$obj2->color =”Blue”; // 此时 $obj1->color 的值仍然为 ”Red”
?>
$obj2 = clone $obj1 把 obj1 的整个内存空间复制了一份存放到新的内存空间,并且让 obj2 指向这个新的内存空间,通过 clone 克隆后,它们在内存中的关系如下图所示。
此时对 obj2 的修改对 obj1 是不可见的,因为它们是两个独立的对象。在学习 C ++ 的时候有深拷贝和浅拷贝的概念,显然 PHP 也存在相同的问题,通过 clone 关键字克隆出来的对象只是对象的一个浅拷贝,当对象中没有引用变量的时候这种方法是可以正常工作的,但是当对象中也存在引用变量的时候,这种拷贝方式就会有问题,下面通过一个例子来进行说明:
<?php
class My_Class {
public $color;
}
$c =”Red”;
$obj1 = new My_Class ();
$obj1->color =&$c; // 这里用的是引用传递
$obj2 = clone $obj1; // 克隆一个新的对象
$obj2->color=”Blue”; // 这时,$obj1->color 的值也变成了 ”Blue”
?>
在这种情况下,这两个对象在内存中的关系如下图所示。
从上图中可以看出,虽然 obj1 与 obj2 指向的对象占用了独立的内存空间,但是对象的属性 color 仍然指向一个相同的存储空间,因此当修改了 obj2->color 的值后,意味着 c 的值被修改,显然这个修改对 obj1 也是可见的。这就是一个非常典型的浅拷贝的例子。为了使两个对象完全独立,就需要对对象进行深拷贝。那么如何实现呢,PHP 提供了类似于__clone 方法(类似于 C ++ 的拷贝构造函数)。把需要深拷贝的属性,在这个方法中进行拷贝:使用示例如下:
<?php
class My_Class {
public $color;
public function __clone() {
$this->color = clone $this->color;
}
}
$c =”Red”;
$obj1 = new My_Class ();
$obj1->color =&$c;
$obj2 = clone $obj1;
$obj2->color=”Blue”; // 这时,$obj1->color 的值仍然为 ”Red”
?>
通过深拷贝后,它们在内存中的关系如图 1 - 4 所示。
通过在__clone 方法中对对象的引用变量 color 进行拷贝,使 obj1 与 obj2 完全占用两块独立的存储空间,对 obj2 的修改对 obj1 也不可见。
自己整理了一篇“如果遇到代码怎么改都没效果,怎么办?”的文章,关注公众号:“琉忆编程库”,回复:“问题”,我发给你。
七、this、self 和 parent 的区别是什么?
this、self、parent 三个关键字从字面上比较好理解,分别是指这、自己、父亲。其中,this 指的是指向当前对象的指针(暂用 C 语言里面的指针来描述),self 指的是指向当前类的指针,parent 指的是指向父类的指针。以下将具体对这三个关键字进行分析。
##1.this 关键字 ##
1 <?php
2 class UserName {
3 private $name; // 定义成员属性
4 function __construct($name) {
5 $this->name = $name; // 这里已经使用了 this 指针
6 }
7 // 析构函数
8 function __destruct() {
9 }
10 // 打印用户名成员函数
11 function printName() {
12 print ($this->name.”\n”) ; // 又使用了 this 指针
13 }
14 }
15 // 实例化对象
16 $nameObject = new UserName (“heiyeluren”);
17 // 执行打印
18 $nameObject->printName (); // 输出: heiyeluren
19 // 第二次实例化对象
20 $nameObject2 = new UserName (“PHP5”);
21 // 执行打印
22 $nameObject2->printName (); // 输出:PHP5
23 ?>
上例中,分别在 5 行和 12 行使用了 this 指针,那么 this 到底是指向谁呢?其实,this 是在实例化的时候来确定指向谁,例如,第一次实例化对象的时候(16 行),当时 this 就是指向 $nameObject 对象,那么执行第 12 行打印的时候就把 print($this->name) 变成了 print ($nameObject->name),输出 ”heiyeluren”。对于第二个实例化对象,print($this- >name) 变成了 print($nameObject2->name),于是就输出了 ”PHP5″。所以,this 就是指向当前对象实例的指针,不指向任何其他对象或类。
2.self 关键字
先要明确一点,self 是指向类本身,也就是 self 是不指向任何已经实例化的对象,一般 self 用来访问类中的静态变量。
1 <?php
2 class Counter {
3 // 定义属性,包括一个静态变量
4 private static $firstCount = 0;
5 private $lastCount;
6 // 构造函数
7 function __construct() {
8 // 使用 self 来调用静态变量, 使用 self 调用必须使用::(域运算符号)
9 $this->lastCount = ++ selft::$firstCount;
10 }
11 // 打印 lastCount 数值
12 function printLastCount() {
13 print ($this->lastCount) ;
14 }
15 }
16 // 实例化对象
17 $countObject = new Counter ();
18 $countObject->printLastCount (); // 输出 1
19 ?>
上述示例中,在第 4 行定义了一个静态变量 $firstCount,并且初始值为 0,那么在第 9 行的时候调用了这个值,使用的是 self 来调用,中间使用域运算符“::”来连接,这时候调用的就是类自己定义的静态变量 $firstCount,它与下面对象的实例无关,只是与类有关,无法使用 this 来引用,只能使用 self 来引用,因为 self 是指向类本身,与任何对象实例无关。
3.parent 关键字
parent 是指向父类的指针,一般使用 parent 来调用父类的构造函数。
1 <?php
2 // 基类
3 class Animal {
4 // 基类的属性
5 public $name; // 名字
6 // 基类的构造函数
7 public function __construct($name) {
8 $this->name = $name;
9 }
10 }
11 // 派生类
12 class Person extends Animal // Person 类继承了 Animal 类
13 {
14 public $personSex; // 性别
15 public $personAge; // 年龄
16 // 继承类的构造函数
17 function __construct($personSex, $personAge) {
18 parent::__construct (“heiyeluren”); // 使用 parent 调用了父类的构造函数
19 $this->personSex = $personSex;
20 $this->personAge = $personAge;
21 }
22 function printPerson() {
23 print ($this->name . ” is ” . $this->personSex . “,this year ” . $this->personAge) ;
24 }
25 }
26 // 实例化 Person 对象
27 $personObject = new Person (“male”, “21”);
28 // 执行打印
29 $personObject->printPerson (); // 输出:heiyeluren is male,this year 21
30 ?>
上例中,成员属性都是 public 的,特别是父类的,是为了供继承类通过 this 来访问。第 18 行:parent::__construct(“heiyeluren”),使用了 parent 来调用父类的构造函数进行对父类的初始化,因为父类的成员都是 public 的,于是就能够在继承类中直接使用 this 来访问从父类继承的属性。
八、抽象类与接口有什么区别与联系?
抽象类应用的定义如下:
abstract class ClassName{
}
抽象类具有以下特点:1)定义一些方法,子类必须实现父类所有的抽象方法,只有这样,子类才能被实例化,否则子类还是一个抽象类。2)抽象类不能被实例化,它的意义在于被扩展。3)抽象方法不必实现具体的功能,由子类来完成。4)当子类实现抽象类的方法时,这些方法的访问控制可以和父类中的一样,也可以有更高的可见性,但是不能有更低的可见性。例如,某个抽象方法被声明为 protected 的,那么子类中实现的方法就应该声明为 protected 或者 public 的,而不能声明为 private。5)如果抽象方法有参数,那么子类的实现也必须有相同的参数个数,必须匹配。但有一个例外:子类可以定义一个可选参数(这个可选参数必须要有默认值),即使父类抽象方法的声明里没有这个参数,两者的声明也无冲突。下面通过一个例子来加深理解:
<?php
abstract class A{
abstract protected function greet($name);
}
class B extends A {
public function greet($name, $how=”Hello “) {
echo $how.$name.”\n”;
}
}
$b = new B;
$b->greet(“James”);
$b->greet(“James”,”Good morning “);
?>
程序的运行结果为
Hello James
Good morning James
定义抽象类时,通常需要遵循以下规则:1)一个类只要含有至少一个抽象方法,就必须声明为抽象类。2)抽象方法不能够含有方法体。接口可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。在 PHP 中,接口是通过 interface 关键字来实现的,与定义一个类类似,唯一不同的是接口中定义的方法都是公有的而且方法都没有方法体。接口中所有的方法都是公有的,此外接口中还可以定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口所覆盖。要实现一个接口,可以通过关键字 implements 来完成。实现接口的类中必须实现接口中定义的所有方法。虽然 PHP 不支持多重继承,但是一个类可以实现多个接口,用逗号来分隔多个接口的名称。下面给出一个接口使用的示例:
<?php
interface Fruit
{
const MAX_WEIGHT = 3; // 静态常量
function setName($name);
function getName();
}
class Banana implements Fruit
{
private $name;
function getName() {
return $this->name;
}
function setName($_name) {
$this->name = $_name;
}
}
$b = new Banana(); // 创建对象
$b->setName(“ 香蕉 ”);
echo $b->getName();
echo “<br />”;
echo Banana::MAX_WEIGHT; // 静态常量
?>
程序的运行结果为
香蕉 3
接口和抽象类主要有以下区别:抽象类:PHP5 支持抽象类和抽象方法。被定义为抽象的类不能被实例化。任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。被定义为抽象的方法只是声明了其调用方法和参数,不能定义其具体的功能实现。抽象类通过关键字 abstract 来声明。接口:可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。在这种情况下,可以通过 interface 关键字来定义一个接口,在接口中声明的方法都不能有方法体。二者虽然都是定义了抽象的方法,但是事实上两者区别还是很大的,主要区别如下:1)对接口的实现是通过关键字 implements 来实现的,而抽象类继承则是使用类继承的关键字 extends 实现的。2)接口没有数据成员(可以有常量),但是抽象类有数据成员(各种类型的成员变量),抽象类可以实现数据的封装。3)接口没有构造函数,抽象类可以有构造函数。4)接口中的方法都是 public 类型,而抽象类中的方法可以使用 private、protected 或 public 来修饰。5)一个类可以同时实现多个接口,但是只能实现一个抽象类。
预告:PHP 面试常考内容之面向对象(3)将于本周五(2019.2-15)更新。
以上内容摘自《PHP 程序员面试笔试宝典》书籍,该书已在天猫、京东、当当等电商平台销售。
更多 PHP 相关的面试知识、考题可以关注公众号获取:琉忆编程库对本文有什么问题或建议都可以进行留言,我将不断完善追求极致,感谢你们的支持。