C++学习:2类和对象

Posted 想文艺一点的程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:2类和对象相关的知识,希望对你有一定的参考价值。


回顾一下面向对象的知识点:

  • 类、对象
  • 成员变量、成员函数
  • 封装、继承、多态

自己的编程习惯:

  • 全局变量: g_xxx
  • 成员变量:m_xxx
  • 静态变量: s_xxx
  • 常量: c_xxx

一、面向对象

1、类和对象

类:相当于一个数据类型的定义

对象:相当于一个数据结构的实例化

在CPP 当中可以使用struct、 class来定义一个类,区别只是:默认权限不同

  • struct 的默认成员权限是 public (外界都可以访问)
  • class 的默认成员权限是 private (只有类内成员函数可以访问)

注意:实际开发中,用class表示类比较多,保留 struct 可能是为了让 C 程序员好过渡


2、对象内存

分析:看起来我们的成员函数也写在 class 当中,为什么不占用对象的内存呢?

  • 每个对象都有自己的独立空间都有属于自己的成员变量。(都有自己独特的属性,属性即成员变量)
  • 因为每个对象的成员函数都一样,没有必要搞很多份,共用一份就可以。(共用一份方法

问题:怎么知道使用成员函数的时候,成员函数要访问成员变量,怎么知道它访问的是哪一个成员变量?

答:通过 this 指针


对象的内存可以存在于 3 个地方

  • 全局区(数据段):全局变量
  • 栈空间:函数里面的局部变量
  • 堆空间:动态申请内存(malloc、 new等)


3、this

发明的目的:成员函数只有一份,怎么做到成员函数当中的成员变量不同。

解决问题:哪个对象调用成员函数,那么就把这个对象的地址保存下来通过这个地址就可以找到这个对象的属性

通过指针的方式来调用成员函数(传入的是 p 的值)

疑问:如果我们将 p 的值更改,那么通过指针访问的 this 的值是不是也会更改?

验证: p的值 == this 的值??

结论:p的值 == this 的值

注意:-858993460 其实是 0xcc, 二进制的 0xcc 其实一个中断向量

因为栈空间是每次都要使用的,所以肯定是脏的,系统每次都会自动填充 0xcc,意义是断点,让 cpu 停下来,不要瞎运行


4、封装

定义:

  • 隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;
  • 将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。

我的理解:

  • private属性隐藏了类内的属性,不能让外界函数进行调用。 (对于 C 语言来说就没有这样的特性

  • 成员变量私有化,提供公共的getter和setter给外界去访问成员变量


二、内存空间

1、内存空间

栈空间(.stack)

  • 每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间

  • 自动分配和回收

堆空间(.heap)

  • 需要主动去申请和释放

代码段(.text)

  • 用于存放代码

数据段(.data)

  • 数据段(全局区):整个程序运行当中都存在的东西

2、堆内存

目的:在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存

分析:如果没有堆空间那会发生什么?

(1)假设开发植物大战僵尸,所有僵尸对象、 植物对象、子弹对象等等全部放到全局区,会发生什么?

  • 这些对象将无法回收,这些对象将会永远放在内存当中。

(2)假设开发植物大战僵尸,所有僵尸对象、 植物对象、子弹对象等等全部放到栈区,会发生什么?

  • 如果放在函数里面,函数退出即僵尸死亡
  • 而实际是 当僵尸的血量(属性)没有之后才死亡

堆空间的申请和释放:

  • malloc \\ free :是一个函数,C 语言当中也可以使用。 (返回申请的首字节地址
  • new \\ delete :是一个函数C++ 当中才可以使用。(new \\ delete 比 malloc \\ free 多一些特性,在构造函数的时候再分析
  • new [ ] \\ delete [ ]

现在的很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在

  • 利:提高开发效率,避免内存使用不当或泄露
  • 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手

注意:

  • malloc 的返回值是 void ( ) 类型的,然后通过强制转化从而达到我们想要访问的效果*。

  • 函数调用完成之后,栈空间消失,堆空间的内存并不能消失,必须通过 free 来进行释放

  • new、delete

  • 内存空间不足,会导致申请堆空间失败,返回 NULL ,所以每次申请之后,我们可以检测一下有没有申请成功

堆空间的初始化

探讨一:我们刚申请堆空间的时候,平台会帮我们进行初始化吗

答案:windows 下的 VS 并不会帮助我们进行初始化。

探讨二:我们为什么要进行初始化?

  • 如果不进行初始化,可以看出指针内部的值是乱的
  • 如果我们这个变量是一个很重要的值,比如控制电流大小,如果出现值很大,那么就会很危险

探讨三:C++ 当中有几种初始化?

  • 加一个 圆括号 :本质还是添加 圆括号 之后,new 函数内部调用了 memset 函数
int *p2 = new int( );

  • 加一个 圆括号 ,圆括号里面加一个 5

  • 申请连续数组的空间
int *p3 = new int[3]// 未初始化
int *p3 = new int[3] { }//将所有元素初始化为 0  
int *p3 = new int[3] { 5 }// 将第一个元素 初始化为 5
  • 通过 memset 的方式来进行初始化:是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法


3、成员变量的初始化

  • 前提是默认情况下会不会帮助我们进行初始化。
  • 如果使用构造函数的话,肯定会通过调用构造函数来进行初始化。


另外一种情况:我们写一个空的、无参的构造函数,看看会不会进行初始化?

结论:

如果没有定义构造函数

  • 全局区会被初始化为 0
  • 栈空间不会进行初始化,
  • 堆空间加括号之后,会进行初始化。

如果自定义了构造函数

  • 全局区会被初始化为 0
  • 栈空间、堆空间 并不会进行初始化 ,需要开发人员手动初始化。(不管怎么 new 都不会进行初始化)

有一种做法,不管什么情况,都会初始化为 0


三、构造函数(constryctor)

  • 面向对象的语言,都有这个功能。
  • 构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作

特性:

  • 函数名与类同名,无返回值,但是不可以写 void
  • 可以有参数可以重载, 可以重载就是说可以有多个同名的构造函数
  • 一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象 (第 2 个图)

注意:通过 malloc 分配的对象不会调用构造函数, 通过 new 分配的对象会调用构造函数

  • 所以 new 比 malloc 会多做一些事情
  • 所以 我们在 C++ 当中一定要使用 new 。


一个广为流传的、很多教程\\书籍都推崇的错误结论默认情况下,编译器会为每一个类生成空的 无参 的构造函数

正确理解:在某些特定的情况下编译器才会为类生成空的无参的构造函数

其中的一种情况

还有哪些特定的情况?以后再提


1、构造函数的调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BBfFHRtU-1633779190649)(C:\\Users\\duxingdong\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210524222001649.png)]

可以发现,少了两个构造函数。这两个被看作是 函数声明


四、析构函数(Destructor)

  • 面向对象的语言,都有这个功能。
  • 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作

特点:

  • 无返回值(void都不能写)。

  • 函数名以~开头,与类同名。

  • 无参,不可以重载,有且只有一个析构函数

注意:

  • 通过malloc分配的对象free的时候不会调用析构函数
  • 构造函数、析构函数要声明为 public,才能被外界正常使用


1、内存清理

我们知道在对象销毁的时候,会调用析构函数,那么我们在析构函数当中需要做哪些内存清理的工作

(1)我们用不用清理对象内部的成员变量

答:不需要,因为在函数结束的时候,我们的对象会被销毁了,栈空间会自己进行回收

疑问:那么我们析构函数到底要做什么内存清理工作呢

案例:

#include <iostream>
using namespace std;

struct Car
{
	int m_price;

	Car()
	{
		cout << "Car::Car() " << endl;
	}
	~Car()
	{
		cout << "Car::~Car() " << endl;
	}
};

struct Person
{
	int m_age;
	Car *m_car; // 包含一个指针,为了使用堆内存

	// 用来做一些 初始化的工作
	Person()
	{
		m_age = 0;     
		m_car = new Car(); // 初始化指针

		cout << "Person::Person()" << endl;
	}
	// 用来做一些 内存清理的工作
	~Person() 
	{
		cout << "Person::~Person()" << endl;
	}
};

int main()
{
	{
		Person p1;
	}


	getchar();
	return 0;
}

分析:

  • person 类当中包含了 car 类
  • person 类当中定义一个 car 类的指针,是为了在堆内存当中使用对象

运行结果:可以发现car 的对象被定义了,但是没有执行它的析构函数

Car::Car( )
Person::Person( )
Person::~Person( )

因为:car 对象并没有销毁,所以会造成内存泄露

解决:在析构函数当中销毁(因为人都不在了,要汽车也没用了

	~Person() 
	{
		delete m_car;
		cout << "Person::~Person()" << endl;
	}
// 运行结果
Car::Car()
Person::Person()
Car::~Car()
Person::~Person()

如果不在析构函数当中清理,在外面清理,那就太麻烦了

  • 一个对象在栈空间,另外一个对象在堆空间


  • 两个对象都在栈空间

  • 两个对象都在堆空间

  • 两个对象都在堆空间在类内定义,那么就应该在类内进行回收

总结一句话:在类内定义的变量,那么就应该在类内的析构函数进行回收


五、程序优化

1、声明和实现分离

  • .h 文件 当中进行声明
  • .c 文件 当中进行实现


2、命名空间

  • 命名空间可以用来避免命名冲突

什么是命名冲突呢?

解决办法:

  • 使用命名空间来解决
  • 相当于我们户口登记,一个城市是一个命名空间

  • 也可以通过增加前缀来解决这个问题(但是不灵活)
class zhangsan_Person
{
    
};

class lisi_Person
{
    
}

怎么使用命名空间?

  • 在类名称前面添加 命名空间

  • 使用 using namespace 命令

解析 :using namespace std; 命令

如果没有这条指令:很多标准函数,我们必须添加前缀

std :: cout << "hello_world" << std :: cin;

如果使用了这条指令,我们就不需要添加前缀了

using namespace std;
cout << "hello world" << endl;

所以肯定有一个文件在 std 的命名空间当中实现了很多标准函数

namespace std
{
    	cout(  )
        {
            
        }
    	
    	cint ( )
        {
            
        }
}
  • 使用命名空间 单独访问命名空间某个元素


思考

同时使用两个命名空间,可以编译通过吗?

解决:可以使目标明确一点,前面加上作用域,就不会有二义性了。


命名空间的嵌套、合并

  • 有个默认的全局命名空间
  • 我们创建的命名空间默认都嵌套在 全局命名空间 里面


命名空间的合并

应用场景


六、继承

含义:可以让子类拥有父类的所有成员(变量\\函数)

问题:如果两个类当中有很多相同的特性,也有很多自己的特性。

  • 我们应该将共性的东西抽离出来作为父类
  • 将 特性的东西留下来,然后让他们去继承父类

关系描述:

  • Student 是子类(subclass,派生类
  • Person 是父类(superclass,超类


1、继承的内存布局

  • 父类成员在前面
  • 子类成员在后面

明确一点:子类对象到最后确实占用空间很大,因为包含了基类的所有成员。

问题:如果基类的成员没有用到,那么子类的空间不就浪费了吗?

  • 如果整个程序的所有对象,都没有用到,那确实是浪费了。

  • 但是如果有一个对象用到,那就不能算是浪费。

  • 这个不是 C++ 本身语法的问题,而是写代码的人的问题,既然所有对象都用不到,那么为什么要将他设计进去。


2、成员访问权限、继承方式

首先 成员访问权限 有三种:

  • public:公共的,任何地方都可以访问(struct默认),可以被继承
  • protected:子类内部、当前类内部可以访问, 也可以被继承
  • private:私有的,只有当前类内部可以访问(class默认) ,子类也没有访问权限

其次 子类继承父类的方式 也有三种:

  • public:保持原来基类的所有属性权限 (同理 struct 默认是 public 继承)
  • protected:
  • private:将继承来的成员变量,重新进行修饰 (同理 class 默认是 private 继承)

对继承方式的理解:对所继承的成员变量,进行权限修饰

总结:GoodStudent 能不能访问父类当中的成员取决于两点:

  • Person 当中使用什么权限?
  • Student 使用什么方式继承?
  • GoodStudent 的访问权限,就是上面两者的权限最小的

一般我们写 public 继承:

  • public 继承可以很好的将父类原有的权限继承下来

  • 因为取权限的最小值,所以原来是 protected ,继承之后还是 protected


问题:既然我们子类没有访问父类 private 成员的权限,那么这个成员还会被继承下来吗?这个成员还占用内存吗?

答:访问权限不影响继承的内存布局

  • 会将成员继承下来
  • 只是我们不能直接进行访问,但是对象内存当中还是有这个变量。
  • 我们可以通过封装进行间接访问

问题:既然我们不能直接访问,那么留下这个成员变量还有什么意义?

  • 封装
  • 间接访问
  • 如果我们子类对象内存当中都没有 m_age 这个变量, 这些 API 也根本不可以进行更改


七、初始化列表

发明目的:顾名思义,初始化列表,就是为了初始化发明的

特点:

  • 只能用在构造函数
  • 一种 便捷的初始化成员变量 的方式
  • 初始化顺序只跟成员变量的声明顺序有关

怎么理解更加便捷呢?

  • 可以通过汇编代码来分析,底层其实是一样的。


1、思考:初始化列表的本质

m_age、 m_height的值是多少?

答案: 23, 183。

总结:

  • m_age = 和 m_height 的等号右边能放什么,初始化别表的小括号里面就可以放什么

2、思考:初始化的先后顺序

我们想:如果我们先初始化 m_height 的话,那么 m_age 就不会是乱码了。

尝试一:更改初始化列表的变量的顺序

尝试二:更改 成员变量声明的顺序:

总结:

  • 成员变量的初始化顺序只和成员变量的定义相关
  • 成员变量的初始化值, 取决于初始化列表当中对应变量小括号里面的值


3、初始化列表与默认参数配合使用

可以实现一种效果:仿佛有三种构造函数

  • 无参的(全部使用默认参数)
  • 有一部分参数(部分使用默认参数,部分使用传入的参数)
  • 全部是指定参数(全部使用传入的参数)

注意:如果函数声明和实现是分离的(声明在类里面,实现在类外面)

  • 初始化列表只能写在函数的实现
  • 默认参数只能写在函数的声明

八、再论构造函数

首先我们要开拓眼界:

  • 构造函数里面不仅仅是初始化成员变量
  • 我们还可以 定义一个类
  • 我们还可以 调用其他子函数

1、构造函数的互相调用

  • 下面的写法是错误的,初始化的是一个临时对象
  • 在构造函数当中调用构造函数,相当于创建了一个临时的对象。
  • 然后将10、20 赋值为临时的对象。

我们来进行证明:使用汇编语言

第一步:确认 this 指针和 调用构造函数

第二步:确认传给第二个构造函数的 this 指针

总结:

1、构建对象的时候,第一个构造函数的 this 指针确实是本对象的地址

2、第二个构造函数的 this 指针却是一个临时对象的地址。(所以并不会将原来的对象进行初始化)


我们将,构造函数应该放到初始化列表当中

使用汇编进行分析:


2、父类的构造函数(继承)

1、子类的构造函数,默认会调用父类的无参构造函数

2、如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数

3、如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数

接下来一条一条进行分析

分析这么做是为了什么:

  • 子类的构造函数只是初始化子类特有的成员变量
  • 而基类的成员变量,理所应当的在基类当中的构造函数里面进行初始化。
  • 所以在初始化子类成员变量之前,应该先将父类的成员变量进行初始化

1、子类的构造函数,默认会调用父类的无参构造函数

分析汇编:


2、如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数


3、如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数


我们正式开发的写法:


3、构造和析构的顺序

  • 首先明确:子类构造函数会主动调用父类的构造函数,子类的析构函数也会主动去调用父类的析构函数

构造函数: 先初始父类,后初始子类。

析构函数: 先释放子类,后释放父类。

汇编分析:

以上是关于C++学习:2类和对象的主要内容,如果未能解决你的问题,请参考以下文章

学习:类和对象——多态

C++学习摘要之一:类和对象

嵌入式C++学习 类和对象

C++类和对象的简单应用举例

C++学习43 输入输出有关的类和对象

C++类和对象1