前言
一、类的默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:
• 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
• 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
二、构造函数
2.1 概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动⽣成⼀个无参的默认构造函数,一旦用户显式定义编译器将不再生成
对于以下Date类:
class Date
{
public:
void Init(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.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数应运而生
#include<iostream>
using namespace std;
类的默认成员函数
class Date
{
public:
1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1; 1和2构成函数重载
}
2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
3.全缺省构造函数 语法上1和3也构成函数重载(函数名相同,参数不同)
如果调用时会产生歧义
Date(int year = 1, int month = 1, int day = 1) //写了这个也就不需要无参构造函数了
{
_year = year;
_month = month;
_day = day;
}
**我们平时使用全缺省构造即可**
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
不需要写析构,没有资源去释放;
系统自动生成的析构函数对内置类型成员不做处理
自定义类型成员(class,struct,union)会调用它们的析构函数
内置类型:int/char/double/..../指针
自定义类型:class/struct/union...
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Data d1();//error
// 调⽤默认构造函数
// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不能跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)
Date d2(2025, 1, 1); // 调⽤带参的构造函数
Date d3();调用全缺省默认构造函数
d1.Print();
d2.Print();
d3.Print();
return 0;
}
在栈类里面,我们可能经常忘记初始化和销毁,有些地方写起来繁琐
我们就可以用构造函数来代替 void stackinit();
class Stack
{
public:
//Stack()无参构造函数
//{
// cout << "Stack()" << endl;
// _a = (int*)malloc(sizeof(int) * 4);
// if (nullptr == _a)
// {
// perror("malloc申请空间失败");
// return;
// }
// _capacity = 4;
// _top = 0;
//}
Stack(int capacity = 4)全缺省构造函数和无参构造函数语法上构成函数重载
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_top = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity;
};
int main()
{
Stack st1;调用无参构造函数
//st1.Init();不需要初始化了,编译器自动调用构造函数初始化
Stack st1();如果通过无参构造函数创建对象时,对象后面不用跟括号,
否则就成了函数声明,编译器无法区分st1是对象还是函数名
Stack st2(10);调用全缺省构造函数
如果无参和全缺省构造函数同时存在语法上可以,但是调用时会有歧义:
对重载函数的调用不明确
**一般使用全缺省构造函数就行,它可以代替无参构造函数**
- ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。默认构造函数是编译器默认⽣成那个叫默认构造,其实不是这样的,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。
对于第五点:
5. 如果类中没有显式定义构造函数,则C++编译器会自动⽣成⼀个无参的默认构造函数,一旦用户显式定义编译器将不再生成
编译器自动生成的默认构造函数:
- 对于内置类型成员,不做处理,不会初始化
- 对于自定义类型成员,回去调用它的默认构造
C++把类型分成内置类型(基本类型)和⾃定义类型。
- 内置类型就是语⾔提供的原生数据类型,如:int/char/double/指针等
- 自定义类型就是我们使⽤class/struct/union等关键字自己定义的类型。
内置类型:
注:
编译器自动生成的默认构造函数对于内置类型成员:
- a. 有的编译器会处理,有点不会处理
- b. c++11在这里打了补丁,声明成员变量时可以给缺省值,编译器默认生成的构造函数可以用缺省值进行初始化
在vs2022,vs2019下,如果只给了int* -a缺省值为nullptr;
那么后面的内置类型成员变量也会置0;
在vs2013下就不会,那个成员变量给了缺省值那个初始化不会捎带其他的成员变量;
结论:
- 一般情况下,构造函数都需要我们自己写;
- 如果内置类型成员都有缺省值,且符合我们的初始化需求
- 全是自定义类型的构造,且这些类型都定义了默认构造
Eg:
定义一颗树根结点
class TreeNode
{
public:
//后面初始化列表
//TreeNode(int val = 0)
// :_val(val)
// , _left(nullptr)
// , _right(nullptr)
//{
//}
TreeNode(int val = 0)//全缺省构造函数
{
_val = val;
_left = nullptr;
_right = nullptr;
}
private:
TreeNode* _left;
TreeNode* _right;
int _val;
//这里一般就要自己写构造函数,因为要对成员变量进行初始化
};
class Tree
{
private:
TreeNode* _root = nullptr; //这个就是第2种情况
};
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Tree t1;
MyQueue q;直接调用Stack的默认构造不需要再写
Date d;//编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数。
}
- 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决.
三、析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,⽐如局部,全局对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值类型。
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
-
跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可,自定义类型成员会调⽤他的析构函数。
-
还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
如果类中没有动态申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;
如果需要释放资源的成员都是自定义类型,默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;
但是有动态资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack; -
⼀个局部域的多个对象,C++规定后定义的先析构。
默认生成的析构函数不会对内置类型进行清理;~stack
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可
四、拷贝构造函数
4.1 概念
在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
如果⼀个构造函数的第⼀个参数是自身类类型的引用(一般常用const修饰),且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷⻉构造是⼀个特殊的构造函数,当已存在的类类型对象创建新对象时由编译器自动调用
4.2 特性
拷贝构造函数也是特殊的成员函数
其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
-
C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
内置类型没有规定,直接拷贝即可 -
若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(Date d) error “Date”: ⾮法的复制构造函数:
//第⼀个参数不应该是“Date”
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void func(int i)
{
//...
}
void func(Date d)
{
//...
}
int main()
{
Date d1(2024,12,29);
Date d2(d1);//拷贝构造
Date d2 = d1;//拷贝构造
这里可以不写拷贝构造,用编译器默认生成的就行
、内置类型成员变量会完成值拷⻉/浅拷⻉
func(10);//直接拷贝即可对内置类型没有要求
func(d1);//自定义类型必须调用拷贝构造完成
return 0;
}
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc error");
return;
}
_capacity = capacity;
_top = 0;
}
拷贝构造
//st2(st1)
Stack(const Stack& st)//深拷贝
{
cout << "Stack(const Stack& s)" << endl;
_a = (int*)malloc(sizeof(int) * st._capacity);
if (nullptr == _a)
{
perror("malloc error");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
_top = st._top;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a ;
int _top;
int _capacity;
};
Stack& func()//出了作用域,对象还在可以用引用返回
{
static Stack st;//在静态区
return st;
}
Stack& func()//出了作用域,对象不在不可以用引用返回,
不然会对析构了的st的空间进行拷贝构造
{
Stack st;
return st;
}
Stack func()//虽然效率低,但只能传值返回,c++11右值引用可以解决,移动构造解决
{
Stack st;
return st;
}
int main()
{
Stack st1;
Stack st2(st1);//如果我们不写拷贝构造,会调用编译器默认生成的拷贝构造
但是是浅拷贝,st1,st2指向同一块空间也就意味这这块空间会被析构2次
栈就必须自己实现拷贝构造,深拷贝
Stack ret = func();
}
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
⾃定义类型成员变量会调⽤他的拷⻉构造
会调用Stack已经写好的拷贝构造
深浅拷贝:
- 浅拷贝
Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝浅拷贝就是一个一个字节的去拷贝,把st1的_a的地址也拷贝了过来,就会导致st1和st2里面的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
- 堆数组空间会被释放2次; (st2使用完堆数组空间,就会把申请的堆数组空间还给操作系统,操作系统就可能会把这块空间给其他对象申请使用,这是st1再次释放空间,注意就导致其他对象造成野指针访问)
- 一个对象(st2)修改数据会影响另外一个(st1);
- 深拷贝
不仅仅拷贝对象中的数据,指向资源也要拷贝一份 就是会把st1中的资源全部拷贝一份,并且不是占用同一块空间
传值返回和传引用返回
传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数栈帧结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。
传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数栈帧结束后还在,才能⽤引⽤返回。
总结
取地址重载放在下一个章节介绍