1、构造函数精讲
- 构造函数可以重载
- 构造函数也可以通过参数表的差别化形成重载
- 重载的构造函数,通过构造函数的实参类型进行匹配
- 不同的构造函数,表示对象的不同创建方式
- 使用缺省参数可以减少构造函数重载的数量
class HumanString{
public:
/*HumanString(){
cout << "HumanString(1)" << endl;
m_age = 0;
m_name = "无名";
}
HumanString(int age){
cout << "HumanString(2)" << endl;
m_age = age;
m_name = "无名";
}*/
// 使用缺省参数可以减少构造函数重载的数量
HumanString(int age=0, const char* name="无名"){
cout << "HumanString(3)" << endl;
m_age = age;
m_name = name;
}
void getInfo(){
cout << "姓名:" << m_name << "年龄:" << m_age << endl;
}
private:
int m_age;
string m_name;
};
int main(){
HumanString h1;// 定义h1,利用h1.HumanString()
h1.getInfo();
HumanString h2(22);// 定义h2,利用h2.HumanString(22)
h2.getInfo();
HumanString h3(18,"张飞");// 定义h3,利用h3.HumanString(18,"张飞");
h3.getInfo();
return 0;
}
1.1 构造函数分类
- 多参构造函数:按多参方式构造
- 无参(缺省)构造函数:按无参方式构造
- 类型转换构造函数:利用不同类型的对象构造
- 拷贝构造函数:利用相同类型的对象构造
1.2 无参构造函数
- 无参构造函数亦称缺省构造函数,但其未必真的没有任何参数,为一个有参构造函数的每个参数都提供一个缺省值,同样可以达到无参构造函数的效果
- 如果一个类没有定义任何构造函数,那么编译器会为其提供一个无参构造函数
- 对基本类型的成员变量进行定义,并初始化为随机数
- 对类类型的成员变量进行定义,调用相应类型的无参构造函数
- 有时必须为一个类提供无参的构造函数,仅仅因为它可能作为另外一个类的类类型成员变量
注意:如果一个类定义了构造函数,无论这个构造函数是否带有参数,编译器都不会再为这个类再提供无参构造函数
class HumanString{
public:
HumanString(int age, const char* name="无名"){
// 在this指针所指向的内存空间中 定义m_age,初值为随机数
// 在this指针所指向的内存空间中,定义m_name,利用m_name.string()
cout << "HumanString(3)" << endl;
m_age = age;
m_name = name;
}
void getInfo(){
cout << "姓名:" << m_name << "年龄:" << m_age << endl;
}
private:
int m_age; // 基本类型成员变量
string m_name;// 类类型成员变量
};
1.3 拷贝构造函数
- 形如
class 类名 {
类名 (const 类名& that) {
…
}
};
- 用于,利用一个已定义的对象,来定义其同类型的副本对象,即对象克隆
- 如果一个类没有定义拷贝构造函数,那么编译器会为其提供一个默认拷贝构造函数
- 对基本类型成员变量进行定义,并赋初值(按字节复制)
- 对类类型成员变量进行定义,并调用相应类型的拷贝构造函数
- 如果自己定义了拷贝构造函数,编译器将不再提供默认拷贝构造函数,这时所有与成员复制有关的操作,都必须在自定义拷贝构造函数中自己编写代码完成
- 若默认拷贝构造函数不能满足要求,则需自己定义
class HumanString{
public:
HumanString(int age=0, const char* name="无名"){
cout << "HumanString(3)" << endl;
m_age = age;
m_name = name;
}
HumanString(const HumanString & that){ // 拷贝构造函数
cout << "HumanString的拷贝构造函数被调用了" << endl;
this->m_age = that.m_age;
this->m_name = that.m_name;
}
void getInfo(){
cout << "姓名:" << m_name << "年龄:" << m_age << endl;
}
private:
int m_age; // 基本类型成员变量
string m_name;// 类类型成员变量
};
int main(){
HumanString h3(18, "张飞");
h3.getInfo();
HumanString h4 = h3;// 这就会触发对象克隆
h4.getInfo();
return 0;
}
- 拷贝构造函数的调用时机
- 用已定义对象作为同类型对象的构造实参
- 以对象的形式向函数传递参数
- 从函数中返回对象
HumanString barHumanString(){
HumanString m;
return m;
}
void fooHumanString(HumanString hu){
}
int main(){
HumanString h1(18, "张飞");
//用已定义对象作为同类型对象的构造实参
HumanString h2 = h1;
h2.getInfo();
fooHumanString(h2);// -> 触发拷贝构造函数
HumanString h3 = barHumanString(); // ->会触发2次构造 -fno-elide-constructors
// 注意:某些拷贝构造过程会因编译优化而被省略
return 0;
}
所有编译器定义的构造函数,其访问控制属性均为公有(public)
1.4 拷贝赋值函数
- 形如
class 类名 {
类名& operator= (const 类名& that) {
…
}
};
用于一个已定义的对象给同类型的对象赋值,即对象赋值
- 如果一个类没有定义拷贝赋值函数,那么编译器会为其提供一个默认拷贝赋值函数
- 对基本类型成员变量,值传递(按字节复制)
- 对类类型成员变量,调用相应类型的拷贝赋值函数
- 如果自己定义了拷贝赋值函数,编译器将不再提供默认拷贝赋值函数,这时所有与成员复制有关的操作,都必须在自定义拷贝赋值函数中自己编写代码完成
- 若默认拷贝赋值函数不能满足要求时,则需自己定义
class HumanString{
public:
HumanString(const HumanString & that){ // 拷贝构造函数
cout << "HumanString的拷贝构造函数被调用了" << endl;
this->m_age = that.m_age;
this->m_name = that.m_name;
}
HumanString(int age=0, const char* name="无名"){
cout << "HumanString(3)" << endl;
m_age = age;
m_name = name;
}
// 拷贝赋值函数
HumanString& operator=(const HumanString& that){
// 对基本类型成员变量,值传递
this->m_age = that.m_age;
// 对类类型成员变量,调用相应类型的拷贝赋值函数
this->m_name = that.m_name;
return *this;
}
void getInfo(){
cout << "姓名:" << m_name << "年龄:" << m_age << endl;
}
private:
int m_age; // 基本类型成员变量
string m_name;// 类类型成员变量
};
int main(){
HumanString h1(18, "张飞");
HumanString h2 = h1;
h2.getInfo();
fooHumanString(h2);
HumanString h3 = barHumanString();
h3 = h1;// 调用拷贝赋值函数
h3.getInfo();
return 0;
}
1.5 类型转换构造
- 形如:
class 目标类型 {
目标类型 (const 源类型& src) {
…
}
};
- 用于
- 利用一个已定义的对象, 来定义另一个不同类型的对象
- 实现从源类型到目标类型的隐式类型转换的目的
注意:
- 通过explicit关键字,可以强制这种通过类型转换构造函数实现的类型转换必须通过静态转换显式地进行.
class Cat{
public:
Cat(const char* name) :m_name(name){
}
void talk(){
cout << m_name << ":喵喵喵" << endl;
}
private:
string m_name;
friend class Dog;// 友元声明
};
class Dog{
public:
Dog(const char* name) :m_name(name){}
/* explicit */ Dog(const Cat& c) :m_name(c.m_name){// 类型转换构造函数
cout << "Dog类的类型转换构造函数被调用" << endl;
}
void talk(){
cout << m_name << ":汪汪汪" << endl;
}
private:
string m_name;
};
int main(){
Cat smallwhite("小白");
smallwhite.talk();
// 利用一个已定义的对象, 来定义另一个不同类型的对象
Dog smallback(smallwhite); // 定义smallback,利用smallback.Dog(smallwhite)
// 实现从源类型到目标类型的隐式类型转换的目的
Dog bigDog = smallwhite; // 定义Dog类对象,利用匿名Dog类对象.Dog(smallwhite) 如果类型转换构造函数被explicit修饰,则这种写会报错
// Dog bigDog = static_cast<Dog> (smallwhite);// 如果类型转换构造函数被explicit修饰,则必须通过静态转换
smallback.talk();
bigDog.talk();
return 0;
}
2、析构函数
析构函数的函数名就是在类名前面加“~”,没有返回类型也没有参数,不能重载
- 调用时机:在销毁对象之前一刻自动被调用,且仅被调用一次
- 对象离开作用域
- delete操作符
- 作用:销毁对象的各个成员变量。
- 如果一个类没有定义析构函数,那么编译器会为其提供一个默认析构函数
- 对基本类型的成员变量,什么也不做
- 对类类型的成员变量,调用相应类型的析构函数
- 销毁对象的各个成员变量
class HumanString{
public:
HumanString(const HumanString & that):m_age(that.m_age),m_name(that.m_name){ // 拷贝构造函数
cout << "HumanString的拷贝构造函数被调用了" << endl;
}
HumanString(int age, const char* name="无名"):m_age(age),m_name(name){
cout << "HumanString(3)" << endl;
//m_age = age;
//m_name = name;
}
~HumanString(){
cout << "析构函数被调用" << endl;
// 对基本类型的成员变量m_age,什么也不做
// 对类类型的成员变量m_name,调用相应类型的析构函数,m_name.~string();
// 释放m_age/m_name本身所占内存空间
}
private:
int m_age; // 基本类型成员变量
string m_name;// 类类型成员变量
};
int main(){
HumanString h1;
HumanString h2(22);
return 0;
}// 右花括号的作用:(1)分别利用h1.~HumanString() h2.~HumanString()(2)释放h1\h2本身所占内存空间
- 对象的销毁过程
- 调用析构函数
- 执行析构函数的代码
- 调用成员变量的析构函数
- 释放对象各成员变量所占内存空间
- 释放整个对象所占用的内存空间
注意:
- 通常情况下,若对象在其生命周期的最终时刻,并不持有任何动态分配的资源,可以不定义析构函数
- 但若对象在其生命周期的最终时刻,持有动态资源则必须自己定义析构函数,释放对象所持有的动态资源
- 析构函数的功能并不局限在释放资源上,它可以执行我们希望在对象被释放之前执行的任何操作
class ABC{
public:
ABC() :m_p(new int), m_f(open("./cfg", O_CREAT | O_RDWR, 0644)){
// 定义了m_i,初值为随机数
// 定义了m_p 初值为指向一块堆内存
// 定义了m_f,初值为文件描述符--》文件表等内核结构(动态资源)
}
~ABC(){
// 持有动态资源则必须自己定义析构函数,释放对象所持有的动态资源
delete m_p;
m_p = NULL;
close(m_f);
// 释放m_i/m_p/m_f本身所占的内存空间
}
private:
int m_i;
int * m_p;
int m_f;
};
- 构造函数和析构函数的作用
构造函数的作用:定义对象的各个成员变量
析构函数的作用:销毁对象的各个成员变量
3、初始化表
- 通过在类的构造函数中使用初始化表,可以通知编译器该类的成员变量如何被初始化
- 类中的基本类型成员变量,最好在初始化表中显式指明如何初始化,否则初值不确定。
- 类中的类类型成员变量,也最好在初始化表中显式指明如何初始化,否则将调动相应类型的无参构造函数。
- 类的常量型和引用型成员变量,必须在初始化表中显式初始化。
- 类的成员变量按其在类中的声明顺序依次被初始化,而与其在初始化表中的顺序无关。
HumanString(int age, const char* name="无名"):m_age(age),m_name(name){// 构造函数
...
}
HumanString(const HumanString & that):m_age(that.m_age),m_name(that.m_name){ // 拷贝构造函数
...
}
4、深浅拷贝构造与拷贝赋值
-
如果类不提供拷贝构造和拷贝赋值编译器将提供默认的拷贝构造和拷贝赋值,而默认的拷贝构造和拷贝赋值函数,对于指针型成员变量都是只复制地址,而并不是复制地址指向的数据,这将导致浅拷贝问题。
-
为了获得完整意义上的对象副本,必须自己定义拷贝构造和拷贝赋值,针对指针型成员变量做深拷贝
-
相对于拷贝构造,拷贝赋值需要做更多的工作
避免自赋值、分配新资源、拷贝新内容、释放旧资源、返回自引用
class String{
public:
String(const char * psz = "") :m_psz(new char[strlen(psz) + 1]){
// 定义m_psz,初值为指向一块堆内存(动态资源)
strcpy(m_psz, psz);
}
~String(/*String * this*/){
// 释放m_psz本身所占的内存空间
delete[] this->m_psz;
}
/*
默认的拷贝构造
String(const String & that){
char*m_psz = that.m_psz; // 只复制了地址,并没有复制地址指向的数据(浅拷贝)
}
*/
// 深拷贝构造函数
String(const String & that) :m_psz(new char[strlen(that.m_psz) + 1]){
strcpy(m_psz, that.m_psz); // 没有复制地址,复制了数据(深拷贝)
}
/*
默认的拷贝赋值构造
String& operator=(const String & that){
this->m_psz = that.m_psz; // 只复制了地址,并没有复制地址指向的数据(浅拷贝)
}
*/
String& operator=(/*String* this*/const String & that){
if (this == &that){// 防止出现用户自己给自己赋值
}
else{
delete[] this->m_psz;// 编译器会先定义一个m_psz,并初始化为空串,所以需要先释放内存
this->m_psz = new char[strlen(that.m_psz) + 1];// 申请新资源
strcpy(m_psz, that.m_psz); // 拷贝新内容
}
return *this;// 返回自引用
}
char* c_str(){ return m_psz; }
private:
char * m_psz;
};
int main(){
String s1("hello");
cout << "s1" << s1.c_str() << ",s1 维护的堆内存地址" << (void *)s1.c_str() << endl;
String s2(s1);// 定义s2.String(s1)--》拷贝构造函数
cout << "s2" << s2.c_str() << ",s2 维护的堆内存地址" << (void *)s2.c_str() << endl;
String s3;
s3 = s2;
s3 = s3;// 自己给自己赋值
cout << "s3" << s3.c_str() << ",s3 维护的堆内存地址" << (void *)s3.c_str() << endl;
return 0;
}
- 总结:
- 无论是拷贝构造还是拷贝赋值,其默认实现对任何类型的指针成员都是简单地复制地址,因此应尽量避免使用指针型成员变量
- 出于具体原因的考虑,确实无法实现完整意义上的拷贝构造和拷贝赋值,可将它们私有化,以防误用
- 如果为一个类提供了自定义的拷贝构造函数,就没有理由不提供相同逻辑的拷贝赋值运算符函数