一文講清C/C++ Const/Const_Cast/Constexpr
本文轉(zhuǎn)載自微信公眾號(hào)「碼磚雜役」,作者我不想種地 。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼磚雜役公眾號(hào)。
很多人搞不清const、const_cast、constexpr的用法,稀里糊涂地用。一般而言,即使亂用,問(wèn)題也不大,因?yàn)殄e(cuò)大發(fā)了會(huì)崩,崩了自然會(huì)被修正,不崩自然也就沒(méi)事。但作為一個(gè)有追求的專(zhuān)業(yè)程序員,自當(dāng)聞過(guò)則喜,搞清楚弄明白。
一、const
C語(yǔ)言的const用法
先講const,這玩意兒怎么翻譯我也拿不準(zhǔn),C語(yǔ)言中該關(guān)鍵字的用法比較簡(jiǎn)單,大概有如下幾種用法:
[1] 修飾普通變量:變量只讀,在程序運(yùn)行過(guò)程中不可修改。
- const int i = 100; //i is read only
- i = 200; //compile error, variable i can not assignable
[2] 修飾指針 const T* p:表示不能通過(guò)p去修改p指向?qū)ο蟮膬?nèi)容,另一方面只能通過(guò)p調(diào)用T類(lèi)的const成員函數(shù)
- const struct Foo *f = new Foo;
- f->dataX = 100; //compile error
- const char* p = "abc";
- p[1] = 'x'; //compile error
- f->nonconst_member_function(); ///compile error (后面再講)
[3] 修飾指針 T* const p:表示指針只能在初始化時(shí)設(shè)置指向,之后便不能修改指向。
- char s1[] = "abc";
- char s2[] = "xyz";
- char* const p = s1;
- p = s2; //compile error
[4] 修飾指針 const T* const p:表示既不能通過(guò)p修改它指向的對(duì)象,又不能更改p的指向。
- const char* const p = "abc";
- p[1] = 'B'; //compile error
- p = "xyz"; //compile error
[5] 修飾函數(shù)參數(shù):c語(yǔ)言中const修飾參數(shù)反映的含義同上所述
小結(jié):C語(yǔ)言中,const的用法差不多就這些,比較簡(jiǎn)單。
C++擴(kuò)充了const的用法
[1] 修飾成員變量:const成員變量只能在初始化列表里做初始化,程序運(yùn)行中不可修改;如果是const整型,則可以C++11標(biāo)準(zhǔn)之后直接初始化。
- struct Foo
- {
- Foo() : PI(3.15) {} // PI is initialized by initializer list
- const int c = 100; //C++11 support
- const float PI;
- };
[2] 修飾成員函數(shù):表示該成員函數(shù)是只讀函數(shù),不會(huì)修改默認(rèn)參數(shù)this的成員變量,如果修改會(huì)編譯報(bào)錯(cuò)。
- class Foo
- {
- int m_money;
- public:
- int get_money() const //✅
- {
- return m_money;
- }
- int set_money(int money) const //❌
- {
- m_money = money; //修改了this->m_money;需去掉函數(shù)const修飾
- }
- };
[3] 修飾引用:引用是C++才有的語(yǔ)法特征,引用是別名,本質(zhì)上跟指針差不多,所以const修飾引用跟修飾指針的語(yǔ)義和約束差不多。
- Foo f;
- const Foo& r = f;
- r.m_data = 1; //compile error
[4] C++中對(duì)const修飾指針的補(bǔ)充
- struct Foo
- {
- int const_member_function() const { return m_data; }
- int non_const_member_function(int data) { m_data = data; }
- int m_data;
- };
- int main()
- {
- const Foo* f = new Foo;
- f->const_member_function(); //OK
- f->non_const_member_function(); //compile ERROR
- return 0;
- }
為什么呢?因?yàn)閏onst成員函數(shù)相當(dāng)于承諾不會(huì)修改this的成員變量,而該承諾會(huì)被編譯器檢查,如果沒(méi)有履行承諾,則編譯器會(huì)報(bào)錯(cuò)。而const Foo* f意味著不能通過(guò)f去修改f指針指向變量的內(nèi)部值。
通過(guò)f->data = 1的方式肯定是不行。
另一方面,你只能通過(guò)f去調(diào)用它的const成員函數(shù),因?yàn)閏onst成員函數(shù)的語(yǔ)義就是不會(huì)修改this的值,編譯器很容易執(zhí)行這個(gè)校驗(yàn)。
const修飾參數(shù)
const可以修飾普通參數(shù),也可以修飾指針/引用參數(shù),因?yàn)樾螀⑹菍?shí)參的副本,所以const修飾普通參數(shù)其實(shí)沒(méi)什么意義,我們著重講講const修飾指針/引用參數(shù)。
比如標(biāo)準(zhǔn)C庫(kù)函數(shù)strcpy的簽名:char *strcpy(char * dst, const char * src);
dst表示目標(biāo)地址,src表示源串,const修飾了源串,這是因?yàn)閺脑创截惖侥繕?biāo)串,不需要修改源串內(nèi)容,這相當(dāng)于向strcpy調(diào)用者承諾:
放心大膽的調(diào)用吧,strcpy函數(shù)實(shí)現(xiàn)保證不會(huì)修改src的內(nèi)容,編譯器會(huì)執(zhí)行這種檢查。
這樣,在review代碼的時(shí)候,如果想追蹤src在哪里被修改了,當(dāng)看到strcpy的簽名,就不用打開(kāi)函數(shù)去看實(shí)現(xiàn),只要不違背承諾,肯定不是這個(gè)函數(shù)內(nèi)改動(dòng)了src。
const char *src是一種承諾,也是一種約束。調(diào)用的地方,const char*形式的形參,既傳const char*實(shí)參,也可以傳char*實(shí)參,因?yàn)閰?shù)const char*是更強(qiáng)的承諾。
但反之不成立。比如第一個(gè)參數(shù)dst是不帶const的,那么如果有一個(gè)變量類(lèi)型為const char* p,那不能把p作為第一個(gè)參數(shù)傳遞進(jìn)strcpy,編譯不過(guò)。
因?yàn)閟trcpy不承諾不修改dst,是一個(gè)更弱的承諾,只有聲明為const指針的參數(shù),才能傳遞const指針實(shí)參。
const其他
const還可以修飾返回值,還可以跟extern結(jié)合,但這些都是一些小語(yǔ)法技巧,一般開(kāi)發(fā)用不太到,真碰到再查不遲。
二、const_cast
const_cast有什么用?
const是C++的一個(gè)強(qiáng)制轉(zhuǎn)換,它用來(lái)去掉const屬性,比如:
- Foo foo;
- const Foo *f1 = &foo;
- Foo* f2 = const_cast<Foo*>(f);
- Foo* f3 = (Foo*)f;
const_cast的作用跟強(qiáng)轉(zhuǎn)差不多,C++加const_cast主要是為了功能完整性,const_cast作用于引用跟作用于指針差不多。
為什么說(shuō)const_cast幾乎都反應(yīng)接口設(shè)計(jì)有問(wèn)題
程序設(shè)計(jì)要言行一致,遵守承諾,這意味著:不應(yīng)該把參數(shù)聲明為const指針,而函數(shù)實(shí)現(xiàn)里借助強(qiáng)制去掉const屬性。
首先,這樣做是危險(xiǎn)的,比如const char* p = "abc"; p指向常量字符串被作為參數(shù)傳遞,被強(qiáng)轉(zhuǎn)+修改,則會(huì)導(dǎo)致程序crash。
其次,這樣做是分裂的,因?yàn)槟慵觕onst修飾相當(dāng)于讓編譯器幫你執(zhí)行檢查,以便在你違背承諾的時(shí)候通過(guò)編譯期檢查報(bào)錯(cuò)提醒你,但在它真正向你報(bào)錯(cuò)的時(shí)候,你又說(shuō)別管啦,老子就是要蠻干。
const_cast或者通過(guò)c風(fēng)格強(qiáng)轉(zhuǎn),基本上都暴露出設(shè)計(jì)上的問(wèn)題。
設(shè)計(jì)良好的程序基本上不需要const強(qiáng)轉(zhuǎn)。因?yàn)閏onst約束在調(diào)用鏈會(huì)傳播,所以,你需要一以貫之的遵守約定,找到導(dǎo)致需要const強(qiáng)轉(zhuǎn)的錯(cuò)誤源頭,這可能會(huì)多費(fèi)一點(diǎn)時(shí)間,但它是值得的。
三、constexpr
const沒(méi)有區(qū)分編譯期常量和運(yùn)行期常量,constexpr是C++11開(kāi)始提出的關(guān)鍵字,被限定為編譯器常量,其意義與14版本有一些區(qū)別。
C++11中的constexpr指定的函數(shù)返回值和參數(shù)必須要保證是字面值,而且必須有且只有一行return代碼,這給函數(shù)的設(shè)計(jì)者帶來(lái)了更多的限制,比如通常只能通過(guò)return 三目運(yùn)算符+遞歸來(lái)計(jì)算返回的字面值。
而C++14中只要保證返回值和參數(shù)是字面值就行了,函數(shù)體中可以加入更多的語(yǔ)句,方便了更靈活的計(jì)算。
這里我們主要講constexpr和const的區(qū)別。
constexpr可以用來(lái)修飾變量、函數(shù)、構(gòu)造函數(shù)。一旦以上任何元素被constexpr修飾,那么等于說(shuō)是告訴編譯器 “請(qǐng)大膽地將我看成編譯時(shí)就能得出常量值的表達(dá)式去優(yōu)化我”。
- constexpr func()
- {
- return 10;
- }
- int main()
- {
- int arr[func()];
- }
編譯期大膽地將func()做了優(yōu)化,在編譯期就確定了func計(jì)算出的值10而無(wú)需等到運(yùn)行時(shí)再去計(jì)算。
這就是constexpr的第一個(gè)作用:給編譯器足夠的信心在編譯期去做被constexpr修飾的表達(dá)式的優(yōu)化。
constexpr還有另外一個(gè)特性,雖然它本身的作用之一就是希望程序員能給編譯器做優(yōu)化的信心,但它卻猜到了自己可能會(huì)被程序員欺騙,而編譯器并不會(huì)對(duì)此“惱羞成怒”中止編譯。
四、結(jié)論
C/C++程序應(yīng)該積極的使用const/constexpr,什么叫積極使用?只要有可能,那么我們就應(yīng)該用const/constexpr。
只要可能就應(yīng)該用xx,這種話(huà)一般而言都是錯(cuò)的,但用在const/constexpr卻很正確,因?yàn)槭褂胏onst/constexpr基本上都會(huì)讓你的程序更健壯、更快,const修飾的整型變量,在gcc開(kāi)優(yōu)化選項(xiàng)的時(shí)候,有可能被直接編譯到匯編代碼指令,而非生成一個(gè)變量,而constexpr的優(yōu)化作用在前面一節(jié)已經(jīng)闡述。
與之對(duì)應(yīng)的是:只要有可能,就不要使用const_cast,它基本上都反映了接口設(shè)計(jì)上的問(wèn)題。
就醬,信不信隨你!