李長(zhǎng)河
(中國(guó)地質(zhì)大學(xué)(武漢)自動(dòng)化學(xué)院,武漢430074)
C++11新標(biāo)準(zhǔn)下引用的使用和教學(xué)方法研究
李長(zhǎng)河
(中國(guó)地質(zhì)大學(xué)(武漢)自動(dòng)化學(xué)院,武漢430074)
引用是提高程序運(yùn)行效率的重要工具。C++11新標(biāo)準(zhǔn)引入的右值引用拓寬了引用的使用范圍,進(jìn)一步提升了程序的性能。引用是C++教學(xué)的重要內(nèi)容,它是后續(xù)教學(xué)內(nèi)容的基礎(chǔ)。但是對(duì)于如何深入理解而又有效地教授學(xué)生使用引用,還很少有教材專門論述。在C++教學(xué)中,深入而又清晰地分析引用的特點(diǎn)和使用方法,能夠幫助學(xué)生靈活而又正確地使用引用。
在C++11新標(biāo)準(zhǔn)下,引用分為左值引用和右值引用[1]。左值引用指的是綁定到左值的引用,新標(biāo)準(zhǔn)頒布之前的引用都是指左值引用。右值引用指的是綁定到右值的引用[2]。引用主要在以下四種場(chǎng)合使用:第一種是在函數(shù)的形參列表中使用左值引用,將其與對(duì)應(yīng)的實(shí)參進(jìn)行綁定,用來高效地讀取或修改實(shí)參的內(nèi)容;第二種是函數(shù)返回值以左值引用的方式返回,避免臨時(shí)對(duì)象的產(chǎn)生;第三種是為觸發(fā)移動(dòng)語(yǔ)義而使用右值引用形參,目的是“竊取”將亡對(duì)象的資源,避免資源的拷貝[3-4],從而提高運(yùn)行效率;第四種是為觸發(fā)動(dòng)態(tài)綁定而使用左值引用,使用左值引用或指針是觸發(fā)動(dòng)態(tài)綁定的前提。
在新標(biāo)準(zhǔn)下,引用分為左值引用和右值引用。因此,掌握左值和右值的概念對(duì)于學(xué)生理解引用是非常重要的。
任何一個(gè)表達(dá)式,要么是左值,要么是右值。對(duì)于程序員來說,左值所在的內(nèi)存空間的地址是可以獲取的(可用取址符&獲?。?,但右值的地址是無法得到的(無法用取址符&獲?。R虼?,既可以讀取左值對(duì)象的內(nèi)容又可以向其寫入數(shù)據(jù),而右值對(duì)象只能執(zhí)行讀操作,不能對(duì)它執(zhí)行寫操作。顯然常量,如‘a(chǎn)’,3.14,10等都是右值,而由程序員定義的用來存放并能夠改變值的對(duì)象是左值。一般來說,右值只能在=符號(hào)右邊,左值沒有限制,如:
int i=0;//正確:用右值常量0初始化左值對(duì)象i
10=i;//錯(cuò)誤:賦值運(yùn)算符左側(cè)必須為左值
int j=i;//左值對(duì)象i可以當(dāng)成右值,只是對(duì)其內(nèi)容進(jìn)行讀操作
引用是一種復(fù)合類型,我們可以把它理解為一個(gè)對(duì)象的別名。也就是說,當(dāng)我們創(chuàng)建一個(gè)對(duì)象的引用的時(shí)候,編譯器將引用的對(duì)象的內(nèi)容與這個(gè)別名綁定,不會(huì)把對(duì)象的內(nèi)容拷貝給引用。例如:
int sum=0;
int&lr=sum;//定義一個(gè)int類型的左值引用,引用左值對(duì)象sum的內(nèi)容
lr+=1;//相當(dāng)于sum+=1
int&&rr1=sum+5;//定義一個(gè)右值引用,引用右值表達(dá)式
int&&rr2=10;//引用字面值常量
通過引用訪問與其綁定的對(duì)象與直接訪問引用的對(duì)象效果是一樣的。左值引用只能綁定到左值對(duì)象,而右值引用必須綁定到右值對(duì)象。例如:
int&&rr3=rr1;//錯(cuò)誤:rr1為左值
雖然rr1為右值引用,但rr1本身是左值對(duì)象,因此rr3不能引用左值對(duì)象。
定義引用時(shí),需要注意以下幾點(diǎn):1)定義一個(gè)引用時(shí)必須要初始化,使其與一個(gè)對(duì)象綁定;2)使用引用時(shí),不能改變綁定的對(duì)象,也就是說,一旦定義一個(gè)引用,它始終與初始化的對(duì)象綁定在一起;3)引用必須綁定到相同數(shù)據(jù)類型的對(duì)象上。
在教學(xué)方法上,正反舉例法可以有效地加深學(xué)生對(duì)概念的理解,這對(duì)于C++等實(shí)踐性比較強(qiáng)的課程是非常適用的教學(xué)方法。通過正反舉例,學(xué)生可以清晰地理解左值、右值、左值引用和右值引用等概念并掌握易錯(cuò)點(diǎn)。另外,對(duì)于前后有關(guān)聯(lián)的知識(shí)點(diǎn),采用遞進(jìn)式教學(xué)方法有助于學(xué)生理解新知識(shí)。例如,在講解引用之前,學(xué)生需要了解左值和右值的概念。只有正確把握了左值和右值的概念,才能掌握引用的概念。
從以上所述可以看出,引用的行為類似于指針常量(注意不是指向常量的指針)的行為。一個(gè)指針常量在定義時(shí)與一個(gè)對(duì)象綁定,使用期間不允許改變其指向。
int i=0;
int*const p=&i;//不允許指針p指向其他對(duì)象
(*p)++;//等價(jià)于i++
cout<
雖然定義引用時(shí)編譯器不會(huì)根據(jù)引用對(duì)象的類型分配存儲(chǔ)空間,但和指針一樣,引用本身需要占用存儲(chǔ)空間。
cout< 引用和普通指針的主要區(qū)別在于:1)定義引用時(shí)必須初始化,定義指針時(shí)不需要初始化;2)賦值行為不同:對(duì)引用賦值修改綁定對(duì)象的值,對(duì)指針賦值改變其指向的對(duì)象。 把這一點(diǎn)講清楚,學(xué)生就不難理解引用了,而且對(duì)于后續(xù)知識(shí)的講解也十分重要。例如,C++的多態(tài)行為既可以作用于指針也可以作用于引用,如果我們沒有講清楚這一點(diǎn),學(xué)生很難理解引用的多態(tài)行為是怎樣實(shí)現(xiàn)的。 通過右值引用可以訪問無名的臨時(shí)對(duì)象,相當(dāng)于給無名的臨時(shí)對(duì)象的取個(gè)名字,本質(zhì)上將一個(gè)短暫的右值轉(zhuǎn)化為持久的左值。也就是說,該右值又“重獲新生”,其生命期與被綁定的右值引用的生命期一致。 double&&rr=sqrt(3.14); 函數(shù)sqrt的返回值保存在一個(gè)臨時(shí)對(duì)象里,rr就是這個(gè)臨時(shí)對(duì)象的引用,通過rr可以直接訪問它。因此,原來臨時(shí)的右值實(shí)際上變成了一個(gè)持久的左值。 右值引用的這個(gè)特性非常重要,是移動(dòng)語(yǔ)義的基礎(chǔ),教學(xué)過程中應(yīng)重點(diǎn)講解。在教學(xué)方法上,使用上面的舉例分析法可使學(xué)生能夠透過現(xiàn)象看到右值引用的本質(zhì)。 如果右值引用聲明&&與類型推導(dǎo)結(jié)合起來,那么&&并非總意味著右值引用類型,此時(shí)&&將成為一種通用引用類型[5]: int i=0; auto&&rr1=10;//rr1被推導(dǎo)為右值引用 auto&&rr2=i;//rr2被推導(dǎo)為左值引用 上面第二條語(yǔ)句中auto&&根據(jù)字面值常量推導(dǎo)出rr1為右值引用,而第三條語(yǔ)句中rr2被推導(dǎo)為一個(gè)左值引用。右值聲明為auto&&的對(duì)象都是通用引用。類似的,如果一個(gè)模板函數(shù)形參為模板類型參數(shù)(T)的右值引用(T&&),那么形參類型也有同樣的行為: template f(10);//par被推導(dǎo)為右值引用 f(i);//par被推導(dǎo)為左值引用,假設(shè)i為左值對(duì)象 在教學(xué)方法上,采用類比法可以加深學(xué)生對(duì)左值引用的理解和避免對(duì)右值引用錯(cuò)誤的認(rèn)識(shí)。通過左值引用和指針類比,學(xué)生可以清晰地認(rèn)識(shí)到左值引用的本質(zhì)。當(dāng)右值引用與類型推導(dǎo)結(jié)合時(shí),便成了一種通用引用。通過類比,學(xué)生可以避免遇見&&即為右值引用的錯(cuò)誤認(rèn)識(shí),進(jìn)一步提高學(xué)生對(duì)引用的認(rèn)識(shí)。 使用左值引用作為函數(shù)形參有兩個(gè)考慮:1)通過形參可以改變實(shí)參的值;2)可以避免對(duì)實(shí)參的拷貝,提高程序運(yùn)行效率。可以通過下面的例子對(duì)上面的知識(shí)點(diǎn)進(jìn)行講解。 假設(shè)有如下函數(shù)調(diào)用: Foo a; passByValue(a);//調(diào)用Foo的復(fù)制構(gòu)造函數(shù),打印輸出copied cout< passByRef(a);//形參x為a的引用,a的值被修改 cout< 在教學(xué)方法中,通過案例對(duì)比法,講解引用形參和非引用形參的區(qū)別。如上面的例子中,函數(shù)passByRef的形參為實(shí)參的引用,在調(diào)用的過程中不會(huì)發(fā)生復(fù)制構(gòu)造。對(duì)形參進(jìn)行修改等價(jià)于修改實(shí)參的值。如果采用非引用形參,實(shí)參向形參傳遞的是值,因此會(huì)發(fā)生復(fù)制構(gòu)造才能將值傳遞給形參。通過對(duì)比講解,學(xué)生可以清楚了解到值傳遞和引用傳遞的區(qū)別。 通過這個(gè)例子的講解,可以提示學(xué)生引用形參有利無弊,并歸納出引用形參的通用性。因此,建議學(xué)生盡量使用引用形參,引起學(xué)生的重視。進(jìn)一步考慮到程序的安全性,如果對(duì)實(shí)參只執(zhí)行讀操作,可以告訴學(xué)生使用const引用,保證實(shí)參的安全性。 與引用形參類似,函數(shù)值以引用的方式返回可以避免復(fù)制構(gòu)造,提高程序執(zhí)行效率。例如,F(xiàn)oo類的成員函數(shù)get返回?cái)?shù)據(jù)成員m_x的引用,與指針類似,返回的值與m_x是同一個(gè)內(nèi)存空間。如果,成員函數(shù)get被改為: string Foo::get(){return m_x;} 在返回時(shí),將以復(fù)制構(gòu)造的方式構(gòu)造一個(gè)臨時(shí)對(duì)象,這個(gè)臨時(shí)對(duì)象是m_x的一個(gè)副本,構(gòu)造的時(shí)候需要分配存儲(chǔ)空間并進(jìn)行數(shù)據(jù)復(fù)制。因此,普通值返回的方式會(huì)大大降低程序的執(zhí)行效率。 在教學(xué)方法上,采用與引用形參相類比的方法,可以很容易把引用返回的優(yōu)點(diǎn)講清楚,學(xué)生也比較容易接受。在此基礎(chǔ)之上,接下來可采用互動(dòng)式與引導(dǎo)式教學(xué)方法把引用對(duì)象的存儲(chǔ)類型的要求講清楚。例如,可以向?qū)W生提問:可以返回局部對(duì)象的引用嗎?互動(dòng)之后,給出答案:函數(shù)不能返回一個(gè)局部對(duì)象的引用。這是因?yàn)楫?dāng)一個(gè)函數(shù)返回時(shí),函數(shù)體中的局部對(duì)象包括非引用形參都會(huì)消亡。因此,引用已經(jīng)消亡的對(duì)象是沒有意義的。例如,下面fun函數(shù)返回的局部對(duì)象i: int&fun(int i){return i;}//錯(cuò)誤:不能返回局部對(duì)象的引用引導(dǎo)和互動(dòng)式的教學(xué)方式不但能夠加深學(xué)生對(duì)所學(xué)知識(shí)的理解,準(zhǔn)確地把握事物的本質(zhì),而且還能自然地引入新的知識(shí)或強(qiáng)化對(duì)已有知識(shí)的認(rèn)識(shí)。 移動(dòng)語(yǔ)義是C++11引入的新的語(yǔ)言特性,可以說移動(dòng)語(yǔ)義就是為了性能而生。臨時(shí)對(duì)象是影響程序運(yùn)行效率的一個(gè)重要因素。一個(gè)程序在運(yùn)行期間,不可避免地會(huì)產(chǎn)生大量的臨時(shí)對(duì)象,這些臨時(shí)對(duì)象的生命期是短暫的,幾乎只被使用一次就會(huì)消亡。程序員無法控制這些臨時(shí)對(duì)象,它們不可以訪問。因此,它們往往也被稱為幽靈對(duì)象。為了解決這個(gè)問題,基于右值引用的移動(dòng)語(yǔ)義便由此而生。 一般情況下,一個(gè)類要啟用移動(dòng)語(yǔ)義,需要定義移動(dòng)成員。改造的Foo類如下: 第二個(gè)構(gòu)造函數(shù)為移動(dòng)構(gòu)造函數(shù),其形參為Foo類型的右值引用,用來接受一個(gè)右值。當(dāng)執(zhí)行完此移動(dòng)構(gòu)造函數(shù)之后,實(shí)參對(duì)象的資源會(huì)被“竊取”。例如: Foo a(“test”); Foo b(std::move(a)); //庫(kù)函數(shù)move將左值a轉(zhuǎn)化為右值 cout<<“b:”< if(!a.get())cout<<“a is empty”< 為了方便測(cè)試,上述代碼利用庫(kù)函數(shù)move將左值對(duì)象a轉(zhuǎn)化為右值,用來觸發(fā)移動(dòng)構(gòu)造函數(shù)。在構(gòu)造對(duì)象b的過程中,程序并沒有為b分配存儲(chǔ)空間和復(fù)制數(shù)據(jù)的操作,而是直接把a(bǔ)的內(nèi)容“竊取”出來。構(gòu)造完畢之后,a已經(jīng)沒有任何資源了。 在教學(xué)方法上,舉例法可以清晰地把移動(dòng)語(yǔ)義講清楚。通過上面的例子,學(xué)生可以非常清楚地看到對(duì)象a的資源是如何被對(duì)象b“竊取”的,而且學(xué)生也會(huì)體會(huì)到在執(zhí)行的過程中,不需要分配存儲(chǔ)空間和復(fù)制數(shù)據(jù),程序的性能得到了明顯的改善。 在學(xué)生掌握了移動(dòng)構(gòu)造函數(shù)之后,可采用任務(wù)驅(qū)動(dòng)的授課方式講解移動(dòng)賦值運(yùn)算符。即,先提問如何“竊取”=符號(hào)右側(cè)對(duì)象的資源,然后分析任務(wù)需求,最后給出最終的實(shí)現(xiàn)。 Foo&operator=(Foo&&rhs){m_x=rhs.m_x;rhs.m_x=nullptr;return*this;} 多態(tài)性是面向?qū)ο蟪绦蛟O(shè)計(jì)的核心技術(shù)之一,它是通過虛函數(shù)的動(dòng)態(tài)綁定來實(shí)現(xiàn)的,即在運(yùn)行期間,根據(jù)基類指針或引用所綁定的對(duì)象來確定具體的行為。因此,觸發(fā)動(dòng)態(tài)綁定的前提是使用指針或引用。下面構(gòu)造一個(gè)抽象基類和兩個(gè)公有派生類: struct B{virtual void fun()=0;}; struct D1:public B{void fun(){cout<<“fun of D1”< struct D2:public B{void fun(){cout<<“fun of D2”< 其中,基類B定義了一個(gè)虛接口,兩個(gè)派生類分別定義了各自版本的實(shí)現(xiàn)。為了測(cè)試基于引用的動(dòng)態(tài)綁定,設(shè)計(jì)如下測(cè)試函數(shù): void test(const B&b){b.fun();} test函數(shù)形參為基類B的引用,函數(shù)體為一個(gè)成員fun函數(shù)的調(diào)用。測(cè)試代碼如下: D1 d1;D2 d2; test(d1);//打印輸出:fun of D1 test(d2);//打印輸出:fun of D2 上面代碼定義了兩個(gè)派生類對(duì)象d1和d2。在調(diào)用test函數(shù)時(shí),當(dāng)基類引用形參b與一個(gè)派生類對(duì)象綁定時(shí),便會(huì)調(diào)用相對(duì)應(yīng)版本的fun函數(shù),從而實(shí)現(xiàn)動(dòng)態(tài)綁定。 在教學(xué)方法上,建議采用案例分析的方法把抽象的概念具體化,從而使學(xué)生能夠深入地體會(huì)和理解動(dòng)態(tài)綁定與多態(tài)性的概念。設(shè)計(jì)的案例要重點(diǎn)突出,精簡(jiǎn)扼要,抓住講解內(nèi)容的本質(zhì)。 筆者運(yùn)用上述教學(xué)方法,通過對(duì)比分析學(xué)生每學(xué)期進(jìn)行的四次上機(jī)考核結(jié)果和課程設(shè)計(jì)的實(shí)訓(xùn)效果發(fā)現(xiàn),學(xué)生逐漸加強(qiáng)了引用的使用,程序的運(yùn)行效率得到了很大的改進(jìn)。 圍繞C++11新標(biāo)準(zhǔn)下引用知識(shí)點(diǎn)在教學(xué)過程中的難點(diǎn)和重點(diǎn)問題,研究和討論了引用的本質(zhì)及其四種不同的應(yīng)用場(chǎng)合,針對(duì)具體教授知識(shí)點(diǎn)的不同特點(diǎn),推薦了相應(yīng)的教學(xué)方法。筆者運(yùn)用上述教學(xué)方法,精心設(shè)計(jì)了相應(yīng)的教學(xué)案例,通過近5年內(nèi)的教學(xué)效果分析,上述工作對(duì)學(xué)生理解和使用引用具有重要作用。 [1]國(guó)際標(biāo)準(zhǔn)組織和國(guó)際電工委員會(huì).ISO/IEC 14882:2011,Information Technology-Programming Languages-C++[S].ISO.2 2011-09-1(3):187-189. [2]Stanley B.Lippman,Josée Lajoie,Barbara E.Moo.C++Primer[M](中文版第5版).王剛,楊巨峰,譯.北京:電子工業(yè)出版社,2013-09-01:471-472. [3]Michael Wong,IBM XL編譯器中國(guó)開發(fā)團(tuán)隊(duì).深入理解C++11:C++11新特性解析與應(yīng)用[M].北京:機(jī)械工業(yè)出版社,2013-06-07:68-85. [4]祁宇.深入應(yīng)用C++11:代碼優(yōu)化與工程級(jí)應(yīng)用[M].北京:機(jī)械工業(yè)出版社,2015-05-01:64-78. [5]Scott Meyers.Effective Modern C++[M].O'Reilly Media.2014-11-7:164-168. Research on the Usages of Reference and Teaching Methods in C++11 New Standard LI Chang-he (China University of Geosciences,School of Automation,Wuhan 430074) Reference is an important feature of C++,and it has been extended in the new standard C++11,where introduces the r-value reference.However,there is seldom analysis of teaching methods for reference under the new standard.Analyzes the characteristics of reference and its applications comprehensively,discusses the effective teaching methods.By using these teaching methods,students are able to fully un?derstand reference and its usages. 1007-1423(2017)27-0003-05 10.3969/j.issn.1007-1423.2017.27.001 是C++語(yǔ)言的一個(gè)重要特性,C++11新標(biāo)準(zhǔn)對(duì)引用進(jìn)行拓展,引入右值引用。目前針對(duì)新標(biāo)準(zhǔn)下引用的教學(xué)方法還很少有專門論述。對(duì)C++新標(biāo)準(zhǔn)下引用進(jìn)行深入分析,闡述使用引用的場(chǎng)合和方法,探討有效的教學(xué)方法。教學(xué)效果表明,這些方法能夠幫助學(xué)生深入理解和正確使用引用。 C++11;左值引用;右值引用;教學(xué)方法 國(guó)家自然科學(xué)基金面上項(xiàng)目(No.61673355)、湖北省“楚天學(xué)子”人才項(xiàng)目(No.162301132807)、中國(guó)地質(zhì)大學(xué)(武漢)“騰飛計(jì)劃”項(xiàng)目(No.G1323531750)、中國(guó)地質(zhì)大學(xué)(武漢)研究生教育教學(xué)改革研究項(xiàng)目(No.YJG2017101) 李長(zhǎng)河(1983-),男,河北秦皇島人,副教授,博士生導(dǎo)師,英國(guó)萊斯特大學(xué)博士研究生學(xué)位,研究方向?yàn)橹悄軆?yōu)化與學(xué)習(xí) 2017-07-18 2017-09-14 C++11;L-value Reference;R-value Reference;Teaching Methods2.2 右值轉(zhuǎn)化為左值
2.3 通用引用
3 使用引用
3.1 左值引用形參
3.2 返回左值引用
3.3 觸發(fā)移動(dòng)語(yǔ)義
3.4 觸發(fā)動(dòng)態(tài)綁定
4 結(jié)語(yǔ)