C++——类继承以及类初始化顺序

 

对于类以及类继承, 几个主要的问题:
1) 继承方式: public/protected/private继承.
这是c++搞的, 实际上继承方式是一种允许子类控制的思想. 子类通过public继承, 可以把基类真实还原, 而private继承则完全把基类屏蔽掉.
这种屏蔽是相对于对象层而言的, 就是说子类的对象完全看不到基类的方法, 如果继承方式是private的话, 即使方法在基类中为public的方法.
但继承方式并不影响垂直方向的访问特性, 那就是子类的函数对基类的成员访问是不受继承方式的影响的.

比较(java): java是简化的, 其实可认为是c++中的public继承. 实在没必要搞private/protected继承, 因为如果想控制,就直接在基类控制就好了.


2) 对象初始化顺序: c++搞了个成员初始化列表, 并确明确区分初时化跟赋值的区别. c++对象的初始化顺序是:
(a) 基类初始化
(b) 对象成员初时化
(c) 构造函数的赋值语句

举例:
假设 class C : public A, public B {
D d;//
}
则初始化的顺序是A, B, D, C的构造函数.

这里基类的初始化顺序是按照声明的顺序, 成员对象也是按照声明的顺序. 因此 c(int i, int j) : B(i), A(j) {} //这里成员初始化列表的顺序是不起作用的;
析构函数的顺序则刚好是调过来, 构造/析构顺序可看作是一种栈的顺序;

比较(java): java中初始化赋值是一回事. 而且对基类的构造函数调用必须显示声明, 按照你自己写的顺序.
对成员对象, 也叫由你初始化.没有什么系统安排的顺序问题, 让你感觉很舒服;


3) 多继承问题: c++支持多继承, 会导致"根"不唯一. 而java则没有该问题;
此外c++没有统一的root object, java所有对象都存在Object类使得很多东西很方便. 比如公共的seriall, persistent等等.


4) 继承中的重载: c++中, 派生类会继承所有基类的成员函数, 但构造函数, 析构函数除外.
这意味着如果B 继承A, A(int i)是基类构造函数, 则无法B b(i)定义对象. 除非B也定义同样的构造函数.
c++的理由是, 假如派生类定义了新成员, 则基类初始化函数无法初始化派生类的所有新增成员.

比较(java): java中则不管, 就算有新增对象基类函数没有考虑到, 大不了就是null, 或者你自己有缺省值. 也是合理的.


5) 继承中的同名覆盖和二义性: 同名覆盖的意思是说, 当派生类跟基类有完全一样的成员变量或者函数的时候, 派生类的会覆盖基类的.
类似于同名的局部变量覆盖全局变量一样. 但被覆盖的基类成员还是可以访问的.如B继承A, A, B都有成员变量a,则B b, b.a为访问B的a, b.A::a则为访问基类中的a. 这对于成员函数也成立.
但需要注意的是, 同名函数必须要完全一样才能覆盖. int func(int j)跟int func(long j)其实是不一样的. 如果基类,派生类有这两个函数, 则不会同名覆盖.
最重要的是, 两者也不构成重载函数. 因此假如A有函数int func(int j), B有函数int func(long j). 则B的对象b.func(int)调用为错误的. 因为B中的func跟它根本就不构成重载.

同名覆盖导致的问题是二义性. 假如C->B=>A, 这里c继承B, B继承A. 假如A, B都有同样的成员fun, 则C的对象c.fun存在二义性. 它到底是指A的还是B的fun呢?
解决办法是用域限定符号c.A::fun来引用A的fun.

另外一个导致二义性的是多重继承. 假设B1, B2都继承自B, D则继承B1, B2. 那么D有两个B而产生二义性.
这种情况的解决办法是用虚基类. class B1 : virtual public B, class B2:virtual public B, D则为class D : public B1, public B2. 这样D中的成员只包含一份B的成员使得不会产生二义性.

比较(java). java中是直接覆盖. 不给机会这么复杂, 还要保存基类同名的东西. 同名的就直接覆盖, 没有同名的就直接继承.

虚基类的加入, 也影响到类的初始化顺序. 原则是每个派生类的成员化初始化列表都必须包含对虚基类的初始化.
最终初始化的时候, 只有真正实例化对象的类的调用会起作用. 其它类的对虚基类的调用都是被忽略的. 这可以保证虚基类只会被初始化一次.

 










c++没有显式接口的概念, 我觉得是c++语言的败点. 这也是导致c++要支持组件级的重用非常麻烦. 虽然没有显式的接口, 但c++中的纯虚函数以及抽象类的支持, 事实上是等同于接口设施的. 当一个类中, 所有成员函数都是纯虚函数, 则该类其实就是接口.
java c++
接口 类(所有成员函数都是纯虚函数)
抽象类 类(部分函数是虚函数)
对象类 对象类











C++构造函数调用顺序
1. 如果类里面有成员类,成员类的构造函数优先被调用;
2. 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
3. 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
5. 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

举例:
#include<iostream>
#include<string>
class A {
public:A{…}
~A{…}
};
class B {
public:B{…}
~B{…}
};
class D {
public:D{…}
~D{…}
};
class E {
public:E{…}
~E{…}
};
class C :public A,public B {
public:C{…}
private:
D objD_;
E objE_;
~C{…}
}

int main(void)
{
C test;
return 0;
}
运行结果是:
A{…}//派生表中的顺序
B{…}
D{…}//成员类的构造函数优先被调用
E{…}
C{…}
~C{…}
~E{…}
~D{…}
~B{…}
~A{…}






从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段:

初始化阶段:
所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中;

计算阶段:
一般用于执行构造函数体内的赋值操作。
下面的代码定义两个结构体,其中Test1有构造函数,拷贝构造函数及赋值运算符,为的是方便查看结果,Test2是个测试类,它以Test1的对象为成员,我们看一下Test2的构造函数是怎么样执行的。

class Test1
{
Test1() //无参构造函数
{
cout << "Construct Test1" << endl ;
}

Test1(const Test1& t1) //拷贝构造函数
{
cout << "Copy constructor for Test1" << endl ;
this->a = t1.a ;
}

Test1& operator = (const Test1& t1) //赋值运算符
{
cout << "assignment for Test1" << endl ;
this->a = t1.a ;
return *this;
}

int a ;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1)
{
test1 = t1 ;
}
};
调用代码:
Test1 t1 ;
Test2 t2(t1) ;
输出:
Construct Test1
Construct Test1
assignment for Test1
解释一下:
第一行输出对应调用代码中第一行,构造一个Test1对象;
第二行输出对应Test2构造函数中的代码,用默认的构造函数初始化对象test1 // 这就是所谓的初始化阶段;
第三行输出对应Test2的赋值运算符,对test1执行赋值操作 // 这就是所谓的计算阶段;


为什么使用初始化列表?
初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?
由下面的测试可知,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。同样看上面的例子,我们使用初始化列表来实现Test2的构造函数。
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}
使用同样的调用代码,输出结果如下:
Construct Test1
Copy constructor for Test1
第一行输出对应 调用代码的第一行
第二行输出对应Test2的初始化列表,直接调用拷贝构造函数初始化test1,省去了调用默认构造函数的过程。
所以一个好的原则是,能使用初始化列表的时候尽量使用初始化列表;


除了性能问题之外,有些时场合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表:
1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面;
2.引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面;
3.没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化;


struct Test1 {
Test1(int a):i(a){}
int i;
};
struct Test2 {
Test1 test1 ;
};
以上代码无法通过编译,因为Test2的构造函数中 test1 = t1 这一行实际上分成两步执行:
1. 调用Test1的默认构造函数来初始化test1;
由于Test1没有默认的构造函数,所以1 无法执行,故而编译错误。正确的代码如下,使用初始化列表代替赋值操作,
struct Test2 {
Test1 test1 ;
Test2(int x):test1(x){}
}


成员变量的初始化顺序: 先定义的成员变量先初始化
成员是按照他们在类中出现的顺序进行初始化的,而不是按照他们在初始化列表出现的顺序初始化的,看代码:
struct foo {
int i ;int j ;
foo(int x):i(x), j(i){}; // ok, 先初始化i,后初始化j
};
再看下面的代码:
struct foo {
int i ;int j ;
foo(int x):j(x), i(j){} // i值未定义
};
这里i的值是未定义的因为虽然j在初始化列表里面出现在i前面,但是i先于j定义,所以先初始化i,而i由j初始化,此时j尚未初始化,所以导致i的值未定义。
一个好的习惯是,按照成员定义的顺序进行初始化。

 

 

 









对于全局对象(global object),VC下是先定义先初始化,但C++标准没做规定。
全局对象默认是静态的,全局静态(static)对象必须在main()函数前已经被构造,告知编译器将变量存储在程序的静态存储区,由C++ 编译器startup代码实现。
startup代码是更早于程序进入点(main 或WinMain)执行起来的代码,它能做些像函数库初始化、进程信息设立、I/O stream产生等等动作,以及对static对象的初始化动作(也就是调用其构造函数);
在main()函数结束后调用它的析构函数。

----------------派生类对象的初始化构造
#include <iostream>
using namespace std;

class A {
private:
int a;
public:
A(int x):a(x) { cout <<a <<" "; }
};
class B: A {
private:
int b, c;
const int d;
A x, y;
public:
B(int v): b(v),y(b+2),x(b+1),d(b),A(v) {
c=v;
cout <<b <<" " <<c <<" " <<d;
}
};
int main(void)
{
B z(1);
return 0;
}
/*
1.定义一个派生类对象,首先初始化它的基类成员(基类部分),即调用基类的构造函数(如果是多继承,则按继承的先后顺序调用基类的构造函数)

2.基类部分初始化完之后,初始化派生类部分,派生类的成员初始化依赖它的声明顺序,并不依赖它的初始化列表的顺序初始化派生类成员,总结来说:就是派生类成员的初始化,依赖它的声明顺序而不是依赖初始化列表的顺序。

3.调用派生类的构造函数,可以理解为就是执行派生类构造函数的函数体而已

4.特别注意:但是,请注意:上面两点调用构造函数或者其他的参数传递是参考初始化列表给出的参数的


详细解释:
首先:B z(1);则依据1,调用基类的构造函数,但是这里不知道该调用基类的哪个构造函数,因为基类有默认的构造函数(即没有参数)和你定义的A(int x)这个构造函数,所以,编译器要进行选择。
依据4,参考到初始化列表b(v),y(b+2),x(b+1),d(b),A(v)中有A(v),所以编译器选择调用你定义的构造函数A(int x),所以打印输出a的值,输出 1,然后,依据2,派生类自身定义的部分是按它的定义顺序初始化的,
即按下面这个顺序,b,c,d,x,y.
int b, c;
const int d;
A x, y;
所以,依据4,分别参考初始化列表b(v),y(b+2),x(b+1),d(b),A(v) 给出的参数信息,可知道初始化b,使用b(v),b被初始化为1。然后,初始化c,由于初始化列表中没有指定c的初始化,所以暂时c不被初始化,然后初始化d,根据初始化列表中的d(b),d被初始化为b的值,即为1。
然后初始化A类对象x和y,依据初始化列表中的x(b+1)初始化x,由于b的值为1,所以即相当于x(2),给除了一个参数2,则调用你定义的构造函数A(int x),打印输出类A的x对象中的a的值,即输出2,同理,依据y(b+2)初始化y,打印输出3。
最后,依据3,调用派生类构造函数,即
B(int v)
{
c=v;
cout <<b <<" " <<c <<" " <<d;
}
这时,直接忽略初始化列表了,执行这个派生类的构造函数,那么执行函数体c=v;则把那个没初始化的c被赋值为v的值,即c的值为1。最后打印输出b和c的值所以再输出两个1。

综上所述:输出1 2 3 1 1 1

 

 

 

 

 

 

 

一、C++成员变量初始化

1、普通的变量:一般不考虑啥效率的情况下 可以在构造函数中进行赋值。考虑一下效率的可以再构造函数的初始化列表中进行

2、static 静态变量(本地化数据和代码范围):
static变量属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。在这种性质上理解,有点类似于全局变量的唯一性。
函数体内static变量的作用范围时该函数体,不同于auto变量,该变量内存只被分配一次,因此其值在下次调用时维持上次的值。
在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外的其它函数访问。
在模块内的static函数只可被这一模块内的其他函数调用,这个函数的适用范围被限制在声明它的模块内。
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因而只能访问类的static成员变量。

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

4、Reference 引用型变量:
引用型变量和const变量类似。需要在创建的时候即进行初始化。也是在初始化列表中进行。但需要注意用Reference类型。

5、字符串初始化
char str[10] = "HELLO";
结尾会被编译器自动加上结尾符'/0',编译的时候可以看到它最后是'',ASC码值是0;
"HELLO"只有5个字符,加上编译器自动添加的'/0',也就是会初始化数组的前6个元素,剩下有元素会被全部初始化为'/0',这个要注意哦;

char str[] = "HELLO";
编译器自动为后面的字符串分配大小并加'/0';

char str[] = {'H','E','L','L','O','/0'};
编译器会根据字符串大小分配空间,可是不会自动分配'/0',所以结尾的时候要自己加上'/0';

char *str = "HELLO";
把指向字符串的指针给定义好的字符指针;

1)用构造函数确保初始化
对于一个空类,编译器会自动声明4个默认函数:构造函数、拷贝构造函数、赋值函数、析构函数(如果不想使用自动生成,就应该明确拒绝),这些生成的函数都是public且inline的。

2)为什么构造函数不能有返回值
(1)假设有一个类C,有如下定义:
构造函数的调用之所以不设返回值,是因为构造函数的特殊性决定的。从基本语义角度来讲,构造函数返回的应当是所构造的对象。否则,我们将无法使用临时对象:
void f(int a) {...} //(1)
void f(const C& a) {...} //(2)
f(C()); //(3),究竟调用谁?
对于(3),我们希望调用的是(2),但如果C::C()有int类型的返回值,那么究竟是调(1)好呢,还是调用(2)好呢。于是,我们的重载体系,乃至整个的语法体系都会崩溃。
这里的核心是表达式的类型。目前,表达式C()的类型是类C。但如果C::C()有返回类型R,那么表达式C()的类型应当是R,而不是C,于是便会引发上述的类型问题。
(2)只是C++标准规定了构造/析构/自定义类型转换符不可以指定返回类型。 但你不能据此就说它们没有返回类型。
(3)本人的意见是构造函数是有返回值的,返回的就是新构造的对象本身,但是不能指定返回类型,因为你用这个类的构造函数表明就是返回这个类的一个对象,没有必要指定返回类型,即使是指定也必须是指定类本身的返回类型,这就多次一举了吧。

3)为什么构造函数不能为虚函数
虚函数调用的机制,是知道接口而不知道其准确对象类型的函数,但是创建一个对象,必须知道对象的准确类型;当一个构造函数被调用时,它做的首要事情之一就是初始化它的VPTR来指向VTABLE。

#include <iostream>
using namespace std;

class Base {
private:
int i;
public:
Base(int x) {
i = x;
}
};

class Derived : public Base {
private:
int i;
public:
Derived(int x, int y) {
i = x;
}
void print() {
cout << i + Base::i << endl;
}
};

int main()
{
Derived A(2,3);
A.print();
return 0;
}

首先,是访问权限问题,子类中直接访问Base::i是不允许的,应该将父类的改为protected或者public(最好用protected)
其次,统计父类和子类i的和,但是通过子类构造函数没有对父类变量进行初始化;此处编译会找不到构造函数,因为子类调用构造函数会先找父类构造函数,但是没有2个参数的,所以可以在初始化列表中调用父类构造函数
最后个问题,是单参数的构造函数,可能存在隐式转换的问题,因为单参数构造函数,和拷贝构造函数形式类似,调用时很可能会发生隐式转换,应加上explicit关键字
#include <iostream>
using namespace std;

class Base {
protected:
int i;
public:
explicit Base(int x) {
i = x;
}
};

class Derived : public Base {
private:
int i;
public:
Derived(int x, int y):Base(x) {
i = y;
}
void print() {
cout << i + Base::i << endl;
}
};

int main()
{
Derived A(2,3);
A.print();
return 0;
}


初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?
由下面的测试可知,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。


初始化列表
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类)的赋值构造函数;
class Student {
public:
Student(string in_name, int in_age):name(in_name),age(in_age) {}
private :
string name;
int age;
};
在初始化的时候调用的是string的拷贝构造函数,而上例会调用两次构造函数,从性能上会有不小提升;


有的情况下,是必须使用初始化列表进行初始化的:const对象、引用对象
初始化列表初始顺序
#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,为什么呢?
因为对于初始化列表而言,对成员变量的初始化,是严格按照声明次序,而不是在初始化列表中的顺序进行初始化,如果改为赋值初始化则不会出现这个问题,
当然,为了使用初始化列表,还是严格注意声明顺序吧,比如先声明数组大小,再声明数组这样。

 


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.

 

 

 

 

 

重复继承(repeated inheritance):一个派生类多次继承同一个基类.
但C++并不允许一个派生类直接继承同一个基类两次或以上.

重复继承的两个种类:复制继承和共享继承

重复继承中的共享继承:通过使用虚基类,使重复基类在派生对象实例中只存储一个副本.

涉及到共享继承的派生类对象的初始化次序规则
① 最先调用虚基类的构造函数.
② 其次调用普通基类的构造函数,多个基类则按派生类声明时列出的次序从左到右.
③ 再次调用对象成员的构造函数,按类声明中对象成员出现的次序调用.
④ 最后执行派生类的构造函数.

析构函数执行次序与其初始化顺序相反.

例:
/*
//Program: repeated inheritance, virtual base class test
//Author: Ideal
//Date: 2006/3/28
*/

#include <iostream.h>

class baseA
{
public:
baseA()
{
cout << "BaseA class. " << endl;
}
};

class baseB
{
public:
baseB()
{
cout << "BaseB class. " << endl;
}
};

class derivedA:public baseB, virtual public baseA
{
public:
derivedA()
{
cout << "DerivedA class. " << endl;
}
};

class derivedB:public baseB, virtual public baseA
{
public:
derivedB()
{
cout << "DerivedB class. " << endl;
}
};

class Derived:public derivedA, virtual public derivedB
{
public:
Derived()
{
cout << "Derived class. " << endl;
}
};

void main()
{
Derived obj;
cout << endl;
}

result:
=========
BaseA class.
BaseB class.
DerivedB class.
BaseB class.
DerivedA class.
Derived class.

————————————————————————————————————————
分析:各类的类层次结构关系为
①Derived从derivedA和虚基类derivedB共同派生而来
②derivedA从baseB和虚基类baseA派生而来, derivedB从baseB和虚基类baseA派生而来

执行顺序(构造函数)
由第①层关系,根据规则可得顺序为derivedB,derivedA,Derived.

然后,对于derivedB,同样根据规则更深入分析得到的顺序是baseA,baseB,derivedB.

对于derivedA,值得注意的是derivedA和derivedB都经过虚基类baseA的派生,所以根据只存储一个副本的处理方法,

由于baseA在derivedB中已经被初始化过,derivedA中将不必再进行初始化,所以执行的将是baseB, derivedA.

最后就是Derived了.

综合可得对应构造函数顺序: baseA(), baseB(), derivedB(); baseB(), derivedA(); Derived();

 

posted @ 2015-08-31 10:52  eric0803  阅读(8849)  评论(1编辑  收藏  举报