【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 修饰的函数) 来操作对象。

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





相关文章
|
3天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
22 10
|
8天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
39 9
|
3天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
12天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以'\0'结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加'\0'。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
12天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `<`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
12天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
12天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。
|
12天前
|
安全 编译器 C++
【C++】string类的使用②(元素获取Element access)
```markdown 探索C++ `string`方法:`clear()`保持容量不变使字符串变空;`empty()`检查长度是否为0;C++11的`shrink_to_fit()`尝试减少容量。`operator[]`和`at()`安全访问元素,越界时`at()`抛异常。`back()`和`front()`分别访问首尾元素。了解这些,轻松操作字符串!💡 ```
|
12天前
|
存储 编译器 Linux
【C++】string类的使用②(容量接口Capacity )
这篇博客探讨了C++ STL中string的容量接口和元素访问方法。`size()`和`length()`函数等价,返回字符串的长度;`capacity()`提供已分配的字节数,可能大于长度;`max_size()`给出理论最大长度;`reserve()`预分配空间,不改变内容;`resize()`改变字符串长度,可指定填充字符。这些接口用于优化内存管理和适应字符串操作需求。
|
12天前
|
C++ 容器
【C++】string类的使用①(迭代器接口begin,end,rbegin和rend)
迭代器接口是获取容器元素指针的成员函数。`begin()`返回首元素的正向迭代器,`end()`返回末元素之后的位置。`rbegin()`和`rend()`提供反向迭代器,分别指向尾元素和首元素之前。C++11增加了const版本以供只读访问。示例代码展示了如何使用这些迭代器遍历字符串。