1.面向过程与面向对象初步了解
C语言是一个典型的面向过程编程语言,面向过程编程侧重于编写一系列的步骤或函数来执行特定的任务。它把问题分解成一步步的过程来解决
在面向过程的代码中,数据和函数是分开的。数据被定义在一处,而处理这些数据的函数则定义在另一处
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成,面向对象编程强调的是将数据及与数据相关的操作封装成对象。每个对象都可以接收消息、处理数据和发送消息给其他对象。
结构: 在面向对象的代码中,数据(即属性)和操作这些数据的代码(即方法)被捆绑在一起形成对象,这种方式强调数据的封装
面向过程是关注于将程序分解成一系列的步骤和过程;面向对象则是将程序视为一组互相协作的对象。
在面向过程中,数据和操作数据的函数是分开的;而在面向对象编程中,数据和操作数据的方法是封装在一起的
我们不妨举些例子:
通过比较生活中的例子,我们可以更容易地理解面向过程编程和面向对象编程的差异。
面向过程编程的例子:制作蛋糕
想象你在按照食谱制作蛋糕,这个过程可以分解成一系列步骤:
收集食材:鸡蛋、面粉、糖、牛奶等
按照步骤处理食材:打蛋、搅拌、烘烤等
最后组合所有处理过的食材,得到一个蛋糕
在面向过程编程中,我们关注的是如何一步一步地执行这些任务,每个步骤都是一个过程或函数,我们按照顺序调用它们,最终得到想要的结果。这里的食材和步骤是分开的,首先我们定义了食材(数据),然后通过一系列的函数(步骤)来处理这些食材
面向对象编程的例子:智能手机
现在想象你在使用一部智能手机。这个手机可以看作是一个对象,它有:
属性(数据):屏幕尺寸、存储空间、品牌名称等。
行为(方法):打电话、发短信、拍照(等。
在面向对象编程中,我们不再关注步骤的具体实现细节,而是将数据和与数据相关的行为(方法)封装在一起形成一个对象。每个对象都有自己的属性和方法,比如智能手机对象拥有拍照的方法。当我们想拍一张照片时,只需要调用这个对象的拍照方法,而不需要知道内部拍照的具体步骤
通过上述比较可以看出:
面向过程类似于逐步执行一项任务的具体步骤,每一步操作数据,彼此之间相互独立
面向对象则更像是与一个能各自独立处理特定事务的“小专家”交流。你不需要知道他们是如何内部操作的,只需要与他们交互即可
2.类
2.1类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数
struct stack { int* a; int size; int capacity; }; void Init(struct stack*s) { …… …… } void destroy(struct stack*s) { ……… ……… } int main() { struct stack st1; return 0; }
c语言中,我们通过这种方式来建立一个栈的结构,c++将结构体升级为了类,类提供了一种封装数据成员(属性或变量)和成员函数(方法)的途径
我们可以直接用stack来定义一个结构体st2:
struct stack st1; stack st2;
并且可以把我们需要的函数定义在结构体中:
1. struct stack 2. { 3. int* a; 4. int size; 5. int capacity; 6. void Init(int n=4) 7. { 8. a = (int*)malloc(sizeof(int) * n); 9. if (nullptr == a) 10. { 11. perror("malloc申请空间失败"); 12. return; 13. } 14. capacity = n; 15. size = 0; 16. } 17. void destroy() 18. { 19. 20. } 21. void push(int x) 22. { 23. //...扩容 24. a[size++] = x; 25. } 26. 27. }; 28. int main() 29. { 30. stack st2; 31. st2.Init(); 32. st2.push(1); 33. st2.push(2); 34. st2.push(3); 35. return 0; 36. }
st2就是一个对象
2.2类的定义
1. class className 2. { 3. // 类体:由成员函数和成员变量组成 4. };
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数、
我们可以简单定义一个日期类:
1. class Date 2. { 3. int year; 4. int month; 5. int day; 6. };
c++习惯定义成员变量时在其前面加上符号_,比如以下情况:
class Date { void Init(int year, int month, int day) { year = year; } int year; int month; int day; };
为了防止混淆,我们可以这样定义:
class Date { void Init(int year, int month, int day) { _year = year; } int _year; int _month; int _day; };
后面我们也会学到用this来区分
现在我们来调用这个函数:
class Date { void Init(int year, int month, int day) { _year = year; } int _year; int _month; int _day; }; int main()
{ Date d1; d1.Init(2005, 6, 23); return 0; }
会发现有问题:
无法访问 private 成员(在“Date”类中声明)
这和我们接下来讲解的内容有关
3.类的访问限定符及封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
在 C++ 中,访问限定符(Access Specifiers)定义了类成员(即变量和函数)的访问权限。这些限定符有助于实现封装和抽象,是面向对象编程的核心概念之一。主要的访问限定符有三种:public、private 和 protected
3.1 public
public 成员在任何地方都可以被访问,不管是类的内部还是外部。
使用 public 限定符可以增加代码的可用性,但同时可能降低封装性,因为外部代码可以直接读写公开的数据成员
示例:
class MyClass { public: int publicData; // 可以从类的外部直接访问 };
3.2 private
private 成员只能被类的成员函数和友元函数访问,不能被类的外部访问。
使用 private 限定符是封装的一种体现。通过限制对成员的直接访问,你可以保护类的内部状态,避免外部代码随意修改,从而维护对象的完整性
示例:
class MyClass { private: int privateData; // 只可以被类本身的成员函数访问 };
3.3 protected
protected 成员可以被其所在类的成员函数、派生类的成员函数访问,但是不能被类的外部直接访问
protected 在处理继承时尤其有用,它允许派生类访问基类中的成员,同时防止了类的外部直接访问这些成员
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
如果后面没有访问限定符,作用域就到 } 即类结束。
class的默认访问权限为private,struct为public(因为struct要兼容C)
所以为了解决上述问题,我们需要按照下面的方式定义:
class Date { public: void Init(int year, int month, int day) { _year = year; } int _year; int _month; int _day; };
class默认为private,所以为了使在类外直接访问,我们需要public修饰
4.类的两种定义方式
在C++中定义类时,主要有两种方式:在单个文件中定义整个类(包括成员变量和成员函数),或者将类的声明和定义分别放在不同的文件中(声明定义分离)。下面将详细讨论这两种方式
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
class stack { public: int* a; int size; int capacity; void Init(int n=4) { a = (int*)malloc(sizeof(int) * n); if (nullptr == a) { perror("malloc申请空间失败"); return; } capacity = n; size = 0; } };
类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
.h文件:
class stack { public: int* a; int size; int capacity; void Init(); };
.cpp文件,如果我们直接进行定义,则会报错
报错是因为找不到Init函数的出处,编译器搜索时默认在局部和全局搜索
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域
我们需要这样定义:
void stack::Init(int n=4) { a = (int*)malloc(sizeof(int) * n); if (nullptr == a) { perror("malloc申请空间失败"); return; } capacity = n; size = 0; }
类的作用域也是影响了搜索规则,比如下面两个类,栈和队列都有init和push函数:
class stack { public: void init(int n); void push(int x); }; class queue { public: void init(int n); void push(int x); };
如果没有域,则会有冲突
所以,类本身就是一个域
5.面向对象三大特性之一:封装
面向对象的三大特性:封装、继承、多态
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装(Encapsulation)是面向对象编程(OOP)中的一个核心概念,它指的是将对象的数据(属性)和操作这些数据的方法(行为)捆绑在一起的做法。这种机制可以防止外部代码随意访问对象内部的状态,从而保护对象的完整性和安全性。封装的关键在于提供了一个抽象层,使得对象的使用者不需要知道对象内部的复杂逻辑,只需要通过对象提供的接口(公共方法)来与对象进行交互
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
封装的优势
安全性:通过隐藏其内部状态,对象不会因为外部代码的直接访问而处于不一致的状态
简化接口:对象提供的公共方法可以是简单的接口,使用者无需了解实现细节即可使用对象
降低复杂性:将数据和操作数据的代码封装在一起有助于减少系统的复杂性
可维护性和可扩展性:封装使得修改和增加功能变得简单,因为修改的影响局限于单个对象内部,不会波及整个系统
重用性:通过封装,可以使对象更加通用,易于在不同场景下复用
6.类的实例化
class Date { public: void Init(int year, int month, int day) { _year = year; } int _year; int _month; int _day; };
我们思考一下,这里的int year;``int month;``int day;是声明还是定义呢?
定义和声明的本质区别是是否开辟一块空间
1. class Date 2. { 3. public: 4. void Init(int year, int month, int day) 5. { 6. _year = year; 7. } 8. int _year; 9. int _month; 10. int _day; 11. }; 12. int main() 13. { 14. Date d1; 15. 16. return 0; 17. }
Date d1;这一步成员变量为对象d1的一部分,这才是定义
我们并不能直接Date::_year;进行访问,因为int year;``int month;``int day;只是声明,并没有空间
我们可以用d1对变量进行访问
而这一步: Date d1;就叫做类的实例化
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
7.类对象模型
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
class Date { public: void Init(int year, int month, int day) { _year = year; } int _year; int _month; int _day; };
上面类的大小是多少个字节呢?
回顾一下结构体对其规则
结构体内存对齐规则
第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 VS中默认的对齐数为8
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
去掉函数,我们可以算出结果是12,但是这里有函数,结果还是12吗?进行测试:
我们发现结果还是12,这与类成员的存储结构有关
Date d1; Date d2; d1._year; d2._year;
这里d1,d2有各自的空间,那如果调用函数呢?
1. d1.Init(1,2,3); 2. d2.Init(1,2,3);
我们上篇内容提到,编译过程会call两个函数
这两个函数的地址是相同的
所以类的对象模型应该是什么样呢?
7.1类对象模型猜测
猜测1:对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
猜想2:只保存成员变量,成员函数存放在公共的代码段
每个成员的函数的地址都是一样的,在公共区域存放函数的代码更加的合理
1. class A2 { 2. public: 3. void f2() {} 4. };
这个类的大小是多少呢?
在C++中,类的大小是由其数据成员决定的。如果一个类没有数据成员,其大小通常不会是0,因为语言标准确保每个对象都必须有一个独一无二的地址,以便能够区分不同的对象。即使一个类没有任何数据成员,编译器也会给对象分配至少一个字节的大小,以保证对象有独立的地址
对于给出的A2类,因为它只含有一个成员函数f2而没有数据成员,它所占用的内存大小应该是1字节
再看下面这串代码:
class fun { public: void Print() { cout << "被调用" << endl; } }; int main() { fun a1; fun* p1 = &a1; p1->Print(); fun* p = nullptr; p->Print(); return 0; }
将p设为空指针,思考p还能调用函数吗?
测试:
为什么可以调用呢?
这里的Print函数并不在指针指向的空间里面,既不在p1指向的空间里面,也不再p2指向的空间里面,p1,p2指向的是对象,对象里面没有函数的地址,虽然有箭头,但是并没有进行解引用
后面我们还会深入了解类和对象有关知识,敬请期待!!!