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


相关文章
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
70 4
|
5月前
|
C++ 运维
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
44 11
|
5月前
|
编译器 C++
【C++】详解运算符重载,赋值运算符重载,++运算符重载
【C++】详解运算符重载,赋值运算符重载,++运算符重载
|
6月前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
6月前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
6月前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
26天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
67 5
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
80 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
30 4