【C++】C++中類的基本使用

1.類、成員的聲明,定義,初始化的基本規則

C++中類的基本模板以下:ios

namespace 空間命名{//能夠定義namespace,也能夠不定義
    class/struct 類名稱{
     public/private: 基本成員; 構造函數():成員初始化列表{ 構造函數主體; }
     返回類型 函數名稱(參數列表) 修飾符{
      函數主體;
     } }; }

例如:c++

//Person.h
#pragma once
#include <string>
using namespace std;
class Person{
    private://定義私有成員
        string name;
        int age;
    public://定義公共成員
        Person() = default;//c++11標準中使用=default,定義構造函數的默認行爲
        Person(string nm,int ag) : name(nm),age(ag){}//帶兩個參數的構造函數
        Person(string nm) : name(nm){}//帶一個參數的構造函數
        Person(int ag) : age(ag){}
        /*
 *          或者寫成:
 *                  Person(string nm) : Person(nm,0){}//調用帶兩個參數的構造函數,初始化age爲0
 *                          Person(int ag) : Person("",ag){}//調用有兩個參數的構造函數,初始化name爲""
 *                                  */
        
        void setName(string name){
            this->name = name;
        }
        string getName(){
            return this->name;
        }

        void setAge(int age){
            this->age = age;
        }
        int getAge(){
            return this->age;
        }
};
//PersonTest.cpp
#include "Person.h"
#include <iostream>
using namespace std;

int main(int argc, char * argv[]){
    Person person("jamy",20), *p = &person;
  //也能夠寫成:Person person = Person("jamy",20);
    cout << "name:" << p->getName() << "\n"
        << "age:" << p->getAge() << endl;
    
return 0;
}

 

在定義類的時候,可使用class關鍵字或struct關鍵字。這種變化僅僅是形式上有所不一樣,實際上咱們可使用這兩個關鍵字中的任何一個定義類。惟一的區別是struct和class的默認訪問權限不太同樣。若是咱們使用struct關鍵字,則定義在第一個訪問說明符以前的成員是public的;相反,若是咱們使用class關鍵字,則這些成員是private的。
出於統一編程的考慮,當咱們但願定義的類的全部成員是public的時,使用struct;反之,若是但願成員是private的,使用class。

構造函數的名字和類名相同。和其餘函數不同的是,構造函數沒有返回類型;構造函數也有一個(可能爲空的)參數列表和一個(可能爲空的)函數體。構造函數的初始值是成員名字的一個列表,每一個名字後面緊跟括起來的成員初始值,不一樣成員的初始值經過逗號分隔開來。例如上面的:編程

        Person(string nm,int ag) : name(nm),age(ag){}

其中name(nm),age(ag){},表示初始name成員爲nm值,初始age成員爲ag值,函數體爲空。在函數體中,也能夠改變參數的值:ide

        Person(string nm,int ag){
            this->name = nm;//賦值,並不是初始化
            this->age = ag;//賦值,並不是初始化
        }

但上面這段代碼並無初始化name和age值,他們只是從新修改name和age的值。而且有些特殊成員(例如引用和constexpr)是不運行這種方式的,因此建議使用初始化的方式。函數

 

2.this使用的注意點

須要注意在C++中,this不是表明本類對象,而是指向本類對象的指針。在使用的須要注意,this是一個指針。this

成員函數經過一個名爲this的額外隱式參數來訪問調用它的那個對象,當咱們調用一個成員函數時,用請求該函數的對象地址初始化this。
例如:spa

person.getName();

則編譯器負責把person的地址傳遞給person的隱式形參this,能夠隱式的認爲編譯器將該調用重寫成了以下的形式:設計

Person::getName(&person);

其中調用Person的getName成員傳入了person對象的地址。

默認狀況下,this的類型是指向類類型很是量版本的的常量指針。在Person的類型中,this的默認類型是Person *const。

this的默認類型是Person *const,那麼有沒有其它的類型呢?答案是確定的,當咱們在定義函數的時候指定const關鍵字,那麼this就是指向類類型常量版本的常量指針,在Person類中也就是 const Person * const類型。指針

例如:rest

void getName() const{//只能取值,不能修改調用對象的屬性值
    return this->name;//this的類型是const Person * const
}

 

3.靜態成員

和其餘成員函數同樣,咱們既能夠在類的內部也能夠在類的外部定義靜態成員函數。當在類的外部定義靜態成員函數時,不能重複static關鍵字,該關鍵字只出如今類內部的聲明語句。

由於靜態數據成員不屬於類的任何一個對象,所在它們並非在建立類的對象時被定義的,這意味着它們不是由類的構造函數初始化的。並且通常來講,咱們不能在類的內部初始化靜態成員。相反的,必須在類的外部定義和初始化每一個靜態成員。但若是給靜態成員加上constexpr,那麼就能夠在類內初始化了。

在類外的定義中,定義靜態數據成員的方式和定義成員函數差很少,須要指定對象的類型名,類名,做用域運算符以及成員函數的名字,

在類外初始化靜態成員的格式以下:

數據類型 類名::靜態成員名 = 初始值

//Account.h
class Account{
    public:
        static double rate(){return interestRate;}
        static void rate(double);
    private:
        static double interestRate;//聲明,不能有初始值,不能在類內部初始化 static 成員
        static constexpr int period = 30;//能夠在類內部初始化 static constexpr 成員
};
//Account.cpp
#include "Account.h"

double Account::interestRate = rate();//初始化靜態成員interestRate,不能再次使用static關鍵字。這裏等同於double Account::interestRate = interestRate。通過interestRate的賦值後,interestRate的值就是0.

void Account::rate(double newRate){//初始化靜態函數rate,不能再次使用static關鍵字
    interestRate = newRate;
}

上面案例中,Account::interestRate = rate()語句至關於Account::interestRate = interestRate,當靜態變量定義後就會分配內存空間。這裏interestRate是int類型,因此默認值爲0。

 

下面的案例有比較清晰的解釋:

    #include <stdio.h>
    class A {
        public:
            static int a; //聲明但未定義
     };
    int main() {
        printf("%d", A::a);
        return 0;
    }

編譯以上代碼會出現「對‘A::a’未定義的引用」錯誤。這是由於靜態成員變量a未定義,也就是尚未分配內存,顯然是不能夠訪問的。

    #include <stdio.h>
    class A {
        public:
            static int a; //聲明但未定義
     };
    int A::a = 3; //定義了靜態成員變量,同時初始化。也能夠寫"int A:a;",即不給初值,一樣能夠經過編譯
    int main() {
        printf("%d", A::a);
        return 0;
    }

   這樣就對了,由於給a分配了內存,因此能夠訪問靜態成員變量a了。


   注意:在類外面  這樣子寫 :int A::a ; 也能夠表示已經定義了。

4.類的繼承

經過繼承(inheritance)聯繫在一塊兒的類構成一種層次關係。一般在層次關係的根部有一個基類(base class),其餘類則直接或間接的從基類繼承而來,這些繼承獲得的類被稱爲派生類(derived class)。

使用冒號(:)表示類的繼承,語法格式爲:

class DerivedClass : public BaseClass{}

 

訪問修飾符

上面的DerivedClass使用public的訪問修飾符能夠訪問BaseClass中的全部成員,這裏須要注意,這裏的public和成員前面的訪問修飾符不是一個含義。

上面DerivedClass能夠訪問BaseClass中的全部成員(由於使用了pubic),若是使用了private的話,那麼DerivedClass不能訪問BaseClass中的任何成員(包括public成員也不能夠)。

DerivedClass能夠訪問BaseClass中的全部成員,但這並不表明DerivedClass的派生類也能夠徹底訪問DerivedClass的成員。這取決於DerivedClass的派生類在訪問DerivedClass所使用的修飾符。

class DerivedClass2 : private DerivedClass{}

DerviedClass2不能訪問DerivedClass的任何成員。

 

virtual函數

被virtual修飾的函數被稱爲虛函數,c++中虛函數所在的類也能夠聲明實例,只有純虛函數所在類纔不能夠聲明實例(後面會講到的)。

使用virtual的函數是設計者但願各個派生類定義適合自身的版本。

class Shape{
    private:
        virtual std::string name(){
            return "i am a shape";
        }
}
class Square : public Shape{
    private:
        std::string name() override{
            return "i am a square";
        }
}

 

final關鍵字

若是不但願發生繼承,那麼就可使用final關鍵字,final關鍵字表示當前類是最終類,不能再有任何派生類。

class Square final{}

或者

class Square final : public Shape{}

 

5.定義抽象類

上面的實例中Shape和Square均可以聲明實例,那麼有沒有什麼限制只能讓Square聲明實例呢?答案是確定的。使用純虛函數(pure virtual)從而令程序實現咱們設計的目的。

咱們在函數體的位置(在聲明語句的分號以前)書寫=0就能夠聲明一個純虛函數。=0只能出如今類內部的虛函數聲明語句處:

class Shape{
    private:
        std::string name() = 0;
}
//Shape();錯誤,不能聲明Shape的實例

 

6.對象的多態性

繼承主要做用就是多態。c++中要實現多態,必須要使用指針(推薦使用智能指針)。使用普通類是沒法實現多態的。例如:

class Shape{
    public:
        virtual std::string name()  {
            return "I am a shape";
    }
};
class Square : public Shape{
    public:
        std::string name() override {
            return "I am a square";
        }
};


咱們首先使用普通類的方式指定一個Shape指向一個Square的實列對象。

    Shape s = Square();
    std::cout << s.name() << std::endl;

輸出結果:

I am a Shape


咱們指望的輸出是「I am a Square」,但實際的輸出是"I am a Shape"。這樣顯然沒有多態的效果,若是咱們換成shared_ptr的話(普通指針均可以,爲了減小資源回收帶來的複雜度,推薦使用智能智能),那麼就解決這個問題:

    std::shared_ptr<Shape> s = std::make_shared<Square>(Square());
    std::cout << s->name()<< std::endl;

輸出結果:

I am a Square

 

7.繼承類中構造函數與析構函數的調用規則

在派生類中構造類的實例時,有義務對全部基類的成員完成初始化操做。
例如:

class Shape{
    private:
        std::string name;
    public:
        Shape(std::string name):name(name){std::cout << "in shape constructor" << std::endl;}
};
class Square : public Shape{
    private:
        long width;
    public:
        Square(std::string name,long wd):Shape(name),width(wd){std::cout << "in square constructor" << std::endl;};
};

咱們在Square的構造函數中調用了Shape(name)初始化基類的成員。
當初始化一個Square時候,輸出:

in shape constructor
in square constructor

固然,若是咱們不在Square中調用Shape的構造函數,那麼Shape的默認構造函數就會被調用。

從上面的輸出結果可知,構造函數的首先從基類開始構造,再是派生類。而析構函數剛好和構造函數相反,先是派生類,再是基類。

struct Shape{
    ~Shape(){std::cout << "shape destructor" << std::endl;}
};
struct Square : public Shape{
    ~Square(){std::cout << "square destructor" << std::endl;}
};

接下來構造一個Square對象,再銷燬:

{
    Square();
}

輸出:

square destructor
shape destructor

 

8. 多重繼承

在前面的例子中,派生類都只有一個基類,稱爲單繼承(Single Inheritance)。除此以外,C++也支持多繼承(Multiple Inheritance),即一個派生類能夠有兩個或多個基類。

多繼承容易讓代碼邏輯複雜、思路混亂,一直備受爭議,中小型項目中較少使用,後來的 Java、C#、PHP 等乾脆取消了多繼承。

多繼承的語法也很簡單,將多個基類用逗號隔開便可。例如已聲明瞭類A、類B和類C,那麼能夠這樣來聲明派生類D:

class D: public A, private B, protected C{
    //類D新增長的成員
}


D 是多繼承形式的派生類,它以公有的方式繼承 A 類,以私有的方式繼承 B 類,以保護的方式繼承 C 類。D 根據不一樣的繼承方式獲取 A、B、C 中的成員,肯定它們在派生類中的訪問權限。

多繼承下的構造函數

多繼承形式下的構造函數和單繼承形式基本相同,只是要在派生類的構造函數中調用多個基類的構造函數。以上面的 A、B、C、D 類爲例,D 類構造函數的寫法爲:

D(形參列表): A(實參列表), B(實參列表), C(實參列表){
    //其餘操做
}

基類構造函數的調用順序和和它們在派生類構造函數中出現的順序無關,而是和聲明派生類時基類出現的順序相同。仍然以上面的 A、B、C、D 類爲例,即便將 D 類構造函數寫做下面的形式:

D(形參列表): B(實參列表), C(實參列表), A(實參列表){
    //其餘操做
}

那麼也是先調用 A 類的構造函數,再調用 B 類構造函數,最後調用 C 類構造函數。

下面是一個多繼承的實例:

#include <iostream>
using namespace std;

//基類
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
protected:
    int m_a;
    int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
    cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
    cout<<"BaseA destructor"<<endl;
}

//基類
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
protected:
    int m_c;
    int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
    cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
    cout<<"BaseB destructor"<<endl;
}

//派生類
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void show();
private:
    int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    cout<<"Derived destructor"<<endl;
}
void Derived::show(){
    cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}

int main(){
    Derived obj(1, 2, 3, 4, 5);
    obj.show();
    return 0;
}

運行結果:

BaseA constructor
BaseB constructor
Derived constructor
1, 2, 3, 4, 5
Derived destructor
BaseB destructor
BaseA destructor


從運行結果中還能夠發現,多繼承形式下析構函數的執行順序和構造函數的執行順序相反。

 

多繼承下的二義性問題

什麼是多重繼承的二義性

class A{
public:
    void f();
}
 
class B{
public:
    void f();
    void g();
}
 
class C:public A,public B{
public:
    void g();
    void h();
};

若是聲明:C c1,則c1.f();具備二義性,而c1.g();無二義性(同名覆蓋)。

解決辦法1:-- 類名限定

調用時指名調用的是哪一個類的函數,如

c1.A::f();
c1.B::f();

解決辦法2:-- 同名覆蓋

在C中聲明一個同名函數,該函數根據須要內部調用A的f或者是B的f。如

class C:public A,public B{
public:
    void g();
    void h();
    void f(){
        A::f();
    }
};


還有一種解決方法就是經過虛函數,下面咱們將講解虛函數的用法。

9. 虛函數

儘管派生列表中一個基類只能出現一次,但實際上派生類能夠屢次繼承同一個類,派生類能夠經過它的兩個直接基類分別繼承同一個基類。在這種狀況下,派生類將包含該類的多個子對象。

在C++中,咱們經過虛繼承解決這個問題,虛繼承的目的是令某個類作出聲明,承諾願意共享它的基類,其中共享的基類子對象被稱爲虛基類。不論虛基類在繼承體系中出現了多少次,在派生類中都只包含惟一一個共享的虛基類子對象。咱們經過在派生列表中添加關鍵字virtual來指定虛基類。

虛基類是由最低層的派生類初始化的,即便虛基類不是派生類的直接基類,派生類的構造函數也能夠初始化虛基類,含有虛基類的對象在構造時,首先會初始化虛基類部分,接下來才按照直接基類在派生列表中出現的次序進行初始化。

class ClassA
{
public:
    ClassA(int height):height_(height) {
        std::cout<<"constructor A"<<std::endl;    
    };
    void funcA()
    {
        std::cout << "in funcA: height_ = "<< height_ << std::endl;
    }
protected:
    int height_;
};


class ClassB:public virtual ClassA
{
public:
    ClassB():ClassA(10)
    {
        std::cout<<"constructor B"<<std::endl;
    }
    void funcB()
    {
        std::cout << "in funcB: height_ = "<< height_ << std::endl;
    }
};


class ClassC :public virtual ClassA
{
public:
    ClassC() :ClassA(100)
    {
        std::cout<<"constructor C"<<std::endl;
    }
    void funcC()
    {
        std::cout << "in funcC: height_ = "<< height_ << std::endl;
    }
};


class Worker :public ClassB, public ClassC
{
public:
    Worker():ClassB(),ClassC(),ClassA(50)
    {
        std::cout<< "constructor Worker" <<std::endl;
    }
    void funcWorker(){
        std::cout << "in funcWorker: height_ = " << height_ << std::endl;
    }
};
int main()
{
    Worker w;
    std::cout << "------------" <<std::endl;
    w.funcA();
    w.funcB();
    w.funcC();
    w.funcWorker();
    int i;
    std::cin >> i;
    return 0;
}

結果:

constructor A
constructor B
constructor C
constructor Worker
------------
in funcA: height_ = 50
in funcB: height_ = 50
in funcC: height_ = 50
in funcWorker: height_ = 50

從運算結果看出,類A的實例只初始化了一次,不管Worker的構造函數的書寫順序是怎樣的,只要有虛基類(就是本案例中的類A),那麼虛基類的實例將會被首先構造。當後面的類試圖再次構造虛基類的實例時就會被拒接(類B和類C都沒有再次構造類A的實列)。本例中在類Worker中必須顯式的調用類A的構造函數,若不顯式調用類A的構造函數則編譯器不知道以誰構造的類A實列爲準(類B構造的仍是類C構造的?)

相關文章
相關標籤/搜索