写在前面
这个大概就是类和对象最后的一篇博客了,算是初阶的一个结尾吧,里面涉及到的内容还是挺多的,我们还是需要静下心来看看.
赋值运算符重载
这个是一个很大的内容,我们可以自己创造赋值运算符的实现规则,我们先来看看什么是赋值运算符重载.
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数原型:返回值类型 operator操作符(参数列表)
为何出现赋值运算符重载
我们先来看一种请况.
为啥会报错,我就想让他们比较一下,我有什么错?可编译器却不允许,今天我必须让它给我允许了,这就是赋值运算符重载 为何会出现的原因
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (d1 == d2)
{
cout << "==" << endl;
}
return 0;
}
赋值运算符重载
多的不说,我们现在就来看看如何使它变得合理.
我们来看看这个函数,现在出现了一个问题,我们得不到类的属性,它被封装了,我们先把属性改成public,后面在解决这个问题.
bool operator==(Date d1, Date d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
这里就可以了,我们调用一下这个函数
int main()
{
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (operator==(d1,d2))
{
cout << "==" << endl;
}
return 0;
}
我们可能会疑惑,我随便取一个函数名就可以把这个函数的功能给写出来,还用弄得这样花里胡哨,但是你写的函数可以被这样调用吗?但是我的就可以.
if (d1 == d2)
{
cout << "==" << endl;
}
这个就是运算符重载的魅力,现在我需要把这个函数给完善下,传引用,没必要在开辟空间了,用const修饰,避免被不小心修改
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
解决不能得到类内封装属性的问题
这个我给两个解决方法,一个是在类里面写一些get函数,得到这些属性的值,另一个是使用友元,但是这种方法破坏了封装,不太建议.
- 在类内写一些 get 方法 这是Java经常用的
- 使用友元 会破坏类的封装,这里我放在下面谈
在类里面写运算符重载
我们还不如直接在类里面写这个函数呢,简单快捷,这样就可以避免破坏封装.
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
public:
int _year;
int _month;
int _day;
};
请问为什么会报这个错误?参数不是很对吗?我们在前面都说过,编译器会默认添加一个this指针类型的参数,而==就是两个操作数,所以报参数过多.我们减少一个参数就可以了.
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
这样函数的调用就变成这样
int main()
{
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (d1 == d2)
{
cout << "==" << endl;
}
return 0;
}
代码里面你说变成 d1.operator==(d2) 这样,就变成这样?有什么可以证明的,这里用对象的地址证明一下吧.
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
cout << "this" << this << endl;
return true;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
cout << "d1" << &d1 << endl;
cout << "d2" << &d2 << endl;
if (d1 == d2)
{
}
return 0;
}
类内运算符重载和类外运算符重载的优先级
如果类内和类外都有一样的,编译器优先调用哪一个重载呢?我们通过现象得知优先调用类里面的重载.
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
cout << "类里面" << endl;
return true;
}
public:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1,const Date& d2)
{
cout << "类外面" << endl;
return true;
}
int main()
{
Date d1(2022, 5, 18);
Date d2(2022, 5, 18);
if (d1 == d2)
{
}
return 0;
}
重载 运算符 “=”
本来我想和大家分享一个日期类,但是如果现在这这里写了,至少还需要几千字,我把它单独放到了一个博客,作为我们这些天学习类一个小总结,在这个日期类里面,你会发现我们上面谈的所有的知识点,这里先来点小菜,这个很简单,目的是为了引出下面的知识点.
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
我们先来调用一下.
int main()
{
Date d1(2022, 10, 18);
Date d2;
d2 = d1;
d2.Print();
return 0;
}
d2 = d1
细心的朋友可能发现我们写的是d2 = d1;而不是在开辟d2的时候给他赋值,这里我要重点谈下.我们通过调试来看看吧.
这个调用的是运算符重载.
int main()
{
Date d1(2022, 10, 18);
Date d2;
d2 = d1;
d2.Print();
return 0;
}
Date d2 = d1
这个调用的是拷贝构造,不是那个运算符重载.
int main()
{
Date d1(2022, 10, 18);
Date d2 = d1;
d2.Print();
return 0;
}
从这里就可以下一个结论了,如果我们给定义变量并初始化的时候,调用拷贝构造,如果赋值的时候两个变量已经被定义过了,就是调用运算符重载.
如何构造前置++和后置++
这个很重要的,我们如何让编译器知道我们想要的是前置++还是后置++,这个我们不知道该如何办,但是C++已经规定了我们如何做,这样就可很好的重载这个运算符了.
C++规定,我们可以通过给后置++加上一个int类型的参数来区分前置++和后置++,这里面–也是一样的,我们也可以这么重载.这里我先说一个规则,具体的可以去看我的Date类博客,里面有详细的实现.
- 前置 寻常写法
- 后置 参数加一个 int 类型的参数
前置
前置是什么都不用做,直接写就可以了.
Date& Date::operator++()
{
*this = *this + 1;
return *this;
}
后置
后置的化是我们给一个参数,我们不用关心编译器是怎么做的,只需要知道用法就可以了,
Date Date:: operator++(int)
{
Date ret(*this);
*this = *this + 1;
return ret;
}
重载流提取 & 流插入
有的时候我们也可能通过这样的方法来进行输入日期,也就是流提取和流插入,我们也要重载它们.
int main()
{
Date d;
cin >> d;
cout << d;
return 0;
}
流提取 >>
这里面我们详细分享一下流提取,把众多情况给考虑到,最后把流插入给写一下就可以了.我们可以在直接在类内重载我们想要的运算符,不过这里的参数一个是 std里面的istream,我们可以这么理解,打卡一个屏幕,看作文件,我们从键盘上输入数据,遇到空格算一个,遇到换行结束.
void operator>>(std::istream& in)
{
in >> _year >> _month >> _day;
}
我们试试代码可以吗?
int main()
{
Date d1;
cin >> d1;
return 0;
}
我们在C++中一直提到过一个东西,对于重载的运算符实际上是调用函数,cin >> d1调用的函数就是 operator>>(cin,d1),那么我们在类内重载的运算符可不是这样的,就会报错,那么我们该如何正确的调用呢?
int main()
{
Date d1;
d1 >> cin;
d1.Pinrt();
return 0;
}
友元
你是不是感觉有点怪,我们是想吧屏幕上的数据放到 d1 中,但是这种调用的方式有点让我难以接受,不妥,很是不妥.我们怎么修改这个函数呢?这里,我们想到了使用 全局的函数,也就是在类外定义这个函数.但是这里就有问题了,我们类的成员是封装的,也就意味者外部的函数是无法得到和修改(或者使用 set,get函数)对象的.
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day;
cout << endl;
}
private:
int _year;
int _month;
int _day;
};
void operator>>(std::istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
}
我们还有另外一种方法,使用友元,一般我是不推荐的,但是这里我们不得不用,我们想来看用法,后面会专门来说.
我们在类里面声明这个函数是我们的朋友,既然是朋友,那么你就可以修改我们的变量了
如果你要是觉得到这里就可以了,那么就大错特错了,我们发现标准库里面的流提取支持连续提取,也就是这样的
int main()
{
int a;
int b;
cin >> a >> b;
printf("a = %d b = %d\n", a, b);
return 0;
}
我们也要有一个返回值,这里就直接修改了,类里面这里就不修改了
std::istream& operator>>(std::istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
流插入 <<
这里流提取差不多,这里直接给代码了.
std::ostream& operator<<(std::ostream& out, Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
那些不能运算符重载
我们可以重载很多运算符,但是还有5个运算符不能重载.
分别是 .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现,大家记住就可以了.
再析权限问题
权限问题是我们的重中之重,现在需要把这个问题再拿出来往深处谈谈
const
我们需要知道是什么造成权限的扩大,也就是在C++中,我们通过const来修饰,那么C++中的const有什么特殊之处,是不是和C++完全一样呢?这里我们只谈一点const修饰的地址可以解引用吗?可以的.
int main()
{
int a = 10;
const int* pa = &a;
cout << *pa << endl;
return 0;
}
那么它可以修改吗?
权限
现在就可以来谈权限了,我遇到了一个问题,大家看看
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day;
cout << endl;
}
private:
int _year;
int _month;
int _day;
};
void func(const Date& d)
{
d.Print();
}
int main()
{
Date d1(2022, 7, 10);
func(d1);
return 0;
}
这里我们可以通过 报错的信息知道,我们在调用d.Print();时,由于d是 一个const修饰的引用
它的调用编译器自动修改成 Print(const Date* const this),但是 我们类里面的Print是Print( Date* const this),权限不匹配,这就是我们遇到的问题,当我们知道问题,这里有又有一个,我们该怎么办?
C++规定了,如果我们想要修改this指针的权限,只需要在函数后面加上一个const就可以了,如果生命和定义分离,那么都加
这里我们又有了另外一个问题,是不是每一个函数都要加上const呢?不是的,如果我们明确了不修改原来的变量,最好都加上const,并且这里还有一个好处,就是普通的变量调用const修饰的函数也不会报错,因为权限问题不会存在.
const成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改 .
这里有一个问题,const成员函数可以可以调用非const的成员函数吗? 答案是 不可以,我们知道,const成员里面的this都是const 类型* const this,但是普通成员函数的是 类型* const this,也会发生权限问题.
取地址运算符重载
这个就是六个默认函数的另外两个,比较简单,一般的话,我们是不写出来的,没有必要,除非你不想让任何人(包括你自己)得到地址,可以写出来,返回一个nullptr.
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
再谈构造函数
这里,我们要把构造函数好好的在深入的谈一下,另外不加了一些内容,我们还是先来看看那个最基本的Date类,以它为例.
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
构造函数时对内置类型不做任何处理吗
这个是肯定的,大家不要纠结,但是这里我发现了课一个现象,可以作为特立,我用的时VS2013,其他的编译器我不确定是不是.
我们的成员变量要是存在一个指针,所有的成员变量被初始化成0
初始化列表
在构造函数中,我们可以通过下面的方式来给成员变量进行赋值.
我们在刚开始的时候就说过,在类里面的的变量只是一个声明,只有当我们进行对象的实例化的时候才会创建,这一点我们时已经知道的,但是这里面说在构造函数里面进行再次赋值是什么意思?我们该如何判断成员变量是在哪个过程进行的对象的实例化?这里就要谈到初始化列表.
我么先来看看初始化列表如何使用的,我们在构造函数后面跟上黄色方框里面的内容,格式是 成员变量后面的括号里面表示要初始化的值,它的作用和我们在函数里内结果是一样的.
int main()
{
Date d1;
d1.Print();
return 0;
}
这里还要和大家提一个醒,这两种方法是可以混合这个用的.
为何出现初始列表
这个就要谈谈类里面是如何"定义"成员变量的?我们这么认为,成员变量是在初始化列表进行定义的,也就是初始化列表的首要作用就是定义变量,那么这里就有一个疑问了,如果我们不写,是不是也会存在初始化列表?是的,编译器对于内置类型进行初始化列表事会给一个随机值,就是我们之前说的,对内置内置类型不做处理,如果我们写了,就按照我们的来.
大家可能还存在另外一个疑问,那就是初始化列表和函数体内赋值好象没有什么区别啊,这两个的作用是不是重复了?我想说的是没有,初始化列表能做的远比函数体内的要多.
这里就有一个问题对于那些定义和初始化必须在一个起的变量你该怎么办?这里就不能在函数体内进行再次赋值.这里也进一步证明了函数体内是赋值,而不是定义.
这里我们只能通过初始化列表来进行帮忙,
成员变量缺省值的实质
我们之前说过成员变量是可以缺省的,它的是指就是给初始化变量时候把随机值给替换掉,如果我们传参了,就按传参的的来,和缺省函数差不多.
初始化列表的初始化顺序
我们想问,初始化列表的顺序是不是和我们写的一样,还是有固定的顺序呢?这个我们按照例题的形式来解释.
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
A.输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值
按照我们想的,我们把1给了_a1,后面又把 _a1 给了 _a2,也就是我们选A,但真的是是这样吗?
这就和我么想的不同了,但是要是这么想就可以了,初始化是按照变量的声明来的,先初始化 _a2,把 _a1这个随机值 _a2,在初始化 _a1,把1给 _a1,这就和结果一样了.
小结
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
- 初始化顺序是按照申明的定义走的
explicit 关键字
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
A(const A& d)
{
cout << "A(const A& d)" << endl;
}
private:
int _a;
};
int main()
{
A aa = 10;
return 0;
}
我么可以接受其他的实例化对象的方式,但是上面的对我来说就有点难以接受了,我们把一个内置类型给了一个自定类型,这是开什么玩笑,但是,在C++中这里还真是允许的,C++中构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用,也就是说,编译器会先把10作为参数实例化一个临时对象,有通过拷贝构造把这个临时对象给拷贝一份.
按理说,我们是需要调用构造函数和拷贝构造的,但是这里编译器直接给优化了,直接把这个临时变量给了aa,把拷贝构造这一步给省了
explicit 关键字
如果我们不想让单参数的构造函数发生这样的转变,我们可以在这个构造函数前面加上一个explicit关键字,这样编译器就不会这么做了
static 成员
现在我们谈点东西了,C++类里面支持static修饰的成员变量和方法,它们普通的方法是不一样的,属于所有的对象,也就是说属于类.我么先来见识一下它们的特性
static 成员变量
和普通成员变量一样,在类里面申明,但是在类外定义,如果不定义,编译器编不过去
class A
{
public:
A()
{
}
public:
static int a;
};
int A::a = 10;
访问手段
如果是public的,我们可以通过下面的两种方式来访,如果不是就只能通过用函数来访问了
int main()
{
A aa;
cout << aa.a << endl;
cout << A::a << endl;
return 0;
}
static修饰的方法
这个更加简单,只要记住,static修饰的方法里面没有this指针就可以了,也就是说,不能在这函数内调用非static修饰的函数,除非你在这个函数内实例化一个对象,通过它来简洁调用,这里就不谈了.
class A
{
public:
A()
{
}
static void func()
{
cout << "static void func" << endl;
}
};
访问手段
这个和static修饰的变量一样,都是告诉编译器去类里面找就行了
int main()
{
A a;
a.func();
A::func();
return 0;
}
友元
我们前面是见过友元的,就在那类Date的流提取和流插入,那是友元函数.
友元函数
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout 的 输出流对象和隐含的this**指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是 实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这 样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
friend std::istream& operator>>(std::istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
std::istream& operator>>(std::istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
- 友元函数不能用const修饰 没有this
- 一个函数可以是多个类的友元函数
友元类
这里我要见识一下有友元类.
从这里我们可以知道,B是A的友元,所以访问和修改A类型对象的中成员变量.
class A
{
friend class B;
public:
A();
private:
int _a;
int _b;
};
class B
{
public:
B();
void fun()
{
cout << _a._a << _a._b << endl;
}
private:
A _a;
};
这里也有一点总结
- 友元关系是单向的,不具有交换性 上面 B是A的友元,但是 A不是B的友元
- 友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
内部类
所谓的内部类就是类里面是否可以定义一个类.
class A
{
public:
A()
{}
class B
{
public:
B()
{}
private:
int _a;
};
private:a
int _a;
};
内部类的大小
我们计算类的大小实际上是计算的是对象的大小,也就是里面的成员变量,与里面是不是有内部类是无关的的
int main()
{
cout << sizeof(A) << endl;
return 0;
}
内部类的特性
内部类B是是外部类A的友元,B直接可以访问A的私有,但是A不能访问B的.
实例化内部类
如果内部类是public的,这里可以通过类域来进行实例化,如果是private,就是天生不让外界来看到的内部类
int main()
{
A::B b;
return 0;
}
再谈封装
C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的 所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内 部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让 整个事情复杂化 ,这个需要到继承和多态那里分享
|