C++入门第四篇----详解C++类和对象知识点(上)

简介: C++入门第四篇----详解C++类和对象知识点(上)

前言:

在本篇文章中,我们首先要接着前2篇中关于类和对象的内容继续往下,C++的类和对象应该是我开始学习程序以来第二个感觉到很难完全掌控和灵活使用的知识点(上一个是文件操作和预处理部分),故在这里我会花费大量的时间去理解和分析类和对象的语法特点和知识体系,有问题的地方倘若各位发现请及时指出。

首先我们依旧拿出这张大图,上面代表着类的6个默认的成员函数,他们的特点是:用户自己显式定义的时候会使用用户显式定义的,当用户没写的时候就会使用编译器自己默认生成的,这就是他们的共同特点。

1.赋值重载续operator=:

在上一篇文章中,我们介绍了运算符重载,它解决了C++对于自定义类型使用常规的内置类型的运算符的方式,而在类中默认的是赋值运算符重载。

注意细节:我们赋值重载的返回值应该仍然是类对应的类型的引用返回,这样才能满足赋值运算符连续赋值的功能,如下:

date& operator=(date& dd)//赋值重载函数
  {
    cout << 3 << endl;
    _a = dd._a;
    _b = dd._b;
    _c = dd._c;
    return *this;
  }

1.问题一:赋值运算符和拷贝构造有什么区别呢?

我们不妨拿下面的例子来看:

class date
{
public:
  date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省)
  {
    cout << 1 << endl;
    _a = a;
    _b = b;
    _c = c;
  }
  date(date& dd)//构造拷贝函数
  {
    cout << 2 << endl;
    _a = dd._a;
    _b = dd._b;
    _c = dd._c;
  }
  date& operator=(date& dd)//赋值重载函数
  {
    cout << 3 << endl;
    _a = dd._a;
    _b = dd._b;
    _c = dd._c;
    return *this;
  }
  ~date()//析构函数
  {
    cout << 4 << endl;
  }
private:
  int _a;
  int _b;
  int _c;
};
int main()
{
  date q1;
  date q2 = q1;
  cout << "----------------" << endl;
  date q3;
  cout << "----------------" << endl;
  q3 = q1;
  cout << "----------------" << endl;
  return 0;
}

在这里,我们重点来看q2=q1以及q3=q1的区别,为了表示我们的变量赋值过程中进入了哪些函数,我们在每个函数内部都打印一个数字作为标识。打印的结果如下:

我们发现一个特别有趣的现象,即q2=q1调用的不是我们的赋值重载运算符,而q3=q1调用的是我们的赋值重载运算符,出现这样问题的原因在于我们的编译器的优化,同时它也反映了赋值重载和拷贝的区别,首先,我们的q2是不存在的,我们是创建一个q2让其等于q1,常规的过程应该是首先创建一个q2对象然后调用赋值重载函数把q1的值给q2,但在仔细一想,我们的构造和赋值完全可以用一个拷贝构造代替,故编译器就为我们将其优化为了一个拷贝构造,再看q3,q3首先是自己先创建出来的对象,然后我们让q3=q1,这里由于不涉及到先创建q3再调用赋值重载的问题,故我们直接调用赋值重载函数赋值即可。

由此,我们总结出来:

拷贝构造是一个已经存在的对象去拷贝初始化另一个对象,而赋值重载是两个已经存在的对象,一个给另一个赋值,再选择调用哪个时也是优先考虑对象是否已经被创建!!!

2.问题二:默认的operator=是如何使用的呢?

倘若我们不写赋值重载,编译器会自动默认生成一个赋值重载函数,跟拷贝重载的的行为类似,默认的operator=对内置类型会完成值拷贝,而对于自定义类型会调用它的赋值重载函数(显式或非显式),

故根据这一条,我们总结出:不需要开辟空间的,只进行浅拷贝的就不需要写赋值重载函数,而涉及到开辟空间的,由于会出现多次释放的问题,故必须自己写赋值重载函数,让其进行深拷贝而不是简单的传值的浅拷贝!!!!!这条结论很关键,要反复思考形成一种行为的反射。
故我们可以总结:构造和析构行为类似,而拷贝构造和赋值重载行为类似。

3.问题三:运算符重载的使用意义以及如何更加规范的使用运算符重载!!!

举一个简单的例子,我们判断a>b可以写一个函数,那a<=b是不是就是a>b反过来呢?这就是运算符重载的一个重要的思路:复用,由于反复进行自定义类型的各种操作符,代码的不仅多而且十分冗长,故我们完全可以利用一个完整写下来的函数来进行逻辑复用,就像我上面举得例子一样:下面让我们来看一个实例:

bool Date::operator>(const Date& d) const//>运算符重载
{
  if (_year > d._year)
  {
    return true;
  }
  else if (_year == d._year && _month > d._month)
  {
    return true;
  }
  else if (_year == d._year && _month == d._month && _day > d._day)
  {
    return true;
  }
  else
  {
    return false;
  }
}
bool Date::operator==(const Date& d) const//==运算符重载
{
  if (_year == d._year && _month == d._month && _day == d._day)
  {
    return true;
  }
    return false;
}
bool Date:: operator != (const Date& d) const//!=运算符重载
{
  return !(*this == d);
}
bool Date::operator >= (const Date& d) const//>=运算符重载
{
  return *this > d || *this == d;
}
bool Date::operator < (const Date& d) const//<运算符重载
{
  return  !(*this >= d);
}
bool Date::operator <= (const Date& d) const//<=运算符重载
{
  return !(*this > d);
}

在这里,我仅仅写下了> == 的逻辑,就利用逻辑复用解决了剩下的全部比较操作符,这个复用思路在书写C++类的函数的时候都适用,应当反复思考成为自己的一种代码思路去进行,减少重复代码的书写,更加提高效率。

4.问题四:前置符号和后置符号如何在自定义类型赋值重载中区分呢?!!!

根据我们的赋值重载的概念可知,operator表示++,只能是operator++,同理–也是如此,那怎样才能做到区分呢?

我们可以联想到我们学到的函数重载的知识,我们是如何做到区分同名函数的呢?没错,通过让参数不同从而进行区分,故对于后置运算我们统一在参数的括号里面加一个对应的数据类型(int,且只能是int,别的数据类型是不允许的)如下:

Date& Date::operator++()//前置++
{
  *this +=1;
  return *this;
}
Date Date::operator++(int)//后置++,注意,由于前置加加和后置加加的运算符重载是相同的写法,故为了区分,我们采用重载函数的方式,给后置加加补上一个int类型,这样就可以区分前置和后置了
{
  Date q(*this);
  *this +=1;
  return q;
}
Date Date::operator--(int)//后置--
{
  Date q(*this);
  *this -=1;
  return q;
}
Date& Date::operator--()//前置--
{
  *this -=1;
  return *this;
}

我这里以这个例子,在这里我们发现我的后置统一在系数加了一个int,从而达到了区分前置和后置的区别,然后在这里,我直接对自定义类型加减是因为我前面实现了一个关于+ -的运算符重载,如下:

Date& Date::operator+=(int day)//日期+=天数
{
  if (day < 0)//为了处理我们输入一个负数,导致我们对应的天数出现负数的bug的情况
  {
    return *this -= (-day);
  }
    _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _year++;
      _month = 1;
    }
  }
  return *this;
}
Date& Date::operator-=(int day)// 日期-=天数
{
  if (day < 0)//同理,这里也是为了处理这种情况
  {
    return *this += (-day);
  }
  _day -= day;
  while (_day <= 0)
  {
    _month--;
    if (_month == 0)
    {
      _year--;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}

故这里不要被迷惑,自定义类型是不能对其自己进行运算符重载的,还是要自己写出对对应的函数,后续再使用前置和后置自定义计算的时候,只要像内置类型那样去使用即可。

!!!!在最后,需要注意的一点:赋值重载运算符是不能作为全局函数使用的,只能在类里书写,因为在全局书写则类里面也会自动生成一个,这就导致编译器不知道应该使用哪个好了,所以赋值重载必须作为成员函数使用!!!

5.函数运算符重载的一个重要作用!!!

倘若我们想要创建一个顺序表,并且按照C语言那种常规的遍历去打印一遍顺序表,我们会面临一个问题,如下:

class List
{
private:
  int* _arr1;
  int _size;
  int _capacity;
public:
  List(int size = 0, int capacity = 4)
    :_arr1(nullptr),
    _size(size),
    _capacity(capacity)
  {
    _arr1 = (int*)malloc(sizeof(int) * _size);
    if (_arr1 == nullptr)
    {
      perror("malloc failed");
      exit(-1);
    }
  }
  void Backpush(int x) 
  {
    _arr1[_size++] = x;
  }
};
#include<iostream>
using namespace std;
int main()
{
  List q1;
  q1.Backpush(1);
  q1.Backpush(2);
  q1.Backpush(3);
  q1.Backpush(4);
  for (int i = 0; i < q1._size; i++)
  {
    cout << q1._arr1[i] << endl;
  }
  return 0;
}

报错信息:

在这里我们想遍历一遍顺序表,但由于我们的private的限制,我们是没法在类外部直接访问里里面的成员的,那我们要是想访问又该如何修改呢?如下,让我们添加两个函数:

int size()
{
  return _size;
}
int& operator[](int i)
{
  return _arr1[i];
}

通过第一个函数我们可以带回来我们顺序表的元素个数,通过第二个函数的赋值重载我们可以把每一个元素带回来,这是我们之前直接访问所做不到的,但有了这两个函数我们就可以这样写:

for (int i = 0; i < q1.size(); i++)
{
  cout << q1[i] << " ";
}

结果为:

2.const成员:

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

常见的用法如下:

Date Date::operator-(int day) const// 日期-天数
{
  Date q(*this);
  q -= day;
  return q;
}

即在函数的声明的后面加上一个const

1.注意const成员函数其实就是在修饰隐藏的this指针,而不是你传入的其他参数,哪怕你再传入一个对应类的对象的引用来,你依旧可以修改它的成员的数值,但你是没法修改*this对应的成员数值的,如下

在这里,你会发现,我们传入一个对象的引用,哪怕是加上const,依旧是可以改变它里面的成员的,但当我们想改变this对应的成员的时候,就会为我们进行错误红线的提示,且报错的内容如下:

此时我们的
this是不能轻易改变的。

2.注意:权限的问题

注意,我们由const修饰的对象,它是不能进入到非const修饰的成员函数里面的,那样属于是将const对象的权限放大了,在前面的知识中我们知道,权限在计算机中只能缩小或者平替,但不能放大,但我们的非const的对象由权限规则,既可以进入const修饰的成员函数,也可以进入非const修饰的成员函数,如下:

class date
{
public:
  date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省)
  {
    cout << 1 << endl;
    _a = a;
    _b = b;
    _c = c;
  }
  date(date& dd)//构造拷贝函数
  {
    cout << 2 << endl;
    _a = dd._a;
    _b = dd._b;
    _c = dd._c;
  }
  date& operator=(date& dd)//赋值重载函数
  {
    cout << 3 << endl;
    _a = dd._a;
    _b = dd._b;
    _c = dd._c;
    return *this;
  }
  ~date()//析构函数
  {
    cout << 4 << endl;
  }
  void change(date& qq) const
  {
    qq._a = 12;
    qq._b = 13;
    qq._c = 20;
  }
private:
  int _a;
  int _b;
  int _c;
};
int main()
{
    date q1;
  date q2 = q1;
  cout << "----------------" << endl;
  date q3;
  cout << "----------------" << endl;
  q3 = q1;
  cout << "----------------" << endl;
  date q4;
  q1.change(q4);
  const date q4;
  q4 = q2;
  return 0;
}![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/370ab321096042e1bc6bec386cdfd1f7.png#pic_center)

我们看报错的结果如下:

在这里,q1使用由const修饰的函数change是可以的,但由const修饰的对象q4是没法使用没有由const修饰的成员函数operator=的,这便是权限的问题

**同理,我们在const修饰的成员函数内是不能调用非const的成员函数,这是由于我们的*this的对象本身倘若不是const还好,但倘若是const类型的对象,根本没法进入非const修饰的函数内部,这样就发生了权限的扩大。

但相应的,非const函数内部是可以存在由const修饰的成员函数的,因为进入非const成员函数一定是非const的成员对象,这类是可以进入const的成员函数的,属于是权限的缩小,**如下:

void Print()
{
  cout << 1 << endl;
}
void mycopy(date& qq) const
{
  Print();
}

报错信息是:

3.注意:const的位置不同,其代表的意思也是不同的

我们常见的const位置有两个地方,在一个函数中:

例如:

int func() const
{
     /;
}

或者是:

const int func() const
{
     //;
}

那么,这两个函数是相同的么?

显然,它们是不同的,其原因在于,第一个const的位置所修饰的是this指针的对象,而第二个是针对返回值进行处理的,第一个const保证了我们的this指针指向的对象是不能被修改的,而第二个指针保证了我们函数的返回值是不能被修改的,这是两种完全不同的情况,第一种暂且不说了,让我们继续分析第二种:

那么,第二种的const有必要加么?

我们之前已经学到,函数传值返回时,倘若数据出了作用域就被销毁的话,我们的返回值是会被临时拷贝一份临时的变量作为返回值返回,而由我们之前学到的,临时变量是具有常属性的,故其实本质上我们的返回值本身就是带上const的,再加上反而是多此一举,但倘若我们是引用返回,我们的返回值本身就是存在的,相应的它的自身属性也不会被改变,故这个时候我们要根据需要是否看这个引用返回的对象是否需要加上常属性const,从而让其不被修改

4.同一个函数。在后面加上const与不加的会构成重载么?

这个问题,我们首先回忆我们函数重载的知识点:在函数重载中,我们知道,编译器处理识别时是根据函数参数的不同来将其标识为不同的个符号,从而让函数构成重载,而在这里我们加上const与不加上const的这两种给了编译器识别的方式,故他们两个是可以构成重载的,而且不同的对象会根据自身的特点去选择调用哪个函数,比如const类型的对象就会调用const成员函数,而非const的对象就会优先调用非const修饰的成员函数。

那这样的函数重载有何意义呢?

大多数情况下,这样写的意义不大,但依旧拿我们的顺序表为例子:

const int& operator[](int i) const
  {
return _arr1[i];
  }
  int& operator[](int i)
  {
return _arr1[i];
  }
  for (int i = 0; i < q1.size(); i++)
{
  q1[i]++;
  cout << q1[i] << " ";
}
for (int i = 0; i < q1.size(); i++)
{
  q1[i]++;
}

你会发现,我们的q1[i]++变的可以被修改了,按理来说,返回const类型的引用应该是不能被修改的,但由于我们重载了一个operator[]的函数,无论是能否改变的q1[i],它都会自动匹配到对应的重载函数中,故对于读和写分离的函数来说,写两个重载一个只读一个只写是最为合适的。

3.取地址&运算符重载和const修饰的&运算符重载(不是特别重要)

正如我在前面所说的,取地址运算符重载也是默认的成员函数,编译器可以默认生成,而我们只需要直接使用即可,故我们平时根本不需要显式写出取地址运算符重载函数,但倘若你不想让他人通过取地址符号找到对应的地址,我们就可以显式实现,如下:

int* operator&()
{
  return nullptr;
}

这样,一旦使用取地址符号,就会取到空指针而不是本来的自定义类型的指针了。

以上就是我们的类和对象的6种默认函数的全部,下面我们让我们讲一讲类和对象的一些其他的知识点:

目录
相关文章
|
1天前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
1天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
1天前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
4天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
70 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
41 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4