构造函数的种类:
-
默认构造函数
-
无参数构造函数
-
一般构造函数(重载构造函数)
-
复制(拷贝)构造函数
class Student
{
private:
int num;
int age;
string name;
public:
//当一个类没有定义任何构造函数时,系统会自动生成默认的无参构造函数。
//系统合成的默认构造函数自己定义的默认构造函数(无参数构造函数)的区别以及为什么在自己提供了其他构造函数的同时再提供一个默认构造函数(无参数构造函数)也总是对的
//原因:合成的默认构造函数无法初始化内置和复合类型的成员,如指针和数组。即默认构造函数非我们所期望,应该由自己明确定义构造函数,而非编译器完成。
// 一旦实现了的任何一种构造函数,系统就不会再自动生成这样一个默认构造函数。
//无参数构造函数
Student()
{
num = 0;
age = 0;
name = "zero";
}
// 一般构造函数(也称重载构造函数)
// 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
// 创建对象时根据传入的参数不同调用不同的构造函数
Student(int num_temp)
{
num= num_temp;
age= 20;
name = "zero";
}
Student(int num_temp, int age_temp, string name_temp)
{
num = num_temp;
age = age_temp;
name = name_temp;
}
//带有默认参数的构造函数(与上面三个构造函数不能共存,切记,切记)
Student(int num_temp = 0, int age_temp = 0, string name_temp = "zero")
{
num = num_temp;
age = age_temp;
name = name_temp;
}
//复制构造函数(也称为拷贝构造函数)
//复制构造函数的形式为X(X&) 函数名与类名必须一致,参数为类对象本身的引用
//若没有显示的复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因看下文。
//复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
Student(const Student & s)
{
num = s.num;
age = s.age;
name = s.name;
}
virtual ~Student(){}
};
int main()
{
// 调用了无参构造函数
Student s1,s2;
// 调用一般构造函数(当没有默认参数的构造函数)
Student s3(1);
Student s4(1,20,"张三");
//当有默认参数的构造函数,可传递任意个数(包括0)的参数去调用该构造函数。
// 调用拷贝构造函数( 有下面两种调用方式)
//复制初始化
Student s5(s4);
//赋值初始化
Student s6 = s4;
}
深拷贝和浅拷贝
- 浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。即浅拷贝只拷贝指针
- 两个指针指向同一块内存,任何一方的变动都会影响到另一方。
- 两个指针指向同一块内存,被析构2次,即delete同一块内存2次,造成程序崩溃。
- 深拷贝,相对于浅拷贝,不仅对象中的数据成员进行了赋值,同时还为动态成员开辟了新的空间。
class Student
{
private:
int num;
int age;
string name;
int gradenumber;
int *grade
public:
Student(int num_temp, int age_temp, string name_temp, int gradenumber)
{
num = num_temp;
age = age_temp;
name = name_temp;
grade - new int[gradenumber];
}
//浅拷贝,默认拷贝构造函数是浅拷贝
Student(const Student & s)
{
num = s.num;
age = s.age;
name = s.name;
grade = s.grade;
}
//深拷贝
Student(const Student & s)
{
num = s.num;
age = s.age;
name = s.name;
grade - new int[s.gradenumber];
for(int i = 0; i < gradenumber; i++)
{
grade[i] = s.grade[i];
}
}
virtual ~Student()
{
delete []grade ;
grade = NULL;
}
};
int main()
{
Student s4(01,20,"张三",5);
Student s5(s4);
Student s6 = s4;
return 0;
}
在什么情况下系统会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化。
class Student
{
private:
int num;
int age;
string name;
int gradenumber;
int *grade
public:
Student(int num_temp, int age_temp, string name_temp, int gradenumber)
{
num = num_temp;
age = age_temp;
name = name_temp;
grade - new int[gradenumber];
}
//浅拷贝,默认拷贝构造函数是浅拷贝
// Student(const Student & s)
// {
// num = s.num;
// age = s.age;
// name = s.name;
// grade = s.grade;
// }
//深拷贝
Student(const Student & s)
{
num = s.num;
age = s.age;
name = s.name;
grade - new int[s.gradenumber];
for(int i = 0; i < gradenumber; i++)
{
grade[i] = s.grade[i];
}
}
virtual ~Student()
{
delete []grade ;
grade = NULL;
}
};
//参数是对象,是值传递,会调用拷贝构造函数
int get_student(Student s) const
{
return s.num;
}
//返回值是对象类型,会调用拷贝构造函数。
Student get_student()
{
Student s(1, 20, "张三", 5);
return s;
}
int main()
{
//调用构造函数
Student s1(1, 20, "张三", 5);
//调用拷贝构造函数
Student s2 s2 (s1);
Student s3 s3 = s1;
//函数形参是类的对象,调用拷贝构造函数
int num = get_student(s1);
//函数返回值是类的对象,调用拷贝构造函数
Student s4 = get_student();
return 0;
}
结论:
- 对象不存在,且没用别的对象来初始化,就是调用了构造函数;
- 对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)
- 对象存在,用别的对象来给它赋值,就是赋值函数。
为什么用成员初始化列表会快一些
- 定义:初始化列表是一种C++初始化列表,有初始化阶段和计算阶段两个阶段。初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
- 初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。例如:
class Student
{
private:
int num;
int age;
public:
//构造函数初始化列表
Student():num(1),age(20)
{}
//构造函数内部赋值
Student(int num_temp, int age_temp)
{
num = num_temp;
age = age_temp;
}
};
- 用成员初始化列表会快一些的原因是:
- 对内置类型成员变量(指针,引用),两者性能差别不大。
- 对非内置类型成员变量(类类型),构造函数体内进行会使用两次构造函数,而成员初始化列表则只需要一次。
- 必须使用成员初始化列表的情况
- const成员或者引用类型的成员。因为const对象或者引用类型只能初始化,不能赋值。
- 需要初始化的数据成员是对象的情况,并且这个对象只有含参数的构造函数,没有无参数的构造函数。
- 派生类初始化基类的私有成员。构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用。
explicit关键字的作用
- 可以阻止不应该允许的经过转化构造函数进行的隐式转换的发生。
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
- effective c++中说:被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。我鼓励你遵循相同的政策。
本系列文章目的为个人准备面试的简单总结,文中多有不足,敬请批评指正!
参考:
https://blog.csdn.net/feitianxuxue/article/details/9275979
https://blog.csdn.net/zxc024000/article/details/51153743
https://blog.csdn.net/zzwdkxx/article/details/53409803
https://blog.csdn.net/sinat_20265495/article/details/53670644