• <类与对象(上)>——《C++初阶》


    GIF:

     

     

     

    目录

    ​编辑

    一、类与对象

    1.面向过程与面向对象:

    2.声明类类型

    类的引入:

                      说明:

            3.类的定义

    3.1类的两种定义方式: 

    3.2通过C语言与C++实现栈对比:

     1.用C语言实现栈

    2.用C++实现栈

    4.类的访问限定符及封装

    4.1 访问限定符

    4.2 封装 

    5.类的作用域 

    6.类的实例化 

    7.类对象模型

    7.1 如何计算类对象的大小

    7.2 类对象的存储方式猜测

    7.3 结构体内存对齐规则

            8.this指针

    8.1 this指针的引出

             8.2 this指针的特性

    后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!                                                                       ——By 作者:新晓·故知


    一、类与对象

    1.面向过程与面向对象:

    • C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

    • C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

    2.声明类类型

    类的引入:

    C 语言中,结构体中只能定义变量,在C++ 中,结构体内不仅可以定义变量,也可以定义函数。

    1.  //在C语言中
    2.  struct ListNode
    3.  {
    4.   int val;
    5.   struct ListNode* next;
    6.  };  //C语言中最早typedef在分号前
    7.  ​
    8.  //在C++中
    9.  //最明显的标志
    10.  struct ListNode
    11.  {
    12.   int val;
    13.   //struct ListNode* next;   //写法1因为C++兼容C,
    14.   //ListNode* next;         //写法2因为struct被升级
    15.  
    16.  ​
    17.  };
    18.  //C++中一般不用再typedef
    19.  //c++兼容c,也有自己的语法
    20.  //c++有两种定义方法:
    21.  struct Student s1;
    22.  Student s2;
    1.  struct Student
    2.  {
    3.   void SetStudentInfo(const char* name, const char* gender, int age)
    4.   {
    5.   strcpy(_name, name);
    6.   strcpy(_gender, gender);
    7.   _age = age;
    8.   }
    9.  ​
    10.   void PrintStudentInfo()
    11.   {
    12.   cout << _name << " " << _gender << " " << _age << endl;
    13.   }
    14.  ​
    15.   //这里并不是必须加_
    16.   //习惯加上用来标识成员变量
    17.   //这里成员变量放在函数后面,是因为C++兼容C,且升级后的类是一个整体
    18.   //编译器不只是向上找,而是在这个整体里面找
    19.   char _name[20];
    20.   char _gender[3];
    21.   int _age;
    22.  };
    23.  int main()
    24.  {
    25.   Student s;
    26.   s.SetStudentInfo("lilei", "男", 23);
    27.   return 0;
    28.  }

    说明:

    • 这里并不是必须加_

    • 习惯加上用来标识成员变量

    • 这里成员变量放在函数后面,是因为C++兼容C,且升级后的类是一个整体

    • 编译器不只是向上找,而是在这个整体里面找

    • struct默认成员公有

    • class默认成员私有

    • 结构的缺陷: 可在结构体外对结构内的数据成员进行修改

    • C++兼容C struct的用法

    • C++同时对struct进行了升级,把struct升级成了类

    • 1.结构体名称可以作为类型

    • 2.里面可以定义

    3.类的定义

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

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

    2.类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

    3.1类的两种定义方式: 

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

    2. 声明放在.h文件中,类的定义放在.cpp文件中  

     在C++中,更多使用第二种方式

    3.2通过C语言与C++实现栈对比:

    栈:可通过数组实现或链表实现

     1.用C语言实现栈

     

    1. //用C语言定义栈
    2. struct Stack
    3. {
    4. int* _a;
    5. int _top;
    6. int _capacity;
    7. };
    8. //C语言数据和方法分离,定义数据再定义方法去操纵数据
    9. void StackInit(struct Stack* ps); //如果没有typedef,就必须这样写
    10. //分离的问题:
    11. //1.太过自由,无法更好的管理
    12. //2.有些不规范使用会直接操作内部成员,也可能会存在误用

    说明:

    1.C语言数据和方法分离,定义数据再定义方法去操纵数据

    2.分离的问题:
    (1)太过自由,无法更好的管理
    (2)有些不规范使用会直接操作内部成员,也可能会存在误用

    1. //C语言太过自由
    2. struct Stack
    3. {
    4. int* _a;
    5. int _top;
    6. int _capacity;
    7. };
    8. void StackInit(struct Stack* ps)
    9. {
    10. ps->_a = NULL;
    11. ps->_top = 0; //写法2:ps->_top=-1; 写法不同,top不同
    12. ps->_capacity = 0;
    13. }
    14. void StackPush(struct Stack* ps, int x)
    15. {}
    16. int StackTop(struct Stack*ps)
    17. {}
    18. int main()
    19. {
    20. struct Struct st;
    21. StackInit(&st);
    22. StackPush(&st, 1);
    23. StackPush(&st, 2);
    24. StackPush(&st, 3);
    25. StackPush(&st, 4);
    26. printf("%d\n", StackTop(&st));
    27. printf("%d\n",st._a[st._top]); //写法1:直接操作内部成员
    28. //printf("%d\n", st._a[st._top-1]); //写法2:直接操作内部成员,存在误用情况
    29. return 0;
    30. }

    2.用C++实现栈

    1. //用C++实现栈
    2. //1.数据和方法封装到一起,类里面
    3. //2.允许自由访问的设计成共有,不允许自由访问的设计成私有
    4. class Stack
    5. {
    6. public
    7. void Init()
    8. {}
    9. void Push(int x)
    10. {}
    11. void Top()
    12. {}
    13. private:
    14. int* _a;
    15. int _top;
    16. int _capacity;
    17. };

    C++中通过类实现低耦合

    1.数据和方法封装到一起,类里面
    2.允许自由访问的设计成共有,不允许自由访问的设计成私有

    3.封装是更严格的管理设计

    1. class Stack
    2. {
    3. public:
    4. void Init()
    5. {}
    6. void Push(int x)
    7. {}
    8. int Top()
    9. {}
    10. private:
    11. void Checkcapacity();
    12. private:
    13. int* _a;
    14. int _top;
    15. int _capacity;
    16. };
    17. int main()
    18. {
    19. Stack st;
    20. st.Init();
    21. st.Push(1);
    22. st.Push(2);
    23. st.Push(3);
    24. st.Push(4);
    25. cout << st.Top() << endl;
    26. return 0;
    27. }

    c++实现栈(框架):

     Stack.h:

    1. #pragma once
    2. //c++实现栈(框架)
    3. class Stack
    4. {
    5. public:
    6. //Init在类内定义
    7. //在类里面定义的函数默认是inline
    8. void Init()
    9. {
    10. _a = nullptr;
    11. _top = 0;
    12. _capacity = 0;
    13. }
    14. //Push和Pop在类内声明
    15. void Push(int x);
    16. void Pop();
    17. //实际上,短小函数可以直接在类内定义,长一点函数声明和定义分离
    18. private:
    19. //这里是声明,因为没有开辟空间
    20. int* _a;
    21. int _top;
    22. int _capacity;
    23. };

     说明:

    对于没有成员变量的类对象,编译会给他们分配1byte占位,表示对象存在过,类中仅有成员函数。

    Stack.cpp:

    1. #include"Stack.h"
    2. //c++实现栈(框架)
    3. //::域作用限定符
    4. //Push和Pop在类外面实现
    5. void Stack::Push(int x)
    6. {
    7. //...
    8. _top++;
    9. }
    10. void Stack::Pop()
    11. {}

     test.cpp:

    1. #include<iostream>
    2. #include"Stack.h"
    3. using namespace std;
    4. //c++实现栈(框架)
    5. int main()
    6. {
    7. Stack st1;
    8. st1.Init();
    9. cout << sizeof(st1) << endl;
    10. return 0;
    11. }

    4.类的访问限定符及封装

    4.1 访问限定符

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

    说明:
    > 三个关键字可以按任意次序出现任意次。
    > 类声明格式中的3个部分必须至少要有一个。
    > private若排第一时则可以省去。
    > 不能在类声明中对数据成员进行初始化。
    > 数据成员可以是任何数据类型, 但不能用auto, register, extern来进行说明。

    【访问限定符说明】
    1. public修饰的成员在类外可以直接被访问
    2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
    3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
    4. class的默认访问权限为private,struct为public(因为struct要兼容C)
    注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

    【面试题】

    问题:C++中struct和class的区别是什么?
    解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
    和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private

    4.2 封装 

    【面试题】 面向对象的三大特性:封装、继承、多态
    在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
    封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
    封装本质上是一种管理:类也是一样,我们使用类数据和方法都封装到一下。
    不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。

     

    5.类的作用域 

    类定义了一个新的作用域,类的所有成员都在类的作用域中在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
    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. }

    6.类的实例化 

    用类类型创建对象的过程,称为类的实例化
    1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
    2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
    3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

     

    7.类对象模型

    7.1 如何计算类对象的大小

    1. class A {
    2. public:
    3. void PrintA()
    4. {
    5. cout<<_a<<endl;
    6. }
    7. private:
    8. char _a;
    9. };
    问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

    7.2 类对象的存储方式猜测

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

    问题:对于上述两种存储方式,那计算机到底是按照那种方式来存储的?
    我们再通过对下面的不同对象分别获取大小来分析看下

     

    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) : __4_ sizeof(A2) : _1___sizeof(A3) : _1___

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

    7.3 结构体内存对齐规则

    1. 第一个成员在与结构体偏移量为0的地址处。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
    3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    【面试题】
    1. 结构体怎么对齐? 为什么要进行内存对齐
    2. 如何让结构体按照指定的对齐参数进行对齐
    3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

    8.this指针

    8.1 this指针的引出

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

    1. class Date
    2. {
    3. public :
    4. void Display ()
    5. {
    6. cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    7. }
    8. void SetDate(int year , int month , int day)
    9. {
    10. _year = year;
    11. _month = month;
    12. _day = day;
    13. }
    14. private :
    15. int _year ; // 年
    16. int _month ; // 月
    17. int _day ; // 日
    18. };
    19. int main()
    20. {
    21. Date d1, d2;
    22. d1.SetDate(2018,5,1);
    23. d2.SetDate(2018,7,1);
    24. d1.Display();
    25. d2.Display();
    26. return 0; }
    对于上述类,有这样的一个问题:
    Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
    C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
    1. class Date
    2. {
    3. public:
    4. void Print()
    5. {
    6. cout << _year << "-" << _month << "-" << _day << endl;
    7. }
    8. //处理结果,引入this指针,是一个隐含的形参,谁调用就传谁的地址
    9. //实参和形参的位置不能显式的显示出来
    10. //const在*之前修饰的是指向内容,在*之后修饰的指针本身
    11. //void Print(Date* this) //真正声明:void Print(Date* const this)
    12. //{
    13. // cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    14. //}
    15. //void Print()
    16. //{
    17. // cout << this << endl;
    18. // cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    19. //}
    20. void Init(int year, int month, int day)
    21. {
    22. _year = year;
    23. _month = month;
    24. _day = day;
    25. }
    26. //处理结果
    27. //隐含的this指针
    28. //void Init(Date*this int year, int month, int day) //真正声明:void Init(Date* const this int year, int month, int day)
    29. //{
    30. // this->_year = year;
    31. // this->_month = month;
    32. // this->_day = day;
    33. //}
    34. //void Init(int year, int month, int day)
    35. //{
    36. // const this = nullptr; //this指针本身不能被修改,因为它被const修饰,但指向的对象可以被修改
    37. // cout << this << endl;
    38. //this指向的对象可以被修改
    39. // this->_year = year;
    40. // _month = month;
    41. // _day = day;
    42. //}
    43. private:
    44. int _year; // 年
    45. int _month; // 月
    46. int _day; // 日
    47. };
    48. int main()
    49. {
    50. Date d1, d2;
    51. d1.Init(2022, 5, 1);
    52. d2.Init(2022, 7, 1);
    53. //this指针处理后实际形式,但显视这样调用会报错,编译器自动处理
    54. //d1.Init(&d1,2022, 5, 1);
    55. //d2.Init(&d2,2022, 7, 1);
    56. //虽然不能显视,但可以去用this
    57. cout << "d1:" << &d1 << endl;
    58. cout << "d2:" << &d2 << endl;
    59. //d1、d2调用的是同一个函数
    60. d1.Print();
    61. d2.Print();
    62. //this指针处理后实际形式,但显视这样调用会报错,编译器自动处理
    63. //d1.Print(&d1);
    64. //d2.Print(&d2);
    65. return 0;
    66. }
    67. //d1、d2调用的是同一个函数,在调用时会将函数处理,注释部分为处理结果

    说明:

    1.处理结果,引入this指针,是一个隐含的形参,谁调用就传谁的地址
    2.实参和形参的位置不能显式的显示出来
    3.const在*之前修饰的是指向内容,在*之后修饰的指针本身

    4.this指针本身不能被修改,因为它被const修饰,但指向的对象可以

    8.2 this指针的特性

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

    2. this指针可以为空

     解:

    1.一般是在,也有编译器会使用寄存器优化在在寄存器中。

    this是把对象的地址作为实参传递,是形参。而形参在栈帧。

    2.this指针可以为空

    空指针是一个存在的地址,但是预留出来的,不存储数据,不能去访问。

    1. //1.下面程序编译运行结果是? A.编译报错 B.运行崩溃 C.正常运行
    2. class A
    3. {
    4. public:
    5. void PrintA()
    6. {
    7. cout << _a << endl;
    8. }
    9. private:
    10. int _a;
    11. };
    12. int main()
    13. {
    14. A* p = nullptr;
    15. p->PrintA();
    16. //编译器内部处理:
    17. //p->PrintA(p);
    18. }
    1. //1.下面程序编译运行结果是? A.编译报错 B.运行崩溃 C.正常运行
    2. class A
    3. {
    4. public:
    5. void PrintA()
    6. {
    7. cout << _a << endl;
    8. //实际访问this指针解引用,但空指针,程序崩溃
    9. //cout << this->_a << endl;
    10. }
    11. private:
    12. int _a;
    13. };
    14. int main()
    15. {
    16. A* p = nullptr;
    17. p->PrintA();
    18. //编译器内部处理:
    19. //p->PrintA(p);
    20. }

     

    1. //2.下面程序编译运行结果是? A.编译报错 B.运行崩溃 C.正常运行
    2. class A
    3. {
    4. public:
    5. void Show()
    6. {
    7. cout << "Show()" << endl;
    8. }
    9. private:
    10. int _a;
    11. };
    12. int main()
    13. {
    14. A* p = nullptr;
    15. p->Show();
    16. }
    1. //2.下面程序编译运行结果是? A.编译报错 B.运行崩溃 C.正常运行
    2. class A
    3. {
    4. public:
    5. void Show()
    6. {
    7. cout << this << endl;
    8. cout << "Show()" << endl;
    9. }
    10. private:
    11. int _a;
    12. };
    13. int main()
    14. {
    15. A* p = nullptr;
    16. //虽然p是空指针,但传过去的给this指针,this指针可以为空,const this不能被修改,但可以初始化,p传参给this的过程完成初始化,传参不会报错
    17. p->Show(); //这里不是解引用,对象里面只有成员变量,这个函数不在对象里面,而是在一个公共代码区域,只需去找call地址,像普通函数调用一样
    18. p->_a; //这里是解引用,访问空指针,会报错
    19. }

     “p->_a”被编译器优化,已被忽略!

     

     

    后记:
    ●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
                                                                           ——By 作者:新晓·故知

  • 相关阅读:
    力扣每日一题-区域和检索 - 数组可修改
    《开源软件的影响力》
    git删除commit的历史大文件记录
    Linux------权限篇1)
    【IMX6ULL笔记】-- 从驱动到应用(基于Qt)- 串口
    SRRC认证测试项目?无线电型号核准测试
    B+树的定义以及查找
    二进制安装docker
    Hippy - 值得关注的免费开源跨端开发框架,由腾讯出品,支持将 JS 代码发布到安卓 / iOS / web
    蔟/块/页/扇区
  • 原文地址:https://blog.csdn.net/m0_57859086/article/details/125629208