一、构造函数的分类及调用
1.构造函数的分类
(1)类的构造函数按照形参分类,可以分为无参构造函数和有参构造函数。
(2)按照类型分类,可以分为普通构造函数和拷贝构造函数。
普通拷贝构造函数可以根据形参的不同进行构造函数重载。但是析构函数不行,析构函数要求不能有形参,所以析构函数不能重载,而且必须没有形参。拷贝构造函数又分为浅拷贝和深拷贝。这两点在下面第四大点单独整理。
2.调用不同的构造函数对应的写法
写个学生类。注意,一下构造函数的成员变量初始化方式为初始化列表。格式为:
构造函数(形参列表):属性1(形参1),属性2(形参2){}
而且属性一和二位置不重要,如下面的第一个带参构造函数。
class Student
{
public:
Student()
{
cout << "this is Student()" << endl;
}
Student(int i, string na) :name(na), id(i)
{
cout << "this is Student(int i, string na)" << endl;
}
Student(Student& stu) :id(stu.id), name(stu.name)
{
cout << "this is Student(Student& stu)" << endl;
}
Student(int i) :id(i)
{
cout << "this is Student(int i)" << endl;
}
private:
int id;
string name;
};
上面有三个构造函数。一个是缺省构造函数,一个是普通构造函数,还一个是普通拷贝构造函数,还一个是只有一个参数的构造函数。注意拷贝构造函数的形参必须是引用形式,否则编译通不过。原因是因为如果我们写成普通形式比如:
Student(Student stu){}
那么调用拷贝构造函数的时候比如:
Student stu1(100,“xiaomeng”);
Student stu2(stu1);
我们知道下一个语句是调用拷贝构造函数,stu1是参数,stu2是要构造出来的对象,然而拷贝构造函数是Student(Student stu)这种形式,那么我们知道传入实参后,将会根据传入的实参构造这个形参,而这种构造依然是Student(Student stu)拷贝构造,那么这将会进入死循环,或者说函数在自己形参里面调用自己行不通,不可能允许你这样的调用。所以拷贝构造函数要求必须是引用形式。
如下调用构造函数的写法:
int main()
{
Student stu1;
Student(stu11);
Student stu2(100, "xiaomeng");
Student stu3(stu2);
Student();
Student(200, "xiaohua");
//Student stu4 = Student(300, "xiaomeng");//两个构造函数符合
Student stu5(400);
Student stu6 = (500);
Student stu7 = 700;//等价于->Student stu7 = Student(700)
Student stu8 = stu2;
Student stu9 { 800,"xiaomeng" };
Student stu10 = { 800,"xiaomeng" };
return 0;
}
运行结果:
只要没有多个构造函数与之匹配,其实调用构造函数有很多写法。怎么写凭自己喜好,但是一定得知道自己是调用哪个构造函数。
注意,调用默认构造函数的时候不要加括号,即如下写法:
Student stu();
这种写法是不对的。编译器会将这个看作函数声明。Student是返回类型,stu是函数名,()是函数形参列表的括号。
还有就是注意下匿名对象,即没有对象名称,只有参数。如
Student (10,“xiaomeng”);调用了Student(int id, string name)构造哈函数,但是没有对象名,这个就是匿名函数,匿名函数的声明周期只有这一句代码,这句代码过后,就立即析构。如下:
class Student
{
public:
Student(int i, string na) :name(na), id(i)
{
cout << "this is Student(int i, string na)" << endl;
}
~Student()
{
cout << "this is ~Student()" << endl;
}
private:
int id;
string name;
};
int main()
{
Student(100, "xiaomeng");
cout << "********************" << endl;
return 0;
}
运行结果:
发现Student (100,“xiaomeng”)这句代码执行完就析构掉这个匿名对象了,因为匿名对象没有对象名,后面没办法使用,所以生命周期只有这一句代码。接着才执行下面输出语句。
二、构造函数的调用规则
1.默认情况下,C++编译器至少给一个类添加三个函数
(1)默认构造函数
(2)默认析构函数
(3)默认拷贝构造函数
2.三个默认函数生成规则
1.如果用户定义有参构造函数,C++不提供默认无参构造函数,但是提供默认拷贝构造函数。
2.如果用户定义拷贝构造函数,C++不提供其他任何构造函数
总结一下就是,只要我们写了构造函数,那么系统就不会生成默认缺省构造函数。如果我们写了拷贝构造函数,那么编译器不仅不生成默认的缺省构造函数,甚至连拷贝构造函数也不会生成。
如下,我们定义一个普通构造函数,系统不生产默认缺省构造函数了,但是生成默认拷贝构造函数。
class Student
{
public:
Student(int i, string na) :name(na), id(i)
{
cout << "this is Student(int i, string na)" << endl;
}
~Student()
{
cout << "this is ~Student()" << endl;
}
private:
int id;
string name;
};
int main()
{
Student stu1(100, "xiaomeng");
//Student stu2;//需要调用默认缺省构造函数,这里不生成error
Student stu2(stu1);//调用系统默认的拷贝构造函数
return 0;
}
我们只定义一个拷贝构造函数,那么系统将不会生成默认缺省构造函数和默认拷贝构造函数。当然如果我们只写一个构造函数,而且还是拷贝构造函数,那么我们将无法创建第一个对象,所以当我们写默认构造函数必须得加上至少其他任意一个构造函数才可以创建对象。第一个对象都创建不了,拿什么拷贝构造。
三、拷贝构造函数调用时机
1.使用一个创建完毕的对象初始化一个对象
如下代码:
class Student
{
public:
Student(int i, string na) :id(i), name(na)
{
cout << "this is Student(int i,string na)" << endl;
}
Student(Student& stu)
{
cout << "this is Student(Student& stu)" << endl;
}
private:
int id;
string name;
};
int main()
{
Student stu1(100,"xiaomeng");
Student stu2(stu1); //调用拷贝构造
return 0;
}
2.值传递的方式给函数参数传值
如下代码:
class Student
{
public:
Student(int i, string na) :id(i), name(na)
{
cout << "this is Student(int i,string na)" << endl;
}
Student(Student& stu)
{
cout << "this is Student(Student& stu)" << endl;
}
private:
int id;
string name;
};
void work(Student stu)
{
return;
}
int main()
{
Student stu1(100,"xiaomeng");
work(stu1);
return 0;
}
3.值方式返回局部对象
如下代码:
class Student
{
public:
Student(int i, string na) :id(i), name(na)
{
cout << "this is Student(int i,string na)" << endl;
}
Student(Student& stu)
{
cout << "this is Student(Student& stu)" << endl;
}
private:
int id;
string name;
};
Student work()
{
Student stu(100,"xiaomeng");
return stu;
}
int main()
{
Student stu1(100,"xiaomeng");
work();
return 0;
}
第二次调用构造函数是work()函数体内第一句代码,第三次调用构造函数(拷贝构造函数)是work()函数体内return语句的时候。
四、深拷贝与浅拷贝
1.浅拷贝
浅拷贝是层次较浅,我们上面代码看不出来浅拷贝和深拷贝,我们举下面这个代码:
class Student
{
public:
Student(int* i, string na) : name(na)
{
id = i;
cout << "this is Student(int i,string na)" << endl;
}
Student(Student& stu)
{
id = stu.id;
name = stu.name;
cout << "this is Student(Student& stu)" << endl;
}
~Student()
{
if (id != nullptr)
{
delete id;
}
cout << "this is ~Student()" << endl;
}
private:
int* id;
string name;
};
int main()
{
int* a = new int(100);
Student stu1(a,"xiaomeng");
Student stu2(stu1);
return 0;
}
发现运行崩溃了,两个构造函数没问题,有一个对象析构的时候出问题了(注意对象是创建在栈空间上,所以后创建的对象先析构)。为什么出问题呢?原因出现在下面这些代码上:
Student(Student& stu)
{
id = stu.id;
name = stu.name;
cout << "this is Student(Student& stu)" << endl;
}
~Student()
{
if (id != nullptr)
{
delete id;
}
cout << "this is ~Student()" << endl;
}
拷贝构造函数仅仅是将需要创建的对象的id指针指向传入的对象的id指针指向的空间,也就是两个对象的id指针指向了同一个空间。两个对象两次析构,但是堆空间只有一个,所以出现了对同一个堆空间释放两次的问题,导致系统崩溃。这就是浅拷贝,也叫做按字节拷贝。id拷贝的时候,只是将stu.id里面存放的地址值拷贝到了this->id中,导致两个对象的id指向同一个堆空间。
2.深拷贝
深拷贝是如果对象数据涉及申请堆空间,那么就申请新空间,将一个对象的堆空间的值拷贝到需要创建的对象的新空间中。而不仅仅是用成员指针指向同一个堆空间。即不是简单按字节拷贝,而是有堆空间需要拷贝就申请堆空间,将堆空间的值进行拷贝。如下代码:
class Student
{
public:
Student(int* i, string na) : name(na)
{
id = i;
cout << "this is Student(int i,string na)" << endl;
}
Student(Student& stu)
{
id = new int();
*id = *stu.id;
name = stu.name;
cout << "this is Student(Student& stu)" << endl;
}
~Student()
{
if (id != nullptr)
{
delete id;
}
cout << "this is ~Student()" << endl;
}
private:
int* id;
string name;
};
int main()
{
int* a = new int(100);
Student stu1(a,"xiaomeng");
Student stu2(stu1);
return 0;
}
注意修改代码的地方:
Student(Student& stu)
{
id = new int();
*id = *stu.id;
name = stu.name;
cout << "this is Student(Student& stu)" << endl;
}
仅仅修改了拷贝构造函数,里面给需要创建的对象的id申请了一个堆空间,将stu.id堆空间的值拷贝到this->id堆空间中。每个对象的id都有自己的堆空间,这样调用析构函数的时候,就不会出现对一个堆空间释放两次而导致程序崩溃的情况了。
运行结果: