详解面向对象,c++面向对象是什么?详解构造函数、析构函数、运算符重载

面向对象与面向过程

在传统的c里,我们设计程序时是面向过程的,比如我要写实现一个点外卖软件,我需要依次实现以下几个功能在这里插入图片描述
这些功能是按照发生的顺序依次运行的,也就是按照整个程序执行的过程来依次发生的。
关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
而c++是面向对象的,比如我们还要实现上的外面软件,我们可以把他划分出几个对象:
在这里插入图片描述
这些对象相互作用,交互共同完成需求。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

c++面向对象概述

c++11目前有一套成体系的面向对象思路,但想要跟深入的理解,必须从类讲起

c++类的引入

在传统的c语言中,为了封装一些数据,我们诞生了结构体

struct Student
{
	char name[10];
	int age;
	char sex[5];
}

但是结构体中不能有函数,只能有编译器设置好的数据类型。因此我们在c++中队结构体进行了升级,使它的内部能有函数,这样子提高代码的逻辑性。

struct Data {
	int _year;
	int _month;
	int _day;
	void printData() {
		cout << _year << "年" << _month<<"月" << _day<<"日"<<endl;
	}
	void initData(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
}

在定义中可以添加函数,并且既能访问成员,又能访问函数。
在这里插入图片描述
在这里插入图片描述
但是struct毕竟是c语言的东西,在c++纯正的面向对象中我们更喜欢用class关键字来声明一个类

class 关键字

上面用struct关键字定义的类,可以等价为下面这个class关键字定义的类。

class Data {
public:
	int _year;
	int _month;
	int _day;
public:
	void initData(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void printData() {
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
};

在struct中,无论是函数还是变量都可以在外界被访问到但是在实际开发中,我们往往不希望有些函数或变量被随意调用,因此诞生了访问限定符。

访问限定符

访问限定符简述

访问限定符分为;private,protect public,三种。
其中private限定只能在本类中访问,protect限定只能在本类和他的子类中访问,public随时可以访问。

默认访问限定符

在一个类中,如果没有访问限定符,那么它默认都是private。
在这里插入图片描述

访问限定符作用域

一个访问限定符的作用范围是从上一个开始到下一个开始。
在这里插入图片描述
print函数被private修饰,而init函数被public修饰可以外界访问。

c++ struct和 class的区别是什么

  1. c++兼容c,因此struct可以当结构体用
  2. c++可以用struct创建类,也可以用class创建类。
  3. c++用struct创建的类成员默认访问方式是public。而class默认是private

封装

封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
比如上述的Data类中,如果外界可以轻易的访问内部存储的日期,那么日期会被任意修改,失去了这个类的意义。

如果像C语言中函数和变量分离的话,每次调用需要人为传参,会极大不安全。
然而我们一旦封装之后,**用访问限定符控制一些东西不让外界访问,从而根本上保证安全性。**为此我们在设计类的时候,成员变量一般是私有的,方法根据设计
这种不关注内部实现,仅通过接口调用的特点也是面向对象的关键之一。

类的细节实现

类的作用域

类本质上是新建了一个作用域,因此不同类之间可以有同名函数,且不构成重载**(重载要求在同一作用域下)**
在这里插入图片描述

类中函数的声明和定义

类中的函数可以在类的中声明同时定义
在这里插入图片描述
也可以在类中声明,类外实现。但是在实现的时候要指明函数的作用域,不然不知道是那个哪个类的函数。在这里插入图片描述
注意:在类中实现的函数默认是内联函数,因此我们往往推荐将短小的且频繁调用的函数在类中实现。

类中变量的声明和定义

对于一个变量来说,区别声明和定义的唯一标准就是是否开辟了空间。开辟空间的叫做定义。
因此在类中的变量都是声明,只有当实例化类的时候,才开闭空间,进行定义操作。

类的实例化

类是一个图纸一样的东西,里面存放的只是声明,不能存储东西,只有将类实例化对象的时候才开辟空间存储东西。
在这里插入图片描述

对象的大小

类里面既有函数,又有变量,那么类实例化出的对象的大小是多少呢?
在这里插入图片描述
对于这个类实例化出的对象d1,我们打印他的大小。
在这里插入图片描述
结果是12,者正好是三个 三个int类型的变量按照原则对齐后的结果,也就是说,对象内部不存放函数,更不存放函数的指针。
因为每个对象里的内容都是不同的,所以变量必须私有,而函数是共同调用的,因此函数可以是共有的,单独放在一区域中。
得到这个结论后,我们再来看下面几个练习题

// 类中既有成员变量,又有成员函数
class A1 {
public:
 void f1(){}
private:
 int _a;
 char _ch;
};

根据前面讲的对象里只有成员变量这一特点,第一个元素是int大小为4个字节,第二个元素是char要从对齐数的整数倍开始放,对齐数是默认对其数(8)和上一个元素中最小的那个,因此对齐数是4,要从4开始放,放完是5。最后要进行内存对齐,变成4的整数倍,得到结果8。
在这里插入图片描述

// 类中仅有成员函数
	class A2 {
	public:
	 void f2() {}
	};
	// 类中什么都没有---空类
	class A3
	{
	};

这两者都是没有成员变量的类,那么他们实例化的对象大小是多少呢?

在这里插入图片描述
在这里插入图片描述
结果是1,所以对于没有成员变量的类,实例化的对象会被赋值为1,表示这个对象存在。

this指针

this指针的引入

在这里插入图片描述
在上述代码中,明明printData()函数是共有的,且里面没有传参,编译器是怎么知道我打印的是d1,还是d2呢?
这时因为编译器会给添加一个this指针,用于指明访问的是哪个对象。

void Data::printData(Data* this) {
	cout << this->_year << "年" <<this-> _month << "月" <<this-> _day << "日" << endl;
}

在编译后printData()就被修改为带有this指针,同时调用的过程中,也会传入调用对象的指针。
注意:我们不能抢编译器的功能,this不能显示的写出来但是我们却可以在类中调用这个this
在这里插入图片描述

this指针的特点

this指针是const 类型,不可以被修改

前面讲过,编译器会自动生成一个this指针,表明调用这个函数的对象,但是我们能自己定义this,却可是使用它。
在这里插入图片描述
可以看到,我们在类中没定义this,却能正常使用他
在这里插入图片描述
但是this不可以被修改
在这里插入图片描述
可以用const修饰的引用来对他起别名,也证明了this是const修饰的常量。
在这里插入图片描述
上图就是被编译器修改后的标准函数。

this指针存在函数的栈帧中

因为this指针实际上是一个形参,他在函数调用产生的栈帧里。因此this指针和正常函数的形参一样,都存储在函数栈帧中
在某些编译器中,this指针也可能放在寄存器里。总而言之,this指针是编译器帮你添加的,我们可以调用它,总之this指针不在对象里存储。

this只能在成员函数内部使用
this与空指针

将这个之前,我们要先补充一个东西,当我们有一个对象的地址的时候,也可以像c语言一样用->来访问函数

Data d1;
d1.Init(2022,4,13);
Data* d2 = &d1;
d1.print();
d2->print();

有了这个知识后,我们来看下面这串代码。
在这里插入图片描述
在编译的时候,编译器会自动补齐this指针,shou()函数被传入一个NULL指针,随后输出一个"show()"字符串。
在这里插入图片描述
在这里插入图片描述
但是这个代码则会报空指针异常,因为打印_a本质上是打印this->_a,而我们传入的this指针是空。
在这里插入图片描述

六个默认成员函数

所谓默认成员函数,就是哪怕我们不写,编译器也会自动生成的成员函数,下面我们依次来学习这些函数

构造函数

前面我们都是手动进行初始化,但是如果一旦忘记初始化,就会出现随机值压栈报错。为此我们引入了构造函数,使其在对象的实例化就完成初始化。
在这里插入图片描述
注意:构造函数所谓构造是对属性的初始化过程,对象空间的开辟是由编译器完成的。

构造函数的特征

  1. 函数名与类名相同
  2. 没有返回值1
  3. 对象实例化时自动化完成
  4. 构造函数可以重载(同时实现含参构造与无参构造)

全缺省和无参构造不能同时存在

在这里插入图片描述
这样子虽然编译会通过,但是调用时会报错。在这里插入图片描述
这是因为编译器分不清我们调用的是全缺省还是无参构造。

一旦显式定义,编译器就不生成构造函数

在这里插入图片描述
可以看到我们没有写构造函数,编译器就给我们生成了一个默认的构造函数,但是这些属性都是随机值,那这个函数有什么用呢?

原始类型不初始化,自定义类型初始化

在这里插入图片描述
我们在Data类中的一个属性是Day类的对象,我们在创建d1对象时会发现,原始数据类型int没有被初始化,而我们自定义的Day类的构造函数被调用了,完成了初始化。
在这里插入图片描述
只有自定义类型day完成了舒适化,其他的没有初始化。
所有对于普通成员变量,不初始化,对于自定义类型,调用他的默认构造函数。
默认构造函数有三种:

  1. 啥都没写编译器自己生成的
  2. 自己写的无参构造函数
  3. 自己写的全缺省构造函数
    注意:3,2,1就不能同时出现。

总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的构造函数,否则都要自己实现构造函数。 但如果我的自定义类型没有构造函数,也不行。

缺省值为默认构造函数使用

前面讲过,默认构造函数只能初始化自定义类型,为解决这种问题,c++11补充了普通变量的缺省值。
在这里插入图片描述
在这里插入图片描述
缺省值解决了这个问题。

析构函数

有构造函数就一定有析构函数。构造函数解决了生成对象后的初始化问题,而析构函数完成了对象清理的过程。
析构函数与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
资源清理就是把对象中malloc的空间进行消除,防止栈溢出,相当于以前写的destory()函数

析构函数的特点

以~号开头,我们可以自己写析构函数,但如果自己没写,就会默认生成。
在这里插入图片描述

析构函数发生顺序

在这里插入图片描述
对于上面这个Stack类,先调用s1的构造函数,再调用s2的构造函数。
但是先调用谁的析构函数呢?这个问题的本质就是,s1和s2谁先被销毁。
我们认为的写一个析构函数,并且输出他的capacity
在这里插入图片描述
可以看到输出的结果是
在这里插入图片描述
证明:析构的顺序和构造的顺序是相反的后定义的先析构。

默认类型不处理,自定义类型会调用对应的析构函数

这个和构造函数一样。
在这里插入图片描述
一口气出来的四个,证明析构函数会调用自定义类型对应的析构函数。

析构函数面试题

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )

C c;

int main()

{

A a;

B b;

static D d;

  return 0;

}

首先我们要明确一点: 全局变量最先构造,最后析构
其次 局部对象按照出现的顺序进行构造,无论是否为static
最后 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
所有构造顺序为 C A B D
析构顺序为 B A D C

拷贝构造

Date d1(2022,3,4);
Date d2 = d1; //拷贝构造
fun(d1); //创建形参时拷贝构造
void fun(Date d) {
}

拷贝构造函数是一个特殊的构造函数,是构造函数的一种重载。
他的形参是同名的对象。
在这里插入图片描述
拷贝构造必须用引用调用,不然会导致无穷递归。

为什么产生无穷递归

这就要从传参讲起,当一个传值传参的过程中,本质上是进行了一次实例化对象的过程。

在这里插入图片描述
要先新建一个d,新建d是拿d1创建的,要拷贝构造,拷贝构造又要拿d1创建,于是就不断进行拷贝,最终无限的递归。
总结:内置类型可以直接拷贝,自定义类型要调用拷贝构造
这里d的改变不会影响d1,因为d是d1的一个拷贝

为什么要加const

在这里插入图片描述
因为不加const的话可能会导致反向赋值这种操作发生。

默认拷贝构造的本质

默认的拷贝构造是浅拷贝,就是直接把待拷贝的对象里的属性给复制到新的对象里,浅拷贝会发生各种问题。
浅拷贝就是直接把这块内存的东西直接放到新的对象里。
注意:默认的拷贝构造会对默认类型进行浅拷贝,但是浅拷贝不一定是错的,比如前面的日期类。

深拷贝

有些对象的拷贝构造不能随便写,要根据实际情况去写
在这里插入图片描述
比如这种类,他就不能用默认的拷贝函数,需要用自己写的深拷贝来解决这种问题。
因为假如按照默认的拷贝构造,则会使两个对象同时指向被拷贝的对象的初始化时malloc的空间,两个都指向了malloc。而对象销毁时析构会被析构两次,以及两者对象操纵同一块空间,会产生各种问题。

自定义类型与普通类型同时存在

自定义类型与普通类型同时存在时,编译器会将普通类型进行浅拷贝,然后调用自定义类型的拷贝构造。

在这里插入图片描述
我们写了一个Stack类,他的拷贝构造我们来打印一个deepcopy。
在这里插入图片描述
这是队列类,他由两个自定义类型栈构成,对q2进行q1的拷贝构造。
在这里插入图片描述
输出两次 deep copy证明确实调用了自定义类型的拷贝构造。

拷贝构造有关面试题

以下代码共调用多少次拷贝构造函数: ( )

Widget f(Widget u)
{  
  Widget v(u);
  Widget w=v;
  return w;
}
main(){
  Widget x;
  Widget y=f(f(x));
}

逐行分析代码,先实例化一个对象x,然后调用f(x),f(x)的返回值为f()的参数。
进入f,f的形参是一个Widget对象,进行一次拷贝构造。
然后用u生成v,进行一次拷贝构造
用v生成w,再进行一次拷贝构造。
并返回w
再进入f(w)
生成形参 u ,进行一次拷贝构造
u生成v进行一次拷贝构造
v生成w,进行一次拷贝构造
返回w。
用w生成y,进行最后一次拷贝构造
最终结果为7次

总结

  1. 如果不写拷贝构造的话,编译器会默认生成
  2. 编译器默认生成的拷贝构造分类讨论:对内置类型直接进行浅拷贝,对自定义类型会调用自定义类型的拷贝构造
  3. 如果编译器默认生成的拷贝构造够用,那就不需要自己写,否则需要自己写。
  4. 拷贝构造一定是引用传参,否则会递归调用。

运算符重载

运算符重载就是为运算符赋予新的作用效果。

int a = 10;
a = a+ 100;

内置类型可以直接用运算符

Data d1;
d1 + 100;

自定义类型不能用各种运算符,需要我们自己定义
而为了解决这种问题,并且提高代码的逻辑性,c++提出了运算符重载

运算符重载函数标准模板

operate +要重载的运算符。
参数是:运算符的操作数
返回值,认为定义的运算结果

 bool operator==(Data d1, Data d2)
 return d1.year == d2.year && d1.month ==d2.month && d1.day == d2.day;

上面就是一个标准的运算符重载函数
注意:正常情况下,Data类中的year属性应该是私有的,所以不能直接用d1.year来访问,为此我们要提供get或set方法。
但是注意,我们往往不会用上面的这种写法

编译器的自动处理

在这里插入图片描述

自定义类型要引用传参

引用传参可以减少拷贝的次数

注意

在这里插入图片描述
三目运算符 、 sizeof, ::域指定运算符, .运算符,.*运算符不能重载。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值