之前我们对C++的类和对象已经入门了,今天我们来看看类和对象中各种个样的默认函数。
一个类就算里面什么也没有,里面也有6个默认的成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数
对于Date类:
class Date
{
public:
void Init()
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date S;
S.Init();
}
我们可以调用Init()函数,进行初始化。但这会有一个问题:每次都要调用Init()函数,十分麻烦。
我们可不可以在对象实例化的时候,就完成对象的初始化呢?
答案是可以,这就是构造函数的作用。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
它有如下特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
所以我们就不用Init()函数了:
class Date
{
public:
Date()//无参构造函数
{
cout << "Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date S;
}
我们来看看会不会自动调用:
还有第五点:
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成
我们来看看:
class Date
{
public:
//Date()//无参构造函数
//{
// cout << "Date()" << endl;
//}
//Date(int year, int month, int day)
//{
//}
void Print()
{
cout << _year << "\ " << _month << "\ " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
我们把带参的构造函数放开:
class Date
{
public:
//Date()//无参构造函数
//{
// cout << "Date()" << endl;
//}
Date(int year, int month, int day)
{
}
void Print()
{
cout << _year << "\ " << _month << "\ " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
这是因为我们显示构造了之后,编译器就不会自动生成默认构造函数,编译器就会报错。
但是还有一个问题,编译器自动生成的默认构造函数,虽然可以编译通过但好像并没有对成员变量进行初始化。
这就涉及到第六点了:
6.:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,
编译器生成默认的构造函数会对自定类型成员进行初始化,但是内置类型不会进行处理。
下面这段代码也证明了这一点:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
我们看到了,编译器就只对我们自定义的Time _t进行默认构造函数初始化。
但是:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
我们来看第七点:
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
所以下面这段代码会有问题:
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
总结一下
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。- :C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,
编译器生成默认的构造函数会对自定类型成员进行初始化,但是内置类型不会进行处理。
(C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。)- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
析构函数
我们用构造函数来初始化对象,如果我们要释放对象资源,我们就要用一个新的函数:析构函数。
我们先来看看析构函数的规则:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载。- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
~Date()//Date的析构函数
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
}
- 编译器自动生成的析构函数,,会对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
我们来看看第六点:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
有资源申请时,一定要写,否则会造成资源泄漏。
比如我们经常写栈,经常会用malloc,realloc开辟空间,像这样的类一定就要自己写析构函数。
再来总结一下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载。- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 编译器自动生成的析构函数,,会对自定类型成员调用它的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
有资源申请时,一定要写,否则会造成资源泄漏。- 析构遵守先进后出,(最先创建的对象最后销毁)
拷贝函数
我们经常会有这种情况:建立好了一个对象,要把这个对象赋值拷贝一个全新的对象。
在函数传参时我们经常会说:形参是实参的一份临时拷贝。就和上面的代码原理类似。
但因为在类中我们引入了析构函数,在生命周期结束时会自动调用释放资源,函数的传参就会出现问题。
class Stack
{
public:
Stack(size_t n = 4)
{
if (n == 0)
{
_data = nullptr;
_top = _capacity = 0;
}
else
{
_data = (int*)malloc(sizeof(int) * n);
if (_data == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
~Stack()
{
free(_data);
_data = nullptr;
_top = _capacity = 0;
}
private:
int* _data;
int _top;
int _capacity;
};
void Print(Stack S)
{
}
int main()
{
Stack S1;
Print(S1);
}
这段代码似乎好像没什么问题,但是:
这是为什么呢?首先我们要知道一点实参和形参都会调用析构函数,这个代码出现问题的地方就是开辟的动态数组_data。
我们把实参和形参的地址对比一下:
我们发现实参数组的地址和形参数组的地址都是一样的,意思就是析构函数会对同一地址的数组进行两次释放。这是非常危险的,为了解决这样问题,C++创建了拷贝函数的概念:
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
来看一下它的特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用(用指针其实也可以),使用传值方式编译器直接报错,因为会引发无穷递归调用
class Stack
{
public:
Stack(size_t n = 4)
{
if (n == 0)
{
_data = nullptr;
_top = _capacity = 0;
}
else
{
_data = (int*)malloc(sizeof(int) * n);
if (_data == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
Stack(const Stack& S)//拷贝构造函数
{
_data = (int*)malloc(sizeof(int) * S._capacity);
if (_data == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = S._top;
_capacity = S._capacity;
memcpy(_data, S._data, sizeof(int)*S._capacity);
}
~Stack()
{
free(_data);
_data = nullptr;
_top = _capacity = 0;
}
private:
int* _data;
int _top;
int _capacity;
};
void Print(Stack S)
{
}
int main()
{
Stack S1;
Print(S1);
}
我们此时再来看实参数组的地址和形参数组的地址:
实参和形参数组的地址不一样了,解决了二次释放的问题。
以后凡是要传参的地方都会先调用拷贝构造函数。
如果拷贝构造函数的参数不是引用:
编译器会直接报错,这是因为:
会无穷无尽的递归,造成栈溢出。
有一个问题,如果我们不写拷贝构造函数,那传参的时候还会拷贝构造吗?
答案是是的,但只会进行最简单的浅拷贝(只负责把数据的表面值传过来),如果像是栈这样的传参,我们自己要写拷贝构造函数将实参的地址和形参的地址区分开来,这样的拷贝叫做深拷贝。
运算符重载
我们都知道 +,可以表示两个数的相加,无论是整形,浮点形,**+**都可以直接使用让两个数相加。
但现在我想让 +号可以让两个日期类相加。
这就涉及到了 运算符重载 了,让符号可以为更多的类型服务。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
这时候 == 就可以用来判断两个日期类是否相等了。
const修饰的成员函数
我们一般写程序的时候对象都是一般的对象:
class Date
{
public:
Date(int year=2023, int month=23, int day=12)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date H;//一个普通的对象
H.Print();
}
但有时候为了保护我们的对象我们会在对象前面加上const防止对象被修改。
但这时候程序会报错,因为H这时候是一个const对象,const的H不能调用非const的Print()函数。 为了可以让Print()变成const成员:
这时候H就可以调用Print()函数了,**此时大家都是const,权限平移。我们之前谈到过权限可以缩小平移,但不能放大。**意思就是:
非const对象可以调用const成员函数。(权限的缩小)
const对象可以调用const成员函数。(权限的平移)
const对象不可以调用非const成员函数。(权限的放大)
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!