1. 程式人生 > >c++| |類和物件(上篇)

c++| |類和物件(上篇)

類和物件(上篇)

1.類和物件的初步認知

  • c語言是面向過程的,關注的是過程分析出求解問題的步驟,通過函式呼叫逐步解決問題

  • c++是基於面向物件的,關注的是物件,將一件事情拆分成不同的物件,考物件之間的互動完成

2.類的引入

c語言中,結構體中只能定義變數,在c++中,結構體內不僅可以定義變數,也可以定義函式

例:

struct Student
{
    void SetStudentInfo(const char* name, const char* gender, int age)
    {
        strcpy(_name, name);
        strcpy(_gender, gender);
        _age = age;
    }
    
    
    char _name[20];
    char _gender[8];
    int sge;
}
​
int main()
{
    Student s;//c++中可以直接不寫struct,直接將Student當做一個類來使用
    struct Student s;//c語言中必須寫strcut
    s.SetStudentInfo("yk","男","18");
    return 0;
}

上面結構體的定義,在c++中更喜歡用class來代替

3.類的定義

class classname
{
    //類體:由成員函式和成員變數組成
    
};  //一定要注意後面的分號

class為定義類的關鍵字,classname為類的名字,{}中為類的主體,注意類定義結束時後面的分號。

類中的元素稱為類的成員:類中的資料稱為類的屬性或者成員變數;類中的方法稱為類的方法或者成員函式

  • 類的兩種定義方式:

    • 宣告和定義全部都放在類體內

    class Person
    {
    public:
        void shoWinfo()
        {
            cout << _name << "-" << gender << "-" << _age << endl;
        }
    public:
        char _name[20];
        char _gender[10];
        int _age;
    }

    注意】:成員函式如果在類中定義,編譯器可能會將其當做行內函數來處理

    • 宣告為放在.h檔案中,類的定義放在.cpp檔案中

    //宣告放在.h檔案中
    class Person
    {
    public:
        void shoWinfo(); 
    public:
        char _name[20];
        char _gender[10];
        int _age;
    }
    //定義放在類的實現檔案中person.cpp中
    #include "person.h"
    ​
    //必須要新增作用域限定符,告訴系統這個函式是哪個類的
    void Person::showinfo()
    {
        cout << _name << "-" << gender << "-" << _age << endl;
    }
  • 【一般情況下,更期望採用第二種方式】

4.類的訪問限定符及封裝

4.1 封裝

【面試題】:面向物件的三大特性:封裝,繼承,多型

  • 再類和物件階段,我們只研究類的封裝特性,那什麼是封裝呢?

    • 封裝:將資料和操作資料的方法進行有機的結合,隱藏物件的屬性和實現細節,僅對外公開介面和物件來進行互動

4.2訪問限定符

c++實現封裝的方式:用類將物件的屬性與方法結合在一塊,讓物件更加的完善,通過訪問許可權選擇性的將其介面提供給外部的使用者使用

  • 訪問限定符

    • public(共有)

    • pretected(保護)

    • private(私有)

  • 【訪問限定符的說明】:

    1. public修飾的成員在類外可以直接被訪問

    2. protected和private修飾的成員在類外不能直接被訪問

    3. 訪問許可權作用域從該訪問限定符出現的位置開始一直到下一個訪問限定符出現為止

    4. class的預設訪問許可權是private,struct為public(因為struct要相容c)

【注意】:

​ 訪問限定符只在編譯時有用,當資料對映到記憶體後,沒有任何訪問限定符上的區別

【面試題】:

  1. 如何在類外訪問一個類的私有的成員變數?

    答:可以在類外訪問該類共有的成員函式,然後使用該成員函式完成對成員變數的訪問

  2. c語言和c++中struct有什麼區別?c++中struct和class有什麼區別?

    答:c語言中的struct裡面只可以寫成員變數而不能寫成員函式,而c++中就可以向裡面寫入成員函式,將資料和方法封裝在一起。c++中struct所有的成員函式和成員變數的訪問限定符都是public(要全面相容c),而c++中的class就不需要將所有的成員函式和成員變數的訪問限定符都是設定為public。struct的預設許可權是public,而class的預設許可權是private。

5.作用域

類定義了一個新的作用域,類的所有成員都在類的作用域中,在類外定義成員,需要使用::作用域解析符,指明成員屬於那個類域。

  • 作用域

  1. 區域性域

  2. 全域性域

  3. 類域

  4. 名字空間域

以下幾個Test函式屬於過載嗎?改成語執行結果列印什麼?

namespace N1
{
    int a = 10;
    void TestFunc(int)
    {
        cout << "N1::TestFunc(int)" << endl;
    }
}
​
int a = 10;
void TestFunc(char)
{
    cout << "TestFunc(char)" << endl;
}
​
class Test
{
public:
    void SetA(int a)
    {
        a = a;
    }
    
    void PrintA()
    {
        count << a << endl;
    }
    
    void TestFunc(double)
    {
        cout << "TestFunc(double)" << endl;
    }
    
private:
    int a;
}
​
int main()
{
    Test t;
    t.SetA(30);
    cout << N1::a << endl;
    cout << a << endl;
    t.PrintA();
    
    return 0;
}   

問題:在使用一個變數,必須先對變數進行宣告,在上述Test類中,a成員變數在SetA函式之後,為什麼沒有編譯器沒有報錯?

  1. 儘量避免成員函式的引數和成員變數同名

  2. 成員變數在類中具有全域性作用域屬性

6.類的例項化

用類型別建立物件的過程,稱為類的例項化

  1. 類只是一個模型一樣的東西,限定了類有哪些成員,定義出一個類並沒有分配實際的記憶體空間來儲存它

  2. 一個類可以例項化出多個物件,例項化出的物件佔用實際的物理空間,儲存類成員變數

  3. 例如:類例項化出物件就像現實生活中使用建築設計圖來建造房子,類就像是設計圖,只設計出需要什麼東西,但是並沒有實體的建築存在,同樣類也只是一個設計,例項化出的物件才能實際儲存資料,佔用實體記憶體

7.類物件模型

7.1如何計算類的大小

問題:類中既可以有成員變數,又可以有成員函式,那麼一個類的物件包含了什麼?如何計算一個類的大小?

答:類的物件裡面只包含了類的成員變數,不包含成員函式,成員函式對於每個物件來說是共有的,而成員變數是每個物件各自私有一份。

7.2 類物件的儲存方式猜測

  • 物件中包含類的各個成員

    • 缺陷:每個物件中成員變數是不同的,但是呼叫同一份函式,如果按照此種方式儲存,當一個類建立多個物件時,每個物件中都會儲存一份程式碼,相同程式碼儲存多次,浪費空間,能否將程式碼只儲存一份,呼叫編譯器只要能找到函式的入口地址即可?

  • 函式單獨存放一份,物件中給一個指標指向存放成員函式表的首地址

    • 缺陷:物件中還是多了一個指標

  • 只儲存成員變數,成員函式存放在公共的程式碼段

【問題】:對於上述的三種儲存方式,那計算機到底是按照那種方式來儲存的?

我們在通過對下面的不同物件分別獲取大小來分析看下:

//類中既有成員變數,又有成員函式
class A1
{
public:
    void f1()
    {
    
    }
    
    void f2()
    {
    
    }
private:
    int _a;
};
​
//類中僅有成員函式
class A2
{
public: 
    void f1()
    {
    
    }
    void f2()
    {
    
    }
};
​
//類中什麼都沒有---空類
class A3
{
    
};

sizeof(A1):4 sizeof(A2): 1 sizeof(A3): 1

結論:一個類的大小,實際就是該類中的“成員變數”之和,當然也要進行記憶體對齊,注意空類的大小

空類比較特殊,編譯器給了空類一個位元組來唯一標識這個類。

7.3 結構體記憶體對齊規則

  1. 第一個成員在與結構體偏移量為0的地址處

  2. 其他成員要對其到某個數字(對齊數)的整數倍的地址處

    • 注意:對齊數=編譯器預設的一個對齊數與該成員大小的較小值

    • VS預設對齊數為8,gcc的預設對齊數為4

  3. 結構體的總大小為:最大對齊數(所有變數型別最大者與預設對齊引數去最小)的整數倍

  4. 如果嵌套了結構體的情況,巢狀的結構體對齊到自己的最大對齊數的整數倍,結構體的整體大小就是所有最大對齊數(含巢狀結構體的對齊數)的整數倍

【面試題】:

  1. 結構體怎麼對齊?為什麼要進行記憶體對齊?

答:1.平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。

​ 2.效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器要做兩次記憶體訪問;而對其的記憶體訪問僅需要一次訪問

​ 總的來說:結構體的記憶體對齊是拿空間來換時間的做法

  1. 如何讓結構體按照指定的引數進行記憶體對齊?

答:

#progma pack(4) //4位元組對齊
​
#progma pack() //恢復對齊狀態
  1. 如何知道結構體中某個成員相對於結構體起始位置的偏移量

答:使用offsetof巨集來判斷結構體中成員的偏移地址。使用offsetof巨集需要包含stddef.h標頭檔案,該巨集定義如下:

#define offsetof(type,member) (size_t)&(((type*)0)->member)

在linux下offset是一個函式:

功能:求一個結構體的成員變數偏移量的大小
#include <stddef.h>
​
size_t offsetof(type, member);
​
引數:
    type:結構體的型別
    member:結構體內的成員變數
返回值:返回給定型別內的給定成員的偏移量,單位為位元組
  1. 什麼是大小端?如何測試一臺機器是大端還是小端,有沒有遇到要考慮大小端的場景?

答:大端:低位位元組序放在了高地址處

​ 小端:低位位元組序放在了低地址處

測試:

#include <stdio.h>
​
int check(int a)
{
    //return ((*(&a)) & 0x1);
    return (*(char*)&a);
}
​
int main()
{
    int a = 1;
    if (check(a) == 1)
    {
        printf("該機器是小端!\n");
    }
    else
    {
        printf("該機器是大端!\n");
    }
    
    return 0;
}

TCP/IP 協議規範:在網路上傳輸資料時,由於資料傳輸的兩端可能對應不同的硬體平臺,採用的儲存位元組順序也可能不一致,因此 TCP/IP 協議規定了在網路上必須採用網路位元組順序(也就是大端模式) 。

8.this指標

8.1 this指標的引出

c++編譯器給每個成員函式增加了一個隱藏的指標引數,讓該指標指向當前物件(函式執行時呼叫該函式的物件),在函式整體中所有成員變數的操作,都是通過該指標去訪問的,只不過所有的操作對使用者是透明的,即使用者不需要來傳遞,編譯器自動完成。

8.2 this指標的特性

  1. this指標的型別:類型別* const

  2. 只能在“成員函式”內部使用‘

  3. 時時刻刻指向當前物件,不屬於物件的一部分,不也會影響sizeof的結果

  4. **this指標是成員函式一個隱含的指標形參,一般情況由編譯器通過ecx暫存器自動傳遞,不需要使用者傳遞

【面試題】:

  1. this指標存在哪裡?

答:也就是成員函式的其它引數正常都是存放在棧中。而this指標引數則是存放在暫存器中

  1. this指標可以為空嗎?

答:this指標可以為空。

​ 當在函式內部的時候不需要訪問成員變數的時候,就可以把this指標置為空,而需要在成員函式中訪問成員變數的時候這個時候就需要this指標來訪問成員變數,這個時候this指標就不能為空,否則會報錯。

例:

class test
{
public:
    void test()
    {
        cout << "in test class" << endl;
    }
    void change()
    {
        cout << "in change class" << endl;
        cout << "_a" << endl;
    }
private:
    int _a;
};
​
int main()
{
    test p = NULL;
    p.test();
    p.change();//出錯,因為通過空指標訪問類中的成員變數_a
    return 0;
}
​