C++面試八股文:C++中,設計一個類要注意哪些東西?

2023-06-09 06:01:32

某日二師兄參加XXX科技公司的C++工程師開發崗位第9面:

面試官:C++中,設計一個類要注意哪些東西?

二師兄:設計一個類主要考慮以下幾個方面:1.物件導向的封裝、繼承及多型。2.big three或者big five。3.運運算元和函數過載、靜態成員、友元、例外處理等相關問題。

面試官:請介紹一下物件導向的三個特性。

二師兄:封裝是將類的函數和資料封裝起來,外部不能直接存取類的資料,而是需要通過方法存取資料。繼承是指一個類可以繼承另一個類的屬性和方法。多型是指一個物件可以表現出多種形態。

面試官:請問多型是如何實現的?

二師兄:多型的是通過父類別的指標或參照指向子類的物件實現的。在物件中維護一個虛指標(vtptr),這個指標指向一個虛表(vtable),當用戶通過父類別物件存取子類的方法時,通過查詢虛表中對應的方法的地址,並跳轉到此地址執行間接存取物件的方法。所以多型是有一點點執行時開銷的。

面試官:你剛才所說的big threebig five是什麼?

二師兄:(嘿嘿,被裝到了)類的big three分別是拷貝建構函式(copy constructor)、拷貝賦值運運算元(copy assignment)和解構函式。而類的big five則多了兩個,分別是移動建構函式(move constructor)和移動賦值運運算元(move assignment)。後面兩個是C++11之後引入的。

面試官:好的。那你知道為什麼要引入移動構造和移動賦值嗎?

二師兄:主要是為了效率。移動構造和移動賦值不需要把所有的資料重新拷貝一遍,而是霸佔了被移動物件的資料的所有權。代價是被移動物件在被移動後不能使用。

面試官:嗯。那你知道為什麼移動構造和移動賦值都要加上noexcept關鍵字嗎?

二師兄:額。。。好像不讓拋異常?

面試官:你知道類的靜態成員變數需要注意哪些問題嗎?

二師兄:要注意哪些問題?額。。。

面試官:在成員方法後加const是為什麼?

二師兄:主要是為了約束這個成員方法不更改物件的任何資料。

面試官:還有其他的原因嗎?

二師兄:好像沒有了吧。。。

面試官:類的成員方法可以通過const符號過載嗎?

二師兄:這個,,應該可以吧。。

面試官:你知道什麼是類的成員方法的參照限定符嗎?

二師兄:沒有聽說過耶。。。

面試官:好的,回去等通知吧。

讓我們來看一看今日二師兄的表現吧,

為什麼移動構造和移動賦值都要加上noexcept關鍵字?

因為在使用移動語意時,通常會將資源的所有權從一個物件轉移到另一個物件,而不是複製資源。如果丟擲異常,那麼在轉移資源的過程中可能會出現問題,導致資源洩漏或其他不可預測的行為。

另外,加上 noexcept 關鍵字還可以提高程式碼的效能,因為編譯器可以在不必要的情況下進行優化。

類的靜態成員變數需要注意哪些問題?

靜態成員變數的初始化順序是不確定的。如果一個靜態成員變數依賴於另一個靜態成員變數的值,要確保第二個靜態化成員先被初始化,否則程式可能會出現未定義的行為。

靜態成員變數的值可以被多個範例同時修改,因此在多執行緒存取靜態成員時要注意資料競爭問題。靜態變數的生命週期與程式的生命週期相同,因此它們可能會佔用大量的記憶體。

在成員方法後加const是為什麼?

一是可以約束此方法不會更改物件的任何資料。二是cosnt物件也可以存取此成員方法。

#include <iostream>
struct Foo
{
    void f1(){std::cout <<"f1" << std::endl;}
    void f2() const{std::cout <<"f2" << std::endl;}
};
int main(int argc, char const *argv[])
{
    Foo foo;
    foo.f1();
    foo.f2();
    const Foo& foo2 = foo;
    foo2.f1();  //這裡無法通過編譯,因為const物件無法存取非const 方法
    foo2.f2();  //這裡可以通過編譯
}

類的成員方法可以通過const符號過載嗎?

這是一個很好的問題,估計很多人沒有思考過。先說答案,底層const可以,而頂層const不可以。

#include <iostream>
struct Foo{};
struct Goo
{
    void f1(Foo& f){std::cout <<"non const function" << std::endl;}
    void f1(const Foo& f){std::cout <<"const function" << std::endl;}
};
int main(int argc, char const *argv[])
{
    Foo foo;
    Goo goo;
    goo.f1(foo);    //無法通過編譯,error: ‘void Goo::f1(Foo)’ cannot be overloaded with ‘void Goo::f1(Foo)’
    return 0;
}

當我們把頂層const改為底層const

#include <iostream>
struct Foo{};
struct Goo
{
    void f1(Foo& f){std::cout <<"non const function" << std::endl;}
    void f1(const Foo& f){std::cout <<"const function" << std::endl;}
};
int main(int argc, char const *argv[])
{
    Foo non_const_foo;
    const Foo const_foo;
    Goo goo;
    goo.f1(non_const_foo);    //可以通過編譯	non const function
    goo.f1(const_foo);    //可以通過編譯 const function
    return 0;
}

那麼我們能否通過在函數括號後加上const來過載函數呢?

#include <iostream>
struct Goo
{
    void f1() {std::cout <<"non const function" << std::endl;}
    void f1() const{std::cout <<"const function" << std::endl;}
};
int main(int argc, char const *argv[])
{
    Goo non_const_goo;
    const Goo const_goo;
    non_const_goo.f1();   
    const_goo.f1();   
    return 0;
}

答案是肯定的,因為const_goo.f1() 可以等同於f1(const Goo* goo),也是底層const

最後一個問題雖然簡單,但我相信至少有80%的C++程式設計師不知道是什麼,

什麼是類的成員方法的參照限定符嗎?

類的成員方法的參照限定符是 C++11 中引入的一種新特性,用於指定成員方法的引數是左值參照還是右值參照。

#include <iostream>
struct Foo
{
    void f1() & {std::cout << "only left reference can call this function" << std::endl;}
    void f1() && {std::cout << "only right reference can call this function" << std::endl;}
};
int main(int argc, char const *argv[])
{
    Foo foo;
    foo.f1();       //left reference
    Foo().f1();     //right reference
    return 0;
}

好了,今日份面試到這裡就結束了,小夥伴們,對於今天二師兄的面試,能打幾分呢?如果是你,以上的問題都能回答的上來嗎?

關注我,帶你21天「精通」C++!(狗頭)