C++经验(七)-- pass by value 和 pass by reference to const 的问题

缺省情况下,C++以 by value 方式传递参数。函数参数都是以实参的副本为初值,如果返回对象的话,返回的也是返回参数的一个副本。而这些副本都是由对象的copy构造函数产出。这也就使得pass by value的操作比较耗时。

class A
{
public:
    A(){}
    virtual ~A(){}

private:
    string m_name{ "" };
    string m_address{ "" };
}

class B : public A
{
public:
    B(){}
    virtual ~B(){}

private:
    string m_schoolName{ "" };
    string m_schoolAddress{ "" };   
}

假设下面的调用:

void display(B stu);

B stu;
display(stu);

上面的调用,会发生什么事。初始以stu对象为蓝本,在调用 display 函数的时候调用了对象 B 的 copy构造函数。同时,在函数 display 结束的时候,会调用一次其析构函数销毁拷贝的对象。

也就是说,这个时候已经调用了一次对象的拷贝构造和一次析构。

但不要忘记类B中有两个string类型的成员变量,也就是说,在调用其拷贝构造构造对象的时候,也会调用两次string类对象的拷贝构造。析构的时候也会析构两次。同时,因为类 B 继承了类 A,类A中也有两个string类型的成员。

也就说,整个display函数的调用过程中,总共发生了:

一次B类的拷贝构造和一次其析构函数,一次A类的构造和一次其析构函数,四次string类的拷贝构造和四次其析构。也就是 总过花费了六次构造和六次析构。这种时间消耗在 pass by value 的调用中是不可避免的。

但我们可以用其他的方式回避上面这些构造和析构的调用,pass by reference to const 这种调用就不会涉及到上面的这些调用。

void display(const B& stu);

这种方式使用首先不会有任何的构造和析构被调用,因为没有新对象产生,并且,将其申明为const类型,也就告诉了函数,这个入参是不能被修改的。函数内也就不会对传入的参数做任何修改。

同样的,再看下下面这个例子。

class A
{
public:
	A(string a) : m_data(a){}
	~A(){}

	virtual void display() const
	{
		cout << "this is A function..." << m_data << endl;	
	}
	
private:

	string m_data{ "" };	
};

class B : public A
{
public:
	B(string a) : A(a), m_data(a){}
	~B(){}
	
	virtual void display() const
	{
		cout << "this is B function..." << m_data << endl;	
	}
	
private:

	string m_data{ "" };	
};

下面的调用。最终会调用哪个成员呢?答案数会调用类 A 的成员。

因为当一个 derived class 的对象以 by value 方式传递并被视作是一个 base class对象时,base class 的拷贝构造函数会被调用,而其内部能够使得该对象像一个dervied class行为的属性会被切除掉。其实表现的完全是一个 base class 的行为。

void displayByValue(A w)
{
	w.display();
}

但是如果是下面的调用,则会避免这个问题。

void displayByReference(const A& w)
{
	w.display();
} 

通常来说,引用是通过指针实现的,也就是传递的通常是指针类型。因此 pass by value 的效率会比 pass by reference 的效率高,这也就是说,如果函数的参数是内置类型,比如int double,或者是stl的迭代器和函数对象,

使用 pass by vlaue 往往比 pass by reference 效率高。

值得注意的一点是,内置类型都比较小,但并不是说所有小型的对象传递都可以使用pass by value ,因为对象虽小,并不代表其 copy 构造函数的代价也会比较小。

一般而言,除了内置类型和stl的迭代器、函数对象使用 pass by value 之外,其他的用户自定义类型都尽量使用 pass by reference to const进行参数传递

值得注意的是,并不是说你知道了尽量使用 pass by reference to const 会提高效率,就想得到了尚方宝剑一样,可以随心所欲的一通错乱的胡砍,不管什么情况下都用这种方式,社会的黑暗肯定会给你两个大嘴巴子。

看下面这个例子。

class A
{
public:
	A(int m, int n){}

private:

	int m_m{ 0 };
	int m_n{ 0 };

friend const A operator* (const A& rs, const A& hs);
}

上面这个例子的乘积操作符是以 pass by value 的方式返回了一个对象(计算结果),刚好我们前面刚刚说过,以 pass by reference 的方式会降低对象的构造和析构的代价。所以我们会尝试一下。

A a(2, 4);
A b(3, 5);

A c = a * b;

假设我们的乘积运算符返回的是一个引用,那么这个对象是在什么时候被定义并存在的呢?调用乘积运算符之前吗?这显然不是我们期望的。那就只能是在调用成绩运算符的过程中了。

我们都知道,函数创建对象的方式有两种,在stack或者heap上创建。

const A& operator* (const A& rs, const A& ls)
{
	A a(rs.m_m * ls.m_m, rs.m_n * ls.m_n);
	return a;
}

上面的这个函数有什么问题?

  • 不可避免的还是调用了对象的构造和析构函数,开销并没有减少。
  • 返回的是一个local对象,而这个对象在函数返回之前就已经被销毁了。会造成未定义的行为错误。

基于上面的两个问题,我们因此尝试从heap上申请创建对象。

const A& operator* (const A& rs, const A& ls)
{
	A* a = new A(rs.m_m * ls.m_m, rs.m_n * ls.m_n);
	return *a;
}

同样的,这个函数还是会有一次构造函数的开销。

但同时存在着一个疑问,这个函数在 heap 创建的对象,该由谁去控制释放呢?如果不释放,肯定会造成内存泄漏。但就算调用者进行了正确并及时的释放,还是会存在一个问题。

A d = a * b * c;

上面的这个连续调用,调用者最后拿到的只有一个对象的指针,但是这个调用其实是调用了两次乘积运算符,也就是说,有了两次的内存申请。最终却只会析构一次。还是会造成内存泄漏。

const A& operator* (const A& rs, const A& ls)
{
	static A a;

	a.m_m = rs.m_m * ls.m_m;
	a.m_n = rs.m_n * ls.m_n;

	return a;
}

上面的这个调用我们返回了一个已经存在的静态变量。上面的这种调用方式确实是避免了任何构造函数的调用,但是这种方式会有多线程调用的风险。考虑下面这个方法:

bool operator== (const A& rs, const A& ls)
{
	return rs.m_m == ls.m_m && rs.m_n == ls.m_n;
}

下面这种调用:

A a, b, c, d;

if((a * b) == (c * d))
{
}
else
{
}

上面这种调用会出现什么问题? (a * b) == (c * d) 这个判断式总会被认作是true。不论 a, b, c, d 的值是什么。为什么呢?如果我们将上面的调用改写一下:

if(operator==(operator*(a, b), operator*(c, d)))
{
}

在调用 operator== 之前,就已经有两个 operator* 的调用了,并且这两个调用都返回了其内部的static对象。 所以这两个判断式是永远为true的。

所以,在必须要返回新对象的时候就让他返回新对象,不要试图修改并让他返回引用。让他正常返回新对象就好了,比如:

inline const A operator* (cosnt A& rs, const A& ls)
{
	return A(rs.m_m * ls.m_m, rs.m_n * ls.m_n);
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值