目录
一、类的默认成员对象:
1、定义:
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我 们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值
2、构造函数:
定义:✨构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化 对象。
构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的 特点就完美的替代的了Init。
注意:因为是初始化,所以在该类类型的对象被创建时进行的; ✨✨
>>特性:
①函数名与类名相同✨
②⽆返回值(个人认为可以说是无返回类型更好记忆)✨
如下:就是一个构造函数
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
③对象实例化时系统会⾃动调⽤对应的构造函数 ✨
④构造函数可以重载。分为:无参构造函数、全缺省构造函数、带参构造函数✨
👉👉如下:在日期类里面写了三个构造函数
⑤如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成✨
⑥⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在!无参和全缺省因为存在歧义,所以不能同时存在,但是是🧑🎓支持重载的;
✨✨记住:不需要传参就可以调用的构造函数就叫默认构造函数。
⑦ 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,根据编译器来进行的。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。✨✨如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,在下里面会讲到✨
总的来说最好自己写一个默认构造函数,推荐全缺省构造函数,可以传参也可以不传参进行
>>使用:
1.无参构造调用:
🧑🎓🧑🎓这里后面不能加括号,加了会导致和函数声明分不清的,给人一种Date类型d1函数名()传空的函数的意思
2.有参构造调用:
🧑🎓🧑🎓 在变量名后面加括号直接传参
3.全缺省构造调用:
🧑🎓🧑🎓可以写括号(传参),也可以不写括号(不传参),可以说是上面的结合进阶版;
>>总结:
✨👉👉默认构造大多数情况要自己写,写全缺省默认构造最好,少数情况可以不写✨✨
✨👉👉如:MyQueue类(2个栈实现队列)中,他的成员变量是2个Stack栈类,这两个栈已经写过了默认构造函数,此时编译器会去调Stack的构造为其初始化🧑🎓在MyQueue中不需要再写默认构造函数✨✨
✨👉👉一个类中有且只有一个默认构造函数函数✨✨
typedef int STDataType;//数据类型
//栈
class Stack
{
public:
//构造函数
Stack(int n =4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if(_a==NULL)
{
perror("malloc file:");
return;
}
//初始化
_top = 0;
_capacity = n;
}
void prin()
{
cout << _top <<" " << _capacity << endl;
}
//析构函数:相当于Destroy,销毁资源
//完成对象中资源的清理释放⼯作
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;//数据
int _top;//栈顶元素
int _capacity;//空间容量
};
class MyQueue
{
public:
void print()
{
cout << "s1:" << endl;
s1.prin();
cout << "s2" << endl;
s2.prin();
}
//显示写析构了,也会去调用自定义类型的析构,进行清理,怕你乱搞
~MyQueue()
{
}
private:
Stack s1;
Stack s2;
};
int main()
{
MyQueue q1;
q1.print();
}
3、析构函数:
定义:析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能(销毁动态空间),⽽像Date(日期类)没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
>>特性:
①析构函数名是在类名前加上字符 ~✨
②无参数无返回值(不需要写返回类型),不需要调用,会自动调✨
③一个类只允许有一个析构函数,若显示定义,系统会自动生成默认的析构函数(不是说不要写析构,默认的析构函数只是对于成员变量只有内置类型的类有用,对于有资源的申请就需要自己析构函数,因为函数结束时,该变量也会随着栈帧一起销毁)✨
👉👉如下:动态内存的销毁就需要我们主动写析构函数
④对象⽣命周期结束时,系统会⾃动调⽤析构函数✨
⑤跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他自己的的析构函数✨
typedef int STDataType;//数据类型
//栈
class Stack
{
public:
//构造函数
Stack(int n =4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if(_a==NULL)
{
perror("malloc file:");
return;
}
//初始化
_top = 0;
_capacity = n;
}
void prin()
{
cout << _top <<" " << _capacity << endl;
}
//析构函数:相当于Destroy,销毁资源
//完成对象中资源的清理释放⼯作
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;//数据
int _top;//栈顶元素
int _capacity;//空间容量
};
class MyQueue
{
public:
void print()
{
cout << "s1:" << endl;
s1.prin();
cout << "s2" << endl;
s2.prin();
}
//显示写析构了,也会去调用自定义类型的析构,进行清理,怕你乱搞
~MyQueue()
{
}
private:
Stack s1;
Stack s2;
};
int main()
{
MyQueue q1;
q1.print();
return 0;
}
⑥我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他自己(自定义类型成员)的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数 ✨
总之🧑🎓🧑🎓当我们给其MyQueue显示的写了析构函数,祖师爷为了防止重复析构的现象发生,编译器不会调用该析构而是继续调用自定义成员变量的析构函数
⑦如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数✨
⑧⼀个局部域的多个对象,C++规定后定义的对象先析构✨
第一题,main函数中,构造按顺序来,先a再b进行构造,析构后定义的:b先析构,a再析构
第二题, 有全局的,也有局部的,全局的生命周期是从程序开始到运行结束
✨构造顺序:c->a->b->d; 主要是static关键字,static修饰的变量,当程序执行到该语句时,才创建该变量,并将其生命周期改成全局变量的生命周期✨
✨析构顺序:b->a->d->c;、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构,先看局部,先定义后析构,再看全局,先定义后析构
static修饰的变量,相比于全局后定义✨
总:构造是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构🧑🎓
>>使用:
不需要调用,对象在销毁时,自动跟着销毁了
>>总结:
✨👉👉析构函数只有在使用了资源的情况才需要显示化写析构函数,否则不需要写
✨👉👉析构函数不需要去我们主动调用,会在该对象被销毁时(对象生命周期结束时)自动调用
✨👉👉一个类只允许有一个析构函数
✨👉👉对于一个类的成员函数有自定义类型的变量时,该类会自动调用成员函数的析构,我们在该类中写了析构函数,编译器也会忽略,自动去调用自定义类型成员变量的析构,防止对资源重复释放
4、拷贝构造函数:
定义:✨如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值(缺省值),则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
>>特性:
①拷⻉构造函数是构造函数的⼀个重载✨
🤔🤔因为不会修改d的值,所以用一个const去缩小权限,保证不会修改d
Date(const Date& d)
②⻉构造函数的参数第一个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤✨
👉👉 如下:如果我们再main函数使用拷贝构造,那么此时d1的值作为参数传给函数中的形参d(Date d = d1)(这个是拷贝构造调用的另一种写法:Date d2 = d1) 类似对形参d再次进行拷贝构造,又会去继续调用形参d的拷贝构造,一直反复调用下去,陷入无限递归🐸🐸
③C++规定:⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,这⾥⾃定义类型传值、传参和传值返回都会调⽤拷⻉构造完成。但是为了避免不好的情况,建议引用传参✨
④若未显示定义,编译器会自动生成拷贝构造函数,自动生成的拷贝构造对内置类型的成员变量会完成值拷贝或者浅拷贝(一个一个字节进行拷贝,在c语言中结构体的拷贝就是值拷贝),自定义类型成员变量会调用自定义类型他自己的拷贝构造✨
⑤像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。✨像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造
如下:🤔为啥对于有指向指向资源的成员变量需要用深拷贝
🐸👉拷贝后st1和st2指针_a指向同一个空间,那么就会析构2次,但是不能对一块空间析构2次;之前我们在c语言学习中,int* a=n,int* b = a;相当于b和a指向的是n这一个空间,对b和a其中一个解引用进行修改就可以导致n改变
🐸👉 深拷贝会在创建一个空间,将st1的内容拷贝到st2的_a的空间里面
总结:若一个类显示的实现了析构函数并释放了资源,那么他就要显示的写拷贝构造函数
⑥传值返回会产⽣⼀个临时对象的拷贝,也会调⽤拷⻉构造(会增加消耗),传值引⽤返回,返回的是返回对象的别名(引⽤)没有产⽣拷⻉(提升效率)。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的。这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回✨
>>使用:
如上图,2种写法都是调用拷贝构造,但是更多人喜欢第一种,好理解;
>>总结:
✨👉👉拷贝构造就是利用一个已有的对象去初始化一个要创建的对象
✨👉👉第一个参数必须时类类型的引用,为了防止不断拷贝构造
✨👉👉对于没有使用资源的类,我们不需要写拷贝构造,编译器自动生成的就可以用,但是当使用了资源时,必须自己写拷贝构造函数。也就是看析构函数是否显示化,若显示化,必须写拷贝构造函数。
✨👉👉函数返回值会调用拷贝构造/返回引用不会调用拷贝构造,且引用返回可以减少拷贝,但一定要保证返回对象在函数结束后仍然存在,才能用引用返回
还有几个默认函数会在下进行介绍
二、运算符重载:
✨✨当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
✨一个类需要哪些运算符重载,看运算符重载后对于类来说是否有意义,⽐如Date类重载operator-就有意 义,但是重载operator+就没有意义
关键字:operator
不推荐下面这种传参,推荐引用传参
✨✨对于运算符重载来说: .* :: sizeof ?: . 这四个运算符不能进行重载!!!!✨✨
类访问重载运算符:
①将类的权限改为公有✨
👉👉公有的权限可以让我们在类外使用它,但是不推荐,因为C++分装后对象如果让你随便改的话没啥意义了
②在类里面提供Get函数(只获取该变量的值,不能修改)
✨获取到值进行比较,调用的话和普通调用函数一样,函数名和传参,也支持下面③的第二种用法
③重载成成员函数:这里只写了一个参数,因为this指针隐藏了,this指针是第一个参数,下面我们写的参数本质还是第二个参数
2种调用方法,哪种舒服用哪种
👉✨👉如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
④友元函数:friend
👉✨👉在类里面声明一下,这个是我的朋友,即这个函数是该日期Date类的友元函数,该函数可以使用类中的所用成员👉✨👉是所有成员
✨👉调用方式和提供Get那里一样,这里因为有了权限就不需要所谓的Get函数了,直接用.访问成员,需要注意该函数的是在全局域里面,不是在类里面定义的!!
写法比较特殊的运算符重载:
1、前置++、后置++
🐸前置++,如下:这里只是告诉你写法,正常情况不能这么直接写*this + 1,因为自定义类型的运算,只要涉及运算符都需要重载,我这个是截取的一小段,所以不会报错,+的重载我写好了,因为内容太多,所以不截取过来了,🧑🎓🧑🎓后面会放到日期类里面好好讲讲🧑🎓🧑🎓这里的Date::想必不陌生吧,就是说这个函数是哪个域的(告诉编译器在Date类域)用到了之前学的,在类域外定义函数
🐸后置++:因为我们要先使用在加1,所以这里只能用Date作为返回值,返回未相加前的值,还要要对该this指针进行+1,这里用到了符合运算符+=重载;区别就是参数列表里面有一个参数,当然也可以写变量名,但是没必要,因为该参数只是区别前置和后置的,并不会接收这个参数
总结:C++中前置比后置的效率更高,一般能用前置就用前置。但是C语言中没区别。
🧑🎓🧑🎓--就是换个符号的事情
2、<< 和 >>流插入和流提取:
✨内置类型能够自动识别因为在库里面以及实现了,我们也可以看出实现方式就是运算符重载✨
🧑🎓🐸<<写在类里面作为成员函数:返回值是cout的类型 ostream,这里用的引用返回,为了支持连续插入
ostream& operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
return out;
}
👉 写在类里面,下面调用会出错,因为成员函数里面有一个隐藏的this指针作为第一个参数,我们之前所过运算符重载,✨✨左操作数是第一个参数,右操作数是第二个参数✨✨
🧑🎓🧑🎓像下面这么写法才行,但是对于我们来说看着蛮奇怪的,倒反天罡了,不喜欢这个,所以我们不推荐这个写法,推荐写全局函数🐸🐸
全局函数:要注意,传类类型的形参要用引用传参,不然会发生拷贝构造
//全局函数
ostream& operator<<(ostream& out,const Date& d)
{
out << d._year << "/" << d._month << "/" <<d._day << endl;
return out;
}
🐸注意的是全局函数里面的,d无法访问成员变量,这里我们就用到了上面访问那说的友元函数,在类里面声明,告诉类:我是你的朋友,让类外面的可以访问类里面的东西。
👉✨这下就可以对自定义类型进行流插入操作了
对于流提取 :大差不差,就是传参时,形参d的const不需要了,因为需要对d做出修改
✨总结✨:重载>>/<<时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,但是不符合使用习惯和可读性。
三、赋值运算符重载:
✨赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟 拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
>>特性:
①赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数(写在类里面作为类的成员函数)✨因为这里不是拷贝构造函数,不会出现不断自己调自己的现象,所以可以传值传参
✨ 赋值运算重载的参数建议写成 const 当前类类型引⽤,因为会传值传参会产生额外的拷贝,所以用引用类型,而const是为了保证不会修改d的值
②有返回值,可以写成值返回,但是建议写成当前类类型引⽤,引⽤返回可以提⾼效率
在C语言中,支持连续赋值,当然C++的赋值运算符重载也支持;C语言:
✨有返回值⽬的是为了⽀持连续赋 值场景
✨如:先将d3赋值给d2,调用完以后,是不是相当于d1接收他们两个的返回值,也就是说将d3赋值给d2的返回值赋值给d1,继续进行赋值重载,也就完成了
③没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型 成员变量会调⽤他的拷⻉构造。
🐸👉在类成员变量中全是内置类型且没有指向什么资源时,不需要显示写赋值重载;
✨✨口诀:有显示的析构函数就要显示写赋值重载和显示写拷贝构造🧑🎓🧑🎓
>>使用
方法都是大差不差;
与拷贝构造区别:
🤔拷贝构造:将已有的对象初始化一个要创建的对象
d2是进行拷贝构造函,最后一行是赋值
🤔赋值重载:2个对象,将其中一个对象赋值(拷贝)给另一个对象
四、取地址运算符重载:
1、const成员函数:
🐸🐸将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 ⾯。✨✨ const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
因为我们不能将隐式的this显示化,所以祖师爷写了个偏方,在函数参数列表后面加const,就将this的权限缩小了
👨👨有一个好处,因为权限可以平移,缩小,所以这个给了const修饰的this,一样可以接收普通对象,当然不并不是所有函数都要加const,但是不需要修改的对this指向对象进行修改的+const
2、取地址运算符重载:
🐸🐸取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址
①普通取地址运算符重载
②const取地址运算符重载:
✨✨一般都不要我们去写,当写了取地址运算符重载函数,编译器会根据该对象选择最佳的那一个函数,当然了普通对象没有写,不会去调用下面这个const的,因为编译器会默认生成,会自动调用这个默认的
四、总结:
继 续 努 力