面經2

const

作用
修飾變量說明是不可修改的
修飾指針-指向常量的指針和指針常量
常量引用,常作爲形參,可以避免被函數修改

//在類中

class  A{
    private:
        const int a;        //常量成員,只能在類初始化的時候被賦值
    public:
        A():a(0){};
        A(int x):a(x){};
        
        int getValue();     //普通的成員函數
        int getValue() const;       //常成員函數,不能修改類中的任何數據成員的值
      
}

void function(){
    A b;
    const A a;  //只能調用上面的常成員函數
    const A *p=&a   //常指針
    const A &q=a;   //常引用
    
    // 指針
    char greeting[]="hello";
    char *p1=greeting;      //指針變量,指向字符數組的而變量
    const char *p2=greeting;        //  指針變量,指向字符數組常量的指針;表示地址是可修改的但是地址上的數值是不可改的。
    char * const p3=greeting;       // 常指針,表示指針是一個常量,始終指向一個地址不可修改
    const char * const p4=greeting; //這個就是都不可以修改
}

// 函數
void function1(const int Var);           // 傳遞過來的參數在函數內不可變
void function2(const char* Var);         // 參數指針所指內容爲常量
void function3(char* const Var);         // 參數指針爲常指針
void function4(const int& Var);          // 引用參數在函數內爲常量,就是引用可以改,但是引用內的數值不可以改


// 函數返回值
const int function5();      // 返回一個常數
const int* function6();     // 返回一個指向常量的指針變量,使用:const int *p = function6();
int* const function7();     // 返回一個指向變量的常指針,使用:int* const p = function7();

指針常量和常量指針

* (指針)和 const(常量) 誰在前先讀誰 ;*象徵着地址,const象徵着內容;誰在前面誰就不允許改變。

好吧,讓我們來看這個例子:

int a =3;
int b = 1;
int c = 2;
const int *p1 = &b//上下兩個是一樣的
int const *p1 = &b;//const 在前,定義爲常量指針
int *const p2 = &c;//*在前,定義爲指針常量 

常量指針p1:指向的地址可以變,但內容不可以重新賦值,內容的改變只能通過修改地址指向後變換。   
    p1 = &a是正確的(改地址),但 p1 = a是錯誤的(改地址內數值)。
指針常量p2:指向的地址不可以重新賦值,但內容可以改變,必須初始化,地址跟隨一生。
    p2= &a是錯誤的,而
p2 = a 是正確的。

static

常常在類中使用,目的是創建一個屬於類的變量、函數… 而不是服務於某一個對象。同時又力求不破壞類的封裝性,即要求此成員隱藏在類的內部,對外不可見。

  • 機制
    靜態數據成員要在程序一開始運行時就必須存在。因爲函數在程序運行中被調用,所以靜態數據成員不能在任何函數內分配空間和初始化。
    靜態數據成員要實際地分配空間,故不能在類的聲明中定義(只能聲明數據成員)。類聲明只聲明一個類的“尺寸和規格”,並不進行實際的內存分配,所以在類聲明中寫成定義是錯誤的。它也不能在頭文件中類聲明的外部定義,因爲那會造成在多個使用該類的源文件中,對其重複定義。

1.可以修飾普通變量。當修飾普通變量的時候就是在內存中的全局數據區中創建這個變量,而且會賦予初值0。不會隨着棧區的函數被釋放。也就意味着只會在第一次的時候被初始化。
如果放在函數外面,就是保證不被其他文件的同名變量衝突,只在文件內使用。
如果放在函數內部,雖然數據存放域在全局域但是實際使用的時候只能在函數內部使用

2.可以修飾普通函數效果其實和普通變量相似。
3.可以修飾成員變量。修飾成員變量使所有的對象只保存一個該變量,而且不需要生成對象就可以訪問該成員。但是還是需要初始化這個成員變量
<數據類型><類名>::<靜態數據成員名>=<值>
4.可以修飾成員函數。訪問這個成員函數就可以不生成對象,還需要注意的一點就是靜態能訪問靜態,靜態函數不能訪問非靜態成員和函數,非靜態函數都可以訪問。

using namespace std;
class A{
private:
    static int a;
public:
    static void getA();
};
int A::a=1;
void A::getA(){
	cout<<a<<endl;
}
int main(){
	A::getA();
	return 0;
}

5.如果靜態對象出現在類中,即使該對象沒有被使用,他也會被構造和析構;而如果靜態對象出現在函數中,該函數沒被調用那麼這個對象也不會產生,但是如果產生了就只產生一次。

#include<iostream>
class A
{
public:
    A(){
        std::cout<<"constructor" <<std::endl;
    }
    ~A(){
        std::cout<<"destructor"<<std::endl;
    }
};

A& ret_singleton(){
    static A instance;
    return instance;
}

int main(int argc, char *argv[])
{
    A& instance_1 = ret_singleton();
    A& instance_2 = ret_singleton();
    return 0;
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2Hg6oEC9-1585662345125)(en-resource://database/848:1)]

this指針

  1. this是一個隱含在非靜態成員函數中的一個特殊指針,它指向了調用它的函數。如
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IS3D6wbe-1585662345126)(en-resource://database/831:1)]
    圖中的Box1.compare調用時,compare成員函數中的this就是指向了Box1這個對象。

2.實際上當一個成員函數被一個對象調用的時候,這個成員函數都會攜帶一個隱形的參數 類::函數名稱(&對象,…) ,然後把這個對象的地址賦值給this指針,而且在成員函數內部每次調用成員變量實際上都是調用this.成員變量即隱式使用了this。如
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5LHcGAfS-1585662345126)(en-resource://database/832:1)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MwirQ9l5-1585662345126)(en-resource://database/833:1)]

3.this 並不是一個常規變量,而是個右值,所以不能取得 this 的地址(不能 &this)。 右值就好像 數字 1,2,3 字符串’aaa’ 是不能得到它的地址的。

在以下場景中,經常需要顯式引用 this 指針:爲實現對象的鏈式引用;爲避免對同一對象進行賦值操作;在實現一些數據結構時,如 list ????????????? 需要了解

inline內聯函數

內聯函數的提出是爲了解決宏會出現的一些問題。
宏常見的問題是 因爲宏是直接替代對於的字符串,因此會出現以下錯誤
#define Add(x,y) x+y
cout<<Add(1,2)*3<<endl;
這時候就是直接替代Add(1,2)爲1+2不會帶上括號。
還有類內的成員函數是沒法用宏來替代

內聯函數的特徵:

  • 相當於直接把內聯函數裏面的內容寫在調用處

    inline int add(int x,int y){return x+y;}
    int a=add(1,2) //就會被編譯器替換成 int a= 1+2
  • 類聲明中的函數除了虛函數實際上都隱式帶有inline。
  • 內聯函數具有函數的特性,如類型檢查。因爲是替代的形式,因此內聯函數都比較簡單

優點是,減少了函數執行的過程,參數壓棧,棧的開闢和回收、返回結果加快了執行的速度。
缺點是,代碼膨脹
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hc7BQ52z-1585662345127)(en-resource://database/834:1)]

編譯器在檢查後也會把內聯函數放入符號表,如果傳入參數和返回參數都符合就會直接替代函數調用。

問題1:虛函數可以是內聯函數嗎

  • 虛函數可以是內聯函數,只要虛函數不呈現多態性的時候
  • 內聯是編譯器在編譯的時候建議內聯,而虛函數多態是在運行的時候,因此編譯器無法知道內聯的是哪個運行代碼,因此虛函數表現多態的時候是不可以內聯的
  • 只有在編譯器知道調用內聯函數的對象是哪一個的時候才行。
#include <iostream>  
using namespace std;
class Base
{
public:
	inline virtual void who()
	{
		cout << "I am Base\n";
	}
	virtual ~Base() {}
};
class Derived : public Base
{
public:
	inline void who()  // 不寫inline時隱式內聯
	{
		cout << "I am Derived\n";
	}
};

int main()
{
	// 此處的虛函數 who(),是通過類(Base)的具體對象(b)來調用的,編譯期間就能確定了,所以它可以是內聯的,但最終是否內聯取決於編譯器。 
	Base b;
	b.who();

	// 此處的虛函數是通過指針調用的,呈現多態性,需要在運行時期間才能確定,所以不能爲內聯。  
	Base *ptr = new Derived();
	ptr->who();

	// 因爲Base有虛析構函數(virtual ~Base() {}),所以 delete 時,會先調用派生類(Derived)析構函數,再調用基類(Base)析構函數,防止內存泄漏。
	delete ptr;
	ptr = nullptr;

	system("pause");
	return 0;
} 

volatile

volatile的意思是易變的,不穩定的。它解決的問題就是程序中易變變量的讀取問題。
前提:在編譯器編譯代碼的時候,如果一個變量前後沒有被改變,那麼編譯器會優化讀取和存儲而把變量放到臨時寄存器中,同時後面每一次訪問這個變量都會從臨時寄存器中取值。如果這時候一個外部程序線程或者一箇中斷程序想要修改這個數值,程序就會出現異常即這個數值一直保持不變。

volatile的作用就是處理上面這種問題,volatile修飾的變量,編譯器就不會對其進行優化,會將它的數值寫入內存中,每次讀取的時候都去內存中讀取,這樣當它被外部程序修改後也能夠讀取出修改後的數值。

static int i =0;
int main(){
    while(1){
        if (i) do_somthing();
    }
}

//中斷程序或者多線程
void interrupt(){
    i=1;
}

上面的程序就是沒有用volatile修飾的變量,當程序在循環while內部的代碼的時候就會把i=0這個副本寫到寄存器中,因此無論後面如何修改i的數值都是0。帶上volatile就可以使編譯器不會對變量i進行優化,會把i的數值寫到內存中去,外部程序的修改也會在內存中體現。

  • 一個參數既可以是const又可以是volatile,比如只讀的狀態寄存器,const表示的是編譯程序內部是不可以改變這個數值的,但是外部程序如果直接修改地址下的內容是可以的。
    如硬件時鐘一般設定爲不能被程序改變(const),但是當它被程序以外的代理改變時候就需要它是volatile的。
  • 可以修飾指針
  • 對於一些程序使用volatile需要注意
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-07EvpxSP-1585662345127)(en-resource://database/835:1)]

assert

斷言,是宏不是函數。c++在頭文件cassert中,可以通過NDEBUG來關閉,但是必須要include之前

#define NDEBUG
#include <cassert.h>

assert(p!=NULL);    //此時的assert就不可用了

sizeof

對數組的話就是計算整個數組所佔空間大小,
對指針就是指針本身佔空間的大小

#pragma pack(n)

設定結構體、聯合以及類成員變量以 n 字節方式對齊
#pragma pack(n) 使用

#pragma pack(push) 
#pragma pack(4)//此時就按照四個字節來對齊,按照結構體順序 mod4超的就換行

struct test{
    char m1;//1byte
    short m2;//2byte
    int m3;//4byte
};
#pragma pack(pop)

上面這個結構體佔了8個字節
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-O92DwhVg-1585662345127)(en-resource://database/836:1)]

如果把上面的換個位置就會得到

#pragma pack(push) 
#pragma pack(4)//此時就按照四個字節來對齊,按照結構體順序 mod4超的就換行

struct test{
    short m2;//2byte
    int m3;//4byte
    char m1;//1byte
};
#pragma pack(pop)

上面這個結構體就要佔12個字節,這就是對齊產生的效果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oCd30iM8-1585662345128)(en-resource://database/837:1)]
這裏浪費的空間會變多,但是在時間效率上就提高很多。
爲什麼會提高效率呢? 因爲機器在讀取的時候要按照週期讀取,比如如果設置成1byte對齊,那麼對於int類型來說就需要4個週期才能讀完,讀完之後還要拼接在一起,這樣時間上就慢了。
類中的虛函數會佔一個字節

位域

Bit mode: 2;

類可以將其(非靜態)數據成員定義爲位域(bit-field),在一個位域中含有一定數量的二進制位。當一個程序需要向其他程序或硬件設備傳遞二進制數據時,通常會用到位域。
位域在內存中的佈局是與機器有關的
位域的類型必須是整型或枚舉類型,帶符號類型中的位域的行爲將因具體實現而定
取地址運算符(&)不能作用於位域,任何指針都無法指向類的位域

extern “C”

被其修飾的變量和函數就會按照C語言的方式來編譯和鏈接


#ifdef __cplusplus
extern "C" {
#endif 

void *memset(void *,int ,size_t);

#ifdef __cplusplus
}
#endif

struct 和typedef struct

首先要理解typedef是什麼意思,typedef int abc; 就是定義一個類型abc,abc的效果和int一模一樣。

typedef struct Student{
    int a;
}Stu;       //  此時的Stu就是一個函數類型,如果需要定義一個變量就要用 Stu stu1; 這個效果相當於 struct Student stu1;

struct Teacher{
    int a;
}tea;       //此時的tea就是一個變量,它的類型是 struct Teacher 。
//如果需要定義一個變量也可以使用
Teacher tea1;   //此處忽略了struct

但是需要注意的是如果存在同名函數


typedef struct Student { 
    int age; 
} S;

void Student() {}           // 正確,定義後 "Student" 只代表此函數

//void S() {}               // 錯誤,符號 "S" 已經被定義爲一個 "struct Student" 的別名

int main() {
    Student(); 
    struct Student me;      // 或者 "S me";
    return 0;
}

struct和class區別

主要區別還是在成員變量上,struct成員變量的訪問是public, class的默認訪問權限是private

union

這是一種特殊的類,類內可以有多個數據成員,但是任意時刻只有一個數據成員有數值,其他的會變成無定義狀態。

存在匿名形式,全局的匿名必須要用static修飾,用 ::變量 來訪問
局部的匿名可以直接訪問變量。


#include<iostream>

union UnionTest {
    UnionTest() : i(10) {};
    int i;
    double d;
};

static union {
    int i;
    double d;
};

int main() {
    UnionTest u;

    union {
        int i;
        double d;
    };

    std::cout << u.i << std::endl;  // 輸出 UnionTest 聯合的 10

    ::i = 20;
    std::cout << ::i << std::endl;  // 輸出全局靜態匿名聯合的 20

    i = 30;
    std::cout << i << std::endl;    // 輸出局部匿名聯合的 30

    return 0;
}

c完成c++中的類

先說下c++中的類的特性:封裝、繼承、多態
封裝:將數據和操作數據的函數綁定在一起,同時能設置訪問權限,比如類中的所有成員變量都是私有的,這就是封裝的意義。
繼承:繼承允許我們依據另一個類來定義一個類,這使得創建和維護一個應用程序變得更容易。這樣做,也達到了重用代碼功能和提高執行時間的效果。
多態:多態的方式有兩種一種是靜態鏈接
靜態多態最簡單的例子 靜態多態就是函數重載

int Add(int a ,int b);
int Add(double a,int b);
//當調用add函數是就會根據參數的類型來判斷用哪個函數;這個實現是在編譯的時候編譯器根據實參的類型來選擇對應的函數

還有一種就是動態多態,就是在程序運行的時候根據基類的指針(引用)的對象來決定到底用哪個類裏面的虛函數。
(這麼理解,我需要提前定義一個出門對象,根據時間來判斷到底乘坐什麼工具;如果沒有多態我就需要把每一個交通工具都定義過去,最後在根據時間判斷調用哪個交通工具函數;多態的效果就是我只需要定義一個出門對象,我根據時間來把創建一個交通工具對象賦給出門對象,最後就只要調用出門函數即可)

#include<iostream>
using namespace std;

class Goout{
    public :
        virtual void takevehicles(int x)=0;
};
class Bus : public Goout{
    public:
        virtual void takevehicles(int x){
            cout<<"take bus"<<endl;
        }
};
class Subway : public Goout{
    public:
        virtual void takevehicles(int x ){
            cout<<"take subway"<<endl;
        }
};

int main(){
    //定義基類
    Goout* go=NULL;
    int i=rand();
	if(i%2==1){
		go=new Bus;
		
	}
	else {
		go=new Subway;
		
	}
	cout<<i<<endl;
	go->takevehicles(1);
    delete go;
    return 0;
}

動態多態的使用條件
如果用了基類的指針指向了派生類的對象,用基類的指針刪除派生類的對象的時候就需要用到虛析構函數,要不然會產生內存泄漏

不能處理友元函數 全局函數 靜態函數 構造
● 基類中必須包含虛函數,並且派生類中一定要對基類中的虛函數進行重寫。不是虛函數的話派生類的函數會被基函數的覆蓋。
● 通過基類對象的指針或者引用調用虛函數。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4nbJxLpq-1585662345128)(en-resource://database/838:1)]

1.封裝

typedef struct __Parent{
    int a;
    int b;
    void (*print)(struct __Parent *This);//這裏
}Parent;

2.繼承

typedef struct __Child{
    Parent parent;
    int c;
}Child;

3.多態

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

typedef struct __Parent{
    int a;
    int b;
    void (*print)(struct __Parent *This);//這裏
}Parent;

typedef struct __Child{
    Parent parent;
    int c;
}Child;

void print_parent(Parent *This){
    printf("%d,%d",This->a,This->b);
}
void print_child(Parent *This){
    print("%d,%d,%d",This->parent.a,This->parent.b,This->c);
}

Parent *create_parent(int a,int b){
    Parent *This;
    This=NULL;
    This=(Parent *)malloc(sizeof(Parent));
    if(This!=NULL){
        This->a=a;
        This->b=b;
        This->print=print_parent;
        printf("Create parent successfully!\n");
    }
    return This;
}

void destroy_parent(Parent *p)  
{  
    if (*p != NULL)
    {  
        free(*p);  
        *p = NULL;  
        printf("Delete parent successfully!\n");  
    }  
} 

Child *create_child(int a,int b, int c){
    Child *This;
    This=NULL;
    This=(Child *)malloc(sizeof(Child));
    if(This!=NULL){
        This->parent.a=a;
        This->parent.b=b;
        This->c=c;
        This->print=print_child;
        
        printf("Create child successfully!\n");
    }
    return This;
}
void destroy_child(Child **p)  
{  
    if (*p != NULL)
    {  
        free(*p);  
        *p = NULL;  
        printf("Delete child successfully!\n");  
    }  
}  
  
int main()  
{  
    Child *p = create_child(1, 2, 3);  
    Parent *q;  
  
 
    q = (Parent *)p;  
    
    q->print(q);  
  
    destroy_child(&p); 
    system("pause");
    return 0;  
  
} 

虛函數的實現機制

c++的對象佈局可以通過多種方式來了解:
1.輸出成員變量的偏移,通過offsetof的宏來得到
2.編譯器的調試器中查看

虛函數表放在只讀數據段(不是函數也不是代碼所以不在代碼段,不用動態分配空間所以不再堆)

只有數據成員的對象
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zUS2i2FY-1585662345129)(en-resource://database/839:1)]

沒有虛函數的對象 和上面的內存分佈是一樣的

當存在一個虛函數對象時
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ytEXDM66-1585662345129)(en-resource://database/840:1)]
會發現再第一個偏移之前多了4個字節。
這4個字節就是__vfptr就是虛函數表vtable指針,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8DIkxSBM-1585662345130)(en-resource://database/841:1)]

當存在多個虛函數對象時

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mOPnj97I-1585662345130)(en-resource://database/842:1)]
發現類對象大小仍然時12個字節,虛函數表指針還是佔4個字節並沒有增加。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zt7JMhgx-1585662345131)(en-resource://database/843:1)]
從上圖中,我們可以得到的結論是:
1.__vfptr只是一個指針,它指向一個指針數組,即虛函數表
2.添加一個虛函數只是往虛函數表中添加一項,並不會影響到類對象的大小以及分佈


當創建兩個類對象時,類對象的地址是不同的,但是他們的虛函數表指針都會指向同一個虛函數表
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UlKFBFeq-1585662345131)(en-resource://database/844:1)]
1.這個虛函數表保存在哪裏不需要在意,我們需要知道虛函數表是在編譯的時候編譯器創建好的,只存一份。
2.定義對象時,編譯器自動將對象的虛函數指針__vfptr指向虛函數表。

當單繼承,但不重寫虛函數時

class Base1{
    public:
        int base1_1;
        int base1_2;
        
        virtual void base1_fun1(){}
        virtual void base1_fun2(){}
};

class Derive1: public Base1{
    public:
        int derive1_1;
        int derive1_2;
};

此時的內存佈局爲
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-a5VKoHpu-1585662345131)(en-resource://database/845:1)]

單繼承並且重寫虛函數時

class Base1{
    public:
        int base1_1;
        int base1_2;
        
        virtual void base1_fun1(){}
        virtual void base1_fun2(){}
};

class Derive1: public Base1{
    public:
        int derive1_1;
        int derive1_2;
        
        virtual void base1_fun1(){};
};

這時候就會在base1的虛表指針指向的指針數組上把0位置上改成Derive1::base1_fun1
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-J22IvLi6-1585662345132)(en-resource://database/846:1)]

在派生類中定義了新的虛函數

class Base1{
    public:
        int base1_1;
        int base1_2;
        
        virtual void base1_fun1(){};
        virtual void base1_fun2(){};
};
class Derive1:public Base1{
    public:
        int derive1_1;
        int derive1_2;
        
        virtual void derive1_fun1()[};  
};

這時候新的虛函數就會被添加在base1的虛函數表中
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fuLQZ1Qh-1585662345133)(en-resource://database/847:1)]

多繼承多虛函數的場景
1.基類有虛函數表的情況
多繼承的情況下會把有虛函數表的類放在地址空間的前面,然後把新添加的虛函數指針加到虛函數表中。
2.如果基類都沒有虛函數表
派生類就會自己創建一個__vfptr,同時也會放在地址空間的最前面,然後再虛函數表中添加虛函數指針。

:: 範圍解析運算符

1.全局作用域符號 ::name 表示作用域是全局命名空間
2.類作用域 class::name,一般作用域類中的靜態變量
3.命名空間作用域 namespace::name

enum

可以限定作用域

    enum class color{ red,blue,yellow};
	enum class flag_color{red=2, blue,yellow};

	color a=color::red;
	flag_color b=flag_color::red;
	std::cout<<int(a)<<int(b);
    //就會輸出02,如果沒有作用域那麼這個red就會出錯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章