目录
前言
运算符重载
如何写出运算符重载?
默认成员函数之赋值重载
特殊情况:前置++与后置++
特殊情况:友元与"operator<<"
内部类
const成员函数
关于const权限放大与缩小的问题
取地址及const取地址重载
再谈构造函数
初始化列表
初始化列表使用的实际场景
static成员函数/变量
类的隐式类型转换及其控制
编译器对构造的一些优化
前言
在前面我们了解到了关于类和对象中的三大默认成员函数,也就是构造函数、析构函数、拷贝构造
类和对象之默认三大成员函数(构造函数,析构函数,拷贝构造)_类的构造函数和成员函数-CSDN博客
接下来这一篇文章我们需要了解的是关于运算符重载与默认成员函数中的赋值拷贝,以及关于类和对象的一些补充内容,接下来就让我们开始吧!
运算符重载
c++和c语言之间的区别其中比较明显的就是c++中有了类与对象的概念,但当有了类的概念之后我们需要讨论的一个问题就是,如果我们继续像c语言中的一样,大多数操作符只能对内置类型有效,那么会不会导致什么问题?
就比如一个日期类(Date)对象,当我们需要有一个功能是对这个日期类对象进行++(日期+=一天)的时候,我们就需要在这个类里面进行写一个函数进行实现,这个函数实现出来以后我们调用就需要用调用函数的方式进行,但很显然这个函数的功能其实是跟++这个运算符的规则相似的,只是一个用于类,一个用于内置类型,既然它们俩个是相似的,那么我们是否可以把++和函数进行结合一下,这样做的好处就是把新的东西改变,对于人来说熟悉的事物肯定比陌生的事物更容易理解,于是c++中就引入了运算符重载的概念!
在进行下面内容之前,我们需要了解的是什么是运算符重载,运算符好理解,那么什么是重载呢?
我们仔细回忆一下,在学习c++语法的时候我们得知一个概念,即函数重载
函数重载就是两个函数函数名是相同的,但他们的类型不同,那么我们在调用函数的时候就可以根据传的实参的类型不同,从而调用出不同的函数
而上述的类型不同可以理解为以下3点:
1.个数不同
2.相同参数位的参数类型不同
(int add(int,double),其中int是参数1和double参数2)只要两个函数的参数1不同活两个函数参数2不同即可构成函数重载,比如int add(int,int)和int add(int,double)就可构成重载
3.顺序不同,本质其实是相同参数位的参数类型不同
既然函数重载是根据参数的不同赋予同一个函数名不同的功能,那么运算符重载同理,就是根据参数的不同,赋予同一个运算符一个不同的功能
如何写出运算符重载?
如下:
class Date
{public:Date(int year = 1900,int month = 1,int day = 1){_year = year;_month = month;_day = day;}//....Date& operator+=(int day)//参数1是this指针,参数2是int{//...}//....private:int _year;int _month;int _day;};int main()
{Date d1;int day = 1000;d1 += 1000 //+=运算符重载的调用,相当于调用d1.operate+=(1000)
}
也就是:
声明:返回值类型 operator操作符(参数...)
调用:类名 操作符 参数2
需要注意的:
1.运算符重载函数一般是写在对应的类中的,如:我们需要对Date类的+=运算符进行重载,并且左操作数为Date类型,右操作数为int类型,那么我们就需要把这个运算符重载函数定义在类中,而返回值就根据你的需要进行设置,上述重载的是+=调用后左操作数的值发生改变,即返回引用
2.既然是运算符重载,那么重载的符号至少得是运算符
像什么c/c++没有定义得符号就不能重载,例如operator@,这个就不行
3.c++中不能重载得操作符有五个:“?:” “.” “::” “sizeof” “.*”
4.内置类型的运算符不能重载,如operator+(int,int)之类的就不能进行重载
5.运算符重载函数的参数必须有一个是类的类型
6.定义在类中的成员函数隐含一个this指针,类外则没有
默认成员函数之赋值重载
赋值重载在运算符重载中是比较特殊的存在,正如标题所说它其实是一个默认成员函数,也就是我们不实现编译器会自己生成的函数。
编译器默认生成的赋值重载与拷贝构造是一样的,就是对自定义类型调用它的赋值重载,对内置类型不处理(注意:在有些编译器下如果你这个类的成员有自定义类型并且调用了它的赋值重载,那么编译器会顺便对内置类型进行处理,vs下int是处理成0,根据编译器的不同处理的有可能不同,因为标准并没有定义,实际运用中我们理解成不处理即可)
赋值重载总得来说还是属于运算符重载的,那么它的声明和定义都跟运算符重载类似
如下:
class Date
{public:Date(int year = 1900,int month = 1, int day = 1){_year = year;_month = month;_day = day;}//...Date& operator=(Date& d)//参数1是this指针{//实现}private:int _year;int _month;int _day;
};int main()
{Date d1(1900,10,1);Date d2;d1 = d2;//赋值重载的调用,相当于d1.operate=(d2)
}
注意:赋值运算符一定要定义在类中,因为如果不在类中显示写,那么编译器就会生成一个默认的赋值重载
特殊情况:前置++与后置++
为了区别前置++和后置++运算符重载,c++中一般给第二个参数为设置为int,若重载的是后置++,那么需要在参数位中写上int,前置++则不用,如下
class Date
{//...Date& operator++(int);//后置++Date& operator++();//前置++//...
};
特殊情况:友元与"operator<<"
接下来我们需要实现的一个是类的流插入运算符,这个运算符的定义还是比较简单的,接下来直接看以下实现代码
#include <iostream>class A
{
public:std::ostream& operator<<(std::ostream& out){out << _a << " " << _b << " " << _c << std::endl;return out;}
private:int _a = 10;int _b = 20;int _c = 30;
};int main()
{return 0;
}
此时有一个问题,就是我们知道双目运算符的左操作数默认其实是参数位的第一个参数,右操作数是参数位的右操作数,在上述代码中,参数位的第一个参数其实隐含了一个this指针,第二个参数才是ostream类型的对象,于是我们要调用A类的流插入只能如下调用
int main()
{A a;a << std::cout;return 0;
}
但这样子显然是非常别扭的,我们有没有什么方法可以让他像内置类型那样调用流插入呢 ?(左操作数为ostream,右操作数为打印的类型)
其实是可以的,我们把这个运算符重载写到全局即可,此时这个函数就没有隐含的this指针了,我们把参数位第一个参数设置为ostream类型,第二个参数设置为类的类型即可
但全局函数有个问题就是我访问形参的类对象的时候是受访问限定符的限制的,我们该如何拿到私有成员的数据呢?
在c++中有两种方式
1.在类里面定义成员函数,它的功能是获得对象的数据,由于这个函数的访问限定符是公有,那么我们也就可以间接通过这个函数获得私有成员的数据
如下代码:
#include <iostream>class A
{
public:int get_a()const{return _a;}int get_b()const{return _b;}int get_c()const{return _c;}
private:int _a = 10;int _b = 20;int _c = 30;
};
std::ostream& operator<<(std::ostream& out, const A& aa)
{out << aa.get_a() << " " << aa.get_b() << " " << aa.get_c() << std::endl;return out;
}int main()
{A a;std::cout << a;return 0;
}
这个代码是能成功运行的!
2.友元函数
c++中一种新的玩法是把一个函数定义为类的友元函数,友元函数不会受访问限定符的限制
定义格式为:friend 声明格式
如下代码也是能成功运行的
#include <iostream>class A
{friend std::ostream& operator<<(std::ostream& out, const A& aa);
public:
private:int _a = 10;int _b = 20;int _c = 30;
};
std::ostream& operator<<(std::ostream& out, const A& aa)
{out << aa._a << " " << aa._b << " " << aa._c << std::endl;return out;
}int main()
{A a;std::cout << a;return 0;
}
而流提取运算符重载与流插入原理大致相同,这里也就不过多介绍了
实际上,在开发中是不太推荐友元函数的,因为友元函数其实本质是一个普通函数,他除了能访问类的数据和成员函数之外与这个类几乎没什么关系了,不像成员函数,成员函数是属于类的,而友元函数不属于类,这从某种角度来说已经破坏了这个类的封装,原本是属于我这个类的函数才能改变数据,友元函数出现后,普通函数也能改变我的数据了,这使得类和友元函数之间的耦合度变高,开发中一般不推荐这种高耦合度的代码
而除了友元函数之外,还有一个概念是友元类
这种定义与友元函数定义都是定义在类中的
友元类的定义格式为:friend class/struct 类名;
如下即为B类是A类的友元
class A
{friend class B;
public:
private:int _a = 10;
};class B
{
public:B(const A& aa){std::cout << aa._a << std::endl;}
private:
};int main()
{A a;B b(a);return 0;
}
关于友元类的几个特点:
1. 友元类的成员函数是另一个类的友元函数
2.友元关系是单向的,不能传递
在上述代码中,B类是A类的友元类,但A类不是B类的友元类
3.友元关系不能传递
例如C类是B类的友元,B类是A类的友元,但这不能证明C类是A类的友元
内部类
在c++中,是可以在类里面再定义一个类,定义的那个类就是内部类,如下,A为B的内部类
class B
{class A{};
};int main()
{return 0;
}
内部类有几个特点,如下:
1.内部类天生是外部类的友元类,在上述代码中A天生就是B的友元类
2.内部类不存储在外部类之中,换句话来说,内部类的成员大小不参与外部类成员大小的计算
实际上,内部类与普通类并没有什么不同,只是内部类是受外部类的类域和访问限定符限制的类
如下代码中运行结果为4
class B
{
public:class A{public:private:int _a;};
private:int _b;
};int main()
{printf("%d", sizeof(B));return 0;
}
const成员函数
class Date
{public:Date()const//相当于Date(const Date* this){//....}private:int _year;int _month;int _day;
};
关于const权限放大与缩小的问题
看如下代码:
#include<iostream>
int main()
{int i = 1;double& d = i;return 0;
}
此时上述代码是无法编译通过,但下述代码是可以编译通过的,思考一下为什么?
#include<iostream>
int main()
{int i = 1;const double& d = i;return 0;
}
原因:当我们把两个不同类型的值进行赋值时,编译器是会发生隐式类型转换的,但注意隐式类型转化并没有把原来的数据类型给改变,既然没有把数据类型给改变那么显然它们之间是有一个临时变量的,这个临时变量的类型就是被赋值对象的类型,除此之外临时变量具有常性!
就例如上述代码中我们把double作为int的引用,显然i这个对象类型是没有改变的,那么d肯定不是引用的i这个数据,它们之间一定生成了一个临时变量,这个临时变量的类型是double并且临时变量是具有常性的,那么它完整的类型即为const double&
这时我们也就自然而然得出一个结论:const对象在进行赋值时,被赋值对象不能是非const类型!
当我们对一个数据对象加const后,我们实际上是对它的权限进行了缩小,由可读可写变为了只读
若一个只读的对象给他取了一个别名(引用)以后反倒是可读可写了,那么const的作用就形同虚设,显然若你是语言设计者也不会允许这种事情发生
接下来我们看下一段代码
#include<iostream>
class Date
{
public:Date(const int n){_n = n;}
private:int _n;
};
int main()
{int a = 1;Date d(a);return 0;
}
上述代码是通过const实参传给一个非const形参,代码是能正常运行的
这也就得出了另一个结论:被赋值对象是const类型时,赋值对象可以是非const类型
也就是对于对象来说,权限可以进行缩小但不能进行放大
const成员函数
在c++中,我们都知道一个类的成员函数通常都隐含了一个this指针,这个this指针占的是第一个参数位,但有些时候我们想把这个this指针指向的数据设置为不能修改时,c++提供了如下方法
class Date
{public:Date()const//相当于Date(const Date* this){//....}private:int _year;int _month;int _day;
};
而在c++中,这种const修饰this指针的函数,我们称之为const成员函数
注意:
1.const成员函数不能调用非const成员函数
2.非const成员函数可以调用const成员函数
这两点的理由与const对象与非const对象相同
取地址及const取地址重载
在c++中,明确定义了如果要对一个类使用操作符,那么就需要对这个运算符进行重载,并且由于取地址这个运算符的含义一般不会进行修改,于是c++把它设置成了默认成员函数
这两个成员函数如果不写编译器默认生成的是返回对象的地址,若写了则按你写的实现,一般如果显示写的话是为了不希望别人获取到你对象的地址或者希望别人获取到某个指定内容,用的非常少
再谈构造函数
我们之前了解到,构造函数就是对一个对象的成员进行初始化,事实上我们也一直这么理解的,但现在有一个问题,就是构造函数中的初始化为什么能多次赋值呢?这不是与初始化只能进行一次的定义相悖了嘛?
没错,我们之前理解的在构造函数体内的初始化实际上就是一个赋值而已,只是表现出来是初始化!
不信?若我们把一个成员设置成const类型的,那么我们在构造函数体内就无法对这个成员进行赋值了,因为const对象只能在初始化的时候给他赋值,这也就验证了我们上述说的
既然构造函数体内写的不是初始化,那么c++的初始化到底是什么东西来完成的呢?
初始化列表
c++中提供了一个语法概念,叫做初始化列表,这个东西就是帮助对象初始化的东西
初始化列表的格式:(初始化列表必须写在构造函数声明与函数体之间)首先一个:开始,之后接成员变量以逗号分隔,并且成员变量后面需要用一个'()'来进行初始化的数据或表达式,如下
class Date
{Date(int year = 1900,int month = 1, int day = 1):_year(year),_month(month),_day(day){}private:int _year;int _month;int _day;
};
①、必须在初始化列表进行初始化的成员变量类型
1.const类型
2.引用类型
3.自定义类型且无默认构造(或者有默认构造但我们想要自己传参)
总的来说,就是只能在初始化的时候进行赋初值的类型都需要在初始化列表初始化
②、由于初始化列表是对成员进行初始化,那么由于初始化只能初始化一次,所以每个成员只能在初始化列表初始化一次(只能出现一次)
③、若我们不在初始化列表对某成员进行初始化,那么编译器会自动对它初始化,内置类型不处理,自定义类型调用它的默认构造函数
④、编译器在走构造函数的时候一定会走初始化列表,无论你写不写
⑤、初始化列表初始化成员的顺序是按照你的成员声明顺序来的,越早声明的对象越早进行初始化
初始化列表使用的实际场景
实际上,在开发中,能使用初始化列表,尽量使用初始化列表初始化,原因如下
初始化列表能对成员对象初始化的,构造函数体不一定能完成
而构造函数体能完成的初始化,初始化列表大都能完成
其实读到这,相信你的心里也有疑问,既然初始化列表那么有用,那么还要构造函数体干嘛,直接只要初始化列表就行了,实际上构造函数体的功能有些时候初始化列表也并不能完成
如下代码:
#include<iostream>class A
{
public://...A(int n = 3):_a((int*)malloc(sizeof(int)* n)), _capacity(n){}//...
private:int* _a;int _capacity;
};int main()
{A a;return 0;
}
实际上,这个代码是能跑过的,咋一看好像没啥问题
但这里有一个问题就是:malloc是有可能失败的,此时我们需要判断是否失败,但难道直接写在初始化列表嘛?显然不行,因为初始化列表是用来对成员进行初始化的,它不负责检查初始化是否成功,于是这时我们的函数体就派上用场了
如下:
#include<iostream>class A
{
public://...A(int n = 3):_a((int*)malloc(sizeof(int)* n)), _capacity(n){printf("malloc fail");exit(-1);}//...
private:int* _a;int _capacity;
};int main()
{A a;return 0;
}
接下来说的是关于初始化列表的最后一个问题:对象整体是什么时候定义的
这个问题实际上就是问对象的成员在什么时候完成定义
假设它是在构造函数体内进行初始化,可能嘛?
显然不可能,因为在构造函数体内对成员的所有操作都是赋值,并不是定义,定义需要做的是完成初始化,那么就是在进入构造函数体之前已经完成了初始化,答案显然就是在初始化列表完成了定义
static成员函数/变量
问:实现一个类,计算程序中创建了多少个类对象
方法1:可以通过定义一个全局变量,当构造了一个类对象后这个全局变量就++
如下:
#include <iostream>
int count = 0;
class A {
public:A(){++count;}A(const A& a){++count;}
};
int main()
{A a;A b;A c(a);printf("%d", count);return 0;
}
但这种方法有个弊端,就是这个count是个全局的,那么别人就可以对它任意修改,此时数据的安全性就得不到保障!
接下来有人就说了,可以定义为一个成员变量来记录创建类的个数,但此时又遇到一个问题,每个对象的成员变量都是对象专属的,这就导致了你设置的成员变量count永远都是1
对于上述问题,c++给出的解决方法是static成员变量
也就是一个成员如果用static修饰之后,这个成员就不是这个对象专属的了,而是通过这个类实例化的对象共有的一个成员,需要注意的是当我们static修饰成员的时候在声明那就不能用缺省值,只能在类外进行定义,原因是我们在声明期间给的缺省值最终是要传给初始化列表进行初始化的,初始化列表进行初始化的是对象的成员变量,而static修饰的成员变量不是属于某个对象的,而是属于整个类的,那么它也就不走初始化列表,既然不走初始化列表,构造函数就无法对它进行初始化,最终也就只能在类中声明,在类外面进行定义,而且这个static成员变量也受访问限定符的限制
static成员变量声明格式:static 类型名 变量名;
static成员变量定义的格式为:类型 类名::变量名 = xxx
static成员变量调用的格式为:
1.可以通过对象调用
2.类名::成员变量名
注意的是当我们在声明期间已经用static修饰了以后在定义期间就不用再用static修饰了
那么当我们解决这个标题下一开始的问题:实现一个类,计算程序中创建了多少个类对象
我们可以写出如下代码
#include <iostream>
class A {
public:A(){_count++;}A(const A& a){_count++;}int getCount(){return _count;}
private:static int _count;
};
int A::_count = 0;int main()
{A a1;A a2;return 0;
}
但新问题又来了,上述代码都是在main函数体内进行定义的,然后通过对象访问static成员,但如果我在另一个函数体内定义对象,定义出来的对象出了作用域就销毁了,我们如何得知这个count是多少
第一种方法:在main函数中定义一个对象,通过这个对象访问这个getCount
第二种方法:匿名对象
匿名对象就是通过类实例化一个对象,这个对象没有名字,这个对象的生命周期为定义的这一行,
匿名对象即:类名()
需要注意的是匿名对象虽然是没有名字的,但同样是一个对象,这个对象出现只是为了得到count的数据,本身并不是创建变量中的一个,我们需要把这个变量给减掉,同样的,方法一中创建的变量也是为了调用这个函数也同样要减掉
如下代码:
#include <iostream>
class A {
public:A(){_count++;}A(const A& a){_count++;}int getCount(){return _count;}
private:static int _count;
};
int A::_count = 0;void Func()
{A a1;A a2;
}int main()
{printf("%d", A().getCount()-1);return 0;
}
第三种方法:static成员函数
static成员函数顾名思义就是用static修饰的成员函数
它主要特点为不含this指针,若你没有接收这个类的形参,是无法访问非静态成员的
但需要注意的一点是,若你有这个类的形参的时候,你可以访问形参的所有成员
因为静态成员函数在定义时是突破了类域的,只要突破了类域就不会受访问限定符的限制了
当然,如果是静态成员变量,那么这个变量是属于整个类的,也就可以对他进行访问
static成员函数的调用
1.类名::函数名()
2.对象.函数名()
如下:
class A {
public:static int add(const A& a){return (a.a1 + a.a2 + a.a3);//没有this指针,可以访问类的形参的成员}static int _add(){return (a1 + a2 + a3);//没有this指针,且没有这个类的形参}
private:int a1 = 10;int a2 = 20;int a3 = 30;
};int main()
{A a;A::add(a);return 0;
}
最后关于静态成员函数和非静态成员函数之间的关系
1.静态成员函数不能调用非静态成员函数(没有this指针)
2.非静态成员函数可以调用静态成员函数
类的隐式类型转换及其控制
在两个相近的内置类型互相赋值时(例如double与int),会默认进行隐式类型转换,这个转换是通过赋值过程中构造一个临时变量,这个临时变量是被赋值对象的类型并且还具有常性!而这个性质在c++中被扩充为,一个类只要满足某种条件(下面说),就能实现全部类型对它的隐式类型转换!
思考如下代码为什么能成功运行!
#include<iostream>
class A
{
public:A(int a):_a(a){}
private:int _a;
};int main()
{A aa = 10;return 0;
}
代码解析:之前说过,两个不同类型的变量赋值,那么中间一定会产生一个临时变量,这个临时变量就是被赋值对象的类型,而在上述代码中显然A和int是不同类型的对象,那么它们中间也产生了一个临时变量,这个临时变量是通过10来构造的,而既然要构造就调用了构造函数,我们上面说过一个类只要满足某种条件,就能实现全部类型对它的隐式类型转换,这个条件就是这个类一定要有另外一个类型的单参数构造函数,在上述代码中,由于A这个类有int类型的单参数构造函数,使得A aa = 10可以成功运行
对于上述情况,若我们不希望隐式类型的转换我们可以用explicit修饰单参数的构造函数
如下
#include<iostream>
class A
{
public:explicit A(int a):_a(a){}
private:int _a;
};int main()
{A aa = 10;//int隐式类型转换成A的构造函数被explicit修饰,转换无法完成return 0;
}
当然,并不是类型转换不能发生,而是隐式类型转换不发生,当我们强转的时候还是能转成功的
另外一个问题就是前面说的都是至多传1个参数的构造函数,当然这种情况包括了多参数半缺省只缺一个形参的值的构造函数和全缺省的构造函数,那么如果构造函数是多参数的时候,我们该怎么让他隐式类型的转换
c++11给出的一个新的玩法,即:类名 对象名 = {参数1,参数2....}
如下:
class A
{
public:A(int a,int b,int c):_a(a){}
private:int _a;
};int main()
{A aa = { 1,2,3 };return 0;
}
编译器对构造的一些优化
下述说的情况并不是一定的,而是根据编译器的不同,优化等级的不同才会产生优化,但总的来说这种优化在现在的编译器中还是非常常见的
编译器对构造的优化体现在同一个表达式中,编译器会对连续构造优化成一个构造
需要注意的是,必须是在同一个表达式中!
一个表达式中必须出现一次拷贝构造才会进行优化
优化大体上分为两种:
1.构造+拷贝构造=构造
2.拷贝构造+拷贝构造=拷贝构造
接下来会对这两种情况分别举例说明
第一种:构造+拷贝构造=构造
如下代码:
class A
{
public:A(int a){std::cout << "A(int a)" << std::endl;}A(const A& a){std::cout << "A(const A& a)" << std::endl;}
};
void Func(A aa)
{}int main()
{Func(A(1));return 0;
}
代码解析:按理来说上述代码在传匿名对象的时候调用了一次构造函数,而Func函数的形参是实参的临时拷贝,那么A(1)传到A aa的过程中肯定出现了一个临时对象,而这个对象是用A(1)拷贝构造出来的,也就是调用了一次构造和一次拷贝构造,但编译器把它进行了优化,把拷贝构造这一步直接省去,直接把aa用1构造出来
第二种:拷贝构造+拷贝构造=拷贝构造
如下代码:
class A
{
public:A(int a){std::cout << "A(int a)" << std::endl;}A(const A& a){std::cout << "A(const A& a)" << std::endl;}
};
A Func()
{A a1(1);return A(a1);
}int main()
{Func();return 0;
}
代码解析:我们先来看不优化的结果,调用Func函数,首先用1构造了a1,调用了一次构造
再用a1构造出一个匿名对象,调用了一次拷贝构造,传参返回,返回的是匿名对象的临时拷贝,此时又调用了一次拷贝构造。再来看优化后的结果,构造a1的时候不是在一个表达式中连续构造,所以这次构造铁定不能优化,而返回的时候是在同一个表达式中进行了两次拷贝构造,编译器把他优化为一次拷贝构造,即返回的对象直接用a1进行拷贝,不会产生那个匿名对象,于是最终答案就是总共调用了一次构造,一次拷贝构造
特殊情况:若一个函数返回之前已经构造好了一个对象,且返回值就是这个构造好的对象,且没有对象来接收它,则优化消失
如下情况:优化消失
class A
{
public:A(int a = 1){std::cout << "A(int a)" << std::endl;}A(const A& a){std::cout << "A(const A& a)" << std::endl;}
};
A Func()
{A aa;return aa;
}int main()
{Func();return 0;
}
代码解析:调用Func函数之后先构造了一个aa,用aa构造了一个临时对象,由于没有对象接收,无需再一次拷贝构造,优化没有发生
最后需要注意的一点是,上述优化是在vs2023下debug版本,release版本的优化将会更彻底,并且大多编译器的release优化都不相同,故不过多介绍了