问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

【C++篇】类与对象(上篇):从面向过程到面向对象的跨越

创作时间:
作者:
@小白创作中心

【C++篇】类与对象(上篇):从面向过程到面向对象的跨越

引用
1
来源
1.
https://cloud.tencent.com/developer/article/2509280

本文是关于C++面向对象编程的基础教程,详细介绍了从面向过程到面向对象的转变,以及类与对象的基本概念。内容包括面向过程与面向对象的区别、类的定义、访问限定符与封装、类的作用域与实例化、对象的内存布局以及this指针的原理与使用。

前言

本文涵盖了从面向过程与面向对象的区别,到类的定义、访问限定符、封装、作用域、实例化、对象大小计算,以及this指针等内容。

一、 面向过程 vs 面向对象

  • 面向过程(C语言):关注解决问题的步骤,通过函数逐步实现。
  • 面向对象(C++):关注对象之间的交互,将问题拆分为多个对象协作完成。

核心区别:面向对象通过对象交互隐藏细节,提高代码复用性和可维护性。

二、类

  1. 类的引入

C语言的结构体:只能定义成员变量。
C++的类(class/struct):可以定义成员变量和成员函数。

// C++实现栈(使用class)
class Stack {
public:
    void Init() { /* 初始化逻辑 */ }
    void Push(int data) { /* 压栈逻辑 */ }
private:
    int* _array;
    int _size;
};
  1. 类的定义
class className
{
// 类体:由成员函数和成员变量组成
};  // 一定要注意后面的分号
  • class 为定义类的关键字,
  • ClassName 为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
  • 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方式

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

💡:这是更推荐的定义方式,因为可以提高代码可读性和编译效率。

成员变量命名规则的建议:在给成员变量命名时,在其前面加一个_或者一个m,原因:避免与函数形参重命名导致的可读性差的问题。

  1. 类的访问限定符与封装

3.1 访问限定符

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

  • public修饰的成员在类外可以直接被访问
  • protected/private修饰的成员在类外不能直接被访问
  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  • 如果后面没有访问限定符,作用域就到}即类结束
  • class的默认访问权限为privatestructpublic(因为struct要兼容C)

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

3.2 封装

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

什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
其实封装本质上是一种管理,让用户更方便使用类。

封装的意义:将数据与操作结合,隐藏实现细节,仅暴露接口。

class Date {
public:
    void SetYear(int year) { _year = year; } // 对外提供接口
private:
    int _year; // 隐藏实现细节
};

举两个例子:

  1. 汽车:汽车是一个十分复杂的一个设备,其内部的复杂系统原理我们并不需要去了解,因为我们不是要造车,而是要开车。只需要会用方向盘、挂挡和油门刹车,它们就类似于封装暴露出来的接口,剩余的都被封装保护起来了,我们看不见摸不着。

  2. 计算机:对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。

  3. 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。

  • 类作用域:成员函数在类外定义时需指定作用域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int  _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
 }

实例化:类像“设计图”,对象是“具体建筑”。

Person p;         // 实例化对象
p._name = "Tom";  // 对象占用实际内存

三、对象

  1. 类的实例化(定义对象)

用类类型创建对象的过程,称为类的实例化

  1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

💡:定义一个类,并没有给其对象开辟内存空间,实例化才是定义对象和开辟内存空间
一个十分形象的比方:类是一张“房子的设计图”,对象是一座“房子”,实例化就是“用设计图造一座房子”

  1. 类对象模型

计算类对象的大小
计算方式与计算结构体大小方法相同,都是用结构体对齐来计算的。

对象大小计算规则

  • 仅计算成员变量之和,遵循内存对齐规则。
  • 成员函数不纳入计算
  • 空类大小为1字节(占位标识)。

内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  • VS中默认的对齐数为8
  1. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
class A {
    char _a;    // 1字节(对齐到1)
    int _b;     // 4字节(对齐到4)
};  // 总大小:8字节(1+3填充+4)

具体计算方法不过多赘述了,存有疑问的话建议阅读我过去的文章:高阶C语言|和结构体与位段的邂逅之旅

  1. this指针的奥秘

3.1 this指针的引出

作用:隐式指向调用成员函数的对象,解决“如何区分不同对象”的问题。

现在看不懂没关系,我们来看一段代码: 我们定义一个日期类Date

#include<iostream>
using namespace std;
class Date
{
    public:
    void Init(int year, int month, int day)
    {
        _year = year;
        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;
}

对于上述类,我们会有这样的一个问题:
Date类中有InitPrint两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

如果将this指针显示出来,其实是这样的:

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

3.2 this指针的特性

  1. this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
  2. 只能在“成员函数”的内部使用
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

💡:this不能在形参和实参显示传递,但是可以在函数内部显示使用

  • this指针存在哪里呢?因为this本质是成员函数的形参,所以它必然存储在栈区
  • this指针可以为空吗?形参当然可以为空,但是要注意成员函数不能对其解引用操作,否则会运行崩溃。

总结

面向对象的核心在于封装,通过类将数据与方法结合,隐藏细节并提供接口。理解this指针、内存对齐和访问控制是掌握C++类与对象的关键。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号