C++ 类和对象(1/3)

C++ 类和对象(1/3)

1. 类的定义

1.1 类定义格式

  1. class为定义类的关键字,date 为类的名字,{ }中为类的主体,注意类定义结束时后面分号不能省 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
  2. 为了区分成员变量,⼀般习惯上成员变量会加一个特殊标识,如成员变量前⾯或者后面加 _   或者 m 开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
  3. C++中struct也可以定义类,C++兼容C中 struct 的用法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,⼀般情况下我们还是推荐用 class 定义类。
  4. 定义在类⾯的成员函数默认为inline。
class Date { public: //无参数的构造函数 Date() { _year = 1900; _month = 1; _day = 1; } void ShowDate() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; 

类的语法结构

class ClassName { access_specifier: member_variables; member_functions(); }; 
  • access_specifier:定义成员的访问权限(publicprivateprotected)。
  • member_variables:类的数据成员(属性)。
  • member_functions:类的成员函数(方法)。

1.2 访问限定符

  1. C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限 选择性的将其接⼝提供给外部的用户使用。
  2. public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到  } 即类结束。
  4. class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
  5. ⼀般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。

1.3 类域

  1. 类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
  2. 类域影响的是编译的查找规则,下⾯程序中 Init 如果不指定类域 Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知 道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
#include<iostream> using namespace std; class Stack { public: // 成员函数 void Init(int n = 4); private: // 成员变量 int* array; size_t capacity; size_t top; }; // 声明和定义分离,需要指定类域 void Stack::Init(int n) { //如果会C++的 new 同理 array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } int main() { Stack st; st.Init(); return 0; }

2. 实例化

2.1 实例化概念

  1. 用类类型在物理内存中创建对象的过程,称为类实例化出对象。
  2. 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
  3. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。

  • 仍然以Date为例:
#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类实例化出对象d1和d2 Date d1; Date d2; d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; } 
  • 输出:

2.2 对象大小

分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外多说一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。

先简单看看[call地址],所以下图存储是合理的

上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。

内存对齐规则(其实和结构体的规则差不多)

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  3. 注意:对齐数 = 编译器默认的齐个对齐数与该成员大小的较小值。
  4. VS中默认的对齐数为8
  5. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  6. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  7. 如果没有占有内存,默认为1。
#include<iostream> using namespace std; // 计算⼀下A/B/C实例化的对象是多⼤? class A { public: void Print() { cout << _ch << endl; } private: char _ch; int _i; }; class B { public: void Print() { //... } }; class C { }; int main() { A a; B b; C c; cout << sizeof(a) << endl; //8 cout << sizeof(b) << endl; //1 cout << sizeof(c) << endl; //1 return 0; }

上面程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个字节呢?因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。

3. this指针

  1. Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这里的问题
  2. 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加一个当前类类型的指针,叫做this指针。比如Date类的Init的真实原型为,void Init(Date* const this, int year, int month, int day)
  3. 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this- >_year = year;
  4. C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
#include<iostream> using namespace std; class Date { public: //省略了this指针,但在函数体内仍然可以使用 // void Init(Date* const this, int year, int month, int day) void Init(int year, int month, int day) { // this->_year = year; _year = year; //可以省略this指针,但为了区分形参和成员变量,建议加上 this->_month = month; this->_day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: // 这⾥只是声明,没有开空间 int _year; int _month; int _day; }; int main() { // Date类实例化出对象d1和d2 Date d1; Date d2; d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }

4. C++和C语言实现Stack对比(代码较长,建议复制到VS等工具进行对比)

面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。通过下⾯两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化。

  1. C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。
  2. C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便。
  3. 在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等着我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力。

C实现Stack代码

#include<stdio.h> #include<stdlib.h> #include<stdbool.h> #include<assert.h> typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps) { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void STDestroy(ST* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->top = ps->capacity = 0; } void STPush(ST* ps, STDataType x) { assert(ps); // 满了, 扩容 if (ps->top == ps->capacity) { int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } ps->a = tmp; ps->capacity = newcapacity; } ps->a[ps->top] = x; ps->top++; } bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; } void STPop(ST* ps) { assert(ps); assert(!STEmpty(ps)); ps->top--; } STDataType STTop(ST* ps) { assert(ps); assert(!STEmpty(ps)); return ps->a[ps->top - 1]; } int STSize(ST* ps) { assert(ps); return ps->top; } int main() { ST s; STInit(&s); STPush(&s, 1); STPush(&s, 2); STPush(&s, 3); STPush(&s, 4); while (!STEmpty(&s)) { printf("%d ", STTop(&s)); STPop(&s); } STDestroy(&s); return 0; } //输出:4 3 2 1 

C++实现Stack代码

#include<iostream> #include<cstdlib> #include<cassert> using namespace std; typedef int STDataType; class Stack { public: // 成员函数 void Init(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } void Push(STDataType x) { if (_top == _capacity) { int newcapacity = _capacity * 2; STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } _a = tmp; _capacity = newcapacity; } _a[_top++] = x; } void Pop() { assert(_top > 0); --_top; } bool Empty() { return _top == 0; } int Top() { assert(_top > 0); return _a[_top - 1]; } void Destroy() { free(_a); _a = nullptr; _top = _capacity = 0; } private: // 成员变量 STDataType* _a; size_t _capacity; size_t _top; }; int main() { Stack s; s.Init(); s.Push(1); s.Push(2); s.Push(3); s.Push(4); while (!s.Empty()) { printf("%d ", s.Top()); s.Pop(); } s.Destroy(); return 0; } //输出:4 3 2 1 

来道题目:

下面程序编译运⾏结果是()A、编译报错 B、运行崩溃 C、正常运行

#include<iostream> using namespace std; class A { public: void Print() { cout << "A::Print()" << endl; cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
  • 结果:明显程序崩溃(B)

通过 -> 间接引用调用class A中 public 中的 Print() ,第一行A::Print() 可以直接调用,但是第二条  -a  呢?回顾上文, public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问。

那么有什么办法解决吗?方法有多种,这里举一种简单的吧
 

int main() { A obj; // 创建一个有效的对象 A* p = &obj; // 指针指向有效对象 p->Print(); // 安全调用 return 0; }

此时结果如下:

由于没有给_a初始化,故得出随机值。

C++ 类和对象的上部分就到这为止了,目前只是简单的讲述了类的用法,其具体的功能并未展现,欲知后事如何,且听下回分解。

Read more

AI生成图片R18提示词:新手入门指南与最佳实践

快速体验 在开始今天关于 AI生成图片R18提示词:新手入门指南与最佳实践 的探讨之前,我想先分享一个最近让我觉得很有意思的全栈技术挑战。 我们常说 AI 是未来,但作为开发者,如何将大模型(LLM)真正落地为一个低延迟、可交互的实时系统,而不仅仅是调个 API? 这里有一个非常硬核的动手实验:基于火山引擎豆包大模型,从零搭建一个实时语音通话应用。它不是简单的问答,而是需要你亲手打通 ASR(语音识别)→ LLM(大脑思考)→ TTS(语音合成)的完整 WebSocket 链路。对于想要掌握 AI 原生应用架构的同学来说,这是个绝佳的练手项目。 从0到1构建生产级别应用,脱离Demo,点击打开 从0打造个人豆包实时通话AI动手实验 AI生成图片R18提示词:新手入门指南与最佳实践 背景与痛点 对于刚接触AI生成图片的新手来说,使用R18提示词往往会遇到几个典型问题: 1. 内容合规性把控困难:多数平台对生成内容有严格限制,容易触发审核机制导致生成失败 2. 提示词效果不稳定:相同的提示词在不同模型或参数下可能产生差异巨大的结果

By Ne0inhk
“神经网络的奥秘”一篇带你读懂AI学习核心

“神经网络的奥秘”一篇带你读懂AI学习核心

引言:“神经网络的奥秘”一篇带你读懂AI学习核心 想学AI却卡在神经网络?这篇带你轻松突破核心难点! 如今打开手机,AI修图、智能推荐、语音助手随时待命;刷到科技新闻,自动驾驶、AI制药、大模型对话的进展不断刷新认知。而这一切AI能力的核心,都离不开一个关键技术——神经网络。 很多人把神经网络当成“高深黑箱”,觉得必须有深厚的数学功底才能理解。但其实,神经网络的核心逻辑和人类大脑的学习方式很相似,哪怕是非科班出身,也能通过通俗的解释搞懂它的运作原理。这篇文章就从“是什么、怎么学、用在哪”三个维度,带你彻底读懂神经网络,真正入门AI学习的核心。 * 引言:“神经网络的奥秘”一篇带你读懂AI学习核心 * 一、先搞懂基础:神经网络到底是什么? * 二、核心奥秘:神经网络是如何“学习”的? * 三、必懂概念:新手入门神经网络的5个关键术语 * 四、实际应用:神经网络在我们身边的5个场景 * 五、新手学习路径:从入门到实战的3个阶段

By Ne0inhk

Trae IDE 安装与使用保姆级教程:字节跳动的 AI 编程神器

一、Trae 是什么? Trae(发音 /treɪ/)是字节跳动推出的 AI 原生集成开发环境(AI IDE),于 2025 年 1 月正式发布。与传统的 IDE + AI 插件组合不同,Trae 从底层架构上就将 AI 能力深度集成,实现了真正意义上的"AI 主导开发"。 核心定位 Trae 以 “自主智能体(Agent)” 为核心定位,彻底重构了传统开发流程: * Chat 模式:智能代码补全、问答、解释和优化 * Builder 模式:自然语言一键生成完整项目框架 * SOLO 模式:AI 自主规划并执行开发任务 版本划分 版本定位核心特色适用人群Trae

By Ne0inhk
SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

SpringAI 大模型应用开发篇-SpringAI 项目的新手入门知识

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 SpringAI 概述         1.1 大模型的使用         2.0 SpringAI 新手入门         2.1 配置 pom.xml 文件         2.2 配置 application.yaml 文件         2.3 配置 ChatClient         2.4 同步调用         2.5 流式调用         2.6 System 设定         2.7 日志功能         2.8 会话记忆功能

By Ne0inhk