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


相关文章
|
11天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
51 18
|
11天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
37 13
|
11天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
37 5
|
11天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
26 5
|
11天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
32 4
|
11天前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
26 3
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
76 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
128 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
138 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
195 4