【C++】类和对象 (上篇)(1)

简介: 【C++】类和对象 (上篇)(1)

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

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题;而C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。我们以洗衣服为例。

面向过程 – 逐步求解问题:

2020062310470442.png

面向对象 – 通过对象之间的交互解决问题:

2020062310470442.png

又比如我们的外卖系统:面向过程关注的是顾客应该如何下单、商家应该如何做出菜品、骑手应该如何将外卖送达;而面向对象关注的是顾客、商家、骑手这四个对象之间的交互,比如顾客下单后商家出餐,然后骑手送餐,而不必关心顾客如何下单、商家如何出餐、骑手如何送达这类面向过程的问题。

二、类的引入

在C语言中我们学习了结构体,知道了结构体可以定义某一种类型,但是不能定义具体的对象;以下面的结构体为例:

struct Student
{
  char name[20];
  char id[11];
  int weight;
  int height;
};
int main()
{
  struct Student stu1 = { "zhangsan", "2202101001", 60, 180 };
}

struct Student 只是定义了学生这种类型,而我们需要用这种类型来创建出具体的学生,比如张三李四;在C++中,学生类型被简称为 “类”,而具体的学生则被称为 “对象”;


但是我们知道,一个对象除了具有自身的属性 (数据) 之外,还应该拥有相应的方法 (行为),比如学生除了姓名、学号、体重、身高这些属性之外,还应该具有吃饭、睡觉、学习、娱乐等行为;


但是C语言结构体中只能定义变量,不能定义函数 (方法),所以C++对C语言的结构体进行了升级 – 在C++中,结构体内不仅可以定义变量,也可以定义函数。比如,之前在数据结构初阶中,我们用C语言方式实现的栈,结构体中只能定义 top、capacity、a 这些变量,而入栈、出栈、初始化这些函数只能在结构体外部定义;而使用C++我们就可以直接将这些函数定义在结构体内部:

//成员函数与成员变量都定义在结构体中
struct Stack
{
  //成员函数
  //初始化
  void Init(int N = 4)  //缺省参数 -- 初始化空间大小
  {
    _data = (int*)malloc(sizeof(int) * N);
    if (_data == nullptr)
    {
      perror("malloc fail\n");
      exit(-1);
    }
    _top = 0;
    _capacity = N;
  }
  //入栈
  void Push(int x)
  {
    if (_top == _capacity)
    {
      int* tmp = (int*)realloc(_data, sizeof(int) * _capacity * 2);
      if (tmp == nullptr)
      {
        perror("realloc fail\n");
        exit(-1);
      }
      _data = tmp;
      _capacity *= 2;
    }
    _data[_top++] = x;
  }
  //取栈顶的数据
  int Top()
  {
    return _data[_top - 1];
  }
  //销毁栈
  void Destroy()
  {
    free(_data);
    _data = NULL;
  }
  //成员变量(属性)
  int* _data;
  int _top;
  int _capacity;
};

同时,C++结构体直接使用 structName 代表类,而不用加 struct 关键字,但是C++兼容C语言结构体的全部用法,使用我们使用 struct + structName 的方式定义变量也是没问题的:

typedef struct SListNode
{
  SListNode* next;  //SListNode 可以直接代表这个类,所以此处可以不用加 struct
  int data;
}SL;
int main()
{
  //C语言用法
  struct SListNode* sl1;
  SL* sl2;
  //C++用法
  SListNode* sl3;
}


2020062310470442.png

最后,在C++中更喜欢用 class 来代替 struct,并且把变量称为属性/成员变量,把函数称为成员函数/成员方法。

三、类的定义

class className
{
   //... 
};

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

类的定义方式

C++类一共有两种定义方式:

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

2020062310470442.png

2、类声明放在.h文件中,成员函数定义放在.cpp文件中 (注意:成员函数名前需要使用类名+域限定符):

2020062310470442.png

类定义的两个惯例1、类的成员变量使用修饰符修饰 – 与C语言结构体不同,由于类中可以同时定义变量和函数,所以函数的形参与类成员变量就可能会发生冲突,这种情况在 Init 函数中十分常见,如下:

class Date
{
public:
  void Init(int year, int month, int day)
  {
    year = year;
    month = month;
    day = day;
  }
private:
  int year;
  int month;
  int day;
};

Init 函数的形参和类成员变量相同,这就导致我们初始化赋值的不确定性,当然我们也可以使用类名+域作用限定符或者this指针来解决这个问题:

void Init(int year, int month, int day)
{
    Date::year = year;
    Date::month = month;
    Date::day = day;
}
void Init(int year, int month, int day)
{
    this->year = year;
    this->month = month;
    this->day = day;
}

但是这样显然比较麻烦,所以在C++中有一个惯例 – 成员变量使用某种修饰符来修饰,其中常见的有四种:_menber、menber_、m_menber、mMenber,前面两种是在成员变量前/后加一个下划线_,第三种m_表示此变量是成员变量,最后一种m表示成员变量,然后不使用_,使用小驼峰;我习惯于第一种方式,所以可以看到我前面类中的成员变量都会有一个前_。

class Date
{
public:
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

2、成员函数定义在成员变量前面 – C语言编译器寻找变量的规则是先到前面去找,然后再到全局去找,所以在C语言中变量必须定义在函数前面,才可以在函数中使用该变量;但是C++编译器不一样,C++编译器会把类看作一个整体,当我们使用一个变量时,它会到整个类中去寻找,然后再到全局去寻找;所以在C++中,我们是可以将成员变量定义成员函数后面的;


上面解释了成员函数定义在成员变量之前的可行性,下面我借用 《高质量C/C++编程》中的解释来阐述为什么要将成员函数定义在成员变量前面:

2020062310470442.png

四、类的访问限定符及封装

访问限定符

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

2020062310470442.png

访问限定符说明:


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

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

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

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

class 的默认访问权限为 private,struct 为 public (因为struct要兼容C);


访问限定符的存在使得用户不能直接修改类中的成员变量,而是只能使用我们提供的特定接口,让类中的数据更加安全,也让用户使用类的方式更加规范。

注意:访问修饰限定符限定的只是类外的访问权限,类内可以随意访问;并且访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

2020062310470442.png

面试题

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


回答:C++需要兼容C语言,所以C++中 struct 可以当成结构体使用;另外C++中 struct 还可以用来定义类,和class定义类是一样的,区别是 struct 定义的类默认访问权限是 public,class 定义的类默认访问权限是 private。(注意:在继承和模板参数列表位置上 struct 和 class 也有区别,只是我们暂时还没学习)

封装

面向对象有很多特性,其中最出名的是:封装、继承和多态;在类和对象阶段,我们主要研究类的封装特性,那什么是封装呢?

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

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

2020062310470442.png

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

在数据结构初阶时,我们曾用C语言来实现栈,其中关于返回栈顶元素的函数接口 – Top就很好的体现了封装的作用:

由于C语言没有访问限定符,也没有封装的概念,所以对于取得栈顶元素就有了两种方法 :一是通过Top函数接口,二是直接访问data数组;


但是这里就出现了一个问题 – 结构体成员top是指向栈顶,还是指向栈顶的下一个位置是不确定的,其取决于Init函数;当 top 被初始化为-1时,top指向栈顶元素;而当其被初始化为0时,则指向栈顶的下一个元素;


所以可能就会出现这样一种情况:用户没有使用Top函数提供的接口,而是直接访问data数组,导致取出的栈顶元素是一个随机值;这种情况在现实中是经常出现的,甚至在有的教材中都是如此;


但是C++就不会出现这种情况,因为C++类成员变量通常都会用 private 修饰,用户不能直接访问类中的数据,只能通过特定的接口 (用 public 修饰的函数) 来操作对象。

在我们现实生活中对于各种文化旅游景点的设置也体现了封装 – 文物用展柜封装起来,使得游客 (类外) 不能直接接触文物,只能是文物相关各种人员 (类内) 才能直接接触文物,从而即达到了观赏的目的,也避免了文物被破坏。





相关文章
|
1天前
|
编译器 C语言 C++
|
1天前
|
编译器 C++
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
|
4天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
4天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
4天前
|
C语言 C++
【C++】日期类Date(详解)③
该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
|
4天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`>`, `==`, `<`, `<=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
4天前
|
定位技术 C语言 C++
C++】日期类Date(详解)①
这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
|
4天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
4天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
4天前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。