C++类和对象中的默认成员函数问题
默认成员函数一共有6个,本文就讲重点的四个,取地址重载就不说了,没用过,也不实用。
构造函数
首先,构造函数:作用就是用来初始化对象(要通过初始化列表的方式)
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。也就是说可以有多种初始化的方式。
这里有两个问题特别容易混淆
问:什么是默认构造函数?
答:我们自己不写,编译器自动生成的;我们自己写的无参的构造函数;我们自己写的全缺省的构造函数。一共三个,总结一下,就是不用参数就可以调用的构造函数。
一般情况,建议一个类最好给一个全缺省的构造函数。
再问:我们不写,编译器自动生成的构造函数有什么价值?
答:默认生成构造函数对基本类型的成员变量是不处理的,对于自定义类型,默认生成构造函数只能调用它的默认构造函数
后来,C++11对于上面这个基本类型不处理的小缺陷,给出了补充语法:声明缺省值,注意这里不是初始化!
class B
{
public:
B()
:_b(0)
{}
private:
int _b;
};
class A
{
private:
// 默认生成构造函数对基本类型成员变量是不处理的。
// 基本类型不处理,是C++语法设计的一个小缺陷
// 所以C++11,给出了补充语法:声明缺省值,来补足这里的缺陷
int _a1 = 1; // 这里不是初始化,这里是给缺省值
int _a2 = 2;
// 默认生成构造函数对自动类型的成员回去调用它的默认构造函数初始化
B _b ;
};
int main()
{
A aa;
return 0;
}
关于初始化列表这个问题也还是比较容易混淆,下面代码的第三句注释非常重要
class B
{
public:
/*B()
:_b(0)
{}
*/
B(int b = 0)
:_b(b)
{}
private:
int _b;
};
class D
{
public:
// D的构造函数中,在哪里调用的B的默认构造函数呢?
// 对象定义的时候自动调用构造函数,
// 调用构造函数的时候必须走一遍初始化列表
// 构造函数的初始化列表可以认为是成员变量定义初始化的地方
// 初始化列表,你显式的写或者不写,都会走一遍
D()
{}
//这里就去调用了B的默认构造函数
private:
int _d = 0; // 成员声明
B _b;
};
int main()
{
A aa;
D d; // 对象定义的时候,要调用它的构造函数来初始化
return 0;
}
同时还有几种类型必须在初始化列表初始化:const、引用、没有默认构造函数的成员
析构函数
这个比较简单
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
看下面代码的注释来理解吧,还是比较简单的
class Date
{
public:
//~Date()
//{
// // ... 这个类没有什么资源需要清理,
// // 所以其实它不写析构函数,编译器自动生成就可以
//}
private:
int _year = 0;
int _month = 1;
int _day = 1;
B _b = 10;
};
namespace yzy
{
class string
{
public:
string(const char* str)
{
_str=new char[strlen(str) + 1];
strcpy(_str, str);
}
~string()
{
cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
}
int main()
{
Date d; // 析构函数不是完成对象的销毁
// 这个对象是存在函数栈帧里面的,函数结束,栈帧销毁,对象就销毁了
// yzy::string s1("hello world");
// 针对s1有两块空间要销毁
// 第一:s1对象本身,他是函数结束,栈帧销毁,他就销毁
// 第二:s1里面的_str指向的堆上的空间,他是析构函数清理的
}
注意:成员变量有指针就要写析构函数这句话是错的!
默认生成的析构函数并不是什么事情都不做,内置类型不处理,自定义类型会去调用它的析构函数
拷贝构造函数
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
先看下面这段代码,我们没有写拷贝构造函数,
class Date
{
public:
private:
int _year = 0;
int _month = 1;
int _day = 1;
};
int main()
{
// 浅拷贝(值拷贝)的类
Date d1;
Date d2(d1);
Date d3 = d1;
return 0;
}
所以可以得出的结论是:我们不写,编译器默认生成拷贝构造函数,会对基本类型完成值拷贝(浅拷贝),对于自定义类型成员,会去调它的拷贝构造
对于string这种类,内含的自定义类型_str必须由我们自己来写默认拷贝构造函数,否则就会犯一块空间被析构两次的经典错误,因为不写的话就是浅拷贝,比如string s2(s1)这种语句,s2会和s1指向同一块空间。
下面的代码就写了自定义类型的拷贝构造函数,也就是自己实现了string的深拷贝
namespace yzy
{
class string
{
public:
string(const char* str="")
: _str(new char[strlen(str) + 1]);
{
strcpy(_str, str);
}
string(const char& s)
: _str(new char[strlen(s._str) + 1]);
{
strcpy(_str, s._str);
}
~string()
{
cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
}
赋值运算符的重载
赋值运算符的重载是已经定义出来的对象的赋值拷贝
默认生成的赋值运算符,特性和拷贝构造是一致的,也就是内置类型默认生成的operator=,会完成浅拷贝,自定义类型会调用它的operator赋值,比如string类中的_str,date类中的_b
下面就是string类的赋值运算符重载函数,比如执行s1=s3,思路就是要先释放掉s1对应的空间,再重新开一块比s3大1的空间,然后再strcpy
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
最后总结,其实上文并没有提这些函数的写法,比如引用啊,各种参数啊,包括this指针,下次可以专门更一期this指针加写法的博客