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

一,用C++設計一個不能被繼承的類

首先想到的是在C++中,子類的構造函數會自動調用父類的構造函數。同樣,子類的析構函數也會自動調用父類的析構函數。要想一個類不能被繼承,只要把它的構造函數和析構函數都定義爲私有函數。那麼當一個類試圖從它那繼承的時候,必然會由於試圖調用構造函數、析構函數而導致編譯錯誤。

可是這個類的構造函數和析構函數都是私有函數了,怎樣才能得到該類的實例呢?可以通過定義靜態來創建和釋放類的實例。基於這個思路,可以寫出如下的代碼:

class FinalClass1
{
public :
      static FinalClass1* GetInstance()
      {
            return new FinalClass1;
      }
 
      static void DeleteInstance( FinalClass1* pInstance)
      {
            delete pInstance;
            pInstance = 0;
      }
 
private :
      FinalClass1() {}
      ~FinalClass1() {}
};

這個類是不能被繼承,但在總覺得它和一般的類有些不一樣,使用起來也有點不方便。比如,只能得到位於堆上的實例,而得不到位於棧上實例。能不能實現一個和一般類除了不能被繼承之外其他用法都一樣的類呢?

#include<iostream>
using namespace std;

template <typename T>
class Base
{
    friend T;
private:
    Base() {}
    ~Base() {}
};

class Finalclass : virtual public Base<Finalclass>
{
public:
    Finalclass() {}
    ~Finalclass() {}
};

class TestClass : public Finalclass
{
    //TestClass(){}繼承時報錯,無法通過編譯
};

int  main()
{
    Finalclass* p = new Finalclass;  // 堆上對象
    Finalclass fs;               // 棧上對象
//  TestClass tc;  // 基類構造函數私有,不可以被繼承。因此不可以創建棧上對象。

    system("pause");
    return 0;
}

Finalclass繼承於Base類,Base爲虛基類,因爲它是Base的友元,所以,它可以訪問基類的私有構造函數,以及析構函數。編譯運行時是正確的。也就是說,可以創建堆上的對象,並且可以構建棧上的對象。

爲什麼必須是虛繼承(virtual)?

通常每個類只初始化自己的直接基類,但是在虛繼承的時候這個情況發生了變化,可能導致虛基類被多次初始化,這顯然不是我們想要的。(例2: AA,AB都是類A的派生類,然後類C又繼承自AA和AB,如果按之前的方法會導致C裏面A被初始化兩次,也會存在兩份數據)爲了解決重複初始化的問題,從具有虛基類的類繼承的類在初始化時進行了特殊處理,在虛派生中,由最低層次的派生類的構造函數初始化虛基類。在我們上面的例1中就是由C的構造函數控制如何進行虛基類的初始化。

爲什麼Finalclass類不能被繼承?

因爲Finalclass是Base的友元,所以Finalclass對象可以正常創建,但由於Finalclass使用了虛繼承,所以如果要創建TestClass對象,那麼TestClass類的構造函數就要負責虛基類(Base)的構造,但是Base的構造函數是私有的,TestClass沒有訪問的權限(ps:友元關係不能被繼承的),所以TestClass類在編譯時就會報錯。這樣Finalclass類就不能被繼承了。

二,多重類構造和析構的順序

http://gaocegege.com/Blog/cpp/cppclass

在類被構造的時候,先執行虛擬繼承的父類的構造函數,然後從左到右執行普通繼承的父類的構造函數,然後按照定義的順序執行數據成員的初始化(與列表初始化順序無關),最後是自身的構造函數的調用。析構函數與之完全相反,互成鏡像。

三,類的sizeof大小

https://blog.csdn.net/fengxinlinux/article/details/72836199

首先要明確一個概念,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 我們這裏指的類的大小,其實指的是類的對象所佔的大小。因此,如果用sizeof運算符對一個類型名操作,得到的是具有該類型實體的大小。

  • 因爲一個空類也要實例化,所謂類的實例化就是在內存中分配一塊地址,每個實例在內存中都有獨一無二的地址。同樣空類也會被實例化,所以編譯器會給空類隱含的添加一個字節,這樣空類實例化之後就有了獨一無二的地址了。所以空類的sizeof爲1。
  • 類的大小爲類的非靜態成員數據的類型大小之和,也就是說靜態成員數據不作考慮。靜態數據成員之所以不計算在類的對象大小內,是因爲類的靜態數據成員被該類所有的對象所共享,並不屬於具體哪個對象,靜態數據成員定義在內存的全局區。
  • 普通成員函數與sizeof無關。
  • 虛函數由於要維護在虛函數表,所以要佔據一個指針大小,也就是4字節。
  • 類的總大小也遵守類似class字節對齊的,調整規則。

四,字節對齊問題

https://blog.csdn.net/hairetz/article/details/4084088

在沒有#pragma pack宏的情況下

  1. 數據成員對齊規則:結構內部各個成員的首地址必然是自身大小的整數倍
  2. 結構體的總大小,也就是sizeof的結果, 必須是其內部最大成員的整數倍,不足的要補齊.
typedef struct bb
{
    int id;             //[0]....[3]
    double weight;      //[8].....[15]      原則1
    float height;      //[16]..[19],總長要爲8的整數倍,補齊[20]...[23]     原則2
}BB;

typedef struct aa
{
    char name[2];     //[0],[1]
    int  id;         //[4]...[7]      原則1

    double score;     //[8]....[15]    原則1
    short grade;    //[16],[17]        
    BB b;             //[24]......[47]     原則1
}AA;

int main()
{
    AA a;
    cout<<sizeof(a)<<" "<<sizeof(BB)<<endl;
    return 0;
}

 
結果是:
48 24

在代碼前加一句#pragma pack(1),你會很高興的發現,上面的代碼輸出爲

32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;

這不是理想中的沒有內存對齊的世界嗎.沒錯,#pragma pack(1),告訴編譯器,所有的對齊都按照1的整數倍對齊,換句話說就是沒有對齊規則。

五,C++ 四種強制轉換類型函數

https://blog.csdn.net/ydar95/article/details/69822540

https://www.cnblogs.com/Allen-rg/p/6999360.html

https://blog.csdn.net/chenyiming_1990/article/details/10203489

  • 去const屬性用const_cast
  • 基本類型轉換用static_cast
  • 多態類之間的類型轉換用daynamic_cast
  • 不同類型的指針類型轉換用reinterpret_cast

1.const_cast(編譯器在編譯期處理)

  • 常量指針被轉化成非常量的指針,並且仍然指向原來的對象;
  • 常量引用被轉換成非常量的引用,並且仍然指向原來的對象;
  • const_cast一般用於修改指針。如const char *p形式。const_cast強制轉換對象必須爲指針或引用。
#include<iostream>
int main() {
    // 原始數組
    int ary[4] = { 1,2,3,4 };

    // 打印數據
    for (int i = 0; i < 4; i++)
        std::cout << ary[i] << "\t";
    std::cout << std::endl;

    // 常量化數組指針
    const int*c_ptr = ary;
    //c_ptr[1] = 233;   //error

    // 通過const_cast<Ty> 去常量
    int *ptr = const_cast<int*>(c_ptr);

    // 修改數據
    for (int i = 0; i < 4; i++)
        ptr[i] += 1;    //pass

    // 打印修改後的數據
    for (int i = 0; i < 4; i++)
        std::cout << ary[i] << "\t";
    std::cout << std::endl;

    return 0;
}

/*  out print
    1   2   3   4
    2   3   4   5
*/

2.static_cast(編譯器在編譯期處理)

  • static_cast 作用和C語言風格強制轉換的效果基本一樣,由於沒有運行時類型檢查來保證轉換的安全性,所以這類型的強制轉換和C語言風格的強制轉換都有安全隱患。在運行時轉換過程中,不進行類型檢查來確保轉換的安全性
  • 用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換。注意:進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。
  • 用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性需要開發者來維護。
  • static_cast不能轉換掉原有類型的const、volatile、或者 __unaligned屬性。(前兩種可以使用const_cast 來去除)
  • 在c++ primer 中說道:c++ 的任何的隱式轉換都是使用 static_cast 來實現。
/* 常規的使用方法 */
float f_pi=3.141592f
int   i_pi=static_cast<int>(f_pi); /// i_pi 的值爲 3

/* class 的上下行轉換 */
class Base{
    // something
};
class Sub:public Base{
    // something
}

//  上行 Sub -> Base
//編譯通過,安全
Sub sub;
Base *base_ptr = static_cast<Base*>(&sub);  

//  下行 Base -> Sub
//編譯通過,不安全
Base base;
Sub *sub_ptr = static_cast<Sub*>(&base);    

3.dynamic_cast(在運行期,會檢查這個轉換是否可能)

dynamic_cast強制轉換,應該是這四種中最特殊的一個,因爲他涉及到面向對象的多態性和程序運行時的狀態,也與編譯器的屬性設置有關

(1)其他三種都是編譯時完成的,dynamic_cast是運行時處理的,運行時要進行類型檢查。

(2)不能用於內置的基本數據類型的強制轉換。

(3)dynamic_cast轉換如果成功的話返回的是指向類的指針或引用,轉換失敗的話則會返回NULL。

(4)使用dynamic_cast進行轉換的,基類中一定要有虛函數,否則編譯不通過。

#include<iostream>
using namespace std;

class Base{
public:
    Base() {}
    ~Base() {}
    void print() {
        std::cout << "I'm Base" << endl;
    }

    virtual void i_am_virtual_foo() {}
};

class Sub: public Base{
public:
    Sub() {}
    ~Sub() {}
    void print() {
        std::cout << "I'm Sub" << endl;
    }

    virtual void i_am_virtual_foo() {}
};
int main() {
    cout << "Sub->Base" << endl;
    Sub * sub = new Sub();
    sub->print();
    Base* sub2base = dynamic_cast<Base*>(sub);
    if (sub2base != nullptr) {
        sub2base->print();
    }
    cout << "<sub->base> sub2base val is: " << sub2base << endl;


    cout << endl << "Base->Sub" << endl;
    Base *base = new Base();
    base->print();
    Sub  *base2sub = dynamic_cast<Sub*>(base);
    if (base2sub != nullptr) {
        base2sub->print();
    }
    cout <<"<base->sub> base2sub val is: "<< base2sub << endl;

    delete sub;
    delete base;
    return 0;
}
/* vs2017 輸出爲下
Sub->Base
I'm Sub
I'm Base
<sub->base> sub2base val is: 00B9E080   // 注:這個地址是系統分配的,每次不一定一樣

Base->Sub
I'm Base
<base->sub> base2sub val is: 00000000   // VS2017的C++編譯器,對此類錯誤的轉換賦值爲nullptr
*/

從上邊的代碼和輸出結果可以看出:
對於從子類到基類的指針轉換 ,dynamic_cast 成功轉換,沒有什麼運行異常,且達到預期結果
從基類到子類的轉換 , dynamic_cast 在轉換時也沒有報錯,但是輸出給 base2sub 是一個 nullptr ,說明dynami_cast 在程序運行時對類型轉換對“運行期類型信息”(Runtime type information,RTTI)進行了檢查.
這個檢查主要來自虛函數(virtual function) 在C++的面對對象思想中,虛函數起到了很關鍵的作用,當一個類中擁有至少一個虛函數,那麼編譯器就會構建出一個虛函數表(virtual method table)來指示這些函數的地址,假如繼承該類的子類定義並實現了一個同名並具有同樣函數簽名(function siguature)的方法重寫了基類中的方法,那麼虛函數表會將該函數指向新的地址。此時多態性就體現出來了:當我們將基類的指針或引用指向子類的對象的時候,調用方法時,就會順着虛函數表找到對應子類的方法而非基類的方法。因此注意下代碼中 Base 和 Sub 都有聲明定義的一個虛函數 ” i_am_virtual_foo” ,我這份代碼的 Base 和 Sub 使用 dynami_cast 轉換時檢查的運行期類型信息,可以說就是這個虛函數。

4.reinterpret_cast(編譯器在編譯期處理)

reinterpret_cast是強制類型轉換符用來處理無關類型轉換的。

改變指針或引用的類型、將指針或引用轉換爲一個足夠長度的整形、將整型轉換爲指針或引用類型。

轉換類型必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,在把該整數轉換成原類型的指針,還可以得到原先的指針值)。

#include<iostream>
#include<cstdint>
using namespace std;
int main() {
    int *ptr = new int(233);
    uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
    cout << "ptr 的地址: " << hex << ptr << endl
        << "ptr_addr 的值(hex): " << hex << ptr_addr << endl;
    delete ptr;
    return 0;
}
/*
ptr 的地址: 0061E6D8
ptr_addr 的值(hex): 0061e6d8
*/

六,指針和引用的區別(一般都會問到)

1. 相同點:

  • 都是地址的概念;
  • 指針指向一塊內存,它的內容是所指內存的地址;引用是某塊內存的別名。

2. 區別:

  • 指針是一個實體,而引用僅是個別名;
  • 引用使用時無需解引用(*),指針需要解引用;
  • 引用只能在定義時被初始化一次,之後不可變;指針可變;
  • 引用不能爲空,指針可以爲空;
  • “sizeof 引用”得到的是所指向的變量(對象)的大小,而“sizeof 指針”得到的是指針本身(所指向的變量或對象的地址)的大小;
  •  指針和引用的自增(++)運算意義不一樣;
  • 從內存分配上看:程序爲指針變量分配內存區域,而引用不需要分配內存區域。

七.C++中Overload、Overwrite及Override的區別

1.Overload(重載):

在C++程序中,可以將語義、功能相似的幾個函數用同一個名字表示,但參數或返回值不同(包括類型、順序不同),即函數重載。
(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。

2.Override(覆蓋):

是指派生類函數覆蓋基類函數,特徵是:
(1)不同的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字

3.Overwrite(重寫):

是指派生類的函數屏蔽了與其同名的基類函數,規則如下:
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。

#include <stdio.h>
#include <iostream>
using namespace std;

class Parent
{
public:
	void F()
	{
		printf("Parent.F()/n");
	}
	virtual void G()
	{
		printf("Parent.G()/n");
	}
	int Add(int x, int y)
	{
		return x + y;
	}
	//重載(overload)Add函數
	float Add(float x, float y)
	{
		return x + y;
	}
};

class ChildOne:Parent
{
	//重寫(overwrite)父類函數
	void F()
	{
		printf("ChildOne.F()/n"); 
	}
	//覆寫(override)父類虛函數,主要實現多態
	void G()
	{
		printf("ChildOne.G()/n");
	}
};


int main()
{
	ChildOne childOne;// = new ChildOne();
	Parent* p = (Parent*)&childOne;
	//調用Parent.F()
	p->F();
	//實現多態
	p->G();
	Parent* p2 = new Parent();
	//重載(overload)
	printf("%d/n",p2->Add(1, 2));
	printf("%f/n",p2->Add(3.4f, 4.5f));
	delete p2;
	system("PAUSE");
	return 0;
}

八,寫string類的構造,析構,拷貝函數和賦值函數

class String
{
public:
    String(const char *str=NULL); //構造函數
    String(const String &other); //拷貝構造函數
    ~String(void); //析構函數
    String& operator=(const String &other); //賦值函數
    ShowString();
	
private:
    char *m_data; //指針
};
	
String::~String()
{
    delete [] m_data; //析構函數,釋放地址空間
}
	
String::String(const char *str)
{
    if (str==NULL)//當初始化串不存在的時候,爲m_data申請一個空間存放'\0';
    {
        m_data=new char[1];
        *m_data='\0';
    }
    else//當初始化串存在的時候,爲m_data申請同樣大小的空間存放該串;
    {
        int length=strlen(str);
        m_data=new char[length+1];
        strcpy(m_data,str);
    }
}
	
String::String(const String &other)//拷貝構造函數,功能與構造函數類似。
{
    int length=strlen(other.m_data);
    m_data=new [length+1];
    strcpy(m_data,other.m_data);
}

String& String::operator =(const String &other) 
{
    if (this==&other)//當地址相同時,直接返回;
        return *this; 
    if(m_data != NULL)
        delete [] m_data;//當地址不相同時,刪除原來申請的空間,重新開始構造;
    int length=strlen(other.m_data);
    m_data=new [length+1];
    strcpy(m_data,other.m_data);
    return *this; 
}
	
String::ShowString()//由於m_data是私有成員,對象只能通過public成員函數來訪問;
{
    cout<<this->m_data<<endl;
}
	
main()
{
	String AD;
	char * p="ABCDE";
	String B(p);
	AD.ShowString();
	AD=B;
	AD.ShowString();
	
}

九,C++拷貝和賦值的區別

https://www.cnblogs.com/wangguchangqing/p/6141743.html

調用的是拷貝構造函數還是賦值運算符,主要是看是否有新的對象實例產生。如果產生了新的對象實例,那調用的就是拷貝構造函數;如果沒有,那就是對已有的對象賦值,調用的是賦值運算符。

調用拷貝構造函數主要有以下場景:

  • 對象作爲函數的參數,以值傳遞的方式傳給函數。 
  • 對象作爲函數的返回值,以值的方式從函數返回
  • 使用一個對象給另一個對象初始化
#include <iostream>

using namespace std;

class Person
{
public:
    Person(){}
    Person(const Person& p)
    {
        cout << "Copy Constructor" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Assign" << endl;
        return *this;
    }

private:
    int age;
    string name;
};

void f(Person p)
{
    return;
}

Person f1()
{
    Person p;
    return p;
}

int main()
{
    Person p;
    Person p1 = p;    // 1
    Person p2;
    p2 = p;           // 2
    f(p2);            // 3

    p2 = f1();        // 4

    Person p3 = f1(); // 5

    return 0;
}

分析如下:

  1. 這是雖然使用了"=",但是實際上使用對象p來創建一個新的對象p1。也就是產生了新的對象,所以調用的是拷貝構造函數。
  2. 首先聲明一個對象p2,然後使用賦值運算符"=",將p的值複製給p2,顯然是調用賦值運算符,爲一個已經存在的對象賦值 。
  3. 以值傳遞的方式將對象p2傳入函數f內,調用拷貝構造函數構建一個函數f可用的實參。
  4. 這條語句拷貝構造函數和賦值運算符都調用了。函數f1以值的方式返回一個Person對象,在返回時會調用拷貝構造函數創建一個臨時對象tmp作爲返回值;返回後調用賦值運算符將臨時對象tmp賦值給p2.
  5. 按照4的解釋,應該是首先調用拷貝構造函數創建臨時對象;然後再調用拷貝構造函數使用剛纔創建的臨時對象創建新的對象p3,也就是會調用兩次拷貝構造函數。不過,編譯器也沒有那麼傻,應該是直接調用拷貝構造函數使用返回值創建了對象p3。

深拷貝、淺拷貝

說到拷貝構造函數,就不得不提深拷貝和淺拷貝。通常,默認生成的拷貝構造函數和賦值運算符,只是簡單的進行值的複製。例如:上面的Person類,字段只有intstring兩種類型,這在拷貝或者賦值時進行值複製創建的出來的對象和源對象也是沒有任何關聯,對源對象的任何操作都不會影響到拷貝出來的對象。反之,假如Person有一個對象爲int *,這時在拷貝時還只是進行值複製,那麼創建出來的Person對象的int *的值就和源對象的int *指向的是同一個位置。任何一個對象對該值的修改都會影響到另一個對象,這種情況就是淺拷貝。

深拷貝和淺拷貝主要是針對類中的指針和動態分配的空間來說的,因爲對於指針只是簡單的值複製並不能分割開兩個對象的關聯,任何一個對象對該指針的操作都會影響到另一個對象。這時候就需要提供自定義的深拷貝的拷貝構造函數,消除這種影響。通常的原則是:

  • 含有指針類型的成員或者有動態分配內存的成員都應該提供自定義的拷貝構造函數
  • 在提供拷貝構造函數的同時,還應該考慮實現自定義的賦值運算符

對於拷貝構造函數的實現要確保以下幾點:

  • 對於值類型的成員進行值複製
  • 對於指針和動態分配的空間,在拷貝中應重新分配分配空間
  • 對於基類,要調用基類合適的拷貝方法,完成基類的拷貝

十,大端小端

  • Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
  • Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。

判斷大小端

  BOOL IsBigEndian()  
    {  
        int a = 0x1234;  
        char b =  *(char *)&a;  //通過將int強制類型轉換成char單字節,通過判斷起始存儲位置。即等於 取b等於a的低地址部分  
        if( b == 0x12)  
        {  
            return TRUE;  
        }  
        return FALSE;  
    }

聯合體union的存放順序是所有成員都從低地址開始存放,利用該特性可以輕鬆地獲得了CPU對內存採用Little-endian還是Big-endian模式讀寫:

 BOOL IsBigEndian()  
 {  
        union NUM  
        {  
            int a;  
            char b;  
        }num;  
        num.a = 0x1234;  
        if( num.b == 0x12 )  
        {  
            return TRUE;  
        }  
        return FALSE;  
 }

 一般操作系統都是小端,而通訊協議是大端的。

十一,守護進程

https://blog.csdn.net/dianacody/article/details/40016927

在後臺運行,不以終端方式與用戶交互;它從被執行開始運轉,直到整個系統關閉時才退出。

守護進程必須與其運行前的環境隔離開來:這些環境包括未關閉的文件描述符控制終端會話和進程組工作目錄以及文件創建掩模等。這些環境通常是守護進程從執行它的父進程(特別是shell)中繼承下來的。

創建一個簡單的守護進程:

  1. step1:創建子進程,父進程退出: (假象--父進程已完成,可退出終端)
  2. step2: 在子進程中創建新會話:使用系統函數setid()--進程組、會話期
  3. step3: 改變當前目錄爲根目錄,用fork創建的子進程繼承了父進程的當前工作目錄
  4. step4: 重設文件權限掩碼: umask(0)
  5. step5: 關閉文件描述符
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXFILE 65535
int main()
{
    pid_t pc;
    int i, fd, len;
    char *buf="this is a Dameon\n";
    len = strlen(buf);
    pc = fork(); //第一步
    if(pc<0){
        printf("error fork\n");
        exit(1);
    }
    else if(pc>0)
        exit(0);

    setsid(); //第二步
    chdir("/"); //第三步
  umask(0); //第四步
  for(i=0;i<MAXFILE;i++) //第五步
      close(i);

   if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
   {
      perror("open");
      exit(1);
  }   
  
   while(1)
   {
       write(fd,buf,len+1);
      sleep(10);
  }
}

 

 

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