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


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

C++中類的基本模板如下:

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

例如:

//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值,函數體為空。在函數體中,也可以改變參數的值:

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

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

 

2.this使用的注意點

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

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

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類型。

例如:

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構造的?)


免責聲明!

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



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