C++第五节 - this指针、构造函数、析构函数

一、类对象的存储方式

只保存成员变量,成员函数存放在公共的代码段

注意点:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	char _a;
};
int main()
{
	class A a;
	cout << sizeof(a) << endl;
	cout << sizeof(A) << endl;
	return 0;
}

sizeof求大小的时候输入对象名和类型名的效果是一样的!

cpp 复制代码
int main()
{
	class A a;
	//cout << sizeof(a) << endl;
	//cout << sizeof(A) << endl;
	a._a = 1;
	a.PrintA();
	return 0;
}

a._a是在对象里面找空间,将其值覆为1;

a.PrintfA()不是在对象中寻找!(函数存放在公共代码段中)

剩下的成员变量是按照C语言中的结构体的内存对齐来存储的!

下面为两个class内存对齐的例子:

  • 第一个class,存放char的时候,对其数为1,因此挨着int存放;
  • 第二个class,存放int的时候,其对其数为4,因此要在对其数的整数倍处存放;
  • 整个结构体的大小为对其数的整数倍;

为什么要进行内存对齐?

对于不同的硬件来说,访问内存并不是说想访问那个字节就访问哪个字节,具体的访问数量跟硬件的设置有关系;

假设硬件CPU一次读取四个字节

  • 访问_ch的时候访问到了4个字节;但是此时可以直接抛弃下面的3个字节,仅仅实现读取一次就完成!
  • 访问_a的时候直接访问4个字节一次读取即可完成;

如果没有内存对齐:

此时读取a需要读取两次!每次只能读取一部分!

怎么修改对其数呢?

cpp 复制代码
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认

调整对其数后,会造成访问效率下降,但是能节省空间;

注意点:空类和仅有成员函数

cpp 复制代码
class A2 {
public:
	void f2() {}
};
// 类中什么都没有---空类
class A3
{};

int main()
{
	class A2 a2;
	class A3 a3;
	cout << sizeof(a2) << endl;
	cout << sizeof(a3) << endl;
	return 0;
}

没有成员变量的类对象,需要1byte,是为了占位,表示对象存在!这个字节不存储有效数据!

不给1个byte的内存,此时如果对对象取地址,无法取地址!

二、this指针

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函
数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

其实,方框类似于编译器自己做的处理,会将对象的地址传过去,然后通过地址调用不同对象所对应的成员函数;(自己不能显示的去写)

  • this不能在形参和实参显示传递,但是可以在函数内部显示使用!
  • this的实际类型为Date* const this,const修饰的是this指针,因此this指针不能被修改,但是this指向的内容可以被修改!(例如this = nullptr是错误的!)

问题1:this指针存放在哪里?(对象里面,堆,栈,静态区,常量区)

this是形参,因此this指针跟普通的参数一样存放在函数调用的栈帧里面!(函数调用结束后,this指针销毁!)

如果this指针存放在对象里面,那么空类得大小就不会为1!

lea指令的作用是将取d1的地址放到ecx中!

问题2:

空指针访问是运行的问题,而不是编译的错误!(因此排除A);

  • p调用Print,不会发生解引用,因为Printf的地址不在对象中,p会作为实参传递给this指针;
  • this是空的,但是函数内部没有堆this进行解引用操作,因此不会发生报错!

调用Print()函数是直接调用,没有发生解引用!

正确答案为: C

问题3:

  • p调用Print,不会发生解引用,因为Printf的地址不在对象中,p会作为实参传递给this指针;
  • this是空的,但是函数内部访问了_a,this->_a,本质上是通过指针进行解引用(汇编指令是解引用)操作找到_a,因此会报错!

正确答案为: B

访问类的成员函数不能使用::限定符,因为使用.传递的时候,编译器会自动传入this指针,但是使用::的时候不知道需要传入什么值!

如果自己传递的话就不满足this指针不能显示传递的条件!

  1. 点运算符(.

    • 点运算符用于访问对象的实例成员。它表示你在访问某个特定对象的属性或方法。例如,如果你有一个类 Data 的实例 data,你可以通过 data.a 来访问 a 这个成员变量。
  2. 双冒号运算符(::

    • 双冒号运算符通常用于访问类的静态成员或命名空间中的成员。在一些语言(如 C++ 和 PHP)中,ClassName::member 用于访问类的静态属性或方法,而不是实例的属性。

因此,Data::a 通常表示你在尝试访问 Data 类的静态成员 a,而 Data.a 则表示你在访问 Data 类的实例 data 的成员变量 a。如果 a 是一个实例变量,你必须先创建 Data 类的一个实例,然后通过该实例来访问 a

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

三、使用C和C++实现栈

C++实现栈本质上和C没有大的区别,但是C++都被封装到一起,使用起来更方便!不需要自己传入指针参数!

即C++中 Stack * 参数是编译器维护的,C语言中需用用户自己维护。

  • 当我们自己在写代码的时候,经常会忘记Init或者Destory,如果不进行Destory就会造成内存泄露!
  • 有的地方写起来很繁琐!例如下面的,我们还不能直接对Stack进行销毁,还需要先保存值,然后将stack销毁,再返回ret;

引入:构造函数主要完成初始化工作;(不是创建对象,对象已经在函数栈帧自动创建了!会自动调用函数,函数的功能不是创建对象,而是做初始化工作!)

析构函数主要完成清理工作;(不是创建对象!会自动调用函数,函数的功能不是销毁对象,而是做清理工作!)

四、构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值。(也不需要写Void)
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

如果把构造函数设置为private,语法上没有问题,但是直接运行会报错!

当我们不写构造函数的时候,编译器会默认生成构造函数**,内置类型不做处理,自定义类型会去调用他的默认构造!**

例如VS2013

此时会发现自定义类型_st被初始化,但是内置类型没有被初始化!

但是VS2019会自动对两种变量都进行处理:

结论:自定义类型一定会被初始化,但是内置类型可能不会被初始化,具体结果取决于编译器!

且如果没有自定义类型,内置类型不会被初始化!

  1. 一般情况下,如果有内置类型,就需要自己写构造函数!
  2. 全部都是自定义类型的成员,可以考虑让编译器自己生成!

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。(这里不是初始化,这里只是声明,这里给的是默认的缺省值,给编译器生成默认构造函数用)

没有给默认值:

cpp 复制代码
class Time
{
public:
 Time()
 {
 cout << "Time()" << endl;
 _hour = 0;
 _minute = 0;
_second = 0;
 }
private:
 int _hour;
 int _minute;
 int _second;
};

给定默认值:

cpp 复制代码
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 Date d(2020,2,2);  // 修改默认值
 return 0;
}

构造函数的调用

普通函数是对象+成员函数;

构造函数是类+对象(参数列表);

且构造函数的调用不能采用以下方式:

cpp 复制代码
Data d1();

没有参数的时候不能这样调用!

这样子写的话会跟函数声明有冲突,编译器不好识别!

可能会把d1当作函数名,返回类型为Data!

注意点:无参和全缺省函数构成函数重载,但是在调用的时候存在歧义!因此也不能同时存在!

默认构造函数:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。(即不传参就可以调用的就是默认构造函数!)

这三个构造函数有且只能存在1个!

  • class类中默认成员函数有6个!即我们不写但是编译器会自己生成!
  • 但是默认构造函数不一定是编译器自己写的!

五、析构函数

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

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。(默认生成的对内置类型不做修改,对自定义类型做处理)
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。

  • 内置类型/基本类型,语言本身定义的基本类型int/char/double/指针等;
  • 自定义、用struct/class等等定义的类型;
相关推荐
醇氧10 分钟前
ab (Apache Bench)的使用
linux·学习·centos·apache
杨荧10 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
小青头12 分钟前
numpy学习笔记
笔记·学习·numpy
Mephisto.java18 分钟前
【大数据学习 | flume】flume的概述与组件的介绍
大数据·学习·flume
monkey_meng22 分钟前
【Rust中的项目管理】
开发语言·rust·源代码管理
喜欢打篮球的普通人24 分钟前
rust高级特征
开发语言·后端·rust
weixin_4786897628 分钟前
【回溯法】——组合总数
数据结构·python·算法
ModelBulider42 分钟前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql
戊子仲秋1 小时前
【LeetCode】每日一题 2024_11_14 统计好节点的数目(图/树的 DFS)
算法·leetcode·深度优先
V搜xhliang02461 小时前
基于深度学习的地物类型的提取
开发语言·人工智能·python·深度学习·神经网络·学习·conda