1. 程式人生 > >面向物件三大特性

面向物件三大特性

一,封裝

面向物件的精髓在於封裝,面向物件要求資料應該儘可能被封裝,越多的東西被封裝,就越少的人可以看到他,而越少的人可以看到他,就有越大的彈性去改變他。因此,越多的東西被封裝,改變那些東西的能力就越大。這就是推崇封裝的原因,改變事物而隻影響有限客戶。通過C++提供的訪問控制符來控制成員的訪問許可權:

  • private:只能由該類中的函式或其友元函式訪問。在類外不能訪問,該類的物件也不能訪問。
  • protected:可以被該類中的函式、子類的函式或其友元函式訪問。在類外不能訪問,該類的物件也不能訪問。
  • public:可以被該類中的函式、子類的函式或其友元函式訪問,也可以由該類的物件訪問。

二,繼承

2.1,C函式庫的侷限性

除非廠商提供了庫函式的原始碼,否則無法根據自己的需求對庫函式進行修改。

使用繼承帶來的優點:

  • 面向物件程式設計的主要目的之一是提供可重用的程式碼,類繼承提供了比修改原始碼更好的方法,不需要訪問原始碼就可以派生出類。尤其是當專案比較龐大時,重用經過測試的程式碼比重新編寫程式碼要好的多。
  • 使用繼承與多型機制,可以很方便的對系統的功能進行擴充套件。

2.2,如何實現繼承?

自定義一個基類Person,定義一個派生類Student

class Person{
private:
    string name;
public
: Person():name("default"){ cout<<"Person default constructor."<<endl; } Person(const string &name):name(name){ cout<<"Person constructor."<<endl; } void show(){ cout<<"name: "<<name<<endl; } }; class Student : public
Person{ private: string number; public: Student():number("000000"){ cout<<"Student default constructor."<<endl; } Student(const string &name, const string &number):Person(name), number(number){ cout<<"Student constructor."<<endl; } void show(){ Person::show(); cout<<"number: "<<number<<endl; } };

C++提供了三種類型的繼承,分別是public、private、protected:

  • public:基類的public與protected屬性在派生類中不變。
  • private:基類的public與protected屬性在派生類中變成private。
  • protected:基類的public與protected屬性在派生類中變成protected。
  • private與protected之間的區別只在基類派生的類中才會體現出來。派生類的成員可以直接訪問基類的保護成員,但不能訪問基類的私有成員。

2.3,派生類建構函式呼叫順序

在派生類建構函式中沒有顯示呼叫基類的建構函式,建立派生類物件時,將使用基類的預設建構函式。下面在Student派生類建構函式中顯示呼叫基類的建構函式。

Student(const string &name, const string &number):Person(name), number(number){
        cout<<"Student constructor."<<endl;
    }

建立派生類物件時,首先建立基類物件。派生類建構函式通過成員初始化列表呼叫基類的建構函式,然後再初始化派生類新增的資料成員。

2.4,派生類解構函式呼叫順序

派生類的解構函式被執行時, 執行完派生類的解構函式後, 自動呼叫基類的解構函式。

#include <iostream>
using namespace std;

class Base
{
public:
    ~Base()
    {
        cout<<"Base Destroy."<<endl;
    }
};

class Derived : public Base
{
public:
    ~Derived()
    {
        cout<<"Derived Destroy."<<endl;
    }
};

void main()
{
    Derived obj;
}

輸出結果:

Derived Destroy.
Base Destroy.

2.5,派生類與基類之間的特殊關係

  • 派生類物件可以使用基類的非私有方法。
  • 基類指標或引用在不進行顯示轉換的情況下指向派生類物件,但基類指標或引用只能呼叫基類方法。
  • 派生類物件是一個特殊的基類物件,任何使用基類物件的地方都可使用派生類物件替換。

三,多型

3.1,什麼是多型?

多型(polymorphism)的字面意思是多種表現形式,多型性可以簡單地概括為”一個介面,多種方法”,程式在執行時才決定呼叫的函式,換句話說,方法的行為應取決於呼叫方法的物件,它是面向物件程式設計領域的核心概念。多型的目的是為了實現介面重用,也就是說,不論傳遞過來的究竟是那個類的物件,函式都能夠通過同一個介面呼叫到對應於各自物件的實現方法。

3.2,使用多型解決分不清物件型別的問題

class Person{
protected:
    string name;
public:
    Person():name("default"){
    }
    Person(const string &name):name(name){
    }
    void show(){
        cout<<"name: "<<name<<endl;
    }
};
class Student : public Person{
protected:
    string number;
public:
    Student():number("000000"){
    }
    Student(const string &name, const string &number):Person(name), number(number){
    }
    void show(){
        cout<<"name: "<<name<<endl;
        cout<<"number: "<<number<<endl;
    }
};
void test(Person &p){
    p.show();
}
int main(){
    Person p("person");
    Student s("student", "11041722");
    test(p);
    test(s);
    return 0;
}

輸出結果:

name: person
name: student

程式分析:

物件p與s分別是基類和派生類的物件,而函式test的形參是Person類的引用。按照類繼承的特點,編譯器把Student類物件看做是一個Person類物件。我們想利用test函式達到的目的是,傳遞不同類物件的引用,分別呼叫不同類的過載了的show成員函式,但是程式的執行結果卻出乎人們的意料,編譯器分不清傳進來的是基類還是派生類物件,無論是基類物件還是派生類物件呼叫的都是基類的show成員函式。

使用多型解決上面的問題

為了要解決上述不能正確分辨物件型別的問題,c++提供了一種叫做多型性(polymorphism)的技術來解決問題。對於上面的程式,這種能夠在編譯時就能夠確定哪個過載的成員函式被呼叫的情況被稱做靜態聯編,而系統在執行時,能夠根據其型別確定呼叫哪個過載的成員函式的能力,稱為多型性或叫動態聯編。下面使用多型技術解決上面的問題,動態聯編正是解決多型問題的方法。 把基類中的show成員函式宣告為虛擬函式:

class Person{
protected:
    string name;
public:
    Person():name("default"){
    }
    Person(const string &name):name(name){
    }
    virtual void show(){
        cout<<"name: "<<name<<endl;
    }
};
class Student : public Person{
protected:
    string number;
public:
    Student():number("000000"){
    }
    Student(const string &name, const string &number):Person(name), number(number){
    }
    virtual void show(){
        cout<<"name: "<<name<<endl;
        cout<<"number: "<<number<<endl;
    }
};
void test(Person &p){
    p.show();
}
int main(){
    Person p("person");
    Student s("student", "11041722");
    test(p);
    test(s);
    return 0;
}

輸出結果:

name: person
name: student
number: 11041722

3.3,虛擬函式

虛擬函式是實現多型的重要機制,下面宣告與定義虛擬函式:

class Person{
protected:
    string name;
public:
    Person():name("default"){}
    Person(const string &name):name(name){}
    virtual void show(){
        cout<<"name: "<<name<<endl<<endl;
    }
};

class Student : public Person{
protected:
    string number;
public:
    Student():number("000000"){}
    Student(const string &name, const string &number):Person(name), number(number){}
    virtual void show(){
        cout<<"name: "<<name<<endl;
        cout<<"number: "<<number<<endl<<endl;
    }
};

int main(){
    Person p("person");
    Student s("student", "11041722");
    p.show();
    s.show();
    return 0;
}

虛擬函式的特點:

  • 如果方法是通過引用或指標而不是物件呼叫的,它將確定使用哪一種方法。如果沒有使用關鍵字virtual,程式將根據引用型別或指標型別選擇方法。如果使用了關鍵字virtual,程式將根據指標或引用實際指向的物件的型別來選擇方法。
  • 在基類的方法宣告中使用關鍵字virtual可使該方法在基類以及所有的派生類中是虛的。

注意:

關鍵字virtual只用於類宣告的方法原型中,而沒有用於方法的定義中。

3.4,虛擬函式的工作原理

通常,編譯器處理虛擬函式的方法是:給每個物件新增一個隱藏成員。隱藏成員中儲存了一個指向函式地址陣列的指標。這種陣列稱為虛擬函式表(virtual function table)。虛擬函式表中儲存了為類物件進行宣告的虛擬函式的地址。例如,基類物件包含一個指標,該指標指向基類中所有虛擬函式的地址表。派生類物件將包含一個指向獨立地址表的指標。如果派生類提供了虛擬函式的新定義,該虛擬函式表將儲存新函式的地址。如果派生類沒有重新定義虛擬函式,該虛擬函式表將儲存基類中同名虛擬函式的地址。如果派生類定義了新的虛擬函式,則該函式的地址也將被新增到虛擬函式表中。呼叫虛擬函式時,程式將通過物件中的vptr指標,找到虛擬函式表,然後在虛擬函式表中查詢要呼叫的函式的地址。

3.5,虛擬函式帶來的額外開銷

使用虛擬函式時,在記憶體與執行速度方面都有一定的開銷。雖然非虛擬函式的效率比虛擬函式高,但是不具有動態編聯功能。

  • 每個物件都將增大,增大量為儲存隱藏成員(是一個指標)的空間。
  • 對於每一個類,編譯器都將建立一個虛擬函式表。
  • 對於每個函式呼叫,都需要執行一項額外的操作,即到表中查詢地址。

3.6,使用虛擬函式應注意的問題

  • 建構函式不能是虛擬函式。建立派生類物件時,將呼叫派生類的建構函式,派生類的建構函式將呼叫基類的建構函式,這種順序不同於繼承機制。派生類不繼承基類的建構函式,所以將類的建構函式宣告為虛的沒什麼意義。
  • 解構函式應當是虛擬函式,除非類不用作基類。
  • 友元不能是虛擬函式,友元不是類成員,而只有成員函式才能是虛擬函式。
  • 重新定義將隱藏方法。重新定義繼承的方法並不是過載。如果重新定義派生類中的函式,無論引數列表是否相同,該操作將隱藏所有的同名基類方法。