当前位置: 首页 > news >正文

椋鸟C++笔记#3:类的默认成员函数

文章目录

      • 类的默认成员函数
      • 构造函数(Constructor)
        • 构造函数的特点
      • 析构函数 (Destructor)
        • 析构函数的特点
      • 拷贝构造函数(Copy Constructor)
        • 拷贝构造函数的特点
      • 运算符重载(Operator Overloading)
        • 赋值运算符重载
        • 前置/后置的自增和自减运算符重载
      • const成员
      • 实现完整的日期类
      • 关于取地址和const取地址运算符(&操作符)

萌新的学习笔记,写错了恳请斧正。

类的默认成员函数

我们写一个类,在类体中什么都不写,这样的类就叫空类

空类真的什么都没有吗?并不是的。

类有六个默认成员函数,如果我们自己没有写这六个函数,类也会自己去补齐它们。

  1. 默认构造函数(Default Constructor) - 用于无参的初始化一个对象。
  2. 析构函数(Destructor) - 用于在对象生命周期结束时进行资源的回收等收尾工作。
  3. 拷贝构造函数(Copy Constructor) - 用于通过同类型的另一个对象来初始化新对象。
  4. 拷贝赋值运算符(Copy Assignment Operator) - 用于将一个对象的内容赋值给另一个同类型的对象。
  5. 移动构造函数(Move Constructor) - C++11引入,用于通过"移动"而非拷贝另一个同类型的对象来初始化新对象。
  6. 移动赋值运算符(Move Assignment Operator) - C++11引入,用于通过"移动"而非拷贝来将一个对象的内容赋值给另一个同类型的对象。

其中后两个先不管,下面我们介绍一下前面几个函数。

构造函数(Constructor)

我们先看这样一个类:

class Date
{
public:void Init(int y, int m, int d){m_year = y;m_month = m;m_day = d;}void Print(){cout << m_year << "-" << m_month << "-" << m_day << endl;}private:int m_year;int m_month;int m_day;
}

对于这个日期类,我们每次要实例化一个对象时都需要这样先创建对象再使用Init函数完成初始化,非常的不方便。

所以在C++中,我们就有了构造函数。构造函数是一个特殊的成员函数,名字与类名相同。当我们创建对象时,其构造函数就会被自动调用,并且在对象的整个生命周期中只调用一次。

比方说我们可以把上面的类改写成这样:

class Date
{
public:Date(int y, int m, int d)	// 构造函数前面就不要加void了{m_year = y;m_month = m;m_day = d;}void Print(){cout << m_year << "-" << m_month << "-" << m_day << endl;}private:int m_year;int m_month;int m_day;
}
构造函数的特点
  1. 函数名与类名相同。

  2. 没有返回值。

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

  4. 同样可以重载:

    class Date
    {
    public:Date()	// 无参{m_year = 1900;m_month = 1;m_day = 1;}Date(int y, int m, int d)	// 带参{m_year = y;m_month = m;m_day = d;}void Print(){cout << m_year << "-" << m_month << "-" << m_day << endl;}private:int m_year;int m_month;int m_day;
    }int main()
    {Date d1;	// 调用无参的构造函数Date d2(2024, 5, 22);	//调用带参的构造函数return 0;
    }
    

    注意:这里实例化时不能这样写:

    int main()
    {Date d();	// 调用无参的构造函数不要带括号!return 0;
    }
    

    这里的语句可能会被认为是在声明一个叫d的函数,其返回值是Date类型的对象。

  5. 如果类中没有显式定义构造函数,编译器会自动生成一个无参的构造函数,如果显式定义了就不会这么做。

    这里默认生成的构造函数是遵循什么规则给对象初始化的呢?且看:

    C++中的类型分为内置类型(基本类型,比如int、double这些自带的)和自定义类型(我们用class、struct、union等自己定义出来的)。而默认生成的构造函数不一定会对内置类型进行初始化(不排除某些编译器有自己的想法?),对于自定义类型编译器会去寻找其构造函数以实现初始化

    但是在C++11中,打了一个补丁:内置成员变量可以在类中声明时给默认值(要给就全给)。

    class Date
    {
    public:Date(int y, int m, int d){m_year = y;m_month = m;m_day = d;}void Print(){cout << m_year << "-" << m_month << "-" << m_day << endl;}private:int m_year = 1900;int m_month = 1;int m_day = 1;
    }int main()
    {Date d;	//1900-1-1d.Print();return 0;
    }
    

析构函数 (Destructor)

析构函数与构造函数相反,在对象销毁时自动调用析构函数,完成对象中资源的清理工作

析构函数的特点
  1. 析构函数名是在类名前加上字符~
  2. 析构函数无参数无返回值类型,不能重载
  3. 一个类只能有一个析构函数。若显式未定义,则自动生成默认的析构函数。
  4. 对象生命周期结束时,自动调用析构函数。
  5. 和构造函数同样的,自动生成的析构函数不会清理内置类型的变量(本身也不需要清理),对于自定义类型的变量去寻找其析构函数并调用。

这里我们还是以之前的Stack类为例:

#pragma once#include <iostream>
#include <string>using namespace std;typedef int StackDataType;class Stack
{
public:Stack(int size);~Stack();void Push(StackDataType data);StackDataType Pop();StackDataType Top();bool IsEmpty();bool IsFull();int GetSize();int GetTop();private:StackDataType* m_data;int m_size;int m_capacity;
};
#include "Stack.h"Stack::Stack(int size = 4)
{m_data = new StackDataType[size];m_size = 0;m_capacity = size;
}Stack::~Stack()
{delete[] m_data;
}void Stack::Push(StackDataType data)
{if (IsFull()){cout << "Stack is full!" << endl;return;}m_data[m_size++] = data;
}StackDataType Stack::Pop()
{if (IsEmpty()){cout << "Stack is empty!" << endl;return -1;}return m_data[--m_size];
}StackDataType Stack::Top()
{if (IsEmpty()){cout << "Stack is empty!" << endl;return -1;}return m_data[m_size - 1];
}bool Stack::IsEmpty()
{return m_size == 0;
}bool Stack::IsFull()
{return m_size == m_capacity;
}int Stack::GetSize()
{return m_size;
}int Stack::GetTop()
{return m_size - 1;
}

拷贝构造函数(Copy Constructor)

如果我们想通过一个已有对象去创建一个新对象,就要用到拷贝构造函数。简单来说,就是我们有一个类X的对象a,现在想再创建一个类X的对象b而且要和a完全一致。那么就直接使用拷贝构造函数,使用a来创建b。

拷贝构造函数的特点
  1. 拷贝构造函数和默认构造函数一样都是构造函数的一个重载形式

  2. 拷贝构造函数的参数只有一个且为类类型对象的引用。使用传值的方式会造成无穷递归调用,会被编译器报错。

    class Date
    {
    public:Date(int y, int m, int d){m_year = y;m_month = m;m_day = d;}Date(const Date& d){m_y = d.m_y;m_m = d.m_m;m_d = d.m_d;}void Print(){cout << m_year << "-" << m_month << "-" << m_day << endl;}private:int m_year = 1900;int m_month = 1;int m_day = 1;
    }
    

    与之类似的,我们一般传参时,能使用引用就尽量使用引用类型,可以提高程序效率。

  3. 如果没有显式定义,编译器会生成默认的拷贝构造函数。但是默认的拷贝构造函数是直接将内存按字节序复制过来,属于浅拷贝(值拷贝)。但是我们很多时候是需要深拷贝的,尤其是存在资源申请时,因为这种情况下依旧直接复制内存并不能申请新的资源,不能完成我们所需的拷贝。

运算符重载(Operator Overloading)

运算符重载是面对对象编程的一种方法。在C++中,运算符重载允许我们为类对象自定义运算符的计算方法。

简单来说,我们自定义一个类,编译器是不知道这个类的加减乘除等计算是如何进行的。像加减乘除等计算,一般只能用于系统的内置类型,但我们可以通过运算符重载来规定自定义类型遇到这些运算符应该怎么处理。

运算符重载函数的名字为:operator后面加上需要重载的运算符符号

比方说下面就是重载了日期类的日期加天数等于新日期的计算(+=的重载):

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){m_year = year;m_month = month;m_day = day;}Date(const Date& date){m_year = date.m_year;m_month = date.m_month;m_day = date.m_day;}~Date() {};int GetMonthDays(int year, int month){if (month == 2){if (IsLeapYear(year)){return 29;}else{return 28;}}else if (month == 4 || month == 6 || month == 9 || month == 11){return 30;}else{return 31;}}bool IsLeapYear(int year){if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0){return true;}else{return false;}}Date& operator+=(int days){while (days > 0){if (days + m_day > GetMonthDays(m_year, m_month)){days -= GetMonthDays(m_year, m_month) - m_day + 1;m_day = 1;if (m_month == 12){m_month = 1;m_year++;}else{m_month++;}}else{m_day += days;days = 0;}}return *this;}void Print(){std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;}private:int m_year;int m_month;int m_day;
};

我们也可以将运算符重载为全局函数。但是需要涉及的成员变量是公有的(这会破坏类的封装性,但是也可以通过友元来解决,后面会讲):

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){m_year = year;m_month = month;m_day = day;}Date(const Date& date){m_year = date.m_year;m_month = date.m_month;m_day = date.m_day;}~Date() {};int m_year;int m_month;int m_day;
};bool operator==(const Date& d1, const Date& d2)
{return d1.m_year == d2.m_year&& d1.m_month == d2.m_month&& d1.m_day == d2.m_day;
}void test()
{Date d1(2024, 1, 1);Date d2(2024, 1, 1);cout << (d1 == d2) << endl;	// 由于优先级问题需要加括号
}

注意:

  1. 只能重载已有的运算符,不能自创新的符号
  2. 不能对内置类型去重载。
  3. 5个运算符不能重载.*(通过成员函数指针访问类的成员函数的运算符)::(作用域限定符)sizeof?:(三目运算符).
  4. 重载操作符至少要有一个类类型参数作为类成员函数重载时,其有一个隐藏的参数,为其第一个参数this。
赋值运算符重载

赋值运算符重载与拷贝构造函数类似。其参数应当选择传引用返引用以提高效率,并且这样能够支持连续赋值。

注意:

  1. 应当注意是否出现自己给自己赋值的情况
  2. 赋值运算符不能重载为全局函数,只能是成员函数(全局函数没有this指针,而且赋值运算符如果不显式实现,编译器会生成一个默认的,此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突)。
  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式在内存逐字节拷贝(浅拷贝)。

我们还是以日期类为例:

Date& Date::operator=(const Date& date)
{if (this != &date){m_year = date.m_year;m_month = date.m_month;m_day = date.m_day;}return *this;
}
前置/后置的自增和自减运算符重载

我们以自增运算符为例,前置自增应当这样重载:

Date& Date::operator++()
{*this += 1;return *this;
}

而后置自增运算符应当如何重载呢?C++规定:为了区分,后置运算符重载时增加一个int类型参数,但是调用该函数时忽略该参数,就像这样:

Date Date::operator++(int)
{Date date = *this;*this += 1;return date;
}void test()
{Date d1;Date d2(2024, 1, 1);d1 = d2++;d1 = ++d2;
}

注意后置自增是使用后再自增,因此要返回自增前的旧值。

const成员

在成员函数的括号后面加一个const,就将这个成员函数称为const成员函数。这里实际修饰的是该成员函数的this指针,表示这个成员函数不能对类的任何成员进行修改。

具体的例子可以看下面实现完整的日期类中。

实现完整的日期类

下面我们通过上面学的内容实现一个日期类吧!

#pragma onceclass Date
{
public:Date(int year = 1900, int month = 1, int day = 1);Date(const Date& date);~Date();int GetMonthDays(int year, int month) const;int GetYearDays(int year) const;bool IsLeapYear(int year) const;Date& operator+(int days) const;Date& operator-(int days) const;Date& operator-=(int days);Date& operator+=(int days);Date& operator++();Date operator++(int);Date& operator--();Date operator--(int);Date& operator=(const Date& date);bool operator==(const Date& date) const;bool operator!=(const Date& date) const;bool operator<(const Date& date) const;bool operator>(const Date& date) const;bool operator<=(const Date& date) const;bool operator>=(const Date& date) const;int operator-(const Date& date) const;void Print() const;private:int m_year;int m_month;int m_day;
};
#include <iostream>
#include "Date.h"using namespace std;Date::Date(int year, int month, int day)
{m_year = year;m_month = month;m_day = day;
}Date::Date(const Date& date)
{m_year = date.m_year;m_month = date.m_month;m_day = date.m_day;
}Date::~Date()
{
}int Date::GetMonthDays(int year, int month) const
{if (month == 2){if (IsLeapYear(year)){return 29;}else{return 28;}}else if (month == 4 || month == 6 || month == 9 || month == 11){return 30;}else{return 31;}
}int Date::GetYearDays(int year) const
{if (IsLeapYear(year)){return 366;}else{return 365;}
}bool Date::IsLeapYear(int year) const
{if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0){return true;}else{return false;}
}Date& Date::operator+(int days) const
{Date date = *this;date += days;return date;
}Date& Date::operator-(int days) const
{Date date = *this;date -= days;return date;
}Date& Date::operator-=(int days)
{while (days > 0){if (days >= m_day){days -= m_day;if (m_month == 1){m_month = 12;m_year--;}else{m_month--;}m_day = GetMonthDays(m_year, m_month);}else{m_day -= days;days = 0;}}return *this;
}Date& Date::operator+=(int days)
{while (days > 0){if (days + m_day > GetMonthDays(m_year, m_month)){days -= GetMonthDays(m_year, m_month) - m_day + 1;m_day = 1;if (m_month == 12){m_month = 1;m_year++;}else{m_month++;}}else{m_day += days;days = 0;}}return *this;
}Date& Date::operator++()
{*this += 1;return *this;
}Date Date::operator++(int)
{Date date = *this;*this += 1;return date;
}Date& Date::operator--()
{*this -= 1;return *this;
}Date Date::operator--(int)
{Date date = *this;*this -= 1;return date;
}Date& Date::operator=(const Date& date)
{if (this != &date){m_year = date.m_year;m_month = date.m_month;m_day = date.m_day;}return *this;
}bool Date::operator==(const Date& date) const
{if (m_year == date.m_year && m_month == date.m_month && m_day == date.m_day){return true;}else{return false;}
}bool Date::operator!=(const Date& date) const
{return !(*this == date);
}bool Date::operator<(const Date& date) const
{if (m_year < date.m_year){return true;}else if (m_year == date.m_year){if (m_month < date.m_month){return true;}else if (m_month == date.m_month){if (m_day < date.m_day){return true;}}}return false;
}bool Date::operator>(const Date& date) const
{return !(*this < date) && *this != date;
}bool Date::operator<=(const Date& date) const
{return *this < date || *this == date;
}bool Date::operator>=(const Date& date) const
{return *this > date || *this == date;
}int Date::operator-(const Date& date) const
{int days = 0;Date temp = *this;if (temp < date){while (temp != date){temp++;days++;}}else{while (temp != date){temp--;days++;}}return days;
}void Date::Print() const
{std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}

关于取地址和const取地址运算符(&操作符)

有人说我们自定义的类型能够直接取地址或者const取地址,因此这两种运算符也是被编译器默认重载了的默认成员函数。

事实上这不能认为是类的默认成员函数,虽然我们自己可以去重载这两个运算符(不推荐,一般也用不上),但是空类生成的时候编译器并没有生成这样一个重载成员函数,这个操作是C++内置的基础功能。只有当显式地为类重载了取地址操作符,这个操作的行为才会改变。

相关文章:

  • 【html】网页布局模板01---简谱风
  • Java_IO流学习
  • GESP 四级冲刺训练营(1):字符串
  • linux内核符号表
  • 踩坑——纪实
  • VUE 页面生命周期基本知识点
  • 瑞芯微RV1126——ffmpeg环境搭建
  • 国产linux系统(银河麒麟,统信uos)使用 PageOffice 国产版在线编辑word文件,同时保存数据和文件
  • Springboot 自定义线程池 ThreadPoolTaskExecutor
  • 标准库算法
  • Android 观察者模式(OBSERVER)应用详解
  • Spring与Netty底层源码解析
  • 一个基于HOOK机制的微信机器人
  • 论文阅读--ViLD
  • 力扣226. 翻转二叉树(DFS的两种思路)
  • canvas 高仿 Apple Watch 表盘
  • canvas 五子棋游戏
  • chrome扩展demo1-小时钟
  • dva中组件的懒加载
  • ES6 ...操作符
  • java小心机(3)| 浅析finalize()
  • Laravel Mix运行时关于es2015报错解决方案
  • Mybatis初体验
  • npx命令介绍
  • use Google search engine
  • Vue2.x学习三:事件处理生命周期钩子
  • 两列自适应布局方案整理
  • 前端代码风格自动化系列(二)之Commitlint
  • 深度解析利用ES6进行Promise封装总结
  • 实现菜单下拉伸展折叠效果demo
  • 微信小程序--------语音识别(前端自己也能玩)
  • 小程序开发之路(一)
  • 栈实现走出迷宫(C++)
  • # Redis 入门到精通(九)-- 主从复制(1)
  • #include到底该写在哪
  • #知识分享#笔记#学习方法
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (2)空速传感器
  • (cljs/run-at (JSVM. :browser) 搭建刚好可用的开发环境!)
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (MATLAB)第五章-矩阵运算
  • (理论篇)httpmoudle和httphandler一览
  • (南京观海微电子)——COF介绍
  • (三十)Flask之wtforms库【剖析源码上篇】
  • (十三)Maven插件解析运行机制
  • (推荐)叮当——中文语音对话机器人
  • (一) 初入MySQL 【认识和部署】
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • (转)Linux下编译安装log4cxx
  • (转载)深入super,看Python如何解决钻石继承难题
  • .【机器学习】隐马尔可夫模型(Hidden Markov Model,HMM)
  • .net Application的目录
  • .Net IOC框架入门之一 Unity
  • .Net 路由处理厉害了