• 掌握C++魔法:深入解析类与对象(上篇)


    W...Y的主页 😊

    代码仓库分享 💕


    🍔前言:

     之前我们学习了从C语言转到C++后我们需要知道的一些关键改动与变化。今天我们就要学习C++独有的类与对象。在谈论类与对象之前我们先说一下什么是面向对象的C++,什么是面向过程的C语言。

    目录

    面向过程和面向对象初步认识

    类的引入

    类的定义

    类的访问限定符及封装

    访问限定符

    类的作用域

    类的实例化

    类对象模型

    如何计算类对象的大小 

     类对象的存储方式猜测 

    结构体内存对齐规则

    this指针(重点)

    this指针的引出

    this指针的特性


    面向过程和面向对象初步认识

    C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。举个实例来说,比如我们需要进行洗衣服的操作,如果用面向过程的角度去了解:

    C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完
    成。如果用面向对象的方式去看待洗衣服:

    面向对象编程(Object-Oriented Programming,OOP)和面向过程编程(Procedural Programming,POP)是两种不同的编程范式,它们在代码组织、数据处理和解决问题的方式上有明显的区别。以下是它们之间的主要区别:
    1. 基本思想:

    1.面向对象编程(OOP):OOP的核心思想是以对象为基本单位,将数据(属性)和操作(方法)封装在一起。程序的设计和实现侧重于对象之间的交互和协作,强调模块化和可重用性。
    2.面向过程编程(POP):POP的核心思想是以过程和函数为基本单位,程序的设计和实现主要关注算法和操作步骤的顺序。数据和函数是分离的。

    2. 数据处理:

    3.面向对象编程(OOP):数据和操作在类(class)内部封装在一起,类的实例(对象)通过方法来操作数据。这提供了数据隐藏和封装的机制。
    4.面向过程编程(POP):数据和函数通常是分开的,函数对数据进行处理,但数据的状态通常是全局可见的。

    3. 继承和多态:

    5.面向对象编程(OOP):OOP支持继承,允许创建新类基于现有类的属性和方法。多态是OOP的另一个关键概念,允许对象以不同的方式响应相同的消息或方法调用。
    6.面向过程编程(POP):POP通常没有继承和多态的概念。

    4. 代码复用:

    7.面向对象编程(OOP):OOP通过继承和组合等机制促进代码的复用,使得可以更轻松地构建和维护大型项目。
    8.面向过程编程(POP):在POP中,代码的复用主要依赖于函数的抽象和模块化,但不如OOP中的复用机制灵活。

    5. 可扩展性:

    9.面向对象编程(OOP):OOP倾向于更好地支持可扩展性,通过添加新类和方法来扩展程序。
    10.面向过程编程(POP):在POP中,为了添加新功能,可能需要修改现有代码或添加新函数,这可能不如OOP那么灵活。

    6. 程序设计思维:

    11.面向对象编程(OOP):OOP鼓励程序员思考问题的方式是从对象和其关系的角度出发,强调现实世界的建模。
    12.面向过程编程(POP):POP鼓励程序员从任务的角度出发,思考如何按照一系列步骤来解决问题。

    综上所述,面向对象编程和面向过程编程代表了不同的编程哲学和方法,适用于不同类型的问题和项目。选择哪种编程范式取决于问题的复杂性、团队的需求以及程序员的偏好。通常,在大型、复杂的软件项目中,面向对象编程更容易维护和扩展,而在一些小型、简单的任务中,面向过程编程可能更加直观和高效。

    类的引入

    C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:
    之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,
    会发现struct中也可以定义函数。

    当我们需要定义一个栈数据时,使用C语言时结构体就与其对应函数分开:

    1. #include
    2. #include
    3. #include
    4. #include
    5. typedef int STDataType;
    6. typedef struct Stack
    7. {
    8. STDataType* a;
    9. int top;
    10. int capacity;
    11. }ST;
    12. void STInit(ST* ps)
    13. {
    14. assert(ps);
    15. ps->a = NULL;
    16. ps->capacity = 0;
    17. ps->top = 0;
    18. }
    19. void STDestroy(ST* ps)
    20. {
    21. assert(ps);
    22. free(ps->a);
    23. ps->a = NULL;
    24. ps->top = ps->capacity = 0;
    25. }
    26. int main(void)
    27. {
    28. ST st;
    29. return 0;
    30. }

    而使用C++进行定义时,函数可以定义到结构体中:

    1. typedef int DataType;
    2. struct Stack
    3. {
    4. void Init(size_t capacity)
    5. {
    6. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    7. if (nullptr == _array)
    8. {
    9. perror("malloc申请空间失败");
    10. return;
    11. }
    12. _capacity = capacity;
    13. _size = 0;
    14. }
    15. void Push(const DataType& data)
    16. {
    17. // 扩容
    18. _array[_size] = data;
    19. ++_size;
    20. }
    21. DataType Top()
    22. {
    23. return _array[_size - 1];
    24. }
    25. void Destroy()
    26. {
    27. if (_array)
    28. {
    29. free(_array);
    30. _array = nullptr;
    31. _capacity = 0;
    32. _size = 0;
    33. }
    34. }
    35. DataType* _array;
    36. size_t _capacity;
    37. size_t _size;
    38. };
    39. int main()
    40. {
    41. Stack s;
    42. s.Init(10);
    43. s.Push(1);
    44. s.Push(2);
    45. s.Push(3);
    46. cout << s.Top() << endl;
    47. s.Destroy();
    48. return 0;
    49. }

     C++兼容C语言struct的所有用法都支持,struct升级成类,类名就是类型,Stack就是类型,不需要加struct。类里面定义的函数。上面结构体的定义,在C++中更喜欢用class来代替

    1. int main()
    2. {
    3. struct Stack s1;//可以这样定义。
    4. Stack s2;//这样更好
    5. return 0;
    6. }

    我们在C++中定义函数时也可以不用使用stack+作用名,直接作用名即可。因为C语言的函数都是全局函数,在哪都可以使用,如果在程序中有多个类似函数就会容易混淆,但是C++我们封装在一个关于Stack的类中,所以就可以不用。

    C++在使用类中的函数时,就如C语言中访问结构体内容一样方便。

    C++:

    1. int main()
    2. {
    3. Stact s1;
    4. s1.Init();
    5. s1.push(1);
    6. reuturn 0;
    7. }

     C语言:

    1. int main()
    2. {
    3. struct Stact s1;
    4. StackInit(&s1);
    5. StackPush(&S1, 1);
    6. reuturn 0;
    7. }

    这样一对比我们就可以直观的看出,C语言与C++哪一个方便了。

     struct不是C++纯正的类,接下来我们来学习一下class!

    类的定义

    1. class className
    2. {
    3. // 类体:由成员函数和成员变量组成
    4. };  // 一定要注意后面的分号

    class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
    号不能省略。

    类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者
    成员函数。

    类的两种定义方式:
    1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内
    联函数处理。

    2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

    一般情况下,更期望采用第二种方式。注意:上博客中放一起为了方便演示,大家后序工
    作中尽量使用第二种。 

    成员变量命名规则的建议:

    1. // 我们看看这个函数,是不是很僵硬?
    2. class Date
    3. {
    4. public:
    5. void Init(int year)
    6. {
    7. // 这里的year到底是成员变量,还是函数形参?
    8. year = year;
    9. }
    10. private:
    11. int year;
    12. };
    13. // 所以一般都建议这样
    14. class Date
    15. {
    16. public:
    17. void Init(int year)
    18. {
    19. _year = year;
    20. }
    21. private:
    22. int _year;
    23. };
    24. // 或者这样
    25. class Date
    26. {
    27. public:
    28. void Init(int year)
    29. {
    30. mYear = year;
    31. }
    32. private:
    33. int mYear;
    34. };

    所以我们进行命名时要区分参数与类中声明变量的区别。 

    其实说直白一点就是将刚刚的struct换成class即可。我们就成功定义了类。那我们将struct换成class后程序有无变化呢?

    继续是刚才的程序我们换成class进行运行:

     出现了非常之多的问题,这时为什么呢?下面就要引出一个知识。

    类的访问限定符及封装

    访问限定符

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

    【访问限定符说明】
    1. public修饰的成员在类外可以直接被访问
    2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
    3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
    4. 如果后面没有访问限定符,作用域就到 } 即类结束。
    5. class的默认访问权限为private,struct为public(因为struct要兼容C)

    pubic是在类里面外面都可以访问,而protected与private不能被访问。
    注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别 

    【面试题】

    问题:C++中struct和class的区别是什么?

    解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。 

     所以我们可以解决上述问题,为什么将struct换成class后就会报错了,因为class默认所有元素都是私有的。我们就可以进行解决:

    1. typedef int DataType;
    2. struct Stack
    3. {
    4. public:
    5. void Init(size_t capacity)
    6. {
    7. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    8. if (nullptr == _array)
    9. {
    10. perror("malloc申请空间失败");
    11. return;
    12. }
    13. _capacity = capacity;
    14. _size = 0;
    15. }
    16. void Push(const DataType& data)
    17. {
    18. // 扩容
    19. _array[_size] = data;
    20. ++_size;
    21. }
    22. DataType Top()
    23. {
    24. return _array[_size - 1];
    25. }
    26. void Destroy()
    27. {
    28. if (_array)
    29. {
    30. free(_array);
    31. _array = nullptr;
    32. _capacity = 0;
    33. _size = 0;
    34. }
    35. }
    36. private:
    37. DataType* _array;
    38. size_t _capacity;
    39. size_t _size;
    40. };
    41. int main()
    42. {
    43. Stack s;
    44. s.Init(10);
    45. s.Push(1);
    46. s.Push(2);
    47. s.Push(3);
    48. cout << s.Top() << endl;
    49. s.Destroy();
    50. return 0;
    51. }

    访问限定符的区间是从一个限定符到另一个限定符或者从限定符到类的结尾。

    那C++为什么要进行这样的操作呢?它是想优化什么问题呢?

    在C语言中我们想要取栈顶元素时,会有一个函数进行封装然后进行调用来达到取栈顶元素的效果:

    1. STDataType STTop(ST* ps)
    2. {
    3. assert(ps);
    4. assert(ps->top > 0);
    5. return ps->a[ps->top - 1];
    6. }

     但是其实如果我们清楚栈的实现逻辑,清楚top指向的位置,我们也可以进行直接去访问栈来获取栈顶元素:

    int num = s1->a[ps->size-1];

    但是这非常考验使用者的代码素养,素养高的人会使用函数素养低的人会一会调用函数一会访问数据。而C++有了类有了访问限定符就只能从函数下手,一般情况下为了安全数据都是私有不能直接访问的。

    类的作用域

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

    1. class Person
    2. {
    3. public:
    4. void PrintPersonInfo();
    5. private:
    6. char _name[20];
    7. char _gender[3];
    8. int  _age;
    9. };
    10. // 这里需要指定PrintPersonInfo是属于Person这个类域
    11. void Person::PrintPersonInfo()
    12. {
    13. cout << _name << " "<< _gender << " " << _age << endl;
    14. }

    类的实例化

    用类类型创建对象的过程,称为类的实例化
    1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
    有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个
    类,来描述具体学生信息。
    类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。
    2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

    1. int main()
    2. {
    3. Person._age = 100;  // 编译失败:error C2059: 语法错误:“.”
    4. return 0;
    5. }

    Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。

    3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设
    计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象
    才能实际存储数据,占用物理空间

     

    类对象模型

    如何计算类对象的大小 

    1. class A
    2. {
    3. public:
    4. void PrintA()
    5. {
    6.   cout<<_a<
    7. }
    8. private:
    9. char _a;
    10. };

    问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
    一个类的大小?

     类对象的存储方式猜测 

    对象中包含类的各个成员

    缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一
    个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么
    如何解决呢?

    代码只保存一份,在对象中保存存放代码的地址

    只保存成员变量,成员函数存放在公共的代码段

    问题:对于上述三种存储方式,那计算机到底是按照那种方式来存储的?

    1. // 类中既有成员变量,又有成员函数
    2. class A1 {
    3. public:
    4.   void f1(){}
    5. private:
    6.   int _a;
    7. };
    8. // 类中仅有成员函数
    9. class A2 {
    10. public:
    11.  void f2() {}
    12. };
    13. // 类中什么都没有---空类
    14. class A3
    15. {};

     sizeof(A1) : 4bite  sizeof(A2) : 1bite  sizeof(A3):1bite

    结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
    注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

    结构体内存对齐规则

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

    总结:其实与C语言中的结构体计算大小方法相同,不明白的可以自行观看博主之前博客。

    结构体大小计算方法icon-default.png?t=N7T8https://blog.csdn.net/m0_74755811/article/details/131757185?spm=1001.2014.3001.5501

    this指针(重点)

    this指针的引出

    我们先来定义一个日期类 Date

    1. class Date
    2. {
    3. public:
    4. void Init(int year, int month, int day)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. void Print()
    11. {
    12. cout <<_year<< "-" <<_month << "-"<< _day <
    13. }
    14. private:
    15. int _year;   // 年
    16. int _month;   // 月
    17. int _day;    // 日
    18. };
    19. int main()
    20. {
    21. Date d1, d2;
    22. d1.Init(2022,1,11);
    23. d2.Init(2022, 1, 12);
    24. d1.Print();
    25. d2.Print();
    26. return 0;
    27. }

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

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

    我们不能显示写this相关实参和形参!!! 

    this指针的特性

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


    以上就是本次全部内容,感谢大家观看,一键三连支持一下博主吧!!!

  • 相关阅读:
    GIS 与BIM 融合的应用领域 探索和研究
    QT系列第1节 QT中窗口使用简介
    RepVGG论文理解与代码分析
    【精选】2023网络安全学习路线 非常详细 推荐学习
    【面经】HTTP篇
    数据建模设计
    基于GAMS的电力系统优化分析
    Tuxera NTFS 2022 for Mac破解版百度网盘免费下载安装激活教程
    CUDA学习笔记6——事件计时
    Excel和Chatgpt是最好的组合。
  • 原文地址:https://blog.csdn.net/m0_74755811/article/details/133905786