C++_类的定义和使用

       

目录

1、类的引用

1.1 类的成员函数

1.2  类成员函数的声明和定义

2、类的定义

2.1 类的访问限定(封装)

3、类重名问题

4、类的实例化

4.1 类的大小

5、隐含的this指针

5.1 空指针问题

结语:


前言:

        C++的类跟c语言中的结构体在概念上是一样的,只不过在c语言中叫结构体,而在C++中叫类。c语言中的结构体内只能定义变量,而C++在此基础上升级后,类里面可以定义变量和函数,并且把类中的内容叫做类的成员,类中的变量叫做成员变量,类中的函数叫做成员函数。

1、类的引用

        在c语言中,结构体的类型是:struct+结构体名称,在没有使用typedef对其进行重命名时,是不能省略struct的。例子如下:

struct ListNode//定义一个结构体
{
	struct ListNode* Node;
	int data;
};

int main()
{
	struct ListNode lst;//定义一个结构体变量时,不能省略的掉struct 
	return 0;
}

        然而在C++中,就可以做到不使用typedef的情况下,省略struct并使用该结构体类型(这里暂且把类叫做结构体,因为用的还是struct定义出来的,方便理解)。例子如下:

struct ListNode//定义一个结构体,也可以理解成定义一个类
{
	ListNode* Node;//成员变量的类型也可以不加struct
	int data;
};

int main()
{
	ListNode lst;//定义结构体变量时,可以不加struct 
	return 0;
}

1.1 类的成员函数

        用c语言实现的栈或者链表,通常是把各个功能函数放在结构体的外面,用的是让函数与结构体成员分开的写法。而在C++中,可以把这些功能函数都放到结构体(类)中,让函数与结构体成员都处于同一作用域。

        类中函数的写法:

#include<iostream>
using namespace std;

struct Stack//结构体(类)
{
    //成员函数
    //栈的初始化
	void Init(int n=4)
	{
		arr = (int*)malloc(sizeof(int) * n);
		if (arr == nullptr)
		{
			perror("malloc");
			return;
		}
		Top = 0;
		capacity = n;
	}

    //压栈
	void push(int x)
	{
		if (capacity == Top)
		{
			int newcapacity = 2 * capacity;
			int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
			if (temp == nullptr)
			{
				perror("Push");
				return;
			}
			arr = temp;
			capacity = newcapacity;
		}
		arr[Top++] = x;
	}

    //成员变量
	int* arr;
	int Top;
	int capacity;
};

int main()
{
	Stack st1;//创建结构体变量
	st1.Init();//调用函数时要表明调用对象
	st1.push(1);
	st1.push(102);
	cout << st1.arr[st1.Top - 2] << endl;//打印栈里元素
	cout << st1.arr[st1.Top - 1] << endl;
	
	return 0;
}

        运行结果:

        可以看到上述代码中,栈的初始化和压栈函数都是直接写在结构体作用域中,而且能够正常实现栈的功能,说明C++支持把函数写进结构体内。并且注意调用函数时要表明调用对象,因为此时在类中的函数不再是全局范围的了,而是只属于当前类,因此调用成员函数时要先创建一个变量,并且用改变量去调用(写法和调用结构体成员一样)。

1.2  类成员函数的声明和定义

        我们一般实现某个功能函数时,都是把该函数的声明放在头文件内,把该函数的定义放在.cpp文件中,做到声明和定义分开,那么在C++中如何实现函数的声明和定义分离呢。

        比如把上述代码分成三个文件:3.cpp、3.h、test.cpp。3.cpp用于存放成员函数的定义,3.h是结构体的创建,test.cpp是主函数实现。

        3.h代码如下:

#pragma once

#include<iostream>
using namespace std;

struct Stack
{
	//成员函数声明
	void Init(int n = 4);//栈的初始化
	void push(int x);//压栈
	
	//成员变量
	int* arr;
	int Top;
	int capacity;
};

        3.cpp代码如下:

#include"3.h"

//成员函数定义
void Stack::Init(int n )//栈的初始化,注意添加作用域限定符
{
	arr = (int*)malloc(sizeof(int) * n);
	if (arr == nullptr)
	{
		perror("malloc");
		return;
	}
	Top = 0;
	capacity = n;
}

void Stack::push(int x)//压栈,注意添加作用域限定符
{
	if (capacity == Top)
	{
		int newcapacity = 2 * capacity;
		int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
		if (temp == nullptr)
		{
			perror("Push");
			return;
		}
		arr = temp;
		capacity = newcapacity;
	}
	arr[Top++] = x;
}

        test.cpp代码如下:


#include"3.h"

int main()
{
	Stack st1;//创建结构体变量
	st1.Init();
	st1.push(1);
	st1.push(102);
	cout << st1.arr[st1.Top - 2] << endl;//检查压栈是否成功
	cout << st1.arr[st1.Top - 1] << endl;
	
	return 0;
}

        可以从3.cpp文件中看到,声明成员函数的定义写法跟以前直接定义函数不一样,而是在成员函数名的前面加了作用域限定符’::’,表达的是该函数并不是全局的函数,而是只针对Stack结构体类型的函数。

        声明和定义分离后的运行结果:

        从结果可以看到,即使分成三个文件,只要使用作用域限制符依然是可以正常运行的。 

2、类的定义

        C++的类其实就是c语言中的结构体,只是类相比于结构体是做了升级的。因为C++兼容c语言,因此仍然可以采用struct来定义一个类,而且是支持类的相关功能的。但是在C++中基本都是用关键字class来定义一个类,比如创建一个栈的类,具体写法为:

class Stack//class后面跟类的名称
{
    //括号内存放成员变量和成员函数
};

int main()
{
    Stack st1;//st1在c语言中是变量,但是在C++中,更喜欢把st1叫做对象
    return 0;
}

2.1 类的访问限定(封装)

        既然了解了class的作用后,将上述代码的struct替换成class,真正的去使用C++的类,但是发现替换后编译器开始报错了:

        报错显示类中的成员都不可访问, 主要是因为C++的类相比于c语言的struct更加的安全,具体体现在类中的空间分为私有域和公有域,公有域是可以让类外随意访问的,而私有域拒绝让类外访问。在用class创建类时,如果没有明确对类进行公有域和私有域的划分,那么默认类里的所有域为私有域,这也是报错的原因。

访问限定符说明:

1、public修饰的成员是可以让类外进行访问。

2、protected和private修饰的成员不可让类外直接进行访问。

3、一个访问限定符的作用域范围是直到遇到下一个访问限定符或者遇到‘}’。

4、class的默认访问权限是private(这也是上述代码报错的原因),而struct默认访问权限是public(这也是为什么上述代码用struct就能够正常运行)。


        像上述把类分成两个区域的这一操作又称为封装,封装:隐藏对象的细节,对外只公开接口,让外部通过接口与对象达成交互。目的是为了更安全的使用代码,通常是把成员变量都放在私有域中,而成员函数放在公有域中,提高用户使用代码的安全性。 

        把struct替换成class并且优化后的代码如下:

#include<iostream>
using namespace std;

class Stack//结构体(类)
{
public:
	//成员函数
	//栈的初始化
	void Init(int n = 4)
	{
		arr = (int*)malloc(sizeof(int) * n);
		if (arr == nullptr)
		{
			perror("malloc");
			return;
		}
		Top = 0;
		capacity = n;
	}

	//压栈
	void push(int x)
	{
		if (capacity == Top)
		{
			int newcapacity = 2 * capacity;
			int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
			if (temp == nullptr)
			{
				perror("Push");
				return;
			}
			arr = temp;
			capacity = newcapacity;
		}
		arr[Top++] = x;
	}

	void Print()//只能在类中进行对private的访问
	{
		cout << arr[Top-1] << endl;
	}

private:
	//成员变量
	int* arr;
	int Top;
	int capacity;
};

int main()
{
	Stack st1;//创建结构体变量
	st1.Init();
	st1.push(1);
	st1.push(102);
	st1.Print();

	return 0;
}

        因此如果要访问类的私有域内容,只能在通过类中的函数进行访问。 

3、类重名问题

        例如,现对一个日期类进行初始化:

#include<iostream>
using namespace std;

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;
};

int main()
{
	Date dt1;//创建对象
	dt1.Init(2022, 2, 2);
	dt1.Print();
	return 0;
}

        运行结果:

        可以发现结果竟然是随机值,原因就是初始化函数的形参与类成员变量同名,然后编译器遵循局部优先的概念,把左值当成了形参本身,结果是形参自己给自己赋值,类成员变量并没有完成初始化。

        解决方法:把成员变量名和形参名进行区分即可。

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;
};

4、类的实例化

       用类的类型去创建一个对象,该操作称之为类的实例化,跟c语言中创建变量是一个意思。在对类实例化之前是不能直接去访问类中的内容,因为类好比一个构想图,光有一份构想图是无法真正获得图内的东西,只有在类实例化后,即真正实现了构想图的内容才可以实际的获得其中。因此在实例化后,系统会为该对象开辟一块空间,这时候以该空间为对象,可以去访问他的类。

        类的实例化代码例子如下: 

class Date
{
	//....
public:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date._year = 12;//错误写法,不能直接访问类
	Date dt1;//类的实例化
	Date dt2;//类的实例化
	//实例化后,可以通过对象访问该对象的类
	dt1._day = 12;
	dt2._day = 12;
	return 0;
}

4.1 类的大小

        一个结构体的大小是根据他的成员类型计算出来的,但是一个类所包含的不只有成员变量,还有成员函数,那么一个类的大小该如何计算呢,用上述日期类作为例子,计算该类的大小。

        可以看到,尽管调用了成员函数,但是该类的大小依然是12,说明系统只为该类的成员变量开辟了空间,并没有给成员函数调用空间。

        因为不同对象的成员变量肯定是不一样的,就拿日期类举例,对象dt1的成员变量可以是2022.2.2,但是重新实例化一个对象dt2的成员变量可以为2023.3.3。只是对象dt1和dt2再调用其成员函数时,实质上全部调用的都是同一个成员函数,因为函数都是实现相同功能,比如dt1中的初始化函数和dt2中的初始化函数所实现的功能都是一样的,因此把类的成员函数放在公共区域(即代码段中),不同对象再调用函数时,统一去代码段中调用,因此函数不计入类的大小。

5、隐含的this指针

        上述说到了类成员函数是放在公共区域的,那么问题来了,不同的对象再调用同一个函数时,编译器是如何知道是哪个对象调用的,因为传参的时候传的只有实参数据,并无其他区分对象的标记号。


        当对象调用函数时,编译器会自行给函数的实参和形参补上该对象的地址和指针,如下图:

        编译器会自动把对象的地址一并当作实参传递给函数形参,并且会添加一个(隐藏指针)this指针作为形参接收对象的地址,用指针this就能访问并且修改具体对象里的成员变量了 ,当然这都是编译器自动完成的。我们可以在函数内部使用this指针,但是不能在形参和实参上直接手动添加。因为是形参,因此this指针存放在栈空间,在vs环境下,this指针通过ecx寄存器进行自动传递,所以无需用户干涉过程。

5.1 空指针问题

        我们都知道如果对一个空指针进行解引用是会报错的,那么以下代码的运行结果是什么呢?

#include<iostream>
using namespace std;

class example_one
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
	void Init(int x)
	{
		_a = x;
	}
private:
	int _a;
};
int main()
{
	example_one* p = nullptr;//定义一个指针p指向空

	p->Print();
	
	return 0;
}

        运行结果:

        结果竟然是正常运行并且打印了Print函数内的信息, 当我们看到语句:p->Print(),第一反应都是对p进行解引用,对空指针进行解引用肯定会报错,原因在于这里并没有对p进行解引用,只是调用了类型为example_one的对象里的函数Print(因为函数不是存储在类里面,而是存储在代码段中),并且把指针的内容传给函数Print,这一过程并没有对空指针进行解引用。


        然而下面这种写法就会报错:

#include<iostream>
using namespace std;

class example_one
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
	void Init(int x)
	{
		cout <<this<< endl;//打印出来的是00000000,表示空指针
		_a = x;//这里可以理解为*this->_a,对空指针this解引用,因此报错
	}
private:
	int _a;
};
int main()
{
	example_one* p = nullptr;
	//p->Print();//可以正常运行
	p->Init(1);
	
	return 0;
}

        结合this指针的概念,这里把p的值作为实参传递给了this指针,因此发生错误的原因在于Init函数内部对this指针进行了解引用操作,即对空指针进行解引用操作,导致报错。

        所以‘->'并不一定是解引用操作,关键点在于右值是否为类里的成员变量,如果只是成员函数那么‘->'不为解引用操作,如果右值为成员变量则‘->'表示解引用操作。

结语:

        以上就是关于C++_类的讲解,类与结构体在概念上虽然相似,但是类的细节更多,较结构体更复杂,类作为C++中的基础需掌握好。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!( ̄︶ ̄)↗ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/240151.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Standoff 12 网络演习

在 11 月 21 日至 24 日于莫斯科举行的 "Standoff 12 "网络演习中&#xff0c;Positive Technologies 公司再现了其真实基础设施的一部分&#xff0c;包括软件开发、组装和交付的所有流程。安全研究人员能够在安全的环境中测试系统的安全性&#xff0c;并尝试将第三方…

GO闭包实现原理(汇编级讲解)

go语言闭包实现原理(汇编层解析) 1.起因 今天开始学习go语言,在学到go闭包时候,原本以为go闭包的实现方式就是类似于如下cpp lambda value通过值传递,mutable修饰可以让value可以修改,但是地址不可能一样value通过引用传递,但是在其他地方调用时,这个value局部变量早就释放,…

低多边形植物模型法线贴图

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时&#xff0c;有几种不同的风格&#xf…

深度学习在人体动作识别领域的应用:开源工具、数据集资源及趋动云GPU算力不可或缺

人体动作识别检测是一种通过使用计算机视觉和深度学习技术&#xff0c;对人体姿态和动作进行实时监测和分析的技术。该技术旨在从图像或视频中提取有关人体姿态、动作和行为的信息&#xff0c;以便更深入地识别和理解人的活动。 人体动作识别检测的基本步骤包括&#xff1a; 数…

web279(s2-001)

目前java小白一个&#xff0c;主要是学学别人的思路 进入题目&#xff0c;登录框一个 抓包也没发现什么东西 网上说是struts2框架 Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架 判断是不是基于struts2的一些方法&#xff1a; 1.通过页面回显的错误消息来判断…

MySQL一行记录是怎么存储的?

文章目录 MySQL 一行记录是怎么存储的&#xff1f;MySQL 的数据存放在哪个文件&#xff1f;表空间文件结构 InnoDB行格式有哪些Compact行格式varchar(n) 中 n 最大取值为多少&#xff1f;行溢出后&#xff0c;MySQL是怎么处理的&#xff1f; MySQL 一行记录是怎么存储的&#x…

IDEA 出现问题:git提交commit时Perform code analysis卡住解决方案

问题 git提交commit时Perform code analysis卡住很久 解决方案一 1、打开 IntelliJ IDEA&#xff0c;进入 File -> Settings&#xff08;或者使用快捷键 CtrlAltS&#xff09;。 2、在弹出的 Settings 窗口中&#xff0c;找到 Version Control -> Commit Dialog 选项…

Flink 有状态流式处理

传统批次处理方法 【1】持续收取数据&#xff08;kafka等&#xff09;&#xff0c;以window时间作为划分&#xff0c;划分一个一个的批次档案&#xff08;按照时间或者大小等&#xff09;&#xff1b; 【2】周期性执行批次运算&#xff08;Spark/Stom等&#xff09;&#xff1b…

机器学习---Adaboost算法

1. Adaboost算法介绍 Adaboost是一种迭代算法&#xff0c;其核心思想是针对同一个训练集训练不同的分类器&#xff08;弱分类器&#xff09;&#xff0c;然 后把这些弱分类器集合起来&#xff0c;构成一个更强的最终分类器&#xff08;强分类器&#xff09;。Adaboost算法本身…

CSS学习

CSS学习 1. 什么是css?2.css引入方式2.1 内嵌式2.2 外联式2.3 行内式2.4 引入方式特点 3. 基础选择器3.1 标签选择器3.2 类选择器3.3 id选择器3.4 通配符选择器 1. 什么是css? 2.css引入方式 2.1 内嵌式 2.2 外联式 提示: 需要在html文件中link目标样式表; 2.3 行内式 注意:…

【EventBus】EventBus源码浅析

二、EventBus源码解析 目录 1、EventBus的构造方法2、订阅者注册 2.1 订阅者方法的查找过程2.2 订阅者的注册过程1. subscriptionsByEventType 映射&#xff1a;2. typesBySubscriber 映射&#xff1a;2.3 总结订阅者的注册过程 3、事件的发送 3.1 使用Post提交事件3.2 使用p…

【STM32】DMA直接存储器存取

1 DMA简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 可以直接访问STM32的存储器的&#xff0c;包括运行SRAM、程序存储器Flash和寄存器等等 DMA可以提供外设寄存器和存储器或者存储器和存储器之间的高速数据传输&#xff0c;无须CPU干预&#xff0c;节…

【lombok】从easyExcel read不到值到cglib @Accessors(chain = true)隐藏的大坑

背景: 在一次使用easyExcel.read 读取excel时&#xff0c;发现实体类字段没有值&#xff0c;在反复测试后&#xff0c;发现去掉Accessors(chain true)就正常了&#xff0c;为了验证原因&#xff0c;进行了一次代码跟踪 由于调用链路特别长&#xff0c;只列举出部分代码&#x…

二蛋赠书十一期:《TypeScript入门与区块链项目实战》

前言 大家好&#xff01;我是二蛋&#xff0c;一个热爱技术、乐于分享的工程师。在过去的几年里&#xff0c;我一直通过各种渠道与大家分享技术知识和经验。我深知&#xff0c;每一位技术人员都对自己的技能提升和职业发展有着热切的期待。因此&#xff0c;我非常感激大家一直…

【C 剑指offer】有序整型矩阵元素查找 {杨氏矩阵}

目录 题目内容&#xff1a; 思路&#xff1a; 图形演示&#xff1a; 复杂度分析 C源码&#xff1a; /** *************************************************************************** ******************** ********************* ******…

DWA(dynamic window approach)算法学习

系列文章目录 A*算法学习-CSDN博客 弗洛伊德算法&#xff08;Floyd&#xff09;和路径平滑弗洛伊德算法&#xff08;Smooth Floyd&#xff09;学习-CSDN博客 D*算法学习-CSDN博客 目录 系列文章目录 前言 搜索空间 —减小速度搜索空间 优化过程 —最大化目标函数 算法实…

《洛谷深入浅出》斯特林数

斯特林数被分为三种&#xff0c;但我们这只介绍两种。即第一类斯特林数&#xff0c;和第二类斯特拉数。 第一类斯特林数指的是&#xff1a; 将n个不同元素&#xff0c;变成m个圆排列的方案数量。第一类斯特林数&#xff0c;分为有符号和无符号。通常我们只研究无符号斯特林数&…

Layui深入

1、代码&#xff1a; <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>注册页面</title> <style> .container { max-width: 600px; margin: 0 auto; padding: 20px; …

Proxmox VE 安装 OpenWrt 配置旁路由教程

话不多说&#xff0c;本篇文章将记录如何在 Proxmox VE 环境通过虚拟机安装 OpenWrt 配置旁路由的过程&#xff0c;仅做参考。 PVE 创建虚拟机 名称随意&#xff0c;GuestOS 选择 Linux&#xff0c;不使用任何 iso 镜像。&#xff08;记住你的 VMID&#xff09; 清空将要创建…

超越边界:Mistral 7B挑战AI新标准,全面超越Llama 2 13B

引言 在人工智能领域&#xff0c;模型的性能一直是衡量其价值和应用潜力的关键指标。近日&#xff0c;一个新的里程碑被设立&#xff1a;Mistral AI发布了其最新模型Mistral 7B&#xff0c;它在众多基准测试中全面超越了Llama 2 13B模型&#xff0c;标志着AI技术的一个重大进步…