总言
类和对象入门:初步认识面向对象、类的基本认识(定义、访问限定符、作用域、实例化、大小计算)、this指针。
1、类的引入
📌什么是面向对象编程(OOP)和面向过程编程(POP)?它们的主要区别是什么?
📌解释一下类和对象的概念,以及它们在OOP中的作用。
📌描述一下封装、继承和多态的概念,并解释它们在OOP中的重要性。
📌你能给出一个使用面向对象设计模式的例子吗?并解释为什么使用这种模式。
1.1、对面向对象和面向过程的初步认识
概念说明
1、C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
2、C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
3、即C++非纯面向对象,可以面向对象和面向过程混编(因其兼容C语言),纯面向对象的语言,比如Java。
简单举例:
比如:设计一个学校外卖系统。
对面向过程:
关注点:点餐、接单、送餐过程,即关注流程函数的实现。
对面向对象:
关注点:用户、商家、骑手,即关注点在对象之间的关系。
1.2、类的引入(结构体在C、C++中的区别)
1)、C语言和C++对结构体的使用区别
1、C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
以实现一个栈来举例:
①以C语言实现栈:我们要定义一个结构体,并在外部实现其对象的增删查改。
// c
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps);
void StackPush(struct Stack* ps, int x);
void StackPop(struct Stack* ps);
②以C++来实现栈,可以看到,我们能在结构体中直接定义需要的功能函数(增删查改)。此外,这些函数也能访问结构体的成员变量。
//C++
struct Stack
{
void Init()
{
a = 0;
top = capacity = 0;
}
void Push(int x)
{
// ...
}
void Pop()
{
// ...
}
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st1;//对C的兼容:比如此处的定义结构体
Stack st2;//升级成为对象:此处创建结构体我们可以直接使用类名
st1.Init();//升级成为对象:以类似于访问结构体成员的方法访问成员函数。
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
即,C++兼容C中结构体的语法,同时将结构体struct升级成了类。(以下演示在C语言中结构体自引用部分曾提及过,这里再次提及,用以回顾区别。)
// C
typedef struct ListNode_C
{
struct ListNode_C* next;//C中定义了结构体,我们在使用时也需要写明struct ListNode_C
int val;
}LTNode;//typedef在此行以后才生效
// C++
struct ListNode_CPP
{
ListNode_CPP* next;//而C++中定义结构体,我们可直接使用ListNode_CPP,因为其也是类名。
int val;
};
需要说明的是,尽管可以使用struct
,但在C++中更喜欢用class
来代替。此外struct
和class
也有细节区别(后续谈到)。
2、类的基本认识
2.1、类的访问限定符和封装
2.1.1、类的访问限定符
📌什么是类的访问限定符,它们在C++中有哪些?
📌解释一下public、private和protected访问限定符的含义。
1)、整体介绍
2)、举例理解
对公有和私有:
在上述,我们演示了用C++来写一个struck栈,此处我们将struct修改为class,即类。
class Stack
{
void Init()
{
a = 0;
top = capacity = 0;
}
void Push(int x)
{
// ...
}
void Pop()
{
// ...
}
int* a;
int top;
int capacity;
};
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
可看到结果报错。即:class的默认访问权限为private,struct为默认为public(因为struct要兼容C)。
那么上述问题需要如何修改呢? 这里我们就需要使用到访问限定符:
class Stack
{
public: //在其头上添加上public访问限定符,表示下述内容为公有,直到遇到下一个访问限定符或者类作用域结束。
void Init()
{
a = 0;
top = capacity = 0;
}
void Push(int x)
{
// ...
}
void Pop()
{
// ...
}
private://将类中的成员变量设置为私有。
int* a;
int top;
int capacity;
};
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
需要注意的是,private
、public
、protected
访问限定符是针对类外访问 。在类中,即使是private私有成员,我们也能正常访问。
一个问题:C++中
struct
和class
的区别是什么?
回答:
1、C++需要兼容C语言,所以C++中 struct
可以当成结构体 使用。此外,C++中struct
还可以用来定义类 ,和class
定义类是一样的。
2、区别是struct
定义的类默认访问权限是public
,class
定义的类默认访问权限是private
。
3、注意:在继承和模板参数列表位置,struct
和class
也有区别,后序将学习到。
2.1.2、类的封装(面向对象的三大特性之一)
1)、面向对象的三大特性:封装、继承、多态
1、此处的含义是,面向对象有很多特性,但封装、继承、多态这三大特性占据相对重要的位置。
2、类和对象章节,主要是研究类的封装特性(继承和多态见后续章节)。
2)、什么是封装?
2.2、类的定义和作用域
2.2.1、基础说明
1)、类的基本结构格式
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class
:定义类的关键字
ClassName
:类的名字
{}
中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容:类的成员,类中可存在成员变量和成员函数。
类中的变量:类的属性或成员变量
类中的函数:类的方法或者成员函数。
2.2.2、类成员函数两种定义方式(声明、定义)
2.2.2.1、声明和定义同时定义在类体中
方式一:声明和定义同时定义在类体中
说明: 成员函数如果在类中定义,编译器 可能会将其当成内联函数处理。
这里的“可能”指的是编译器有选择的余地,并且,是否真正内联还取决于编译器的优化策略、函数的复杂度、调用频率以及其他一些因素。
当函数被内联时,调试器在逐语句调试时可能无法“进入”该函数,因为该函数体已经被嵌入到调用它的代码中。这意味着调试器可能不会显示一个单独的内联函数步骤,而是直接跳过到该函数调用之后的代码。然而,调试器通常会提供一些方式来查看内联函数的源代码或汇编代码,以帮助开发者理解程序的执行流程。
演示代码如下:
class Stack
{
public:
void Init()
{
a = 0;
top = capacity = 0;
}
void Push(int x)
{
// ...
}
void Pop()
{
// ...
}
int* a;
int top;
int capacity;
};
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
验证操作如下:
根据之前所学(C++基础章),debug版本下默认内联不起效,此处几个函数都有Call操作。(以下为相关汇编语言)
st1.Init();
00275A91 lea ecx,[st1]
00275A94 call std::operator<<<std::char_traits<char> > (027129Eh)
st1.Push(1);
00275A99 push 1
00275A9B lea ecx,[st1]
00275A9E call std::endl<char,std::char_traits<char> > (0271299h)
st1.Push(2);
00275AA3 push 2
00275AA5 lea ecx,[st1]
00275AA8 call std::endl<char,std::char_traits<char> > (0271299h)
st1.Push(3);
00275AAD push 3
00275AAF lea ecx,[st1]
00275AB2 call std::endl<char,std::char_traits<char> > (0271299h)
我们设置后让它生效来观察,可看到直接没有call操作了。
说明符合条件情况下,当成员函数声明和定义都在类中时,其会被当成内联函数处理。
Stack st1;
st1.Init();
00085A91 mov dword ptr [st1],0
00085A98 mov dword ptr [ebp-0Ch],0
00085A9F mov dword ptr [ebp-10h],0
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
00085AA6 xor eax,eax
2.2.2.2、声明和定义分离
方式二:声明和定义分离
声明和定义分离:是指类声明放在.h
文件中,成员函数定义放在.cpp
文件中。
此处我们以队列来演示:
//Queue.h头文件 :涵盖了我们定义的类及其成员变量、成员函数声明
struct QueueNode
{
QueueNode* next;
int val;
};
class Queue
{
public:
void Init();
void Push(int x);
void Pop();
private:
QueueNode* head;
QueueNode* tail;
};
关于类成员函数声明和定义分离中,有一点需要注意:
我们在定义成员函数时,需要说清楚对应函数出自哪个域(此处即类域::
后续讲到)。
如果没有作用域限定符指出函数的来源,此处使用其中的类成员tail
、head
编译会报错。因为编译器是先在局部域中寻找相关相关变量,若没有,则去全局域中寻找。在没有展开命名空间的情况下,编译器是不会自动对命名空间进行搜索的。
//Queue.cpp文件 :涵盖了类成员函数的具体实现方法
#include "f.h"
void Queue::Init()//成员函数名前需要加类名:: 含义是Init这个函数属于Queue该类域
{
head = tail = nullptr;
}
void Queue::Push(int x)
{}
void Queue::Pop()
{}
从这里可以得出一个结论:在成员函数声明和定义分离时,不能使用内联,因为内联要求声明和定义不分离。
2.2.2.3、小结
简单使用总结:
1、类中函数若是小函数,想要成为inline,可以直接放在类里面定义。
2、类中函数若是大函数,则应该声明与定义分离。
使用声明定义分离的主要意义: 方便看类体的整体概况。
例如:如果只是想了解有些什么函数,而不具体了解该函数怎么实现,就可以直接在.h
定义了类体的文件中查看。而当要具体看某个函数的实现时,再跳转到对应的.cpp
文件。
2.2.3、类的作用域
📌什么是类的作用域,它与对象的作用域有何不同?
📌解释一下类的作用域解析运算符(::)的含义和用途。
📌类的作用域与命名空间的作用域有何异同?
📌什么是类的作用域嵌套,它有哪些规则和限制?
📌类的作用域与函数的作用域有何关联?
1)、基本说明
类定义了一个新的作用域,类的所有成员都在类的作用域中。 在类体外定义成员时,需要使用::
作用域操作符指明成员属于哪个类域。
以下为一个演示例子:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
假若不加Person::
,此处的PrintPersonInfo
可以和类中的同名函数共存,原因是二者作用域不一致。但此处_name
等私有成员变量就不能够被访问。
2)、扩展:类域与生命周期
1、有些域会影响生命周期,有些域只影响访问。 比如,类域和命名空间域只影响访问,而局部域会影响生命周期。
2、实际上,域更多的影响的是编译器的搜索规则。 比如,默认情况下先在局部域中找,然后再在全局域中寻找。若指定了命名空间,则在命名空间里找,指定了类,则在类里找。
3、生命周期与存储的位置有关系。 在Linux中,我们将学到进程内存地址空间,比如栈区、堆区、全局区、代码区等。在不同区域存储的变量有其一定的生命周期。 比如堆区是malloc或new出来的,因此其生命周期若我们不free或delete就一直存在。
2.3、类的实例化
2.3.1、如何区别一个变量是声明还是定义?
1)、问题引入
问题: 区分函数是声明还是定义相对而言比较容易,如此处的 void PrintPersonInfo();
,其是函数声明,后续的void Person::PrintPersonInfo()
则是函数定义。然而,如何区分一个变量是声明还是定义呢? 比如此处的 _name[20]
、_gender[3]
、_age
。
//person.h头文件
int age;//此处的变量是定义
class Person
{
public:
void PrintPersonInfo();//这里是函数声明
private:
char _name[20]; //提问:此处是变量声明还是定义?
char _gender[3];
int _age;
};
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
2)、解释声明和定义
概述: 对于变量声明和定义,其最大的区别在于是否给变量开辟了空间。 声明只是告诉编译器某个变量或函数的名称和类型,而定义则是实际创建变量并为其分配内存空间的过程。
声明(Declaration): 声明是告诉编译器某个变量或函数的名称以及其类型。它通常出现在代码的顶部,并在其作用域内声明变量的名称和类型。声明不分配内存空间给变量,它只是让编译器知道变量即将被使用。
int a; // 这是一个声明,告诉编译器有一个名为a的整数类型变量
定义(Definition): 定义是实际创建变量的过程,它不仅告诉编译器变量的名称和类型,还为变量分配内存空间。定义通常在声明之后进行,并且只能进行一次。
int a = 10; // 这是一个定义,它创建了一个名为a的整数类型变量,并为其分配了内存空间,同时初始化为10
3)、回到上述问题
上述类中成员变量并没有开辟空间,因此其属于声明。只有当我们用person
这个类去定义一个对象时(类的实例化),才算真正的开辟空间。如下:
int main()
{
Person p1;//对象p1:是用Person类的实例化
Person p2;
cout << sizeof(Person) << endl;//问题一:关于类的大小计算。
return 0;
}
此处有一个问题,既然只有类时相关变量还没有开辟空间,为什么我们可以直接用类计算出大小?
这就像建造房子,虽然我们暂时还没把房屋建造出来,但是因为拥有了平面设计图,我们是能知道这房屋能够住多少人的。
类似的,虽然我们并没有用类去实例化一个对象,但类的模板在那里,我们自然也能算出其大小。
2.3.2、类的实例化解释
1)、相关解释
基本介绍: 用类类型创建一个或多个对象的过程,称为类的实例化。
事项说明:
1、类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员。定义出一个类并没有分配实际的内存空间来存储它。
2、一个类可以实例化出多个对象,实例化出的对象才占用实际的物理空间,存储类成员变量。
2.3.3、扩展:关于头文件中定义一个全局变量,如何在多个.cpp文件中展开使用说明
2.3.3.1、问题说明
person.h头文件内容:
//这是person.h头文件
#pragma once
#include<iostream>
using namespace std;
int age;//我们在头文件中定义了一个全局变量
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
person.cpp文件内容:
//这是person.cpp函数方法实现文件
#include "person.h"
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
test.cpp文件内容:
#include "person.h"
int main()
{
Person p1;
Person p2;
cout << sizeof(Person) << endl;
return 0;
}
三个文件总体预览已经运行结果演示:可发现最后链接错误。这是因为person.h
文件在person.cpp
、test.cpp
两个文件中展开,头文件中的 int age
该全局变量将被定义两次。
2.3.3.2、解决方案一:extern
解决方案一: 在person.h
中将原代码修改为:extern int age
。
原理:此处extern
是告诉编译器这是一个变量的声明。(PS:需要与extern C
区别开,后者是告诉编译器其中代码需要用C语言编译)。
//person.h文件内容:
#pragma once
#include<iostream>
using namespace std;
extern int age;//添加一个extern:告诉编译器这是一个声明
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
然后在其中一个.cpp
文件中定义全局变量,其他文件只需要包含这个声明,就可以在其它.cpp
文件中使用该全局变量。
//person.cpp文件内容:
#include "person.h"
int age = 10;//定义
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
2.3.3.3、解决方案二:static
解决方案二: 如果不使用extern解决,则可使用static
来解决。
原理:限制了被修饰变量的链接属性,让其只能在当前文件可见。(而普通的全局变量则是在所有展开文件中可见(放入符号表))。
解释:如果用static
修饰的全局变量是在.h
头文件中,而在其它两个不同的.cpp
文件中都包含了该头文件,那么该static
修饰的全局变量在两个.cpp
文件中是不同的存在(连地址都不同)。若没被static
修饰,则在正确声明和定义的情况下,该全局变量是同一个。
//person.h文件内容:
#pragma once
#include<iostream>
using namespace std;
static int age;//我们在头文件中定义了一个全局变量
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//person.cpp文件内容:
#include "person.h"
void Person::PrintPersonInfo()
{
//cout << _name << " " << _gender << " " << _age << endl;
cout << "person.cpp age:" << &age << endl;
}
//test.cpp文件内容:
#include "person.h"
int main()
{
Person p1;
Person p2;
p1.PrintPersonInfo();
cout << "test.cpp age:" << &age << endl;
return 0;
}
演示结果:可看到虽然person.h
在person.cpp
和test.cpp
文件中展开,但头文件中全局变量age在这两个中的地址不同。
2.4、类对象模型
2.4.1、如何计算对象的大小(引入篇)
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
以下为演示使用代码:
class A
{
public:
void PrintA() //类中有成员函数
{
cout << _a << endl;
}
private:
char _a; //类中有成员变量
};
int main()
{
cout << sizeof(A) << endl;
}
成员函数是否要计算在内呢? 我们可以简单推测与验证一番。
假如成员函数计算在内,PrintA
是函数指针类型,为4个字节,再加上一个char类型的_a
,一共5字节。和结构体类似,此处需要对齐,那么总的来说是8字节。
来看看运行结果:结果却显示此处只有1字节,说明成员函数不计算在内。
这就有一个疑惑,类中成员是如何存储的,以至于才会得到上述这样的大小结果?
2.4.2、类对象的存储方式猜测
演示代码如下:
class A
{
public:
void PrintA()
{
cout << "_a " << &_a << endl;
}
char _a;
};
1)、类对象存储方式推测一:成员函数和成员变量都存储在类对象里
这种存储方式导致结果/问题:每个对象中都会保存一份代码,相同代码保存了多次,造成浪费空间。
class A
{
public:
void PrintA()
{
cout << "_a " << &_a << endl;
}
char _a;
};
int main()
{
A aa1;
A aa2;
aa1._a = 1; //不同对象中,此处的_a是同一个变量吗?
aa2._a = 1;
printf("aa1_a:%p\n", &(aa1._a));
printf("aa2_a:%p\n", &(aa2._a));
aa1.PrintA(); 不同对象中,此处的PrintA()是同一个函数吗?
aa2.PrintA();
}
验证结果:同一个类类型实例化出的地址不同的对象,其中,相同的成员变量是独自存储的,但却调用同一份成员函数(即成员函数相同)。
2)、类对象存储方式推测二:将成员函数置成一张虚表,在对象中存储指向该虚表的指针
结果:这中存储方式的设计保障了成员函数只存储一份,不向方案一一样存储多份。
3)、对象存储方式三:只存储成员对象,将成员函数存储在公共代码区(代码段)
对第三种存储方式的含义理解·实例:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void func()
{
cout << "void A::func()" << endl;
}
//private:
char _a;
};
int main()
{
//以下运行结果是什么? A、崩溃 B、编译报错 C、正常运行
A* ptr = nullptr;
ptr->func();
return 0;
}
回答:程序正常运行。
解释:
1、首先,要明确代码含义:ptr = nullptr
,将ptr定义为空指针,而后ptr->func()
对空指针解引用。
2、根据我们所学,空指针解引用是会报错的。但此处既然能正常运行,说明并没有经历解引用空指针这一操作。
3、而要达成这样的情况,在三种类对象存储方式中,只能是第三种存储方式。
原因:
1、若是第一种存储方式(即将函数存储在对象中),调用时就需要解引用指针,在对象中找该函数的地址。但是对象是空指针,解引用会崩溃。
2、对于第二种存储方式(在对象中存储的是指向函数地址存储的指针),仍旧需要解引用访问对象,通过对象中存储的函数指针,找到函数地址。由于对象为空指针,仍旧会崩溃。
3、对于第三种(函数地址存储在一个公共代码区),在编译链接时会到该公共代码区找到函数地址对其进行call。而非去ptr指针指向的对象中寻找该函数地址(不去ptr对象中寻找,就不涉及ptr指针的解引用,就并不会发生空指针解引用问题)
2.4.3、内存对齐规则与例题分析
1)、结构体内存对齐规则:同样适用于类
2)、一些例题分析
分析例题一:
下面不同对象的大小分别是什么?
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
cout <<" " << sizeof(A1) << endl;
cout <<" " << sizeof(A2) << endl;
cout <<" " << sizeof(A3) << endl;
return 0;
}
注意事项:
1、一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
2、注意空类的大小,空类比较特殊,编译器给了空类一个字节用于占位,不存储实际数据,而是标识对象的存在(反过来理解亦然。正因为对象存在,拥有不同的地址,所以不能视作内存大小都为0。)
class A3
{};
int main()
{
A3 a1;
A3 a2;
A3 a3;
cout <<" " << sizeof(a1) << endl;
cout << &a1 << endl;
cout << " " << sizeof(a2) << endl;
cout << &a2 << endl;
cout << " " << sizeof(a3) << endl;
cout << &a3 << endl;
return 0;
}
2.5、this指针
2.5.1、为什么要拥有this指针(引入篇)
1)、关于类中定义的变量为什么下标要加_
的原因:
在不加下标的情况:
class Date
{
public:
void Init(int year, int month, int day)
{
year = year; //此处的初始化函数:是谁为谁赋值?谁是类成员,谁是传入形参?
month = month;
day = day;
}
private:
int year; // 年
int month; // 月
int day; // 日
};
int main()
{
return 0;
}
①如上述代码,为防止在类中定义成员函数时,无法区分成员和形参(传入实参)。
②故在CPP中,常见变量写法:_year
,year_
,m_year
以下为命名规则的一些方法说明:
Ⅰ、单词与单词之间首字母大写间隔(驼峰法),例:GetYear
Ⅱ、单词全部小写,单词之间用下划线分割,例:get_year
Ⅲ、这里我们主要采用驼峰法:
a、函数名、类名等所有单词首字母大写 DateMgr
b、变量首字母小写,后面单词首字母大写 dateMgr
c、成员变量首单词前面加下划线"_" _dateMgr
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
return 0;
}
2)、this指针引入:
根据我们上述代码,我们实现一下这个日期类几个简单功能,让它能够在我们输入日期时,将其打印出来:
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout <<_year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Data d1;
Data d2;
d1.Init(2022, 11, 11);
d2.Init(2022, 12, 12);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样的一个问题:
Date类实例化了出了两个对象。函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
3)、为什么需要this指针:
C++中通过引入this
指针解决该问题,即:C++编译器给每个 “非静态的成员函数” 增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
在编译器眼里,上述代码实际上是这样的:
//编译器对类成员函数的处理
class Date
{
public:
//C++编译器给每个非静态的成员函数增加了一个隐藏的指针(this指针)
void Init(Date* const this, int year, int month, int day)//此处const加在*之后,修饰的是 this指针本身不能被修改
{
this->_year = year;//编译器的处理:在成员变量前面都会加一个this指针,指向相应对象
this->_month = month;
this->_day = day;
}
//下面这个成员函数也一样
void Print(Date* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year; // 年 -> 声明
int _month; // 月
int _day; // 日
};
//编译器对实际传参的处理:
int main()
{
Data d1;
Data d2;
d1.Init(&d1,2022, 11, 11);//传递的是类对象的地址
d2.Init(&d2,2022, 12, 12);
d1.Print(&d1);
d2.Print(&d2);
return 0;
}
注意事项:
1、对我们用户而言,我们不能在实参和形参位置处显示传递和接受this
指针,但我们 可以在成员函数内部可以使用this
指针。
2、注意 this
指针是被const
修饰的,即使我们能在成员函数中使用,我们也不能随意修改它的指向。
4)、一些例题分析:
分析例题一:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
// 2.this指针存在哪里? A、堆 B、栈 C、静态区 D、常量区
class A
{
public:
void Print( )
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // C、正常运行
return 0;
}
分析例题二:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
// 2.this指针存在哪里? A、堆 B、栈 C、静态区 D、常量区
class A
{
public:
void PrintA()
{
cout << this << endl;
cout << _a << endl;
cout << _a << endl;
//cout << this_a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA(); // B、运行崩溃
return 0;
}
2.5.2、this指针特性
1、this指针的类型:类类型* const
,即成员函数中,不能给this
指针赋值。
2、只能在“成员函数”的内部使用。
3、this
指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this
形参。所以,对象中不存储this
指针。
4、this
指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx
寄存器自动传递,不需要用户传递。
关于this指针存在哪的问题说明:
通常情况下this
指针存在栈区,因为它是一个形参,形参存在栈上。有时编译器为了提高this
访问效率,会对其进行优化处理,比如vs下 优化处理后this
指针存放在寄存器(ecx
) 中。
2.5.3、C语言实现栈和C++实现栈的对比
后续学习。