C++类和对象(上)
面向对象和面向过程
面向对象(Object Oriented, OO
)和面向过程(Procedure Oriented, PO
)是两种不同的编程范式,它们在解决问题的方式、关注点、特性以及适用场景等方面存在显著差异。
核心关注点
- 面向对象:以对象为核心,强调事件的角色和主体。它通过将现实世界或问题域中的事物抽象为对象,通过对象之间的交互来解决问题。面向对象关注问题的本质,即“谁来做这件事”。
- 面向过程:以过程为核心,强调事件的流程和顺序。它将问题分解成一系列步骤,并通过函数或过程来实现这些步骤。面向过程关注问题的解决步骤,即“怎么做这件事”。
编程思路
- 面向对象:将构成问题的事物分解成若干个对象,每个对象都封装了数据(属性)和操作这些数据的方法(行为)。对象之间通过消息传递进行交互,以完成特定的任务。面向对象适合解决复杂的问题,需要多方的协作和抽象。
- 面向过程:直接将问题分解成一系列详细的步骤,并通过函数或过程来实现这些步骤。然后,按照特定的顺序调用这些函数或过程来解决问题。面向过程适合解决简单的问题,不需要过多的协作和抽象。
特性与优势
- 面向对象
- 封装:将数据和操作数据的方法绑定在一起,隐藏对象的内部实现细节,只对外提供接口。这有助于提高程序的安全性和可维护性。
- 继承:子类可以自动共享父类的数据结构和方法,实现代码的复用和扩展。
- 多态:允许不同子类型的对象对同一消息作出不同的响应,提高了程序的灵活性和可扩展性。
- 高复用性、高扩展性:由于具有封装、继承和多态等特性,面向对象编程更容易实现代码的复用和扩展。
- 易于维护:由于对象之间的耦合度较低,且具有良好的封装性,因此面向对象程序更易于维护和修改。
- 面向过程
- 流程清晰:面向过程编程的流程化使得编程任务明确,具体步骤清晰,便于节点分析和调试。
- 高效:对于不复杂的事件,面向过程编程的执行效率通常较高。
- 代码复用性低:由于缺少封装、继承和多态等特性,面向过程编程的代码复用性相对较低。
- 扩展性差:随着问题的复杂化,面向过程程序的扩展性和可维护性可能会受到影响。
适用场景
- 面向对象:适用于解决复杂的问题,特别是需要多方协作和抽象的问题。例如,大型软件系统的开发、游戏开发等。
- 面向过程:适用于解决简单的问题,特别是那些不需要过多协作和抽象的问题。例如,小型工具软件的开发、脚本编写等。
例子
我们举一个外面点餐的程序,对于面向过程来说主要考虑的是点餐下单,接单分配骑手,骑手送餐等过程。
而对于运用面向对象的思想来说这个程序主要考虑的就是用户,商家,骑手三者之间的关系了。
类的引入
C++兼容C语言,我们都知道在C语言中存在结构体,结构体的用法可以继续使用,同时struct
结构体,已升级成了类。在C++中struct
的不仅可以定义成员变量也可以定义成员函数。并且对于类名就是类型了。如下面的stack的结构体,在cpp
中的部分实现。
using namespace std;
struct Stack
{
//成员函数
void Init()
{
a = nullptr;
top = 0;
capacity = 0;
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
s1.Init();
return 0;
}
对于C语言中的结构体实现栈结构
- 函数不能放在
struct
结构体内- 类型必须是
struct
+ 结构体名
类的定义
class ClassName
{
//类体:由成员函数和成员变量组成
};
class为定义类的关键字,ClassName
为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
其中类的定义分为两种方式
类的定义和声明全部放一个类体中
using namespace std;
class Stack
{
public:
//成员函数
void Init()
{
a = nullptr;
top = 0;
capacity = 0;
}
void Push(int x)
{
if(top == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
a = (int*)realloc(a, sizeof(int) * newcapacity);
capacity = newcapacity;
}
}
int Top()
{
return a[top - 1];
}
private:
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
return 0;
}
注意:成员函数如果在类中定义,编译器可能会将其当成内联函数进行处理。
类的声明和定义分离
类的声明和定义是可以分离的,对于类的声明可以放在.h
文件中,对于类的定义可以放在.cpp
文件中
.h文件
class Stack
{
public:
//成员函数
void Init();
void Push(int x);
int Top();
private:
//成员变量
int* a;
int top;
int capacity;
};
.cpp
文件
void Stack::Init()
{
a = nullptr;
top = 0;
capacity = 0;
}
void Stack::Push(int x)
{
if (top == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
a = (int*)realloc(a, sizeof(int) * newcapacity);
capacity = newcapacity;
}
}
int Stack::Top()
{
return a[top - 1];
}
在类的定义和声明分离的情况下,在函数定义的前面一定要加上类域的限制既类名::
。
类的访问限定符
在C++中类的访问限定符有三种一个是public
(公有),protected
(受保护的),private
(私有的)
public
:
- 公有成员在类的外部是可访问的。也就是说,公有成员可以通过任何类的对象来访问。
- 公有成员通常用于定义类的接口,即类向外界提供的服务。
protected
- 保护成员在类的内部(包括派生类)是可访问的,但在类的外部不可直接访问。
- 保护成员主要用于实现类的继承时,子类需要访问父类的某些成员,但这些成员又不应该被类的外部访问。
private
:
- 私有成员仅能在类的内部被访问,即只能在类的成员函数或友元函数中被访问。
- 私有成员通常用于封装类的内部实现细节,隐藏类的数据,只通过公有的成员函数来访问或修改这些数据。
其中访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}
结束。
class
和struct
的默认访问限定符是不同的,class
默认是私有的,struct
默认是公有的。我们可以通过下面代码证明。
使用class
using namespace std;
class Stack
{
//成员函数
void Init()
{
a = nullptr;
top = 0;
capacity = 0;
}
void Push(int x)
{
if(top == capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
a = (int*)realloc(a, sizeof(int) * newcapacity);
capacity = newcapacity;
}
}
int Top()
{
return a[top - 1];
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.top = s1.top--;A
return 0;
}
会出现下面的报错
然而对于struct
却能够正常运行。
类的实例化
类的实例化是面向对象编程中的一个基本概念,它指的是创建一个类的实例(或对象)的过程。在面向对象编程中,类(Class)是一种用户定义的类型,它描述了具有相同属性和方法(也称为成员函数)的一组对象。通过实例化类,我们可以创建出该类的对象,这些对象被称为类的实例。
每个实例都是类的一个具体实现,它拥有类定义的所有属性和方法,但可以有自己的属性值(即状态)。这些属性值在实例化时可以被初始化,也可以在实例的生命周期内被修改。
我们以下面的日期类为例
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
其中在类中成员变量只是一种声明,这些变量没有开空间,不能够通其中还过Date.
直接访问。Date d
就是实例化会开空间就能够访问了。其中Date类是没有空间的,只有Date类实例化出的对象才有具体的空间。
下面我们通过一个例子更加深入的理解类的实例化。
类就像是一张设计图纸,而类实例化出的对象则是根据这张图纸搭建出的实体的建筑,实际搭建出的建筑是占据物理空间的。
类对象模型
类对象的大小
当我创建了一个对象的时候,我们可以通过sizeof
函数计算创建的对象的大小。
class Date
{
public:
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;
cout << sizeof(d1) << endl;
return 0;
}
我们发现输出的结果为12,正好为三个成员变量的大小,并没有计算函数的大小。我们可以理解不同的对象都有自己的成员变量,但是所执行的函数都是一样的,所以函数并不是在每个对象中存储一份,而是存放在一个公共代码区中 。
类的存储方式
成员变量的存储
- 成员变量存储位置
- 类的成员变量存储了对象的属性信息。每个对象都有自己的成员变量副本,这些变量按照声明的顺序在内存中连续存储。
- 非静态成员变量存储在对象的内存中,属于对象的一部分。
- 静态成员变量则不同,它们不属于任何对象实例,而是属于类本身。静态成员变量在全局数据区(静态区)中存储,它们在程序开始执行时分配存储单元,并在程序执行完毕后释放。
- 内存对齐
- 成员变量在内存中的存储还受到内存对齐规则的影响。内存对齐是为了提高内存访问效率而设计的,编译器会在成员变量之间添加填充字节以确保每个成员变量的起始地址符合特定的对齐要求。
成员函数的存储
- 存储位置
- 成员函数(包括普通成员函数和静态成员函数)在内存中的存储方式与成员变量不同。成员函数通常存储在代码段(Code Segment)中,而不是与对象实例一起存储在数据段中。
- 这意味着所有对象实例共享同一份成员函数的代码,而不是每个对象都复制一份。
- this指针
- 当成员函数被调用时,编译器会自动传递一个指向调用对象的指针(即
this
指针)给函数。这使得成员函数能够访问和修改对象的成员变量。
- 当成员函数被调用时,编译器会自动传递一个指向调用对象的指针(即
我们再看下面这段代码
class A
{};
class B
{
void f1() {}
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
我们发现A类中无成员函数和成员变量,B中存在成员函数但是成员函数不是在每个对象中存储一份,而是存放在一个公共代码区当中的。但是输出的结果都为1。C++规定对于没有成员变量的类分配一个字节进行占位,不存储数据,只是表示对象存在过。
this指针
this指针引出
下面我们以日期类为例
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
不能直接访问类里面的年月日,因为类里面的年月日只是声明,并没有为此开空间,而是在函数参数中第一个位置存在一个类的指针,称为this指针。当我们实例化一个对象的时候,例如实例化d1
对象后,调用函数时this指针就会被赋值为d1
,此时的年月日就是实例化对象中的年月日了。
class Date
{
public:
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout <<this-> _year << " " <<this-> _month << " " << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
C++规定this在实参和形参的位置不能显示写,但是在类里面可以显示的使用。
this指针的特性
- this指针是一个形参,一般存在栈帧里面,VS编译器下面一般会用
ecx
寄存器直接传递- this指针的类型为 类类型
* const
,既成员函数中,不能给this指针赋值。- this指针只能在成员函数的内部使用
- this指针本质上是“成员函数”的形参当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
例子
Dem01
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上述代码正常运行原因是因为
上述代码中p为空指针但是p是一个对象,该对象调用了成员函数,但是成员函数并不是属于某个对象,而是存在于公共代码区,这时并没有发生解引用所以是可以正常运行的。
同理下面也是
Demo2
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
(*p).Print();
return 0;
}
上述代码正常运行原因是因为
上述代码中p为空指针但是p是一个对象,该对象调用了成员函数,但是成员函数并不是属于某个对象,而是存在于公共代码区,这时并没有发生解引用所以是可以正常运行的。
本专栏为“小菜C++学习之路
该文章仅供学习参考,如有问题,欢迎在评论区指出。