【C++初阶:类和对象(中篇)】构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载 上

简介: 【C++初阶:类和对象(中篇)】构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

文章目录

【写在前面】

这篇文章是类和对象的核心知识

一、类的6个默认成员函数

class Date {};

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面 6 个默认成员函数。

注意,这里其实严格上有 8 个默认成员函数,其余 2 个是 C++11 的,后面再学,其次 C++98 这里有 2 个是不太重要的,提一下即可。

二、构造函数

💦 概念

❗ 场景 ❕

class Stack
{
public:
  void Init()
  {
    _a = nullptr;
    _top = _capacity = 0; 
  }
  void Push(int x);
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack s1;
  s1.Push(3);
  return 0;
}

📝 说明

C 语言中,可能会忘记调用 Init 函数,然后一顿错误操作。这时 C++ 就能做到对象定义出来就初始化 —— 构造函数。

可以认为构造函数的出现就是为了替代 Init。

❗ 利用构造函数替代 Init ❕

class Stack
{
public:
  Stack()//1.函数名和类名相同//2.无返回值
  {
    _a = nullptr;
    _top = _capacity = 0; 
  }
  Stack(int capacity)
  {
    _a = malloc(sizeof(int)*capacity);
    if(_a == nullptr)
    {
      cout << "malloc fail" << endl;
      exit(-1); 
    }
    _top = 0;
    _capacity = capacity;
  }
  void Push(int x);
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack s1;//3.对象实例化时编译器自动调用对应的构造函数
  Stack s2(10);//4.构造函数可以重载
  s1.Push(3);
  return 0;
}

📝 说明

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

💦 特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

1️⃣ 函数名与类名相同。

2️⃣ 无返回值。

3️⃣ 对象实例化时编译器自动调用对应的构造函数。

4️⃣ 构造函数可以重载。


接下来我们就来理解几个概念用 ❓ ❔ 包含


❓ 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 ❔

class A
{
public:
  A()
  {
    _a1 = 0;
    _a2 = 1;
    cout << "A()" << endl;
  }
private:
  int _a1;
  int _a2;
};
class Date
{
public:
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;  
  }
private:
  //内置类型/基本类型:int、char、指针 ... ———— 不会初始化
  int _year;
  int _month;
  int _day;
  //自定义类型:struct/class ———— 调用它的无参构造函数初始化
  A _a;
};
int main()
{
  Date d1;//对象实例化时自动调用构造函数
  d1.Print();//再调用Print函数
  return 0;
}

📝 说明:


不是说我们不写构造函数,编译器会自动生成一个无参的默认构造函数 ❓

这也是早期 C++ 设计留下的一个缺陷,有些编译器会把这初始化为 0,1,1

C++ 把我们的变量分成内置类型 (int、char、指针 …) 和自定义类型 (struct/class)

我们不写构造函数,编译器默认生成构造函数,但是编译器做了一个偏心的处理

1、内置类型不会初始化

2、自定义类型它会调用它的无参构造函数初始化

其实这块内容算是 C++ 在早期设计时的语法缺陷,这种偏心的处理导致语法反而复杂了,后面也发现了这个问题,但是为时已晚不能修改,因为 C++ 要向前兼容。好多书上也没有讲清楚,这也是很多人说 C++ 难学的地方。

关于编译器生成的默认构造函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认构造函数,但看起来默认构造函数又没什么用 ❓

这里说看起来没什么用主要体现在 d1 调用了编译器生成的默认构造函数,但是 _year、_month、_day 依然是随机值。

C++ 把类型分成内置类型 (基本类型) 和自定义类型,这里只是说它不会处理内置类型,而自定义类型它会去调用它的无参构造函数

到了 C++11 时 C++ 针对这里的情况做了补充 ❓

C++11 时,语法委员会针对这里打了一个补丁,也就是在变量声明中加上缺省值。

这个补丁雀实会给很多初学者造成困扰,很多人会认为它是初始化,其实它是缺省值。

class A
{
public:
  A()
  {
    cout << "A()" << endl;
  }
private:
  int _a1;
  int _a2;
};
class Date
{
public:
  Date()
  {
    _year = 2022;//只初始化_year,其它默认
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;  
  }
private:
  //C++11 缺省值
  int _year = 0;
  int _month = 1;
  int _day = 1;
  A _a;
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

📝 说明

就同函数的缺省参数一样。如果构造函数里没有初始化,那么对于内置类型它会使用缺省值初始化

❓ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数 (不传参数就可以调用的那个函数) ❔

class Date
{
public:
  Date()
  {
    _year = 0;
    _month = 1;
    _day = 1;
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;//Date d1(3);ok
  return 0;
}

📝 说明

如上两个构造函数 Date 本来是可以构造函数重载的,但是他们不能同时存在 ~ Why ?❓

因为编译器调用的时候会出现歧义,一般情况下我们推荐写一个全缺省的构造函数,因为它比较好用。

这里有一个误区:有的童鞋会认为,我们不写,编译器默认生成的构造函数就叫做构造函数,这个是不对的,它还有无参的构造函数和全缺省的构造函数都是默认构造函数。

❓ 成员变量的命名风格 ❔

class Date
{
public:
  Date(int year)//不好的命名风格
  {
    year = year;  
  }
  Date(int year,int month)//好的命名风格
  {
    _year = year;
    _month = month;
  }
private:
  int year;
  int _year;
  int _month;
};
int main()
{
  Date d1(1);
  Date d2(1, 2);
  return 0;
}

📝 说明

当然这样不仅仅是规范的问题,还可能会造成运行时错误。

执行 Date d1 (1) ; 后,year 的值是 1 还是随机值 ❓

站在编译器的角度 this->year = year,所以是 1 ???

NO ~~,实际上在 Date 里它会采用就进原则,也可以认为是局部优先。

当 year = year 时,它会先找到形参,然后就变成了自己赋值给自己了。

当 _year = year 时,它先找局部变量,没找到,它会再外面的域搜索,找到了,编译器就会在前面加 this。

或者直接 this->year = year 但不推荐。

三、析构函数

💦 概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的 ???

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

class Date
{
public:
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;  
  }
  ~Date()
  {
    cout << "~Date()" << endl;//证明我来过
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  return 0;
}

📝 说明

像 Date 这样的类是不需要析构函数的,因为它内部没有什么资源需要清理

💦 特性

析构函数是特殊的成员函数。

其特征如下:

1️⃣ 析构函数名是在类名前加上字符 ~。

2️⃣ 无参数无返回值 (不支持重载)。

3️⃣ 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

4️⃣ 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。


❓ 析构函数有什么意义 ❔

析构函数具体有什么意义要看具体是什么类,哪些类的析构函数是有意义的呢 ❓

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int)*capacity);
    if(_a == nullptr)
    {
      cout << "malloc fail" << endl;
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  ~Stack()
  {
    free(_a);
    _a = nullptr;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack st;
  return 0;
}

📝 说明

在之前我们写栈时都需要写一个 Destroy 在程序结束前销毁动态开辟的内存,很多童鞋可能在使用完动态开辟的内存没有 Destroy 导致后面的一些内存泄漏的操作。而析构函数的出现就是为了解决这种场景的,对象实例化后,同构造函数一样,它不需要我们主动调用,它是在对象生命周期结束后自动调用,注意析构函数没有参数所以不能重载。

构造函数是为了替代 Init,析构函数是为了替代 Destroy。

一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数 ❓

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int)*capacity);
    if(_a == nullptr)
    {
      cout << "malloc fail" << endl;
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack st;
  return 0;
}

📝 说明

与构造函数类似,我们不写,编译器默认生成的构造函数也会做偏心的处理

1、内置类型成员不处理

2、自定义类型成员会去调用它的析构函数

为什么内置类型不处理 ❓

因为它不好处理,如果这个指针是一个文件指针,那你也要去 free 吗 ???

那对于什么样的类可以不写析构呢或者它的价值是什么 ❓

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int)*capacity);
    if(_a == nullptr)
    {
      cout << "malloc fail" << endl;
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  ~Stack()
  {
    free(_a);
    _a = nullptr;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
class MyQueue
{
private:
   Stack _pushST;
   Stack _popST;  
};
int main()
{
  MyQueue mq;
  return 0;
}

📝 说明

这是之前的一个题 —— 两个栈实现队列

现在对于 MyQueue,我们可以不写构造函数和析构函数,让编译器自动生成构造函数和析构函数也可以 Init 和 Destroy,具体如下:

1、初始化 _pushST 和 _popST

2、销毁 _pushST 和 _popST

💨 由以上就可以看出编译器默认生成的析构函数也是有价值的

四、拷贝构造函数

💦 概念

在现实生活中有双胞胎的存在,那么我们的对象也可以有。

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  //Date d3(d2);
  //Date(Date d)
  //{
  //  _year = d._year;
  //  _month = d._month;
  //  _day = d._day;
  //}
  //Date d3(d2);
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  //Date d3(d2);
  Date(const Date* p)
  {
    _year = p->_year;
    _month = p->_month;
    _day = p->_day;
  } 
  void Print()
  {
    cout << _year << "/"<< _month << "/" << _day << endl; 
  } 
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  Date d2(2022, 1, 1);
  d1.Print();
  d2.Print();
  //拷贝复制一个d2对象
  Date d3(d2);
  d3.Print();
  //指针也可以,但不好
  Date d4(&p2);
  d4.Print();
  return 0;
}

📝 说明

error C2652:非法的复制构造函数,第一个参数不应是 “Date”。

这里会引发一个无穷递归的现象,只是语法进行了强制检查,所以它由运行时错误转向了编译时错误。

怎么解决 ❓

1、这里用引用解决了问题,因为形参是实参的别名,即 d 是 d2 的别名

   注意如果引用传参,不是做输出型参数,最好加 const

2、当然也能使用指针解决,但并没有引用好用

💦 特性

拷贝构造函数也是特殊的成员函数,其特征如下:

1️⃣ 拷贝构造函数是构造函数的一个重载形式。

2️⃣ 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

❓ 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝 ❔

class AA
{
public:
  AA()//下面的拷贝构造也是构造,写了就不会生成,所以这里要写一个默认构造函数出来才能通过
  {}
  AA(const AA& a) 
  {
    cout << "AA(const AA& a)" << endl;
  }
};
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
  AA _aa;
};
int main()
{
  Date d1;
  Date d2(2022, 1, 1);
  d1.Print();
  d2.Print();
  //拷贝复制一个d2对象
  Date d3(d2);
  d3.Print();
  return 0;
}

📝 说明

我们不写,编译器默认生成拷贝构造,跟构造和析构不太一样,它不会去区分内置类型和自定义类型,都会处理。

1、内置类型,字节序的浅拷贝 (一个字节一个字节的拷贝)。

   AA(const AA& a)

2、自定义类型,会去调用它的拷贝构造完成拷贝。

   2022/1/1

❓ 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了 (内置类型和自定义类型),是否意味着编译器自己生成的就行呢。我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试 ❔

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int)*capacity);
    if(_a == nullptr)
    {
      cout << "malloc fail" << endl;
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  ~Stack()
  {
    free(_a);
    _a = nullptr;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack st1;
  Stack st2(st1);
  return 0;
}

📝 说明

经调试发现虽然已经完成了拷贝,但是程序崩了

为什么会崩 ❓

此时 st1 和 st2 指向同一块空间,那么问题来了,谁先被析构呢 —— 根据栈的特性后进先出,所以这里先被析构的是 st2。st2 被析构后再析构 st1 时程序就崩溃了,因为同一块空间不能释放两次。

编译器默认生成的拷贝构造并不能解决所有问题,像 Stack 这样的类,编译器默认生成拷贝构造完成的就是浅拷贝。解决方案就是自己实现深拷贝,对于深拷贝,因为比较复杂,所以后面要另写一篇文章。

❓ 拷贝 | 拷贝构造 ❔

int p()
{
  int ret = 0;
  return ret; 
}
Date q()
{
  Date ret;
  return ret;
}

📝 说明

我们说了,在传值返回时,它会先拷贝 (实际上是两次拷贝,但有些编译器会优化成一次)。

为什么需要拷贝 ❓

当函数调用结束,函数栈帧会被销毁,ret 是属于这个栈帧的局部变量,所以拷贝是为了解决局部变量被销毁的问题。使用引用可以解决拷贝消耗,但要注意如果出了作用域返回对象不在了就不能用引用。注意有些编译器在函数结束时不会清理栈帧,所以能返回正确返回值 (如果不是正确返回值,那么说明栈空间被清理了),但那是非法的。


相关文章
|
19小时前
|
存储 编译器 C语言
从C语言到C++_11(string类的常用函数)力扣58和415(中)
从C语言到C++_11(string类的常用函数)力扣58和415
5 0
|
19小时前
|
存储 C语言 C++
从C语言到C++_11(string类的常用函数)力扣58和415(上)
从C语言到C++_11(string类的常用函数)力扣58和415
5 0
|
19小时前
|
算法 编译器 C语言
从C语言到C++⑩(第四章_模板初阶+STL简介)如何学习STL(下)
从C语言到C++⑩(第四章_模板初阶+STL简介)如何学习STL
4 0
|
19小时前
|
存储 编译器 程序员
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针(下)
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针
4 0
|
1天前
|
C++
【C++基础】类class
【C++基础】类class
9 1
|
1天前
|
编译器 C++
C++程序中的对象赋值和复制
C++程序中的对象赋值和复制
7 1
|
1天前
|
安全 程序员 编译器
C++程序中的基类与派生类转换
C++程序中的基类与派生类转换
8 1
|
1天前
|
C++
C++程序中的类成员函数
C++程序中的类成员函数
8 1
|
1天前
|
C++
C++程序中的类封装性与信息隐蔽
C++程序中的类封装性与信息隐蔽
8 1
|
1天前
|
C++
C++程序中的类声明与对象定义
C++程序中的类声明与对象定义
10 1