目录
前言:
当一个类什么都不写的时候,我们通常叫它空类,那么空类中真的什么都没有吗?
事实上,任何一个类在什么都没写的时候,都会生成六个默认成员函数,分别为构造函数,析构函数,拷贝构造函数,赋值重载函数,以及普通对象与const对象取地址函数。
由于大多数后两个函数不需要我们手动操作,所以今天主要讲解的内容是前面四个函数。为了方便,我们会以Date类为例子,更加方便让大家理解。
理解构造函数,析构函数,拷贝构造函数,赋值重载函数对理解C++雷雨对象有十分重要的作用。
一、构造函数
1、概念
构造函数是一种特殊的函数,它用于创建和初始化对象时调用。它通常与类关联,用于在创建类的实例时执行特定的初始化操作。构造函数的名称与类名称相同,并且没有返回类型。在创建对象时,构造函数会自动调用,并且可以接收参数来初始化对象的属性。这有助于确保每个数据成员都有个合适的初始值,并且在对象整个生命周期中只调用一次。
所以大家别被它的名字所误导,构造函数并不执行构造功能,它的主要作用只是进行初始化。
2、特性介绍
2.1、函数名必须要与类名一致。
2.2、无返回值。
2.3、在对象实例化时编译器会自动调用一遍对应的构造函数。
2.4、构造函数可以多个重载。(例:)
class Date
{
public:
Date()//一个没有参数的构造函数,我们称之为无参构造函数
{
cout << "Date()" << endl;
}
Date(int year, int month, int day)//带参构造函数
{
_year = year;
_month = month;
_day = day;
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
Date B(1800, 1, 1);
return 0;
}
当我们实例化A时,编译器就会默认调用符合A的无参构造函数,当我们实例化B时,由于B更加满足带参构造函数,所以会调用带参构造函数。
2.5、如果类中没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,编译器就不会再生成。
class Date
{
public:
Date(int year, int month, int day)//带参构造函数
{
_year = year;
_month = month;
_day = day;
cout << "Date(int year, int month, int day)" << endl;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
return 0;
}
当我们显式定义了构造函数,且没手动写一个无参构造函数时,此时A就不能进行无参数的初始化,因为编译器也没用生成构造函数,A找不到它适合的构造函数,编译就会报错。
class Date
{
public:
//Date(int year, int month, int day)//带参构造函数
//{
// _year = year;
// _month = month;
// _day = day;
// cout << "Date(int year, int month, int day)" << endl;
//}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
return 0;
}
但当我们把显式定义的带参构造函数注释掉时,就可以正常进行编译,这就是编译器默认生成了一个无参构造函数的结果。
2.6、编译器生成的默认构造函数,会对自定义类型自动调用自定义类型自己的构造函数,对于内置类型来说,则不会自动调用。为了弥补这个缺点,在C++11中又规定可以在内置类型声明时给予默认值(这个默认值的作用具体的我们会在类与对象下中讲解到)。
但实际上,由于只对自定义类型调用默认构造,倘若自定义类型中仍然有自定义类型成员参数,调用时函数就会层层递进,直至不再包含自定义类型成员函数为止。倘若自定义类型中没有可以调用的有效的构造函数,就会编译报错。
2.7、当一个构造函数是无参的构造函数与全缺省的构造函数,就被我们称为默认构造函数。(包括编译器默认生成的构造函数其实也是无参的,自然就可以被叫做默认构造函数) 。(确定有默认构造函数是为了当我们在实例化一个类时,只确定对象名,允许不给参数让对象初始化)
二、析构函数
1、概念
析构函数的功能与构造函数相反,但并不是完成对对象本身的销毁,局部销毁工作是由编译器完成的。一个对象在销毁的时候,编译器会自动调用他的析构函数,完成对象中资源清理的工作。
2、特性
2.4、 对象的生命周期结束时,编译器会自动调用析构函数
2.5、如果类中没有涉及申请资源,析构函数可以不写,直接使用编译器默认生成的析构函数就行。但如果涉及到了资源申请,一定要写,否则可能会造成资源泄露。
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;
}
对于以上代码,输出的结果是~Time(),这是因为我们生成了一个Date类的对象,在该对象生命周期结束时,会自动调用编译器默认生成的析构函数,由于该对象成员参数有一个自定义类型,就会该自定义类型变量的析构函数,于是打印出了~Time()。
三、拷贝构造函数
1、概念
拷贝构造函数与构造函数的差别主要就是名字上面的拷贝造成的。拷贝就是用一个已经存在的东西复制出一份原本不存在的东西。
拷贝构造函数就是指只有单个形参(这个形参就是原本存在的东西),且该形参是对本类类型对象的引用(一般还会加上const修饰),在用已存在对象创建新对象时,编译器自动调用的构造函数。
2、特性
2.1、拷贝构造函数是构造函数的一个重载形式。
2.2、拷贝构造函数的参数只有一个,且必须为该类类对象的引用,不能使用传值,会引发无穷递归导致编译器直接报错。
2.3、若没有显示定义,则编译器会生成一个默认的拷贝构造函数。这个默认生成的拷贝构造函数将会按照字节序,对内存存储进行拷贝。
2.4、对内置类型来说会调用默认拷贝构造函数进行浅拷贝,对自定义类型还是会调用该类型的拷贝构造函数。
2.5、若类中没有涉及内存资源申请的话,可以直接使用编译器默认生成的拷贝构造函数进行浅拷贝,但如果涉及了内存的管理,就必须进行手动的编写拷贝构造函数。
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A(1900, 1, 1);
Date B(A);
return 0;
}
对于以上代码,在vs2022,64位环境下的运行结果为:
我们可以看到,定义A的时候调用了 构造函数,之后定义B时,由于A已经存在,所以直接调用了拷贝构造函数。最后生命周期结束时,由于B最后定义,所以最先析构,随后进行A的析构。
让我们来稍微深入理解一下:
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(1900, 1, 1);
Test(d1);
return 0;
}
结果为:
在这个过程中,我们会先对d1进行定义与初始化,随后就会调用构造函数,随后调用test函数的时候,给定义初始化temp的时候,又会调用拷贝构造函数,随后在返回的时候,会生成temp的副本来进行传值返回,生成副本的时候,已经退出了test函数,于是temp在进行了一次对副本的拷贝构造函数之后,立马又进行了析构。
四、赋值运算符重载
我们要讲述的运算符重载与复制运算符重载,其实都是函数重载的一种。只不过运算符的重载需要关键字operator的帮助。
1、运算符重载
2、赋值运算符重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//如果定义为全局函数,没有显式实现,编译器就会生成一个默认的赋值运算符重载函数,导致与全局的函数产生重载冲突。所以必须是类的成员函数,而不是全局函数。
Date& operator=(const Date& d)//传引用返回,提高效率。参数类型固定为(const Date&)
{
if (this != &d)//如果是自己与自己赋值,可以直接免去内置类型复制操作,节省空间
{
_year = d._year;//归根结底还是对内置类型的运算,所以重载时不能改变内置类型=的含义。
_month = d._month;
_day = d._day;
}
return *this;//返回*this,即对象本身。
}
private:
int _year;
int _month;
int _day;
};
与拷贝构造函数类似,当没有显式定义时,就会生成一个默认的赋值运算符重载函数,以字节序逐内存的进行拷贝(浅拷贝)。所以赋值运算符重载在没有涉及内存管理时也不需要手动实现编写,但如果涉及到了内存的管理分配,就必须手动搓一个函数出来了。
3、前置++与后置++的重载
由于都是通过关键字operator来进行运算符的重载,所以当我们进行前置++与后置++的重载时,就难免弄混。为了解决这个问题,我们规定,重载后置++时,需要在参数列表中加一个int类型的形参(此参数只是为了分辨函数的重载,并无实际作用):
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
总结:
类的六个默认成员函数是理解C++类的关键,理解了这六个函数,尤其是前四个,就可以对C++类的实现与作用有质的提升。
我们在类与对象中主要讲解了类的六个默认函数的前四个,由于后两个编译器都会自动生成,并不需要我们重新定义。只有特殊情况,才会需要我们手动定义,一般来说,都直接使用编译器默认生成的就行。
作者本人语言功底有限,有些讲解的粗糙,甚至不好理解,请大家谅解。
若有讲解之处,请大家提出来,感谢大家监督!