文章目录
- 前言
- 类的六个默认成员函数:
- 1. 构造函数
- 概念
- 特性
- 做了什么?
- 易错注意:
- 显式定义和默认构造函数
- 2. 析构函数
- 概念
- 特征
- 做了什么?
- 注意事项:
- 3.拷贝构造函数
- 概念
- 特征
- 做了什么?
- 注意事项:
- 4.赋值运算符重载
- 运算符重载
- 赋值运算符的重载
- 注意事项:
- 5.取地址及const取地址操作符重载
- 总结
- C++语言系列学习目录
前言
本节是要学习六个默认成员函数。主要是从四个方面讲解:
1)什么是该默认成员函数?
2)默认成员函数做了什么?
3)一些易错的注意事项
4)什么时候用默认成员函数,什么时候显式实现?
本篇用 日期类(Date)、栈(Stack) 、队列(Queue)三种类来举例
类的六个默认成员函数:
- 如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
1. 构造函数
构造函数就与我们所写的Init()方法一样,用于类对象属性的初始化。但这个构造函数不用用户调用,而是在类对象实例化时自动调用。
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
特性
我们围绕第一个问题展开:什么是构造函数?
其有如下特征:
- 函数名与类名相同
- 无返回值
class Date
{
public:
//函数名和类名相同,无返回值
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
- 函数可以重载
class Date
{
public:
//无参数
Date()
{
_year = year;
_month = month;
_day = day;
}
/*
//全缺省:注意全缺省和无参数不能同时存在,他们实例化方式可以相同,编译器无法辨别
Date(int year=2024,int month=4,int day=27)
{
_year = year;
_month = month;
_day = day;
}
*/
//半缺省
Date(int year,int month=4,int day=27)
{
_year = year;
_month = month;
_day = day;
}
//不缺省
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
- 对象实例化时编译器自动调用
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。后面实例化对象按照显式定义的函数调用(举例看下面代码注释)
Date类举例:
class Date
{
public:
/*
// 5.如果用户显式定义了构造函数,编译器将不再生成默认构造函数,后面也不能用Date d1;这样实例化,而是采用Date d1(2024,4,27);这样来实例化对象;
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这里我们用Date实例化了个对象d1,调用Print函数打印日期,发现个问题如下图:
我们发现我们的实例化对象d1并没有初始化啊,那默认构造函数到底干了什么呢?
做了什么?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。默认生成的构造函数对两者处理不同:
1)内置类型不做处理;
2)自定义类型调用它自己的默认构造函数;
易错注意:
- 实例化对象,错误:
int main()
{
Date d1; //表示实例化一个Date类对象;
Date d1(); //表示一个返回值为Date的d1()函数方法;
return 0;
}
- 无参数构造函数、全缺省构造函数、默认构造函数三种都可以当做默认构造函数,只能存在其中一个,不然会发生实例化时编译器不知道调用哪一个构造函数的错误。因为都可以用
Date d1;
来实例化对象。 - C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//可以在这里给缺省值
int _year=2024;
int _month=4;
int _day=27;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
显式定义和默认构造函数
那么什么时候我们采用显式定义,什么时候采用默认构造函数呢?
我给出的答案是,一般都自己显式定义比较好。
默认构造函数在以下几种情况下可以使用:
- 内置类型成员都具有缺省值(默认值);
- 类中全是自定义类型,如:Queue;
2. 析构函数
析构函数是一个特殊的成员函数,其名称与类名相同,前面加上波浪号(~)作为前缀。析构函数的主要作用是执行对象生命周期结束时的清理工作。当一个对象的生命周期结束时,无论是因为超出作用域、被显式删除,还是因为其所在的动态内存分配被释放,析构函数都会被自动调用。
概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征
那么什么是析构函数呢?
它有以下特征:
- 函数名就是在类名前加~;
- 无参数、无返回类型;
- 一个类只能由一个析构函数。若没有显示定义,系统自动调用默认析构函数。注意:析构函数不能够重载;
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
Stack类 举例:
class Stack {
public:
//构造函数
Stack(int capacity=4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
//析构函数:显式定义
~Stack()
{
//释放动态调用的空间资源
free(_a);
_a = NULL;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
{
Stack s1(10);
}
//s1作用域结束自动调用 ~Stack();
//不论显示定义,还是默认析构函数,都不需要显示调用;
return 0;
}
做了什么?
默认析构函数做了什么?
- 内置类型不做处理;
- 自定义类型调用它的析构函数;
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可
注意事项:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
- Queue类,成员全是自定义类型,会调用成员各自的析构函数。所以不用调用析构函数。
- 只有堆上的资源需要手动释放。
3.拷贝构造函数
拷贝构造函数是C++中的一种特殊的构造函数,用于创建一个新对象,该对象是已存在对象的副本。拷贝构造函数在多种情况下会自动被调用。
概念
概念:拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数有以下特征:
- 拷贝构造函数是构造函数的重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
Date类 举例:
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;
}
做了什么?
默认拷贝构造:依旧是一样的,对内置类型进行浅拷贝,自定义类型调用它们自己的拷贝构造。不需要显示定义的类,比如:Date、Queue。需要显式定义的类,比如:Stack。因为有动态空间的开辟,所以需要深拷贝。
注意事项:
这个比较容易错,大家请注意!
- 默认拷贝构造只是浅拷贝,需要深拷贝的对象需要显示定义拷贝构造函数。
举个Stack的例子:我们如果只是浅拷贝来处理Stack会出现错误。
比如:Stack s2(s1);
如果只是浅拷贝: s2._a=s1._a; 只是这种两个指针指向同一个开辟的空间;
-
问:这种会出现怎样的问题呢?
-
答:在函数结束的时候,s1调用一次析构函数,把空间释放了;s2也会在调用一次,此时原本空间已经被释放了,无法再次释放,会报错。
- 强调,显示定义拷贝构造函数,只能传一个参数并且必须为该类的引用。不能传值。
原因:如果进行传值传参,会在过程中调用拷贝构造去拷贝一个data临时对象,用于传值。如下图,便无限的调用下去,没有结束点,进入死循环。所以发生报错。
4.赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator
后面接需要重载的运算符符号。
函数原型:返回值类型 operator
操作符(参数列表)
注意:
- 不能新增加运算符;
- 保持运算符具有原有语意;
- 不改变运算符原有操作数个数;(比如
+
,就只能两个对象进行) - 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符的重载
1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
Date类 举例:
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;
};
int main()
{
Date d1(2024,5,5);
//拷贝构造函数
Date d2(2024,5,6);
Date d3;
d3 = d1 = d2;
return 0;
}
注意事项:
- 赋值运算符重载是默认构造函数,不写,会进行简单的赋值和浅拷贝类似。通常需要为包含动态分配内存的类重载赋值运算符,以执行深拷贝操作。深拷贝意味着为新对象分配新的内存,并复制原对象所指向的内存内容,从而确保两个对象独立拥有自己的内存资源。
- 注意区分什么是赋值,什么时候是构造:
Date d1(2024,5,5); //构造
Date d2 = d1; //构造
Date d3;
d3=d1; //赋值
辨别方法:已存在的对象初始化另一个对象叫构造;两个都存在的对象,则是赋值。
//小技巧:看前面有没有Date 类
5.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
总结
本章节介绍了四个主要的默认成员函数,还有两个不常用就没有过多介绍。
C++语言系列学习目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加,添加超链接