李长河
(中国地质大学(武汉)自动化学院,武汉430074)
C++11新标准下引用的使用和教学方法研究
李长河
(中国地质大学(武汉)自动化学院,武汉430074)
引用是提高程序运行效率的重要工具。C++11新标准引入的右值引用拓宽了引用的使用范围,进一步提升了程序的性能。引用是C++教学的重要内容,它是后续教学内容的基础。但是对于如何深入理解而又有效地教授学生使用引用,还很少有教材专门论述。在C++教学中,深入而又清晰地分析引用的特点和使用方法,能够帮助学生灵活而又正确地使用引用。
在C++11新标准下,引用分为左值引用和右值引用[1]。左值引用指的是绑定到左值的引用,新标准颁布之前的引用都是指左值引用。右值引用指的是绑定到右值的引用[2]。引用主要在以下四种场合使用:第一种是在函数的形参列表中使用左值引用,将其与对应的实参进行绑定,用来高效地读取或修改实参的内容;第二种是函数返回值以左值引用的方式返回,避免临时对象的产生;第三种是为触发移动语义而使用右值引用形参,目的是“窃取”将亡对象的资源,避免资源的拷贝[3-4],从而提高运行效率;第四种是为触发动态绑定而使用左值引用,使用左值引用或指针是触发动态绑定的前提。
在新标准下,引用分为左值引用和右值引用。因此,掌握左值和右值的概念对于学生理解引用是非常重要的。
任何一个表达式,要么是左值,要么是右值。对于程序员来说,左值所在的内存空间的地址是可以获取的(可用取址符&获取),但右值的地址是无法得到的(无法用取址符&获取)。因此,既可以读取左值对象的内容又可以向其写入数据,而右值对象只能执行读操作,不能对它执行写操作。显然常量,如‘a’,3.14,10等都是右值,而由程序员定义的用来存放并能够改变值的对象是左值。一般来说,右值只能在=符号右边,左值没有限制,如:
int i=0;//正确:用右值常量0初始化左值对象i
10=i;//错误:赋值运算符左侧必须为左值
int j=i;//左值对象i可以当成右值,只是对其内容进行读操作
引用是一种复合类型,我们可以把它理解为一个对象的别名。也就是说,当我们创建一个对象的引用的时候,编译器将引用的对象的内容与这个别名绑定,不会把对象的内容拷贝给引用。例如:
int sum=0;
int&lr=sum;//定义一个int类型的左值引用,引用左值对象sum的内容
lr+=1;//相当于sum+=1
int&&rr1=sum+5;//定义一个右值引用,引用右值表达式
int&&rr2=10;//引用字面值常量
通过引用访问与其绑定的对象与直接访问引用的对象效果是一样的。左值引用只能绑定到左值对象,而右值引用必须绑定到右值对象。例如:
int&&rr3=rr1;//错误:rr1为左值
虽然rr1为右值引用,但rr1本身是左值对象,因此rr3不能引用左值对象。
定义引用时,需要注意以下几点:1)定义一个引用时必须要初始化,使其与一个对象绑定;2)使用引用时,不能改变绑定的对象,也就是说,一旦定义一个引用,它始终与初始化的对象绑定在一起;3)引用必须绑定到相同数据类型的对象上。
在教学方法上,正反举例法可以有效地加深学生对概念的理解,这对于C++等实践性比较强的课程是非常适用的教学方法。通过正反举例,学生可以清晰地理解左值、右值、左值引用和右值引用等概念并掌握易错点。另外,对于前后有关联的知识点,采用递进式教学方法有助于学生理解新知识。例如,在讲解引用之前,学生需要了解左值和右值的概念。只有正确把握了左值和右值的概念,才能掌握引用的概念。
从以上所述可以看出,引用的行为类似于指针常量(注意不是指向常量的指针)的行为。一个指针常量在定义时与一个对象绑定,使用期间不允许改变其指向。
int i=0;
int*const p=&i;//不允许指针p指向其他对象
(*p)++;//等价于i++
cout<
虽然定义引用时编译器不会根据引用对象的类型分配存储空间,但和指针一样,引用本身需要占用存储空间。
cout< 引用和普通指针的主要区别在于:1)定义引用时必须初始化,定义指针时不需要初始化;2)赋值行为不同:对引用赋值修改绑定对象的值,对指针赋值改变其指向的对象。 把这一点讲清楚,学生就不难理解引用了,而且对于后续知识的讲解也十分重要。例如,C++的多态行为既可以作用于指针也可以作用于引用,如果我们没有讲清楚这一点,学生很难理解引用的多态行为是怎样实现的。 通过右值引用可以访问无名的临时对象,相当于给无名的临时对象的取个名字,本质上将一个短暂的右值转化为持久的左值。也就是说,该右值又“重获新生”,其生命期与被绑定的右值引用的生命期一致。 double&&rr=sqrt(3.14); 函数sqrt的返回值保存在一个临时对象里,rr就是这个临时对象的引用,通过rr可以直接访问它。因此,原来临时的右值实际上变成了一个持久的左值。 右值引用的这个特性非常重要,是移动语义的基础,教学过程中应重点讲解。在教学方法上,使用上面的举例分析法可使学生能够透过现象看到右值引用的本质。 如果右值引用声明&&与类型推导结合起来,那么&&并非总意味着右值引用类型,此时&&将成为一种通用引用类型[5]: int i=0; auto&&rr1=10;//rr1被推导为右值引用 auto&&rr2=i;//rr2被推导为左值引用 上面第二条语句中auto&&根据字面值常量推导出rr1为右值引用,而第三条语句中rr2被推导为一个左值引用。右值声明为auto&&的对象都是通用引用。类似的,如果一个模板函数形参为模板类型参数(T)的右值引用(T&&),那么形参类型也有同样的行为: template f(10);//par被推导为右值引用 f(i);//par被推导为左值引用,假设i为左值对象 在教学方法上,采用类比法可以加深学生对左值引用的理解和避免对右值引用错误的认识。通过左值引用和指针类比,学生可以清晰地认识到左值引用的本质。当右值引用与类型推导结合时,便成了一种通用引用。通过类比,学生可以避免遇见&&即为右值引用的错误认识,进一步提高学生对引用的认识。 使用左值引用作为函数形参有两个考虑:1)通过形参可以改变实参的值;2)可以避免对实参的拷贝,提高程序运行效率。可以通过下面的例子对上面的知识点进行讲解。 假设有如下函数调用: Foo a; passByValue(a);//调用Foo的复制构造函数,打印输出copied cout< passByRef(a);//形参x为a的引用,a的值被修改 cout< 在教学方法中,通过案例对比法,讲解引用形参和非引用形参的区别。如上面的例子中,函数passByRef的形参为实参的引用,在调用的过程中不会发生复制构造。对形参进行修改等价于修改实参的值。如果采用非引用形参,实参向形参传递的是值,因此会发生复制构造才能将值传递给形参。通过对比讲解,学生可以清楚了解到值传递和引用传递的区别。 通过这个例子的讲解,可以提示学生引用形参有利无弊,并归纳出引用形参的通用性。因此,建议学生尽量使用引用形参,引起学生的重视。进一步考虑到程序的安全性,如果对实参只执行读操作,可以告诉学生使用const引用,保证实参的安全性。 与引用形参类似,函数值以引用的方式返回可以避免复制构造,提高程序执行效率。例如,Foo类的成员函数get返回数据成员m_x的引用,与指针类似,返回的值与m_x是同一个内存空间。如果,成员函数get被改为: string Foo::get(){return m_x;} 在返回时,将以复制构造的方式构造一个临时对象,这个临时对象是m_x的一个副本,构造的时候需要分配存储空间并进行数据复制。因此,普通值返回的方式会大大降低程序的执行效率。 在教学方法上,采用与引用形参相类比的方法,可以很容易把引用返回的优点讲清楚,学生也比较容易接受。在此基础之上,接下来可采用互动式与引导式教学方法把引用对象的存储类型的要求讲清楚。例如,可以向学生提问:可以返回局部对象的引用吗?互动之后,给出答案:函数不能返回一个局部对象的引用。这是因为当一个函数返回时,函数体中的局部对象包括非引用形参都会消亡。因此,引用已经消亡的对象是没有意义的。例如,下面fun函数返回的局部对象i: int&fun(int i){return i;}//错误:不能返回局部对象的引用引导和互动式的教学方式不但能够加深学生对所学知识的理解,准确地把握事物的本质,而且还能自然地引入新的知识或强化对已有知识的认识。 移动语义是C++11引入的新的语言特性,可以说移动语义就是为了性能而生。临时对象是影响程序运行效率的一个重要因素。一个程序在运行期间,不可避免地会产生大量的临时对象,这些临时对象的生命期是短暂的,几乎只被使用一次就会消亡。程序员无法控制这些临时对象,它们不可以访问。因此,它们往往也被称为幽灵对象。为了解决这个问题,基于右值引用的移动语义便由此而生。 一般情况下,一个类要启用移动语义,需要定义移动成员。改造的Foo类如下: 第二个构造函数为移动构造函数,其形参为Foo类型的右值引用,用来接受一个右值。当执行完此移动构造函数之后,实参对象的资源会被“窃取”。例如: Foo a(“test”); Foo b(std::move(a)); //库函数move将左值a转化为右值 cout<<“b:”< if(!a.get())cout<<“a is empty”< 为了方便测试,上述代码利用库函数move将左值对象a转化为右值,用来触发移动构造函数。在构造对象b的过程中,程序并没有为b分配存储空间和复制数据的操作,而是直接把a的内容“窃取”出来。构造完毕之后,a已经没有任何资源了。 在教学方法上,举例法可以清晰地把移动语义讲清楚。通过上面的例子,学生可以非常清楚地看到对象a的资源是如何被对象b“窃取”的,而且学生也会体会到在执行的过程中,不需要分配存储空间和复制数据,程序的性能得到了明显的改善。 在学生掌握了移动构造函数之后,可采用任务驱动的授课方式讲解移动赋值运算符。即,先提问如何“窃取”=符号右侧对象的资源,然后分析任务需求,最后给出最终的实现。 Foo&operator=(Foo&&rhs){m_x=rhs.m_x;rhs.m_x=nullptr;return*this;} 多态性是面向对象程序设计的核心技术之一,它是通过虚函数的动态绑定来实现的,即在运行期间,根据基类指针或引用所绑定的对象来确定具体的行为。因此,触发动态绑定的前提是使用指针或引用。下面构造一个抽象基类和两个公有派生类: 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定义了一个虚接口,两个派生类分别定义了各自版本的实现。为了测试基于引用的动态绑定,设计如下测试函数: void test(const B&b){b.fun();} test函数形参为基类B的引用,函数体为一个成员fun函数的调用。测试代码如下: D1 d1;D2 d2; test(d1);//打印输出:fun of D1 test(d2);//打印输出:fun of D2 上面代码定义了两个派生类对象d1和d2。在调用test函数时,当基类引用形参b与一个派生类对象绑定时,便会调用相对应版本的fun函数,从而实现动态绑定。 在教学方法上,建议采用案例分析的方法把抽象的概念具体化,从而使学生能够深入地体会和理解动态绑定与多态性的概念。设计的案例要重点突出,精简扼要,抓住讲解内容的本质。 笔者运用上述教学方法,通过对比分析学生每学期进行的四次上机考核结果和课程设计的实训效果发现,学生逐渐加强了引用的使用,程序的运行效率得到了很大的改进。 围绕C++11新标准下引用知识点在教学过程中的难点和重点问题,研究和讨论了引用的本质及其四种不同的应用场合,针对具体教授知识点的不同特点,推荐了相应的教学方法。笔者运用上述教学方法,精心设计了相应的教学案例,通过近5年内的教学效果分析,上述工作对学生理解和使用引用具有重要作用。 [1]国际标准组织和国际电工委员会.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版).王刚,杨巨峰,译.北京:电子工业出版社,2013-09-01:471-472. [3]Michael Wong,IBM XL编译器中国开发团队.深入理解C++11:C++11新特性解析与应用[M].北京:机械工业出版社,2013-06-07:68-85. [4]祁宇.深入应用C++11:代码优化与工程级应用[M].北京:机械工业出版社,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++语言的一个重要特性,C++11新标准对引用进行拓展,引入右值引用。目前针对新标准下引用的教学方法还很少有专门论述。对C++新标准下引用进行深入分析,阐述使用引用的场合和方法,探讨有效的教学方法。教学效果表明,这些方法能够帮助学生深入理解和正确使用引用。 C++11;左值引用;右值引用;教学方法 国家自然科学基金面上项目(No.61673355)、湖北省“楚天学子”人才项目(No.162301132807)、中国地质大学(武汉)“腾飞计划”项目(No.G1323531750)、中国地质大学(武汉)研究生教育教学改革研究项目(No.YJG2017101) 李长河(1983-),男,河北秦皇岛人,副教授,博士生导师,英国莱斯特大学博士研究生学位,研究方向为智能优化与学习 2017-07-18 2017-09-14 C++11;L-value Reference;R-value Reference;Teaching Methods2.2 右值转化为左值
2.3 通用引用
3 使用引用
3.1 左值引用形参
3.2 返回左值引用
3.3 触发移动语义
3.4 触发动态绑定
4 结语