<C++>类和对象|类和结构体|封装|类对象模型|this指针

<C++>类和对象|类和结构体|封装|类对象模型|this指针_第1张图片

文章目录

  • 1. 面向过程和面向对象
  • 2. 类的引入
    • 2.1 C语言中的结构体
    • 2.2 C++中的结构体
    • 2.3 类的概念
    • 2.4 访问限定符
    • 2.5 封装
  • 3. 类的作用域
  • 4. 类的实例化
  • 5. 类对象模型
    • 5.1 结构体内存对齐规则
    • 5.2 类对象的存储
  • 6. this指针
    • 6.1 this指针的引入
    • 6.2 this指针的特性
    • 6.3 关于this指针的问题

1. 面向过程和面向对象

C语言面向过程的,关注的是过程,在解决问题时往往注重求解问题的步骤,通过函数调用逐步求解问题。

<C++>类和对象|类和结构体|封装|类对象模型|this指针_第2张图片
<C++>类和对象|类和结构体|封装|类对象模型|this指针_第3张图片
C++是基于面向对象的,解决问题时往往关注的是解决问题的对象

<C++>类和对象|类和结构体|封装|类对象模型|this指针_第4张图片

c++是兼容c语言的,因此c++并不是纯面向对象,而是基于面向对象,c++也可以和c语言一样面向过程;Java是纯面向对象的语言。


2. 类的引入

2.1 C语言中的结构体

struct Stack
{
	int* a;
	int top;
	int capacity;
};
int main()
{
	struct Stack st;//c语言中struct Stack是一个类型,不能直接使用Stack定义变量
	//Stack st;//error
	typedef struct Stack Stack;//c语言必须使用typedef将struct Stack重定义为Stack
	Stack st;//正确
	return 0;
}
  • c语言中的结构体只能包含成员变量,不能包含函数
  • 未使用typedef时无法使用Stack定义变量,必须用struct Stack定义变量

2.2 C++中的结构体

struct Stack
{
	int* a;
	int top;
	int capacity;
	//C++中的结构体可以包含成员函数
	void Init()
	{
		a = nullptr;
		top = capacity = 0;
	}
	void Push(int val)
	{
		//检查是否需要扩容
		if (capacity == top)
		{
			int newcapacity = capacity == 0 ? 4 : 2 * capacity;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (tmp)
				a = tmp;
		}
		a[top] = val;
		top++;
	}
	int Top()
	{
		return a[top - 1];
	}
};

int main()
{
	struct Stack st1;//C++中结构体定义保留了C语言的方法
	Stack st2;//C++中的结构体定义可以直接省略struct

	//和访问成员变量一样的方式访问成员函数
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	cout << st2.Top() << endl;
}

运行结果:

!<C++>类和对象|类和结构体|封装|类对象模型|this指针_第5张图片

  • C++中的结构体保留了C语言结构体的写法,支持和C语言一样使用struct Stack定义结构体
  • C++中的结构体可以包含成员函数,结构体变量访问成员函数的方法和访问成员变量的方法一样使用.操作符或者->操作符

2.3 类的概念

(英语:class)在物件导向程式设计中是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的特性和方法。

特性是成员变量;方法是成员函数

这么一看,那class是不是和c++中的结构体一样呢?都具有成员函数和成员变量。

我们将定义Stack时的struct替换成class

class Stack
{
	int* a;
	int top;
	int capacity;
	//C++中的结构体可以包含成员函数
	void Init()
	{
		a = nullptr;
		top = capacity = 0;
	}
	void Push(int val)
	{
		//检查是否需要扩容
		if (capacity == top)
		{
			int newcapacity = capacity == 0 ? 4 : 2 * capacity;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (tmp)
				a = tmp;
		}
		a[top] = val;
		top++;
	}
	int Top()
	{
		return a[top - 1];
	}
};

int main()
{
	struct Stack st1;//C++中结构体定义保留了C语言的方法
	Stack st2;//C++中的结构体定义可以直接省略struct

	//和访问成员变量一样的方式访问成员函数
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	cout << st2.Top() << endl;
}

运行结果<C++>类和对象|类和结构体|封装|类对象模型|this指针_第6张图片

❓ 请思考:怎么将struct替换为class后Push不可以访问了?

2.4 访问限定符

如果任何一个类的成员(成员函数、成员属性)我们在类的外部都可以随意访问,那么是不是会造成不安全?比如有Stack类,我们规定只能取出栈顶的元素,如果Stack中的成员属性可以随机访问,那么我们在类外部是不是可以取a[0],a[1]……呢?那是不是就不符合栈的特点了,所以我们引入了访问限定符。

  1. 被访问限定符public修饰的成员可以在类外部随意访问
  2. 被访问限定符protected修饰的成员不可以在类外部随意访问,可以在类内部访问
  3. 被访问限定符private修饰的成员不可以在类外部随意访问,可以在类内部访问

如何使用访问限定符呢?
class Stack举例子,我们只想让别人访问Stack的顶部元素,不能访问其他任意位置,因此我们将Top函数设置为public访问属性;而成员属性a,top,capacity全部设置为private访问属性,使它们在类外部无法直接访问,当我们要取出栈里元素时只能调用public的Top函数,这个函数就是取出栈顶元素,换句话说,我们通过访问限定符限制了我们只能取出栈的顶部元素。

再回到上面的代码,为什么将struct替换成class后显示Push不可以访问呢?那是不是说明了此时Push成员函数的访问属性为private或者protected,没错**,**

结论1对于class来说若没有显示规定访问属性则默认为private,对于struct来说若没有显示规定访问属性则默认为public(因为c++需要兼容c语言的struct)

尝试将class的成员函数设置为public访问属性

class Stack
{
	int* a;
	int top;
	int capacity;

public:// 成员方法设置为public
	void Init()
	{
		a = nullptr;
		top = capacity = 0;
	}
	void Push(int val)
	{
		//检查是否需要扩容
		if (capacity == top)
		{
			int newcapacity = capacity == 0 ? 4 : 2 * capacity;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (tmp)
				a = tmp;
		}
		a[top] = val;
		top++;
	}
	int Top()
	{
		return a[top - 1];
	}
};

int main()
{
	struct Stack st1;//C++中结构体定义保留了C语言的方法
	Stack st2;//C++中的结构体定义可以直接省略struct

	//和访问成员变量一样的方式访问成员函数
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	cout << st2.Top() << endl;
}

运行结果[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5PuDZml-1689904140752)(images/image-20230720102601767.png)]

加上public后,我们仍然不能在类外部访问成员属性[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNkny7WL-1689904140752)(images/image-20230720102835303.png)]
结论2:成员访问限定符的作用范围是从当前位置开始持续到下一个访问限定符,若没有下一个访问限定符则对接下来所有的成员都作用

class Stack成员属性a,top,capacity都是默认的private访问控制权限,持续到成员函数Init,因此不能在类外部直接访问成员属性。

2.5 封装

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件

<C++>类和对象|类和结构体|封装|类对象模型|this指针_第7张图片
<C++>类和对象|类和结构体|封装|类对象模型|this指针_第8张图片

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用


3. 类的作用域

类定义了一个新的作用域,类的成员属性就属于该作用域中声明的变量,若要在类外部访问该类的属性,需要使用作用域操作符::来指明该变量属于哪个类

例如在Stack.h文件中声明class Stack

//Stack.h文件中
class Stack
{
private:
	int* a;
	int top;
	int capacity;
public:
	void Init();
	void Push();
	int Top();
};

在Stack.cpp文件中定义class Stack

//Stack.cpp文件中

//使用了Stack中的成员属性需要指名类域为Stack
void Stack::Init()
{
	a = nullptr;
	capacity = top = 0;
}
//使用了Stack中的成员属性需要指名类域为Stack
void Stack::Push(int val)
{
	//检查是否需要扩容
	if (capacity == top)
	{
		int newcapacity = capacity == 0 ? 4 : 2 * capacity;
		int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
		if (tmp)
			a = tmp;
	}
	a[top] = val;
	top++;
}
//使用了Stack中的成员属性需要指名类域为Stack
int Stack::Top()
{
	return a[top - 1];
}

4. 类的实例化

使用类类型定义对象的过程称为类的实例化,例如class Stack st;,我们使用类型Stack定义了一个对象st,这个过程称为Stack的实例化。

  1. 类规定了创建的对象具有哪些属性、方法,类是一个模型,类的实例化是根据该模型去创建对应的对象。类自身的定义并不占物理存储空间。
  2. 类实例化创建的对象占物理存储空间,用来存储成员变量
  3. 类就像建筑图纸,实例化出来的对象就像根据图纸构造出来的房子,图纸实际上不占物理存储空间,但是房子占物理存储空间
  4. 一个类可以实例化出多个对象,例如Stack st1;Stack st2;每一个对象都具有独立的存储空间
    <C++>类和对象|类和结构体|封装|类对象模型|this指针_第9张图片

5. 类对象模型

类和结构体很相似,结构体的大小是所有成员变量加起来的大小并进行内存对齐,那么类的大小是不是和结构体一样呢?

我们来看不同的类的大小

//既有成员函数又有成员属性
class A
{
private:
	char b;
public:
	void f() {};
};
class B
{
private:
	char b;
	int a;
public:
	void f() {};
};

//只有成员属性
class C
{
private:
	char b;
	int a;
};

//只有成员函数
class D
{
public:
	void f() {};
};

//空类
class E
{

};
int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
	cout << sizeof(D) << endl;
	cout << sizeof(E) << endl;
}

运行结果:<C++>类和对象|类和结构体|封装|类对象模型|this指针_第10张图片

结论:

  1. 类的大小不算成员函数的大小,只算成员属性加起来的大小并进行内存对齐
  2. 空类或者没有成员属性的类占一个字节,该字节仅仅用来占位说明对象存在而不存储数据。

5.1 结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8

  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

结构体内存对齐

5.2 类对象的存储

类的大小只算类中的成员属性,那么使用类定义对象时,对象中的成员方法存放在哪里❓

由于同一个类的成员函数只有一份,因此将一个类中的成员函数与全局函数共同存放在公共代码区。当对象调用该函数时会去公共代码区寻找该函数的执行指令<C++>类和对象|类和结构体|封装|类对象模型|this指针_第11张图片

上述结论也可以从汇编指令中得到<C++>类和对象|类和结构体|封装|类对象模型|this指针_第12张图片

不同对象调用同名的成员函数是同一个,它存放在公共代码区

结论:存储对象的空间中只会存储该对象所属类的成员属性,不会存储成员函数,若要调用成员函数就要去公共代码区寻找该函数。


6. this指针

6.1 this指针的引入

我们来看Date类的定义

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
};

int main()
{
	Date d1, d2;
	d1.Init(2023, 7, 20);
	d2.Init(2022, 8, 10);
	d1.Print();
	d2.Print();
}

运行结果<C++>类和对象|类和结构体|封装|类对象模型|this指针_第13张图片

上述代码存在一个问题:前面提到过,类中的函数存放在公共代码区,也就是d1.Initd2.Init调用的是同一个函数,但是Init函数中的代码为_year = year;_month = month;_day = day,再调用Init函数时,编译器是如何知道此时的_year,_month,_day是当前对象的成员属性呢?也就是说d1.Init(2023, 7, 20)是如何将2023赋值给d1的_year的呢?

在C++中,每一个类的成员函数在调用时编译器都会默认隐式传一个参数,该参数为this指针,用来指向调用函数的当前对象。

例如,Init成员函数的定义在编译器看来是这样的

void Init(Date* this, int year, int month, int day)//编译器隐式传递this指针
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

而调用语句d1.Init(2023,7,20)会被编译器转换为d1.Init(&d1, 2023, 7, 20),因此编译器会根据this指针访问该对象的成员属性

从下面汇编指令可以得出编译器调用时会隐式传递this指针<C++>类和对象|类和结构体|封装|类对象模型|this指针_第14张图片

前3步是将参数20,7,2023依次压栈,第4步是将d1的地址存放在寄存器ecx中(&d1并没有压栈而是存放在寄存器中)

所以,this指针是用来简化我们调用成员函数的步骤,使我们不需要显示的传递对象地址,而传递对象地址这件事由C++编译器帮我们完成。

总结:
C++编译器给每一个非静态成员函数添加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中访问所有“成员属性”的操作都由该指针变量完成,所有操作对用户来说都是透明的,即用户不需要显示的传递该指针参数,由编译器自动完成

6.2 this指针的特性

  1. this指针的类型是*const this,即在成员函数中不可以更改this指针的指向。
  2. this指针只能在成员函数中显示调用,不能在函数参数中显示声明,也不能调用成员函数时显示传递该参数。
  3. this指针的本质是对象地址的形参,即调用成员函数时会将对象的地址作为实参传递给形参,对象中不存储this指针
  4. this指针是成员函数的第一个隐藏的参数,不需要用户显示传递,一般情况下编译器通过寄存器ecx自动传递。

6.3 关于this指针的问题

  1. this指针存放在哪里?
    this指针通常存储在栈区,有时编译器会将它优化到register中,VS中C++编译器将this指针存放在寄存器ecx
  2. this指针可以为空吗?
    可以

练习

1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

运行结果:<C++>类和对象|类和结构体|封装|类对象模型|this指针_第15张图片

解释:
前面说过,成员函数是存放在公共代码区的,调用成员函数时只会区公共代码区寻找函数代码段,即使这里this指针为空,也不会对空指针进行解引用,而Print函数内部也没有对空指针进行解引用,因此程序正常运行。上述代码等价于<C++>类和对象|类和结构体|封装|类对象模型|this指针_第16张图片

我们可以观察上述代码的汇编指令

<C++>类和对象|类和结构体|封装|类对象模型|this指针_第17张图片

2.下面程序编译运行结果是? A、编译报错 B、运行崩溃C、正常运行

class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

解释:
函数调用不会出错,问题在于Print函数内部访问了this指针所指向对象的_a成员,这就造成了解引用空指针,程序会崩溃。<C++>类和对象|类和结构体|封装|类对象模型|this指针_第18张图片

你可能感兴趣的:(C++,c++,开发语言)