目录
默认成员函数
在C++中,每一个类都会默认生成6个成员函数
- 默认构造函数
- 默认析构函数
- 默认拷贝构造函数
- 赋值重载
- 普通对象取地址
const
对象取地址
默认成员函数,指用户没有显式地定义,编译器会自动生成的成员函数
构造函数
构造函数介绍
在C语言中,如果要给一个整体初始化时,需要使用一个函数,并且需要自行调用
#include <stdio.h>
typedef struct Date
{
int year;
int month;
int day;
}Date;
//初始化函数
void InitDate(Date* d)
{
d->day = 20;
d->month = 3;
d->year = 2024;
}
int main()
{
Date d = {0};
InitDate(&d);
printf("%d/%d/%d", d.year, d.month, d.day);
return 0;
}
输出结果:
2024/3/20
在C++中也可以像C语言一样将初始化函数封装到类中
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _year;
int _month;
public:
void Init(int year = 2024, int month = 3, int day = 20)
{
_day = day;
_year = year;
_month = month;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;
d.Init();//全缺省不给实参默认为缺省值
d.print();
return 0;
}
输出结果:
2024-3-20
但是,这种方法每次对成员变量进行初始化都需要调用Init()
函数,难写不说还容易忘记,所以在C++中支持在对象实例化的同时调用的一种函数,该函数即为构造函数,此时上面的代码可以改成
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;//对象实例化时自动调用构造函数,不给任何参数时使用全缺省值
d.print();
return 0;
}
输出结果:
2024-3-20
构造函数使用
在C++中,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数的主要任务并不是开空间创建对象,而是初始化对象
C++中的构造函数有下面的特点:
- 构造函数与类名相同
- 构造函数无返回值(没有也不能写
void
) - 对象实例化时会自动调用对应的默认构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
- 默认构造函数不会对内置类型做任何改变,但是对于自定义类型来说,会调用自定义类型的构造函数。但是在,C++11 中针对内置类型成员不初始化的缺陷,内置类型成员变量在类中声明时可以给默认值
📌
无参构造函数、全缺省构造函数以及编译器默认生成的构造函数,都可以认为是默认构造函数,并且默认构造函数只能有一个。
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
Date()
{
//无任何参数的也称为默认构造函数
cout << "无参构造函数调用" << endl;
}
//有参但不是缺省参数的构造函数重载
Date(int year, int day, int month)
{
_year = year;
_month = month;
_day = day;
cout << "有参构造函数调用" << endl;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;//不写参数时调用无参构造,不给参数时不用写括号,否则编译器将无法辨认是函数声明是对象实例化
Date d1(2024, 3, 20);
d.print();
d1.print();
return 0;
}
输出结果:
无参构造函数调用
有参构造函数调用
-858993460--858993460--858993460
2024-3-20
在上面的代码中,默认构造函数并没有对成员变量做出改变,在实例化后,成员变量中的值为随机值,该现象也同样使用编译器默认生成的构造函数
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
//编译器自动生成构造函数
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;//不写参数时调用编译器自动生成的构造
d.print();
return 0;
}
输出结果:
-858993460--858993460--858993460
📌
需要注意的是,因为默认构造函数一个类中只能有一个,所以全缺省构造函数和无参构造函数不能同时被运行调用,因为编译器无法区分调用哪一个默认构造函数
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
Date()
{
//无任何参数的也称为默认构造函数
cout << "无参构造函数调用" << endl;
}
//全缺省默认构造函数
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;//不写参数时调用无参构造以及编译器自动生成的构造
d.print();
return 0;
}
报错信息:
类 "Date" 包含多个默认构造函数
构造函数细节知识
- 对于调用无参或者全缺省的默认构造函数的理解
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;
//不可以理解为d.Date();
d.print();
return 0;
}
输出结果:
在上面的代码中,因为实例化的对象并没有传递参数,所以使用构造函数的缺省值,但是注意对比下面的print
函数,print
函数时对象在显式调用,但是不建议理解为构造函数也是对象显式调用,即d.Date();
理解是不准确的,因为Date d;
本身就是在创建对象,此时对象还没出现,如果直接理解为显式调用,那么和前面显式使用init()
初始化对象没有区别
- 成员变量给初始值的理解
#include <iostream>
using namespace std;
class Date
{
private:
int _year = 0;
int _month = 0;
int _day = 0;
public:
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;
d.print();
return 0;
}
输出结果:
2024-3-20
在上面的代码中,成员变量都给了初始值0,但是这里并不是初始化,而是类似全缺省函数一样给的缺省值,因为前面的知识已经说明类中的成员变量只有在实例化之后才有实际空间。如果此处理解为初始化,因为类还没有被实例化没有实际的地址空间,这个变量将没有空间存放,而初始化则是将初始化的值存入变量对应的地址空间,则与初始化的含义相违背
析构函数
在使用C语言实现一些需要动态内存管理开辟的空间时,都需要写一个Destroy
函数,再最后程序结束之前调用该函数来释放对应的空间,如果没有释放空间可能会因为程序长时间运行导致出现内存泄漏。但是在一些需要大量调用销毁函数的时候容易忘记调用Destroy
函数最后导致内存泄漏。
在C++中,为了解决上面的问题,出现了析构函数,析构函数的作用是销毁对象中资源的清理工作
📌
注意,此处是对象中的资源清理工作,并不是直接销毁对象,因为对象是在当前函数栈帧空间销毁时才销毁
析构函数使用
析构函数:与构造函数功能相反,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
析构函数的特点:
- 析构函数名是在类名前加上字符
~
。 - 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include <iostream>
using namespace std;
//顺序表类
class SeqList
{
private:
int* data;
int _size;//有效数据个数
int _capacity;//顺序表容量大小
public:
//构造函数初始化
SeqList(int capacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (!tmp)
{
perror("malloc fail");
}
else
{
data = tmp;
}
_size = 0;
_capacity = capacity;
}
//析构函数释放空间
~SeqList()
{
free(data);
_size = 0;
_capacity = 0;
}
};
int main()
{
SeqList s;//对象实例化时会调用构造函数
//对象准备销毁时会调用析构函数
return 0;
}
- 对于编译器生成的默认析构函数,不处理内置类型,对自定义类型成员调用它的析构函数
#include <iostream>
using namespace std;
class Time
{
private:
int _time;
public:
Time()
{
_time = 0;
}
~Time()
{
}
};
class Date
{
private:
int _day;
int _month;
int _year;
Time _t;
public:
Date(int day = 20, int month = 3, int year = 2024)
{
_day = day;
_month = month;
_year = year;
}
};
int main()
{
Date d;
//因为d对象实例化时会创建对象_t,所以在对象d销毁之前,需要销毁d对象的资源和_t对象中的资源,而因为d对象没有显式的析构函数,此时编译器会默认生成析构函数,在销毁自定义类型对象_t时d对象的默认析构函数会调用_t对象的析构函数销毁其中的资源
return 0;
}
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成内存泄漏
拷贝构造函数
拷贝构造函数使用
在对象实例化时,有时可能需要创建出两个一模一样的对象,如果多次实例化,那么每一次传参就会变得很麻烦,为了解决这个问题,C++的默认构造函数中提供一个构造函数为拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const
修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
📌
使用const
关键字进行修饰可以防止被拷贝对象被修改
拷贝构造函数的特点:
- 拷贝构造函数是构造函数的一个重载形式
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
//初始化构造函数
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
//拷贝对象
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
//第一个Date对象
Date d1(2024, 3, 21);
Date d2(d1);//拷贝d1对象的值
//也可以写成下面的形式
Date d3 = d2;
d1.print();
d2.print();
d3.print();
return 0;
}
输出结果:
2024-3-21
2024-3-21
2024-3-21
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
#include <iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
//初始化构造函数
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
//拷贝对象
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
//第一个Date对象
Date d1(2024, 3, 21);
Date d2(d1);//拷贝d1对象的值
d1.print();
d2.print();
return 0;
}
报错信息:
类 "Date" 的复制构造函数不能带有 "Date" 类型的参数
在上面的代码中,拷贝构造函数的形参为Date
类型的对象,Date
类型的对象需要拷贝时将再次调用Date
类型的拷贝构造,此时因为Date
类型的拷贝构造形参依旧是Date
类型的对象,故继续调用Date
类型的拷贝构造,如此往复无穷进行,如下图所示
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝,而深拷贝是指具体的拷贝方式需要自行实现,例如栈、链表等的拷贝
📌
注意,拷贝构造函数中的形参不可以使用指针类型,虽然实现的结果相同,但是此时编译器不认为是拷贝构造函数,而只是一个普通的成员函数
- 因为编译器会默认生成拷贝构造函数,而默认的拷贝构造与前面的析构和构造不同,拷贝构造会对内置类型按照字节方式直接拷贝,而对自定义类型来说,与前面的析构和构造相同,都会调用对应类型的拷贝构造函数,但是如果自定义类型没有实现拷贝构造,那么将直接按照字节序进行拷贝
在日期类中,可以直接使用默认的拷贝构造函数而不需要自己去实现,但是对于下面的情况则需要自己实现拷贝构造
#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
在上面的代码中,定义了一个栈类,而栈类中的成员变量有一个指针类型和两个内置类型,当Stack s2(s1)
需要调用拷贝构造函数时,因为Stack
类中并未实现拷贝构造,编译器会默认生成拷贝构造,此时将按照字节方式进行浅拷贝,此时对于内置类型的_size
和_capacity
的拷贝不会有任何问题,但是对于*_array
来说,因为进行字节方式的浅拷贝,所以s2
对象中的*_array
和s1
对象中的*_array
都指向同一块空间,而因为_size
和_capacity
是各自独有的空间,s1
对象中的改变不影响s2
对象,现在插入了4个数据,s1中的_size = 4
,s2
对象中的_size
依旧为0,如果s2
需要插入数据,那么将从栈底开始插入,此时会覆盖掉原来s1
中的数据。
另外,因为s1
和s2
对象都出现了资源申请,所以最后在程序即将销毁两个对象时会调用析构函数进行对象资源的释放,因为s2
对象后实例化,所以在资源清理时先进行清理,此时会释放掉s2
中*_array
指向的空间,此时s1
对象中的*_array
指向的空间也同时被销毁,但是s2
的*_array
置为了空指针,而s1
的*_array
并不是空指针(依旧指向原来空间的位置),当释放完s2
的资源时将释放s1
的资源,此时释放s1
的资源时将出现非法访问,因为s1
对象的*_array
指向的空间已经归还给操作系统了,从而会出现程序运行崩溃
综上所述,在对象有资源申请时,需要自行实现拷贝构造,否则直接使用编译器自动生成的拷贝构造函数即可
拷贝构造函数使用实例
#include <iostream>
using namespace std;
//生成指定天数后的某一天
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2024, int month = 3, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int GetMonthDays(int year, int month)
{
int monthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0) && (year % 100 == 0) || (year % 400 == 0)))
{
return monthDay[month] + 1;
}
return monthDay[month];
}
//获取指定天数后的某一天
//返回引用
Date& GetAfterXDate_pe(int days)
{
_day += days;
while (_day > GetMonthDays(_year, _month))
{
_day -= GetMonthDays(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;//返回调用本函数的对象的引用
}
//返回自定义类型值
Date GetAfterXDate_p(int days)
{
Date tmp(*this);//使用本类的拷贝构造复制对象中的内容
tmp._day += days;
while (tmp._day > GetMonthDays(tmp._year, tmp._month))
{
tmp._day -= GetMonthDays(tmp._year, tmp._month);
tmp._month++;
if (tmp._month == 13)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;//返回自定义类型的值
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
Date d;//当天的日期
d.print();
d.GetAfterXDate_p(100).print();//获取一百天以后的当天,将返回值作为对象调用print函数,但是不改变初始的日期
d.print();
cout << endl;
Date d1;
d1.print();
d1.GetAfterXDate_pe(100).print();//获取一百天后的当天,但会改变初始的日期
d1.print();
return 0;
}
输出结果:
2024-3-20
2024-6-28
2024-3-20
2024-3-20
2024-6-28
2024-6-28
在上面的代码中,对于返回对象的引用操作并不是非法引用,尽管this
指针的作用域只在成员函数中,但是因为this
指向的是实例化的对象,而实例化的对象只有在其所在空间被销毁才会销毁,所以可以理解为this
指针并未被销毁,所以可以返回this
的引用操作
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
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(2022,1,13);
Test(d1);
return 0;
}
输出结果:
Date(int,int,int):000000B51D1DFC58
Date(const Date& d):000000B51D1DFD48
Date(const Date& d):000000B51D1DFD94
~Date():000000B51D1DFD48
~Date():000000B51D1DFD94
~Date():000000B51D1DFC58
在上面的代码中,首先创建了一个d1
对象,此时会调用Date
类中的构造函数,接下来Test
函数传入实参为自定义类型对象d1
,因为Test
函数的形参是自定义类型,并且是值传递,所以此时形参d
需要拷贝传入的实参对象d1
的内容,故会调用Date
的拷贝构造函数进行对象拷贝,在Test
函数体中出现了temp
对象的创建和拷贝,故此时依旧会调用拷贝构造函数拷贝d
对象中的内容给temp
对象,所以总共调用了两次拷贝构造函数,而因为temp
对象出了Test
函数将进行销毁,故会调用Date
类中的析构函数,而因为形参d
也需要被销毁,故也会调用Date
类的析构函数,最后就是d1
对象的销毁,该对象销毁也需要调用析构函数,故总共有三次析构函数的调用
如果将上面的代码中Test函数中的值形参改成引用形参时,在实参传入时将不会调用拷贝函数
#include <iostream>
using namespace std;
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(2022, 1, 13);
Test(d1);
return 0;
}
输出结果:
Date(int,int,int):0000002DC037F748
Date(const Date& d):0000002DC037F834
~Date():0000002DC037F834
~Date():0000002DC037F748
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用