文章目录
一、空类不空
一般我们知道,当定义一个空的类时,如下:
class Empty {};
当C++编译之后,真正的类Empty就会变成如下形式:
class Empty { public: Empty(); // 缺省构造函数 ~Empty(); // 析构函数 Empty( const Empty& ); // 拷贝构造函数 Empty& operator=( const Empty& ); // 赋值运算符 Empty* operator&(); // 取址运算符 const Empty* operator&() const; // 取址运算符 const };
一般的书上都会介绍前面四种:默认构造函数,拷贝构造函数,默认赋值函数以及析构函数。后面的取地址运算符也是。(C++11又增加两个:移动构造函数、移动赋值运算符)需要注意的,只有当你需要用到这些函数的时候,编译器才会去定义它们。如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会别编译器创建。所有这些函数都是inline和public的。
我们发现后面三个是运算符重载,说到运算符重载,需要注意的是有五种运算符是不能重载的,分别是:.、?:、siezof、::、.*。
在这些默认函数中,有两个是我们现在需要讨论的:拷贝构造 和 赋值运算。
二、声明一个类
为了后面说明方便,先声明一个类,结构如下:
#ifndef _TEST__ #define _TEST__ #include <iostream> #include <sstream> class Test { public: Test(); // 默认构造函数 ~Test(); // 默认析构函数 Test(const Test &ex); // 拷贝构造函数,默认形式 Test( Test &ex); // 拷贝构造函数 //Test(Test ex); // 不合法,防止递归无限调用 Test& operator = (const Test& ex); // 赋值运算,默认形式 Test& operator = (Test& ex); // 赋值运作 //Test& operator = (const Test ex); // 赋值运算,合法,引用作为重载条件,调用是需要转型 //Test operator = (const Test& ex); // 赋值运算,合法,但是返回不能作为重载条件 Test& operator = (int k); // 赋值运算 // ... static Test func1(Test ex); static Test& func2(Test &ex); private: int key; std::string str; static int num; // 用于计数 }; #endif
三、什么时候调?
3.1、拷贝构造调用情景
1、当用类的一个对象初始化该类的另一个对象时。
Test aaa; Test ccc(aaa); // 拷贝构造,形式1 Test ddd = aaa; // 拷贝构造,形式2
2、一个对象作为函数参数,以值传递的方式传入函数体,进行形参和实参结合时。
// 声明 void func1(Test ex); void func2(Test& ex); // 调用 func1(aaa); // 拷贝构造,Test ex = aaa func2(aaa); // 不调用,Test& ex = aaa
3、一个对象作为函数返回值,以值传递的方式从函数返回,函数执行完成返回调用者时。
// 定义 Test func1() { Test res; return res; // 拷贝构造,Test tmp = res; tmp 为临时变量 } // 调用 ccc = func1(); // 临时变量tmp赋值给ccc, 同样,如果返回引用也不会调用
4、需要产生一个临时类对象时.
这一条没有理解,感觉说的还是第3中情况,没有想到其他例子。
3.2、赋值运算调用情景
除了定义引用、带等号的拷贝构造调用外,其他等号赋值运算调用的都是相应的赋值运算。
Test& d = a; // 定义引用,既不调用拷贝构造,也不调用赋值运算 Test b = a; // 拷贝构造,不调用赋值运算 // 其他,调用赋值运算 ccc = aaa; // 赋值运算 ccc = func1(); // 会调用赋值运算,func1返回的是Test对象或者引用
因为赋值运算实质还是函数调用,如果类重载多重赋值运算的话,具体调用哪个是根据参数决定的。
四、定义原则和注意事项?
4.1、拷贝构造函数-定义原则
1、定义一般定义形式遵从默认的格式:
<ClassName>::<ClassName>(const<ClassName>&<ref_name>) // 例如: Test(const Test &ex);
C++ Primer中给出的定义是:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
2、函数体
拷贝构造函数和赋值函数的功能是相同的,为了不造成重复代码,拷贝构造函数可以实现如下:
Test::Test(const Test& ex) { *this=ex; //调用重载后的"=" } //说明: 参见Effective C++ 第60页(条款12的结尾)
这部分不同人也有不同观点,和具体的=重载实现有关,特别是涉及到内存释放等的。个人建议还是按照自己需求来实现。
4.2、拷贝构造函数-注意事项
1、参数为引用,防止无限递归;
2、关于重载:
构造函数可以重载,如重载 Test(const Test& a, int b) {},但是这种形式就不太符合拷贝构造函数的基本形式了,从这种意义上来说,也就不算是拷贝构造函数了。但是带不带const或volatile是可以算作是重载的,一般定义建议采用const的默认版本。所以说拷贝构造函数有带const和不带等形式。
Test(Test &ex); // 非const的拷贝构造函数 Test(const Test &ex); // const的拷贝构造函数,默认形式 Test(volatile Test &ex);// volatile的拷贝构造函数 // ...
3、对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数,因为默认的拷贝构造是浅拷贝。
4.3、赋值运算-定义原则
赋值运算重载的形式有很多种,根据不同的需求可自行定义,但是重载之间一定要注意重载的规则,比如返回值,引用不能作为区分。类似的面试中经常会出现字符串赋值运算符的实现:
String& String::operator = (const String &str) { if(this == &str) { return *this; } delete []m_pData; m_pData = NULL; m_pData = new char[strlen(str.m_pData) + 1]; strcpy(m_pData, str.m_pData); return *this; }
参考4.1中提到的,对应的拷贝构造就不能使用这种赋值运算来实现,而应该实现如下:
String::String(const String &str) { int length = strlen(str.m_pData); m_pData = new char[length + 1]; strcpy(m_pData, str.m_pData); }
4.4、赋值运算-注意事项
1、首先判断是否是自身;
2、参数虽然可以自己定义,但是建议采用引用类型,减少拷贝构造函数的调用;
3、参数建议采用const版本,因为赋值运算不会改变=号右边的对象;
4、返回值,建议返回引用,返回时 return *this;
一方面,不是引用会多调用一次拷贝构造;
另一方面,返回对象值是不能做左值,返回引用可以做左值。
5、和拷贝构造类似,凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数,因为默认的是浅拷贝。
五、派生问题:C++智能指针
我们熟悉的智能指针实现中,拷贝构造和重载运算起到很重要的作用。其中重要的一点是:
拷贝构造会将引用计数加一;
赋值会将=号左边引用计数减一,如果计数等于0就会释放,=号右边引用计数加一;
六、实例
借助上面定义的Test类,我们做如下调用:
int Test::num = 0; int main(int argc, char const *argv[]) { // 默认构造 std::cout<<"Test aaa;"<<std::endl; Test aaa; std::cout<<"const Test bbb;"<<std::endl; const Test bbb; // 拷贝构造 std::cout<<"Test ccc(aaa);"<<std::endl; Test ccc(aaa); std::cout<<"Test ddd = bbb;"<<std::endl; Test ddd = bbb; // 赋值运算 std::cout<<"ddd = aaa;"<<std::endl; ddd = aaa; std::cout<<"ddd = bbb;"<<std::endl; ddd = bbb; std::cout<<"ddd = 10;"<<std::endl; ddd = 10; // 拷贝构造和赋值运算结合 std::cout<<"ddd=Test::func1(aaa);"<<std::endl; ddd=Test::func1(aaa); // 参数和返回相当于Test ex = aaa; std::cout<<"ddd=Test::func2(aaa);"<<std::endl; ddd=Test::func2(aaa); // 参数和返回相当于Test &ex = aaa; 没有拷贝构造,也没有赋值 std::cout<<"Test eee = Test::func2(aaa);"<<std::endl; Test eee = Test::func2(aaa); std::cout<<"return 0"<<std::endl; return 0; }
运行结果如下:
注解:因为没有找到合适的方法描述整个对象的转变过程,只记录了拷贝构造,没有记录赋值运算。
1的产生路线其实是原始的对象ddd:
Test ddd = bbb; // key = 4, str ="4<=2"; ddd = aaa; // key = 1, str = "1"; ddd = bbb; // key = 2, str = "2"; ddd = 10; // key = 10, str = "10"; ddd = Test::func1(aaa); // key = 1, str ="6<=5<=1"; [中间有6形参过渡] ddd = Test::func2(aaa); // key = 1, str = "1"; [中间没有形参过渡,这也验证了引用传参不会拷贝构造]
此处这样是为了方便对比,如果把后两条位置对换,就可以得到更清晰的结果:
附录完整代码:拷贝构造与引用传参.txt
参考
1、http://blog.sina.com.cn/s/blog_5f76aaf20100cwlj.html
2、http://www.cnblogs.com/hnrainll/archive/2011/05/17/2048620.html
3、http://blog.csdn.net/tunsanty/article/details/4264738
4、http://blog.csdn.net/stand1210/article/details/52547312
5、http://blog.csdn.net/prstaxy/article/details/22990223
6、http://rsljdkt.iteye.com/blog/770072
转载标明出处:https://blog.evanxia.com/2016/09/1061