9.11 构造函数

构造函数是一类特殊的成员函数,它主要用于为对象分配内存空间,并对类的数据成员进行初始化,或者说设置实例对象到某一个初始状态(数据和状态相对应)。构造函数分为实例构造函数和静态构造函数两种,下面我们将分别对它们进行介绍。

实例构造函数

实例构造函数用于创建和初始化实例。在使用new运算符创建新的对象时,将调用类的实例构造函数,如代码清单9-24所示。

代码清单9-24 实例构造函数


class Car

{

//字段

private string name;

//构造函数

public Car()

{

name="Polo";

}

}


实例构造函数特点如下:

❑构造函数的名字必须和类同名;

❑构造函数不允许有返回类型;

❑构造函数可以具有0~n个参数;

❑构造函数可以重载,以提供初始化类的不同方法;

❑若在声明类时没有定义构造函数,则系统自动生成一个函数体为空的默认构造函数;

❑可以对实例构造函数使用public、protected、private修饰符;

❑引用基类构造函数时使用base()方法,如果有参数则将参数传入,如base(参数1,参数2……);

❑引用自身构造函数时使用this()方法,如果有参数则将参数传入,如this(参数1,参数2……)。

1.默认构造函数

如果没有为类提供任何构造函数,那么CLR将会自动提供一个构造函数作为默认构造函数,它是实例构造函数的一种,它的特点是:

❑没有参数;

❑构造函数体即方法体为空。

我们来看这样一个类,如代码清单9-25所示。

代码清单9-25 默认构造函数


//没有提供任何构造函数,此时CLR会自动提供一个默认构造函数

class Car

{

private string name;

public void DoSmothing(string name)

{

this.name=name;

}

public string Name

{

get

{

return name;

}

}

}


从上述代码可以看到,我们没有为类提供任何构造函数,那么对它进行编译,看看CLR是否自动生成了默认构造函数呢?从反编译后的Car类和代码清单9-26可以看出,CLR提供了一个默认构造函数.ctor,如图9-9所示。

9.11 构造函数 - 图1

图 9-9 Car类的CIL代码结构

其内容如代码清单9-26所示。

代码清单9-26 Car类默认构造函数.ctor的CIL代码


.method public hidebysig specialname rtspecialname instance void.ctor()cil managed

{

//Code size 7(0x7)

.maxstack 8

IL_0000:ldarg.0

IL_0001:call instance void[mscorlib]System.Object:.ctor()

IL_0006:ret

}//end of method Car:.ctor


这是由CLR生成的默认构造函数,它没有任何参数,它的任务就是简单地调用下基类(此处是System.Object,任何类都是System.Object的直接或间接的派生类,这里是直接派生自System.Object类)的构造函数(IL_0001行)。

现在,我们为Car类提供了一个构造函数,如代码清单9-27所示。

代码清单9-27 带构造函数的Car类


class Car

{

private string name;

//我们提供了一个构造函数,简单的为name自动赋值

public Car()

{

name="Polo";

}

public void DoSmothing(string name)

{

this.name=name;

}

public string Name

{

get

{

return name;

}

}

}


我们看看添加了构造函数以后,反编译出的CIL代码有何不同,如代码清单9-28所示。

代码清单9-28 添加了构造函数的Car类的CIL代码


.method public hidebysig specialname rtspecialname instance void.ctor()cil managed

{

//Code size 21(0x15)

.maxstack 8

IL_0000:ldarg.0

IL_0001:call instance void[mscorlib]System.Object:.ctor()

IL_0006:nop

IL_0007:nop

IL_0008:ldarg.0

IL_0009:ldstr"Polo"

IL_000e:stfld string ProgrammingCSharp4.Car:name

IL_0013:nop

IL_0014:ret

}//end of method Car:.ctor


我们可以发现,除了仍然调用基类的构造函数以外(IL_0001),新增了为字段赋值的代码(加粗部分),这也正是我们提供的构造函数所做的工作。

2.带参数的构造函数

如果想在创建类实例的同时就使用一些值进行初始化,该怎么办呢?答案就是构造函数,而且是带参数的构造函数,上节讲到的构造函数是默认构造函数,它没有参数。前面我们讲了,构造函数可以重载,那么我们在上例的基础上,再提供一个带参数的构造函数,先来看看C#代码,如代码清单9-29所示。

代码清单9-29 带参数的构造函数


class Car

{

private string name;

public Car()

{

name="Polo";

}

//带参数的构造函数

public Car(string name)

{

this.name=name;//使用this关键字以和参数的name分开

}

public void DoSmothing(string name)

{

this.name=name;

}

public string Name

{

get

{

return name;

}

}

}


那么,我们就可以在实例化Car类的同时,初始化类的字段name,如代码清单9-30所示。

代码清单9-30 使用带参数的构造函数初始化类的实例


class ClassExample

{

static void Main()

{

//使用带参数的构造函数实例化Car类

Car car=new Car("Benz");

Console.WriteLine(car.Name);

}

}


其输出如下所示:


Benz


下面,我们看看代码清单9-29编译生成的CIL代码,如图9-10所示。

9.11 构造函数 - 图2

图 9-10 Car类的CIL代码结构

可见,不带参数的默认构造函数仍然存在。其中,带参数的构造函数CIL代码如代码清单9-31所示。

代码清单9-31 构造函数的CIL代码


.method public hidebysig specialname rtspecialname instance void.ctor(string name)cil managed

{

//Code size 17(0x11)

.maxstack 8

IL_0000:ldarg.0

IL_0001:call instance void[mscorlib]System.Object:.ctor()

IL_0006:nop

IL_0007:nop

IL_0008:ldarg.0

IL_0009:ldarg.1

IL_000a:stfld string ProgrammingCSharp4.Car:name

IL_000f:nop

IL_0010:ret

}//end of method Car:.ctor


从代码清单9-31可以看出:

❑构造函数执行时,首先调用基类的默认构造函数(不带参数的)(IL_0001);

❑执行参数的赋值操作(IL_000a)。

3.静态构造函数

静态构造函数用于初始化任何静态数据,或用于执行仅需执行一次的特定操作。在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。

下面来看一个静态构造函数示例,如代码清单9-32所示。

代码清单9-32 静态构造函数


class Car

{

private static string name;

//静态构造函数,仅执行一次

static Car()

{

Console.WriteLine(“静态构造函数调用了!”);

name="Polo";

}

//静态属性

public static string Name

{

get

{

return name;

}

}

}


静态构造函数有以下几个特性:

❑静态构造函数不能使用任何访问修饰符;

❑静态构造函数不能具有任何参数;

❑静态构造函数的执行时机,是在创建类的第一个实例,或者访问任何类的静态成员之前,自动执行的,并仅执行一次,以完成对静态成员的初始化工作;

❑不能直接调用静态构造函数;

❑程序中,用户无法控制执行静态构造函数的时机。

我们看一个实例,如代码清单9-33所示。

代码清单9-33 静态构造函数使用示例


class ClassExample

{

static void Main()

{

System.Console.WriteLine(Car.Name);

System.Console.WriteLine(Car.Name);

}

}


在这里,我们练习调用了两次Car类的静态成员属性Name,其输出如下:

静态构造函数调用了!


Polo

Polo


我们连续访问了两次Car对象的静态Name属性,只打印出了一次“静态构造函数调用了!”,这说明静态构造函数仅仅执行了一次。

同样,我们也观察一下静态构造函数生成的CIL代码,大家注意一下有什么不同,如图9-11所示。

9.11 构造函数 - 图3

图 9-11 静态函数生成的CIL代码结构

4.构造函数的可访问性

除了刚刚讲过的静态构造函数没有访问修饰符以外,实例构造函数都要有一个访问修饰符,如果没有表示隐式的private修饰符,构造函数是private,当前类只能通过自身进行实例化(构造函数虽然为private,但自身仍然是可以访问的)。除了private以外,构造函数还可以使用public以及protected修饰符,前者不需多讲,我们一直在用;后者表示构造函数是“受保护”的,如何保护呢?这意味着,当前类不能直接被实例化,而只能实例化当前类的子类,相关内容我们将在第10章具体阐述。

5.私有构造函数

下面,我们讨论私有构造函数。顾名思义,构造函数的访问修饰符为private,这意味着其他对象无法访问类的构造函数,也就无法直接将类进行实例化。因此,只能依赖类自身提供访问自身实例的途径。下面,我们看一个私有构造函数的例子,如代码清单9-34所示。

代码清单9-34 私有构造函数


class Car

{

private string name;

//私有构造函数,只能在内部调用,也就是说只能自己实例化自己

private Car()

{

Console.WriteLine(“私有实例构造函数调用了!”);

name="Polo";

}

//静态方法,用以返回类的示例

public static Car getInstance()

{

return new Car();

}

//属性

public string Name

{

get

{

return name;

}

}

}


在上述代码中,特意提供了一个静态的、返回Car类型的方法:getInstance(),它的责任是实例化自身,并将实例返回。建议大家参阅《设计模式之禅》中的单例模式,此模式是私有构造函数的典型应用之一。接下来我们看看如何使用这样一个构造函数私有的类,如代码清单9-35所示。

代码清单9-35 私有构造函数类的调用


class ClassExample

{

static void Main()

{

//通过调用静态方法getInstance()返回一个Car类型的实例

Car car=Car.getInstance();

System.Console.WriteLine(car.Name);

}

}


输出为:


私有实例构造函数调用了!

Polo