目录
1、面向过程和面向对象初步认识:
2、类的引入:
3、类的访问限定符及封装 :
3.1、访问限定符:
3.2、封装:
4、类的定义:
5、类的作用域:
6、类的实例化:
7、类对象模型:
7.1、如何计算类对象的大小:
7.2、类对象的存储方式猜测:
7.3、内存对齐规则:
8、this指针:
8.1、this指针的引出:
8.2、this指针的特性:
8.3、关于this指针常见的面试题:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
在C语言中,若定义一个学生的 结构体 ,则如下所示:
//struct Student
//{
// //在C语言中定义结构体的话,结构体成员中只能声明变量、
// char _name[20];
// char _gender[3];
// int _age;
//};
//上述代码,若在C语言中就看做是定义的一个结构体,若在C++中则看做是定义了一个类、
//由于C++兼容C语言,所以C++也兼容C语言中的关键字struct的用法,所以在C++中也能够成功执行上述代码,只不过是,上述代码在C语言看来就是定义了一个结构体,而在C++看来则是定义了一个类,除此之外,在C++中,该类里面不仅仅可以声明变量,也可以声明和定义函数,C++同时对C语言中的关键字struct进行了升级,把C语言中的关键字struct定义的结构体升级成了类,主要意义在于:
//1、现C++中的类名 Student 可以直接作为类型进行使用、
//2、在类里面,除了可以声明变量之外,还可以声明和定义函数、
//在C++中,下述代码则会被视为定义的一个类,其中,struct为类关键字,Student为类名,或者是类标签、
struct Student
{
//声明和定义函数、
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Print()
{
cout << _name << " " << _gender << " " << _age << endl;
}
//声明变量、
//C++中,当在类里面声明变量时,一般在变量名前面或者后面加上_ ,(或者是其他形式,看公司,C++并没有规定必须写成什么形式,但Java中有着明确的规定),注意不是必须要加的,主要是用来表明这是类里面的成员变量,否则可能在某些地方会出现歧义、
char _name[20];
char _gender[3];
int _age;
};
//注意: 上面的类中的变量的声明和函数的声明和定义的位置是随意的,变量上,函数下或者变量下,函数上或者任意位置,都是可以的,这是因为: 类是一个整体,此时编译器并不是只会向上找,而是在该类这个整体里面去寻找,所以上述写法也是可以的,除了类之外,则编译器均默认是向上查找的,那么上述类里面的写法就会出现错误,还要知道,在C++中看到上述代码,要把他看做是定义的一个类,不要把他看做是定义的一个结构体,而我们所谓的C++兼容C语言中的关键字struct的用法,只不过是在使用时可以按照结构体的用法去使用,但本质上已经升级成了类,所以要看做类去处理、
在C语言中:
结构体:
//struct ListNode
//{
// int val;
// struct ListNode* next; //不可以写成:ListNode* next,即使加上typedef也是不可以的,如下所示:
//};
结构体:
//typedef struct ListNode
//{
// int val;
// ListNode* next; //错误写法、
//}ListNode;
在C++中可以写成如下所示:
类: 类,不是只能定义一个,可以定义多个、
//struct ListNode
//{
// int val;
// //C++兼容C语言中struct的用法:
// struct ListNode* next;//正确写法、
// //新增用法,这是因为C++对C语言中的struct进行了升级,把C语言中的struct结构体升级成了类,下面则是类的使用方法:
// ListNode* next; //正确写法、
//};
在C++中一般不需要再对struct ListNode进行typedef为ListNode,因为类名可以直接当做类型进行使用,除非类名过长,若再想对类名进行重命名的话,一般写成下面这种情况;
一、
//struct ListNode
//{
// int val;
// ListNode* next;
//};
//typedef ListNode ST; //struct ListNode s2 或者 ListNode s2; 或者 ST s2; 其中: ST s2; === ListNode s2;
二、
//struct ListNode
//{
// int val;
// ListNode* next;
//};
//typedef struct ListNode ST; //struct ListNode s2 或者 ListNode s2; 或者 ST s2; 其中: ST s2; === struct ListNode s2;
上述方法一般不经常使用,意义不大、
int main()
{
C++兼容C语言中struct的用法,如下所示;
//struct Student s1; //s1在C++中常称为对象,类定义的对象、
//新增用法如下:
Student s2; //s2在C++中常称为对象,类定义的对象、
//访问类里面的变量:
s2._age = 10;
//调用类里面的函数;
s2.Init("惠俊明", "男", 25); //初始化、
s2.Print();//打印、 //惠俊明 男 25
return 0;
}
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
//C语言定义栈结构、
#define _CRT_SECURE_NO_WARNINGS 1
#include
//在C语言中定义的栈结构的数据和方法是分离的、
struct Stack
{
//栈结构的结构体成员变量/数据/属性、
int* a;
int top;
int capacity;
};
//栈结构操纵数据的方法、
void StackInit(struct Stack*ps)
{
ps->a = NULL;
ps->top = 0; //ps->top = -1;
ps->capacity = 0;
}
void StackPush(struct Stack*ps, int x)
{} //具体实现省略、
int StackTop(struct Stack*ps)
{} //具体实现省略,以此方法为例、
//在C语言中定义的栈结构的数据和方法是分离的,会存在什么样的问题呢?
//太过自由,不能很好的进行管理,如下所示:
int main()
{
//定义一个栈(局部结构体变量)、
struct Stack st;
//初始化栈、
StackInit(&st);
//入栈数据、
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
//取栈顶元素、
//方法一: 规范的方法->调用函数接口来取栈顶元素、
printf("%d\n", StackTop(&st));
//方法二: 不规范的方法、
printf("%d\n", st.a[st.top]);
//方法二也是可以的,但是会在某些情况下存在歧义,比如:top不明确到底是栈顶元素的位置还是栈顶元素的下一个位置,
//此时,使用者就可能存在误用,top不明确到底是栈顶元素的位置还是栈顶元素的下一个位置,取决于初始化,若初始化top为0
//则正确访问应该写成:printf("%d\n", st.a[st.top-1]); 若初始化top为-1,则正确写法应是: printf("%d\n", st.a[st.top]);
//数据结构只是一个思想,并没有规定必须把top初始化为某个值,所以使用方法二是不合适的,即,太过自由,不能很好的进行管理,是不好的、
//这是因为方法二的使用和栈结构的结构体成员变量/数据/属性的定义是具有联系的,比如:top初始化的值决定了方法二中到底使用那个语句,
//其次,如果改成链表来实现栈结构的话,那么方法二也需要进行改动才可以达到目的、
return 0;
}
//接下来看一下C++是如何解决上述问题的: C++不会存在像上面这种误用的情况,所以不会出现歧义的,这是因为C++设计出了类、
//C++定义栈结构、
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//在C++中定义的栈结构的 类的数据和类的方法 是合并的、
//1、类的数据和类的方法一起封装到类里面、
//2、当封装到一起后才可以控制访问权限,这样在C语言中若直接通过 数据 进行访问的话,可能会存在歧义,但是若在C++中,把类的方法和类的数据都封装到类中,再把类的数据设置为私有,把类的方法设置为公开,这样一来,在类外面就不可以访问私有的内容,这样就不可以通过 类的数据 来进行访问,就避免了上述可能会出现的歧义、
class Stack
{
//C++的封装本质上就是一种更加严格管理的设计、
//一般情况下,设计类,成员变量/成员对象/类的数据/类的属性都是设置成私有或者保护的,类的方法,即成员函数的权限要分情况考虑,若想开源的就设置成公用,若不想开源的就设置成私有或者保护即可,所以成员函数(类的方法)并不是一定为公用
//也不是一定为私有或保护的,要根据自己的情况进行设定,但成员变量/成员对象/类的数据/类的属性在一般情况下都会设置成私有或保护的、
private:
void CheckCapacity()
{} //用户没有必须去调用判断是否需要增容的函数接口,所以就设置成私有或者保护的即可、
public:
//栈结构操纵数据的方法,即类的方法如下,其次,调用函数的函数名可以简化,并且也不需要把结构传过来、
void Init()
{}
void Push(int x)
{}
int Top()
{}
private:
//栈结构的成员变量/成员对象/类的数据/类的属性、
int* _a;
int _top;
int _capacity;
};
int main()
{
//定义一个栈、
Stack st;
//初始化栈、
st.Init();
//入栈数据、
st.Push(1);
st.Push(2);
st.Push(3);
//取栈顶元素、
//只能通过调用函数接口来取栈顶元素、
cout << st.Top() << endl;
错误方法:
//cout << st._a[st._top] << endl;//这是因为在类里面已经把 类的数据 设置为私有,所以在类的外面不可以进行访问,这样要想成功运行,必须使用上述唯一的方法,所以就避免了出现歧义,这样就不需要考虑到底把top初始化为0还是-1了,不需要关心,这就体现出了C++强制的通过定义使得用户在使用的时候更加规范,这样一来,不需要考虑栈到底是使用顺序表还是链表来实现的,也不需要考虑top的值到底初始化为0还是-1,只需要调用对应的函数接口就可以达到目的,底层的实现过程不需要考虑,都不会影响调用函数接口得到的结果,这就叫做低耦合,关联关系低,在C语言中如果只使用方法一的话也可以做到低耦合,但是避免不了会有人误用了方法二,这样就会出现问题,而在C++中,已经规定了类似于C语言中方法二的用法是错误的,所以就避免了出现歧义的情况关联关系越高,越不好、
return 0;
}
class className //类关键字:class 类名:className
{
//类体:由成员函数和成员变量(成员对象)组成、
}; // 一定要注意后面的分号
//类成员函数在 类内 定义、
//Stack.h文件
#pragma once
class Stack
{
public:
//类成员函数的声明和定义:
void Init() //不需要再指定类域、
{
//从 类内 进行的访问、
_a = nullptr;
_top = 0;
_capacity = 0;
}
void Push(int x)//不需要再指定类域、
{}
void Pop()//不需要再指定类域、
{}
private:
int* _a;
int _top;
int _capacity;
};
//类成员函数在 类外 定义、
//Stack.h文件
#pragma once
class Stack
{
public:
//类成员函数的声明:
void Init();
void Push(int x);
void Pop();
private:
int* _a;
int _top;
int _capacity;
};
//Stack.cpp文件
#include"Stack.h"
//类成员函数的定义:
void Stack::Init() //指定类域、
{
_a = nullptr;
_top = 0;
_capacity = 0;
}//访问限定符限制的是从 类外 进行的访问,此处在类成员函数Init的定义里面访问类成员变量,即类成员函数访问类成员变量不属于从类外进行的访问,因为,虽然该类成员函数的定义在类外,但是该类成员函数的声明在类内,而我们要以声明所在的位置为准,所以,即使该类成员函数的定义在类外,但,该类成员函数仍属于类内,而该类成员函数的函数体内中的所有内容均属于该类成员函数,而该类成员函数仍属于类内,所以,该类成员函数的函数体中的所有内容也均属于类内,这样的话,类成员函数访问类成员变量就属于是从类内进行的访问,所以这种情况下,访问限定符不起任何作用、
//该类成员函数属于类内,可以从该类成员函数的声明和定义中看出来:
//其一,在该类成员函数的定义中,该类成员函数的函数名前面标明了类域、
//其二就是该类成员函数的声明存在于类体里面、
//所以由此可知,该类成员函数是属于这个类内,故该类成员函数定义中的所有内容均属于这个类内,所以此处并
//不属于从 类外 进行的访问,所以即使类的数据是私有的,也是可以访问的,还要知道,访问限定符限制的是从 //类外 进行的访问,但并不限定从 类内 进行访问,从 类内 可以任意访问到私有或者保护和公用的内容、
void Stack::Push(int x) //指定类域、
{}
void Stack::Pop() //指定类域、
{}
注意:可以采用方式一进行类的定义,也可以采用方式二,也可以方式一和二混用,但在一般情况下,更期望采用第二种方式,但这是常见的两
种方式,还有其他方式,比如把方式一整体放在test.cpp里面中main函数的上面也是可以的、
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//类域、
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//这里需要指定PrintPersonInfo是属于Person这个类域、
void Person::PrintPersonInfo()
{
cout << _name << " "<<_gender << " " << _age << endl;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//类,不是只能定义一个,可以同时定义多个,如下所示:
//Stack类域、
class Stack
{
public:
void Push(int x)
{}
};
//Queue类域、
class Queue
{
public:
void Push(int x)
{}
};
//这两个Push函数可以同时存在,这是因为处于不同的作用域内,在不同的作用域内可以存在函数名相同且形参列表相同的函数,以及相同的变量名,
//上述的两个Push函数不构成函数重载(在同一个作用域内,函数名相同,但形参列表必须不同)、
#include
using namespace std;
//静态类成员变量:
class Person
{
public:
//int m_A; //非静态类成员变量,只是声明、
static int _mA;//静态类成员变量,只是声明,类内声明、
//静态类成员变量也具有访问权限、
private:
static int _mB;//静态类成员变量,只是声明,类内声明、
};
int Person::_mA = 100; //类外定义和初始化,要指定类域、
//类外定义和初始化,但不能在main函数中进行,还要保证在 类体外 的下面进行,因为使用到了 Person::,若在 类体外 上面进行的话,系统会向上查找Person::,就找不到,所以会报错,
//在满足上述条件的基础下,可以在任意位置进行静态类成员变量的定义和初始化,其格式如上所示、
//上述在进行静态类成员变量的定义和初始化时,必须要进行指定类域,若写成: int _mA = 10; 的话,这代表的是全局变量,这样就看不出来是对类体中的静态类成员变量进行的定义和初始化
//所以必须要指定类域才可以、
//类外定义和初始化:
int Person::_mB = 200;
void test01()
{
Person p;
cout << p._mA << endl; //100
Person p2;
p2._mA = 200;
cout << p._mA << endl;//200
}
void test02()
{
//非静态类成员变量属于对象,而不属于类,所以其访问形式只能通过对象进行访问,不可以通过类名加作用域限定符的形式进行访问、
//静态类成员变量不属于对象,而属于类,所以它有两种访问形式:
//1、通过对象进行访问静态类成员变量:
Person p;
cout << p._mA << endl; //100
//2、通过类名加作用域限定符的形式进行访问静态类成员变量,也可以不用创建对象再使用对象来进行访问,因为静态类成员变量本身就不属于对象,而属于类、
cout << Person::_mA << endl; //100
//cout << Person::_mB << endl; //错误写法,在 类外 不可以访问私有的静态类成员变量、
}
int main()
{
//test01();
test02();
return 0;
}
#include
using namespace std;
//静态类成员函数:
//1、均共享同一个静态类成员函数、
//2、静态类成员函数只能访问静态类成员变量,不能访问非静态类成员变量、
class Person
{
public:
//void func()//非静态类成员函数的声明和定义、
//{}
static void func()//静态类成员函数的声明和定义、
{
_mA = 100;//类体内的类成员函数访问类成员变量不受访问限定符的限制,限定符只限制从类外进行的访问,类成员函数可以访问类成员变量,类体内的一切对于类成员函数而言都是透明的,
//所以上述操作是可以的,即使静态类成员变量_mA是私有的,也是可以的,这就是所谓的静态类成员函数可以访问静态类成员变量、
cout << "static void func调用" << endl;
//_mB = 200;
//报错,静态类成员函数不可以访问非静态类成员变量,原因是:首先,该静态类成员函数在内存中(公共代码区)只有一份,非静态类成员变量属于对象,而不属于类,所以其访问形式只能通过对象进行访问
//所以要先创建对象,再通过该对象来访问该非静态类成员变量,而静态类成员函数也是共用的,大家共享同一份静态类成员函数,比如由该类名Person实例化出来了两个对象分别为:P1和P2,然后再通过这两个对象调用同一个静态类成员函数func来访问该非静态类成员变量_mB,而该函数体(不管是静态类成员函数还是非静态类成员函数)中并不能区分不同的对象,虽然
//对象P1和P2在调用这个静态类成员函数func,但是该函数体内并不能区分不同的对象,所以静态类成员函数不可以访问非静态类成员变量,因为静态类成员函数中并没有内含this指针,但是在非静态类成员函数中是可以访问非静态类成员变量的,即使其非静态类成员函数的函数体内不能区分不同的对象
//但非静态类成员函数中都内含了一个this指针,所以也可以达到目的,而这里为什么静态类成员函数可以访问静态类成员变量,是因为,静态类成员变量_mA并不属于对象,而是属于类,该静态类成员变量_mA是共享的,大家均共用同一份静态类成员变量_mA,所以在该静态类成员函数的函数体内
//并不需要区分该静态类成员变量_mA到底属于哪个对象,因为静态类成员变量_mA根本就不属于对象,而属于类,大家都共享同一份静态类成员变量_mA,不需要来区分该静态类成员变量_mA到底属于哪个对象,所以、静态类成员函数是可以访问静态类成员变量的、
}
//静态类成员变量、
static int _mA;//静态类成员变量,只是声明,静态类成员变量要在类内声明、
//非静态类成员变量、
int _mB;//非静态类成员变量,只是声明、
//静态类成员函数也具有访问权限、
private:
static void func2()
{
cout << "static void func2调用" << endl;
}
};
//静态类成员变量要在类外定义和初始化、
int Person::_mA = 0; //静态类成员变量在类外定义和初始化,要指定类域、
void test01()
{
//非静态类成员函数也属于类,而不属于对象,但是由于非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),所以即使它属于类
//但是其访问形式也只有一种,即只有通过对象才能访问非静态类成员函数(此时this指针有实值),而不可以通过下述的方式2进行访问非静态类成员函数、
//静态类成员函数不属于对象,而属于类,所以它有两种访问形式:
//1、通过对象进行访问静态类成员函数:
//Person p;
//p.func(); //static void func调用
//2、通过类名加作用域限定符的形式进行访问静态类成员函数,也可以不用创建对象再使用对象来进行访问,因为静态类成员函数本身就不属于对象,而属于类、
Person::func(); //static void func调用
1、
//Person p;
//cout << p._mA << endl;//100
2、
//cout<< Person::_mA << endl;//100
1、
//Person p1;
//p1.func2(); //报错,在 类外 不可以访问私有的静态类成员函数、
2、
//Person::func2();//报错,在 类外 不可以访问私有的静态类成员函数、
}
int main()
{
test01();
return 0;
}
类的定义不能像下面这样进行定义,否则在实例化的时候会出现错误:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
Person p;
};
在实例化的过程中将会无穷无尽,这是被C++语法所不允许的,结论:在类的定义中,类成员变量不能使用当前类名作为其类型、
class A
{
public:
void PrintA()
{
cout<<_a<
class stu
{
public:
void SetStuInfo()
{
}
void PrintInfo()
{
}
//类的成员函数以其地址的形式进行存储、
private:
char _name[10];
int _age;
char _sex[5];
};
一:对象中包含类的所有成员:
当计算由类实例化出来的对象所占内存空间的大小时,若对象中包含类的所有的成员,包括类的成员函数时,那么在计算时,就默认类的成员函
数以其地址(4/8byte)进行存储:
缺陷:每个对象中类的成员变量所存储的值是不同的,但是调用同一份类的成员函数,如果按照此种方式存储,当一个类创建多个对象时,每个
对象中都会保存一份代码(类成员函数的地址),相同代码(类成员函数的地址)保存多次,浪费空间,但这并不是说方式一不可以,它是可以的,但
不太好,那么如何解决呢?
对于上述两种方式,计算机采用的是第二种方式,所以当计算类实例化出来的对象所占内存空间大小的时候,由于对象中只保存了类的成员变量
(虚函数指针和虚基类指针也属于数据部分),而未保存类的成员函数,把类的成员函数存放在了公共的代码段上,类实例化出的所有对象都使
用同一份类成员函数,也就是说在内存中的公共代码区上只有一份拷贝,各个对象通过各自的this指针对他们进行访问,从而节约内存空间,所
以,计算类实例化出来的对象所占内存空间大小就等价于只计算类的成员变量所占内存空间的大小,忽略(不计算)类的成员函数所占内存空间的
大小,其计算方法和C语言中计算结构体变量所占内存大小是一样的,也遵循内存对齐的原则,所以,一个类对象中只包含了类的成员变量,并
没包含类的成员函数、
//类体中既有成员变量,又有成员函数、
class A1 //8byte
{
public:
void f1(){}
private:
int _a;
char _ch;
};
//类体中仅有成员函数、
class A2 //1byte
{
public:
void f2() {}
};
//类中什么都没有---空类、
class A3 //1byte
{};
//sizeof(A2)和sizeof(A3)结果应该是一样的,两者都没有类的成员变量,按理说结果应该都是0byte,但是,当类体里面不存在类的成员变量时,系统会给它开辟1byte的内存空间,这是因为,若某个类型所占内存空间大小为0byte的话,由该类型定义的实例化出的对象(变量)的地址就是空地址,这样就没办法表示该实例化出的对象(变量)存在过,这1byte不是为了存储类成员变量的,而是为了表示实例化出的对象存在过,即,没有类成员变量的类对象,编译器会给他们分配1byte用来占位,表示实例化出的对象存在过,即使考虑上内存对齐,最后的结果也是1byte、
//类体中仅有类成员变量、
class A2 //1byte,这1byte就是用来存储类成员变量_ch的、
{
char _ch;
};
1、第一个成员在与结构体(或类体)偏移量为0的地址处、
#include
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
//此处调用的类成员函数Init是同一份类成员函数(公共代码区),此处调用的类成员函数Print是同一份类成员函数(公共代码区)、
d1.Init(2022, 5, 11);
d2.Init(2022, 5, 12);
d1.Print(); //2022-5-11
d2.Print(); //2022-5-12
return 0;
}
观察上述打印出来的结果,考虑,在此处,两次调用的类成员函数Print是同一份类成员函数(公共代码区),而观察类体内的类成员函数Print的声
明和定义可知,虽然通过对象d1和d2来调用该类成员函数Print,但是该类成员函数Print的函数体内并不能进行区分对象,那么该类成员函数Print
是怎么来打印出正确匹配的结果呢,即,当通过对象d1调用类成员函数Print时,该类成员函数Print是如何知道应该打印对象d1中的类成员变量,
而不是打印对象d2中的类成员变量的呢,同理,两次调用的类成员函数Init是同一份类成员函数(公共代码区),而观察类体内的类成员函数Init的
声明和定义可知,虽然通过对象d1和d2来调用该类成员函数Init,但是该类成员函数Init的函数体内也不能进行区分对象,(一般情况下,不管是普
通的函数还是静态或非静态类成员函数,其函数体内都不能进行对象的区分),那么该类成员函数Init是怎么来初始化出正确匹配的结果的呢,即
当通过对象d1来调用类成员函数Init时,该类成员函数Init是如何知道应该设置对象的d1中的类成员变量,而不是设置对象d2中的类成员变量的
呢,,此时,这两个类成员函数均不属于某一个对象,而是属于类,这是因为,每个非静态类成员函数内部均内含了一个this指针,上述代码中
的两个类成员函数会被编译器进行处理,如下所示:
C++中通过引入this指针解决该问题,即:C++编译器给每个"非静态类成员函数"增加了一个隐藏的this指针参数,让该指针指向当前对象(该非静
态类成员函数运行时调用该非静态类成员函数的对象),在非静态类成员函数的函数体中对所有的类成员变量的操作,都是通过该指针去访问的,
只不过所有的操作对用户是透明的,即,用户不需要来传递,编译器自动完成、
//隐含的this指针、
//每一个非静态类成员函数中都内含了一个this指针,但是每一个非静态类成员函数的形参和实参部分都不能 显式 的写出来有关this指针的内容
//因为这是 隐含的 this指针,是编译器做的活,但是在把非静态类成员函数的声明和定义(不分开写)都放在类体里面的前提下,是可以且只能在该非静态类成员函数的函数体内显式的使用this指针变量的、
#include
using namespace std;
class Date
{
public:
//const放在了this的左边,修饰的是this指针变量,其本身不能被修改,但是this指针变量所指的内容是可以被改变的、
void Print(Date* const this) //该行代码不可以这样写,即,非静态类成员函数的形参部分不能显式的写出有关this指针的内容,this是一个形参指针变量,也是一个新增的关键字、
{
cout << this << endl;//该行代码可这样写,不会报错,因为在满足上述的前提下,可以且只能在该非静态类成员函数的函数体内显式的使用this指针变量的、
//所有访问类成员变量的地方,前面都会加上一个 this-> 、
cout << this->_year << "-" << this->_month << "-" << this->_day << endl; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
//此处访问的是某个对象的_year,_month,_day,并不是类体中的,具体是哪个对象,就看实参传的是哪个对象的地址,这是因为类体中的类成员变量只是声明,不是定义、
}
//const放在了this的左边,修饰的是this指针变量,其本身不能进行改变,但是this指针变量所指的内容是可以被改变的、
void Init(Date* const this, int year, int month, int day) //该行代码不可以这样写,原因同上,Date* const this要放在第一个形参位置上、
{
//this = nullptr; //this指针本身不能被修改、
cout << this << endl;//该行代码可这样写,不会报错,原因同上、
//所有访问类成员变量的地方,前面都会加上一个 this-> 、
//this指针所指的对象是可以被修改的、
this->_year = year; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
this->_month = month; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
this->_day = day; //该行代码可这样写,不会报错,原因同上,若自己不加上this->的话,编译器也会自己加上,所以一般情况下不需要手动加上、
//此处访问的是某个对象的_year,_month,_day,并不是类体中的,具体是哪个对象,就看实参传的是哪个对象的地址,这是因为类体中的类成员变量只是声明,不是定义、
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
//此处调用的类成员函数Init是同一份类成员函数(公共代码区),此处调用的类成员函数Print是同一份类成员函数(公共代码区)、
d1.Init(&d1, 2022, 5, 11); //&d1要放在实参的第一个位置上,该行代码不可以这样写,原因同上、
d2.Init(&d2, 2022, 5, 12); //&d2要放在实参的第一个位置上,该行代码不可以这样写,原因同上、
d1.Print(&d1); //2022-5-11 ,该行代码不可以这样写,原因同上、
d2.Print(&d2); //2022-5-12 ,该行代码不可以这样写,原因同上、
return 0;
}
1、问:this指针是存在哪的?
答:一般情况下(大部分编译器下)是存在于栈区上的,因为this指针是形参,当然,有的编译器会使用寄存器进行优化(比如VS编译器就会使用寄存器进行优化,并且在VS编译器下,在调用函数建立栈帧之前先要进行压参数,该编译器是从右往左压参数的,其他编译器并不一定是从右往左,并没有进行明确的规定)(为什么进行优化?因为我们可能在非静态类成员函数中频繁的使用this指针,若存在栈区上,即存在内存中,其在内存中的读取速度比较慢,频繁的使用this指针就会导致整体效率较低,所以要使用寄存器进行优化,是因为寄存器的读取速度更快,具体在三级缓存中有讲解,所以当频繁使用this指针时,进行寄存器优化会提高效率),将this指针存放在ecx寄存器上,也就是说,this指针在某些编译器下可能会存在于ecx寄存器中,比如VS编译器、
2、问:this指针可以为空吗?
答:可以,因为无论是传递什么,传递过去的都是一个值,空指针也只是一个值,如何理解空指针? 以32位为例,在进程地址空间中,空指针是一个存在的地址,若是32位的话,内存区域总共占4G,是规定的,也和指针的大小具有关联,从0x00000000到0xFFFFFFFF,总共能够存储2^32次方个地址,每个地址存储单位是byte,则总共能够存储2^32byte个地址,即有这些个编号,1个byte等于8bit,所谓,指针就是地址,地址就是指针,地址是一个编号,所以指针也是编号,所以空指针即为0x00000000,第0byte的编号,所以空指针是一个存在且有效的地址,而对空指针进行解引用操作报错的原因是因为:空指针所在的位置是预留出来的,不存储任何数据,不能进行初始化,所以不能够去空指针所指向的内存空间中访问数据,因为他其中是不存储任何数据的,系统会对这一操作进行检查, 若检查到对空指针进行解引用操作,即访问空指针所指的内存空间中的数据时会报错,这一报错是检查规定的行为,内核是存在于高地址上的,还要知道,虚拟内存不是电脑上的物理内存,虚拟内存和物理内存之间还需要通过页表去进行映射,空指针一定是0X00000000,但不是物理内存中的0处的地址,而是虚拟内存中0处的地址,物理内存可以给任何程序进行映射,不需要再进行划分具体的区域,每个程序中都有物理内存,虚拟内存是每个进程地址空间中都有的,进程地址空间也可以成为虚拟进程地址空间、
例题1、//1.下面程序编译运行结果是?A、编译报错 B、运行崩溃 C、正常运行 #include
using namespace std; class A { public: //编译器处理为: void Show(A * const this) void Show()//传过来的是一个空指针、 { //cout << this << endl; cout << "Show()" << endl; } private: int _a; //public: // int _a; }; int main() { A*p=(A*)0x11223344;//程序也能正常运行,打印出Show() 、 A* p = nullptr;//在非静态类成员函数Show调用的过程中并没有对指针变量p,即空指针指向的那块空间进行访问,而是去公共代码区中查找该非静态类成员函数Show的地址,所以不会出现对空指针解引用这样的运行时错误,即运行崩溃,并且,不管指针变量p中存储的值是什么,程序都能正常运行,就是因为,会直接去公共代码区查找,并不会去指针变量p所指的内存空间中进行访问,即所谓的不进行解引用操作,即使指针变量p中存储的值是空指针也是可以的,压根就不会去指针变量p所指的内存空间中进行访问,当然就不会出现运行崩溃的错误、 p->Show();//编译器处理为: p->Show(p); 此处的两个 -> 均不是解引用操作,和普通函数的调用是一样的,去公共代码区里查找该非静态类成员函数Show的地址,变成call地址进行调用、 //p->_a = 10; //错误写法,此时 -> 为解引用操作,不可对空指针nullptr进行解引用操作,会报运行时错误、 } //要知道,访问操作符 .和-> 首次出现在C语言中的结构体章节中,而在C语言中的结构体的定义里面,只能进行结构体成员变量的声明,所以通过结构体类型定义出来的结构体变量 //中把结构体的定义里面的东西全部包含了,所以在C语言中的访问操作符 .和-> 代表的意思均为解引用,故,若在C语言中使用到访问操作符 .和-> 的话,一律把他们看成解引用操作 //而在C++中,已经不存在结构体的概念了,而是把结构体升级成了类,而在类的定义中(类体中),不仅仅能声明类成员变量(静态和非静态),还能声明和定义类成员函数(静态和非静态),而通过类名实例化出的对象中只包含了 //类成员变量(静态和非静态),此时,访问操作符 .和-> 并不全是代表解引用操作,若访问操作符访问的内容是类成员变量(静态和非静态),则访问操作符代表的是解引用操作,这是因为,对象只包含了类成员变量(静态和非静态),若访问的 //内容是类成员函数(静态和非静态)而不是类成员变量(静态和非静态)的话,那么访问操作符代表的就不再是解引用操作了,这是因为,对象中并没有包含类成员函数(静态和非静态),所以若在C++中使用到了访问操作符 .和-> 的话 //首先要观察访问操作符访问的内容到底是类成员变量(静态和非静态)还是类成员函数(静态和非静态),访问操作符访问的内容若包含在了对象中,即访问的内容是类成员变量(静态和非静态)的话,那么访问操作符代表的意思就是解引用, //访问操作符访问的内容若不包含在了对象中,即访问的内容是类成员函数(静态和非静态)的话,那么访问操作符代表的意思就不是解引用操作、 //此时,指针变量p中存储的是空指针nullptr,当进行 p->Show(); 时,看似好像在 //进行解引用操作,但是由于指针变量p中存储的是空指针nullptr,所以对空指针nullptr进行解引用操作是不对的,会报错,但是这种错误不会是编译错误,即, //对空指针nullptr进行解引用或者是对野指针进行解引用操作的话,都属于运行时错误,所以一定不会选择A,而是从B和C中选取,而该题的正确答案应该是C,正常运行 //原因是因为,对象里面只包含类体中的类成员变量(静态和非静态),而不包含类成员函数(静态和非静态),所以,题目中的 -> 并不是在进行解引用操作,并且,每个非静态类成员函数中都内含了一个 //this指针,所以在进行p->Show();时,其实参部分是把指针变量p传参过去,即把一个空指针nullptr传参了过去,这里虽然const修饰的是this指针变量本身,其本身 //不能被修改,但是空指针nullptr是实参传参过去的,并不受const的控制,也就是所谓的,this指针可以为空指针nullptr,即,const修饰的指针变量this本身不能被修改 //但是可以被初始化,而对象中包含类成员变量(静态和非静态),所以若进行 p->_a=10; 的话,-> 才起到解引用的作用,即对空指针进行了解引用,所以会出现运行时错误,
例题2、#include
using namespace std; //2.下面程序编译运行结果是?A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout << _a << endl; } 编译器处理为: //void PrintA(A* const this) //{ // cout << this->_a << endl;//此处进行操作this->_a,由于对象中包含了类成员变量(静态和非静态),所以此处的 -> 为解引用操作,而形参指针变量this接收到的 // //是空指针nullptr,故此处对空指针进行了解引用操作,所以会报运行时错误,相当于访问this指针变量所指向的空间中的内容,所以程序会运行崩溃、 //} private: int _a; }; int main() { A* p = nullptr; p->PrintA(); //编译器处理为: p->PrintA(p); 这里不会出现问题,只是将指针变量p中的空指针nullptr当做实参传给形参this指针变量、 //此处的 -> 不是解引用操作,和普通函数的调用是一样的,去公共代码区里查找该非静态类成员函数PrintA的地址,变成call地址进行调用、 }
注意:之前所谓的,在类外不能访问类体中私有或保护的内容,其实并不是不能访问,而是不能直接(目前所学的,通过类名加作用域限定符或者
通过对象的形式进行的访问称为直接访问)访问,可以通过间接的方法去进行访问,具体等后期再进行阐述、