类的默认成员函数(C++)

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

1.构造函数

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

特性

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

key:开空间是函数栈帧的事情,调用函数开辟空间。

  • 函数名必须与类名想同
  • 无返回值(void也不用写)
  • 对象实例化时自动调用对应的构造函数
  • 构造函数可以重载
class Time
{
public:
  //无参的构造函数
  Time()
  {
    cout << "None Pram" << endl;
  }
  //带参数的构造函数
  Time (int hour, int minute, int second)
  {
    _hour = hour;
    _minute = minute;
    _second = second;
    cout << _hour << ":" << _minute << ":" << _second << endl;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
int main()
{
  Time t1;  //调用无参的构造函数
  Time t2(9, 19, 30);  //调用带参的构造函数
  Time t3();   //该方法调用无参构造函数会报错,该种写法是写一个返回值为Time类型函数的写法。
}


8ecbfa4b889c496db7bab7255991788b.png

  • 如果类中没有显示的定义构造函数,C++编译器会默认生成一个无参的默认构造函数,一旦用户显式定义编译器将不再自动生成。
class Time
{
public:
  带参数的构造函数
  //Time(int hour, int minute, int second)
  //{
  //  _hour = hour;
  //  _minute = minute;
  //  _second = second;
  //  cout << _hour << ":" << _minute << ":" << _second << endl;
  //}
private:
  int _hour;
  int _minute;
  int _second;
};
int main()
{
  //1.将显示定义的构造函数屏蔽以后,编译器会默认自动生成一个
  //无参的默认构造函数,因此不会报错。
  //2.如果将显示定义的构造函数展开,编译器不在生成,因为用户定义了一个。
  //此时如果还这样声明变量,就会报错,要以自己定义的方式传参。
  Time t1;  
}

编译器生成的默认构造函数对内置类型(int/char/double/指针)不做处理,对自定义类型(struct,class,union)等,会去调用它们自己的默认构造函数。(对内置类型不处理是C++的缺陷)。c++11又增加了一个特性,弥补了缺陷,即内置类型的成员变量在类中声明时可以给默认值

无参的构造函数、全缺省的构造函数和没写编译器生成的构造函数,都可以认为是默认构造函数,默认构造函数有且只能有一个


75d108375d0a4ab6b6fd5ee0fe7923f8.png

2.析构函数

与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时自动调用析构函数,完成对象中资源清理工作。对象生命周期到了会自动销毁。

特性

  • 析构函数名是在类名前加上字符~
  • 无参数无返回值类型
  • 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  • 对象在生命周期结束时,C++编译器系统自动调用析构函数
class Stack
{
public:
  Stack(int capacity=3)
  {
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == nullptr)
    {
      printf("malloc failed");
    }
    _capacity = capacity;
    _size = 0;
    printf("malloc success\n");
  }
  ~Stack()
  {
    if (_arr)
    {
      free(_arr);
      _capacity = 0;
      _size = 0;
    }
    cout << "~stcak" << endl;
  }
private:
  int* _arr;
  int _capacity;
  int _size;
};
int main()
{
  Stack s1;
  return 0;
}


fcf11932f9ca411097f401d271e4ee7f.png

  • 当没有显示定义析构函数时,编译器会自动默认生成析构函数,它对内置类型不作处理,对自定义类型去调用它自己的析构函数。
  • 如果类中没有申请资源,可以不写析构函数,直接使用编译器默认生成的。否则会造成资源泄露。

3.拷贝构造函数

在创建对象时,可以创建一个与已存在对象一摸一样的新对象。该函数只有单个形参,该形参是本类类型对象的引用(常用const修饰)。

特性

  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数的参数只有一个必须是类型对象的引用,使用传值方法会引起无穷调用,因为每一次传值调用,传过去的值都是一次拷贝构造(传值都会临时拷贝一份给形参传过去),这样会引发无穷递归。
class Time
{
public:
  //全缺省的构造函数
  Time(int hour=0, int minute=0, int second=0)
  {
    _hour = hour;
    _minute = minute;
    _second = second;
    cout << _hour << ":" << _minute << ":" << _second << endl;
  }
  //复制本应该有两个形参,为什么只写了一个?
  //因为还隐含了一个this指针。
  //使用const是防止传进来的d被修改
  Time(const Time& d)
  {
    this->_hour = d._hour;
    _minute = d._minute;
    _second = d._second;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
int main()
{
  Time t1;
  Time t2(t1);  //拷贝构造的语法,获得一个以t1完全一样的t2
  return 0;
}


02ca313abaa5481c9b8dbe44908f37c7.png

3481ce912e834791aa7983b318cade84.png

问题1:为什么不用=符号完成拷贝构造呢?

虽然d2、d3都能完成拷贝构造的任务,然而d1并不直观、不明确,容易让人误以为是赋值。

问题2:指针也能完成拷贝构造的任务,为什么要用引用呢?

d4传地址有点冗余麻烦,把d1的地址赋值给d5是什么意思?显然对比d2、d4、d5,发现使用引用是最符合正常人的逻辑的。

  • 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或值拷贝
class TestCls {
public:
    TestCls()
    {
        cout << "TestCls()" <<endl;
        p = new int;
    }
    TestCls(const TestCls& d)
    {
        cout << "TestCls(const TestCls& testCls)" << endl;
        a = d.a;
        //p = testCls.p;   //如果是编译器默认生成的,会直接把指针的地址赋给p
        p = new int;
        *p = *(d.p);      //为拷贝类的p指针分配空间,实现深度拷贝
    }
    ~TestCls()
    {
        delete p;
        std::cout << "~TestCls()" << std::endl;
    }
private:
    int a;
    int* p;
};
int main()
{
    TestCls t1;
    TestCls t2(t1);
    return 0;
}


600927925b524d2d8d84e43cc2a8da83.png

上面的例子也可以看出,像内置函数类型(简单的赋值操作),一般默认是深拷贝,但是涉及到指针的问题,涉及到自定义类的问题,我们在写构造函数时,一定要注意深浅拷贝的问题。

class Time
{
public:
  Time()
  {
    _hour = 1;
    _minute = 1;
    _second = 1;
  }
  Time(const Time& t)
  {
    _hour = t._hour;
    _minute = t._minute;
    _second = t._second;
    cout << "Time::Time(const Time&)" << endl;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year = 1970;
  int _month = 1;
  int _day = 1;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d1;
  return 0;
}

注意这段代码,d1的成员变量和_t的成员变量内存中是挨着的。

2be395f9426f44e0a0a9a3565d5dcf7a.png

  • 类中如果没有涉及到资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,必须写拷贝构造函数,否则就是浅拷贝(只赋给指针)
  • 拷贝构造典型场景
    使用已存在对象创建新对象;函数参数类型为类类型对象;函数返回值为类类型对象。
class Date
{
public:
 Date(int year, int minute, int day)
 {
 cout << "Date(int,int,int):" << this << endl;
 }
 Date(const Date& d)
 {
 cout << "Date(const Date& d):" << this << endl;
 }
 ~Date()
 {
 cout << "~Date():" << this << endl;
 }
private:
 int _year;
 int _month;
 int _day;
};
Date Test(Date d)
{
 Date temp(d);
 return temp;
}
int main()
{
 Date d1(2022,1,13);
 Test(d1);
 return 0;
}


02176da83d8848f3b0d297089e29c1fb.png因此,为了提高效率,一般对象传参时,尽量使用引用类型;返回时,根据实际场景,能用引用尽量使用引用。

4.赋值重载函数

运算符重载

C++为了增强代码的可读性引入运算符重载,运算符重载是具有特殊函数名的函数。

内置类型可以直接使用运算符运算,编译器知道如何运算;自定义类型无法直接使用运算符,编译器不知道如何运算,因此需要自己实现运算符重载。


函数定义语法:


返回值类型 operator操作符(参数列表)


不能创建新的操作符,如:operator@

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

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

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

  • .*(见的很少) ::(域作用限定符) sizeof ?:(三目运算符) .(函数调用符号)注意以上5个符号不能进行函数重载
class Date
{
public:
  //构造函数
  Date(int year = 1970, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  // 运算符重载
  //相当于bool operator==(Date* const this, const Date& d);
  //注意:这里左操作数是this,右操作数是d。
  bool operator==(const Date& d)
  {
    return _year == d._year
      && _month == d._month
      && _day == d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 8, 3);
  Date d2(d1);
  Date d3(1, 1, 1);
  //相当于
  //cout<<d1.operator==(&d1, d2)<<endl;
  cout << (d1 == d2) << endl;  //结果为1
  cout << (d1 == d3) << endl;   //结果为0
  return 0;
}

赋值运算符重载

即给一个已经存在的对象赋值(已经存在),拷贝构造(给一个新对象赋值)。

  • 参数类型:const T&,传递引用可以提高效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this:保证连续赋值不出错。
// 赋值运算符重载
//参数加引用和const,传参更快,防止参数修改
//返回引用,防止拷贝构造。
  Date& operator=(const Date& d)
  {
    //防止  d1 = d1;
    if(this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;
  }
  //可以这样调用
  d2 = d1;  // d2.operator=(&d2, d1)   第一个参数是地址,第二个参数是传引用
  d3 = d2 = d1;
  • 赋值运算符只能重载成类的成员函数,不能重载成全局函数
    因为赋值运算符重载如果用户不显示的定义在类里面,编译器就在类里面自动生成一个。此时如果再在全局声明一个,就和类里面的冲突了。
  • 用户没有显示的实现时,编译器会默认生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量直接赋值,自定义类需调用自己实现的。

const成员函数

使用const修饰的成员函数

取地址运算符重载

//写在类里面
  //取地址运算符重载
  Date* operator&()
  {
    return this;
  }
  //通过这样设置可以保证,类外面的成员只能访问地址,不能修改地址。
  const Date* operator&()const
  {
    return this;
  }

正常情况下,如果我们不写这个成员函数,编译器会自动生成。所以一般不需要我们自己去写。

那我们什么时候应该写这个成员函数呢?

比如当你不想让别人取到这个类型对象的地址的时候,你可以return nullptr ,这样就永远无法获得这个对象的地址了。

目录
相关文章
|
4天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
2天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
2天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
2天前
|
存储 程序员 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
53 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
55 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
43 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
49 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
35 3