类和对象(上)

个人主页:星轨初途

个人专栏:C语言数据结构C++学习(竞赛类)C++专栏(开发类)


文章目录

  • 前言
  • [一、从 struct 到 class:类的初探](#一、从 struct 到 class:类的初探)
  • 二、类域与声明定义分离
  • 三、实例化与对象模型(底层探秘)
    • [1. 什么是实例化(概念)](#1. 什么是实例化(概念))
    • [2. 对象的大小与存储模型](#2. 对象的大小与存储模型)
    • [3、 内存对齐规则](#3、 内存对齐规则)
  • [四、隐藏的牵绊:this 指针](#四、隐藏的牵绊:this 指针)
  • 五、实战检验
        • [题目 1:下面程序编译运行结果是?](#题目 1:下面程序编译运行结果是?)
        • [题目 2:下面程序编译运行结果是?](#题目 2:下面程序编译运行结果是?)
        • [题目 3:this 指针存在内存的哪个区域?](#题目 3:this 指针存在内存的哪个区域?)
  • [六、C语言 vs C++ 体验对比](#六、C语言 vs C++ 体验对比)
  • 结束语

前言

嗨!我们又见面啦!上一篇我们学习了C++的基础语法,已经对C++有了初步的了解,今天我们将正式迈入C++的灵魂殿堂------类和对象 , 在实际的工程开发中,C语言的面向过程思维在面对复杂的大型系统时往往难以维护;而C++的面向对象特性(封装、继承、多态)则是构建现代软件的基石。我把类和对象分为三个章节进行详细讲解,准备好了吗?我们开始!


一、从 struct 到 class:类的初探

在C语言中,我们用 struct 把数据打包。但在C++中,struct 迎来了一次史诗级升级,同时引入了专门用于面向对象的关键字 class

1、类的基本定义

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

  • 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加 _ 或者 m 开头,注意 C++ 中这个并不是强制的,只是一些惯例,具体看公司的要求。

  • 定义在类内部 的成员函数默认为 inline

💡 开发规范避坑:

为了区分成员变量和局部变量,一般习惯上成员变量会加一个特殊标识 。

例如在成员变量前面或者后面加 _ 或者 m_ 开头 。虽然C++对此并非强制,但这是企业开发中极度依赖的规范约定 。

cpp 复制代码
class Date 
{
public:
    void Init(int year, int month, int day) 
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    // 加上 _ 前缀,与形参 year, month, day 完美区分
    int _year;  
    int _month;
    int _day;
};
int main()
{
    return 0;
}
  • C++ 中 struct 也可以定义类,C++ 兼容 C 中 struct 的用法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,一般情况下我们还是推荐用 class 定义类。
cpp 复制代码
#include<iostream>
using namespace std;
// C++升级struct升级成了类
// 1、类里面可以定义函数
// 2、struct名称就可以代表类型
// C++兼容C中struct的用法
typedef struct ListNodeC
{
    struct ListNodeC* next;
    int val;
}LTNode;
// 不再需要typedef,ListNodeCPP就可以代表类型
struct ListNodeCPP
{
    void Init(int x)
    {
        next = nullptr;
        val = x;
    }

    ListNodeCPP* next;
    int val;
};
int main()
{
    return 0;
}

2. 访问限定符(权限控制)

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

  • public(公有)修饰的成员在类外可以直接被访问 。

  • protected(保护)和 private(私有)修饰的成员在类外不能直接被访问 ,以后继承章节才能体现出他们的区别。

  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止 。

  • class和struct核心区别: class 定义成员没有被访问限定符修饰时默认为 private,而 struct 默认为 public(为了兼容C语言) 。

🛠️ 工程经验:

一般成员变量都会被限制为 privateprotected,需要给外部调用的成员函数才会设为 public 。这就是封装的核心:只暴露出安全的接口,把容易被乱改的数据藏起来。


二、类域与声明定义分离

  • 类定义了一个新的作用域,类的所有成员都在类的作用域中 。

  • 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域 。

在大型软件项目中,我们通常把类的声明写在 .h 头文件中,而把具体的方法定义写在 .cpp 源文件中。此时,类域操作符 :: 就至关重要了:

cpp 复制代码
// 在 .cpp 文件中实现
void Stack::Init(int n)
 { // 必须指定 Stack:: 告诉编译器这是 Stack 类的成员函数
    array = (int*)malloc(sizeof(int) * n);
    // ...
}

类域影响的是编译的查找规则,如果没有 Stack::,编译器就会把它当成一个普通的全局函数,进而找不到类内部定义的 array 变量从而报错 。


三、实例化与对象模型(底层探秘)

1. 什么是实例化(概念)

  • 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量 。

  • 用类类型在物理内存中创建对象的过程,称为类实例化出对象 。

  • 类里面的变量只是声明,没有分配空间 ,用类实例化出对象时,才会分配空间。

打个比方:类就像是建筑设计图,设计图规划了有多少个房间,但不能住人 。实例化出的对象就像用设计图修建出的真实房子,分配物理内存存储数据 。

cpp 复制代码
#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(2026, 3, 16);
    d1.Print();

    d2.Init(2026, 3, 16);
    d2.Print();

    return 0;
}

2. 对象的大小与存储模型

类实例化出的每个对象,都有独立的数据空间,所以对象中包含成员变量 。那么,成员函数存在哪里呢?

  • 成员函数被编译后是一段指令,存储在一个单独的区域(公共代码段) 。

  • 对象中只存储成员变量,并不存储成员函数指针 。因为无论实例化多少个对象,它们调用的都是同一份代码指令,如果在每个对象中都存一份函数指针,那就太浪费内存了 。

  • 对象的成员变量同样遵循C语言中的内存对齐规则 。(不了解的可以去网上搜索了解,这里就不做详细讲解啦)

🔥 极限避坑:空类的大小

如果一个类既没有成员变量,它的大小是多少?

cpp 复制代码
class C {}; 
// sizeof(C) 是多少?
class B
{
  public:
  void print()
  {
    //...
  }
}
// sizeof(B) 是多少?

答案是 1 个字节

C++ 要求每个对象都必须有唯一的内存地址,即使类中没有任何数据成员,也需要分配至少 1 字节的内存来标识这个对象的存在(避免多个空类对象共享同一个地址)。

而成员函数是存储在代码段(全局区)的,所有对象共享同一份成员函数代码,不会占用对象本身的内存空间

因为如果一个字节都不给,怎么表示对象存在过呢 ?所以这里给1字节,纯粹是为了占位标识对象存在

3、 内存对齐规则

可能大家对内存对齐规则已经忘了,我们这里简单复习一下

规则:

  • 第一个成员在与结构体偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  • VS中默认的对齐数为8。
  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

来我们举一个例子进行练习一下吧!

cpp 复制代码
#include<iostream>
using namespace std;
// 计算一下A实例化的对象是多大?
class A
{
public:
    void Print()
    {
        cout << _ch << endl;
    }
private:
    char _ch;
    int _i;
};

类A对象大小计算(以VS默认对齐数8为例)

  1. char _ch:占1字节,偏移0~0。
  2. int _i:对齐数为min(8,4)=4,需对齐到偏移4的位置,偏移4~7,占4字节。
  3. 总大小为8字节,是最大对齐数4的整数倍。

所以class A实例化对象的大小为8字节(成员函数不占对象内存)。


四、隐藏的牵绊:this 指针

既然所有的对象都共享同一份成员函数代码,那么当执行 d1.Init()d2.Init() 时,函数内部怎么知道现在是在给 d1 赋值还是在给 d2 赋值呢 ?

这就引出了C++中最精妙的设计:this 指针

  • 编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做 this 指针 。

  • 类的成员函数中访问成员变量,本质都是通过 this 指针访问的 。

  • C++规定不能在实参和形参的位置显式地写 this 指针 (编译时编译器会自动处理),但是可以在函数体内显式使用 this 指针 。

你可以把它理解为编译器在背后帮你做了这样一件事:

cpp 复制代码
// 我们写的:
void Init(int year)
 {
    _year = year;
}
d1.Init(2026);

// 编译器偷偷转换的(类似C语言的传参思维):
void Init(Date* const this, int year)
 {
    this->_year = year;
}
Init(&d1, 2026);

五、实战检验

为了彻底检验你对"对象模型"和"this指针"的理解,我们来看三道极其经典的连环面试题 !

题目 1:下面程序编译运行结果是?

A、编译报错

B、运行崩溃

C、正常运行

cpp 复制代码
#include<iostream>
using namespace std;
class A
 {
public:
    void Print()
     {
        cout << "A:: Print()" << endl;
    }
private:
    int _a;
};

int main() 
{
    A* p = nullptr;
    p->Print();
    return 0;
}

答案:C、正常运行 > 深度解析: 很多人看到 p->Print() 觉得 p 是空指针,肯定会报空指针解引用错误(运行崩溃)。但别忘了我们前面讲的:成员函数并不存在对象里面 !它存在公共代码段。

这里的 p->Print() 在编译后,本质上是把 nullptr 作为实参传给了隐含的 this 指针。

进入 Print 函数内部后,代码只是打印了一句话,并没有去访问任何成员变量(即没有对 this 指针进行解引用操作)。既然没有解引用空指针,程序自然能完美正常运行!

题目 2:下面程序编译运行结果是?

A、编译报错

B、运行崩溃

C、正常运行

cpp 复制代码
#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、运行崩溃 > 深度解析: 这题和上一题唯一的区别,就是函数内部多了一句 cout << _a << endl;

别忘了,访问成员变量的本质是 this->_a。此时 this 接收到的是 nullptr,去执行 nullptr->_a,这属于典型的空指针解引用,所以程序在运行时一定会崩溃。

题目 3:this 指针存在内存的哪个区域?

A. 栈

B. 堆

C. 静态区

D. 常量区

E. 对象里面

答案:A. 栈 > 深度解析: 这是一个超级易错题!很多人会误选 E(对象里面)。

记住,this 指针本质上是成员函数的一个隐藏形参 。形参和局部变量一样,都是在函数被调用时,压入函数所在的**栈帧(Stack)**中去的。函数调用结束,栈帧销毁,this 指针也就跟着销毁了。它绝对不在对象里面(对象里面只存成员变量)!


六、C语言 vs C++ 体验对比

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

通过前面的讲解,我们可以初步了解面向对象三大特性之一的封装 。

在以前用C语言写 Stack 时,数据和方法是分离的,任何人都可以在外部直接修改内部的 top 值,这是非常危险的。

在C++中:

  1. 数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据 。

  2. 封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题 。

  3. 成员函数每次不需要传对象地址,因为 this 指针隐含传递了,使用极其方便 ,使用类型不再需要typedef用类名就很方便


结束语

嗨ヾ(◍°∇°◍)ノ゙!今天的内容就到这里。从 structclass,从内存对齐到隐藏的 this 指针,我们不仅学习了语法,更剖析了C++编译器在背后为开发者做的底层优化。下一篇我们将继续深入类和对象的默认成员函数,敬请期待啦!φ(>ω<*)

相关推荐
阿蒙Amon2 小时前
C#常用类库-详解Moq
开发语言·c#·log4j
留院极客离心圆2 小时前
C++ 进阶笔记:栈内存 vs 堆内存
开发语言·c++
留院极客离心圆2 小时前
C++ 进阶笔记:宏
开发语言·c++·笔记
無限進步D2 小时前
关于高校C语言课程的学习方法
c语言·开发语言·学习方法·入门
星空露珠2 小时前
迷你世界UGC3.0脚本Wiki生物模块管理接口 Monster
开发语言·数据结构·算法·游戏·lua
星空露珠2 小时前
迷你世界UGC3.0脚本Wiki世界模块管理接口 World
开发语言·数据库·算法·游戏·lua
这是个栗子2 小时前
前端开发中的常用工具函数(四)
开发语言·javascript·ecmascript·find
格林威2 小时前
工业相机彩色图像采集:为什么我的图是绿色的?附海康/Basler/堡盟相机设置
开发语言·人工智能·数码相机·opencv·计算机视觉·c#·工业相机
阿贵---2 小时前
C++中的装饰器模式
开发语言·c++·算法