继上篇,今天带来类与对象(中)的文章
C++中,类里面会形成6个默认成员函数。当用户不去显式实现时,编译器会自动生成的成员函数,被称作默认成员函数。 接下来,一个个介绍。
在之前写线性表,栈实现时,我们都会用到Init和Destory的函数。C++中,出于简化和防止忘记,引出了构造函数和析构函数,对应实现Init和Destory的效果。
构造函数
构造函数的名字与类名相同,创建类的类型对象时由编译器自动调用。构造函数的任务是负责初始化对象,并不会去开空间创建对象。
接着聊聊构造函数的特性
先说基本的:①. 构造函数的函数名与类名相同 | ②.无返回值 | ③.对象实例化时,自动调用构造函数
④.构造函数也可以重载
⑤默认构造函数分为未被人为定义、编译器默认生成的构造函数和人为定义的 全缺省构造函数和无参构造函数。
⑥.C++把类型分为内置类型和自定义类型。int,char,double…就是内置类型,struct,union就是自定义类型。编译器生成的默认构造函数会对自定义类型调用其默认成员函数。
⑦.内置类型可以给初始值
class Time
{
public:
//无参构造函数
Time()
{
}
//带参构造函数
Time(const int hour,const int minute,const int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
//全缺省构造函数
Time(const int hour = 1, const int minute = 1, const int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
//7.内置类型也可以在声明时给默认值
// private:
// int _hour = 1;
// int _minute = 1;
// int _second = 1;
};
//4.这样我们就实现了3种不同的初始化方式,当然不写的话编译器也会自动去生成默认的构造函数
int main()
{
Time t;
return 0;
}
析构函数
构造函数是用来初始化的,析构函数并不是用来对对象本身进行销毁的。在对象被销毁时自动调用析构函数,来对对象中的资源进行清理工作,譬如动态开辟的内容进行销毁。
接下来,说说析构函数的特性
①构造的形式是在类名前加上~ | ②析构函数也没有返回值,与构造函数类似 | ③析构函数不能重载,与构造函数不同
④编译器生成的默认析构函数会对自定义类型调用其默认成员函数,这一点与构造函数类似
⑤对象生命周期结束时,系统会调用默认的析构函数
调用析构函数的顺序:局部对象(后定义的先析构,与栈类似)——> 局部的静态 ——> 全局对象(后定义的先析构)
在下面代码中,对象的销毁对象是怎样的呢?
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void func()
{
static Stack s3(3);
Stack s4(4);
}
Stack s6(6);
static Stack s7(7);
void TestStack()
{
Stack s1(1);
static Stack s2(2);
func();
Stack s5(5);
}
例如这个代码里头,(记住后定义的先析构),先被销毁的是func中的s4,再是s5,接着s1.到这就把局部内的对象给销毁了,再接着是局部的静态,那么先销毁s3,再销毁s2.到这就把局部静态的对象给销毁了。最后来销毁全局对象,那么还是后定义的先析构,所以先销毁s7,再将s6销毁。这就是整个过程。 整个顺序就是s4,s5,s1,s3,s2,s7,s6.
拷贝构造函数
特性:① 拷贝构造函数是构造函数的一个重载形式
②拷贝构造函数的参数只有一个且是类的类型对象的引用,通常用const修饰。并且这里不能传值,只能是引用,否则会引起无穷递归。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
再给大家画图理解一下第二点。
首先,在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
传值调用的话,这里的值是Date类型的,属于我们定义的类的类型,因此会继续去调用,造成无穷递归。
但,如果是引用调用的话,引用传递的是一个地址值,(与指针类似),是通过地址去找到值得,地址的话属于我们的指针类型,是一个简单类型,因此按简单类型的赋值拷贝,不会有拷贝构造函数的调用。
所以,我们定义拷贝构造函数时,必须以引用的参数来接收。
那么,我们知道,我们不显示声明拷贝构造函数时,系统会默认的去调用。大多数情况其实是可以的,但如果出现类中有指针数据成员时就有问题了。因为默认的拷贝构造函数,是将成员拷贝构造的,那么就会出现不同的两个指针指向同一块空间的情况。当一个实例销毁时,指针指向的那块空间就会被销毁。当销毁另一个实例,就会导致程序崩溃,因为此时的第二个指针已经无效了,那么再次去释放已经被释放的空间程序就会崩溃,相当于重复释放一块空间两次。 因此有些时候,拷贝构造函数还是得我们自己去声明定义。
赋值运算符重载
定义:C++为了增强代码的可读性引入了运算符重载。
形式为关键字operator后面接需要重载的运算符符号。
特性:①首先不能通过连接其他符号来创建新的操作符,必须是C和C++语法中存在的
②重载操作符必须有一个类类型参数
③.* \::\sizeof \?:\ . 注意以上5个运算符不能重载。 但是解引用*可以重载
④作为类成员函数重载时,形参看起来比实际操作数少1,因为第一个参数是this指针,this指针是被隐藏的
⑤赋值运算符只能重载成类的成员函数不能重载成全局函数
如果重载成全局函数的话,那类中的private类变量用不了,若直接放开成public也会出现不安全的情况。这里引入的办法一个是重载成类的成员函数,再一个是利用友元来解决,这个后续再提到,现在推荐的就是直接重载成类的成员函数。
⑥用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
这一点与拷贝构造函数类似,所以有些情况得我们自己去写赋值运算符重载函数。
否则呢,在赋值的时候,如果没有写赋值运算符重载,那直接将内容原封不动得拷贝过去,就会导致两个对象都指向同一块空间的情况。不仅使得其中一个对象原本的空间丢失,在销毁时还会导致共同指向的空间被释放两次的情况。
⑦前置++和后置++
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++
//这里是前置++,通过引用返回来提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++
//这里就是形式上的不同。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时
//该参数不用传递,编译器自动传递
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
//注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,
//而temp是临时对象,因此只能以值的方式返回,不能返回引用,与前置++不同!temp出这个作用域
//立马就要被销毁了,但前置++没有这个临时变量,是直接返回去的,就可以使用引用返回来提高效率。
private:
int _year;
int _month;
int _day;
};