C/C++知識點總結(一)

1.struct和class的區別

C++中的struct對C中的struct進行了擴充,它已經不再只是一個包含不同數據類型的數據結構了,它已經獲取了太多的功能。

struct能包含成員函數嗎? 能!

struct能繼承嗎? 能!!

struct能實現多態嗎? 能!!!

最本質的一個區別就是默認的訪問控制,體現在兩個方面:

1)默認的繼承訪問權限。struct是public的,class是private的

2)struct作爲數據結構的實現體,它默認的數據訪問控制是public的,而class作爲對象的實現體,它默認的成員變量訪問控制是private的

2.string 和 int的互相轉換

int轉換成string

(1)to_string函數

(2)藉助字符串流

int aa = 30;
stringstream ss;
ss<<aa; 
string s1 = ss.str();
cout<<s1<<endl; // 30

(3)使用snprintf函數

#include <stdio.h>
#include <stdlib.h>

int main()
{
     char str[10]={0};
     snprintf(str, sizeof(str), "0123456789012345678");
     printf("str=%s/n", str);
     return 0;
}
str=012345678

string轉換成int

(1)atoi函數

string str = "123";
int n = atoi(str.c_str());
cout<<n; //123

(2)stoi函數

3.可重入和不可重入函數

在 實時系統的設計中,經常會出現多個任務調用同一個函數的情況。如果這個函數不幸被設計成爲不可重入的函數的話,那麼不同任務調用這個函數時可能修改其他任 務調用這個函數的數據,從而導致不可預料的後果。那麼什麼是可重入函數呢?所謂可重入是指一個可以被多個任務調用的過程,任務在調用時不必擔心數據是否會 出錯。不可重入函數在實時系統設計中被視爲不安全函數。

滿足下列條件的函數多數是不可重入的:
(1)函數體內使用了靜態的數據結構;

(2)函數體內調用了malloc()或者free()函數;

(3)函數體內調用了標準I/O函數。


如何寫出可重入的函數?在函數體內不訪問那些全局變量,不使用靜態局部變量,堅持只使用缺省態(auto)局部變量,寫出的函數就將是可重入的。如果必須訪問全局變量,記住利用互斥信號量來保護全局變量。或者調用該函數前關中斷,調用後再開中斷。多任務環境中或者實時系統設計中,應該儘可能的使用可重入函數,例如下面的函數:

int count_apple(int *package,int n)
{
    int temp = 0;
    int i;
    if(package == NULL)
       exit(1);
    for(i = 0;i < n; i++)
     temp += *(package++);
    return temp;
}

該函數功能是計算不同籃子裏的蘋果數,函數體內沒有訪問全局變量,不使用靜態局部變量,只使用局部變量,所以這個函數具有可重入的

如果必須使用全局變量,那麼爲了保證函數的安全,必須利用互斥信號量或者中斷機制來保護全局變量。例如下面函數:

int *package;
int count_apple(int n)
{
   int temp = 0;
   int i;
   P操作(申請信號量);
   if(package == NULL)
   {
      V操作(釋放信號量);
       exit(1);
   }
   for(i = 0; i < n; i++)
      temp += *(package++);
   V操作(釋放信號量);
   return temp;
}

象上面的PV操作機制就可以讓可重入函數安全的使用全局變量了,而且保證了可並行性。

不可重入函數,例如:

static int sum = 0;
int cout_pear(int *package,int n)
{ 
   int i;
   for(i = 0; i < n; i++)
      sum += *(package ++); //(1)
   return sum;
}

這個函數由於使用了靜態全局變量,對sum的並行性操作結果是未知的,是不安全的操做。若此函數被多個進程調用的話,結果是未知的。因爲,但語句(1)執行完一次或者幾次後,另外使用這個sum的函數可能正好被調度,並得到運行機會,那麼這個新運行的函數將使sum變成了另外的值,所以當(1)重新獲得運行機會時,sum的值已經變成了另外的值,這是不可預料的結果。

4.using namespace std的作用

using namespace std這條語句是爲了告訴編譯程序使用std命名空間。

C++標準中引入命名空間的概念,是爲了解決不同模塊或者函數庫中相同標識符衝突的問題。有了命名空間的概念。標識符就被限制在特定的範圍(函數)內,不會引起命名衝突。

這條using語句通知編譯程序,程序需要使用std命名空間,C++標準程序庫中的所有標識符都被定義於一個名爲std的命名空間中。

舉一程序例

我們可以看出,第二條語句聲明瞭我們可以使用標準程序庫的標識符。

如果沒有第二條語句,我們就無法正常的使用cout,只能寫成 std::cout<<"Hello world"。

也就是說,每一次運用標識符;都要在前面加上std;  具體格式  std::格式符。

所以,爲了方便,我們在開始聲明瞭using namespace std

5.strlen和sizeof的區別

strlen()是函數,要在運行時才能計算。參數必須是字符型指針(char*)。當數組名作爲參數傳入時,實際上數組就退化成指針了。

它的功能是:返回字符串的長度。該字符串可能是自己定義的,也可能是內存中隨機的,該函數實際完成的功能是從代表該字符串的第一個地址開始遍歷,直到遇到結束符NULL。返回的長度大小不包括NULL。

sizeof(...)是運算符,在頭文件中typedef爲unsigned int,其值在編譯時即計算好了,參數可以是數組、指針、類型、對象、函數等。

它的功能是:獲得保證能容納實現所建立的最大對象的字節大小。
       由於在編譯時計算,因此sizeof不能用來返回動態分配的內存空間的大小。實際上,用sizeof來返回類型以及靜態分配的對象、結構或數組所佔的空間,返回值跟對象、結構、數組所存儲的內容沒有關係。
      具體而言,當參數分別如下時,sizeof返回的值表示的含義如下:
    數組——編譯時分配的數組空間大小;
    指針——存儲該指針所用的空間大小(存儲該指針的地址的長度,是長整型,應該爲4);
    類型——該類型所佔的空間大小;
    對象——對象的實際佔用空間大小;
    函數——函數的返回類型所佔的空間大小。函數的返回類型不能是void。

6.引用的好處

1.傳遞引用給函數與傳遞指針的效果是一樣的。這時,被調函數的形參就成爲原來主調函數中的實參變量或對象的一個別名來使用,所以在被調函數中對形參變量的操作就是對其相應的目標 對象(在主調函數中)的操作。
2.使用引用傳遞函數的參數,在內存中並沒有產生實參的副本,它是直接對實參操作;而使用一般變量傳遞函數的參數,當發生函數調用時,需要給形參分配存儲單元,形參變量是實參變量的 副本;如果傳遞的是對象,還將調用拷貝構造函數。因此,當參數傳遞的數據較大時,用引用比 用一般變量傳遞參數的效率和所佔空間都好。
3.使用指針作爲函數的參數雖然也能達到與使用引用的效果,但是,在被調函數中同樣要給形參分配存儲單元,且需要重複使用”*指針變量名”的形式進行運算,這很容易產生錯誤且程序的閱 讀性較差;另一方面,在主調函數的調用點處,必須用變量的地址作爲實參。而引用更容易使用,更清晰。

7.如何判斷系統是大端還是小端

大端:數據的高位字節存放在高地址內,數據的低位字節存放在低地址內。

小端:數據的高位字節存放在低地址內,數據的高位字節存放在高地址內。

方法一:將字符數據賦給整型數據,通過讀取整型數據的值來判別大端還是小端

#include<iostream>
using namespace std;
int main()
{
	unsigned int data = 0;
	unsigned int *point = &data;
	*(char*)point = 0x22;
	if(data == 0x22)
		cout << "這是一個小端機" << endl;
	else if(data == 0x22000000)
		cout << "這是一個大端機" << endl;
	else
		cout << "無法判定該機器類型" << endl;
	return 0;
}

方法二:通過聯合體的共享內存特性,來判斷大端機、小端機

union是一個聯合體,所有變量公用一塊內存,在內存中的存儲是按最長的那個變量所需要的位數來開闢內存的。

#include<iostream>
using namespace std;
 
union UN{
	char ch;
	int data;
};
 
int main()
{
	union UN un;
	un.data = 0x1a2b3c4d;
	if(un.ch == 0x4d)
		cout << "這是一個小端機" << endl;
	else if(un.ch == 0x1a)
		cout << "這是一個大端機" << endl;
	else
		cout << "無法判定該機器" << endl;
	return 0;
}

方法三:通過指針來判斷

將一個整型數據賦給字符型數據,通過查看字符型數據的值來判定是大端機還是小端機。將整型賦給字符型,會發生數據的丟失。如果是大端機,則會丟失低字節;如果是小端機,則會丟失高字節。和第一種方法很類似,一個是查看整型的值,一個是查看字符型的值。

int main()
{
	int data = 1;
	char* p = (char*)&data;
	if(*p == 1)
		cout << "這是一個小端機" << endl;
	else if(*p == 0)
		cout << "這是一個大端機" << endl;
 
	return 0;
}

8.宏定義中do{......}while(0)

加了do{......}while(0),就使得宏展開後仍然保留初始的語義,從而保證程序的正確性。

9.C與C++混編中出現extern “C”塊的應用

#ifdef __cplusplus
    extern "C"{
#endif
    ...
#ifdef __cplusplus
}
#endif

__cplusplus是C++的預定義宏,表示當前開發環境是C++。在C/C++混合編程的環境下,extern “C”塊的作用就是告訴C++編譯器這段代碼按C標準編譯,以儘可能保持C++與C的兼容性。

10.struct和class的區別

struct的成員函數訪問默認權限是public, 默認繼承方式是public

class的成員函數訪問權限默認是private,默認繼承方式是private

11.靜態成員函數

當調用一個對象的成員函數(非靜態成員函數)時,系統會把該對象的起始地址賦給成員函數的this指針。而靜態成員函數並不屬於某一對象,它與任何對象都無關,因此靜態成員函數沒有this指針。既然它沒有指向某一對象,也就無法對一個對象中的非靜態成員進行默認訪問(即在引用數據成員時不指定對象名)。
可以說,靜態成員函數與非靜態成員函數的根本區別是:非靜態成員函數有this指針,而靜態成員函數沒有this指針。由此決定了靜態成員函數不能訪問本類中的非靜態成員。
靜態成員函數可以直接引用本類中的靜態數據成員,因爲靜態成員同樣是屬於類的,可以直接引用。在C++程序中,靜態成員函數主要用來訪問靜態數據成員,而不訪問非靜態成員。

12.對象的存儲空間

空類型對象的存儲空間爲1,當聲明該類型的對象的時候,它必須在內存中佔有一定的空間,否則無法使用這些對象。至於佔用多少內存由編譯器決定,C++中每個空類型的實例佔1字節的空間。

每個對象所佔用的存儲空間只是該對象的非靜態數據成員的總和,其他都不佔用存儲空間,包括成員函數和靜態數據成員。函數代碼是存儲在對象空間之外的,而且,函數代碼段是公用的,即如果對同一個類定義了10個對象,這些對象的成員函數對應的是同一個函數代碼段,而不是10個不同的函數代碼段。

13.this指針的作用

在每一個成員函數中都包含一個特殊的指針,這個指針的名字是固定的,稱爲this指針。它是指向本類對象的指針,它的值是當前被調用的成員函數所在的對象的起始地址。

this指針是隱式使用的,它是作爲參數被傳遞給成員函數。本來,成員函數volume的定義如下:
int Box::volume(){
    return (height*width*length);
}

C++把它處理爲:
int Box::volume(Box *this){
    return (this->height * this->width * this->length);
}

this指針有以下特點。
(1)只能在成員函數中使用,在全局函數、靜態成員函數中都不能使用this。
(2)this指針是在成員函數的開始前構造,並在成員函數的結束後清除。
(3)this指針會因編譯器不同而有不同的存儲位置,可能是棧、寄存器或全局變量。
(4)this是類的指針。
(5)因爲this指針只有在成員函數中才有定義,所以獲得一個對象後,不能通過對象使用this指針,所以也就無法知道一個對象的this指針的位置。不過,可以在成員函數中指定this指針的位置。
(6)普通的類函數(不論是非靜態成員函數,還是靜態成員函數)都不會創建一個函數表來保存函數指針,只有虛函數纔會被放到函數表中。

14.類模板

操作兩個數的類模板。

template<class T>          // 聲明一個模板,虛擬類型名爲T
class Operation {
public:
    Operation (T a, T b):x(a),y(b){}
    T add(){
        return x+y;
    }
    T subtract(){
        return x-y;
    }
private:
    T x,y;
};

用類模板實現對兩個數的加、減操作。

#include <iostream>
using namespace std;
template<class T>                              // 聲明一個模板,虛擬類型名爲T
class Operation {
public:
    Operation (T a, T b):x(a),y(b){}
    T add(){
        return x+y;
    }
    T subtract(){
        return x-y;
    }
private:
    T x,y;
};
int main(){
    Operation <int> op_int(1,2);
    cout<<op_int.add()<<" "<<op_int.subtract()<<endl;     // 輸出3、-1
    Operation <double> op_double(1.2,2.3);
    cout<<op_double.add()<<" "<<op_double.subtract()<<endl;     // 輸出3.5、-1.1
    return 0;
}

15.構造函數和析構函數的調用順序

(1)在全局範圍中定義的對象(即在所有函數之外定義的對象),它的構造函數在文件中的所有函數(包括main函數)執行之前調用。但如果一個程序中有多個文件,而不同的文件中都定義了全局對象,則這些對象的構造函數的執行順序是不確定的。當main函數執行完畢或調用exit函數時(此時程序終止),調用析構函數。
(2)如果定義的是局部自動對象(如在函數中定義對象),則在建立對象時調用其構造函數。如果函數被多次調用,則在每次建立對象時都要調用構造函數。在函數調用結束、對象釋放時先調用析構函數。
(3)如果在函數中定義靜態(static)局部對象,則只在程序第一次調用此函數建立對象時調用構造函數一次,在調用結束時對象並不釋放,因此也不調用析構函數,只在main函數結束或調用exit函數結束程序時,才調用析構函數。

16.派生類的構造函數

(1)對基類成員和子對象成員的初始化必須在成員初始化列表中進行,新增成員的初始化既可以在成員初始化列表中進行,也可以在構造函數體中進行。
(2)派生類構造函數必須對這3類成員進行初始化,其執行順序是這樣的:①先調用基類構造函數;②再調用子對象的構造函數;③最後調用派生類的構造函數體。
(3)當派生類有多個基類時,處於同一層次的各個基類的構造函數的調用順序取決於定義派生類時聲明的順序(自左向右),而與在派生類構造函數的成員初始化列表中給出的順序無關。
(4)如果派生類的基類也是一個派生類,則每個派生類只需負責其直接基類的構造,依次上溯。
(5)當派生類中有多個子對象時,各個子對象構造函數的調用順序也取決於在派生類中定義的順序(自前至後),而與在派生類構造函數的成員初始化列表中給出的順序無關。
(6)派生類構造函數提供了將參數傳遞給基類構造函數的途徑,以保證在基類進行初始化時能夠獲得必要的數據。因此,如果基類的構造函數定義了一個或多個參數時,派生類必須定義構造函數。
(7)如果基類中定義了默認構造函數或根本沒有定義任何一個構造函數(此時由編譯器自動生成默認構造函數)時,在派生類構造函數的定義中可以省略對基類構造函數的調用,即省略"<基類名>(<參數表>)"這個語句。
(8)子對象的情況與基類相同。
(9)當所有的基類和子對象的構造函數都可以省略時,可以省略派生類構造函數的成員初始化列表。
(10)如果所有的基類和子對象構造函數都不需要參數,派生類也不需要參數時,派生類構造函數可以不定義。

總的來說,構造函數的調用順序是:①如果存在基類,那麼先調用基類的構造函數,如果基類的構造函數中仍然存在基類,那麼程序會繼續進行向上查找,直到找到它最早的基類進行初始化;②如果所調用的類中定義的時候存在對象被聲明,那麼在基類的構造函數調用完成以後,再調用對象的構造函數;③調用派生類的構造函數。

17.析構函數

析構函數在下面3種情況時被調用。
1)對象生命週期結束被銷燬時(一般類成員的指針變量與引用都不自動調用析構函數)。
2)delete指向對象的指針時,或delete指向對象的基類類型指針,而其基類虛構函數是虛函數時。
3)對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用

18.多態

在面向對象方法中一般是這樣表述多態性的:向不同的對象發送同一個消息,不同的對象在接收時會產生不同的行爲(即方法);也就是說,每個對象可以用自己的方式去響應共同的消息。

當把基類的某個成員函數聲明爲虛函數後,就允許在其派生類中對該函數重新定義,賦予它新的功能,並且可以通過指向基類的指針指向同一類族中不同類的對象,從而調用其中的同名函數。虛函數實現了同一類族中不同類的對象可以對同一函數調用作出不同的響應的動態多態性。

使用虛函數,系統要有一定的空間開銷。當一個類帶有虛函數時,編譯系統會爲該類構造一個虛函數表,它是一個指針數組,用於存放每個虛函數的入口地址。系統在進行動態關聯時的時間開銷是很少的,因此,多態是高效的。

虛函數是多態的基礎,在C++中沒有虛函數就無法實現多態特性;因爲不聲明爲虛函數就不能實現“動態聯編”,就不能實現多態。

19.純虛函數

純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0”,如下所示:

virtual void funtion()=0;

將函數定義爲純虛函數,則編譯器要求在派生類中必須予以重載以實現多態性。同時含有純虛函數的類稱爲抽象類,它不能生成對象。如果一個類中含有純虛函數,那麼任何試圖對該類進行實例化的語句都是錯誤的,因爲抽象基類是不能被直接調用的,而必須被子類繼承重載以後,再根據要求調用其子類的方法,且在子類中一定要實現純虛函數的定義,不然編譯時會出錯。

一點需要注意,純虛函數不能實例化,但可以聲明指針。

20.構造函數不能是虛函數

在C++中,構造函數不能聲明時爲虛函數,這是因爲編譯器在構造對象時,必須知道確切類型,才能正確地生成對象;其次,在構造函數執行之前,對象並不存在,無法使用指向此對象的指針來調用構造函數。

這就要涉及到C++對象的構造問題了,C++對象在三個地方構建:(1)函數堆棧;(2)自由存儲區,或稱之爲堆;(3)靜態存儲區。無論在那裏構建,其過程都是兩步:首先,分配一塊內存;其次,調用構造函數。好,問題來了,如果構造函數是虛函數,那麼就需要通過vtable 來調用,但此時面對一塊 raw memeory,到哪裏去找 vtable 呢?畢竟,vtable 是在構造函數中才初始化的啊,而不是在其之前。因此構造函數不能爲虛函數。

21.析構函數可以聲明爲虛函數

析構函數可以聲明爲虛函數;C++明確指出,當derived class對象經由一個base class指針被刪除、而該base class帶着一個non-virtual析構函數,會導致對象的derived成分沒被銷燬掉。

析構函數不是虛函數容易引發內存泄漏。

#include<iostream>
using namespace std;
class Base{
public:
    Base(){ std::cout<<"Base::Base()"<<std::endl; }
    ~Base(){ std::cout<<"Base::~Base()"<<std::endl; }
};
class Derive:public Base{
public:
    Derive(){ std::cout<<"Derive::Derive()"<<std::endl; }
    ~Derive(){ std::cout<<"Derive::~Derive()"<<std::endl; }
};
int main(){
    Base* pBase = new Derive(); 
    /*這種base classed的設計目的是爲了用來“通過base class接口處理derived class對象”*/
    delete pBase;
    return 0;
}

程序的執行結果是:
Base::Base()
Derive::Derive()
Base::~Base()

22.單例模式

單例模式使用舉例。

#include<iostream>
using namespace std;
class CSingleton{
private:
    CSingleton(){                         // 構造函數是私有的
    }
    static CSingleton *m_pInstance;
public:
    static CSingleton * GetInstance(){
        if(m_pInstance == NULL)               // 判斷是否第一次調用
            m_pInstance = new CSingleton();
        return m_pInstance;
    }
};
CSingleton * CSingleton::m_pInstance=NULL;     // 初始化靜態數據成員
int main(){
    CSingleton *s1= CSingleton::GetInstance();
    CSingleton *s2= CSingleton::GetInstance();
    if(s1==s2){
        cout<<"s1=s2"<<endl;               // 程序的執行結果是輸出了s1=s2
    }
    return 0;
}
程序的執行結果是:
s1=s2

用戶訪問實例的唯一方法只有GetInstance()成員函數。如果不通過這個函數,任何創建實例的嘗試都將失敗,因爲類的構造函數是私有的GetInstance()的返回值是當這個函數首次被訪問時被創建的,所有GetInstance()之後的調用都返回相同實例的指針
單例類CSingleton有以下特徵:①有一個指向唯一實例的靜態指針m_pInstance,並且是私有的;②有一個公有的函數,可以獲取這個唯一的實例,並且在需要的時候創建該實例;③其構造函數是私有的,這樣就不能從別處創建該類的實例。

23.虛函數表分析

#include <iostream>

using namespace std;

class Base {
public:
    virtual void f() {cout<<"base::f"<<endl;}
    virtual void g() {cout<<"base::g"<<endl;}
    virtual void h() {cout<<"base::h"<<endl;}
};

class Derive : public Base{
public:
    void g() {cout<<"derive::g"<<endl;}
};

//可以稍後再看
int main () {
    cout<<"size of Base: "<<sizeof(Base)<<endl;

    typedef void(*Func)(void);
    Base b;
    Base *d = new Derive();

    long* pvptr = (long*)d;
    long* vptr = (long*)*pvptr;
    Func f = (Func)vptr[0];
    Func g = (Func)vptr[1];
    Func h = (Func)vptr[2];

    f();
    g();
    h();

    return 0;
}

運行下上面的代碼發現sizeof(Base) = 8, 說明編譯器在類中自動添加了一個8字節的成員變量, 這個變量就是_vptr, 指向虛函數表的指針。_vptr有些文章裏說gcc是把它放在對象內存的末尾,VC是放在開始, 我編譯是用的g++,驗證了下是放在開始的:

驗證代碼:取對象a的地址與a第一個成員變量n的地址比較,如果不等,說明對象地址開始放的是_vptr. 也可以用gdb直接print a 會發現_vptr在開始

class A
{
public:
      int n;
      virtual void Foo(void){}
};

int main()
{
     A a;
     char *p1 = reinterpret_cast<char*>(&a);
     char *p2 = reinterpret_cast<char*>(&a.n);
     if(p1 == p2)
     {
        cout<<"vPtr is in the end of class instance!"<<endl;
     }else
     {
        cout<<"vPtr is in the head of class instance!"<<endl;
     }
     return 1;
}

  包含虛函數的類纔會有虛函數表, 同屬於一個類的對象共享虛函數表, 但是有各自的_vptr.
  虛函數表實質是一個指針數組,裏面存的是虛函數的函數指針。

Base中虛函數表結構:

Derive中虛函數表結構:

運行上面代碼結果:
    size of Base: 8
    base::f
    derive::g
    base::h

說明Derive的虛函數表結構跟上面分析的是一致的:
(1)d對象的首地址就是vptr指針的地址pvptr,
(2)取pvptr的值就是vptr虛函數表的地址
(3)取vptr中[0][1][2]的值就是這三個函數的地址
(4)通過函數地址就直接可以運行三個虛函數了。
(5)函數表中Base::g()函數指針被Derive中的Derive::g()函數指針覆蓋, 所以執行的時候是調用的Derive::g()

 

 

 

 

 

 

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