全面剖析构造函数
构造函数的概念构造函数的种类1 // 无参数构造函数2 // 一般构造函数(也称重载构造函数)3 // 复制构造函数(也称为拷贝构造函数)拷贝构造函数的特性[C/C++]构造函数初始化方式初始化列表
4 // 类型转换构造函数(根据一个指定的类型的对象创建一个本类的对象)explicit关键字的作用
等号运算符重载(区别拷贝构造函数)
– the End –
构造函数的概念
构造函数:名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
讲之前我们先引入一个日期类:
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day < } private: int _year; int _month; int _day; }; int main() { Date d1,d2; d1.SetDate(2018,5,1); d1.Display(); Date d2; d2.SetDate(2018,7,1); d2.Display(); return 0; } 对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 例如,以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译系统对象分配内存空间,并自动调用该构造函数->由构造函数完成成员的初始化工作 class Date { public: Date(int year = 0, int month = 1, int day = 1)// 构造函数 { _year = year; _month = month; _day = day; } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; 编译系统为对象的每个数据成员(_year _month _day)分配内存空间,并调用构造函数Date( )自动地初始化year,month,dar值分别为0 1 1 构造函数的种类 class Complex { private : double m_real; double m_imag; public: 1 // 无参数构造函数 // 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做 Complex(void) { m_real = 0.0; m_imag = 0.0; } 2 // 一般构造函数(也称重载构造函数) // 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理) // 例如:你还可以写一个 Complex( int num)的构造函数出来 // 创建对象时根据传入的参数不同调用不同的构造函数 Complex(double real, double imag) { m_real = real; m_imag = imag; } 3 // 复制构造函数(也称为拷贝构造函数) //复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中 Complex(const Complex & c) { // 将对象c中的数据成员值复制过来 m_real = c.m_real; m_imag = c.m_imag; } 拷贝构造函数的特性 一、拷贝构造函数是构造函数的一个重载形式 因为拷贝构造函数的函数名也与类名相同。 二、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用 要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用。 三、若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝 四、编译器自动生成的拷贝构造函数不能实现深拷贝 某些场景下浅拷贝并不能达到我们想要的效果。例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了: 举个例子,现有以下栈(Stack)类: class Stack { public: Stack(int capacity = 4) { _ps = (int*)malloc(sizeof(int)* capacity); _size = 0; _capacity = capacity; } void Print() { cout << _ps << endl;// 打印栈空间地址 } private: int* _ps; int _size; int _capacity; }; 我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。 int main() { Stack s1; s1.Print();// 打印s1栈空间的地址 Stack s2(s1);// 用已存在的对象s1创建对象s2 s2.Print();// 打印s2栈空间的地址 return 0; } 结果打印s1栈和s2栈空间的地址相同(因为s2栈是s1栈的浅拷贝,所以s2._ps和s1._ps两个指针储存的是同一个地址,即指向同一块内存空间),这就意味着,在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈 这是我们想要的效果吗?显然不是,我们希望在创建时,s2栈和s1栈中的数据是相同的,但是在创建完s2栈后,我们对s1栈和s2栈之间的任何操作能够互不影响,这种情况下编译器自动生成的拷贝构造函数就不能满足我们的要求了。 // 调用拷贝构造函数( 有下面两种调用方式) Complex c2= 5.2; // 类型转换构造函数,参看下面 Complex c5(c2); Complex c4 = c2; 注意和 = 运算符重载(= 运算符重载参看下面)区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2 //这一点特别重要,这儿是初始化,不是赋值。这两种方式都是要调用拷贝构造函数的。 既然说到初始化的问题,那就谈谈构造函数初始化的方式: [C/C++]构造函数初始化方式 (1)在声明成员变量的时候直接初始化,如果一个成员的初始值是确定的,在构造函数中不用去改变,那么可以直接在声明的时候就进行赋值。 (2)用初始化列表进行初始化:其实构造函数的初始化只能够在初始化列表中进行(如果没有显式的放在初始化列表,实际上也执行了默认的初始化),并且初始化列表是在构造函数的函数体之前执行的,因此如果不在里面初始化,而是在函数体初始化,实际上是浪费了性能了。另外,诸如引用类型的成员变量、const的成员变量以及一些无默认的构造函数的类对象,则必须在初始化列表中进行初始化,否则报错。 (3)最后“初始化”的地方就是构造函数体了,其实这里不是真正的初始化(因为初始化只能初始化一次,而构造函数体内可以进行多次赋值),因为到这里,都已经初始化完毕了,这里仅仅只是进行一些赋值操作,只不过看起来像是在初始化一样。 我们来详细介绍下初始化列表的方式: 初始化列表 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。 class Date { public: // 构造函数 Date(int year = 0, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; 注意事项: 1、每个成员变量在初始化列表中只能出现一次 因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。 2、类中包含以下成员,必须放在初始化列表进行初始化: 1.引用成员变量 引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。 int a = 10; int& b = a;// 创建时就初始化 2.const成员变量 被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。 const int a = 10;//correct 创建时就初始化 const int b;//error 创建时未初始化 3.自定义类型成员(该类没有默认构造函数) 若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。 在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数: 1.我们不写,编译器自动生成的构造函数。 2.无参的构造函数。 3.全缺省的构造函数。 3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。 对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,对于自定义类型,使用初始化列表可以提高代码的效率 //对于自定义类型,使用初始化列表可以提高代码的效率 class Time { public: Time(int hour = 0) { _hour = hour; } private: int _hour; }; class Test { public: // 使用初始化列表 Test(int hour) :_t(12)// 调用一次Time类的构造函数 {} private: Time _t; }; 对于以上代码,当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。 我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了: class Time { public: Time(int hour = 0) { _hour = hour; } private: int _hour; }; class Test { public: // 在构造函数体内初始化(不使用初始化列表) Test(int hour) { Time t(hour);// 调用一次Time类的构造函数 _t = t;// 调用一次Time类的赋值运算符重载函数 } private: Time _t; }; 这时,当我们要实例化一个Test类的对象时,在实例化过程中先调用了一次Time类的构造函数,又调用了一次Time类的赋值运算符重载函数,效率就降低了。 4、成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关 int i = 0; class Test { public: Test() :_b(i++) ,_a(i++) {} void Print() { cout << "_a:" << _a << endl; cout << "_b:" << _b << endl; } private: int _a; int _b; }; int main() { Test test; test.Print(); //打印结果test._a为0,test._b为1 return 0; } Test类构造函数的初始化列表中成员变量_b先初始化,成员变量_a后初始化,按道理打印结果test._a为1,test._b为0,但是初始化列表的初始化顺序是成员变量在类中声明次序,所以最终test._a为0,test._b为1。 ———————————————— 4 // 类型转换构造函数(根据一个指定的类型的对象创建一个本类的对象) //需要注意的一点是,这个其实就是一般的构造函数,但是对于出现这种单参数的构造函数,C++会默认将参数对应的类型转换为该类类型,有时候这种隐私的转换是我们所不想要的,所以需要使用**explicit**来限制这种转换。 // 例如:下面将根据一个double类型的对象创建了一个Complex对象 Complex(double r) { m_real = r; m_imag = 0.0; } explicit关键字的作用 构造函数对于单个参数的构造函数还支持隐式类型转换。 // 调用类型转换构造函数 Complex c2= 5.2; 在语法上,代码中 Complex c2= 5.2等价于以下两句代码: Complex tmp(5.2); //先构造 Complex c2(tmp); //再拷贝构造 在早期的编译器中,当编译器遇到 Complex c2= 5.2; 这句代码时,会先构造一个临时对象,再用临时对象拷贝构造c2;但是现在的编译器已经做了优化,当遇到 Complex c2= 5.2; 这句代码时,会按照Complex c2(5.2);处理,这就叫做隐式类型转换。 对于单参数的自定义类型来说, Complex c2= 5.2; 这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。 explicit Complex(double r) { m_real = r; m_imag = 0.0; } 等号运算符重载(区别拷贝构造函数) 具体细节讲之前大家可以先来了解一下运算符重载【《–戳这里】的概念: // 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建 // 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作 Complex& operator=(const Complex& rhs) { // 首先检测等号右边的是否就是左边的对象本身,若是本对象本身,则直接返回 if (this == &rhs) { return *this; } // 复制等号右边的成员到左边的对象中 this->m_real = rhs.m_real; this->m_imag = rhs.m_imag; // 把等号左边的对象再次传出 // 目的是为了支持连等 eg: a=b=c 系统首先运行 b=c // 系统首先运行 b=c 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象) return *this; } }; 最后来说一下类的6个默认成员函数 如果一个类中什么成员都没有,我们简称其为空类。 class Date {}; //空类 但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。 – the End – 以上就是我分享的【全面剖析构造函数(为什么+有什么+怎么用+相关知识拓展)】相关内容,感谢阅读! 关注作者,持续阅读作者的文章,学习更多知识! https://blog.csdn.net/weixin_53306029?spm=1001.2014.3001.5343 2022.1.13 ————————————————