类(四)C++、Java、Python中属性、构造方法初始化顺序、父类子类构造方法调用顺序、初始化参数列表

一、C++

1、不存在继承时候类的实例化顺序:
先声明/初始化类中变量,再执行构造函数

变量的声明/初始化顺序又按照先静态再非静态

无论创建几个对象, 该类的静态成员只构建一次, 所以静态成员的构造函数只调用1次

class MyClass
{
	public MyClass(int a) //3. 然后才是执行构造函数
	{
		val = a;
	}
	public int val = 20; //1. 首先声明int型变量val并赋值20
	public int i = 10; //2. 其次声明int型变量 i 并赋值10
}

int main()
{
	MyClass a1 = new MyClass(201); //0.声明MyClass类型对象a1,并调用构造函数进行实例化
	return 0;
}

2、存在继承时候类的实例化顺序:

静态数据成员的构造函数 -> 父类的构造函数 ->
非静态的数据成员的构造函数 -> 自己的构造函数

#include <iostream>
#include <Windows.h>

using namespace std;

class M{
	public:
		M(){cout << __FUNCTION__ << "uuu"<<endl;}
		M(int x,int y){cout << __FUNCTION__ << "uuu"<<endl;this->x=x;this->y=y;}
		
};

class N{
	public:
		N(){cout << __FUNCTION__ << endl;}
};

class A{
	public:
	A(){cout << __FUNCTION__ << endl;}
};

class B :public A{
	public:
		B(){cout << __FUNCTION__ << endl;}
	private:
		M m1;
		M m2=M(1,1);
		static N ms;
};

N B::ms; /* 静态成员 */

int main(){
	B b;
	system("pause");
	return 0;
}

运行结果
N::N 静态数据成员的构造函数
A::A 父类的构造函数
M::M 非静态数据成员的构造函数 uuu
M::M 非静态数据成员的构造函数 vvv
B::B 自己的构造函数

最先输出的是静态N 即使静态N在非静态m1,m2之后
另外需要注意对非静态数据成员赋初值在C++ 11 以后才支持
M m2=M(1,1)这行代码会出警告:
[Warning] non-static data member initializers only available 
with -std=c++11 or -std=gnu++11

3、父类的多个构造函数子类实例化时调用哪个

子类无参构造 调用父类无参构造

#include <iostream>
using namespace std;
class A{
  public:
	  int a;
	  int b;
 
	  A(){cout<<"A Constructed\n";}
};
 
class B:A{
  public:
	  int c;
	  int d;
 
	  B(){cout<<"B Constructed\n";}
};
 
int main(){
	B b;
	return 0;
}

运行结果
A Constructed
B Constructed


子类B有参构造函,仍调用父类A的无参构造 即使父类存在和子类构造参数匹配的构造函数

#include <iostream>
using namespace std;
 
class A{
  public:
	  int a;
	  int b;
	 
	  A(){cout<<"A Constructed 1\n"; }
	  A(int a,int b){
	    this->a=a;
	    this->b=b;
	    cout<<"A Constructed 2\n";
	  }
};
 
class B:A{
  public:
	  int c;
	  int d;
	 
	  B(){cout<<"B Constructed 1\n";}
	 
	  B(int c,int d){
	    this->c=c;
	    this->d=d;
	    cout<<"B Constructed 2\n";
	  } 
};
 
int main(){
  B b(1,1);
  return 0;
}

运行结果
A Constructed 1
B Constructed 2
若要实现调用父类的有参构造 采用初始化参数列表
关于初始化参数列表更具体的相关内容见本文最后

#include <iostream>
using namespace std;
 
class A{
  public:
	  int a;
	  int b;
	  A(){cout<<"A Constructed 1\n";}
	 
	  A(int a,int b){
	    this->a=a;
	    this->b=b;
	    cout<<"A Constructed 2\n";
	    cout<<"this.a is "<<this->a<<" this.b is "<<this->b<<"\n";
	  }
};
 
class B:A{ 
  public:
	  int c;
	  int d;
	 
	  B(){cout<<"B Constructed 1\n";}
	 
	  B(int c,int d):A(100,200){
	    this->c=c;
	    this->d=d;
	    cout<<"B Constructed 2\n";
	  }
};
 
int main()
{
  B b(1,1);
  cout<<"b.c is "<<b.c<<" b.d is "<<b.d <<"\n";
  return 1;
}

运行结果
A Constructed 2
this.a is 100 this.b is 200 
B Constructed 2
b.c is 1 b.d is 1

总结
在C++中,子类的构造过程中必须调用其父类的构造函数,但如果父类有多个构造函数时,父类构造函数的选择遵循两个规则:

第一个规则: 子类的构造过程中,必须调用其父类的构造方法
若不写任何构造方法,编译器会自动添加一个默认构造方法,就是无参构造方法
若写了构造方法,即使写的构造方法都是有参的 编译器也不会自动添加默认构造方法

所以子类构造方法中,若没有显示地通过初始化参数列表调用基类的含参构造方法,默认调用父类无参构造方法,不管创建子类实例对象时是使用子类有参还是无参的构造方法,若通过初始化参数列表则不会调用父类的无参构造函数

第二个规则: 如果子类的构造方法中既没有显示的通过初始化参数列表调用基类构造方法,而基类中又没有无参的构造方法(这里是说我们写了父类的其他含参构造方法而没有写无参),编译出错,若父类任何构造函数都没写是不会报错 因为编译器会自动加上一个

二、Java

注意C++中以上关于类中变量、构造方法的初始化顺序及父类构造函数的选择问题的全部叙述在Java中完全一样
存在的两个区别是:
1、C++显示通过初始化参数列表指定调用父类含参构造函数在Java中是通过super(参数列表)来实现的

2、C++和Java对于数据成员/变量什么时候实例化不太一样,java必须通过new显示实例化才可以,而C++声明即实例化

具体见实例说明

父类
class People{

    String name;
    public People(){
        System.out.printf("1");
    }

    public People(String name){
        System.out.printf("2");
        this.name=name;
    }

}
子类
class Child extends People{

    People father;

    public Child(String name){
        System.out.printf("3");
        this.name=name;
        father=new People(name+"F");
    }

    public Child(){
        System.out.printf("4");
    }
}
测试类
public class Test {
    public static void main(String[] args) {

        Child c=new Child("mike");

    }
}
运行结果
132 
首先,该子类构造方法并没有显示的通过super()调用基类的构造方法
	所以默认会调用基类中没有参数的构造方法 输出1
其次,再调用子类自己的含参构造方法 输出3 
	然后,在自己的构造方法中因为new实例化了含参的基类对象father 
	因此再调用基类Pepole的含参构造 输出2 
	
	注意这里输出的这个2 和继承完全没关系
		是因为father本来就是People类型 是Child的一个数据成员 实例化father这个对象 
		所用到的构造函数编译器自己去匹配People的构造函数:需要调用含一个参数的构造函数


注意Java数据成员与构造方法调用顺序和C++一样,都是
静态数据成员->父类构造函数->非静态数据成员->自己构造函数

然而这里在调用了父类构造输出1之后 并没有先对非静态数据成员father进行实例化输出2 ,
而是直接进入自己的构造函数输出3 然后再输出2

这并不矛盾因为
Java必须显示通过new才会实例化/初始对象 而这里的father是在自己的构造方法中才new的
而C++是声明即实例化不需要new显示实例化
因此在本文最开始C++代码中m1,m2  两个非静态数据成员 均在类子B调用自己的构造函数之前进行实例化了
修改上述Child如下:

class Child extends People{

    People father=new People(name+"F");

    public Child(String name){
        System.out.printf("3");
        this.name=name;
    }

    public Child(){
        System.out.printf("4");
    }
}

运行结果
123
同样:静态数据成员->父类构造函数->非静态数据成员->自己构造函数
首先,该子类构造方法并没有显示的通过super()调用基类的构造方法
	所以默认会调用基类中没有参数的构造方法 输出1
其次,由于非静态数据成员father在声明时就通过new实例化 
	因此需要先对费静态数据成员实例初始化 输出2
最后,调用子类自己的含参构造方法 输出3 
继续修改Chlid如下
class Child extends People{

    People father;

    public Child(String name){
        System.out.printf("3");
        this.name=name;
    }

    public Child(){
        System.out.printf("4");
    }
}

运行结果
13
类数据成员father始终没有通过new实例化
继续修改Child让其在构造方法中吸纳时调用super()
class Child extends People{

    People father;

    public Child(String name){
    	super(name);
        System.out.printf("3");
        this.name=name;
        father=new People(name+"F");
    }
    
    public Child(){
        System.out.printf("4");
    }
}

运行结果
232
最开始通过super()调用了父类的含参构造函数输出2 不再是默认构造

Java进阶

class Base {
	private String name="base";
	
	public Base() {
		tellName();
		printName();
	}
	
	public void tellName() {
		System.out.println("Base tell name:"+name);
	}
	
	public void printName() {
		System.out.println("Base print name:"+name);
	}
}

class Dervied extends Base {
	private String name="dervied";
	
	public Dervied() {
		tellName();
		printName();
	}
	
	public void tellName() {
		System.out.println("Dervied tell name:"+name);
	}
	
	public void printName() {
		System.out.println("Dervied print name:"+name);
	}
}

public class Test2 {
	public static void main(String[] args) {
		new Dervied();
	}	
}
运行结果
Dervied tell name: null
Dervied print name: null
Dervied tell name: dervied
Dervied print name: dervied

1、 执行 new dervied,因为 Dervied继承于父类Base,所以先调用父类无参构造方法 
  注意,这时子类name域的值尚未初始化
  因为初始化顺序是:静态数据成员->父类构造函数->非静态数据成员->自己构造函数
同时
因为子类重写了父类的方法tellName()printName(),
所以在父类的构造函数中调用的 telIName方法实际上是调用子类自己的Dervied. tellName方法
tellName方法要打印子类自己的Dervied.name域的值,但是当前 Dervied对象中的name域还没有被初始化
所以打印出来的值为 null
printName方法执行与 tellName方法一致。

2、非静态数据成员初始化 name被赋值
3、调用子类 Dervied中的构造方法,构造方法中再执行tellName()printName()
因为这时子类的name域已经初化完,因此可以正常打印
同样这里调用的仍然是子类自己的tellName()printName()


父类被重写的方法子类一般不会去执行 都是执行被重写后自己的那个方法 
当然java也像C++,Python中那样也有方法可以实现在子类中仍然调用父类被重写的方法
本文暂时不讨论

三、Python

传送门(https://blog.csdn.net/Wjf7496/article/details/109649120)

四、C++中初始化参数列表解析

C++成员变量初始化:

1、普通变量:
不考虑效率在构造函数中进行赋值,考虑则在构造函数的初始化列表中进行

2、静态变量:属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。在这种性质上理解,有点类似于全局变量的唯一性

函数体内static变量作用范围是该函数体,该变量内存只被分配一次,
因此其值在下次调用时维持上次的值,这不同于auto变量

模块内的static全局变量可以被模块内所有函数访问,但不能被模块外的其它函数访问
模块内的static函数只可被这一模块内的其他函数调用

类中的static变量属于整个类所拥有,对类的所有对象只有一份拷贝
类中的static方法属于整个类所拥有,该函数不接受this指针,只能访问类的static变量

3、const 常量变量:
需要在声明/创建的时候即初始化,一般采用在构造函数的初始化列表中进行

1)使用初始化列表提高效率

class Student 
{
	public:
		Student(string in_name, int in_age)
		{
			name = in_name;
			age = in_age;
		}
	private :
		string name;
		int age;
};

因为在构造函数中,是对name进行赋值,不是初始化
而string对象会先调用它的默认构造函数,再调用string类(貌似是basic_string类)的赋值构造函数;
对于上例的age,因为int是内置类型,应该是赋值的时候获得了初值。

要对成员进行初始化,而不是赋值,可以采用初始化列表(member initialization list)

class Student 
{
	public:
		Student(string in_name, int in_age):name(in_name),age(in_age) {}
	private :
		string name;
		int age;
};

这里在初始化的时候调用的是string的拷贝构造函数,而不采用初始化参数列表会会调用两次构造函数
因此初始化参数列表从性能上会有不小提升
有的情况下,是必须使用初始化列表进行初始化的:const对象、引用对象
2)初始化列表的初始顺序:严格按照声明次序,而不是在初始化列表中的顺序进行初始化

#include <iostream>
using namespace std;

class Base 
{
	public:
		Base(int i) : m_j(i), m_i(m_j) {}
		Base() : m_j(0), m_i(m_j) {}
		
		int get_i() const
		{
			return m_i;
		}
		int get_j() const
		{
			return m_j;
		}

	private:
		int m_i;
		int m_j;
};

int main()
{
Base obj(98);
cout << obj.get_i() << endl << obj.get_j() << endl;
return 0;
}

输出为一个随机数和98,
因为初始化列表对成员变量的初始化是严格按照声明次序,而不是在初始化列表中的顺序进行初始化,
如果改为赋值初始化(在构造函数中赋值)则不会出现这个问题

这里Base obj(98)会调用Base(int i) : m_j(i), m_i(m_j) {}这个构造函数
由于声明时
private:
		int m_i;
		int m_j;
先声明的m_i,因此初始化参数列表 
先执行 m_i(m_j) 但此时m_j没值 因此obj.get_i() 得到一个随机数
再执行m_j(i),将Base(int i)的参数i(值为98)赋值给m_j  因此obj.get_i() =98

尽管参数列表是m_j(i), m_i(m_j)  最后也不会得到两个98 的运行结果
也不会先执行m_j(i)得到m_j=i=98 
再执行m_i(m_j) 得到m_i=m_j=98

当然,为了使用初始化列表,还是严格注意声明顺序,比如先声明数组大小,再声明数组这样。
C++多继承时父类构造数的调用顺序
构造函数初始化按下列顺序被调用:
首先,任何虚基类的构造函数按照它们被继承的顺序构造;
其次,任何非虚基类的构造函数按照它们被继承的顺序构造;
最后,任何成员对象的构造函数按照它们声明的顺序调用;

#include <iostream>
using namespace std;
class OBJ1{
	public:
		OBJ1(){ cout<<"OBJ1\n"; }
};

class OBJ2{
	public:
		OBJ2(){ cout<<"OBJ2\n";}
};

class Base1{
	public:
		Base1(){ cout<<"Base1\n";}
};

class Base2{
	public:
		Base2(){ cout <<"Base2\n"; }
};

class Base3{
	public:
		Base3(){ cout <<"Base3\n"; }
};

class Base4{
	public:
		Base4(){ cout <<"Base4\n"; }
};

class Derived 
:public Base1, virtual public Base2,public Base3, virtual public Base4{//继承顺序
	public:
	Derived() :Base4(), Base3(), Base2(),Base1(), obj2(), obj1(){//初始化列表
		cout <<"Derived ok.\n";
	}
	protected: 
		OBJ1 obj1;//声明顺序
		OBJ2 obj2;
};

int main()
{
	Derived aa;//初始化
	cout <<"This is ok.\n";
	return 0;
}

运行结果:
Base2                     虚基类按照被继承顺序初始化
Base4                     虚基类按照被继承的顺序 
Base1                     非虚基类按照被继承的顺序初始化
Base3                     非虚基类按照被继承的顺序 

OBJ1                      成员对象按照声明的顺序初始化
OBJ2                      成员对象按照声明的顺序 
Derived ok. 
This is ok.

依然是:静态数据成员->父类构造函数->非静态数据成员->自己构造函数

先调用父类构造函数 输出前四行
再初始化自己的费静态数据成成员obj1,obj2
最后执行自己的构造函数输出Derived ok. 

同时需要注意protected才允许继承 在子类可直接访问obj1,obj2 私有private不可以

参考:
C++成员变量、构造函数的初始化顺序

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值