类和对象万字详解

目录

一、面向对象与面向过程的区别

面向过程:

面向对象:

二、类的引入

class与struct爱恨情仇

class的语法

类的定义:

类的限定访问符

类的实例化

类对象模型

this指针的应用

三、封装

四、类的六个默认成员函数

构造函数

再谈构造函数

初始化列表

析构函数

拷贝构造函数

拷贝构造函数的无限递归问题

运算符重载

赋值重载

前置++和后置++的重载

五、const成员

六、static成员

七、友元

友元函数

友元类

八、内部类

九、匿名对象

十、拷贝对象是的一些编译器优化

十一、再次理解类和对象


一、面向对象与面向过程的区别

很多同学都听说过、c语言是一个面向过程而c++是一个面向对象的语言。这里的对象与我们接下来要讲的类和对象是同一个东西。

那么什么是面向过程呢?举个例子:

洗衣服的例子。

面向过程:

我们洗衣服需要进过:拿盆子——放水——放衣服——放洗衣粉——手搓——换水等等步骤:

如果将这个抽象成编程,我们写程序也会按照这个步骤一步步往下写。

面向对象:

假设我们有一个洗衣机。我们将洗衣服那些繁琐的步骤交给洗衣机来做,那么我们只需要让衣服、洗衣服、洗衣机三个物品进行相互作用就可以实现洗衣服这项工程。并不需要关心衣服到底是如何洗的。

上面提到的:人、洗衣机、洗衣粉,就是我们要讲到的类与对象中的对象

那么上面那种一步步繁琐的过程是怎么实现的呢,我们将这些动作是怎么做的教会给洗衣机,然后每次洗衣服的时候让洗衣机进行操作就行了,我们不再需要关心底层是如何实现的了,只需要按时取衣服就行了。

将洗衣服的动作交给洗衣机这一步就是————封装。我们下面会详细讲,先感性的认识一下:就像将一些方法教给机器人,然后我们每次需要的时候只需要命令就好了,不需要关心底层到底是什么步骤。

二、类的引入

class与struct爱恨情仇

上文说到的封装的机器人其实就指的是类,方法可以看作是函数,封装这个动作就是将一些函数写进类中。

类是c++区别于c独有的一个数据类型。但巧的是,在c语言我们也有类似的数据类型——结构体。

回忆一下结构体:它的作用就是将许多不同的数据类型打包进一个结构体中。比如:

struct Date
{
    int year;
    int month;
    int day;
};

在c语言中的结构体我们只能将一些数据打包起来,但是在此基础上进行一个升级:打包数据的同时,我们也允许将一些函数打包进去,就形成了——类,这个数据类型。 类其实就是c结构体的升级版。也是因为这个关键的数据类型我们才实现了面向过程。

在vs2022下编写这段程序,我们发现它不会报错。

同时我们发现,如果使用struct也同样不会报错。

因为struct在c++也被升级了,那都升级了为什么还要产生一个class来使用,并且还成为了类似正宫的地位?

因为struct与class其实还是存在着一些不同。等会再讲,先了解一下class的语法。

class的语法

类的定义:

class classname
{

};

class是定义类的关键字,classname是我们自己设置的类名,{}中就是类的主体,也是类域,注意括号后的‘;’不可省略。

类体中的内容称为类的成员,变量被称为类的属性成员变量;类中的函数被称为类的方法或成员函数。

类的两种定义方法:

  1. 声明和定义全在类体中,需注意:成员函数在类中定义可能被编译器当成内联函数处理。

  1. 类声明放在.h文件中,成员函数定义放在.cpp中,注意,成员函数前需要加上类名::

为什么要将声明与定义分开存放呢?

第一为了方便阅读,你想想,当你将声明与定义一起存放时,你在阅读一个类的函数声明时,你本来只想看看这个类封装了什么方法,但是你看到的却是大段大段的函数定义查找起来并不方便,所以如果将它们分开,我们只会看到方法整整齐齐的放在那里。

第二是为了如果你不想泄露源代码,可以只将.cpp文件转换成二进制文件发送出去,不需要将头文件与方法源码一起发送出去。不过这都是后话了。

那我们为什么需要在.cpp文件中对成员函数加上类名::?

还是以上面那个洗衣机为例:

如果不加上类名::就会出现如上情况:根本分不清哪个函数是哪个类的成员函数。因此我们需要将类名::加上去,表明这个成员函数是属于哪一个类域的。

不过在博客中,为了方便讲解,会将声明与定义放在一起。

在定义类的成员变量时有一个小细节:在成员变量前加上一个_,这是一种编程习惯,为了区分参数与成员变量。就比如:

class Date
{
    void Init(int year,int month,int day)
    {
        year=year;
        month=month;
        day=day;
    }
    int year;
    int month;
    int day;
};

在上面这个函数里面year是谁的year容易误解,所以干脆成员变量前面都加上_

class Date
{
    void Init(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    int _year;
    int _month;
    int _day;
};

类的限定访问符

其实上面这一段中的方法在外界是无法使用的

class Date
{
    void Init(int year,int month,int day)
    {
        year=year;
        month=month;
        day=day;
    }
    int year;
    int month;
    int day;
};

因为类拥有限定访问符的约束

限定访问符的说明:

1.public修饰的成员在类外可以直接被访问。

2.protect和private修饰的成员在类外不能直接被访问,因为我们没有讲到继承,在这里protect与private的作用是类似的。

3.访问限定符的作用域为它出现的位置开始到下一个限定符的出现为止。

class Date
{
public:
    void Init(int year,int month,int day)
    {
        year=year;
        month=month;
        day=day;
    }
private:
    int year;
    int month;
    int day;
};

就像这里,public的作用域到private为止,而private的作用域到}为止

4.如果后面没有限定访问符,作用域到}为止。

5.class的默认访问权限是private,而struct的默认访问限定符是public。

这就是上面讲到的struct与class的一点区别,而且这也是上面那段代码无法运行的原因。

解答: C++ 需要兼容 C 语言,所以 C++ struct 可以当成结构体使用。另外 C++ struct 还可以用来
定义类。和 class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public class 定义的类
默认访问权限是 private 。注意:在继承和模板参数列表位置, struct class 也有区别,后序给大
家介绍。

类的实例化

class Date
{
public:
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

那么我们在敲下上面这段代码是不是就意味着我们可以在Date里面存放数据了吗?

提出一个问题:Date里面的_year、_month、_day是声明还是定义?

上面的这一段代码其实只是告诉编译器我们有这几个数据可以使用,并没有分配内存给它们存放数据,因此我们只是将它们声明是无法使用的。

打个比方:上面这个现象就相当于我没有一张别墅的图纸,我们能拿着这张图纸直接住进去吗?显然不行,我们需要用这张图纸将房子建出来我们才可以真正使用,居住进去。

接下来我们造房子。

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void test1()
{
	Date p1(2023,3,2);
	p1.print();
}

 

 于是我们就将Date实例化为p1,可以将p1的数据初始化,并且调用其函数。就像图纸一样,我们这个图纸可以用来建造很多栋房子,我们这个Date类也可以进行多次实例化并产生多个不同的对象,比如p1 p2 p3。

这就是我们讲的类与对象中的对象。

类对象模型

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

在c语言中我们很容易算出来这个类的大小为12,但是到了c++中呢,这里面还包含了成员函数。

复习一下结构体内存对齐:

1. 第一个成员在与结构体偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS 中默认的对齐数为 8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

看看结果:

在c++的语法体系下,我们得出的内存大小还是12字节,因此我们大胆猜测一下,也许类的成员函数并没有放在对象的内存空间之中。

其实仔细想想也很合理,如果我们定义了多个对象,那岂不是在每个对象下都会存放相同的代码,太浪费空间了。这些代码都统一存放在代码段里面,当我们调用的时候才回去查找。

 检测一下有没有理解上面知识点:

// 类中既有成员变量,又有成员函数
class A1 {
public:
    void f1(){}
private:
    int _a;
};
// 类中仅有成员函数
class A2 {
public:
   void f2() {}
};
// 类中什么都没有---空类
class A3
{};

这三个类的对象各需要分配多大的内存:

答案是:4、1、1

第一个我们可以理解,但是后面明明就没有成员变量为什么还需要占用内存呢?

我们定义一个变量不可能不分配任何内存给它,所以至少会占用一个字节, 而这个字节叫做占位符

this指针的应用

Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

Date的每个对象都有一个共同的类模板,而不同的对象调用的都是同一个函数,那么我们我们怎么知道上面这个函数里的_year _month  _day就是p1的成员变量而不是p2、p3的呢?

这里就要提一个this指针了,这个指针的类型就是Date *。

在每次我们调用函数的时候,函数的参数不只是我们输入的那些,就像上面的(year,month,day)每次都会有一个编译器偷偷帮我买加上的一个参数:this

这个this就是为了区分各个不同的变量而存在的。

将函数的参数完整的写出来就是Date(Date *this,int year,int month,int day);

包括我们的函数也可以像下面这样声明:

Date(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

这样就明确的指出了这是哪个对象的成员变量。

下面问两个面试题:

1.this指针存放在哪里?

1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。
2. 只能在 成员函数 的内部使用
3. this 指针本质上是 成员函数 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以 对象中不存储 this 指针,存储在栈上
4. this 指针是 成员函数 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传 递,不需要用户传递

2.this指针可以为空吗?

先来看看下面这个代码

Date *ptr=nullptr;
pyr->fun();

猜测一下它的运行结果是什么:程序错误、程序崩溃、正常运行?

很多同学没想到的是居然正常运行了,ptr虽然没有指向一个具体的类对象,但是我也没有做什么坏事呀,第一我没有调用一个没有内存空间的成员函数,第二我调用的成员函数也不是存放在类对象里面的,而是存放在代码共享段,我自己去代码段粒查找就行了。

但是需要注意的是,我调用的这个函数也没有访问类的成员变量。因此也会去访问一个不纯在的变量。

三、封装

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来
和对象进行交互。
封装本质上是一种管理,让用户更方便使用类 。比如:对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB 插孔等,让用户和计算机进行交互,完成日
常事务。但实际上电脑真正工作的却是 CPU 、显卡、内存等一些硬件元件。

 

这里也显示是出了,编程并不是一个从虚无开始创建程序的一个过程,它同时野需要我们对生活的观察与总结,因为我们最终还是需要解决生活中的问题,回归到生活中来的。

四、类的六个默认成员函数

如果一个类什么成员都没有,就简称为空类。

空类里面真的什么都没有吗?其实我们的编译器会自动默认生成六个默认成员函数。

默认成员函数:用户没有自己实现的话,编译器会自动生成的函数。

为什么要生成呢?

c++这个语言是经历过对c的总结与升级而形成的,所以我们在这里至少也可以知道,这几个默认成员函数的存在是为我们程序员减负而存在的。

这里我们只讲解前面四个函数,后面两个用的少。

构造函数

构造函数虽然听起来像是开空间创建对象,但其实很贴切的名字应该是初始化函数

它的作用是初始化对象。

其特征如下:

1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数

class Date
{
public:
    Date()
    {    
        _year = 1;
		_month = 1;
		_day = 1;
    }
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

上面这一段代码其实就用到了构造函数,而且有两个,因为是允许重载的,而且一个有参一个无参,他们的调用方式分别如下:

Date d1;//创建对象的时候就自动调用构建函数进行初始化,而且是无参的那个函数
Date d2(2023.3.4);//自动调用有参的构造函数

构造函数一个优势就在于,我们在创建对象的时候就会自动调用构造函数,而不用我们手动的敲写:Init(1,1,1);这样的代码,其实多两行也没关系,但是怕就是怕我们自己忘记调用了。这个好处更容易体现在下面的析构函数中。

现在要讨论一下构造函数自动调用的相关细节

我们将我们自己写出来的构造函数先屏蔽一下,看看系统默认的构造函数效果是什么

#include<iostream >
using namespace std;
class Date
{
public:
	//Date()
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}
	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void test1()
{
	Date d1;
	d1.print();
}
int main()
{
	test1();
	return 0;
}

我们可以看出系统自动给我们的年月日是随机数,这其实就是系统自动分配内存时本来就有的数据,编译器在初始化时并没有对我们的数据进行赋值。

我们定义的_year,_month,_day,其实是内置数据类型(语言本身就有的int,char,long之类的),与之相对的是自定义类型(class,struct,Union之类的)。

编译器对内置类型的初始化————不会对他进行赋值。

编译器对自定义类型的初始化————调用自定义类型自己本身的构造函数初始化。

注意:

编译器自动生成的条件是我们不写编译器才会自动生成默认的,但我们一旦实现了任意一种构造函数,编译器就不会自动生成了。

我们不能这样去调用一个构造函数或者建立对象:Date d1();。这样程序会报错,谁知道你是不是在声明一个返回值为Date类型的函数呢,这样的问题也被称之为——二义性

再谈构造函数

Date(int year, int month, int day)
 {
     _year = year;
     _month = month;
     _day = day;
 }
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值 ,而不能称作初始化。因为 初始化只能初始 化一次,而构造函数体内可以多次赋值

初始化列表

初始化列表的书写格式:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式

class Date
{
public:
    Date(int year,int month,int day)
    :_year(year),
    _month(month),
    _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
}

1每个成员变量再初始化列表中只能出现一次(初始化列表只能初始化一次)

2类中包含以下成员,必须放在初始化列表初始化:

        引用、const、自定义成员变量

const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量不能先定义再初始化,它们在初始化列表内定义,并且必须在定义时就初始化,因此必须在初始化列表内初始化。

3尽量使用初始化列表,因为不管你用不用,对于自定义类型的成员变量一定会优先于初始化列表初始化。

析构函数

通过刚刚的构造函数的学习,我们知道了一个对象是怎么来的,那么一个对象要怎么销毁呢?

析构函数:与构造函数相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数的特征:

1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。
class stack
{
public:
	stack()
	{
		_a = nullptr;
		_capacity = 0;
		_top = 0;
	}
	~stack()
	{
		free(_a);
		_a = nullptr;
	}
private:
	int * _a;
	int _capacity;
	int _top;
};

这里我就写了一个简单的栈类,这里面我就实现了析构函数,~符号的意思就是“取反”,所以这里也与构造函数相对应。

我们在c语言实现栈的时候,我们建立一个栈就需要手动调用初始化函数(自己写的Init函数),销毁一个栈的时候就需要手动调用一个销毁函数(自己写的destory函数),如果我们忘记了调用销毁函数,那么这个我创建的栈就无人销毁造成了内存泄漏,非常严重的问题

但是在c++的语法体系下下,我们不需要自己调用销毁函数,这个栈自己走到了生命周期尽头就会被编译器自己调用的析构函数销毁。这里也展示出了封装的好处,将繁琐的细节封装起来,方便用户使用。

拷贝构造函数

但我们知道,这来给你个都是类,出了生命周期是要析构的,如果同一个空间被析构两次就有问题了。 

 因此我们编译器在这里合理的拷贝方式是——重新申请一块空间然后赋值(这就是深拷贝)。

 

 那么是不是所有的指针类型或者数组都要重新申请空间呢?并不是,如果是其他体量很庞大不适合这样开辟空间的数据结构,就应该用到直接拷贝地址的方式了(这就是浅拷贝)。

那么这样说的话编译器怎么知道要怎么拷贝呢?因为不论它怎么选你总能挑出编译器的刺,它就说:干脆你自己选择要怎么拷贝算了。

于是编译器的拷贝规则是:内置类型的就由我来完成,但是自定义类型(struct、class之类的)就由用户自己定义如何拷贝——通过拷贝构造函数

拷贝构造函数的无限递归问题

回到上面的话题:为什么不能用传值作为参数而必须要用引用作为参数呢?

这是一个传值为参数的拷贝构造函数声明以及调用时的代码:

Date(Date d);      Date d2(d1);

我们要知道:在传值的过程我们时需要将d1的值先拷贝进d,就是在进行函数本体操作前,参数需要先拷贝进来。

 在将d1拷贝给d的时候,就会调用Date的内部拷贝构造函数——套娃开始了。在调用第二层拷贝构造时发现参数还需要拷贝,那就再接着调用第三层拷贝构造函数。

这就是形成无限递归的原因。

那要怎么解决这个问题呢?很简单,不是要拷贝递归嘛,我直接用引用传参,这样你就不会在拷贝前对参数拷贝了。

但是我们要注意一点:函数声明定义的时候我们尽量在前面加上const,不只是这里而是每次使用引用的时候,到加上一个const,因为权限能平移、缩小但是不能放大。

如:Date (const Date &d);

拷贝函数的用法:

Date d2(d1);
Date d3=d2;//这个属于拷贝构造函数而不是复制重载

运算符重载

在讲复制重载前,我们需要先讲运算符重载,因为复制重载就属于运算符重载。

c++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,也拥有返回类型,以及参数,其返回值与参数列表与普通函数相似

函数名字为:operator后面接需要重载的运算符符号

函数原型:返回值类型operator操作符参数列表

举个例子:int operator==Date a,Date b);

我们就可以这样调用函数:a==b.

就可以很简便的计算出两个日期是否相等。

注意:
1.不能通过连接其他符号来创建新的操作符:比如 operator@
2.重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+ ,不 能改变其含义
3.作为类成员函数重载时,其形参看起来比操作数数目少 1个, 因为成员函数的第一个参数为隐藏的this
4.“.* :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。

我们先试着在类外定义一个运算符重载:

这个运算符重载函数有多少个参数由这个是几目的运算符,下面这个就是双目的。

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
   && d1._month == d2._month
        && d1._day == d2._day;
}

你会发现程序会报错:

 因为你在类里面的声明是privqate的类型,在类外是无法访问的。

解决办法就是在类里面定义一个重载函数:

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	Date(Date& d)
	{

	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}
	void fun()
	{
		cout << this << endl;
	}
	bool operator==(const Date& d1, const Date& d2)
	{
		return d1._year == d2._year
			&& d1._month == d2._month
			&& d1._day == d2._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

我们发现它又报错了,说是参数太多了,双目运算符刚好两个参数呀,但是我们不要忘记了,在类里面定义的函数有一个隐形参数this,因此我们不需要第一个参数。 

这样程序就没有错误了。

 调用方式: d1==d2;或者operator==(d1,d2);

第一种调用方式就体现出来了运算符重载的优势——可读性强。

接下讲讲第四个默认成员函数

赋值重载

赋值重载就属于运算符重载,但是这是运算符重载函数里面唯一一个默认成员函数。是可以被编译器默认生成的。

Date& operator=(const Date& d)//返回值支持连续连必须赋值
{
 if (this= &d)//避免自己自己给自己赋值
 {
     _year = d_year;
     _month = d_month;
     _day = d_day;
 }
 return *this;
}

这就是赋值重载的定义形式。

以下是格式:

参数类型 const T& ,传递引用可以提高传参效率
返回值类型 T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回 *this :要复合连续赋值的含义

参数要加上const的原因一是需要保护对象的成员属性,二是为了不让参数权限放大导致传参失败。

返回值这样设计的好处就是如果

d1=d2=d3;

如果你不给这个函数一个返回值,是编译不过去的,因为程序先操作d2=d3,操作完发现d1=__

这个后面没有东西给它传参。所以我们需要给这个函数一个返回值

赋值重载不能重载为全局函数

class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}

以上的代码会编译失败,如果不在类里面弄显示实现,编译器就会自己在类里面实现一个默认赋值重载,两者就会产生冲突。

用户没有显示实现,编译器默认生成一个重载函数,是以值的逐字节拷贝

前置++和后置++的重载

在写这类函数的时候我们会发现,不论怎么写都会是下面的形式:

Date &operator++();

不论是前置还是后置。

因此我们就需要有个标识符,来区别这两个东西。

c++规定后置++需要增加一个int类型的参数,但是调用函数的时候该参数不需要传递,,编译器自动传递。

定义如下:

 Date operator++(int)
 {
     Date temp(*this);
     _day += 1;
     return temp;
 }

后置++是要将+1前的值返回,然后再对值+1,所以我们要先将+1前的值保存,然后再+1。

一下是前置++的函数定义:

 Date& operator++()
 {
 _day += 1;
 return *this;
 }

五、const成员

既然类成员函数中的this参数是隐式的,我们想要对它加const限定要加呢?

在函数()之后加上一个const就可以实现这个功能,

右边的书写形式只是为了让大家更好的理解,编写是是不能这样书写的

加上这个的作用就是:在这个函数内无法对类本身进行修改

六、static成员

先提出一个面试题:创建一个类,计算在这个程序中创建了多少分个对象

我们第一个想到的应该是对构造函数下手——每每创建一个对象就给计数加1,那么这个计数要怎么定义呢,因为你创建的不同的对象都会调用一个成员函数——构造函数,我们就可以定义一个全局变量,每次创建对象都给这个变量加1.

实现方法如下:

#include<iostream>
using namespace std;

int scount = 0;//全局变量
class stack
{
public:
	stack()
	{
		_a = nullptr;
		_capacity = 0;
		_top = 0;
		scount++;//给全局变量++
	}
	~stack()
	{
		free(_a);
		_a = nullptr;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

void test1()
{
	stack s1;
	stack s2;
	stack s3;
	stack s4;
	stack s5;
	stack s6;
	cout << scount << endl;

}
int main()
{
	test1();
	return 0;
}

 但如果我手贱呢?

void test1()
{
	stack s1;
	stack s2;
	stack s3;
	stack s4;
	stack s5;
	stack s6;
    sount++;
    sount++;
	cout << scount << endl;

}

这样就失效了。

我们还有另外一种方式,static成员变量,这个成员变量厉害就立在在于这里面的数据是全体对象共享的,而且我们将它封装进了类里面。

1. 静态成员 所有类对象所共享 ,不属于某个具体的对象,存放在静态区
2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明
3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问
4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员
5. 静态成员也是类的成员,受 public protected private 访问限定符的限制
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}

七、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为: 友元函数 友元类

友元函数

问题:现在尝试去重载 operator<< ,然后发现没办法将 operator<< 重载成成员函数。 因为 cout 输出流对象和隐含的 this 指针在抢占第一个参数的位置 this 指针默认是第一个参数也就是左操作数了。但是实际使用中cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。 operator>> 同理。
class Date
{
 friend ostream& operator<<(ostream& _cout, const Date& d);
 friend istream& operator>>(istream& _cin, Date& d);
public:
 Date(int year = 1900, int month = 1, int day = 1)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
private:
 int _year;
 int _month;
 int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
 _cout << d._year << "-" << d._month << "-" << d._day;
 return _cout; 
}
istream& operator>>(istream& _cin, Date& d)
{
 _cin >> d._year;
 _cin >> d._month;
 _cin >> d._day;
 return _cin;
}
int main()
{
 Date d;
 cin >> d;
 cout << d << endl;
 return 0;
}
1友元函数 可访问类的私有和保护成员,但 不是类的成员函数
2友元函数 不能用 const 修饰
3友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制
4一个函数可以是多个类的友元函数
5友元函数的调用与普通函数的调用原理相同

友元类

1友元关系是单向的,不具有交换性
    比如 Time 类中声明 Date 类为其友 元类,那么可以在 Date 类中直接 访问Time类的私有成员变量,但想在 Time 类中访问 Date类中私有的成员变量则不行。
2友元关系不能传递
   如果 C B 的友元, B A 的友元,则不能说明 C A 的友元。
3友元关系不能继承,在继承位置再给大家详细介绍
class A
{
private:
 static int k;
 int h;
public:
 class B // B天生就是A的友元
 {
 public:
 void foo(const A& a)
 {
 cout << k << endl;//OK
 cout << a.h << endl;//OK
 }
 };
};
int A::k = 1;
int main()
{
    A::B b;
    b.foo(A());
    
    return 0;
}

友元的方式尽量少用,因为这破坏了函数的封装性

八、内部类

概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类 ,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的 public protected private 都是可以的。
2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。

九、匿名对象

class A
{
public:
 A(int a = 0)
 :_a(a)
 {
 cout << "A(int a)" << endl;
 }
 ~A()
 {
 cout << "~A()" << endl;
 }
private:
 int _a;
};
class Solution {
public:
 int Sum_Solution(int n) {
 //...
 return n;
 }
};
int main()
{
 A aa1;
 // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
 //A aa1();
 // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
 // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
 A();
 A aa2(2);
 // 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
 Solution().Sum_Solution(10);
 return 0;
}

上面指的好用就是——如果只是为了调用一次成员函数,就可以这样使用。

十、拷贝对象是的一些编译器优化

class A
{
public:
 A(int a = 0)
 :_a(a)
 {
 cout << "A(int a)" << endl;
 }
 A(const A& aa)
 :_a(aa._a)
 {
 cout << "A(const A& aa)" << endl;
 }
A& operator=(const A& aa)
 {
 cout << "A& operator=(const A& aa)" << endl;
 if (this != &aa)
 {
 _a = aa._a;
 }
 return *this;
 }
 ~A()
 {
 cout << "~A()" << endl;
 }
private:
 int _a;
};
void f1(A aa)
{}
A f2()
{
 A aa;
 return aa;
}
int main()
{
 // 传值传参
 A aa1;
 f1(aa1);
 cout << endl;
 // 传值返回
 f2();
 cout << endl;
 // 隐式类型,连续构造+拷贝构造->优化为直接构造
 f1(1);
 // 一个表达式中,连续构造+拷贝构造->优化为一个构造
 f1(A(2));
 cout << endl;
 // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
 A aa2 = f2();
 cout << endl;
 // 一个表达式中,连续拷贝构造+赋值重载->无法优化
 aa1 = f2();
 cout << endl;
 return 0;
}

十一、再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现
实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创
建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
1. 用户先要对现实中洗衣机实体进行抽象 --- 即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
2. 经过 1 之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言( 比如: C++
Java Python ) 将洗衣机用类来进行描述,并输入到计算机中
3. 经过 2 之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
能洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到, 类是对某一类实体 ( 对象 ) 来进行描述的,描述该对象具有那 些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化 具体的对象

猜你喜欢

转载自blog.csdn.net/qq_64484137/article/details/129291918