程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++那些細節

C++那些細節

編輯:關於C++

一.簡介

C或者C++中最靈活的東東就是指針了,在操作一個對象,或者數組等等,我們常常用到指針,可以給編程帶來很多靈活性。但是,指針不僅僅能指向固定數據類型或是對象,指針還可以指向函數,這就是所謂的函數指針。 有了函數指針,我們可以通過指針調用函數,更重要的是我們可以將函數指針作為參數傳遞給函數,進而可以進行完成注冊,回調等等功能,可以說,有了函數指針,我們的程序可以設計的更加靈活了。

二.普通函數指針

1.函數指針的定義方式

看一個最簡單的例子:
// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//定義一個函數指針
void (*pFunc)(int);

//聲明並定義一個函數
void PrintInt(int i)
{
	cout<結果:
1

請按任意鍵繼續. . .



聲明函數指針的時候,一般的格式為 返回值 (*函數指針名)(參數1,參數2...)但是這樣聲明有一個弊端,就是麻煩,每次需要一個函數指針的時候,都需要寫這麼一大串,很麻煩,所以我們就想到了用typedef這個關鍵字,定義某一種類型的函數指針,給它一個別名,這樣,每次使用這種函數指針的時候就可以像定義一個int型變量那麼簡單。



比如上面的函數指針,我們就可以這樣寫:
// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//使用typedef定義函數指針,此時,pFunc就不再是函數指針本身的名稱,而是這類函數指針的一個別名
typedef void (*pFunc)(int);

//聲明並定義一個函數
void PrintInt(int i)
{
	cout<結果:
1

請按任意鍵繼續. . .



這樣寫之後,我們就定義了一種函數指針的類型,返回值為void,參數為int的函數指針,之後使用pFunc就可以很方便的定義這種類型的函數指針啦!

三.成員函數指針

成員函數指針是C++中最麻煩的東東之一,准確的說是非靜態成員函數指針。

1.靜態成員函數指針

靜態成員函數指針與普通函數指針一樣,我們看一個例子:

 

// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//Base類
class Base
{
public:
	static void Print(int num)
	{
		cout<

 

結果:

1 請按任意鍵繼續. . .

靜態成員函數與普通的函數沒有太多區別,雖然他們定義在類中,但是這個成員函數不會因為對象的不同而做出不同的處理,因為它沒有this指針,所以我們可以將它看成普通的函數。

 

2.非靜態成員函數

 

但是非靜態成員函指針就麻煩得多,原因非靜態成員函數有一個隱藏的參數--this指針,這個東東在不同的對象中是不一樣的,所以很麻煩。我們定義非靜態成員函數指針的時候就需要將對象的類型也寫出來。調用的時候,也要根據相應的對象來調用這個函數。看一個例子:

 

// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//Base類
class Base
{
public:
	void Print(int num)
	{
		cout<結果:

 

1 請按任意鍵繼續. . .

 

分析一下成員函數指針的定義和使用,我們這樣定義函數指針,(Base::*pFunc)(int),其實就相當於(*pFunc)(Base*, int),相當於普通的函數指針需要多一個this指針作為參數,而這個this指針在不同的對象中一定是不同的,所以成員函數指針之間是不能互相轉化的,只有同類型的對象的函數才能賦給這種對象的函數指針。

在指針賦值的時候,注意一下寫法,普通的函數指針在賦值的時候,可以不寫&符號,但是成員函數指針賦值的時候比較嚴格,如果不寫的話會報出這樣的錯誤:

error C3867: “Base::Print”: 函數調用缺少參數列表;請使用“&Base::Print”創建指向成員的指針

而且在賦值的時候,不要用對象賦值,要用 類名::函數名 這種方式賦值,使用 對象名.函數名的時候會報出這樣的錯誤:

error C2276: “&”: 綁定成員函數表達式上的非法操作

可見,對於成員函數指針的寫法還是挺嚴苛的。

最後再分析一下使用,由於成員函數指針需要一個this指針作為參數,這個參數又不能直接給出,所以我們就只能通過對象來調用函數,在使用函數的時候,由於函數是一個整體,所以需要用(),在括號內部,我們通過*func獲得函數,然後前面使用base.就將base作為this指針傳遞給了函數。

 

那麼,既然這個函數是非靜態成員函數,那麼這個函數支不支持動態綁定呢?換句話說就是,我們聲明的函數指針是基類的函數指針,子類覆寫了這個函數,那麼,用子類對象調用的時候,是調用基類的函數還是子類的函數呢?我們看一個例子:

 

// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//Base類
class Base
{
public:
	//虛函數
	virtual void Print(int num)
	{
		cout<結果:

 

base: 1 child: 1 sizeof Member Function: 4 請按任意鍵繼續. . .

 

從上面的結果,我們看出,在最後使用函數指針的時候,這個this指針的對象可以是我們聲明函數指針的時候的對象的子類,並且如果我們覆寫了基類的函數,是可以調用子類的函數的(注意是覆寫基類的virtual函數,如果只是單純的覆蓋是沒有多態效果的)。

我們分析一下原因,還是這樣看待這個函數指針,把它看成普通函數指針增加了一個類對象的this指針作為參數,這個形參我們可以聲明為基類的指針,我們給實參的時候,可以給基類的對象,當然也可以給子類對象,這就跟我們普通函數傳遞參數的情況一樣,然後這個參數傳遞進去,如果有virtual函數,那麼就可以觸發多態。但是我們在給函數指針賦值的時候,卻只能使用基類的函數,因為我們沒有聲明子類的函數。

 

四.函數指針的作用

1.函數指針作為函數的參數

有了函數指針,我們就可以像傳遞普通指針那樣將一個函數作為參數傳遞給另一個函數,這大大的增加了我們編程的靈活性。比如處理一個事務,我們可以隨意更換處理的handler,當然,使用面向對象思想,通過多態也可以實現相應的功能。我們看一個例子:
// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//定義pcall這種函數指針的類型
typedef int (*pcall)(int, int);

//處理函數,接受一個pcall類型的函數作為參數
void Func(pcall p, int x, int y)
{
	cout<結果:
begin func:

result is : 3

end func

begin func:

result is : 1

end func

請按任意鍵繼續. . .

 

2.函數回調

能將函數作為參數進行傳遞並不是我們最終的目的,函數指針最有用的功能之一就是回調。關於函數普通調用和回調,我們用買東西做個比喻,普通調用就好比我們直接去買東西,到了商店就買到了東西;而函數回調就好比我們去蛋糕店預定一個蛋糕,這時蛋糕店肯定會留下你的聯系方式,當蛋糕做好了,蛋糕店就會給你打電話,讓你去取蛋糕。從這裡,我們看到,函數回調也是需要有一些前提步驟的,首先我們要將回調的函數注冊給調用方,這就好比我們去蛋糕店留下了聯系方式,而當某種條件滿足了的時候,就調用回調函數,這個條件在我們的例子中就是蛋糕做好了。最後,蛋糕店給你打電話讓你去取蛋糕,這個過程就是調用我們注冊好的函數了。 回調函數有什麼好處呢?還是拿買蛋糕的例子來說,如果不用回調函數,那麼,我們如果去蛋糕店買蛋糕,買完之後,我們並不知道蛋糕什麼時候做好,那麼要麼我們就得一直呆在蛋糕店等著,要麼我們就得隔一會兒給蛋糕店打一個電話,問一下蛋糕有沒有做好。看一個沒有用回調函數的例子:
// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

class Baker
{
private:
	int m_iTime;			  //做蛋糕的時間
	static const int m_iMaxTime = 10;//假設10分鐘做完
public:
	//做蛋糕,如果做好了返回true,否則返回false
	bool MakeCake();
	//構造函數
	Baker();
};

Baker::Baker() : m_iTime(0){}

bool Baker::MakeCake()
{
	//假設每次調用該函數,m_iTime+1
	m_iTime += 1;
	if (m_iTime == m_iMaxTime)
	{
		cout<<蛋糕做好了!<結果:
蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕沒做好

蛋糕做好了!

我來取蛋糕啦!

請按任意鍵繼續. . 



這裡,我們如果想要第一時間知道蛋糕有沒有做好,就必須一致查詢蛋糕做沒做好,換句話說,我們不能去干別的事情去了,要一直在那裡等蛋糕。在程序中來看,我們的客戶不停的進行查詢,如果這是一個線程的話,那麼這個線程在這段時間就不能繼續做其他事了。.

而如果我們用回調函數,這個過程就變成了這樣:
// C++Test.cpp : 定義控制台應用程序的入口點。
//

#include stdafx.h
#include 
#include 
#include 
using namespace std;

//聲明一種回調函數
typedef void (*CallBackFunc)(void);

class Baker
{
private:
	int m_iTime;			  //做蛋糕的時間
	static const int m_iMaxTime = 10;//假設10分鐘做完
	CallBackFunc m_pfCallBack;//回調函數
public:
	//注冊:留下買蛋糕的人的聯系方式
	void Invoke(CallBackFunc);
	//打電話通知買蛋糕的人
	void Notify();
	//做蛋糕,如果做好了,直接通知客戶
	void MakeCake();
	//構造函數
	Baker();
};

Baker::Baker() : m_iTime(0){}

void Baker::MakeCake()
{
	while(m_iTime < m_iMaxTime)
	{
		//假設每次調用該函數,m_iTime+1
		m_iTime += 1;
	}
	cout<<蛋糕做好了!<結果:
留下了您的聯系方式!

好了叫我就好,我去玩兒啦!

蛋糕做好了!

我來取蛋糕啦!

請按任意鍵繼續. . .





通過函數回調,雖然實現過程比較麻煩,先得注冊,然後再類對象內部進行判斷。但是這樣做是有好處的,就是客戶在注冊之後,完全不需要再管蛋糕做沒做好了,只要蛋糕做好了,蛋糕店會立刻通知客戶。比如我們做異步加載時,如果我們需要資源,就可以主線程去加載線程注冊一下,然後主線程繼續做其他工作,加載線程加載完所需要的東西之後,反過來通過回調函數通知主線程,這樣,加載和主要功能就可以同時進行,大大的提升了用戶體驗。



其實函數指針回調在C#中的實現就是代理,C#將回調這件事做得特別簡單,可惜C++中沒有代理,所以我們做起來還是比較麻煩的,不過回調真的灰常有用。

五.其他問題

函數指針和指針函數

函數指針和指針函數僅僅是名字比較像,寫法容易搞混,其他並沒有什麼聯系,二者可以共存。 函數指針我們說過了,就是指向函數的指針,其本質是一個指針。 指針函數,指的是函數的返回值是一個指針,其本質是一個函數。 兩者容易搞混的地方就在於寫法:
//定義一個函數指針
typedef int (*pFunc)(void);
//定義一個指針函數
int* PointerFunc(void)
{
	int a = 1;
	return &a;
}
函數指針*和名字是放在一起的,而指針函數*是和返回值放在一起的。
當然,我們也可以定義一個指針函數的函數指針:
//定義一個指針函數的函數指針
typedef int* (*pFUN)(void);




 

 

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved