本系列暴死,不会更新了。

该系列为本人的学习笔记,主要由本人整理书写而成。部分内容来自教材、视频课程等,不能保证完全原创性。

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

# 类的默认成员函数

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

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

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

  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-1
        d.Print();
        return 0;
    }

# 析构函数 (Destructor)

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

# 析构函数的特点

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

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

Stack.h
#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;
};
Stack.cpp
#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 指针,表示这个成员函数不能对类的任何成员进行修改。

建议:只要能加 const 就加 const,避免权限放大的问题。

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

# 实现完整的日期类

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

Date.h
#pragma once
class 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;
};
Date.cpp
#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++ 内置的基础功能。只有当显式地为类重载了取地址操作符,这个操作的行为才会改变。