文章目录
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
不写,编译器自动生成;写了就不生成。
构造函数主要完成初始化的工作
析构函数主要完成清理的工作
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另一个对象
取地址重载主要是普通对象和const对象取地址,这两个很少会自己实现
- 我们不实现时,编译器生成的默认构造函数和析构函数,针对成员变量:内置类型不处理,自定义类型会调这个成员对象的构造和析构
- 我们不实现时,编译器生成拷贝构造和operator=,会完成按字节的值拷贝(浅拷贝)
- 也就是说有些类,我们是不需要去实现拷贝构造和operator=的,因为编译器默认生成的可以用。
1.构造函数
构造函数的概念
构造函数—>在对象构造时调用的函数,这个函数完成初始化的工作。注意完成的不是构造!!
特性
- 没有返回值
- 函数名和类名相同
- 实例化时自动调用
- 构造函数可以重载
- 对于当前的类,如果没有显式构造函数,编译器会生成默认无参的构造函数(语法坑,c++的双标),一旦用户显式定义编译器不再生成。而该默认构造函数会做如下的事。
- 针对内置类型的成员变量没有做处理
- 针对自定义类型的成员变量,调用它的默认构造函数初始化。不过对于该成员变量的类来说,默认构造函数初始化有三种。不过对应编译器自己生成的我们没法验证
- 无参的构造函数和全缺省的构造函数是重载,语法上可以同时存在,但是都是默认构造函数。编译错误–编译器无法知道是用哪个默认构造函数
- 调用默认构造函数有三种
- 自己实现无参的构造函数
- 自己实现的全缺省构造函数
- 我们不写,编译器自己生成
- 以上的特点都是不用传参数
- 调用默认构造函数有三种
实例
构造函数的存在场景
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year,int month,int day)
{
//c++要用必须要先声明。但是这里却没有
_year=year;//这里凭空访问_year,这里的参数实际是隐含的this指针.
_month=month;
_day=day;
}
//void Init(Data* this,int year,int month,int day)
//this->_year=year;
void P()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
//编译器处理后实际代码
void P(Date* this)
{
cout<<this->_year<<"-"<<this->_month<<"-"<<this->_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//在没有初始化INIT前调用了,会出现随机值,很危险。c++通过6个默认成员函数来处理
d1.Init(2020,4,7);
Date d2;
d2.Iint(2020,4,7);
d1.Print();
d2.Print();
}
构造函数的重载
class Date
{
public:
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
Date()
{
_year=0;
_month=0;
_day=0;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year,_month,_day;
};
int main()
{
Date d1(2020,4,8);
d1.Print();
//构造函数可以重载不带参数的。
//Note:无参是不能加括号的
Date d2();//error
Date d2;//
d2.Print();
}
全缺省方式及注意
既然有一个写了就没有无参了,要写两个,有没有更好的方式呢。
更加推荐全缺省方式。
class Date
{
//更好的方式---全缺省
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
注意:Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确
//等价
class Date()
{
Date()
{
_year=0;
_month=1;
_day=1;
}
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=month;
}
};
///Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确
int main()
{
Date d1;//可以调用全缺省的也可以调用无参的。这种选择是不能发生的。
d1.Print();
}
注意:对于全缺省和无参的直接使用类名+对象名
实例化,没有类名+对象名()的用法
至于编译通过是编译器把这个当成函数的声明了;
class Date()
{
Date(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
int main()
{
Date d1;
Date d2();
d2.Print();
}
编译器生成的默认构造函数的作用
C++里面把类型分为两类:内置类型(基本类型),自定义类型。
内置类型:int/char/double/指针/内置类型数组
等等
自定义类型:struct/class
的类型
对于自定义类型,回去调用它的默认构造函数(无参数/全缺省)进行初始化。
class Time
{
public:
Time()
{
_hour=0;
_minute=0;
_second=0;
cout<<"Time()"<<endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
d1.Print();//此时发现还是随机值,似乎这个构造函数还不如不存在。好像没做任何事情
//但是我们再来一个类
//此时进行调试,发现_t对象是构造了的,初始化好了,虽然d1的其他变量没有初始化。
}
class A{
public:
A(int a=10)
{
cout<<"A()"<<endl;
_a=a;
}
private:
int _a;
};
class Date{
public:
private:
int _year;
int _month;
int _day;
A _a;
};
int main(void){
Date d1;
}
成员变量的命名风格
如果这样处理,根据就近原则,两个变量都是函数的参数。
class Date
{
Date(int year=0,int month=1,int day=1)
{
year=year;
month=month;
day=month;
}
};
因此一般对成员变量前加_达到区分的目的
class Date
{
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
};
2.析构函数
2.1概念
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
对象生命周期结束的时候调用。
2.2特性
- 析构函数名是在类名前面加上字符
- 无参数无返回值
- 因此无法重载
- 一个类有且只有一个析构函数,若未显式定义,系统会生成默认的析构函数
- 对象生命周期结束时,c++编译系统自动调用析构函数
- 关于编译器自动完成的析构函数,是否会完成一些事情
- 和构造函数的双标特性一样
析构函数存在场景
将类里面的动态开辟的内存清理了。
对于日期类来说,其成员变量在对象生命周期结束时自动释放函数(main
)栈帧,因此析构函数写不写都无所谓。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
~Date()
{
cout<<"~Date()"<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//析构:对象声明周期到了以后,自动调用。完成对象里面的资源清理工作,不是完成d1和d2的销毁。d1,d2的销毁是该对象所在的函数栈帧结束的时候销毁
Date d1;
Date d2;
}
但是对放在堆区上的内存,需要我们在析构函数中手动释放。
构造析构的顺序
至于构造函数的执行顺序和析构函数的执行顺序。考虑到是栈帧。
先构造d1,d2,s1,s2,析构先析构s2,再析构s1
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=month;
}
~Date()
{
cout<<"~Date()"<<endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int n=10)
{
_a=(int*)malloc(sizeof(int)*n);
cout<<"malloc:"<<_a<<endl;
_size=0;
_capacity=n;
}
~Stack()
{
if(_a)
{
free(_a);
cout<<"free:"<<_a<<endl;
_a=nullptr;
_size=_capacity=0;
}
}
private:
int *_a;
int _size;
int _capacity;
};
int main()
{
Date d1;
Date d2;
Stack s1;
Stack s2;
}
编译器生成的默认析构函数的作用
当前类由编译器自动生成的析构函数是否会完成什么事情
- 对当前类的内置类型、基本类型 int/char 不会处理
- 对当前类中的自定义类型,调用它的析构函数
class Time
{
public:
~Time()
{
cout<<"~Time()"<<endl;
}
};
class Date
{
public:
Date(int year=0,int month=0,int day=0)
{
//....
}
private:
int _year;
int _month;
int _day;
};
编译器生成的默认析构函数价值
当一个类包含了其他类,比如leetcode里面的题目的时候,我们可以不写当前的类的默认构造,加入其他类,这样就能自然而然地初始化其他类和销毁其他类
class MyQueue{
public:
void Push(){}
private:
Stack s1,s2;
}
3.拷贝构造函数
3.1概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器调用
3.2特征
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 默认生成的是浅拷贝
- 浅拷贝是存在问题的,对于像Stack这样的类,会导致同一块空间析构的时候释放两次。所以有些类是要手动实现深拷贝。
- 假如不析构就会产生内存泄漏,因此一定要浅拷贝。
3.3拷贝构造的无穷递归
原因是这样的,如果没有&,当调用拷贝构造的时候,由之前的函数栈帧的学习中,对于拷贝构造函数的参数,将d1拷贝给参数发生的。而拷贝这个过程就是拷贝构造函数要进行的过程。因此发生了语义上的无穷递归。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//Date d2(d1)
Date(Date d)//error:存在递归拷贝
{
_year=d._year;
_month=d._month;
_day=d._day;
}
private:
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
Date d1(2020,5,12);
Date d2(2020,5,12);//如果上面发生更改,下面也要改。就很恶心
//于是产生了拷贝构造
Date d2(d1);<----->Date d2=d1;//***
}
下面说明的是,进行func
的时候,需要将d1拷贝给参数d。
于是将d1拷贝给参数d的这个过程,需要调用拷贝构造函数。
于是可想而知,假如func
函数是拷贝构造函数,就会存在自身无穷地调用自身。
而进行引用作为参数的时候,是不进行传值拷贝的
void func(Date d)
{
}
void fund(Date& d)
{
}
int main()
{
Date d1(2020,5,12);
func(d1);//这里调试可以发现跳到Date的拷贝构造函数
fund(d1);//这里调用直接就进入fund了,因为是引用
}
递归的过程:
//Date d2(d1) //对象初始化的时候自动调用构造函数(此时为拷贝构造)
//调用之前先传参:Date d(d1)--->此时的传参又发生拷贝构造。如此就递归下去了
Date(Date d)//error:存在递归拷贝
{
_year=d._year;
_month=d._month;
_day=d._day;
}
由此说明传值的时候要用拷贝构造
因此解决方案是:传引用来处理。&d和&d1的地址同。
Date(Date& d)//sol:引用解决
{
_year=d._year;
_month=d._month;
_day=d._day;
}
3.4加上const的原因
同时忘了防止自己不小心赋值反了,把传引用的部分用const。导致拷贝错误还改变了自身的值。
Data(const Data& d)
{
d._year=_year;
_month=d._month;
_day=d._day;
}
3.5编译器默认生成的的拷贝构造——浅拷贝
因此对于malloc
的类我们要手动实现进行深拷贝。
class Date
{
public:
Date(int year=0,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year=0;
int _month=0;
int _day=0;
};
int main()
{
Date d1(2020,5,12);
Date d2(d1);
}
浅拷贝是传值拷贝,所以对于指针相关的就会导致将指针的值传过去。也就是指向了同一个地址。因此会导致同一块空间析构两次,abort
操作系统发送信号终止进程。
class Stack
{
public:
Stack(int n=10)
{
_a=(int*)malloc(sizeof(int)*n);
cout<<"malloc:"<<_a<<endl;
_size=0;
_capacity=n;
}
~Stack()
{
if(_a)
{
free(_a);
cout<<"free:"<<_a<<endl;
_a=nullptr;
_size=_capacity=0;
}
}
private:
int *_a;
int _size;
int _capacity;
};
int main()
{
//浅拷贝问题
Stack st1(10);
Stack st2(st1);
Stack st3(30);
//st1=st3;(赋值operatro=)
}
3.6编译器默认生成的拷贝构造作用
默认生成的拷贝构造:
- 对于内置类型成员,会完成按字节序的拷贝(浅拷贝)
- 对于自定义类型成员,会调用该成员的类拷贝构造函数。
拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理细节是不一样的,这个和构造和析构是不一样的
#include<iostream>
#include<malloc.h>
using namespace std;
class A{
public:
A(){}
A(const A& a){
cout<<"拷贝:A()"<<endl;
}
};
class Data{
public:
Data(int year=1,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
~Data()
{
cout<<"~Data()"<<endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Data d1(2022,1,1);
Data d2(d1);
return 0;
}