当前位置: 首页 > news >正文

C++ 类和对象详解(1)

类和对象是 C++ 面向对象编程的核心概念,它们为代码提供了更好的封装性、可读性和可维护性。本文将从类的定义开始,逐步讲解访问限定符、类域、实例化、对象大小计算、this 指针等关键知识,并对比 C 语言与 C++ 在实现数据结构时的差异,快速掌握类和对象的基础用法。

1. 类的定义

类是对现实事物的抽象描述,它封装了数据(成员变量)和操作数据的方法(成员函数)。在 C++ 中,我们通过class关键字定义类,其基本结构如下:

1.1 类定义格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省
略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或
者成员函数。

C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是
struct中可以定义函数,一般情况下我们还是推荐用class定义类。

定义在类面的成员函数默认为 inline 函数,声明和定义分离就不是了。

class 类名 {// 成员变量(属性)// 成员函数(方法)
}; // 注意:分号不能省略

示例:Stack 类
下面是一个栈(Stack)类的定义:

#include<iostream>
#include<assert.h>
#include<stdlib.h>
using namespace std;class Stack {
public:// 成员函数:初始化栈void Init(int n = 4) {array = (int*)malloc(sizeof(int) * n);if (nullptr == array) {perror("malloc申请空间失败");return;}capacity = n; // 容量top = 0; // 栈顶指针(指向待插入位置)}// 成员函数:入栈void Push(int x) {// 扩容逻辑(简化)array[top++] = x;}// 成员函数:取栈顶元素int Top() {assert(top > 0); // 确保栈非空return array[top - 1];}// 成员函数:销毁栈void Destroy() {free(array);array = nullptr;top = capacity = 0;}private:// 成员变量(加_区分普通变量)int* array; // 存储数据的数组size_t capacity; // 容量size_t top; // 栈顶指针
};

1.2 成员命名习惯

为了区分成员变量和普通变量,通常会在成员变量前加_m_(如_yearm_capacity)。这不是 C++ 的强制规定,而是工程中的常见惯例,便于代码阅读。

示例:Date 类

class Date {
public:void Init(int year, int month, int day) {_year = year;   // 成员变量_year_month = month; // 成员变量_month_day = day;     // 成员变量_day}private:int _year;  // 年份int _month; // 月份int _day;   // 日期
};

1.3 class 与 struct 的区别

C++ 中的struct也可以定义类,它与class的主要区别是:

  • class默认访问权限为private(私有);
  • struct默认访问权限为public(公有),且兼容 C 语言中struct的用法(如仅作为数据结构)。

实际开发中,推荐用class定义类,struct多用于纯数据结构(如链表节点)。

示例:C++ 的 struct 类

// C++中struct可定义函数
struct ListNodeCPP 
{void Init(int x){ // 成员函数:初始化节点val = x;next = nullptr;}ListNodeCPP* next; // 成员变量int val;
};//C++中不用 typedef //补充:C语言中
typedef struct ListNodeC
{int val;struct ListNodeC* next;
}LN;

2. 访问限定符

访问限定符用于控制类成员在类外的访问权限,是 C++ 实现封装的核心手段。C++ 提供三种访问限定符:

2.1 访问权限规则

限定符权限说明
public类内、类外均可访问(公有)
private仅类内可访问,类外不可直接访问(私有)
protected类内可访问,子类可访问,类外不可直接访问(继承时体现差异)(私有)

访问权限的作用域从该限定符出现的位置开始,直到下一个限定符或类结束(})为止。

2.2 封装的意义

通过访问限定符,我们可以将成员变量设为private,仅通过public的成员函数操作数据,避免外部代码随意修改数据导致的错误。例如 Stack 类中,arraytop等变量被private保护,外部只能通过PushTop等函数操作,确保栈的逻辑正确。

3. 类域

类定义了一个独立的作用域(类域),类的所有成员都属于这个作用域。在类外定义成员函数时,需要用::(作用域操作符)指明所属的类。

3.1 类外定义成员函数

如果成员函数的声明和定义分离(类内声明,类外定义),必须通过类名::函数名指定类域,否则编译器会将函数视为全局函数。

示例:类外定义 Stack::Init

class Stack {
public:void Init(int n = 4); // 类内声明
private:int* array;size_t capacity;size_t top;
};// 类外定义,需指定Stack类域
void Stack::Init(int n) {array = (int*)malloc(sizeof(int) * n);if (nullptr == array) {perror("malloc失败");return;}capacity = n;top = 0;
}

3.2 类域的作用

类域确保了不同类的成员可以重名而不冲突。例如,两个类都可以有Init函数,通过类域区分:Stack::InitDate::Init。(注意:类域和命名空间域只影响名字的隔离,不影响生命周期)

4. 类的实例化

用类创建对象的过程称为实例化。类本身只是一个 “模板”,不占用实际内存(可以说是一个声明,声明不占空间);实例化后的对象才会分配内存,存储成员变量。

4.1 实例化的意义

  • 类:抽象的 “设计图”,描述对象有哪些属性和方法(如 “房子设计图”);
  • 对象:根据类创建的具体实例,占用内存,可存储数据(如 “用设计图盖的房子”)。

示例:实例化 Date 对象

int main() {Date d1; // 用Date类实例化对象d1Date d2; // 实例化对象d2d1.Init(2024, 3, 31); // 调用d1的Init方法d2.Init(2024, 7, 5);  // 调用d2的Init方法return 0;
}

4.2 对象存储的内容

对象中只存储成员变量,成员函数被所有对象共享(存储在代码段)。原因是:

  • 成员变量是每个对象的独立数据(如 d1 和 d2 的_year可以不同);
  • 成员函数是相同的操作逻辑(如 d1 和 d2 的Init函数代码完全一样,无需重复存储)。

5. 对象大小计算

5.1 对象存储方式

函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。

再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year/_month/_day存储各自的数据,但是d1和d2的成员函数 Init 指针却是一样的,存储在对象中就浪费了。

如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。

对象的大小由成员变量决定,遵循内存对齐规则;成员函数不占用对象的内存。

5.2 内存对齐规则

  1. 第一个成员在偏移量为 0 的地址;
  2. 其他成员对齐到 “对齐数” 的整数倍地址(对齐数 = min (编译器默认对齐数8,成员大小),VS 默认对齐数为 8);
  3. 对象总大小是最大对齐数的整数倍;
  4. 嵌套结构体时,嵌套对象对齐到自身最大对齐数的整数倍,总大小包含嵌套对象的对齐数。

5.3 示例:计算对象大小

class A {
public:void Print() { cout << _ch << endl; }
private:char _ch; // 大小1int _i;   // 大小4
};class B {
public:void Print() {} // 无成员变量
};class C {}; // 空类int main() {A a;B b;C c;cout << sizeof(a) << endl; // 8(1+3填充+4)cout << sizeof(b) << endl; // 1(占位,标识对象存在)cout << sizeof(c) << endl; // 1(空类大小为1)return 0;
}

如果不对齐,则会导致编译器读取,出现错误。(编译器规定每次读的时候,是从固定的整数倍开始读固定字节的,为的是提高机器效率)

  • 类 A:_ch(1 字节)+ 3 字节填充(对齐到 4)_i(4 字节),总 8 字节;
  • 类 B 和 C:无成员变量,但为了标识对象存在,占用 1 字节。

6. this 指针

当多个对象调用同一个成员函数时,函数如何区分操作的是哪个对象?C++ 通过隐藏的this指针解决这个问题。

6.1 this 指针的特性

  • this指针是成员函数的隐含参数,类型为类名* const(指向当前对象,不可修改指向);
  • 调用成员函数时,编译器自动将对象地址作为this指针传入;
  • 函数体内可显式使用this指针访问成员变量(如this->_year = year),但不能在形参或实参中显式声明。
  • C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。

示例:this 指针的隐含传递

年月日,本质是通过 this 指针访问的,this 不可修改:

#include<iostream>
using namespace std;
class Date
{
public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){this->_year = year;this->_month = month;this->_day = day;}// void Print(Date* const this)void Print(){cout << this->_year << "/" << this->_month << "/" << _day << endl;}private:// 这里只是声明,没有开空间int _year;int _month;int _day;
};int main()
{// Date类实例化出对象d1和d2Date d1;Date d2;// d1.Init(&d1, 2024, 3, 31);d1.Init(2024, 3, 31);// d1.Print(&d1);d1.Print();// d2.Init(&d2, 2024, 7, 5);d2.Init(2024, 7, 5);// d2.Print(&d2);d2.Print();return 0;
}

6.2 经典问题:空指针调用函数

问题 1:以下代码运行结果?

class A {
public:void Print() { cout << "A::Print()" << endl; }
private:int _a;
};int main() {A* p = nullptr;p->Print(); // 正常运行?崩溃?return 0;
}

答案:正常运行

解答:

Print函数未访问成员变量(无需解引用this),即使p为 nullptr,仍可调用。

class a
{
public:void print(){cout << this << endl;cout << "a::print()" << endl;}
private:int _a;
};int main()
{a* p = nullptr;// mov ecx  pp->print(); // call  地址//p->_a = 1;a aa;return 0;
}

this 是指向对象的地址,指向对象的指针,是为了方便访问成员变量的,这里 p 就是对象的地址。

问题 2:以下代码运行结果?

class A {
public:void Print() { cout << _a << endl; } // 访问成员变量
private:int _a;
};int main() {A* p = nullptr;p->Print(); // 正常运行?崩溃?return 0;
}

答案:运行崩溃

解答:

Print函数访问_a(即this->_a),而this为 nullptr,解引用空指针导致崩溃。

问题3:this指针存在内存哪个区域的 ()

A. 栈 B.堆 C.静态区 D.常量区 E.对象里面

答案:A. 栈

解答:

this 是形参,参数在栈帧中。

7. C 与 C++ 实现 Stack 对比

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

面向对象的封装特性让 C++ 的代码更简洁、安全。对比 C 语言和 C++ 实现的 Stack:

特性C 语言实现C++ 实现
数据与方法分离(如STInitST结构体)封装在类中(成员变量 + 成员函数)
访问控制无限制(可直接修改ST的成员)private保护成员,仅通过public函数访问
调用方式需显式传递结构体地址(如STPush(&s, 1)隐含this指针(如s.Push(1)
代码可读性需记住函数与结构体的关联类内函数直接操作成员,逻辑清晰

C 语言实现核心代码

typedef struct Stack {int* a;int top;int capacity;
} ST;void STInit(ST* ps) { // 需显式传参ps->a = NULL;ps->top = 0;ps->capacity = 0;
}void STPush(ST* ps, int x) { // 需显式传参// 入栈逻辑
}

C++ 实现核心代码

class Stack {
public:void Init(int n = 4) { // 隐含this指针// 初始化逻辑}void Push(int x) { // 隐含this指针// 入栈逻辑}
private:int* _a;int _top;int _capacity;
};

C++,相当于来说,更加规范,因为进行了封装:

一个直观的感受,访问栈顶元素:

C语言中:

但是,在C++中:

是不能实现的。

在 C++ 面向对象编程中,类的默认成员函数是实现封装、简化代码的核心机制。当我们未显式定义某些成员函数时,编译器会自动生成它们,以保证类的基本功能可用。

http://www.dtcms.com/a/329244.html

相关文章:

  • 飞算JavaAI实现数据库交互:JPA/Hibernate + MyBatis Plus基础功能学习
  • STM32的UART奇偶校验注意
  • 20.04ubantu 编译lio_sam问题解决
  • 推荐系统论文分享之多任务模型--PLE(一)
  • Java 中 static 关键字详解(更新版)
  • JavaScript手录16-定时器
  • 基于51单片机的手机蓝牙控制8位LED灯亮灭设计
  • 传统Python开发工程师转型大模型智能体开发工程师路径
  • jq实现页面区域内拖动功能
  • InfluxDB 在工业控制系统中的数据监控案例(一)
  • 自然语言处理的实际应用
  • 晓知识: 微服务CAP定理
  • 5. synchronized 关键字 - 监视器锁 monitor lock
  • 基于 MybatisPlus 将百度天气数据存储至 PostgreSQL 数据库的实践
  • 飞算JavaAI云原生实践:基于Docker与K8s的自动化部署架构解析
  • 深入理解 C++ 中的虚函数:原理、特点与使用场景
  • Nginx学习笔记(七)——Nginx负载均衡
  • Orange的运维学习日记--43.Ansible进阶之变量与加密
  • SQL详细语法教程(二)--DML(数据操作语言)和DQL(数据查询语言)
  • 健永科技工业自动化RFID解决方案
  • Linux:线程
  • LeetCode215~ 234题解
  • 《算法导论》第 23 章 - 最小生成树
  • 中高级餐饮服务食品安全员考试核心知识点汇总
  • 亚马逊精准词失灵:广告效能瓶颈的系统破解与矩阵重构
  • RK3588——DMABUF+CMA的完美组合
  • YOLO-v2-tiny 20种物体检测模型
  • 基于C语言基础对C++的进一步学习_C和C++编程范式、C与C++对比的一些补充知识、C++中的命名空间、文件分层
  • Java Redis基础入门:快速上手指南
  • 广东省省考备考(第七十五天8.13)——判断推理(图形推理题型总结)