【C++】带你一篇了解什么是OPP(面向对象编程),什么是封装?类和对象(上)

#代码星辉·七月创作之星挑战赛#

目录

1. 面向对象编程

2. 面向对象编程和面向过程编程

3. 类的引入

4. 类的定义

4.1 类的两种定义方式

4.1.1 类的声明和定义全部放在类体中

4.1.2 声明放在头文件(.h)中,定义放在(.cpp)文件中

5. 类的访问限定符和封装

5.1 访问限定符

5.2 封装

6. 类的作用域

7. 类的实例化

8. 类对象模型

8.1 如何计算类对象的大小


学习C语言的小伙伴都知道,C语言是面向过程的,分析问题找到解决步骤,通过函数调用逐步解决问题。

面向过程编程也叫结构化编程。虽然结构化编程的理念提高了程序的清晰度,可靠性,并且方便维护。但它再编写大型的程序时,仍然面临这巨大的挑战,OOP(面向对象编程)提供了一种新的方法。与强调算法的过程性编程不同的是,OOP强调的是数据

——引自《C++ Primer Plus(第六版)》


1. 面向对象编程

C++是面向对象的,关注的是对象,将一件事情分解成不同的对象,然后通过对象之间的交互来解决问题。

在C++中,类是一种规范,描述的是一种新型数据格式,对象是根据这个规范构造的数据结构。那么有小伙伴会问,什么是类?(这个我们在 3. 类的引入 中会详细讲解)


2. 面向对象编程和面向过程编程

举下面这个例子能够更形象展示OPP与面向过程性编程的区别:

此举例改变自《C++ Primer Plus(第六版)》:

      曼联足球俱乐部的一名新成员被要求记录球队的统计数据。很自然他会借助计算机来完成这项任务。

如果这个新成员是过程性程序员,可能会这样考虑:

       我要输入每名运动员的姓名,进球数,助攻数,登场数等其他重要的基本统计数据。之所以使用计算机,是为了简化统计工作,因此让他来计算某些数据。另外,我还希望程序能够显示这些结果。应该如何组织呢?我让main()调用一个函数来获取输入,调用另外一个函数来进行计算,然后调用第三个函数来显示结果。那么,获得下一场比赛的数据后,又改怎么做呢?当然不想从头开始,可以添加一个函数来更新统计数据。可能需要在main函数中添加一个菜单,选择是输入,计算,更新还是显示数据等。则如何表示这些数据呢。可以使用一个字符串来存储选手的姓名,用另外一个数组来存储每位球员的进球数,再用一个数组存储助攻数等等。这种方法太不灵活了。因此可以设计一个结构体来存储每位球员的所有信息,然后用这种结构组成的数组来表示整个球队。

       总之,采用过程性编程时,首先要考虑遵守的步骤,然后考虑如何表示这些数据。

如果换成一个OOP程序员,又将如何考虑呢?

       首先要考虑数据——不仅要考虑如何表示数据,还要考虑如何使用数据

       OOP程序员会想,我要跟踪的是什么?当然是球员。因此要有一个对象表示整个球员的各个方面(不仅仅是进球数或助攻数)。因此这将是基本数据单元——一个表示球员的姓名和统计数据的对象。我需要一些处理该对象的方法。首先需要一种将基本信息加入到该单元中的方法;其次,计算机应计算一些东西,如进球率。因此要添加一些执行计算的方法。程序应自动完成这些计算,而无需用户的干扰。另外,还需要一些更新和显示信息的方法。所以,用户与数据交互的方式有三种:初始化,更新和报告——这就是用户接口。

       总之,采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述之后,需要确定如何实现接口和数据存储。最后,使用寻得设计方案创建出程序。


3. 类的引入

在面向过程化编程中,我们通常使用结构体来描述对象(以C语言为例)。在C语言中,结构体中只能定义变量,但是在C++中,结构体中也可以定义函数:

struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
	char _name[20];
	char _gender[3];
	int _age;
};
int main()
{
 Student s;
 s.SetStudentInfo("Peter", "男", 18);
 return 0; 
}

但是在C++中,我们更喜欢使用Class来代替。


4. 类的定义

class className
{
	// 类体:由成员函数和成员变量组成
 
}; // 一定要注意后面的分号
  1. class是定义一个类的关键字,classname是类名,{}中是类的主体,注意类的定义后有分号。

  2. 类中的元素称为类的成员:类中的数据又叫类的属性/类的成员变量、类的函数又叫做类的方法/类的成员函数。

4.1 类的两种定义方式

4.1.1 类的声明和定义全部放在类体中

class Student
{
public:
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
public:
	char _name[20];
	char _gender[3];
	int _age;
};

注意:

如果成员函数在类中定义, 编译器有可能会将其当成内联函数处理。

知识点补充:

类中的成员函数都有内联属性 。


4.1.2 声明放在头文件(.h)中,定义放在(.cpp)文件中

//student.h
//学生
class Student 
{
public:
	void SetStudentInfo(const char* name, const char* gender, int age);
	void PrintStudentInfo();
public:
	char _name[20];
	char _gender[3];
	int _age;
};
 
//test.cpp
#include "student.h"
void Student::SetStudentInfo(const char* name, const char* gender, int age)
{
	strcpy(_name, name);
	strcpy(_gender, gender);
	_age = age;
}
 
void Student::PrintStudentInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}


5. 类的访问限定符和封装

5.1 访问限定符

刚才有细心的小伙伴已经发现了刚才那段代码中出现了public,这个关键字是什么呢?这个就是我么要接下来要说的访问限定符,C++中除了public(公有)外,还有private(私有)和protected(保护)限定符。

那么C++中引入访问限定符的意义是什么?这个就要补充一下实现类的封装的方式:

用类将对象的属性和方法结合在一起,使得对象的信息更加完整,通过访问限定符将接口提供给外部用户使用

这些访问限定符有什么含义呢?

访问限定符说明:

  • public修饰的成员,在类外也可以访问
  • private和protected修饰的成员,在类外不能访问(在对于类内和类外而言private和protected没有区别,但是针对后面要讲的子类是有区别的

5.2 封装

封装:将类的属性和方法进行有机结合,隐藏对象的属性和实现细节,仅仅对外公开接口与对象进行交互。所以说封装的本质是一种管理机制:我们将类的数据和方法都封装到一起,不想让别人看到,也就是使用private/protected把成员封装起来,开放一些公有的成员函数对成员的合理访问。

下面举一个比较直观地例子来进行说明:

//C语言中数据和方法是分离的
struct Stack
{
	int* _a;
	int _top;
	int _capacity;
};
 
void StackInit(struct Stack* ps)
{
	assert(ps);
	ps->_a = NULL;
	ps->_capacity = 0;
	ps->_top = 0;
}
void StackPush(struct Stack* ps, int x)
{
 
}
struct Stack StackTop(struct Stack* ps)
{
	
}
 
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]);     //可能就存在误用
	printf("%d\n", st._a[st._top - 1]); //可能就存在误用
}

上面的代码是我们学习数据结构时,使用C语言实现的一个栈。如果我们想访问栈顶元素,通常情况下,我们只需要调用已经写好的函数StackTop()即可。但是因为C语言中数据和方法是分离的,所以我也可以通过访问数组下标拿到栈顶元素,但是如果调用者并不清楚程序员的定义方式,就有可能造成误用。例如,这个代码中我们定义,_top是栈顶元素下一个元素的下标,因此栈顶元素的下标就是_top-1,但是调用者不清楚就可能导致误用成_top

为了解决这一个问题,我们将函数定义在类中,然后同时将成员变量一起封装在类中,这样外界就无法调用。此时我们如果想访问栈顶元素,就只能通过调用公用函数的接口。也就避免了上述问题的发生。

class Stack
{
private:
	void Checkcapacity()
	{
	}
public:
	void Init()
	{
	}
	void Push(int x)
	{
	}
	void Top()
	{
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

6. 类的作用域

类的定义就意味着创建了一个新的作用域,类的所有成员都在这个类作用域中。此时,如果要在类的作用域外定义一个成员,就需要使用::作用域解析符。

下面举一个例子:

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

using namespace std;
void Student::SetStudentInfo(const char* name, const char* gender, int age)
{
	strcpy(_name, name);
	strcpy(_gender, gender);
	_age = age;
}

void Student::PrintStudentInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

7. 类的实例化

使用类类型来创建对象,称为类的实例化:

  • 类只是一个模型,限定了类中有哪些成员,但是定义了一个类,并没有分配实际的内存空间来储存成员变量

  • 一个类可以实例化多个对象,实例化对象占用物理空间,储存成员变量

  • 打一个比方,类实例化对象,就像使用设计图建造房子一样。类就是设计图,只是设计建筑需要什么东西,但是并没有实体建筑。类也只是一个设计,在实例化之前并没有实际的储存数据,占用物理空间。

继续使用前面讲到的栈举一个例子:

class Stack
{
//成员函数
private:
	void Checkcapacity()
	{
	}
public:
	void Init()
	{
	}
	void Push(int x)
	{
	}
	void Top()
	{
	}

//成员变量
private:
	int* _a;
	int _top;
	int _capacity;
};
 
int main()
{
	Stack st;    //类的实例化,st就是一个对象
	st.Init();
	st.Push(1);
	st.Top();
	return 0;
}

8. 类对象模型

8.1 如何计算类对象的大小

我们在学习C语言的时候知道,结构体中只定义变量,因此我们是可以计算得到结构体的大小的,只要使用sizeof()即可:

注意:这里的结果可能是12,因为不同位数操作系统int*的占用字节数不同。 

但是我们刚才讲过了,C++中结构体中不仅可以定义变量,也可以定义函数,那么此时结构体的大小又应该怎么算呢?

我们发现似乎不可思议的事情,因为求得这样定义的Stack类的大小居然同样也是16!!!

我们似乎可以做一个大胆但又很合理的猜测:类对象的储存方式是,只储存成员变量,成员函数存放在公共的代码段。

那么为什么要这样存放呢?

在上述中说到,类就像是一份建筑图纸,而所建造的每一个房子中的name,capacity,top应当是不一样的。但是所调用的方法Init(),Top()应当是同一个方法。因此没有必要把函数在对象中存一份。我们也可以通过汇编看看不同的对象是否调用同一个函数。

int main()
{
	Stack st1;    //类的实例化,st就是一个对象
	st1.Init();
	
	Stack st2;
	st2.Init();

	return 0;
}

我们这里实例化两个对象,并且分别了调用两个对象的初始化函数,然后通过汇编代码看一下两个函数调用的地址:

PS:这里如果看不到两个call的小伙伴可以去看我的另一篇文章C++内联函数,然后将按照其中编译器设置的第三步骤,优化inline函数扩展改为默认值即可。

我们能够发现st1和st2所调用得Init()函数是同一份。因此如果都把函数存在类中,就会造成浪费。因此我们可以把函数放在一个公共的区域,这个区域叫做代码段。

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。注意:最小内存单元是1.操作系统规定都要有地址记录,就像sizeof(void) = 1。


(本篇完) 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值