C++繼承概念梳理

繼承(inheritance)機制是面向對象程序設計使代碼可以複用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能。


類的成員具有三種訪問限定符:public                protect(保護)          private    

(父)類和派生(子)類之間繼承關係也是三種:    public      protect       private

說明:   關鍵字protect修飾的成員爲保護成員,保護成員可以在本類的成員函數中訪問,也可以被本類的派生類的成員函數訪問,而類外的任何訪問都是非法的。即它是半隱藏的。(類的對象也是不能訪問)

聲明一個派生類的格式一般爲:

class 派生類名 : 繼承方式  基類名{

        派生類新增的數據成員和成員函數

};


說明:

1.基類private成員在派生類中是不能被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義爲protected。可以看出保護成員限定符是因繼承纔出現的。

2.public繼承是一個接口繼承,保持is-a原則,每個父類可用的成員對子類也可用,因爲每個子類對象也都是一個父類對象

3.protected/private繼承是一個實現繼承,基類的部分成員並非完全成爲子類接口的一部分,是 has-a 的關係原則,所以非特殊情況下不會使用這兩種繼承關係,在絕大多數的場景下使用的都是公有繼承。私有繼承意味着is-implemented-in-terms-of(是根據……實現的)。通常比組合(composition)更低級,但當一個派生類需要訪問基類保護成員或需要重定義基類的虛函數時它就是合理的。

4.不管是哪種繼承方式,在派生類內部都可以訪問基類的公有成員和保護成員,基類的私有成員存在但是在子類中不可見(不能訪問)

5.使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式

6.在實際運用中一般使用都是public繼承,極少場景下才會使用protetced/private繼承

三種繼承場景的測試時用例:

class Person
{
public:
	//Person(const string name)
	//	:_name(name)
	//{
	//}
	//Person(const string& name)
	//	:_name(name)
	//{}

	void Display(){
		cout << _name << endl;
	}

protected:
	string _name;
private:
	int _age;
};

//class Student : public Person
//class Student : protected Person
class Student : private Person
{
public :
	void Show()
	{
		Display();
		cout << _name << endl;
		cout << _num << endl;
	}
protected:
	int _num;
};

int main()
{
	Person p;
	Student s;
	s.Show();
	s.Display();
	system("pause");
	return 0;
}
在public繼承權限下,子類和派生類對象之間有:
1.子類對象可以賦值給父類對象(切割/切片)
2.父類對象不能賦值給子類對象
3.父類的指針/引用可以指向子類對象

4.子類的指針/引用不能指向父類對象(可以通過強制類型轉換完成)

父子類之間的切片行爲:


int main()                // ---父子類同上
{
	Person p;
	Student s;
	// 1.子類對象可以賦值給父類對象(切割/切片)
	p = s;
	// 2.父類對象不能賦值給子類對象
	//s = p;                //出錯
	// 3.父類的指針/引用可以指向子類對象
	Person* p1 = &s;
	Person&r1 = s;
	// 4.子類的指針/引用不能指向父類對象(可以通過強制類型轉換完成)
	Student* p2 = (Student*)&p;
	Student&r2 = (Student&)p;

	system("pause");
	return 0;
}

對切片/切割的說明:

在進行切片時,並沒有發生類型轉換。沒有產生臨時變量。


繼承體系中的作用域

1. 在繼承體系中基類和派生類都有獨立的作用域。

2. 子類和父類中有同名成員,子類成員將屏蔽父類對成員的直接訪問。(在子類成員函數中,可以使用基類::基類成員訪問)--隱藏--重定義

3. 注意在實際中在繼承體系裏面最好不要定義同名的成員

隱藏屬性的探究:

class Person
{
public:
	void f1()
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f()"<<endl;
	}

	cout<<"Student::f4()"<<endl;
	}

	int _stunum;
};

int main()
{
	Student s;
	cout<<sizeof(s)<<endl;
	// 隱藏 重定義
	s._stunum = 200;              //這裏的_stunum爲派生類的stunum,基類中的_stunum被隱藏
	//s.Person::_stunum = 20;     //這樣可以訪問基類中的_stunum

	s.Person::f1();
	s.f1();

	return 0;
}

在下面的代碼中,Person類中的函數 f1(in i) 和 Student類中的 f1()之間是什麼關係?   ------- 重載?  ------隱藏?

class Person
{
public:
	int f1(int i)
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f1()"<<endl;
	}

	int _stunum;
};
這兩個函數其實構成了隱藏!這裏要打到隱藏的條件只需要一條:只要函數名相同即可!

總結

不論對於變量還是函數來說,只要名稱相同,就能構成隱藏。

來看一個有意思的題:

class Person
{
public:
	void f()
	{
		cout<<"Person::f()"<<endl;
	}

	void f1()
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f()"<<endl;
	}

	void f2()
	{
		cout<<this->_stunum<<endl;
		cout<<"Student::f1()"<<endl;
	}

	void f3()
	{
		this->_stunum = 3;
		cout<<"Student::f1()"<<endl;
	}

	void f4()
	{
		this->f();
		cout<<"Student::f4()"<<endl;
	}

	int _stunum;
};

int main()
{


	Student* p = NULL;
	//p->f1();
	//p->f2();
	//p->f3();
	//p->f4();

	// A.代碼編不過
	// B.可以編譯通過,程序會崩潰
	// C.可以編譯通過,正常輸出Student::f()
	// D.以上選項都不對

	return 0;
}

答案:     C B B C

解析:(首先需要明白,類的成員變量存儲在類中,即和類是緊密關聯的,而類的成員函數是存放在公共代碼段中的)p雖然是指針但是它並沒有指向具體的對象,所以調用p並不會發生崩潰。當調用f2、f3時在函數中都對_stunum做了操作,而訪問_stunum都是通過this指針進行訪問的,而這個this指針所指向的位置是NULL,那麼對空地址訪問當然會出錯了。如果還是不明白的的話請看圖:


派生類的構造函數,拷貝構造函數,賦值運算符的重載探究:

繼承體系下,派生類中如果沒有顯示定義這六個成員函數,編譯器則會合成這六個默認的成員函數

派生類對象的構造與析構:

1、構造函數的調用按照先調用基類的構造函數,後調用派生類的構造函數的順序執行,析構函數的調用剛好相反

2、基類沒有缺省構造函數,派生類必須要在初始化列表中顯式給出基類名和參數列表

3、基類沒有定義構造函數,或者構造函數沒有參數,則派生類也可以不用定義,全部使用缺省構造函數

4、基類定義了帶有形參表構造函數,派生類就一定定義構造函數

5、如果派生類的基類也是一個派生類,每個派生類只需負責其直接基類的數據成員的初始化。

class Person
{
public :
	Person(const char* name = "xxx")
		: _name(name )
	{
		cout<<"Person()" <<endl;
	}

	Person(const Person& p)
		: _name(p ._name)
	{
		cout<<"Person(const Person& p)" <<endl;
	}

	Person& operator=(const Person& p )
	{
		cout<<"Person operator=(const Person& p)"<< endl;

		if (this != &p)
		{
			_name = p ._name;
		}

		return *this ;
	}

	~Person()
	{
		cout<<"~Person()" <<endl;
	}

protected:
	string _name ;      // 姓名
};

// 合成
class Student : public Person
{
public:
	Student()
		:Person("ssss")
		,_stunum(0)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		,_stunum(s._stunum)
	{}

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_stunum = s._stunum;
		}

		return *this;
	}

	~Student()
	{
		//Person::~Person();  //這裏會調用兩次基類的析構
	}

protected:
	int _stunum;
};


class BB : public AA
{

};

int main()
{
	Student s1;
	/*Student s2(s1);

	Student s3;
	s1 = s3;*/
	//Person p;

	//s1.~Student();
	//p.~Person();


	return 0;
}

說明:

1.子類的構造函數,拷貝構造函數,賦值運算符的重載,都是合成版本的,即子類先調父類的構造,拷貝構造,賦值等函數,對子類對象中父類的部分構造,在調子類的完成對子類的構造。

2.析構函數在子類中會被編譯器處理成和父類的析構同名。

3.析構函數先調用子類的,然後編譯器自動的調用父類的析構函數。(這樣做保證了子類先析構,符合棧的後進先出原則)

單繼承:   一個子類只有一個直接父類時稱這個繼承關係爲單繼承

多繼承: 一個子類有兩個或以上直接父類時稱這個繼承關係爲多繼承

菱形(鑽石)繼承:  有兩個子類共同繼承了一個父類,而又有一個孫子類繼承了這兩個子類。

缺點:1、數據冗餘           2、數據的二義性

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	cout<<sizeof(D)<<endl;
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}


解決菱形繼承的數據冗餘問題:    指定訪問的具體成員。

解決菱形繼承的數據冗餘和二義性問題: 虛繼承

虛繼承:C++使用虛擬繼承(Virtual Inheritance),解決從不同途徑繼承來的同名的數據成員在內存中有不同的拷貝造成數據不一致問題,將共同基類設置爲虛基類。這時從不同的路徑繼承過來的同名數據成員在內存中就只有一個拷貝,同一個函數名也只有一個映射。

1. 虛繼承解決了在菱形繼承體系裏面子類對象包含多份父類對象的數據冗餘&浪費空間的問題。
2. 虛繼承體系看起來好複雜,在實際應用我們通常不會定義如此複雜的繼承體系。一般不到萬不得已都不要定義菱形結構的虛繼承體系結構,因爲使用虛繼承解決數據冗餘問題也帶來了性能上的損耗。

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};


int main()
{
	cout<<sizeof(D)<<endl;
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}


爲什麼這裏的D的大小是24?    這裏添加了兩個指針分別保存偏移量的地址

繼承和友元:友元關係不能繼承,也就是說基類友元不能訪問子類私有和保護成員

class Person
{
     friend void Display( Person &p , Student&s);
protected :
     string _name ; // 姓名
};
class Student : public Person
{
protected :
     int _stuNum ; // 學號
};
void Display( Person&p , Student &s)
{
     cout <<p . _name<< endl ;
     cout <<s. _name<< endl ;
     cout <<s. _stuNum<< endl ;
}
void TestPerson1 ()
{
     Person p;
     Students;
     Display(p,s);
}
繼承與靜態成員:基類定義了static成員,則整個繼承體系裏面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例。
class Person
{
public:
     Person ()
     {
            ++ _count ;
     }
protected :
     string _name ; // 姓名
public:
     staticint _count; // 統計人的個數。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
      int _stuNum ; // 學號
};
class Graduate : public Student
{
protected :
     string _seminarCourse ; // 研究科目
};
void TestPerson1 ()
{
     Students1 ;
     Students2 ;
     Students3 ;
     Graduate s4 ;
     cout <<" 人數:"<< Person ::_count << endl;
     Student ::_count = 0;
     cout <<" 人數:"<< Person ::_count << endl;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章