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中初始化賦值是一回事. 並且對基類的構造函數調用必須顯示聲明, 按照你本身寫的順序.
對成員對象, 也叫由你初始化.沒有什麼系統安排的順序問題, 讓你感受很舒服;java


3) 多繼承問題: c++支持多繼承, 會致使"根"不惟一. 而java則沒有該問題;
此外c++沒有統一的root object, java全部對象都存在Object類使得不少東西很方便. 好比公共的seriall, persistent等等.ios


4) 繼承中的重載: c++中, 派生類會繼承全部基類的成員函數, 但構造函數, 析構函數除外.
這意味着若是B 繼承A, A(int i)是基類構造函數, 則沒法B b(i)定義對象. 除非B也定義一樣的構造函數.
c++的理由是, 假如派生類定義了新成員, 則基類初始化函數沒法初始化派生類的全部新增成員.

比較(java): java中則無論, 就算有新增對象基類函數沒有考慮到, 大不了就是null, 或者你本身有缺省值. 也是合理的.c++


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)將更加難於正確地修改或擴展基類的實現。(基類設計者的責任是提供一組適當的基類構造函數)this

舉例:
#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{…}spa






從概念上來說,構造函數的執行能夠分紅兩個階段,初始化階段和計算階段,初始化階段先於計算階段:

初始化階段:
全部類類型(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

 

 

 

 

 

 

 

1、C++成員變量初始化

一、普通的變量:通常不考慮啥效率的狀況下 能夠在構造函數中進行賦值。考慮一下效率的能夠再構造函數的初始化列表中進行

二、static 靜態變量(本地化數據和代碼範圍):
static變量屬於類全部,而不屬於類的對象,所以無論類被實例化了多少個對象,該變量都只有一個。在這種性質上理解,有點相似於全局變量的惟一性。
函數體內static變量的做用範圍時該函數體,不一樣於auto變量,該變量內存只被分配一次,所以其值在下次調用時維持上次的值。
在模塊內的static全局變量能夠被模塊內全部函數訪問,但不能被模塊外的其它函數訪問。
在模塊內的static函數只可被這一模塊內的其餘函數調用,這個函數的適用範圍被限制在聲明它的模塊內。
在類中的static成員變量屬於整個類所擁有,對類的全部對象只有一份拷貝。
在類中的static成員函數屬於整個類所擁有,這個函數不接受this指針,於是只能訪問類的static成員變量。

三、const 常量變量:
const常量須要在聲明的時候即初始化。所以須要在變量建立的時候進行初始化。通常採用在構造函數的初始化列表中進行。

四、Reference 引用型變量:
引用型變量和const變量相似。須要在建立的時候即進行初始化。也是在初始化列表中進行。但須要注意用Reference類型。

五、字符串初始化
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();

相關文章
相關標籤/搜索