类和对象(二)——6个默认成员函数
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类和对象(二)——6个默认成员函数相关的知识,希望对你有一定的参考价值。
目录
前言
如果一个类中什么成员都没有,简称空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
一.构造函数
1.1定义
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
注意:虽然构造函数名为构造,但是它并不是开辟空间创建对象,而是给对象成员变量赋初值。
1.2特性
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载。说明构造函数可以有多个。
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就是函数声明。调用带参构造函数时需要加括号并且输入需要初始化的值。
//如果调用无参构造函数写成
//变成了函数声明,声明了一个函数名为d3,返回值为Date类的函数
Date d3();
- 如果类中没有编写构造函数,C++编译器会自动生成一个无参的默认构造函数,但是一旦编写了构造函数,编译器不会生成。
#include<iostream> #include<Windows.h> #pragma warning(disable:4996) using namespace std; class Date{ public: private: int _year; int _month; int _day; }; int main(){ Date d1;//正确 Date d2(2021, 7, 9);//错误 system("pause"); return 0; }
上面代码定义了两个对象d1和d2,但是d2对象会发生错误,但是d1对象不会有错误,因为类中没有编写构造函数,编译器会默认生成无参的默认构造函数。无参的默认构造函数d1可以调用,d2不能调用,所以会有错。
#include<iostream>
#include<Windows.h>
#pragma warning(disable:4996)
using namespace std;
class Date{
public:
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;//错误
Date d2(2021, 7, 9);//正确
system("pause");
return 0;
}
上面代码定义了两个对象d1和d2,但是d1对象会发生错误,但是d2对象不会有错误,因为类中编写构造函数,编译器不会默认生成无参的默认构造函数。编写的构造函数d2可以调用,但是d1不能调用。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数,全缺省的构造函数和我们没有编写编译器默认生成的构造函数都可以认为是默认成员函数。
什么是全缺省的构造函数?
通过上面代码,我们发现当我们定义对象不输入值时,调用的是全缺省的构造函数,输出值就是缺省值。定义对象输入值时,调用的也是全缺省的构造函数,输出值就是输入值。所以当用户即编写了无参构造函数和全缺省构造函数时,当定义一个不输入值的对象时,编译器就不知道调用哪个函数而报错了。
这里说明一下:无参构造函数,定义对象不输入值可以调用,全缺省构造函数,定义的对象输入值可以调用,不输入值也可以调用。带参不缺省构造函数,定义对象需要传参才能调用。
- 对于编译器默认生成的构造函数,看起来对成员变量并没有赋予初始值,还是随机值,那它有什么用呢?
解答:对于内置类型,构造函数确实没有做处理,但是,对于自定义类型,编译器会调用自定义类型的构造函数。
上面代码输出Time()说明,在定义对象d1时,编译器默认生成的构造函数调用了自定义类型Time的构造函数。
1.3初始化列表
class Date{
public:
Date(int year=0, int month=1, int day=1){
_year = year;
_month = month;
_day = day;
_year = day;
_month = year;
_day = month;
}
private:
int _year;
int _month;
int _day;
};
虽然调用构造函数,对象中的成员变量都有了一个初始值,但是这并不能称其为成员的初始化,只能称其为赋初值,因为初始化只能一次,但是在构造函数体内可以多次赋值,如上。
于是构造函数有了另外一种形式
初始化列表:以一个冒号开始,以逗号分隔数据成员列表,每个成员变量后面跟一个放在括号里的初始值或者表达式。
class Date{
public:
//初始化列表,初始化
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
//里面还可以进行赋值
}
private:
//内置类型,成员变量的声明
int _year;
int _month;
int _day;
};
注意:每个成员变量在初始化列表中只能出现一次。
一般的构造函数也可以实现类似的功能,那为什么要有初始化列表呢?
以下三个成员,必须放在初始化列表位置进行初始化。
- 引用成员变量
- const成员变量
- 没有默认构造函数的自定义类型成员。就是用户编写了构造函数,但是编写的是带参的构造函数。
class A{
public:
//带参构造函数
A(int a){
_a = a;
}
private:
int _a;
};
class B{
public:
//初始化列表
B(int a, int b)
:_a(a)
, _b(b)
, _c(10)
{
}
private:
//引用
int& _a;
//自定义类型,但是它的构造函数需要传参
A _b;
//const修饰变量
const int _c;
};
为什么这三类需要调用的是初始化列表的?
因为引用和const在定义时必须初始化,没有默认构造函数的自定义类型成员,不能在类里自己进行初始化,必须定义对象传参才行,初始化列表是用来初始化的。
注意:成员变量在类中声明次序就是其初始化列表中初始化的顺序。与其在初始化列表中的先后次序无关。
二.析构函数
2.1定义
析构函数与构造函数的功能相反,但是析构函数不是完成对象的销毁,局部变量的销毁是随着函数栈帧的释放而释放的,是编译器完成的。而对象再销毁时会自动调用析构函数,完成类的清理工作。比如:申请的动态内存的释放,文件的关闭等等。
2.2特性
- 析构函数名是类名加上字符~。
- 无参数无返回值
- 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
- 对象生命周期结束,C++编译系统自动调用析构函数。
class Stack{
Stack(int capacity = 10){
_a = (int *)malloc(sizeof(int)*capacity);
assert(_a);
_size = 0;
_capacity = capacity;
}
//析构函数,无参数,无返回值
~Stack(){
if (_a){
//完成资源清理工作
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
int *_a;
int _size;
int _capacity;
};
- 对于默认生成的析构函数和构造函数一样,对于成员变量内置类型,不会做资源清理工作,而对于自定义类型,对象生命周期结束会自动调用自定义类型的析构函数。
#include<iostream>
#include<string.h>
#include<assert.h>
using namespace std;
#include<Windows.h>
#pragma warning(disable:4996)
class String{
public:
String(const char *a = "tom"){
int len = strlen(a) + 1;
_a = (char *)malloc(sizeof(char)*len);
strcpy(_a, a);
}
~String(){
if (_a){
cout << "~String" << endl;
free(_a);
_a = nullptr;
}
}
private:
char *_a;
};
class Preson{
private:
//自定义类型
String _name;
//内置类型
int _age;
};
int main(){
//类的实例化
Preson p;
system("pause");
return 0;
}
注意:
- 对于对象,先定义先调用构造函数,后定义后调用构造函数。然而先定义后调用析构函数,后定义先调用析构函数。因为存储在栈里。
三.拷贝构造函数
3.1定义
拷贝构造函数,只有单个形参,该形参是对本类型对象的引用(一般用const修饰)。在用已存在类类型对象创建新对象时由编译器自动调用。
定义很抽象,举个例子说明:
我们发现拷贝构造就是将d1的值拷贝一份给d2。 注意:d2(d1)和d2=d1都是调用了一份拷贝构造。
3.2特征
- 拷贝构造函数其实是构造函数的一个重载函数,只是参数不一样。
- 拷贝构造函数参数只有一个且必须使用引用传参。使用传值会引发无穷递归调用。
错误写法:使用传值。
原因:由于实参传值给形参时,会一直调用拷贝构造函数。
正确写法:
- 若未显示定义,系统默认会生成默认的拷贝构造函数。默认拷贝构造函数对对象是按内存存储字节序进行拷贝的,这种拷贝我们叫做浅拷贝,或者值拷贝。
那系统自己会生成默认拷贝函数并且可以按字节进行值拷贝,我们为什么还要编写拷贝函数呢?
看下面例子:
原因:String类实例化str1时,调用构造函数,为成员变量_a在堆上开辟了一段空间,开辟成功后返回了一个地址。String类实例化str2使用str1拷贝构造的str2,str2对象的成员函数_a和对象str1的成员变量_a指向对象的同一块地址空间。当两对象生命周期结束后,调用析构函数,对象str2后定义先调用析构函数,将成员变量_a,指向的空间释放了。由于str1的成员变量_a也指向同一块空间,但是已经被释放了,当str1调用析构函数释放空间时就会发生奔溃。同一块空间不能释放两次。
注意:对象一定有构造函数或者拷贝构造函数产生。
四.赋值运算符重载
想了解运算符重载的小伙伴可以看博客运算符重载
4.1定义
一般运算符只能应用于自定义类型之间,C++为了增强代码的可读性,引入了运算符重载。运算符重载是具有特殊函数名的函数。也具有返回值,函数名以及参数列表。
函数名字:由关键字operator后面加需要重载的运算符符号。
函数原型:返回值+operator操作符(参数列表)
仔细看上面代码,会不会感觉拷贝构造函数和赋值重载函数功能几乎一样,那还需要赋值重载运算函数干嘛?
这里说一下区别,拷贝构造函数是在对象定义的时候编译器会自动调用的,而赋值重载函数是两对象定义完后,可能经过了几次其它赋值,而需要两对象值相同时调用的。
4.2特性
赋值运算符主要要注意四点:
- 参数的类型
- 返回值,是返回this指针(例如i++)还是其他值(i+1)。
- 要排除自己给自己赋值的情况(没意义)
- 返回值如果是this指针(*this),可以是引用
- 一个类如果没有显示定义赋值运算符重载函数,编译器会自动生成一个,与拷贝构造函数生成的功能类似,是按对象字节进行值拷贝。也就是浅拷贝。当由一个对象的成员函数再对象开辟空间时,赋值重载时,两对象成员变量指向同一块空间,释放是会导致程序奔溃。具体代码可看上面拷贝构造函数。
5.取地址操作符重载
函数书写如下:
class Date{
public:
Date *operator&(){
return this;
}
private:
int _year;
int _month;
int _day;
};
当用户未显示定义时,编译器会自动生成。所以一般应用不是很广泛。引用场景可以是不想让别人得到对象的地址。
class Date{
public:
Date *operator&(){
return nullptr;
}
private:
int _year;
int _month;
int _day;
};
6.const取地址操作符重载
const关键字修饰的对象或者变量具备常性,可读,不可被修改。
6.1const成员
const修饰的成员变量
假如有一种情况,一个函数传给this指针的变量是一个可读但是不可修改的变量时,由于this指针被隐含起来了,该如何修饰this指针呢?
如下代码:
此时编译时会报错,因为对象d1是不可以修改的,调用Print函数时,d1传给this指针,但是this指针是可以被修改的所以会报错。
正确的写法是:再Print函数后面加一个const
class Date{
public:
void Print()const
{
cout << _year << _month << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
const Date d1;
d1.Print();
system("pause");
return 0;
}
将const修饰的类成员函数称之为const成员函数,实际const修饰的是成员函数隐藏的this指针,表示该成员函数不能对this指针指向对象的成员变量进行修改。
下面有四个问题:
1. const对象可以调用非const成员函数吗?
解答:不可以,因为const修饰的对象是不可修改的,非const成员函数this指针可以修改。
2. 非const对象可以调用const成员函数吗?
解答:可以,因为非const修饰的对象是可读可修改的,const成员函数this指针不可以修改,可以读,属于权限缩小
3. const成员函数内可以调用其它的非const成员函数吗?
解答:不可以,
4. 非const成员函数内可以调用其它的const成员函数吗?
解答:可以。
6.2const取地址操作符重载函数
函数编写:
class Date{
public:
const Date* operator&()const{
return this;
}
private:
int _year;
int _month;
int _day;
};
与取地址重载函数相同,当用户不编写时,编译器会自动生成。所以应用也不广泛。
以上是关于类和对象(二)——6个默认成员函数的主要内容,如果未能解决你的问题,请参考以下文章