对象更有用的玻璃罩——常引用

282 篇文章 34 订阅

原创案例讲解——”玻璃罩const”系列的三篇文章:

1. 使用常对象——为共用数据加装一个名为const的玻璃罩

2. 常(const)+ 对象 + 指针:玻璃罩到底保护哪一个

3. 对象更有用的玻璃罩——常引用


  本文讲在基于对象的程序设计中,函数中传递参数使用更广泛的技术,利用引用及常引用的话题。

  先从引用的作用开始谈起。


  一、引用用在参数传递中的优势:带回修改值及节省开支

  先从一个经典的例子开始。

  假如现在要交换两个整数,编写出的程序如下:

//程序1
#include <iostream>  
using namespace std;  
void swap(int x, int y); //用整形本身做形参

int main()
{
	int a=5,b=3;
	swap(a,b);    //函数调用时传值
	cout<<"a="<<a<<" , b="<<b<<endl;  //输出a=5,b=3,根本不能完成交换值
	system("pause");
	return 0;
}

void swap(int x,int y)
{
	int t;
	t=x;
	x=y;
	y=t;
}
  这个程序无法完成任务。因为在第9行调用swap()函数时,将实参a,b的值传递给了实参x,y,函数swap()在执行时,确实也交换了x和y。但是,任务最终交换a和b的要求却不能完成,前述的交换已经与a,b无关。

  在传统C语言中,可以利用指针实现在函数中改变实参所对应存储单元的值。

//程序2
#include <iostream>  
using namespace std;  
void swap(int *x, int *y);

int main()
{
	int a=5,b=3;
	swap(&a,&b); //将a和b的地址值传递给形式参数x和y
	cout<<"a="<<a<<" , b="<<b<<endl;  //将输出a=3, b=5,交换成功
	system("pause");
	return 0;
}

void swap(int *x,int *y) //x指向a,y指向b
{
	int t;
	t=*x;
	*x=*y;   //对*x的修改,即是对实参a的修改
	*y=t;    //对*y的修改,即是对实参b的修改
}
  在将实际参数a和b的地址值传递给形式参数x和y后,swap()函数中所做出的针对x和y所指向的单元的修改,改的就是a和b的值。函数调用完后,尽管x和y的生命周期结束,但“交换”的结果却留在了main()函数中。

  在C++中,引入了引用类型专门处理此类问题。

//程序3
#include <iostream>  
using namespace std;  
void swap(int &x, int &y);

int main()
{
	int a=5,b=3;
	swap(a,b); //a即是x,b即是y
	cout<<"a="<<a<<" , b="<<b<<endl;  //将输出a=3, b=5,交换成功
	system("pause");
	return 0;
}

void swap(int &x,int &y) //x即是a,y即是b
{
	int t;
	t=x;
	x=y;   //对x的修改,即是对实参a的修改
	y=t;    //对y的修改,即是对实参b的修改
}
  在第9行调用函数swap()时,按函数传值的特点和引用类型的含义,x与a共用存储单元,y与b共用存储单元,所以在执行函数swap时,对x和y的修改,就是对a和b的修改。函数调用完后,x和y的生命周期结束了,但a和b显然保存的是交换后的值。

  略做一个总结,可以发现程序3中的诸多优势。

  程序3与程序2相比,都能实现在函数中修改实参对应的值,但在实现中,不用意识到地址的存在,并且从调用的角度,swap(a,b)比swap(&a,&b)直观、简单的多,这会有效减少程序中在调用时可能犯的错误。程序2中需要两个存储地址值的单元x和y,而程序3中的x和y直接用的就是a和b的单元,从空间角度,节省了可能我们并不在意的一点点空间。

  程序3与程序1相比,两者在调用的形式上完全一样,函数体的写法完全一样,仅是函数原型中有些许差别。但是,程序3之伟大之一在于,可以在函数中对实参的值进行修改,而程序1却不行。另外一个显著区别在于,程序1在调用sway()时,要为形参分配存储单元,然后将实参的值写入,而程序3中x和y直接用的就是a和b的单元,不用分配空间,也不用花时间赋值。从本例中,程序1的空间多占用8个字节(x和y分别4字节),赋值要多占用一点点时间,这点空间和时间微不足道。但是,如果形参和实参是对象,并且数据成员比较多,尤其是某些成员是数组等占用空间很大时,引用的机制带来的在开支上的节约就不是可以轻言忽略了。

  综上,鉴于可以实现修改的功能,以及在空间、时间上的巨大优势,可以提高程序的执行效率。当函数参数中需要涉及对象(类)时,我们用引用类型。

  引用,专为对象而生!


  二、类对象的引用做形式参数

  看一个例子:

//程序4
#include <iostream>  
using namespace std;  
class  Test  
{  
private:   
	int x;   
	int y;  
public:  
	Test(int a, int b){x=a;y=b;}  
	void printxy() const;  
	void setX(int n) {x=n;}  
	void setY(int n) {y=n;}  
} ;  
void Test::printxy() const   
{  
	cout<<"x*y="<<x*y<<endl;  
}  

void doSomething(Test &p1)  //形参是引用类型 
{
	p1.setX(5); 
	p1.printxy( ); 
}

void main(void)  
{      
	Test t1(3,5);
	doSomething(t1); //实参是对象,t1和p1是同义词,p1占用的就是t1的地址
	t1.printxy(); 
	system("pause");  
} 
  可以知道,程序执行的结果是输出了两次:x*y=25。第一次的输出表明,在调用doSomething()函数中,t1的x数据成员被修改;而第2次的输出则说明,这个修改确实影响了实参t1,尽管随着程序调用结束,这种引用关系已经解除。

  这是一件令人感到快意的事。但,问题由此而生。如果在需求中,doSomething()函数确定为不允许修改t1,这种机制不正好成了bug的温床,当程序员无意中错误地写入了诸如第22行对数据成员修改的语句,这种错误将被编译器包庇下来。假如项目灰常大,那是令人抓狂的后果。

  为了限制这种无意的修改,我们想到了玻璃罩——const。


  三、用对象的常引用做形参

  所谓对象的常引用,就是将引用用const进行限定。自然,引用不能被修改。

  将对象说明为常引用形式是:

    const 类型名 &对象名;

  下面是用对象的常引用做形参的例子。

//程序5
#include <iostream>  
using namespace std;  
class  Test  
{  
private:   
	int x;   
	int y;  
public:  
	Test(int a, int b){x=a;y=b;}  
	void printxy() const;  
	void setX(int n) {x=n;}  
	void setY(int n) {y=n;}  
} ;  
void Test::printxy() const   
{  
	cout<<"x*y="<<x*y<<endl;  
}  

void doSomething(const Test &p1)  //形参是常引用 
{
	p1.setX(5);  //将招致错误——error C2662: “Test::setX”: 不能将“this”指针从“const Test”转换为“Test &”
	p1.printxy( ); 
}

void main(void)  
{      
	Test t1(3,5);
	doSomething(t1); //实参是对象
	system("pause");  
} 
  第22行在编译时会出现错误,这说明用常引用做形参可以避免函数中对对象的修改。无论实际参数是否为常对象,这种限制都是存在的。


  四、小结

  在对象做函数的形式参数时,用对象的引用做形式参数是一个直观且高效的处理方法,提倡用好。

  当不允许在函数体内对参数的值作出修改时,常将形式参数指定为const,这也是需要用好的C++的特性。


  五、玻璃罩系列总结

  我将const比喻为一个玻璃罩,由const决定的“常XX”的目标就是防止出现不该发生的对变量/对象的改变,“只许看不许摸”式的限制,成了一个有效的机制。

  下面将限制由严格到更灵活做个排列,也作为对系列文章的总结。

方法示例含义详解链接
使用常对象Test const t1;或
const Test  t1;
对象在初始化之后,在任何情况下都不能被修改。这是最严格的限制。
(整体任何时候、任何场合都不能改)
名为const的玻璃罩
类中定义常数据成员class Test{
   const int x;
   ……
}
Test类的任一对象,其数据成员x 均不能被修改;访问x的成员函数也必
须为常成员函数。
(对象中的某一特定部分在任何时候、任何场合都不能改)
名为const的玻璃罩
类中定义常成员函数class Test{
    int x;
   ……
   void f() const;
}
f 函数中,对本类的数据成员,可以访问,但不可以修改(在本函数之外,随便;但进了本函数所管辖范围,谁都不要改,无论是在其他场合可改变的非 const 对象,还是在其他场合也不能改变的const对象。f 的地盘 f 做主。)名为const的玻璃罩
函数参数中使用指向常对象
的指针作形式参数
void f(const Test *p1, Test *p2)通过*p1访问实参的对象,无论对象是否加了const 限定,都不可以被修改。而在同一个函数中,p2所指向的对象却是可以被修改的。
(在 f 的地盘上,根据声明区别对待,不该改的别该,该改的任你改。——民主社会的追求。 )
(用指针避免了函数调用中“大”对象的复制,可以提高效率。)
玻璃罩保护哪一个
函数参数中使用指向常引用
作形式参数
void f(const Test &t1, Test &t2)通过t1访问实参的对象,无论对象是否加了const 限定,都不可以被修改。而在同一个函数中,t2所引用的对象却是可以被修改的。
(在 f 的地盘上,根据声明区别对待,不该改的别该,该改的任你改。——这也是实现“民主”的一种途径。 )
(用引用除了可以避免了函数调用中“大”对象的复制,可以提高效率外,其调用形式更加直观。)
本文
  最后一种“函数参数中使用指向常引用作形式参数”是C++中提供的精华,应该提倡在需要的时候用好。《c++编程规范——101条规则、准则与最佳实践》中也强调:

  • 始终用const 限制所有指向只输入参数的指针和引用;
  • 优先按const 的引用取得其他用户定义类型的输入。

  在C++类库中对(成员)函数的处理也是遵循了这些原则。为进一步加强读者对此的映像,列举出C++标准类库中string类的成员函数的原型以供赏析,也以此作为本系列三篇文章的结束。以这些砖,引出读者更高质量的程序来。

//以下文档来自MSDN,可以看出其中对象的常引用作形式参数的应用有多么广泛。
//要看懂这些内容只需再结合一点“类模板”的有关知识即可,请读者自行解决。
namespace std {
//    TEMPLATE CLASSES
template<class E>
    struct char_traits;
struct char_traits<char>;
struct char_traits<wchar_t>;
template<class E,
    class T = char_traits<E>,
    class A = allocator<E> >
    class basic_string;
typedef basic_string<char> string;
typedef basic_string>wchar_t> wstring;
//    TEMPLATE FUNCTIONS
template<class E, class T, class A>
    basic_string<E, T, A> operator+(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    basic_string<E, T, A> operator+(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    basic_string<E, T, A> operator+(
        const basic_string<E, T, A>& lhs,
        E rhs);
template<class E, class T, class A>
    basic_string<E, T, A> operator+(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    basic_string<E, T, A> operator+(
        E lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator==(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator==(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator==(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator!=(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator!=(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator!=(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator<(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator<(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator<(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator>(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator>(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator>(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator<=(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator<=(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator<=(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator>=(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    bool operator>=(
        const basic_string<E, T, A>& lhs,
        const E *rhs);
template<class E, class T, class A>
    bool operator>=(
        const E *lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    void swap(
        const basic_string<E, T, A>& lhs,
        const basic_string<E, T, A>& rhs);
template<class E, class T, class A>
    basic_ostream<E>& operator<<(
        basic_ostream <E>& os,
        const basic_string<E, T, A>& str);
template<class E, class T, class A>
    basic_istream<E>& operator>>(
        basic_istream <E>& is,
        basic_string<E, T, A>& str);
template<class E, class T, class A>
    basic_istream<E, T>& getline(
        basic_istream <E, T>& is,
        basic_string<E, T, A>& str);
 template<class E, class T, class A>
    basic_istream<E, T>& getline(
        basic_istream <E, T>& is,
        basic_string<E, T, A>& str,
        E delim);
    };




(全文完)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迂者-贺利坚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值