目录
前言
哈喽,大家好,相信看了我上一篇博客的小伙伴已经对类和对象有了一些初步认识,今天我将着重介绍类里面的默认成员函数,希望想要了解的小伙伴不要走开哦。
提示:以下是本篇文章正文内容,下面案例可供参考
一、类的六个默认成员函数
如果一个类里面什么成员都没有,我们一般会把它简称为空类。但空类里真的什么都没有吗?当然不是,任何一个类在我们不写的情况下,都会默认生成六个成员函数。下面我将依次介绍。
二、构造函数
1.概念
我们先来创建一个日期类。
Class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
对于该日期类,我们可以通过SetDate函数来给对象设置内容,但每次创建对象的时候都要调用该函数来设置未免有些麻烦,那能不能在对象创建的过程中就自动设置好初始值呢?构造函数就是这个用处。
构造函数是一个特殊的成员函数,名字和类名相同,创建对象时由编译器自动调用,来保证每一个对象都有一个合适的初始值,并且在对象的生命周期内只调用一次。
2.特性
通过上面的叙述,我们也大概了解了,构造函数虽然名字叫构造,但实际是用来初始化对象的。其特征主要如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载
class Date
{
public :
// 1.无参构造函数
Date ()
{}
// 2.带参构造函数
Date (int year, int month , int day )
{
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2 (2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
}
5.编译器可能会自己生成构造函数。
如果类中没有显示定义构造函数,那c++编译器将会自己生成一个无参的默认构造函数,如果用户自己定义了构造函数,那编译器将不会自动生成。 那可能有小伙伴会有疑惑,我们写的和编译器自动生成的构造函数有什么区别呢?
这里我想告诉大家,如果是我们自己写的构造函数,我们可以让构造函数实现任意的功能,我们想让它干嘛就把对应的功能写进去就好了,当调用时自然就会实现。
而编译器自动生成的构造函数,里面的内容是编译器自带的,我们是无法改变的。那么编译器生成的默认函数有什么功能呢?这里就要讲到c++把类型分为内置类型和自定义类型。内置类型也就是基本类型,int,char这些,而自定义类型就是我们自己设置的,如结构体和类。而编译器生成的构造函数不会对内置类型起到初始化作用,也就是说编译器生成的构造函数根本不会管内置类型,它只会去管自定义类型,它会去调用自定义类型的构造函数(也就是说当类嵌套类的时候,外面类的构造函数回去调用里面类的构造函数)。可以看出,编译器自动生成的构造函数作用是非常有限的,大多数情况下构造函数需要我们自己去写。
6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
// 默认构造函数
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;
}
以上代码是不能通过编译的,因为类中同时出现了两个默认构造函数(无参数调用的构造函数),当没有参数时,编译器会不知道调用哪个。
三、析构函数
1.概念
和构造函数相同,析构函数虽然名字叫析构,但并不是用来销毁对象的,而是在对象的内部进行一些资源清理工作,在对象销毁时,编译器会自动调用析构函数。
2.特征
析构函数时特殊的成员函数,特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,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;
};
参考上述代码,当在堆上开辟空间的的时候,往往需要用到析构函数。相信很多小伙伴在使用malloc的时候都会忘记释放空间, 析构函数帮我们很好的解决了这一点。
5.编译器会自动生成析构函数
和构造函数相同,编译器自动生成的析构函数唯一的作用就是调用类中自定义类型的析构函数。
四、拷贝构造函数
1.概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,编译器自动调用。
2.特征
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝 。和构造函数和析构函数不同,系统生成的默认拷贝构造函数会对内置类型进行处理,他会把类中的所有内置类型进行替换,在某些情况下,类中只用系统默认的拷贝构造函数足够能完成任务。
但是,如果涉及到在堆中开辟空间,再用默认拷贝构造函数则会发生报错,这就需要用我们以后讲的深拷贝去学习。
五、赋值运算符重载
1.运算符重载概念
c++为了增加代码的可读性,引入了运算符重载。运算符重载是具有特殊函数名的函数。也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型或者枚举类型的操作数。
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义。
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的,操作符有一个默认的形参this,限定为第一个形参。
5. .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
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)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
赋值运算符重载注意事项:
1.赋值运算符重载和拷贝构造的区别 拷贝构造使用已经存在的对象去初始化新创建的对象,而赋值运算符重载是两个对象都存在,把其中一个的数据复制到另一个上去。
2.参数前面加const 防止赋值的时候两边写反导致发生错误,例如想把a赋值给b,结果把b赋值给了a。
3.返回*this
注意返回值是*this而不是void。以内置类型举例,a=b=c,这个代码是从右往左执行,把c赋值给b返回值是b,然后把b赋值给a。所以赋值运算符重载也要有返回值,为了避免调用拷贝重载,这里返回值使用引用。
4.编译器会自动生成拷贝运算符重载 一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝。但和拷贝构造一样,在某些情况下会崩掉,需要学到深度拷贝后才能解决。
六、const修饰类的成员函数
将const修饰的类成员函数成为const成员函数,const修饰的实际上是隐藏的this指针,表明该函数不能对类的任何成员进行修改。
注意:
const的对象是不能调用非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 ; // 日
};
总结
本文主要介绍了类中的六个默认成员函数,希望看到这篇文章后大家能对类和对象有更深一步的认识,能帮助到大家就是我最大的荣幸,如果觉得这篇文章写的不错可以点赞支持一下博主哦,您的鼓励就是我创作的最大动力。