为何将line构造函数参数改为const引用后,point拷贝构造仍被调用2次?
相关C++代码
class point { public: point(double x, double y) : x(x), y(y) { std::cout << "point parameterized constructor of: " << getThis() << std::endl; } point(const point& that) : x(that.x), y(that.y) { std::cout << "point copy constructor of: " << getThis() << std::endl; } ~point() { std::cout << "point destructor of: " << getThis() << std::endl; } private: double x; double y; point* getThis() { return this; } }; class line { public: line(const point& startPoint, const point& endPoint) : startPoint(startPoint), endPoint(endPoint) { std::cout << "line parameterized constructor: " << getThis() << std::endl; } ~line() { std::cout << "line destructor of: " << getThis() << std::endl; } private: point startPoint; point endPoint; line* getThis() { return this; } }; int main() { point p1(3.0, 4.0); point p2(5.0, 6.0); line l1(p1, p2); return 0; }
程序输出
point parameterized constructor of: 0x577fffc00 point parameterized constructor of: 0x577fffbe0 point copy constructor of: 0x577fffbb0 point copy constructor of: 0x577fffbc8 line parameterized constructor of: 0x577fffbb0 line destructor of: 0x577fffbb0 point destructor of: 0x577fffbc8 point destructor of: 0x577fffbb0 point destructor of: 0x577fffbe0 point destructor of: 0x577fffc00
背景情况
最初line的构造函数参数是值传递:
line(point startPoint, point endPoint) : startPoint(startPoint), endPoint(endPoint) {...}
此时Clang-Tidy给出警告:
Clang-Tidy: The parameter 'endPoint' is copied for each invocation but only used as a const reference; consider making it a const reference
值传递时程序输出里point的拷贝构造函数被调用4次,改成const引用参数后,拷贝构造调用次数减半为2次,但原本以为会完全消除拷贝构造调用,特此询问原因。
原因解析
核心问题出在line类的私有成员上:point startPoint和point endPoint是独立的成员对象,不是引用或指针。
当构造函数参数是const引用时,初始化列表里的startPoint(startPoint)本质是把引用指向的外部对象(main里的p1)拷贝到line自身的startPoint成员中,这必然会触发一次point的拷贝构造;同理endPoint(endPoint)是拷贝p2到line的endPoint成员,又触发一次拷贝构造——这就是输出里两次拷贝构造的来源。
而之前用值传递时,会发生两次额外的拷贝:调用line构造函数时,p1会先拷贝到函数参数startPoint,p2拷贝到参数endPoint;接着初始化成员时,又从这两个参数各拷贝一次到成员对象。总共2(参数拷贝)+2(成员初始化拷贝)=4次拷贝,这也是改成引用后次数减半的原因。
如何完全消除拷贝?
如果想彻底避免拷贝构造调用,有两种常见方案:
- 将
line的成员改为引用类型:
注意:必须保证被引用的对象(比如class line { public: line(const point& startPoint, const point& endPoint) : startPoint(startPoint), endPoint(endPoint) {} private: const point& startPoint; const point& endPoint; };main里的p1、p2)生命周期长于line对象,否则会出现悬空引用问题。 - 使用移动语义:如果
point实现了移动构造函数,当传入临时对象时可以用移动代替拷贝;如果传入命名对象,可通过std::move转移所有权,但这会让原对象进入有效但未定义的状态,需根据业务场景判断是否适用。
内容的提问来源于stack exchange,提问作者nyms1

