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();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM