类与对象(二)--类的六个默认成员函数超详细讲解

简介: 类与对象(二)--类的六个默认成员函数超详细讲解

1.类的默认六个成员函数✒️

🔎什么是默认的成员函数?当我们创建一个类,如果没有显式的定义以下六个成员函数,编译器会为我们自动生成这些函数。

🔥.构造函数

🔥析构函数

🔥.拷贝构造函数

🔥.赋值重载函数

🔥.普通对象取地址重载函数

🔥.const对象取地址重载函数

✋也就意味着,即使我们创建一个空类,编译器也自动生成这六个成员函数,只不过是隐式的

2.构造函数

2.1构造函数的概念✒️

🔎构造函数是一种特殊的成员函数,用于初始化类的对象。构造函数在对象被创建时自动调用,确保对象在使用之前处于合适的状态。

观察以下代码🚦

class Date
{
public:
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Init(2022, 7, 5);
  d1.Print();
  Date d2;
  d2.Init(2022, 7, 6);
  d2.Print();
  return 0;
}

✋上述代码中的Date类可以通过Init成员函数(公有方法)给对象设置日期,但是如果我们每次创建对象的时候都要调用这个函数的话就显得比较麻烦。构造函数就能很好的解决这个问题。因为构造函数在对象在创建的时候由编译器自动调用,并且在对象的整个生命周期只调用一次。

⭐️构造函数是一个特殊的成员函数,其函数名和类名相同,且没有返回值!

2.2构造函数的特性✒️

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

特性:

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

2️⃣. 无返回值。void类型也不是

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

4️⃣. 构造函数可以重载。也就意味着可以有多个函数名相同参数不同的构造函数。

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

✋上述代码中通过无参构造创建了对象d1,对象后面不需要加括号,如果加了括号就变成了

Date d1() ,编译器会认为这行代码是在声明一个返回值为Date的函数。

观察以下代码🚦

⁉️为什么我将不带参数的构造函数注释掉后创建对象d1会报错呢?编译器不是会自动生成不带参数的构造函数吗?

上面我们已经提到,如果我们已经显式的定义构造函数那么编译器就不会再生成构造函数。✋就像上述代码中,我们其实已经显示定义了构造函数date(int y,iny m,int d),编译器就不会再生成任何的构造函数,也就不会生成Date()。当Date d1创建d1这个对象时编译器找不到Date()这个函数,也就会报错了。

6️⃣.成员初始化列表。构造函数可以使用成员初始化列表,在构造函数体之前对成员进行初始化

7️⃣.编译器默认生成的构造函数在初始化成员变量时,对于自定义类型变量调用它们自己的构造函数,对于内置类型变量则不做处理。

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。

观察以下代码🚦

✋C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

8️⃣.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

⭐️注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

3.析构函数

3.1析构函数的概念✒️

🔎跟构造函数相反,析构函数是用来对对象被销毁时清理的。但是值得注意的是,析构函数并不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的。当对象被销毁的时候,会自动调用该对象的析构函数,完成对对象资源的具体清理,一般是动态资源。析构函数是最后一次使用对象执行的动作。

3.2析构函数的特征✒️

特征:

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

2️⃣. 无参数无返回值类型

3️⃣. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。

跟构造函数不同,析构函数必须无参。

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

5️⃣.类中成员的销毁过程依赖成员的数据类型。对于内置类型系统会自动回收,不需要析构函数。但是对于自定义类型,编译器就会调用该成员本身的析构函数

✋对于上面代码,Date类中有Time类类型的变量,当调用Date类的析构函数时,也会调用Time的析构函数。

6️⃣.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时(比如new在堆上开辟的空间),一定要写,否则会造成资源泄漏(内存泄漏)。

✋上述类对象在初始化构造的时候会调用构造函数,用new开辟动态空间,如果我们不定义析构函数对该动态资源进行处理,默认生成的析构函数也不会回收,这样一来就会造成内存泄漏。

4.拷贝构造函数

4.1拷贝构造函数的概念✒️

🔎拷贝构造函数是用来用一个同类对象作为模板,生成构造一个一模一样新的对象。

⭐️拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),再用已存在的类类型对象创建新对象时由编译器自动调用。

4.2拷的特征✒️ 重载形式。

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

2️⃣. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。

3️⃣.如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数对于内置类型变量按字节完成拷贝,也叫浅拷贝(值拷贝)。对于自定义类型变量则调用其拷贝构造函数完成拷贝。

4️⃣.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

举例🚦

✋上面代码中A类成员有一个指针arr指向了一片动态空间。如果启用编译器默认的拷贝构造函数进行值拷贝的话,会将指针的值再复制一遍,也就意味着与复制出来的指针指向了同一片地址。这样一来就会非常危险,因为当需要销毁对象a和对象a1的时候会调用其析构函数,也就会对同一片空间释放两次,这是不被允许的。

5️⃣. 拷贝构造函数典型调用场景🚦

🔥 使用已存在对象创建新对象

🔥 函数参数类型为类类型对象

🔥 函数返回值类型为类类型对象

4.3思考❓

观察以下代码思考其构造函数,析构函数的调用顺序以及输出结果🚦

✋其实Test函数返回值的时候也会进行拷贝构造,但是现在很多编译器都将这一步优化掉了,所以也就看不到了。不同的编译器对以上代码的输出结果可能会有差别,比如在linux中的g++工具编译此代码的析构顺序就是3、2、1。

⭐️为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。

4.4深拷贝和浅拷贝⭐️✒️

🔎深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在对象拷贝过程中涉及的两个概念,主要关注于如何处理对象的成员变量(尤其是指针类型的成员变量)。

❓上面讲到,编译器自动生成的拷贝构造函数对对象的拷贝方式是浅拷贝,也就是按值拷贝。那么什么又是深拷贝?二者的区别在哪呢?

4.4.1浅拷贝⭐️✒️

  • 浅拷贝只是简单地复制对象的值,包括成员变量。如果对象包含指针,那么浅拷贝只是复制指针的值,而不是复制指针所指向的内容。
  • 对象的拷贝和原始对象共享相同的资源(如动态分配的内存区域),这可能导致潜在的问题,因为一个对象的修改可能会影响到另一个对象(之前出现的对一片空间释放两次)
  • 默认情况下,C++ 的复制构造函数和赋值运算符执行的是浅拷贝。

4.4.2深拷贝⭐️✒️

  • 深拷贝会复制对象的值,同时为对象的指针类型成员变量分配新的内存,并复制指针所指向的内容。这样,原始对象和拷贝对象将拥有独立的资源,对一个对象的修改不会影响另一个对象。
  • 深拷贝需要程序员显式实现,通常涉及到复制构造函数、赋值运算符或者自定义的拷贝逻辑。

4.4.3总结✒️✒️

  • 浅拷贝简单快速,但容易引发潜在的问题,特别是当对象包含动态分配的资源时。
  • 深拷贝较为安全,但由于需要额外的内存分配和复制操作,可能效率较低。在实现深拷贝时需要小心管理资源,防止内存泄漏等问题。

5.赋值运算符重载

5.1什么叫赋值运算符重载?✒️

🔎C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。运算符重载允许程序员自定义特定运算符的操作,以适应用户定义的类型。

🚩函数名字为:关键字operator后面接需要重载的运算符符号

注意🚦

🔥不能通过连接其他符号来创建新的操作符:比如operator@

🔥重载操作符必须有一个类类型参数

🔥用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

🔥作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

🔥 “.*”  “::”  “sizeof”  “?:”  “.” 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

举例🚦

🚩我们先定义一个日期类,再重载一个比较日期类是否相等的运算符==

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
   bool operator==(const Date& d2)
  {
    return _year == d2._year
      && _month == d2._month
      && _day == d2._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
 
void Test()
{
  Date d1(2018, 9, 26);
  Date d2(2018, 9, 27);
  cout << (d1 == d2) << endl;//因为没有重载<<,所以需要用括号括起来
  cout << d1.operator==(d2) << endl;//调用成员函数的形式也可以
}

✋上述代码中的成员函数 operator==()会有一个隐式的this指针,所以在声明的时候只需要再设一个形参就可以了。

5.2赋值运算符重载✒️

1️⃣.赋值运算符重载格式

🔥参数类型:const T&,传递引用可以提高传参效率

🔥返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

🔥检测是否自己给自己赋值

🔥返回*this :要复合连续赋值的含义

 Date& operator=(const Date& d) {
   if (this != &d) {//如果参数是对象本身,那就不用继续赋值
     _year = d._year;
     _month = d._month;
     _day = d._day;
   }
   return *this;
 }

2️⃣.赋值运算符只能重载成类的成员函数不能重载成全局函数

✋✋上面我们已经讲过,赋值运算重载函数是默认成员函数,如果我们不在类中显式定义那么编译器就会自动生成一个赋值运算符重载函数。这样一来如果我们再全局定义了也一个赋值运算重载函数,就会和编译器生成的重载函数起冲突。所以,赋值运算符重载函数只能定义成类成员。

3️⃣. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(拷贝而不是拷贝构造)。

注意🚦内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

4️⃣如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

⭐️跟上面拷贝构造函数需要自己显式定义的道理是一样的,如果类中涉及到资源的管理,比如有一个指针arr维护了一片动态空间。这样的类就需要自己实现一个赋值重载函数,不然靠编译器自动生成的函数只会复制一个相同值的指针,也就是指向同一片空间的指针,这样就容易发生错误。

5.3 前置++和后置++重载✒️

🔎通过上面对运算符重载的学习,我们知道对于一个操作符来说,顺序非常重要,就像i++和++i的含义是不一样的。前置++和后置++重载又该怎么写呢?

前置++

对于一个日期类的对象来说,+1其实就是往后加一天。

Date& operator++() {//前置++
  _day++;
  return *this;
}

返回+1之后的结果 ,this指向的对象函数结束后不会销毁,故以引用方式返回提高效率

⭐️后置++

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载

C++规定后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。

  Date operator++(int) {//后置++
    Date temp(*this);
    _day++;
    return temp;
  }

后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 一份,然后给this+1。又temp是临时对象,因此只能以值的方式返回,不能返回引用。

6.const成员✒️

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

🔎由于我们的类里this指针是隐式的,想要用const修饰this指针该怎么办呢?

🔎c++规定,若在类里面的成员函数想用const修饰this指针,就将const写在函数括号的外面。

举例🚦

//显示日期
void Dispaly()const {
  cout << _year << "-" << _month << "-" << _day << endl;
}

6.1思考❓

1 、const对象可以调用非const成员函数吗?

💡不能,权限放大。

2、非const对象可以调用const成员函数吗?

💡可以,权限变小

3、const成员函数内可以调用其它的非const成员函数吗?

💡不能,权限放大

4、非const成员函数内可以调用其它的const成员函数吗?

💡能,权限变小

7.取地址及const取地址操作符重载✒️

🔎这两个重载一般都不用自己重新定义,用编译器自动生成的就够了

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
}
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

✋ 这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容

相关文章
|
2月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
39 3
|
2月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
51 1
|
4月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
4月前
|
存储 算法 搜索推荐
【C++】类的默认成员函数
【C++】类的默认成员函数
|
6月前
|
编译器 C++ 存储
【C++语言】类和对象--默认成员函数 (中)
【C++语言】类和对象--默认成员函数 (中)
【C++语言】类和对象--默认成员函数 (中)
|
7月前
|
编译器 C++
类与对象(三)--构造函数体中的赋值和初始化列表的区别
类与对象(三)--构造函数体中的赋值和初始化列表的区别
|
7月前
|
编译器 C语言 C++
类的6个默认成员函数(上)
类的6个默认成员函数(上)
44 0
|
7月前
|
存储 编译器 程序员
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
|
安全 编译器 C++
[C++] 类与对象(中)类中六个默认成员函数(1)上
[C++] 类与对象(中)类中六个默认成员函数(1)上
|
7月前
|
编译器 C++
C++:类的默认成员函数
C++:类的默认成员函数
86 0