1. 程式人生 > 其它 >面向物件過程中static的應用

面向物件過程中static的應用

技術標籤:C++ 模板超程式設計指標c++static靜態屬性

你真的懂得“C++中的static”?

面向物件過程中的static

⑴ static和非static屬性的類的成員變數

⒈ static與非static成員變數的異同點:

① 記憶體分配:

對於非靜態資料成員,他們都是類的例項化物件的成員,而靜態資料成員被當作是類型別的成員。無論這個類的物件定義了多少個,靜態資料成員在程式中也只有一份拷貝,由該型別的所有物件共享訪問,即靜態資料成員是該類的所有物件所共有的。

② 用法不同:

非靜態成員變數更像一個為每個特定的類物件定製的“私有飾品”,只可自己把玩不許他人觸碰。靜態型別的成員變數更像是一個為類型別準備的“公共汽車”,只要是該類的物件就可以操作汽車。因此,靜態成員變數可以被任意一個本類的類物件改動。

③ 誕生時間不同:

對於靜態成員變數來說,類型別一旦被定義,它們就會存在於“靜態儲存區”之中,相當於元老級人物。而非靜態成員變數則不同,他們需要人為的定義類物件之後才可以被分配記憶體。

記憶體分割槽的知識點:記憶體四區

④ 都遵從類的管理屬性

無論是static還是非static變數都遵從private,protected,public這三種訪問規則。

⒉ 由於不同的性質,static和非static成員變數所具備的功能的異同

① 所屬物件不同導致定義位置不同(定義=初始化!=宣告)

classA
{
private:
staticinta;//靜態成員變數需類內宣告
public:
intb;//非靜態成員變數宣告
public:
A():b(0){};//非靜態成員變數的初始化只能由建構函式來完成
};
intA::a=0;//靜態成員變數類外初始化

注意:無論static型別的成員變數是什麼屬性,private也好protected也罷,都必須使用類外初始化的方式進行初始化。private,protected和public三種屬性用於限制資料的訪問許可權的與初始化方式沒有半毛錢關係。

② 記憶體分配時機不同導致static成員變數可以初始化類物件的成員變數,也可以當作類中成員函式的預設引數

在預設建構函式中初始化非靜態成員變數:

classA
{
private:
staticinta;//靜態成員變數需類內宣告
public:
intb;//非靜態成員變數宣告
public:
A():b(a){};//使用靜態成員變數初始化非靜態成員變數
};
intA::a=0;//靜態成員變數類外初始化

在普通成員函式中作為預設引數:

classA
{
private:
staticinta;
public:
intb;
public:
voidInit(intobj=a)//靜態成員變數可以作為成員函式的預設引數
{
b=a;
}
};
intA::a=0;

注意:在靜態成員函式中是不可以使用非靜態成員變數也不可以使用靜態成員變數作為預設引數。

③ 分配時機不同導致static成員變數可以是“不完整資料型別”,但是static與非static型別都可以是“不完整資料型別的指標型別“。

針對於非static成員變數:

classA
{
private:
A*obj;//非static變數可以是“不完整資料指標型別”
public:
A():obj(nullptr){};
};

我們有點熟悉上面的結構:乍一看“這不就類似於資料結構中連結串列的組成單元嗎“!

針對於static成員變數:

classA
{
private:
staticA*obj;//static變數也可以是“不完整資料指標型別”
};
A*A::obj=nullptr;//必須類外初始化

這個必要麻煩的地方就在於,我們必須類外初始化static屬性的不完整型別指標obj。

除此之外,我們還可以不使用指標定義不完整型別物件class A型別的物件obj:

classA
{
private:
staticAobj;//static變數也可以是“不完整資料指標型別”
};

上述所示,static屬性使得不完整資料型別class A可以直接定義類物件,而不需要通過class A型別的指標去定義一個變數。但說實在的:使用指標定義還是最高效的!

④ 由於記憶體區域不同,導致沒有this指標指向任何一個static成員變數

#include<iostream>
usingnamespacestd;

classA
{
private:
staticinta;
public:
intb;
public:
voidInit(intobj=a)//靜態成員變數可以作為成員函式的預設引數
{
b=a;
}
};
intA::a=0;

intmain()
{
Aobj;
obj.Init();
cout<<A::b<<endl;//我們需要使用類型別去訪問改類的公有靜態成員變數
}

注意:我們這裡是通過class A型別來訪問的public屬性的靜態成員變數,但是我們也可以使用物件名來訪問公有靜態成員變數,但是這樣不直觀:

#include<iostream>
usingnamespacestd;

classA
{
private:
staticinta;
public:
intb;
public:
voidInit(intobj=a)//靜態成員變數可以作為成員函式的預設引數
{
b=a;
}
};
intA::a=0;

intmain()
{
Aobj;
obj.Init();
cout<<obj.b<<endl;//我們需要使用物件去訪問改類的公有靜態成員變數
}

顯然,只看main函式中的程式碼我麼不知道class A型別中的成員變數b是普通成員變數還是static屬性的靜態成員變數。這樣做不直觀不提倡這樣做!

⑤ 對於所有物件的共有屬性我們最好將這些屬性設定為static型別的,因為這樣相同的資料在記憶體中只拷貝一份,這樣資料的拷貝就不會針對於任何一個類的例項化物件,這樣的話就節省了一些記憶體空間。

如果我們採用普通形式的記憶體分配:

classA
{
public:
inta;//物件特有屬性,因物件而異
intb;//物件的共有屬性而且每個物件都有權修改
};

這樣的話佔用記憶體如下:

#include<iostream>
usingnamespacestd;

classA
{
public:
inta;//物件特有屬性,因物件而異
intb;//物件的共有屬性而且每個物件都有權修改
};

intmain()
{
cout<<sizeof(A)<<endl;//佔用記憶體為8位元組
}

但是如果我們將所有類物件共有且都可以修改的部分設定為static型別,佔用記憶體就會改善:

#include<iostream>
usingnamespacestd;

classA
{
public:
inta;//物件特有屬性,因物件而異
staticintb;//物件的共有屬性而且每個物件都有權修改
};
intA::b=0;

intmain()
{
cout<<sizeof(A)<<endl;//每個物件佔用記憶體為4位元組
}

看上述結果對比我們可以清晰地得知:對於每個物件共有的且與每個物件都會影響的這個屬性,設定為static型別是最好的,這樣做節省了記憶體開銷提高了記憶體利用效率。

⑵ static和非static屬性的成員函式

⒈ static和非static屬性的成員函式的異同點

① 操作的成員資料不同

static屬性的成員函式不需要物件就可以單獨使用,因此可以訪問static成員變數,但是不是可以使用普通的非static成員變數。非static屬性的成員函式可以使用static和非static的成員變數。

② 儲存地址不同並且訪問成員函式地址的方式也不同

首先我們要明確:非static和static型別的成員函式都是用於處理成員資料的工具,這些工具是類的物件共有的,也就是說static和非static成員函式都是類所屬的。只不過是static型別的成員函式只針對於static型別的成員變數,而非static型別的成員函式可以操作“static型別/非static型別的成員變數“。

1. 訪問成員函式地址的錯誤做法一:

A. 錯誤原因:使用類物件訪問成員函式的地址

B. 錯誤程式碼示例:

#include<iostream>
usingnamespacestd;

classA
{
public:
voidtest01();
staticvoidtest02();
};
voidA::test01()
{
cout<<"呼叫test01"<<endl;
};

voidA::test02(){
cout<<"呼叫test02"<<endl;
};

intmain()
{
Aobj;
cout<<&obj.test01<<endl;//錯誤
cout<<"靜態成員函式的地址:"<<A::test02<<endl;
}

C. 這樣做為什麼錯呢?錯在哪裡呢?

我們前面說過“無論是動態成員函式還是靜態成員函式都是類所有的“,就好比”類的每一個物件相當於工廠中的工人,工廠的工具是這些工人共用的,而非工人為了工作每個人給自己配個工廠,那效率太低了!“。因此我們的物件只有使用成員函式的權利並沒有訪問”成員函式的地址“的權力。

D. 改正方法:

將使用物件訪問函式地址&obj.test01改為使用類型別來訪問成員函式地址&A::test01即可。

2. 訪問成員函式地址的錯誤做法二:

A. 錯誤原因:使用cout輸出&A::test01訪問地址的結果,cout沒有輸出成員函式指標的過載版本,但是有輸出普通函式指標的過載版本。

B. 錯誤程式碼示例:

#include<iostream>
usingnamespacestd;

classA
{
public:
voidtest01();
staticvoidtest02();
};
voidA::test01()
{
cout<<"呼叫test01"<<endl;
};

voidA::test02(){
cout<<"呼叫test02"<<endl;
};

typedefvoid(A::*f)();

voidShowInf()
{
cout<<"呼叫ShowInf函式"<<endl;
}

intmain()
{
Aobj;
ffunc1=&A::test01;
cout<<&ShowInf<<endl;//正常輸出普通函式地址
cout<<&A::test01<<endl;//錯誤
cout<<"靜態成員函式的地址:"<<A::test02<<endl;
}

C. 錯誤原因結果解析:

我們從輸出結果看到,輸出的成員函式地址居然是bool型別的,這顯然不對,地址應該是16進位制的整數而非bool型別的常量。而cout輸出流物件對於普通的函式地址則可以正常的輸出。我們換用printf函式進行輸出,printf可以輸出任何資料型別。

printf輸出地址格式如下:

intPrintVal=9;
/*按整型輸出,預設右對齊*/
printf("%d\n",PrintVal);
/*按整型輸出,補齊4位的寬度,補齊位為空格,預設右對齊*/
printf("%4d\n",PrintVal);
/*按整形輸出,補齊4位的寬度,補齊位為0,預設右對齊*/
printf("%04d\n",PrintVal);

/*按16進位制輸出,預設右對齊*/
printf("%x\n",PrintVal);
/*按16進位制輸出,補齊4位的寬度,補齊位為空格,預設右對齊*/
printf("%4x\n",PrintVal);
/*按照16進位制輸出,補齊4位的寬度,補齊位為0,預設右對齊*/
printf("%04x\n",PrintVal);

/*按8進位制輸出,預設右對齊*/
printf("%o\n",PrintVal);
/*按8進位制輸出,補齊4位的寬度,補齊位為空格,預設右對齊*/
printf("%4o\n",PrintVal);
/*按照8進位制輸出,補齊4位的寬度,補齊位為0,預設右對齊*/
printf("%04o\n",PrintVal);

D. 錯誤糾正:

正確程式碼:

#include<iostream>
usingnamespacestd;

classA
{
public:
voidtest01();
staticvoidtest02();
};
voidA::test01()
{
cout<<"呼叫test01"<<endl;
};

voidA::test02(){
cout<<"呼叫test02"<<endl;
};

typedefvoid(A::*f)();

intmain()
{
Aobj;
ffunc1=&A::test01;
//%X跟%x是輸出十六進位制數字,%X是輸出的十以上的字母大寫,%x是輸出十以上的字母小寫。
printf("非static成員函式地址:%x\n",func1);
printf("非static成員函式地址:%X\n",func1);
cout<<"靜態成員函式的地址:"<<A::test02<<endl;
}

正確輸出結果:

我們看到輸出的函式的真實地址不依賴於類型別的例項化後的物件,而僅僅依賴於類型別本身。正確輸出成員函式地址的做法如下:

a. 我們要正確訪問類型別的成員函式地址,首先必須要選對操作目標,操作目標不是類物件而是整個類型別;

b. 其次我們必須要選擇輸出成員函式地址的正確做法,使用printf這個C風格的萬能輸出函式而非cout輸出流物件。

3. 訪問成員函式的錯誤做法正確做法:

程式碼示例:

#include<iostream>
usingnamespacestd;

classA
{
public:
voidtest01();
staticvoidtest02();
};
voidA::test01()
{
cout<<"呼叫test01"<<endl;
};

voidA::test02(){
cout<<"呼叫test02"<<endl;
};

typedefvoid(A::*f)();

intmain()
{
Aobj;
ffunc1=&A::test01;
//%X跟%x是輸出十六進位制數字,%X是輸出的十以上的字母大寫,%x是輸出十以上的字母小寫。
printf("非static成員函式地址:%x\n",func1);
printf("非static成員函式地址:%X\n",func1);
cout<<"靜態成員函式的地址:"<<A::test02<<endl;
}

輸出結果:

可能你每次執行相同的程式碼輸出結果都不相同,這是因為每次執行編譯器都會分配不同的記憶體空間供你使用。

4. 為什麼輸出成員函式指標與輸出靜態成員變數的指標方法不同?

靜態函式是獨立於物件的,是類擁有的,所以我們呼叫靜態函式,既可以通過類呼叫(如Ctest::statFunc())也可以通過物件呼叫(如Ctest Object; Object.statFunc())。無論是通過類呼叫還是物件呼叫,對應的都是同一個函式。

但是,對於動態函式,只能通過物件來呼叫。因為我們在動態成員函式中,往往都需要訪問物件的成員變,我們知道同一型別的不同物件,它們擁有類中成員變數的不同副本,所以假如動態成員函式由類來呼叫,我們怎麼知道在函式中訪問的是哪一個物件的成員變數。另外要說一下,我們說動態函式的呼叫必須通過物件來呼叫,是不是說動態成員函式是跟物件繫結的,是不是不同的物件所呼叫的成員函式就是不同的呢(注意,我們說成員函式不同,是指函式程式碼儲存的地址不同,不是說函式的行為不同),所以我們要輸出動態函式的地址,必須通過物件來獲取呢,如Ctest Object; &Object.statFunc,我們在編譯器上編譯一下,可以發現程式設計出錯,原因是物件只能用來呼叫函式的,並不能用來獲取函式的地址。因為我們要獲取函式地址,還是得通過類來獲取的,動態函式同樣是跟類繫結而不是跟物件繫結的。C++呼叫非靜態的成員函式時,採用的是一種__thiscall的函式呼叫方式。採用這種呼叫方式,編譯器在編譯的時候,會在呼叫的函式形參表中增加一個指向呼叫該成員函式的指標,也就是我們經常說的this指標。呼叫的形式類似於Ctest::dynFunc(Ctest* this, otherparam...),在函式體中,涉及到物件的成員變數或者其他成員函式,都會通過這個this指標來呼叫,從而達到在成員函式中處理呼叫物件所對應的資料,而不會錯誤處理其他物件的資料。可見,雖然我們必須通過物件來呼叫動態函式,但是其實我們訪問的都是同一個成員函式。所以我們上面採用&Ctest::dynFunc類名來獲取成員函式地址是沒錯的,動態函式同樣是跟類繫結而不是跟物件繫結的。

借鑑出處:非靜態成員函式與靜態成員函式的區別