构造时成员初始化的陷阱

转自:http://coolshell.cn/articles/1106.html



让我们先来看一段代码:

 1 public class Test {
 2     public static void main(String[] args) {
 3         SubClass subClass = new SubClass();
 4         System.out.println(subClass.whenAmISet);
 5     }
 6 }
 7
 8 class ParentClass {
 9     ParentClass() {
10         print();
11     }
12
13     void print() {
14         System.out.println("I‘m ParentClass");
15     }
16 }
17
18 class SubClass extends ParentClass {
19     public String whenAmISet = "set when declared";
20
21     // SubClass() {
22     // super();
23     // }
24
25     @Override
26     void print() {
27         System.out.println("I‘m SubClass");
28         whenAmISet = "set in print()";
29     }
30 }

在继续往下阅读之前,请先给自己一些时间想一下上面的这段程序的输出是什么?是的,这看起来的确相当简单,甚至不需要编译和运行上面的代码,我们也应该知道其答案,那么,你觉得你知道答案吗?你确定你的答案正确吗?



很多人都会觉得那段程序的输出应该是“set in print()”,这是因为当子类SubClass 的构造函数被调用时,其会隐晦地调用其基类ParentClass的构造函数(通过super()函数),于是基类ParentClass的构造函数会调用print() 函数,因为这个类的实例是SubClass的,而且在子类SubClass中对这个函数使用了override关键字,所以,实际上调用到的是:SubClass.print(),而这个方法设置了whenAmISet 成员变量的值为:“set in print()”。

当然,上面的结论是错误的。如果你编译并运行这个程序,你会发现,程序实际输出的是“set when declared ”。怎么为这样呢?难道是基类ParentClass 的print() 方法被调用啦?也不是!你可以在基类的print中输出点什么看看,你会发现程序运行时,ParentClass.print()并没有被调用到(不然这对于Java所有的应用程序将会是一个极具灾难性的Bug)。

其实,代码的输出是:

I‘m SubClass
set when declared

虽然上面的结论是错误的,但推导过程是合理的,只是不完整,下面是整个运行的流程:

  1. 进入SubClass 构造函数。
  2. SubClass 成员变量的内存被分配。
  3. ParentClass 构造函数被隐含调用。
  4. ParentClass 构造函数调用print()。
  5. SubClass 的print 设置whenAmISet 值为 “set in print()”。
  6. SubClass 的成员变量初始化被调用。
  7. 执行SubClass 构造函数体。

等一等,这怎么可能?在第6步,SubClass 成员的初始化居然在 print() 调用之后?是的,正是这样,我们不能让成员变量的声明和初始化变成一个原子操作,虽然在Java中我们可以把其写在一起,让其看上去像是声明和初始化一体。但这只是假象,我们的错误就在于我们把Java中的声明和初始化看成了一体。在C++的世界中,C++并不支持成员变量在声明的时候进行初始化,其需要你在构造函数中显式的初始化其成员变量的值,看起来很土,但其实C++用心良苦。

在面向对象的世界中,因为程序以对象的形式出现,导致了我们对程序执行的顺序雾里看花。所以,在面向对象的世界中,程序执行的顺序相当的重要。

下面是对上面各个步骤的逐条解释。

  1. 进入构造函数。
  2. 为成员变量分配内存。
  3. 除非你显式地调用super(),否则Java 会在子类的构造函数最前面偷偷地插入super() 。
  4. 调用父类构造函数。
  5. 调用print,因为被子类override,所以调用的是子类的。
  6. 于是,初始化发生在了print()之后。这是因为,Java需要保证父类的初始化早于子类的成员初始化,否则,在子类中使用父类的成员变量就会出现问题。
  7. 正式执行子类的构造函数(当然这是一个空函数,虽然我们没有声明)。

你可以查看《Java语言的规格说明书》中的相关章节来了解更多的Java创建对象时的细节。

C++的程序员应该都知道,在C++的世界中在“构造函数中调用虚函数”是不行的,Effective C++ 条款9:Never call virtual functions during construction or destruction,Scott Meyers已经解释得很详细了。

在语言设计的时候,“在构造函数中调用虚函数”是个两难的问题。

  1. 如果调用的是父类的函数的话,这个有点违反虚函数的定义。
  2. 如果调用的是子类的函数的话,这可能产生问题的:因为在构造子类对象的时候,首先调用父类的构造函数,而这时候如果去调用子类的函数,由于子类还没有构造完成,子类的成员尚未初始化,这么做显然是不安全的。

C++选择了第一种,而Java选择了第二种。

  1. C++类的设计相对比较简陋,通过虚函数表来实现,缺少类的元信息。
  2. 而Java类的则显得比较完整,有super指针来导航到父类。
时间: 2024-10-23 23:48:13

构造时成员初始化的陷阱的相关文章

Java构造时成员初始化的陷阱

1.首先列出代码 Base.java public class Base { Base() { preProcess(); } void preProcess() {} } Derived.java public class Derived extends Base { public String whenAmISet = "set when declared"; @Override void preProcess() { whenAmISet = "set in prePr

Java 构造时成员初始化的陷阱

1.首先列出代码 Base.java public class Base { Base() { preProcess(); } void preProcess() {} } Derived.java public class Derived extends Base { public String whenAmISet = "set when declared"; @Override void preProcess() { whenAmISet = "set in prePr

C++:用成员初始化列表对数据成员初始化

1.在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行. 例如: class Complex{ private: double real; double imag; public: Complex(double r,double i) //声明构造函数原型 { ........... } }; Complex::Complex(double r,double i) //在构造函数中用赋值语句对数据成员赋初值 { real = r; imag = i; } 2.另一种初始化数据成员的

【C/C++】构造函数、默认构造函数、成员初始化列表

常见问题 Q1. 下列关于构造函数的描述中,错误的是( ) A. 构造函数可以设置默认的参数 B. 构造函数在定义类对象时自动执行 C. 构造函数可以是内联函数 D. 构造函数不可以重载 Q2. 下列代码中a.b的各个成员变量值是多少? 1 class Student 2 { 3 public: 4 Student() {} 5 void show(); 6 private: 7 string name; 8 int number; 9 int score; 10 }; 11 Student a

C++对象模型——成员初始化列表(第二章)

2.4    成员初始化列表 (Member Initialization List) 当编写一个类的构造函数时,有可能设定类成员的初始值,或者通过成员初始化列表初始化,或者在构造函数内初始化,除了四种情况,其实任何选择都差不多. 本节中,首先澄清何时使用初始化列表才有意义,然后解释初始化列表内部的真正操作是什么,然后再看一些微妙的陷阱.  下列情况中,为了让程序能够被顺利编译,必须使用成员初始化列表(不能在构造函数内初始化): 1.    当初始化一个引用的成员时 2.    当初始化一个常量

C#类的成员初始化顺序

首先我们来看看引用类型的成员初始化过程 我们来看一个例子吧 class Program {     static void Main(string[] args)     {         DriveB d = new DriveB();     } } class BaseA {     static DisplayClass a = new DisplayClass("基类静态成员初始化"); DisplayClass BaseA_c = new DisplayClass(&qu

Item 9:在析构/构造时不要调用虚函数 Effective C++笔记

Item 9: Never call virtual functions during construction or destruction. 父类构造期间,对虚函数的调用不会下降至子类.如果这并非你的意图,请不要这样做! 这个问题阿里实习面试曾经问到过,看这篇文章: 2014阿里巴巴面试经历 看Scott Meyers举的例子: class Transaction { // base class for all public: // transactions Transaction(){ /

C++类中常量数据成员和静态数据成员初始化

常量数据成员初始化原则: 在每一个构造函数的初始化列表中初始化 静态数据成员初始化原则: 类内声明,类外初始化(因为它是属于类的,不能每构造一个对象就初始化一次) // test_max.cpp : 定义控制台应用程序的入口点. #include "stdafx.h" #include <iostream> #include <vector> using namespace std; class A { public: A(int i):a(0) {} A():

【转载】C++ 与“类”有关的注意事项总结(十二):按成员初始化 与 按成员赋值

原文:C++ 与"类"有关的注意事项总结(十二):按成员初始化 与 按成员赋值 一.按成员初始化(与构造函数和拷贝构造函数有关) 用一个类对象初始化另一个类对象,比如: Account oldAcct( "Anna Livia Plurabelle" ); Account newAcct( oldAcct ); 被称为缺省的按成员初始化(default memberwise initialization),缺省是因为它自动发生,无论我们是否提供显式构造函数,按成员是