【C++】类和对象(一)

文章目录

一、类的引入

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

🌰C语言方式实现的栈,结构体中只能定义变量。现在以C++的方式实现,结构体中也可以定义函数。

cpp 复制代码
typedef int DataType;
struct Stack
{
	void Init(size_t capacity)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(const DataType& data)
	{
		// 扩容
		_array[_size] = data;
		++_size;
	}
	DataType Top()
	{
		return _array[_size - 1];
	}
	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
	DataType* _array;
	size_t _capacity;
	size_t _size;
};
int main()
{
    //我们可以直接用stack来定义一个结构体s。
    //C++中s也可以称作一个对象。
	Stack s;
	s.Init(10);
	s.Push(1);
	s.Push(2);
	s.Push(3);
	cout << s.Top() << endl;
	s.Destroy();
	return 0;
}

c语言中,我们通过这种方式来建立一个栈的结构,c++将结构体升级为了类,类提供了一种封装数据成员(属性或变量)和成员函数 上面结构体的定义,在C++中更喜欢用class来代替

二、类的定义

语法

cpp 复制代码
class className
{
 // 类体:由成员函数和成员变量组成
};  
// 一定要注意后面的分号

class 为定义类的关键字 ,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略

类体中内容称为类的成员 :类中的变量称为类的属性成员变量 ; 类中的函数称为类的方法 或者成员函数

我们可以简单定义一个日期类:

cpp 复制代码
class Date
{
	int year;
	int month;
	int day;
};

C++习惯定义成员变量时在其前面加上符号_,比如以下情况:

cpp 复制代码
class Date 
{
    void Init(int year, int month, int day)
	{
		year = year;
	}
	int year;
	int month;
	int day;
};

为了防止成员变量和成员函数的形参混淆,我们可以这样定义:

cpp 复制代码
class Date 
{
    void Init(int year, int month, int day)
	{
        _year = year;
	}
	int _year;
	int _month;
	int _day;
};

现在我们来调用这个函数:

cpp 复制代码
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;
}

发现无法调用该成员函数

这就引出我们接下来要讲的内容,类的访问限定符

三、类的访问限定符及封装

访问限定符

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

在 C++ 中,访问限定符(Access Specifiers)定义了类成员(即变量和函数)的访问权限。这些限定符有助于实现封装和抽象,是面向对象编程的核心概念之一。主要的访问限定符有三种:publicprivateprotected

public

  • public成员在任何地方都可以被访问,类的内部、外部都可访问>
  • 使用public限定符可以提高代码的使用率,但是会降低封装性,因为外部代码可以直接读写公开的数据成员

private

  • private成员只能被类的成员函数和友元函数访问,不能被类的外部访问
  • 使用 private 限定符是封装的一种体现。通过限制对成员的直接访问,你可以保护类内部成员的状态,避免外部代码随意修改,从而维护对象的完整性

protected

  • protected 成员可以被其所在类 的成员函数、派生类 的成员函数访问,但是**不能被类的外部直接访问**
  • protected 在处理继承时尤其有用,它允许派生类 访问基类中的成员,同时防止了类的外部直接访问这些成员

【访问限定符说明】

  1. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  2. 如果后面没有访问限定符,作用域就到}即类结束。
  3. class的默认访问权限为privatestruct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

面试题

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

答:C++兼容C语言,C++中struct可以当作结构体使用,也可以当作类去使用。在struct当类使用的时候,跟class关键字的区别是默认访问限定符不同。struct的默认访问限定符是public,class的默认访问限定符是private。在继承和模板参数列表位置,struct和class也存在区别。

四、类的两种定义方式

  1. 声明和定义全部放在类体中,需注意:成员函数在类中定义 ,编译器可能 会将其当成内联函数处理
cpp 复制代码
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;
	}
};
  1. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

stack.h文件

cpp 复制代码
class stack
{
public:
	int* a;
	int size;
	int capacity;
	void Init();
};

stack.cpp文件,如果我们直接定义该成员函数则会报错


报错原因是:Init()函数被编译器当作全局函数,Init()函数操作Stack类的成员变量被编译器认定是访问未定义的标识符。

这说明,如果程序员不加指定,编译器则无法找到该函数的出处,根据搜索规则,只会先去局部域找然后去全局域搜索这些变量是否被定义过。所以我们要使用访问限定符::告诉编译器该函数操作的成员变量 属于 哪一个类域

cpp 复制代码
void stack::Init(int n=4)
	{
		a = (int*)malloc(sizeof(int) * n);
		if (nullptr == a)
		{
			perror("malloc申请空间失败");
			return;
		}
		capacity = n;
		size = 0;
	}

四、类的作用域

由此我们可知,除了全局域、局部域类作用域也会影响编译器的搜索规则 ,比如下面两个类,栈和队列都有init和push函数:

cpp 复制代码
class stack {
public:
	void init(int n);
	void push(int x);
};
class queue {
public:
	void init(int n);
	void push(int x);
};

如果没有域,则会有冲突。所以,类本身就是一个域

五、面向对象三大特性之一:封装

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

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

封装(Encapsulation)是面向对象编程(OOP)中的一个核心概念,它指的是将对象的数据(属性)和操作这些数据的方法(行为)捆绑在一起的做法。这种机制可以防止外部代码随意访问对象内部的状态,从而保护对象的完整性和安全性。封装的关键在于提供了一个抽象层,使得对象的使用者不需要知道对象内部的复杂逻辑,只需要通过对象提供的接口(公共方法)来与对象进行交互
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。

五、类的实例化

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

思考 :这里的int _yearint _monthint _day是声明还是定义?它是声明

声明和定义的本质区别是是否开辟一块空间

cpp 复制代码
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
	}
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

Date d1通过实例化对象,为该对象的所有成员变量开辟空间,整体定义了该对象的所有成员变量

我们可以通过对象,对该对象的成员变量进行访问。

cpp 复制代码
int main()
{
    Date d1;
    d1._year++;
    //Date::_year++//不能通过类域限定访问,因为他只是声明,没有开辟一块空间
    return 0;
}

类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它

类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

六、类对象模型

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

6.1如何计算类对象的大小

cpp 复制代码
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
	}
	int _year;
	int _month;
	int _day;
};
int main()
{
    cout << sizeof(d1) << endl;
	Date d1;
	return 0;
}

思考:上面Date类的大小是多少字节?

先来回顾以下结构体内存对齐规则

回顾:结构体内存对齐规则

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

  2. 其他成员变量要对齐到对齐数的整数倍的地址处。

    对齐数 = min(编译器默认的对齐数, 该成员大小)

    VS中默认的值为8

  3. 结构体总大小最大对齐数(每个成员变量都有一个对齐数)的整数倍

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

这里存在成员函数的情况下,类所占字节大小依然是12,验证测试如下:

我们尝试调用以下不同对象的成员函数

cpp 复制代码
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
	}
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;
	d1.Init(2024, 01, 01);
	d2.Init(2025, 01, 01);
	return 0;
}

从反汇编代码中,我们发现两个对象分别调用的成员函数地址是相同的。

所以类的成员变量、成员函数到底是如何存储的呢?

6.2类对象的存储方式

每个对象中存储各自的成员变量 ,成员函数因为要反复被不同的对象调用,所以成员函数存储在一段公共的区域

cpp 复制代码
class A2 {
public:
   void f2() {}
};

这个类的大小是多少呢?

在C++中,没有成员变量的类对象大小占位1字节,不实际存储任何数据,是为了标识该对象已经被实例化了。

对于给出的A2类,因为它只含有一个成员函数,它所占用的内存大小应该是1字节

再看下面的代码示例:将p设为空指针,p还能调用函数吗?

cpp 复制代码
class fun {
public:
	void Print()
	{
		cout << "被调用" << endl;
	}
};
int main()
{
	fun a1;
	fun* p1 = &a1;
	p1->Print();
	fun* p = nullptr;
	p->Print();
	return 0;
}

测试:

可以调用的原因:

这里的Print函数并不在指针指向的空间里面,既不在p1指向的空间里面,也不再p指向的空间里面,p1,p指向的是对象,对象里面没有函数的地址,虽然有箭头,但是并没有进行解引用。

相关推荐
霁月风19 分钟前
设计模式——适配器模式
c++·适配器模式
sp_fyf_202420 分钟前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
香菜大丸41 分钟前
链表的归并排序
数据结构·算法·链表
jrrz082841 分钟前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time1 小时前
golang学习2
算法
咖啡里的茶i1 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1071 小时前
Webserver(4.9)本地套接字的通信
c++
@小博的博客1 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
南宫生2 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
爱吃喵的鲤鱼2 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++