1. 程式人生 > >C++類的理解(一):類的初識

C++類的理解(一):類的初識

一、類的意義,以及類與物件:
類的內容比較多,但類是面向物件的基礎,所謂面向物件(object),其實就是一種更高層次的模組化,也稱為封裝。C語言中,稍微複雜點的資料結構都是用結構體來寫的,結構體本質上就是把一些相關的資訊打包儲存,比如貓:

struct Cat
{
    char name[20];
    int age;
    char sex;
};

這樣我們通過鍵盤輸入很多貓的資訊就可以用結構體來儲存:

Cat cats[100];
for(int i=0;i<n;++i)
{
    scanf("%s %d %c",name,age,sex);
    strcpy
(cats[i].name,name); cats[i].age=age; cats[i].sex=sex; }

如果沒有結構體,那我們可能就要用三個陣列來存放這些資料,然後用他們的下標來一一對應聯絡,這是可行的,但對於寫程式的人來說負擔是很大的,比如:

//拙劣的程式碼:
char name[100][100];
int age[100];
char sex[100];
//然後name[i][100],age[i],sex[i]是相關聯的一組數,描述同一個貓的特徵。(累得很) 

結構體將屬性打包,而類不僅打包屬性,還打包操作這些屬性的方法(函式)。
還用貓來打比方:

class Cat
{
public
: //類的許可權系統,後面再說,現在暫時都用public char name[100]; int age; char sex; public: void printInfo() { cout<<"This cat's name is "<<name<<endl; cout<<"This cat's age is "<<age<<endl; } };

這個時候我們不僅像結構體那樣定義了屬性,還定義了一個輸出屬性資訊的函式方法。
而此時如果執行這個類的定義,記憶體裡發生了什麼呢?
回答是:什麼也沒發生,因為現在只是定義了這個類,而沒有定義一個類的例項,也叫物件,物件和例項是一回事。

貓是一個類,我倆的寵物貓“天天”則是貓這個類的一個例項;
車是一個類,我的賓士車是車這個類的一個物件;
書是一個類,《c++ primer》是書的一個物件。

所以,類只是一個模板,是抽象的,而物件是具體的。問一個類的屬性具體是多少一般是不可以的,而要問一個物件的屬性是多少才是正確的。
比如:
車是個類,問:車的顏色是什麼? 答不出來。
貓是個類,問:貓的名字是什麼?天下貓的名字千千萬,答不上來。
但問:我的賓士車(物件)是什麼顏色?可以答:黑色。

再回到剛剛那個問題,Cat這個類運行了之後記憶體裡什麼都沒發生,因為類是抽象的,計算機並沒有給它分配任何儲存的空間,只有當類的物件定義了之後記憶體才會根據這個模板,給他分配空間。

Cat mypet;

這句話出來的一瞬間,系統才會分配一個Cat型別大小的空間,這個時候mypet.name, mypet.age, mypet.sex都是有了自己對應大小的空間了,有了空間,我們就可以對他們的值進行初始化:

strcpy(mypet.name,"tiantian");
mypet.age=1;
mypet.sex='F';

然後可以呼叫類的函式來輸出這個貓的資訊:

mypet.printInfo();

注意這裡是mypet.printInfo(),而不是Cat.printInfo() 因為前者的意思是列印我的寵物資訊,後面那個是列印貓的資訊,那就相當於在問系統,貓的名字,貓的性別,系統答不上來的。

現在如果我又定義了一隻貓:

Cat anotherCat;

然後系統又會根據模板,造出另一個貓的物件,這兩個物件的空間是互相獨立的,互不干擾。

二、類的初始化
所謂初始化,就是把一個剛建立的資料設定成我想要的值,而不是一個我不能掌控的隨機數。
前面我們的Cat型別,初始化用的就是一般的賦值,這種方法是可行的,但當屬性很多,而大部分屬性都是預設的情況下,這種方法會有些讓人煩躁,比如:
定義一個二叉樹節點:

class Node
{
public:
    int index;
    Node* left;
    Node* right;
};

此例中,初始化left和right是指標,指標初始化時預設寫0一般,用上面的程式碼,每一次新建一個Node都要寫

Node node;
node.index=123;
node.left=0;
node.right=0;

後面兩行寫多了就煩了。

c++為類提供了初始化函式,這個函式在物件被建立時有系統自動呼叫,是建立物件的最後一步。也就是說,初始化函式是建立物件的一部分,初始化函式退出之後,該物件才完成了建立。

初始化函式和類名要保持一致,且沒有返回值,連void都沒有!!!

class Cat
{
public:     //類的許可權系統,後面再說,現在暫時都用public
    char name[100];
    int age;
    char sex;
public:
    Cat(char* name,int age,char sex)   //初始化函式
    {
        strcpy(this->name,name);
        this->age=age;
        this->sex=sex;
    }
    void printInfo()
    {
        cout<<"This cat's name is "<<name<<endl;
        cout<<"This cat's age is "<<age<<endl;
    }
};

至此我們來看如何用這個初始化函式,
比如剛剛我們的程式碼,建立一個物件myCat;

Cat myCat; 

這麼寫,首先宣告它是錯的,我們想想前面的建立物件的步驟,分配空間沒問題,我類模板放在那邊呢,屬性都寫著呢,要多少空間就定下來了(注意啊,函式不佔空間啊,物件裡面佔空間的只有屬性啊),但是初始化的時候,初始化函式問我要三個引數,我給了嗎?沒給!沒給就報錯啊,編譯都通不過。

之前我這麼定義為啥沒問題,難道我就不能先這麼定義著,然後再通過前面那種方式給他賦值嗎?答案:不能!

為什麼?因為之前我沒給初始化函式,沒給的話,c++預設會自動給一個引數表為空且什麼都不做的初始化函式,也就是像下面那樣子:

class Cat
{
public:     //類的許可權系統,後面再說,現在暫時都用public
    char name[100];
    int age;
    char sex;
public:
    Cat()   //初始化函式,沒寫的話,系統就自動給一個這樣的初始化函式,什麼都沒做。
    {
    }
    void printInfo()
    {
        cout<<"This cat's name is "<<name<<endl;
        cout<<"This cat's age is "<<age<<endl;
    }
};

而當你給了一個初始化函式時,系統就不給預設的了,所以原先那種就不行了,那麼怎麼辦?
看下面的程式碼:

scanf("%s %d %c",name,age,sex);
Cat myCat(name,age,sex);

通過myCat後面跟一個括號,把引數傳給初始化函式。括號裡面的東西要和初始化函式的引數表一一對應。沒有第二種寫法!

再來看剛剛二叉樹那個例子,給他寫一個初始化函式:

class Node
{
public:
    int index;
    Node* left;
    Node* right;
public:
    Node(int index)
    {
        this->index=index;   //this指標應該沒啥問題的吧
        left=0;
        right=0;
    }
};

然後我初始化時候引數表只要一個index數值就好,所以它的物件這麼建立:

Node t(10); 
//此時: t.index==10,  t.left==0,  t.right==0

三、類的物件的使用
定義好了一個類,此時它就和基礎型別無異了,可以定義該類的普通物件,也可以定義指向該類的指標物件。假設我們所有的程式在64位機子上跑,首先64位機和32位機區別在哪?這個你現在應該知道的,你告訴我吧。
然後我定義一個普通物件和指標物件(還用上面的自定義了初始化函式的Cat類):

Cat myCat("TianTian",1,'F');
Cat *p;

題外話1:

兩個問題,myCat 變數佔多大的空間,p指標又佔多大空間?這兩個問題是理解指標變數和普通變數很關鍵的問題。
答:
myCat佔 sizeof(Cat) 這麼大的空間,而p佔8位元組。
為什麼?因為普通變數大小很直接,但指標存的是地址,64位機地址就是64bits,也就是8位元組,是定的。所以說不管是: int*, double*,bool*, Cat* 這些指標,或者是int**,char**,這種指向指標的指標,他們佔的大小都是8位元組,因為存的都是地址。

那麼現在在理解下下面這個類定義為什麼不對:

class Node
{
public:
    int index;
    Node next;
public:
    ......
}

答:暫時先略,你先思考哦

回到上面,Cat* p; 是一個指標,但目前是一個危險的懸空指標,使用它操作類如下:

p=&myCat;
p->name;    //=="TianTian"
p->age;     //==1
p->sex;     //=='F'
p->printInfo();

然後通過上面的方法可以操作物件的屬性和方法。
或者也可以寫成這樣(對是對,不過沒人這樣用):

(*p).name;
(*p).printInfo();
//因為*p自然就是取所指空間的值,和myCat是等價的,上面的方法方便些,注意兩者的區別。
//不要混用,比如p.name是錯的,因為p是指標,不是一個類,沒有name屬性
//(*p)->name,也是錯的(*p)不是一個指向類的指標,它只是一個普通物件空間myCat。

這一篇的內容很基礎,是能用好c++類的前提,另外從我曾經剛開始接觸類和物件的體驗來說,初學時上面所舉的例子可能容易接受,但可能想象不出在計算機程式設計領域類能表示什麼,或者一些奇奇怪怪的東西用類怎麼描述,比如描述一個類:圓,或者磁碟,或者CPU,或者一個多邊形複雜體的樣子。有些東西光是人用語言來描述就都很困難了,用一個類就能描述出來嗎?

回答是:當然。。。不行!!!
那這玩意兒有啥用呢?有的書上會說萬物皆物件,這又算啥?
實際上,在工程中,我們描述或者定義一個型別時,只定義我們感興趣的部分,屬性和操作都是這樣的。比如圓我們只關心圓心和半徑:

class Circle
{
public:
    int x;
    int y;
    int r;
};

題外話2:

如果我還有點Point這個物件我可以這麼寫:

class Point
{
public:
    int x;
    int y;
public:
    Point(int x,int y)
    {
        this->x=x;
        this->y=y;
    }
};

class Circle
{
public:
    Point center;
    int r;
public:
    Circle(int x,int y, int r): center(x,y)
    {
        this->r=r;
    }
};

int main()
{
    Circle p(1,2,3);
}

這時候main函式中要構造Circle物件p,要構造p就要先構造p裡面那個Point物件center,而Point是沒有預設建構函式的,因為我自定義了一個,所以只能通過上面那種方式給center的建構函式傳值,如果有多個屬性是沒有預設建構函式的物件的話,都寫在後面,用逗號隔開。
這裡面要注意這些變數的構造順序:

開始構造 p(1,2,3)
……開始構造p.Center(1,2)
……p.Center構造完成
p構造完成

它的順序是像遞迴一樣,先把基礎的構造好了,才會去構造上層的東西。
然後要用資料的時候就是p.center.x 這種寫法。

繼續回到上面,剛剛我們在討論我們用類描述東西時候是描述我們感興趣的部分,比如剪刀,我們如果只關心大小,我們就:

class Scissor
{
public:
    int size;
    //Color color;
};

如果還關心顏色那就加個Color color; 屬性,並定義Color類:

class Color
{
public:
    int r;
    int g;
    int b;
}

如果有人非要關心什麼形狀,這種難以定量描述的屬性,直接點,我們可以在電腦上存個圖片,然後加一個屬性char shape_url[255]; 初始化的時候把圖片地址賦過來,也是可以的!
總之,無論什麼屬性都要轉變成數值可表示的東西才行,實在不好表示的,像某人的聲音,這種東西就跟剛剛的圖片一樣,弄個連結作為屬性就好。