C++类与对象(中)

类的六个默认成员函数

任何一个类在我们不写的情况下,编译器都会自动生成六个默认成员函数。

class Date{};//Date类是一个空类,会自动生成6个默认成员函数

构造函数

概念

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

特性

构造函数是特殊的成员函数,构造函数的主要任务是初始化对象

其特性如下:

  1. 函数名与类名相同

  2. 无返回值

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

  4. 构造函数可以重载

    class Date
    {
    public :
     // 1.无参构造函数
     Date ()
     {}
    // 2.带参构造函数
     Date (int year, int month , int day)
     {
         _year = year ;
         _month = month ;
         _day = day ;
     }
    private :
     int _year ;
     int _month ;
     int _day ;
    };
    void TestDate()
    {
     Date d1; // 调用无参构造函数
     Date d2 (2015, 1, 1); // 调用带参的构造函数
    
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:调用了无参构造函数,该函数无参,返回一个日期类型的对象
Date d3;
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(初始化),一旦用户显式定义编译器将不再生成。

    //显示定义了构造函数
    class Date
    {
    public:
        // 如果显式定义了构造函数,编译器将不再自动生成构造函数
        Date (int year=1, int month=1, int day=1)
        {
            _year = year;
            _month = month;
            _day = day;
        }
        void Print()
        {
            cout<<_year<<endl;
            cout<<_month<<endl;
            cout<<_day<<endl;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    void Test()
    {
        Date d;
        d.Print();//打印1 1 1
    }
    
    //不定义构造函数
    class Date
    {
    public:
        // 如果没有定义构造函数,编译器将自动生成构造函数
        void Print()
        {
            cout<<_year<<endl;
            cout<<_month<<endl;
            cout<<_day<<endl;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    void Test()
    {
        Date d;
        d.Print();//打印随机值
    }
    
  2. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

    在特性5中,不定义构造函数的情况下,会打印随机值,但是class Date中有自动生成的构造函数,构造函数会对d初始化的,为什么还是随机值?

    //因为自动生成的构造函数,只会对自定义类型初始化,而不会对内置类型初始化
    //补充:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 `int/char/double/指针...`,自定义类型就是我们使用`class/struct/union`自己定义的类型,
    

    接下来我们用一个代码详细解释自动生成的构造函数初始化

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

    解释:

    调试这个程序,我们可以看到

    在d中内置类型是随机值,自定义类型被初始化了.

    在主函数中,Date类实例化一个对象d,调用构造函数进行初始化,
    自动生成的构造函数不对内置类型初始化.对于自定义类型,自动生成的构造函数会调用这个自定义类型的默认构造函数对这个自定义类型的成员变量初始化.
    //注意:在一个类中只能有一个默认构造函数
    
  3. 成员变量的命名风格

    //错误写法
    class Date
    {
    public:
        Date(int year)
        {
            //这里两个year都会被当成是函数传参,而成员变量不会被初始化
            year=year;
        }
    private:
        int year;
    };
    //正确写法
    class Date
    {
    public:
        Date(int year)
        {
            _year = year;
        }
    private:
        int _year;
    };
    
  4. C++11改进

    class MyQueue
    {
    public:
        void push(int x)
        {
    
        }
        int pop()
        {
    }
    private:
        //C++11打的补丁,针对编译器自己生成默认成员函数不初始化的问题
        //给的缺省值,编译器自己生成的默认构造函数用
        int _size=0;//不是初始化,这是声明
    
        Stack _st1;
        Stack _st2;
    };
    

结论

一般情况一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成

  1. 类里面成员都是自定义类型成员,并且这些成员都提供了默认构造函数
  2. 如果还有内置类型成员,声明时给了缺省值

析构函数

概念

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

特性

析构函数是特殊的成员函数

特性如下:

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

  2. 无参数无返回值

  3. 不能重载

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

  5. 对象周期结束时,c++编译系统自动调用析构函数

    typedef int DataType;
    class SeqList
    { 
    public :
        SeqList (int capacity = 10)
        {
            _pData = (DataType*)malloc(capacity * sizeof(DataType));
            assert(_pData);
     
            _size = 0;
            _capacity = capacity;
            }
     
        ~SeqList()
        {
            if (_pData)
            {
                free(_pData ); // 释放堆上的空间
                _pData = NULL; // 将指针置为空
                _capacity = 0;
                _size = 0;
            }
        }
     
    private :
        int* _pData ;
        size_t _size;
        size_t _capacity;
    };
    
  6. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认 析构函数,对会自定类型成员调用它的析构函数。

    class Date
    {
    public:
        void Print()
        {
            cout<<_year<<"—"<<_month<<"-"<<_day<endl;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
                      
    int main()
    {
        Date d1;
        /出了作用域,会自动调用析构函数(不是销毁),是资源清理。
        /在栈销毁时,d1自动销毁
        /有些情况不需要资源清理,有些需要
        Date d2;
        /d1先构造,d2后构造
        /d2先析构,d1后析构(在栈中,后进先出)
        return 0;
    }
    

    自动生成的默认析构函数

    不会对内置类型处理

    对自定义类型处理

拷贝构造函数

概念

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

特征

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

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

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

  3. 在特性2中为什么会发生无穷递归

    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;
     }
    private:
        int _year;
        int _month;
        int _day;
    };
    int main()
    {
        Date d1;
        Date d2(d1);//1
        return 0;
    }
    
    //1
    因为在调用拷贝构造函数时,如果是传值传参,需要调用拷贝构造,调用拷贝构造需要传参,无限递归。
    
  4. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝

    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;
    };
    int main()
    {
        Date d1;
        / 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
        Date d2(d1);
        return 0;
    }
    
  5. 编译器生成的默认拷贝构造函数可以完成字节序的值拷贝,对于像Date类这种类自动生成的拷贝构造函数是足够的,但是对于向Stack这种类是有问题的。

    // 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
    class String
    {
    public:
        String(const char* str = "jack")
        {
            _str = (char*)malloc(strlen(str) + 1);
            strcpy(_str, str);
        }
        ~String()
        {
            cout << "~String()" << endl;
            free(_str);
        }
    private:
        char* _str;
    };
    int main()
    {
        String s1("hello");
        String s2(s1);
        /主函数中实例化了两个string类对象,两个对象中的_str指向同一块空间,S2调用析构函数把_str指向的空间释放掉,但是s1的析构函数会在释放一次,程序崩溃。(所以不能使用值拷贝)
        return 0;
    }
    /在对于stack这种类时,浅拷贝会出现问题
    /    1、指向一块空间,修改数据会互相影响
    /    2、这块空间析构时会释放两次,程序会崩溃
    /    解决方案:要自己实现拷贝构造,--深拷贝
    
  6. 默认生成的拷贝构造函数

    1、对于内置类型的成员会完成值拷贝,浅拷贝

    2、对于自定义类型的成员,会去调用这个成员的拷贝构造函数

结论

一般的类,自己生成拷贝构造就够用了。只用像stack这样的类,自己直接管理资源(主动动态开辟数组,主动回收)需要自己实现深拷贝

赋值运算符重载

运算符重载

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

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

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

注意:

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

  • 参数:运算符操作数,1、参数使用引用,2、如果参数不需要改变,使用const

  • 返回值:运算符运算后结果

  • 运算符重载后,就可以直接使用运算符

  • 重载操作符必须有一个类类型或者枚举类型的操作数

  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义

  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参

  • .*::sizeof?:. 注意以上5个运算符不能重载

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

int main()
{
    Date d1(2022, 6, 12);
    Date d2(2022, 6, 12);

    if (d1 == d2)//编译器会处理对应重载运算符调用(先调类内)if(operator==(d1,d2))
    {
        cout << "==" << endl;
    }

    return 0;
}

赋值运算符重载

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;
       }
   }
private:
   int _year ;
   int _month ;
   int _day ;
};

赋值运算符:

  • 返回值类型为Date&(能够连续赋值)

  • 返回*this

  • 参数类型为引用(防止调用构造函数)

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

  • 赋值重载如果不写,编译器会自动生成一个,完成对象按字节序的值拷贝。

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;
};
int main()
{
    Date d1;
    Date d2(2018101);
 
 // 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
    d1 = d2;
    return 0;
}
  • 编译器生成的默认赋值重载函数是字节序的值拷贝,对于像日期类是足够的,但是对于下面这种类会出现崩溃.
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
    String(const char* str = "")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }
    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }
private:
    char* _str;
};
int main()
{
    String s1("hello");
    String s2("world");
 
    s1 = s2;
    return 0;
}

取地址运算符的重载

Date* operator&()
{
   return this;
}

const Date* operator&() const
{
   return this;
}

这两个取地址重载属于默认成员函数,不用自己写,会自动生成使用大部分情况

当两个函数都存在时,会调用最匹配的

Date类的实现

(74条消息) Date类的实现_百言灵都的博客-CSDN博客

Date类的实现的补充

cin 流提取cout流插入

看以下代码

void TestDate7()
{
    Date d1;
    cin>>d1;//1 cin是流提取
    cout<<d1;//2 cout是流插入
    
    int i=1;
    double d=2.2;
    cout<<i;//3
    cout<<d;//4
}

cin cout自动识别内置类型,无法识别自定义类型

//1
错误,因为cin cout无法识别自定义类型
//2
错误,因为cin cout无法识别自定义类型
//3
输出1,因为cin cout自动识别内置类型
//4
输出2.2,因为cin cout自动识别内置类型

cin cout如何识别内置类型?

cin和cout是包含在iostream中istream类型和ostream类型的对象

cincout能够自动识别内置类型是因为在ostream中已经写好了重载函数

cout流插入

cin,cout如何识别自定义类型?

自己写<<重载函数

void operator<<(std::ostream& out)//因为要使用cout所以是ostream类型
{
    out << _year << "-" << _month << "-" << _day << endl;
}

但是,依旧报错

这是因为位置不对

1)cout<<d1;
/实际上是调用cout.operator<<(d1); -> operator<<(&cout,d1);
void operator<<(std::ostream& out) -> void operator<<(Date* const this, std::ostream& out);
/调用函数时,形参和实参对应不上,发生错误
补充:
(2)我们再来看一下cout<<内置类型
int i=1;
cout<<i;
/实际上是调用operator<<(&cout,i);
ostream& operator<<(int val) -> ostream& operator<<(ostream* const this,int val)
/调用函数时,形参与实参对应上,运行成功

所以可以采用以下写法:

但是这种写法不合理,需要进一步更改:

把它改成友元函数

1)在Date类的定义中写上friend void operator<<(std::ostream& out, const Date& d);2)在类外写<<重载函数(要写在cpp文件中,不能写在.h文件中)
void operator <<(std::ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
}

正确使用

cout<<d1;
/实际上是调用operator<<(cout, d1);

但是无法连续输出,所以需要返回值为cout

cout识别自定义类型<<重载函数正确写法

Date.h

Date.cpp

cin识别自定义类型>>重载函数正确写法

Date.h

Date.cpp

在Date类中,需要对日期的合法性进行检查,如果日期非法,程序崩溃
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值