C++类与对象(中)

Posted aaaaaaaWoLan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类与对象(中)相关的知识,希望对你有一定的参考价值。

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情
况下,都会自动生成下面6个默认成员函数。

class Date {};

构造函数

概念

对于以下的日期类:

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	void Display()
	{
		cout <<_year<< "-" <<_month << "-"<< _day <<endl;
	}

private:
int _year;
int _month;
int _day;
};

int main()
{
	Date d1,d2;
	d1.SetDate(2018,5,1);
	d1.Display();
	
	Date d2;
	d2.SetDate(2018,7,1);
	d2.Display();
	
	return 0;
}    

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

特性

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

  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); // 调用带参的构造函数
    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    
    Date d3();
    }
    

    对于Date d3();

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

    class Date
    {
    public:
    /*
        // 如果用户显式定义了构造函数,编译器将不再生成
        Date (int year, int month, int day)
        {
     	   _year = year;
     	   _month = month;
     	   _day = day;
        }
    */
    private:
    int _year;
    int _month;
    int _day;
    };
    
    void Test()
    {
    // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
    	Date d;
    }
    
  6. 无参的构造函数全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参
    构造函数、全缺省构造函数、我们没写时编译器默认生成的构造函数,都可以认为是默认成员函数。尽量使用全缺省构造函数

    // 默认构造函数
    class Date
    {
    public:
        Date()
        {
     	   _year = 1900 ;
     	   _month = 1 ;
     	   _day = 1;
        }
        
        Date (int year = 1900, int month = 1, int day = 1)
        {
     	   _year = year;
     	   _month = month;
     	   _day = day;
        }
    private :
    int _year ;
    int _month ;
    int _day ;
    };
    
    // 以下测试函数能通过编译吗?
    void Test()
    {
        Date d1;
    }
    

    编译结果:

    我们可以看到编译器对其报错:Date含有多个默认构造函数。说明一个类只能包含一个默认构造函数。

  7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1对象调用了编译器生成的默认构造函数,但是d对象_ year/_ month/_ day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

    解答:

    C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

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

    上述代码就是编译器使用了自身的默认构造函数,使Date的_t成员调用了它自己的默认构造函数。

  8. 成员变量的命名风格

    // 我们看看这个函数,是不是很僵硬?
    class Date
    {
    public:
        Date(int year)
        {
            // 这里的year到底是成员变量,还是函数形参?
            year = year;
        }
    private:
    int year;
    };
    
    // 所以我们一般都建议这样
    class Date
    {
    public:
        Date(int year)
        {
        _year = year;
        }
    private:
    int _year;
    };
    
    // 或者这样。
    class Date
    {
    public:
        Date(int year)
        {
        	m_year = year;
        }
    private:
    int m_year;
    };
    

    看第一种命名方式的结果:

d1的year最后没有被初始化为1,而是随机值,因为在Date构造函数中,等号左边的year代表的是局部变量year,即参数,所以实际上是参数year自己在给自己赋值,并没有对成员变量year赋值,编译器使用了就近原则。

所以成员变量最好加标识

析构函数

概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

这里的资源清理函数并不是指将数据从栈上pop掉,而是对已经申请的堆空间进行销毁,关闭已经打开的文件,将成员变量置为合适的值等。

特性

析构函数是特殊的成员函数。
其特征如下:

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

  2. 无参数无返回值。

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

  4. 对象生命周期结束时,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;
    };
    
  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,与构造函数相同,编译器生成的默认析构函数,会对自定类型成员调用它的析构函数。

    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;
    };
    
    class Person
    {
    private:
    	String _name;
    	int _age;
    };
    
    int main()
    {
    	Person p;
    	return 0;
    }
    

    结果:

说明编译器自动生成的析构函数是会使自定义类型成员调用它自己的默认析构函数的。

当我们在一个类里面,将其他类作为成员时,在该类里我们就不用再写这些类的构造函数和析构函数,因为编译器会帮我们调用它们自己的。比如题目:用两个栈实现一个队列。当然,内置类型还是需要进行构造和初始化的。

拷贝构造

概念

拷贝构造就像构造函数的一个双胞胎兄弟。

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

特征

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

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

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

    示例:当我们想把d1拷贝给d2时

    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);
        return 0;
    }
    

    Date d2(d1) 等价于 Date d2 = d1

    为什么要用引用的方式传参?

    如果我们把拷贝构造函数的传参方式写为:

    Date(Date d)
    {
        _year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    

    则会引发无穷的递归:

    形参d是实参d1的一份拷贝,所以,它也要调用拷贝构造函数:

    所以参数的传递要改成引用的形式

    当然,最好在形参的类型前加上const,防止使用时失误修改了形参,进而修改了在外面的实参。

  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
    贝,这种拷贝我们叫做浅拷贝,或者值拷贝

    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;
    }
    
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
    日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

    // 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
    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 s2 = s1 的指令,所以s1将自己的值,也就是"hello\\0"的首字符地址赋给了s2,s2也指向了这块空间,指向这块空间没有错,错的是执行析构函数时,对同一块空间free了两次,导致了错误。

    如果我们把析构函数中的free删除,结果就不会报错了

这么写虽然不会报错,但这个做法并不好,析构函数就是为了节省资源,这么写就达不到我们想要的目的了,所以在特殊情况下,还是要自己写拷贝构造函数。

运算符重载

运算符重载

自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调用这个重载函数 。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意 :

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

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

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

  • ..*::sizeof?: 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。 重载操作符必须有一个类类型或者枚举类型的操作数

// 全局的operator==
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;
}

void Test ()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout<<(d1 == d2)<<endl;
}

d1 == d2就相当于operator==(d1, d2)

但为了保证我们成员变量的私有性,不能以这种全局形式的函数比较成员变量。

下面就是将运算符重载函数放在类里面,这样就保护了我们成员变量的私有性。

class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d2)
{
    return _year == d2._year;
	&& _month == d2._month
	&& _day == d2._day;
}
private:
	int _year;
	int _month;
	int _day;
};
void Test ()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout<<(d1 == d2)<<endl;
}

关于这一段函数:

bool operator==(const Date& d2)
{
    return _year == d2._year;
	&& _month == d2._month
	&& _day == d2._day;
}

函数的参数,bool operator==(const Date& d2) 就相当于 bool operator==(const Data* this, const Date& d2)

d1 == d2 就相当于 operator==(&d1, d2)

赋值运算符重载

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

赋值运算符主要有四点:

  1. 参数类型

  2. 返回值

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

  4. 返回 * this

    在类的函数里就已经可以完成赋值了,为什么要返回* this呢?

    我们先看赋值运算符=,可以实现a = b = c的连续赋值,连续赋值的原理是,b=c会有一个返回值,大小为b,所以a = b,可以将这个返回值b赋给a。

    既然这样,我们返回类型是写成Date还是Date&呢?

    最好是写成Date&,这样相比写成Date可以少一次拷贝调用,之前就说到过,函数生成的返回值是一个临时拷贝的常变量,所以实际上会先进行一次拷贝构造,再将拷贝构造的结果给返回。

    而如果是引用返回,就不用进行拷贝构造,因为创造的临时变量并不会开辟空间,只是返回值的一个别名。

  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

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

Date(d4) = d1 (d4未创建)是运算符赋值重载还是拷贝构造呢?

答案是:拷贝构造。正因为d4未创建,所以要用拷贝构造来实现这个赋值功能。所以Date(d4) = d1就等价于Date d4(d1)。

那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
    
    String(const char* str = "")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }

    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }
    
private以上是关于C++类与对象(中)的主要内容,如果未能解决你的问题,请参考以下文章

[ C++ ] C++类与对象之 类中6个默认成员函数

C++ 类与对象(上)

C++基础总结:C++ 类与对象初探——继承和重载

C++从入门到入土第四篇:类与对象(中篇)

C++类与对象(中)

从零开始学c++——类与对象