一、类对象的存储方式
只保存成员变量,成员函数存放在公共的代码段
注意点:
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指针不能显示传递的条件!
-
点运算符(
.
):- 点运算符用于访问对象的实例成员。它表示你在访问某个特定对象的属性或方法。例如,如果你有一个类
Data
的实例data
,你可以通过data.a
来访问a
这个成员变量。
- 点运算符用于访问对象的实例成员。它表示你在访问某个特定对象的属性或方法。例如,如果你有一个类
-
双冒号运算符(
::
):- 双冒号运算符通常用于访问类的静态成员或命名空间中的成员。在一些语言(如 C++ 和 PHP)中,
ClassName::member
用于访问类的静态属性或方法,而不是实例的属性。
- 双冒号运算符通常用于访问类的静态成员或命名空间中的成员。在一些语言(如 C++ 和 PHP)中,
因此,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;
引入:构造函数主要完成初始化工作;(不是创建对象,对象已经在函数栈帧自动创建了!会自动调用函数,函数的功能不是创建对象,而是做初始化工作!)
析构函数主要完成清理工作;(不是创建对象!会自动调用函数,函数的功能不是销毁对象,而是做清理工作!)
四、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。(也不需要写Void)
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
如果把构造函数设置为private,语法上没有问题,但是直接运行会报错!
当我们不写构造函数的时候,编译器会默认生成构造函数**,内置类型不做处理,自定义类型会去调用他的默认构造!**
例如VS2013
此时会发现自定义类型_st被初始化,但是内置类型没有被初始化!
但是VS2019会自动对两种变量都进行处理:
结论:自定义类型一定会被初始化,但是内置类型可能不会被初始化,具体结果取决于编译器!
且如果没有自定义类型,内置类型不会被初始化!
- 一般情况下,如果有内置类型,就需要自己写构造函数!
- 全部都是自定义类型的成员,可以考虑让编译器自己生成!
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个!即我们不写但是编译器会自己生成!
- 但是默认构造函数不一定是编译器自己写的!
五、析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。(默认生成的对内置类型不做修改,对自定义类型做处理)
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
- 内置类型/基本类型,语言本身定义的基本类型int/char/double/指针等;
- 自定义、用struct/class等等定义的类型;