C++初阶教程——类与对象(中篇)

一、默认成员函数

在C++中,存在六个默认成员函数,如果这些函数没有被显式定义,那么编译器会自动生成 这些函数。它们分别是默认构造函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符和析构函数。

二、构造函数

2.1构造函数的概念

对于下面的类:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
private:
    int _day;
    int _month;
    int _year;
public:
    void Date_Init(int day, int month, int year)
    {
        this->_day = day;
        this->_month = month;
        this->_year = year;
    }
    void print()
    {
        cout << this->_year << "/" << this->_month << "/" << this->_day;
    }
};

int main()
{
    Date d1;
    d1.Date_Init(30, 10, 2024);
    d1.print();
    return 0;
}

对于Date类,可以定义上面这种初始化函数。但是如果每次创建对象的时候都像这样去调用该方法设置信息,有些繁琐。那么能否在对象创建时,就将信息设置进去呢?

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

2.2构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然叫构造,但是构造函数的主要任务不是开辟空间创建对象,二是初始化对象。其特性为:

  1. 函数名与类名相同。

  2. 没有返回值。

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

  4. 构造函数可以重载。

    cpp 复制代码
    #include<iostream>
    using namespace std;
    
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
    public:
         Date(int day, int month, int year)//带参构造函数
        {
            this->_day = day;
            this->_month = month;
            this->_year = year;
        }
         Date(){}//无参构造函数
        void print()
        {
            cout << this->_year << "/" << this->_month << "/" << this->_day;
        }
    };
    
    int main()
    {
        Date d1(30, 10, 2024);
        Date d2;//调用无参构造函数时,不要加(),否则变成函数声明了。
        d1.print();
        return 0;
    }
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成无参的默认构造函数,一旦显示定义任何一个构造函数,编译器就不再生成

  6. C++把类型分为内置类型和自定义类型。编译器生成的默认构造函数对内置类型的成员变量不做处理 ,对自定义类型的成员变量调用自定义类型的默认构造函数

    cpp 复制代码
    ​
    #include<iostream>
    using namespace std;
    
    class Time
    {
    private:
        int _hour;
        int _minute;
    public:
        Time()
        {
            cout << "Time构造函数已调用" << endl;
            _hour = 1;
            _minute = 1;
        }
    };
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
        Time _t;//自定义类型
    public:
         Date(int day, int month, int year)
        {
            this->_day = day;
            this->_month = month;
            this->_year = year;
        }
         Date(){}
        void print()
        {
            cout << this->_year << "/" << this->_month << "/" << this->_day;
        }
    };
    
    int main()
    {
        Date d2;
        return 0;
    }
    
    ​
  7. 无参的构造函数和全缺省的参数都成为默认构造函数,并且默认构造函数只能存在一个

三、析构函数

3.1析构函数的概念

析构函数完成的不是对对象本身的销毁,对象的销毁由编译器完成。析构函数在对象销毁时被自动调用,完成对对象中资源清理。

3.2析构函数的特性

  1. 析构函数名是在类名前加~。

  2. 无参数且无返回类型。

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

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

  5. 编译器自动生成 的析构函数,对于内置类型的成员,销毁时不需要清理资源,最后系统直接将其内存回收即可,对于自定义类型的成员,会调用这个成员的析构函数。

    cpp 复制代码
    #include<iostream>
    using namespace std;
    
    class Time
    {
    private:
        int _hour;
        int _minute;
    public:
        ~Time()
        {
            cout << "Time析构函数已调用" << endl;
            _hour = 1;
            _minute = 1;
        }
    };
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
        Time _t;
    public:
         Date(int day, int month, int year)
        {
            this->_day = day;
            this->_month = month;
            this->_year = year;
        }
         Date(){}
        void print()
        {
            cout << this->_year << "/" << this->_month << "/" << this->_day;
        }
    };
    
    int main()
    {
        Date d2;
        return 0;
    }
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类,有资源申请时,一定要写,否则会造成资源泄漏。

四、拷贝构造函数

4.1拷贝构造函数的概念

拷贝构造函数是一个特殊的构造函数,它用于创建一个对象作为另一个同类型对象的副本。当你需要通过值传递对象、返回对象、或者以对象为参数调用函数时,编译器可能会自动调用拷贝构造函数。

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

4.2拷贝构造函数的特性

拷贝构造函数也是特殊的成员构造函数,其特性如下:

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

  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值的方式会引发无穷递归调用。

    cpp 复制代码
    #include<iostream)
    using namespace std;
    
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
    public:
        Date(int year = 1900, int month = 1, int day = 1)
        {
            _year = year;
            _month = month;
            _day =  day;
        }
        Date(Date d)
        {
            _year = d.year;
            _month = d.month;
            _year = d.year;
        }
    };
    int main()
    {    
        Date d1;
        Date d2(d1);
        return 0;
    }

    这么写是错误的,d2拷贝d1,需要用到拷贝构造函数,在上面的代码中,拷贝构造函数的参数d不是引用类型,而是按值传递。按值传递意味着在调用拷贝构造函数时,会创建d的副本,这又会调用拷贝构造函数,从而导致无限递归,最终导致程序栈溢出并崩溃。

    cpp 复制代码
    #include<iostream)
    using namespace std;
    
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
    public:
        Date(int year = 1900, int month = 1, int day = 1)
        {
            _year = year;
            _month = month;
            _day =  day;
        }
        Date(const Date& d)
        {
            _year = d.year;
            _month = d.month;
            _year = d.year;
        }
    };
    int main()
    {    
        Date d1;
        Date d2(d1);
        return 0;
    }

    拷贝构造函数中,只能使用引用做传递参数。

  3. 若为显式定义,编译器会生成默认 的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

    cpp 复制代码
    #include<iostream>
    using namespace std;
    
    class Time
    {
    private:
        int _hour;
        int _minute;
    public:
        Time()
        {
            _hour = 1;
            _minute = 1;
        }
        Time(const Time& t)
        {
            _hour = t._hour;
            _minute = t._minute;
            cout << "Time::Time(const Time&)" << endl;
        }
    };
    class Date
    {
    private:
        int _day;
        int _month;
        int _year;
        Time _t;
    public:
        Date(int day, int month, int year)
        {
            this->_day = day;
            this->_month = month;
            this->_year = year;
        }
        Date() {}
        void print()
        {
            cout << this->_year << "/" << this->_month << "/" << this->_day;
        }
    };
    
    int main()
    {
        Date d1;
        Date d2(d1);
        return 0;
    }

    上面的代码中,Date类没有显式定义拷贝构造函数,编译器为Date类生成一个默认的拷贝构造函数。在生成的拷贝构造函数中,内置类型是按照字节方式是直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

  4. 如果类中没有进行资源申请,拷贝构造函数可以不显式定义,但是一旦涉及到资源申请时,则拷贝构造函数是一定要显式定义的,否则就是浅拷贝。下面的代码中,a1对象在构造函数中,默认是申请了10个元素的空间。a2对象使用a1拷贝构造,而Array类没有显示定义拷贝构造函数,而编译器会给Array类自动生成一份默认的拷贝构造函数,默认拷贝构造函数是按值拷贝的,将a1中的内容原封不动的拷贝到a2中,因此a1和a2指向了同一块内存空间。当程序退出时,a2和a1要销毁,等于同一块地址空间被释放了两次。

    cpp 复制代码
    #include<iostream>
    using namespace std;
    
    class Array
    {
    private:
        int* _array;
        size_t _size;
        size_t _capacity;
    public:
        Array(size_t capacity = 10)
        {
            _array = (int*)malloc(capacity * sizeof(int));
            if (_array == nullptr)
            {
                perror("malloc申请空间失败");
                    return;
            }
            _size = 0;
            _capacity = capacity;
        }
        void push(const int& data)
        {
            _array[_size] = data;
            _size++;
        }
        ~Array()
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    };
    
    int main()
    {
        Array a1;
        a1.push(1);
        a1.push(2);
        a1.push(3);
        a1.push(4);
        a1.push(5);
        a1.push(6);
        Array a2(a1);//浅拷贝
        return 0;
    }
  5. 拷贝构造函数调用的典型场景:使用已存在的对象创建新对象,函数参数类型返回为类类型对象,函数返回值类型为类类型对象。

cpp 复制代码
#include<iostream>
using namespace std;

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;
}

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

五、赋值运算符重载

5.1运算符重载

C++为了增强函数的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名以及参数列表。

cpp 复制代码
ReturnType operatorOperators(paralist)
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		 _year = year;
		 _month = month;
		 _day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

如果这么写的话,运算符重载成全局函数需要成员变量是公有的,如何保证封装性呢?可以写成友元函数或者重载成成员函数。

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	bool operator==(const Date& d1)
	{
		return _year == d1._year && _month == d1._month && _day == d1._day;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		 _year = year;
		 _month = month;
		 _day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

运算符重载需要注意:

  1. 不能通过来凝结其他符号来创建新的操作符。
  2. 重载操作符必须有一个类类型参数。
  3. 用于内置类型的运算符,其含义不能改变。
  4. 作为类成员函数重载时,其形参看起来比操作数少1,因为成员函数的第一个参数为隐藏的this。
  5. ".*","::","sizeof","?:","." 这五个运算符不能重载。

5.2赋值运算符重载

  1. 赋值运算符重载个数:

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

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

    3. 检测是否有自己给自己赋值。

    4. 返回*this:要符合连续赋值的含义。

      cpp 复制代码
      class Date
      {
      public:
      	Date(int year = 1900, int month = 1, int day = 1)
      	{
      		_year = year;
      		_month = month;
      		_day = day;
      	}
      
      	Date(const Date& d)
      	{
      		_year = d._year;
      		_month = d._month;
      		_day = d._day;
      	}
      
      	Date& operator=(const Date& d)
      	{
      		if (this != &d)
      		{
      			_year = d._year;
      			_month = d._month;
      			_day = d._day;
      		}
      
      		return *this;
      	}
      private:
      	int _year;
      	int _month;
      	int _day;
      };
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数。

    cpp 复制代码
    class Date
    {
    public:
    	Date(int year = 1900, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	int _year;
    	int _month;
    	int _day;
    };
    // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
    Date& operator=(Date& left, const Date& right)
    {
    	if (&left != &right)
    	{
    		left._year = right._year;
    		left._month = right._month;
    		left._day = right._day;
    	}
    	return left;
    }
    // 编译失败:
    // error C2801: "operator ="必须是非静态成员

    赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

    1. 用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
      内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
      重载完成赋值。

      cpp 复制代码
      class Time
      {
      public:
      	Time()
      	{
      		_hour = 1;
      		_minute = 1;
      		_second = 1;
      	}
      	Time& operator=(const Time& t)
      	{
      		if (this != &t)
      		{
      			_hour = t._hour;
      			_minute = t._minute;
      			_second = t._second;
      		}
      		return *this;
      	}
      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;
      	Date d2;
      	d1 = d2;
      	return 0;
      }

      注意:如果类中未涉及到资源管理,那么赋值运算符是否实现都可以;一旦涉及到资源管理则必须要显式实现。如果没有显示的实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的赋值运算符重载。这就会引起一个老生常谈的问题,拷贝过程中会导致两个对象共享同一片地址空间,这会引起两个问题:

      1. 其中一个对象原来的空间丢失了,导致内存泄漏。
      2. 共享同一块内存空间,最后销毁时会把同一块内存空间释放两次而引起程序崩溃。

      5.3前置++和后置++的重载

      cpp 复制代码
      class Date
      {
      public:
      	Date(int year = 1900, int month = 1, int day = 1)
      	{
      		_year = year;
      		_month = month;
      		_day = day;
      	}
      	// 前置++:返回+1之后的结果
      	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
      	Date& operator++()
      	{
      		_day += 1;
      		return *this;
      	}
      	// 后置++:
      	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
      	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器会自动传递
          // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
      	// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
      	Date operator++(int)
      	{
      		Date temp(*this);
      		_day += 1;
      		return temp;
      	}
      private:
      	int _year;
      	int _month;
      	int _day;
      };
      int main()
      {
      	Date d;
      	Date d1(2022, 1, 13);
      	d = d1++;
      	d = ++d1; 
      	return 0;
      }

六、const成员

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

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
	void Print() const
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
void Test()
{
	Date d1(2022, 1, 13);
	d1.Print();
	const Date d2(2022, 1, 13);
	d2.Print();
}

七、取地址及const取地址操作符重载

这两个运算符一般不需要重载,使用编译器生成的默认取地址重载即可,只有特殊情况才需要重载。

cpp 复制代码
class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
相关推荐
秃头佛爷1 小时前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨1 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷6 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode