c++之引用_重載_類的封裝_構造函數初探


站在編譯器和C的角度剖析c++原理, 用代碼說話


引用深入

引入簡單的介紹請參考引用初探這篇文章. 然後我們先進行普通引用的深入. 那什麼是普通引用呢?就是基本數據類型的引用,而不是結構體或者類的引用,這個話題我們會單獨專題討論,這塊兒是比較複雜的. 迴歸正題. 先上代碼:

int getAA1()
{
    int a;
    a = 10;
    return a;//基礎類型返回的時候也會有一個小的副本.
}
int & getAA2()
{
    int a;
    a = 10;
    return a;
}
int main(int argc, const char * argv[]) {
    int a1 = 0;
    int a2 = 0;
    a1 = getAA1();
    a2 = getAA2();
    int &a3 = getAA2();
    printf("a1%d\n", a1);//10
    printf("a2%d\n", a2);//10
    printf("a3%d\n", a3);//亂碼
    return 0;
}

首先分析這個易犯錯誤模型. 咱們先討論a3爲什麼會亂碼?一個函數返回不管是一個引用還是常量,如果不是靜態變量或全局變量,那麼出了作用域就會釋放掉內存空間.這種說法其實少了一個階段. 當return後, 首先編譯器會將這個值產生一個副本存放在寄存器或者內存其他地方,然後作用域內的有明確地址的內存釋放,但是小副本還沒釋放,當一旦執行賦值操作後,這個副本纔會被釋放. 是不是對a3的亂碼有點眉目了. 回顧上一篇說到引用的本質, 那麼當函數返回引用的時候,其實返回的就是地址. 因爲你用int &a3 =這種方式接的時候其實編譯器也是偷偷的取了地址. 那麼,返回出這個地址給了a3接上,這時這個地址指向的內存空間就釋放了,但是當我們調用Printf的時候,隱藏做了個*p的操作, 這時不亂碼就怪了!!!那麼爲什麼a2會正確呢?是因爲返回引用後直接用一個變量接到了. 這裏有些同學就會疑惑:what?難道還能用一個不是引用的變量去接引用?是可以的. a2 = getAA2();這裏返回的是引用,但是a2卻不是引用類型,那麼賦值的時候進行了的是引用值拷貝操作,他們倆的地址肯定是不同的,只是將地址指向的內存空間的值進行了拷貝,這樣的話迴歸我們的話題,將一個值已經存在了變量中了,那你隨便釋放祖先都無所謂啊. 然後a1的問題呢,大家憑直覺都知道是對的,但是自行深層次用上面的方法考慮一下吧?
接下來再考慮一種情況:

int j(){
    static int a = 10;
    a++;
    printf("a%d\n", a);
    return a;
}
int & j1(){
    static int a = 10;
    a++;
    printf("a%d\n", a);//101
    return a;
}
int main(){
    j();//11
    j();//12 
    j1();//11
    j1();//12
    //j() = 100;會報錯,常量值沒有內存不能當左值
    j1() = 100;
    return 0;
}

首先a放在了靜態區,所以它的地址並不會隨着作用域的影響而析構. 具體到j() = 100;的問題我們自然清楚了吧,返回的是個常量並沒有內存空間,所以是沒法當左值的. 但是j1() = 100;返回的是本質就是地址,所以OK. 當引用當左值的時候,編譯器一看是個引用,機會自動*p操作. 結論就是當被調用的函數當左值的時候必須返回一個引用.
我們再看一個案例:

int g1(int *p){
    *p = 100;
    return *p;
}
int & g2(int *p){
    *p = 100;
    return *p;
}
int main(void){
    int a1 = 10;
    a1 = g1(&a1);
    int &a2 = g2(&a1);
    printf("a1%d\n", a1);//100
    printf("a2%d\n", a2);//100
    return 0;
}

a1 = g1(&a1);是典型的C中間接賦值操作,就不說了. int &a2 = g2(&a1);這種方式就不會亂碼的原因是,內存已經提前打造好了,雖然返回的是引用,但是其實返回的就是打造好的這塊地址. 所以沒有被析構掉.
當我們使用引用的語法的時候,我們不去關心編譯器是怎麼做的,當我們分析亂碼問題的時候,我們纔去考慮編譯器怎麼用的. 我們必須思考問題的時候跳進去思考,一開始很多人都會覺得這樣太繁瑣了,很簡單的事想的那麼複雜了,但是解決問題能力就是從這種方式提高的.
在C中我們經常使用二級指針來進行間接分配內存地址空間, 那麼我們看這樣一段代碼:

struct Teacher
{
    char name[64];
    int age;
};
int getTe(Teacher **myp){
    Teacher *p = (Teacher *)malloc(sizeof(Teacher));
    if(p == NULL){
        return -1;
    }
    memset(p, 0, sizeof(Teacher));
    *myp = p;
    return 0;
}
int getTe2(Teacher* &myp){//指針的引用
    myp = (Teacher *)malloc(sizeof(Teacher));
    myp->age = 34;
    return 0;
}
int main(void){
    Teacher *p = NULL;
    getTe(&p);
    printf("%d", p->age);
    getTe2(p);
    return 0;
}

getTe(&p);就是我們C中的方式,這裏不做討論. 首先不要被getTe2中的形參Teacher* &myp嚇住,這就是指針的引用,我們之前一種用的都是變量的引用,其實一樣的,就是傳入的值不能是一個變量而是一個指針了. myp是p的別名,所以當我們操作myp的時候就相當於是給p分配內存空間了,但是p的地址已經是有了的. 這樣就是不用二級指針同樣打造了二級指針的效果. 具體過程就是編譯器發現是個引用,然後就對p取地址然後扔進函數中,&p不就是二級指針了嗎?爲什麼耳機指針分配內存就能不受作用域影響?好問題,我也是剛思考到這個問題,這樣,當定義一個指針指向NULL的時候,這個4字節的內存區有個地址,但是這個棧中的這個內存中的內容時NULL, 如果不用二級指針的話傳入的參數是一個空指針,對空指針的操作是能夠使程序崩潰的. 所以用二級指針,使用malloc後,是在堆上分配了內存,並且地址號就存在了p的內存空間中了,這樣*p找到了malloc的分配的內存中值. 那出了作用域豈不是內存釋放了?這樣p的內存空間中的內容不就是指向樂意個可能不存在的地方?堆上的內存你不free是永遠不釋放的.
我們再討論一下常引用問題. 在C++中可以聲明const引用, const Type& name = var;, const引用讓變量擁有隻讀屬性.

int main()
{
    int a = 10;
    const int &b = a; 
    //int *p = (int *)&b;
    b = 11; //err
    //*p = 11; //只能用指針來改變了
    cout<<"b--->"<<a<<endl;
    printf("a:%d\n", a);//11
    printf("b:%d\n", b);//11
    printf("&a:%d\n", &a);//-272632296
    printf("&b:%d\n", &b);//-272632296
    system("pause");
    return 0;
}

感覺上一篇中C和C++之const沒有講清楚,這裏再強調一下:

int main(){
    const int a  = 2;
    int* p = (int*)(&a);
    *p = 30;
    cout<<&a<<endl; //0x28ff08
    cout<<p<<endl;  //0x28ff08
    cout<<a<<endl;//3
    cout<<*p<<endl;//20
}

++中用const定義了一個常量後,不會分配一個空間給它,而是將其寫入符號表(symbol table),這使得它成爲一個編譯期間的常量,沒有了存儲與讀內存的操作,使得它的效率也很高。但是const定義的常量本質上也是一個變量,是變量就會有地址,那麼什麼時候會分配內存?
我們看到,通過 int*p = (int*)(&a);這種方法,可以直接修改const常量對應的內存空間中的值,但是這種修改不會影響到常量本身的值,因爲用到a的時候,編譯器根本不會去進行內存空間的讀取。這就是c++的常量摺疊(constant folding),即將const常量放在符號表中,而並不給其分配內存。編譯器直接進行替換優化。除非需要用到a的存儲空間的時候,編譯器迫不得已纔會分配一個空間給a,但之後a的值仍舊從符號表中讀取,不管a的存儲空間中的值如何變化,都不會對常量a產生影響。
但是常引用是有不同的,當使用常量(字面量)對const引用進行初始化時,C++編譯器會爲常量值分配空間,並將引用名作爲這段空間的別名,使用常量對const引用初始化後將生成一個只讀變量. 也就是說兩個在const int &b = a;的時候就都有了地址,並且地址相同. 這可是不同於普通類型的常量的.

inline內聯函數

inline關鍵字必須和函數實現放在一塊兒,內聯是一個請求,告訴編譯器進行內聯編譯. 什麼是內聯編譯:會直接將函數體插入到函數調用的地方. 是不是又想到了宏? 宏代碼片段是由預處理器處理,進行簡單的文本替換,沒有任何編譯的過程.內聯函數是由編譯器處理,直接將編譯後的函數體插入到調用的地方.當然現代編譯器進行了優化,在我們沒有寫inline聲明的時候,編譯器進行了內聯編譯.
寫內聯函數注意事項:
在內聯函數中不能存在任何形式的循環語句, 不能存在過多的條件判斷語句, 函數體不能過於龐大, 不能對函數進行取地址操作, 函數內聯聲明必須在調用語句之前.
內聯函數相對於普通函數的優勢只是省去了函數調用時的壓棧,跳轉和返回的開銷.因此,當函數體的執行開銷大於壓棧,跳轉和返回所用的開銷時,那麼內聯函數將無意義.
結論:
1. 內聯函數在編譯時直接將函數體插入函數調用的地方。
2. inline只是一種請求,編譯器不一定允許這種操作
3. 內聯函數省去了普通函數調用是壓棧,跳轉和返回。

#define MYFUNC(a, b) ((a) < (b) ? (a) : (b))
inline int myfunc(int a, int b)
{
    return a < b ? a : b;
}
int main()
{
    int a = 1;
    int b = 3;
    //int c = myfunc(++a, b);
    //帶參數的宏和普通函數區別
    int c = MYFUNC(++a, b);  //((++a) < (b) ? (++a) : (b))
    printf("a = %d\n", a); //2  //3
    printf("b = %d\n", b); //3  //3
    printf("c = %d\n", c); //2  //3
    printf("Press enter to continue ...");
    system("pause");
    return 0;
}

默認參數和佔位參數

這些概念都很簡單,我們直接用代碼進行分析就行:

//默認參數
void printAB(int x = 3)
{
    printf("x:%d\n", x);
}
//在默認參數規則 ,如果默認參數出現,那麼右邊的都必須有默認參數
void printABC(int a, int b, int x = 3, int y=4, int z = 5)
{
    printf("x:%d\n", x);
}
//佔位參數,主要是爲了兼容舊的系統
int func (int x, int y, int )
{
    return x + y;
}

int func2(int a, int b, int = 0)
{
    return a + b;
}


int main_04()
{

    printAB(2);
    printAB();
    //    func(1, 2);兩個還調不起來
    //func(1, 2, 3);

    //如果默認參數和佔位參數在一起,都能調用起來。。。。
    func2(1, 2);
    func2(1, 2, 3);
    return 0;
}

函數重載

編譯器調用重載函數的原則:
1. 將所有同名函數作爲候選者
2. 嘗試尋找可行的候選函數
3. 精確匹配實參
4. 通過默認參數能夠匹配實參
5. 通過默認類型轉換匹配實參
6. 匹配失敗

//函數名稱一樣,但是參數數量不一樣,類型不一樣。。參數的順序也不一樣。。。
//函數重載。。
//函數返回值,不是判斷函數重載的標準。。。
void myprint(int a)
{
    printf("a:%d \n", a);
}
void myprint(int a, char *p)
{
    printf("a:%d \n", a);
}
void myprint(char *p, int a)
{
    printf("a:%d \n", a);
}
void myprint(double a)
{
    printf("a:%d \n", a);
}
void main()
{ 
    myprint(10);
    myprint(10,"ddddd");
    myprint("ddd",11);
    system("pause");
}

當函數重載遇上默認函數參數 出現二義性問題:

int func(int a, int b , int c= 0)
{
    printf("a:%d ", a);
    return 0;
}
int func(int a, int b)
{
    printf("a:%d ", a);
    return 0;
}
void main()
{
    int c = 0;
    //存在二義性,調用失敗,編譯不能通過
    //c = func(1, 2); //二義性,編譯器區分不出來調用哪個。。。。
    c = func(1, 2, 3); //這裏就沒有二義性了
}

函數類型定義

首先我們做幾個區分: 定義一個數組類型typedef int MYTYPEAarry[10];之後就可以這樣使用MYTYPEAarry a1;, 就相當於int a[10];.
定義一個數組類型指針類型,typedef int (*MYArrayP)[10];, 表示指向數組的指針, 可以這樣使用MYArrayP myarray = &a1;. int (*myP)[10]; //告訴編譯器給我分配4個字節的內存。。。我要做一個指針變量 這個變量指向一個數組.

int func(int x) // int(int a)
{
    return x;
}
int func(int a, int b)
{
    return a + b;
}
int func(const char* s)
{
    return strlen(s);
}
//定義一個類型,,函數類型。。
//這個函數是 int aaa(int a);
typedef int(*PFUNC)(int a); // int(int a)
typedef int(*PFUNC2)(const char *p); // int(int a)
int main(int argc, char *argv[])
{
    int c = 0;
    //func是一個函數名,函數名就代表函數的入口地址,函數名就是函數指針變量
    {
        PFUNC p = func;
        c = p(1);
    }
    //c = p("ddddd");
    printf("c = %d\n", c);
    {
        PFUNC2 myp2 = func;
        myp2("aaaa");
    }  
    return 0;
}

類的封裝

類的封裝:
封裝,也就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏.
類通常分爲以下兩個部分:類的實現細節和類的使用方法.
封裝的基本概念:一些類的屬性是對外公開的,一些類的屬性是需要保密的,因此,需要在類的表示法中定義屬性和行爲的公開級別。類似文件系統中文件的限制.
C++中類的封裝:
成員變量:
c++中用於表示類屬性的變量
成員函數:
c++中用於表示類行爲的函數
在c++中可以給成員變量和成員函數定義訪問級別:
public:
成員變量和成員函數可以在類的內部和外部訪問和調用.
private:
成員變量和成員函數只能在類的內部進行訪問和調用

class Cube
{
public:
    int getA()
    {
        return m_a;
    }
    int getB()
    {
        return m_b;
    }
    int getC()
    {
        return m_c;
    }
    void setABC(int a=0, int b = 0,int c=0)
    {
        m_a = a;
        m_b = b;
        m_c = c;
    }
    void setA(int a)
    {
        m_a = a;
    }
    void setB(int b)
    {
        m_b = b;
    }
    void setC(int c)
    {
        m_c = c;
    }
public:
    int getV()
    {
        m_v = m_a*m_b*m_c;
        return m_v;
    }
    int getS()
    {
        m_s = 2*(m_a*m_b + m_b*m_c + m_a*m_c);
        return m_s;
    } 
protected:
private:
    int m_a;
    int m_b;
    int m_c;
    int m_v;
    int m_s;
};
int main()
{
    Cube c1;
    c1.setA(1);
    c1.setB(2);
    c1.setC(3);
    cout<<"s是:"<<c1.getS();
    Cube c2;
    c2.setABC(1, 2, 3); //默認參數
    cout<<"s是:"<<c2.getS();
    return 0;
}
class Point
{
public:
    void setP(int _x0, int _y0)
    {
        x0 = _x0;
        y0 = _y0;
    }
    int getX()
    {
        return x0;
    }
    int getY()
    {
        return y0;
    }
private:
    //定義圓心和圓的半徑
    int x0;
    int y0;
};
class  AdvCircle
{
public:
    void setCircle(int _x1, int _y1, int _r)
    {
        x1 = _x1;
        y1 = _y1;
        r = _r;
    }
    void judge(int x0, int y0)
    {
        int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
        if (a > 0)
        {
            cout<<"點在圓外";
        }
        else
        {
            cout<<"點在圓內";
        }
    }
    //類做函數參數的時候,類封裝了屬性和方法,在被調用函數裏面, 不但可以使用屬性,而且可以使用方法(成員函數);
    //這也是面向對象和麪向過程的一個重要區別。。。。
    void judge(Point &p)
    {
        int a = (x1-p.getX())*(x1-p.getX()) + (y1-p.getY())*(y1-p.getY()) - r*r;
        if (a > 0)
        {
            cout<<"點在圓外";
        }
        else
        {
            cout<<"點在圓內";
        }
    }
private:
    //定義圓心和圓的半徑
    int x1;
    int y1;
    int r;   
};
int main()
{
    Point myp;
    AdvCircle c1;
    myp.setP(1, 1);
    c1.setCircle(2, 2, 3);
    //c1.judge(myp);
    c1.judge(1, 1);  
    system("pause");
}
void main111()
{
    //定義點
    int x0 = 1;
    int y0 = 1;
    //定義圓心和圓的半徑
    int x1 = 2;
    int y1 = 2;
    int r = 3;
    int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
    if (a > 0)
    {
        cout<<"點在圓外";
    }
    else
    {
        cout<<"點在圓內";
    }
    system("pause");  
}

.hpp和.cpp

我們需要將我們的代碼進行.h和實現進行分離了:
MyPoint.hpp:

#pragma once
class MyPoint
{
public:
    void setP(int _x0, int _y0);
    int getX();
    int getY();
private:
    //定義圓心和圓的半徑
    int x0;
    int y0;
};

MyCircle.hpp:

#pragma once//表示這個頭文件只能加載一次,類似於C中頭文件一開始的`ifdef`
#include "MyPoint.hpp"
class MyCircle
{
public:
    void setCircle(int _x1, int _y1, int _r);
    void judge(int x0, int y0);
    //類做函數參數的時候,類封裝了屬性和方法,在被調用函數裏面, 不但可以使用屬性,而且可以使用方法(成員函數);
    //這也是面向對象和麪向過程的一個重要區別。。。。
    void judge(MyPoint &p); 
private:
    //定義圓心和圓的半徑
    int x1;
    int y1;
    int r;
};

MyPoint.cpp:

#include "iostream"
#include "MyPoint.hpp"
using namespace std;
void MyPoint::setP(int _x0, int _y0)
{
    x0 = _x0;
    y0 = _y0;
}
int MyPoint::getX()
{
    return x0;
}
int MyPoint::getY()
{
    return y0;
}

MyCircle.cpp:

#include "iostream"
#include "MyCircle.hpp"
using namespace std;
void MyCircle::setCircle(int _x1, int _y1, int _r)
{//運用域作用符號,這個括號就相當於在類的內部了
    x1 = _x1;
    y1 = _y1;
    r = _r;
}
void MyCircle::judge(int x0, int y0)
{
    int a = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) - r*r;
    if (a > 0)
    {
        cout<<"點在圓外";
    }
    else
    {
        cout<<"點在圓內";
    }
}
void MyCircle::judge(MyPoint &p)
{
    int a = (x1-p.getX())*(x1-p.getX()) + (y1-p.getY())*(y1-p.getY()) - r*r;
    if (a > 0)
    {
        cout<<"點在圓外";
    }
    else
    {
        cout<<"點在圓內";
    }
}

構造函數初探

在我們之前的代碼的類中我們都是顯示的對成員變量進行初始化操作, 這是不好的,因爲如果要初始化一個很多對象的時候,就過於浪費生命了. 所以C++的類中引入了構造方法,

class Test
{
public:
    //構造函數 無參構造函數 默認構造函數
    Test()
    {
        a = 10;
    }
    //帶參數的構造函數
    //調用方法3種
    Test(int mya)
    {
        a = mya;
    }
    //第三中初始化對象的方法
    //賦值構造函數 copy構造函數
    //copy構造函數的用法 4種應用場景,因爲涉及到拷貝構造函數,所以這次先不考慮這種
    Test(const Test & obj)
    {
        ;
    } 
protected:
private:
    int a;
};
int main()
{
    //1 ()
    Test t1(10); //c++默認調用有參構造函數 自動調用
    // =
    Test t2 = 11; //c++默認調用有參構造函數自動調用
    //手工調
    Test t3 = Test(12); //我們程序員手動調用構造函數
    return 0;
}

聯繫方式: [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章