类和对象(上)

发布于:2024-11-11 ⋅ 阅读:(106) ⋅ 点赞:(0)

目录

  • 面向过程和面向对象初步认识
  • 类的引入
  • 类的访问限定符及封装
  • 类的定义
  • 类域
  • 类的实例化
  • 类对象模型
  • this指针
  • C++和C语言实现Stack的对比

1. 面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

2. 类的引入

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数

#include <iostream>

using namespace std;

//类和对象
//1个类 实例化 N个对象

//C++把struct升级成了类
//1、类里面可以定义函数
//2、struct名称就可以代表类型

//C++兼容C中struct的用法
typedef struct ListNodeC
{
	struct ListNodeC* next;
	int val;
}LTNode;

struct ListNodeCPP
{
	ListNodeCPP* next;
	int val;
};

struct Stack
{
	//成员函数
	void Init(int n = 4)
	{
		array = (int*)malloc(sizeof(int) * n);
		
		if (nullptr == array)
		{
			perror("malloc申请空间失败");
			return;
		}

		capacity = n;
		top = 0;
	}

	//成员变量
	int* array;
	size_t capacity;
	size_t top;
};

int main()
{
	struct Stack st1;
	st1.Init(100);

	Stack st2;
	st2.Init();

	LTNode node1;
	ListNodeCPP node2;

	return 0;
}

3. 类的访问限定符及封装

3.1 访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用

访问限定符

  1. public修饰的成员在类外可以直接被访问
  2. protected 和 private 修饰的成员在类外不能直接被访问(此处protected 和 private 是类似的,以后继承章节才能体现出它们的区别)
  3. class 的默认访问权限为 private,struct 为 public(因为 struct 要兼容C)
  4. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止;如果后面没有访问限定符,作用域就到 } 结束
  5. ⼀般成员变量都会被限制为private/protected需要给别人使用的成员函数会放为public

面试题:

C++中 struct 和 class 的区别是什么?

C++需要兼容C语言,所以C++中 struct 可以当成结构体使用;另外C++中 struct 还可以用来定义类,和 class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public ,class 定义的类默认访问权限是 private 的。(在继承和模板参数列表位置,struct 和 class 也有区别,后续再介绍)

3.2 封装

面向对象的三大特性:封装、继承、多态

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

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

封装本质上是一种管理,让用户更方便使用类。

4. 类的定义

#include <iostream>
#include <assert.h>

using namespace std;

class Stack
{
public:
	//成员函数
	void Init(int n = 4)
	{
		array = (int*)malloc(sizeof(int) * n);
		
		if (nullptr == array)
		{
			perror("malloc申请空间失败");
			return;
		}

		capacity = n;
		top = 0;
	}

	void Push(int x)
	{
		//扩容
		//...

		array[top++] = x;
	}

	int Top()
	{
		assert(top > 0);

		return array[top - 1];
	}

private:
	//成员变量
	int* array;
	size_t capacity;
	size_t top;
};

//封装

int main()
{
	class Stack st1;
	Stack st2;
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	st2.Push(4);

	//cout << st2.array[st2.top] << endl;//err
	cout << st2.Top() << endl;

	return 0;
}

5. 类域

类定义了一个新的作用域,类的所有成员都在类的作用域中在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到_a等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的_a等成员,就会到类域中去查找。

//Stack.h

class Stack
{
public:
	void Init();
	void Push(int x);

private:
	int* _a;
	int _top;
	int _capacity;
};

class Queue
{
public:
	void Init();
	void Push(int x);
};
//Stack.cpp

#include "Stack.h"

void Stack::Init()
{
	_a = nullptr;
	_top = 0;
	_capacity = 0;
}

void Stack::Push(int x)
{

}

void Queue::Push(int x)
{

}

//class Date
//{
//public:
//	void Init(int year, int month, int day)
//	{
//		//这样写编译能通过,但是没有初始化上,因为局部优先,局部没有才会到类域里去找
//		year = year;
//		month = month;
//		day = day;
//	}
//
//private:
//	int year;
//	int month;
//	int day;
//};

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;//year_  m_year
	int _month;
	int _day;
};

int main()
{
	Date d;
	d.Init(2024, 3, 31);

	return 0;
}

6. 类的实例化

  • 用类在物理内存中创建对象的过程,称为类实例化出对象。
  • 类是对对象进行一种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。

7. 类对象模型

7.1 类对象的存储方式猜测

方式一
方式二
方式三


类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?

首先,函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。

再分析一下,对象中是否有存储指针的必要呢,Stack实例化 st1 和 st2 两个对象,st1 和 st2 都有各自独立的成员变量存储各自的数据,但是 st1和 st2的成员函数指针却是一样的,存储在对象中就浪费了。如果用 Stack 实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。因此,函数指针是不需要存储的

函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。

7.2 如何计算类对象的大小

和C语言中的结构体一样,类对象的大小也符合内存对齐的规则

//Stack.h

class Stack
{
public:
	void Init();
	void Push(int x);

private:
	int* _a;
	int _top;
	int _capacity;
};

//Stack.cpp

#include "Stack.h"

void Stack::Init()
{
	_a = nullptr;
	_top = 0;
	_capacity = 0;
}
//Test.cpp

#include "Stack.h"
#include <iostream>

using namespace std;

int main()
{
	//类 -> 对象   1 -> 多
	//类的实例化
	Stack st1;
	Stack st2;

	st1.Init();
	st2.Init();

	//对象占用空间的大小,只考虑成员变量
	cout << sizeof(st1) << endl;//12
	cout << sizeof(Stack) << endl;//12

	return 0;
}

#include <iostream>

using namespace std;

//空类 -> 1
//这一个字节,不存储有效数据,标识对象被定义出来了
class A3
{

};

//1byte
class A2
{
public:
	void f2()
	{

	}
};

int main()
{
	cout << sizeof(A3) << endl;//1
	A3 aa1;
	A3 aa2;

	cout << sizeof(A2) << endl;//1
	
	return 0;
}

8. this指针

8.1 this指针的引出

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

那么这里就要看到C++给了一个隐含的this指针来解决这里的问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问;只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

//注释掉的部分就是编译器的处理
#include <iostream>

using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//void Print(Date* const this)
	void Print()
	{
		//this = nullptr;//err

		//cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;//年
	int _month;//月
	int _day;//日
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2024, 4, 2);
	d2.Init(2024, 4, 3);

	//d1.Print(&d1);
	d1.Print();

	//d2.Print(&d2);
	d2.Print();

	return 0;
}

8.2 this指针的特性

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

题1
首先,就算是空指针访问也不是编译错误Print()函数不在对象里面,也就是说不需要在对象里找函数的地址,这里p调用的意义是:第一,说明它是A类型的指针,就去A里面找这个成员函数(但是函数地址是在公共代码区里找);第二,传值给隐含的this(如果是对象,就把对象的地址传给this,如果是指针就把指针传给this)传一个空指针并不会报错,只有用空指针进行访问才会报错。因此,本题选C。


题2
函数里的 _a 实际上是 this->_a ,这里用空指针进行访问,所以会运行崩溃。因此,本题选B。


this指针是一个形参,所以它是存在栈上的(有些地方它是存在ecx寄存器上的,因为只要用到成员变量就要用到this指针,所以this指针会被频繁使用,而寄存器的特点就是很快,因此这相当于是一种优化)

9. C++和C语言实现Stack的对比

  • C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。
  • C++中函数名称有一些简化,因为它在类域里,不会有冲突;还有一些相对方便的语法,比如: Init 给的缺省参数会方便很多;成员函数不需要传对象地址,因为this指针隐含地传递了,方便了很多;使用类型不再需要typedef,用类名就很方便。
  • 在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大,等着我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力。

网站公告

今日签到

点亮在社区的每一天
去签到