C++入门篇之6大默认函数
Posted 捕获一只小肚皮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++入门篇之6大默认函数相关的知识,希望对你有一定的参考价值。
前言
上一章,博主介绍了类的该来以及使用,现在我们开始更加深入的章节吧,那就是类的默认函数,而我们要着重讲解的将会是前4个默认函数,至于最后两个,因为最后两个几乎没什么用
默认函数的引入
上一节我们已经知道了类,现在博主有个小问题,就是如果我们定义一个空类,比如下面:
class Date
{
};
Date类是否真的就是空的呢?答案是否定的,在这个空类里面还有6个成员函数哦,并且他们都是编译器自动调用
~
这六个成员函数分别是构造函数,析构函数,拷贝构造,赋值构造以及取地址重载(有两个)
.
构造函数
概念:
注意哦~,大家不要被这个名字给误导了,构造函数的作用不是构造,而是对
类对象
进行初始化
.它是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
特征:
- 无返回值(注意注意…,不是说返回值为void,而是指
不写
) - 函数名与类名相同
- 编译器自动调用
- 支持重载
按照上面的特征,我们写一个日期类,并且加上一个构造函数试试:
class Date
{
public:
Date() //定义一个无参构造函数
{
_year = 1999;
_month = 8;
_day = 26;
}
Date(int year, int month, int day) //定义一个有参数构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
构造函数的调用方式为:定义对象时,在对象后面加括号,然后按照构造函数的定义依次传参,如下
int main()
{
Date d1(2021,12,13); //对d1进行了初始化,让year等于2021 month等于12 day等于13
return 0;
}
效果如上图
注意点:如果我们
还定义了无参数构造函数
,那么在定义对象的时候,只需要写对象名就行,不需要加括号,否则就变成了函数声明.如下
Date d2; //这样系统便会自动调用无参构造函数.
Date d3(); //注意哦,如果这样写,就变成了一个函数声明,表示一个叫d3的函数,没有形参,返回值类型为 Date
注意点1
如果我们在定义类时,没有显示的定义构造函数(手动构造的意思),那么C++编译器便会自动生成一个无参构造函数,相反,只要我们手动写了任何形式的构造函数,编译器都不会生成默认函数
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
像上面的类一样,我们并没有显示定义构造函数,那么编译器就会自动生成一个构造函数,对数据进行初始化,调试效果如下:
咦?说好的自动生成构造函数,并且进行初始化呢?结果好像并不是想象中的那样哎~,其实并不是编译器没有处理,只是这是C++的一个小缺陷,它初始化成员属性时用的值刚好也是 基础类型变量没初始化时编译器给其赋的值,所以我们才看到如上效果.
但是其构造函数会对自定义类型进行一定处理,什么处理呢?那就是通过自身的构造函数去调用自定义类型的构造函数.
class MMM
{
public:
MMM()
{
da = 0;
}
private:
int da;
};
class Date
{
private:
int _year;
int _month;
int _day;
MMM a;
};
int main()
{
Date d1;
return 0;
}
可以清晰的发现,a的构造函数被调用了
注意点2
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数
只能有一个
。注意:无参构造函数、全缺省构造函数、编译器自动生成的构造函数,都可以认为是默认成员函数,这一点一定要记清楚哦
我们仍然以日期类举例
class Date
{
public:
Date() //定义一个无参构造函数
{
}
Date(int year = 1999, int month = 12, int day = 13) //定义一个有参数构造函数
{
}
private:
int _year;
int _month;
int _day;
};
像上面这样,无参构造和全缺省构造只能存在一个,大家可以想想为什么?原因是会引起歧义,比如我们定义类对象时,这样写Date d1;
这种形式,编译器到底该调用哪一个构造函数呢?是不是不知道了?
析构函数
析构函数的作用清理资源(释放动态内存),其写法和构造函数及其相似
特征:
-
函数名是
~
加类名,并且~
在前面. -
无参数,无返回值,无返回值什么意思构造函数讲解了哦~
-
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
-
对象生命周期结束时,C++编译系统自动调用析构函数
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10) //构造函数
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList() //析构函数
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
注意点:析构函数和构造函数类似,对于基础类型(int,char等)相当于没做处理,而对于自定义类型来说,该类对象的析构函数会调用其自定义类型的析构函数.
拷贝构造
顾名思义,就是在定义类对象时候,可以把另外同类型对象的值,拷贝进正在定义的对象中.
为了更能理解拷贝构造以及定义,博主在这里先介绍其使用.
使用方法:括号法和赋值法
括号法:
Date d1(2012,12,13); //创建一个对象,并初始化.
Date d2(d1); //这就代表在调用拷贝构造,作用是把d1的值复制一份给d2
赋值法:
Date d1(2012,12,13); //创建一个对象,并初始化.
Date d2 = d1; //这就代表在调用拷贝构造,作用是把d1的值复制一份给d2
现在熟悉了其使用以后,怎么进行显示定义呢?看下面
特征:
- 属于构造函数的一种,也就是说函数名和类一样
- 只有一个参数,并且参数类型与函数名一样
所以,这样定义是否对呢?比如有一个类叫做Date
Date(Date date)
{
}
答案是否定的,因为这样就陷入了一个死递归状态
,为何?
假设我们开始使用拷贝构造了,如下:
Date d1(2012,12,13);
Date d2(d1);
在调用过程中,d1会传给date形参,也就是等价于date = d1
,好家伙,这一步又相当于在调用拷贝构造,于是又有一个形参date,然后date = d1
无限死递归下去,下图更好解释:
所以,应该怎么解决这种情况呢?没错,这就需要用到我们前面讲过的引用
,因为引起这种死递归的原因不就是值传递吗,我们把参数设置成引用就不会发生值传递了.
Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
我们知道,拷贝构造函数也是类的6大默认成员函数之一,那如果我们不写显示拷贝构造,编译器自动生成的拷贝构造会怎么样呢?如下:
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(2021,8,15);
Date d2(d1);
return 0;
}
现在进行监视:
我们发现,即使我们不写拷贝构造函数,编译器会自动帮我们进行拷贝,那是不是说,拷贝函数我们就不需要手写了呢?请看下面例子:
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
return 0;
}
我们运行,就会发现程序会崩溃,至于崩溃的原因就是第一次定义对象时,调用的是构造函数,在堆区开辟了一块空间,但是第二次定义对象时候,调用的是拷贝构造,也就是说,这一次并没有再开辟空间,而是直接把s1的内容复制一份给s2,那么s1和s2的成员都是指向了一块空间,但是会调用两次析构函数,同一块内存空间不可调用两次,所以会崩溃,这涉及到浅拷贝和深拷贝,博主会在后面的章节中单独介绍.这里我们只需要记住,如果只有基础类型,而不涉及动态空间,就可以不用写显示拷贝,反之需要.
赋值运算符重载构造
在讲赋值构造之前,我们先讲解一下运算符重载构造.
运算符重载
我们知道1 + 1 = 2,编译器同样也知道,因为他们是基础类型,都是int,那如果有一个类了.他们定义的对象这样相加减,什么意思呢?,比如:
Date d1(2012,12,13); Date d2(2008,8,6); d1 + d2;
两个日期对象相加,是year相加,month相加,还是day相加呢?或者说全都相加?这会造成严重的理解偏差,所以才有运算符重载,他的意思就时再定义一下,使得对象运算有规定意义.
语法规则:
返回值类型 operator 操作符 (形参列表)
注意哦,新参列表是隐藏了一个this指针的哦,我们写形参列表时候,就要注意数量了,它往往比操作符需要的操作数数量少1.什么意思呢?比如
+
需要两个操作数,但是我们写运算符重载时,只需要写一个形参列表,因为还隐藏了一个this指针.
示例: 假设我们需要实现对象加整数,其意义是整数是加在month上,也就是实现+=
.
void operator+=(int n)
{
_month = _month + n;
}
那么我们指向操作d1+=6
(原来d1日期是2020.2.3)后,d1将会变成2020.8.3,我们验证一下:
注意哦,虽然我们写的是+=,但是编译器在处理时是吧
d1+=6
翻译成了d1.operator+=(&d1,6);
,这样才能明白为何会达到这样的效果.
赋值重载
现在我们明白了运算符重载什么意思,那我们试试赋值重载吧,应该怎样设计?
日期类为例:
void operator=(Date date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
通过上面的例子,我们几乎可以认定为这是成功的,但是真的吗? 还记得
=
还有一个特性吗?那就是连续赋值,比如a =b =c=d
;如果我们也这样对类进行操作,可以吗?很明显,发生错误.
为什么会发生这样的错误呢?因为d2=d1
时,编译器会翻译成d2.operator=(&d2,d1)
,而这个函数并没有返回值,那么d3也就接收不到参数了,所以会报错.修改如下:
Date operator=(Date date)
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this; //大家一定要记得this指针哦,它是被隐藏了的,但是支持我们显示写
}
这样就不会有问题了.但是大家想想上面是否还可以再修改呢? 答:可. 我们可以形参和返回值变为引用,这样就减少了形参拷贝.
再次修改:
Date& operator=(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this;
}
有人会问,我们不是已经有了拷贝构造函数吗?为何还需要赋值重载,直接拷贝不行吗?其实这里有一个误区哦~
,拷贝构造使用地方是创建对象时,而赋值重载适用地方是已经存在两个同类型对象,然后把其中一个对象的值复制给另一个对象.下面例子刚好区分了他们:
Date d1(1,2,3); //这是在构造初始化
Date d2();
运算符重载练习
我们需要一个日期类.
要求:
- 实现日期加减整数,代表日期前进或回退整数天(即分别实现
operator+(int day),operator-(int day),operator+=(int day),operate-=(int day)
)- 可以实现日期的比较,代表知道哪个日期在前或在后(即分别实现
operator<(Date& date),operator<=(Date& date).....
)- 实现日期减去日期,代表两个日期之间相距多少天(即分别实现
operator-(Date& date)
).- 需要实现一个输入当年当月可以知道当月有多少天的函数.
- 实现日期的前置++,后置++
- 实现日期的前置–,后置–
- 实现赋值运算符重载
const成员
什么意思呢?我们先看看下面的例子:
class Date
{
public:
void print()
{
cout<<"year是"<<_year<<endl;
cout<<"month是"<<_month<<endl;
cout<<"day是"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;
d1.print();
d2.print();
return 0;
}
大家运行一下,看看是什么结果,博主的运行结果是下面这样:
我们会发现,一直报d2的错误,那么d2到底哪里错了呢?其实这就是const的问题.我们知道d2的类型是const Date
,那么d2在调用print函数时,系统便会传给print一个this指针(上一章节讲过).而this指针的类型是Date*
,但是d2的地址类型是const Date*
,将一个const修饰的指针传给普通指针接收,将会扩大权限这种情况和常量引用相似,所以为了解决这种办法,我们需要加上const修饰this指针,而加的地方就是在函数后面.
void print() const //注意看哦,是加在这里的
{
cout<<"year是"<<_year<<endl;
cout<<"month是"<<_month<<endl;
cout<<"day是"<<_day<<endl;
}
同时,大家一定要注意,const加成员函数后面修饰的只有this指针,而不会修饰其他形参
.
const4问
- const对象可以调用非const成员函数吗?
答:很明显,不可以,上面的d2调用print出错就是验证,权限被放大了
- 非const对象可以调用const成员函数吗?
答:很明显,可以的,因为未被const修饰的指针可以传给const修饰的指针,比如
int a = 10; const int* b = &a; //这样是可以的,权限可以缩小 const int c = 10; int* d = &c; //这样是不可以的,权限不能被放大
- const成员函数内可以调用其它的非const成员函数吗?
答:很明显,不可以,这个和第一问类似,只要调用成员函数就一定传this指针,但是被const修饰的函数去调用未被修饰的,就会扩大权限
- 非const成员函数内可以调用其它的const成员函数吗?
答:可以的,这和第二问类似,权限可以被缩小.
取地址及const取地址操作符重载
取地址重载其实就说使用operator
对&
进行操作,这个很好实现
Date* operator&()
{
return this;
}
而const取地址重载,其实就是说上面那个函数被const修饰了,我们只需要对返回值修改即可
const Date* operator&() const
{
return this;
}
但是这两个函数即使我们不写,编译器也会自动生成,所以说这两个函数几乎没有太大作用.
以上是关于C++入门篇之6大默认函数的主要内容,如果未能解决你的问题,请参考以下文章