• 类和对象(上)


    目录

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

    2.类的引入

    3.类的定义

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

    4.1 访问限定符

    4.2 封装

    5.类的作用域

    6.类的实例化

    7.类的对象大小的计算

    8.类成员函数的this指针

    8.1 this指针引入

    8.2 this指针的特性

    9.C语言和C++实现Stack的对比


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

    面向对象和面向过程是两种不同的编程思想和编程范式。

    面向过程编程是一种以过程为中心的编程思想,它将问题分解为一系列的步骤,每个步骤都是一个函数或过程。程序的逻辑是按照步骤一步步执行的,数据和函数是分离的,即数据是被函数所操作的。面向过程的程序设计着重于解决问题的步骤和流程,注重程序的执行效率和资源利用。C语言是一种面向过程的编程语言,它的设计目标是为了编写高效的系统级程序。

    面向对象编程是一种以对象为中心的编程思想,它将问题分解为一系列的对象,每个对象都有自己的属性(数据)和方法(函数)。对象是具有状态和行为的实体,对象之间通过消息传递来进行交互。面向对象的程序设计着重于抽象和封装,注重程序的可重用性和扩展性。C++是一种支持面向对象编程的语言,它在C语言的基础上增加了面向对象的特性,例如类、对象、继承、多态等。

    面向对象编程相对于面向过程编程具有以下特点:

    1. 封装性:将数据和方法封装在对象中,通过访问控制来隐藏对象内部的实现细节。

    2. 继承性:通过继承关系实现代码的重用,并且可以在子类中扩展或修改父类的行为。

    3. 多态性:同一类型的对象可以具有不同的行为,通过多态可以实现代码的灵活性和可扩展性。

    4. 抽象性:将对象的共同特征抽象出来形成类,通过类来定义对象的行为和属性。

    总之,面向对象和面向过程是两种不同的编程思想,面向对象的编程范式更加灵活、可扩展和可维护,而面向过程的编程范式更加注重程序的执行效率和资源利用。

    2.类的引入

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

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

    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++中更喜欢用class来代替。

    3.类的定义

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

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

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

    类的两种定义方式:

    • 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。(注意:内联是建议性关键字 ,加上内联不一定成为内联函数)

    • 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::(::是C++中的作用域限定符,C++的类和命名空间一样,都是一个作用域。(凡是{}括起来的都是一个域)

    补充:C++的域

    在C++中,域(Scope)是指程序中声明的变量、函数和类的可见性和可访问性范围。域的作用是限制变量、函数和类的可见性,以防止名称冲突和混淆。

    C++中有以下几种不同的域:

    1. 全局域(Global Scope):全局域是整个程序范围内的域,其中声明的变量、函数和类可以在程序的任何地方访问。全局变量和全局函数可以在程序中的任何函数或块中直接访问。

    2. 命名空间域(Namespace Scope):命名空间是将相关的变量、函数和类组织在一起的一种机制。在命名空间中声明的变量、函数和类可以在该命名空间内访问,也可以通过命名空间限定符在其他地方访问。

    3. 类域(Class Scope):类域是类中声明的变量、函数和类的可见性范围。在类内部声明的变量和函数可以在类的任何成员函数中访问,包括类的构造函数和析构函数。

    4. 局部域(Local Scope):局部域是在函数循环代码块内部声明的变量的可见性范围。在局部域中声明的变量只能在该局部域内部访问,不能在其他局部域或全局域中访问。

    使用域的好处是可以将变量、函数和类封装起来,避免名称冲突,提高代码的可读性和可维护性。域还可以帮助控制变量和函数的可见范围,从而提供更好的封装和信息隐藏。

    C++确实非常看重域的概念。域的概念在C++中起到了至关重要的作用,它不仅可以限制变量、函数和类的可见性,还可以帮助提高代码的可读性、可维护性和安全性。

    以下是C++看重域的几个方面:

    1. 封装和信息隐藏:域可以帮助实现封装和信息隐藏的概念。通过将变量、函数和类声明在适当的域内,可以将其隐藏起来,并只暴露必要的接口,从而提供更好的封装。这样可以减少代码的依赖性,增加代码的模块化和可重用性。

    2. 避免名称冲突:域可以防止不同部分的代码中的变量、函数和类之间发生名称冲突。通过将不同的实体声明在不同的域内,可以确保它们的名称不会相互干扰。这样可以增加代码的可读性和可维护性,并减少出错的可能性。

    3. 灵活性和可扩展性:域允许在不同的作用域内声明相同名称的变量、函数和类。这使得可以在不同的上下文中使用相同的名称,而不会引发冲突。例如,在不同的命名空间中可以声明相同名称的函数,或者在类的成员函数中可以使用与类成员变量相同的名称。这提供了更大的灵活性和可扩展性。

    总之,域是C++语言中非常重要的概念,它可以帮助实现封装、信息隐藏、避免名称冲突以及提供灵活性和可扩展性。掌握域的使用方法和原则可以帮助编写更可靠、高效和易维护的代码。

    成员变量命名规则的建议:成员变量前面加个_:

    1. class Date
    2. {
    3. public:
    4. void Init(int year, int month, int day)
    5. {
    6. //左边的year是形参还是成员变量?
    7. year = year;
    8. month = month;
    9. day = day;
    10. }
    11. public:
    12. int year;
    13. int month;
    14. int day;
    15. };
    16. int main()
    17. {
    18. Date d;
    19. d.Init(2023, 10, 21);
    20. cout << d.year << ":" << d.month << ":" << d.day;
    21. return 0;
    22. }

    结果为三个是随机值。说明所有的变量都是形参。

    1. class Date
    2. {
    3. public:
    4. void Init(int year, int month, int day)
    5. {
    6. //左边的year是形参还是成员变量?
    7. _year = year;
    8. _month = month;
    9. _day = day;
    10. }
    11. public:
    12. int _year;
    13. int _month;
    14. int _day;
    15. };

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

    4.1 访问限定符

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

    【访问限定符说明】

    1. public修饰的成员在类外可以直接被访问

    2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

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

    4. 如果后面没有访问限定符,作用域就到 } 即类结束。

    5. class的默认访问权限为private,struct为public。(为了兼容C)

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

    4.2 封装

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

    在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?

    封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

    封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。

    在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

    5.类的作用域

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

    1. class Person
    2. {
    3. public:
    4. void SetPersonInfo(const char* name, const char* gender, int age);
    5. void PrintPersonInfo();
    6. private:
    7. char _name[20];
    8. char _gender[3];
    9. int  _age;
    10. };
    11. // 这里需要指定PrintPersonInfo是属于Person这个类域
    12. void Person::PrintPersonInfo()
    13. {
    14. cout << _name << " " << _gender << " " << _age << endl;
    15. }
    16. void Person::SetPersonInfo(const char* name, const char* gender, int age)
    17. {
    18. strcpy(_name, name);
    19. strcpy(_gender, _gender);
    20. _age = age;
    21. }
    22. int main()
    23. {
    24. Person p;
    25. const char* name = "Tom";
    26. const char* gender = "男";
    27. p.SetPersonInfo(name, gender, 18);
    28. p.PrintPersonInfo();
    29. return 0;
    30. }

    6.类的实例化

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

    1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它(C语言的结构体也是这样);比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。 类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。 谜语:"年纪不大,胡子一把,主人来了,就喊妈妈" 谜底:山羊

    2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。

      1. int main()
      2. {
      3. Person._age = 100;  // 编译失败:error C2059: 语法错误:“.”
      4. return 0;
      5. }
    3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

    Person man;类比 int a;类是一个类型,对象是一个变量。

    7.类的对象大小的计算

    类对象的存储方式猜测:

    • 对象中包含类的各个成员

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

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

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

    验证:

    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. {};
    16. int main()
    17. {
    18. cout << sizeof(A1) << endl;
    19. cout << sizeof(A2) << endl;
    20. cout << sizeof(A3) << endl;
    21. return 0;
    22. }

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

    复习:结构体内存对齐:详见:C语言自定义类型

    1. 第一个成员在与结构体变量偏移量为0的地址处。

    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

      对齐数= 编译器的默认对齐数 与 该成员大小 的较小值

      vs下的默认对齐数是8。

      Linux下的gcc编译器没有默认对齐数,对齐数就是成员自身大小。

    3. 结构体的总大小为最大对齐数的整数倍。(最大对齐数就是所有成员的对齐数中最大的值)

    4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    8.类成员函数的this指针

    8.1 this指针引入

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

    8.2 this指针的特性

    1. this指针是const指针,即this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。

    2. 只能在“成员函数”的内部使用。

    3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

    4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

    5. 成员函数实参和形参位置不能显示传递和接收this指针,但是可以在成员函数内部使用this指针。

    面试题:

    1. this指针存在哪里?

      this指针作为形参存储在函数栈帧中,有的编译器进行优化会存储在寄存器中。

    2. this指针可以为空吗?

      1. // 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
      2. class A
      3. {
      4. public:
      5. void Print()
      6. {
      7. cout << "Print()" << endl;
      8. }
      9. private:
      10. int _a;
      11. };
      12. int main()
      13. {
      14. A* p = nullptr;
      15. p->Print();
      16. return 0;
      17. }
      18. // 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
      19. class A
      20. {
      21. public:
      22. void PrintA()
      23. {
      24. cout<<_a<
      25. }
      26. private:
      27. int _a;
      28. };
      29. int main()
      30. {
      31.   A* p = nullptr;
      32.   p->PrintA();
      33.   return 0;
      34. }

    两道题是首先排除最扯淡的编译报错。

    以前C语言中一旦出现nullptr->a的操作一定会报错,那是因为nullptr->a相当于(*nullptr).a,出现了空指针解引用就会出现运行时出错。

    但是这里的p->PrintA()会对p指针进行解引用吗?答案是:不会!原因归纳为以下几点:

    1. 成员函数存储在公共代码区中,不在p指向的对象里面。成员变量存储在p指向的对象中。

      所以p不会解引用,但是访问成员变量p->_a时这里的p会解引用。

    2. 成员函数调用时,调用该函数的对象的地址作为第一个隐藏的参数(this指针)。

      所以p->PrintA() ==> p->PrintA(p)

    3. 成员函数内部一旦访问成员变量a就相当于this->a,this为nullptr时就会出现运行时错误。

      所以代码一正常运行,代码二运行崩溃。

    9.C语言和C++实现Stack的对比

    C语言:

    1. typedef int STDataType;
    2. typedef struct Stack
    3. {
    4. STDataType* a;
    5. int top;//栈顶元素的下一个位置
    6. int capacity;
    7. }Stack;
    8. void StackInit(Stack* pst)
    9. {
    10. assert(pst);
    11. pst->a = NULL;
    12. pst->top = 0;
    13. pst->capacity = 0;
    14. }
    15. void StackPush(Stack* pst, STDataType x)
    16. {
    17. assert(pst);
    18. if (pst->top == pst->capacity)
    19. {
    20. int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
    21. STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
    22. if (tmp == NULL)
    23. {
    24. perror("StackPush:");
    25. exit(-1);
    26. }
    27. pst->a = tmp;
    28. pst->capacity = newcapacity;
    29. }
    30. pst->a[pst->top] = x;
    31. pst->top++;
    32. }
    33. void StackPop(Stack* pst)
    34. {
    35. assert(pst);
    36. assert(pst->top > 0);
    37. pst->top--;
    38. }
    39. STDataType StackTop(Stack* pst)
    40. {
    41. assert(pst);
    42. assert(pst->top > 0);
    43. return pst->a[pst->top - 1];
    44. }
    45. bool StackEmpty(Stack* pst)
    46. {
    47. assert(pst);
    48. return pst->top == 0;
    49. }
    50. int StackSize(Stack* pst)
    51. {
    52. assert(pst);
    53. return pst->top;
    54. }
    55. void StackDestory(Stack* pst)
    56. {
    57. assert(pst);
    58. free(pst->a);
    59. pst->a = NULL;
    60. pst->capacity = 0;
    61. pst->top = 0;
    62. }
    63. int main()
    64. {
    65. Stack s;
    66. StackInit(&s);
    67. StackPush(&s, 1);
    68. StackPush(&s, 2);
    69. StackPush(&s, 3);
    70. StackPush(&s, 4);
    71. printf("%d\n", StackTop(&s));
    72. printf("%d\n", StackSize(&s));
    73. StackPop(&s);
    74. StackPop(&s);
    75. printf("%d\n", StackTop(&s));
    76. printf("%d\n", StackSize(&s));
    77. StackDestroy(&s);
    78. return 0;
    79. }

    可以看到,在用C语言实现时,Stack相关操作函数有以下共性:

    1. 每个函数的第一个参数都是Stack*

    2. 函数中必须要对第一个参数检测,因为该参数可能会为NULL

    3. 函数中都是通过Stack*参数操作栈的

    4. 调用时必须传递Stack结构体变量的地址

    结构体中只能定义存放数据,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。

    C++:

    1. typedef int STDataType;
    2. class Stack
    3. {
    4. public:
    5. void StackInit()
    6. {
    7. _a = nullptr;
    8. _top = 0;
    9. _capacity = 0;
    10. }
    11. void StackPush(STDataType x)
    12. {
    13. if (_top == _capacity)
    14. {
    15. int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    16. STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
    17. if (tmp == nullptr)
    18. {
    19. perror("StackPush:");
    20. exit(-1);
    21. }
    22. _a = tmp;
    23. _capacity = newcapacity;
    24. }
    25. _a[_top++] = x;
    26. }
    27. void StackPop()
    28. {
    29. _top--;
    30. }
    31. STDataType StackTop()
    32. {
    33. assert(_top > 0);
    34. return _a[_top - 1];
    35. }
    36. int StackSize()
    37. {
    38. return _top;
    39. }
    40. bool StackEmpty()
    41. {
    42. return _top == 0;
    43. }
    44. void StackDestory()
    45. {
    46. free(_a);
    47. _a = nullptr;
    48. _top = 0;
    49. _capacity = 0;
    50. }
    51. private:
    52. STDataType* _a;
    53. int _top;
    54. int _capacity;
    55. };
    56. int main()
    57. {
    58. Stack s;
    59. s.StackInit();
    60. s.StackPush(1);
    61. s.StackPush(2);
    62. s.StackPush(3);
    63. s.StackPush(4);
    64. cout << StackTop() << endl;
    65. cout << StackSize() << endl
    66. s.StackPop();
    67. s.StackPop();
    68. cout << StackTop() << endl;
    69. cout << StackSize() << endl
    70. s.StackDestory();
    71. return 0;
    72. }

    C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack *参数是编译器维护的,C语言中需用用户自己维护。

  • 相关阅读:
    IATF16949认证审核要点
    【PSO-RFR预测】基于粒子群算法优化随机森林回归预测研究(Matlab代码实现)
    【TensorRT】神经网络中的量化
    解读2022年度敏捷教练行业现状报告
    【我的OpenGL学习进阶之旅】解决OpenGL在使用glUniform系列api时出现了 GL_INVALID_OPERATION 1282错误
    【论文阅读】点云地图动态障碍物去除基准 A Dynamic Points Removal Benchmark in Point Cloud Maps
    代码随想录算法训练营第20天|654.最大二叉树、合并二叉树、700. 二叉搜索树中的搜索、98.验证二叉搜索树
    electron 应用开发优秀实践
    程序的编译和链接
    微信小程序- css相比,wxss区别?小程序关联微信公众号如何确定用户的唯一性?微信小程序中的用户ID(openid和unionid)
  • 原文地址:https://blog.csdn.net/qq_63981383/article/details/133968197