Good Part 传引用
场景:
class Student {
public:
string name;
string description;
uint32_t age;
Student (...) {...}
virtual print_type();
};
class MiddleSchoolStudent : public Student {
// 实现了一种print_type()
}
bool is_adult(Student student) {
if (student.age >= 18) {
student.print_type();
return true;
}
return false;
}
问题:
- 这里is_adult函数在调用时,传递了Student的值,底层操作会调用Student的拷贝构造函数,构造两个string对象。当is_adult函数生命周期结束,这个构造出来的student会被析构掉。造成了很大的开销,这些开销在这个函数里用传引用的方式是完全可以避免的。
- 如果is_adult(MiddleSchoolStudent(…)) 这么做,会产生切割问题,即调用的是 Student类的 print_type() 方法,因为这就是个Student对象。
一些解释:
注意,传引用Student& student,在这里我们不涉及对student内容的修改,因此我们最好的方式是加一个const Student&,即const引用来保证传进来的对象不会被修改。这么做有两个好处:
- 避免构造函数析构函数的资源消耗,提升代码效率。
- 避免出现切割问题,保持多态性
但是有些场景不适合,可以思考下为什么不适合:传内置类型(int, double …),STL的迭代器,函数对象(这个我暂时没遇到过)
Bad Part 返回引用
场景:
复用上面的声明
Student* make_a_stack_good_student(const Student& student) {
Student good_student(student);
return &good_student;
}
Student* make_a_heap_good_student(const Student& student) {
Student* good_student = new Student(student);
return good_student;
}
问题:
首先明确我们在Good Part里传引用的原因1是说减少构造zhan的开销,但是这两个方法都无法避免构造开销。而且会造成严重问题,上面的函数命名其实已经很好的的说明了问题所在:
- 栈上的对象会在方法生命周期结束后销毁。空指针(很不幸,我自己工作中就写过这样的Bug,就是为了避免返回值)
- 堆上的对象返回引用,外面根本不知道什么时候销毁,造成内存泄露的可能性非常大。
一些解释:
虽然书上没有写,但是其实有些地方是可以返回堆上的引用,可以在此讨论下:
- 构造的时候,比如把复杂的构造封装到一个函数里了,保证最后能被析构。
- 另外可能传入参数本身就是堆上的,那封装一些对它的操作再返回这个引用也是可以的,不过这种操作最好还是封装到对象本身。