类和对象(二)

六个默认的成员函数

对于一个类来说,即使我们在类中什么函数也不定义,编译器也会自动为我们生成一系列函数,我们称之为默认成员函数,原因就在于如果用户什么函数也不定义,那么编译器就会自动生成。一般来说有6个默认成员函数:

  1. 构造函数:主要对实例化的对象进行初始化;
  2. 析构函数:主要完成清理工作;
  3. 拷贝构造是使用同类对象初始化创建对象;
  4. 赋值运算符重载:主要把一个对象赋值给另一个对象;
  5. 取地址重载:主要是普通对象和const对象取地址;

构造函数

构造函数是一个特殊的成员函数,构造函数的名字与类名相同,创建类对象时由编译器自动调用,确保每个数据都有初始值。构造函数具有以下特征:

  • 函数名与类名相同;
  • 无返回值;
  • 实例化对象时,编译器自动调用;
  • 构造函数可重载
  • 若果用户没有定义构造函数,编译器会自动生成一个无参数的构造函数,若用户定义构造函数,则编译器不再自动生成。

构造函数的作用在于当我编写代码时没有对创建的类对象进行初识化的时候,编译器会自动调用一个构造函数对这个类对象进行初始化。而对于这个默认构造函数又分为三大类:

  1. 用户没有写时,编译器自动生成的无参的构造函数;
  2. 全缺省的构造函数;
  3. 用户自己编写的无参的构造函数;

以上三种都叫做默认构造函数,注意他们不仅是构造函数,还是默认的构造函数!!!

如上图,我编写三个默认构造函数,此时代码就会报错 “ 含有多个默认构造函数 ” 。但是,如果我没有写默认构造函数时,系统会自动生成一个默认的构造函数,并且这个函数我们是看不见的。

1、那么构造函数是如何初始化类对象的呢以及嵌套类对象???

如上图,在不同的默认构造函数下代码的执行结果可以看出,当我们实例化一个类对象后,编译器就会根据这个类的构造函数对实例化的类对象进行初始化。如果说用户没有编写默认的构造函数则编译器会自动生成一个无参的默认构造函数;如果说用户编写了默认的构造函数,此时系统不再生成而是调用用户编写的默认的构造函数。

如上图,当我在Person的类对象中添加Animal的类对象Animal _a1时:在调试状态下,我会发现,当代码调试到第24行准备进入Person对Person内的变量初始化时,此时光标会进入到Animal类中,调用Animal的构造函数对Animal的变量进行初始化,完成Animal的初始化后再进入到Person构造函数中对Person变量初始化。当然,我这里想强调的不是初始化的问题,而是当一个类对象(Person)中含有另一个类对象(Animal)的实例化对象(_a1)时,构造函数是如何对其(Person)内部的类对象(_a1)进行初始化,其具体代码步骤是如何进行的。


2、如果Person是默认生成的构造函数呢???

如上图,当Person中的构造函数是默认生成的时,此时对于Person类对象的变量成员来说,使用编译器生成的无参的默认构造函数,因此Person类对象的成员变量就是随机值。而对于Animal实例化的_a1类对象成员来说,Person会调用_a1对应的默认构造函数进行初始化。并且cpp中对于这种类型有分别:

  • 内置类型/基本类型:int/char/double/指针构成的变量;
  • 自定义类型:class、struct定义的类型。

在默认生成的构造函数中,cpp会对自定义类型进行初始化,而对于内置类型则不做处理。例如,上述的_a1就是自定义类型,编译器会调用它对应的构造函数初始化,而对于_weight和_age变量不做处理(随机值)。


3、如果Animal中不存在默认的构造函数呢???

如上图,当我在Animal定义了一个非默认的构造函数,此时编译器就不会再生成默认的构造函数,在标题1中我说明了当类对象中嵌套一个类对象的初始化步骤。因此这里当Person也会相应的调用Animal的默认构造函数对_a1进行初始化,但是不巧的是此时Animal已经有构造函数,不会再生成默认的构造函数,那么当Person想要调用的时候就会报错。


补充:这里实例化Person对象的时候,因为类中已经有构造函数,所以编译器不会在生成无参的构造函数,因此在实例化p1时,类中必须要有无参的构造函数,实例化p2时要有含两个参数的构造函数,p3同理。

析构函数

析构函数:对象在销毁的时候会自动调用析构函数,将对象的资源进行清理。

析构函数也是成员函数:

  • 析构函数在类名前要加~;
  • 析构函数无参数、无返回值;
  • 一个类中只能有一个析构函数,如果没有显示定义,编译器会自动生成(不能重载);
  • 类对象生命周期结束时,编译器会自动调用析构函数。

析构函数在内置类型和自定义类型的处理上与默认构造函数类似:

  1. 析构函数对内置类型不做处理,;
  2. 析构函数对于自定义类型会调用他对应的析构函数处理;

如上图,接下里演示析构函数的代码运行:

如上图,在第一阶段,代码从第78行到第80行:main函数中对q1的初始化,先调用Stack的默认构函数对_s1和_s2进行初始化,此时代码完成自定义类型的初始化工作;

如上图,在第二阶段,代码从第80行到第81行:此时q1的生命周期即将结束,编译器会清理Stack中新开的资源(malloc产生的),因此就会调用析构函数,注意这里的析构函数会被调用两次,因为q1中存在两个Stack实例化对象,结果如下图。

对于析构函数:

  1. 析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象;
  2. 全局对象先于局部对象进行构造;
  3. 局部对象按照出现的顺序进行构造,无论是否为static;

拷贝构造函数 

拷贝构造函数:就是对已经存在的类对象进行拷贝;

  • 拷贝构造函数是对构造函数的重载;
  • 拷贝构造函数的参数是对类对象的引用,不能使传值调用;

1、类对象传参时的拷贝构造函数

接下来演示拷贝函数的代码细节:

如上图,实例化一个p1类对象,并完成初始化,此时代码即将运行第127行:

如上图,此时调试代码运行时过程就是:在光标到127行时,此时就会调用111行的拷贝构造函数(因为自定义类型传参需要调用拷贝构造函数),代码执行完成后返回127行进入到120行Test函数中(当然这是个空函数),Test函数执行完成后代码进入129行,此时整个过程结束。在这个过程中,Test传参列表中的p根据主函数的p1进行初始化,而p1是自定义类型的变量,因此需要调用拷贝构造函数。

一般对于拷贝构造函数中的参数用const修饰,防止代码错误造成原类对象数据被修改。

注意:

如上图,当Test函数用引用变量传参的时候此时就不会在调用拷贝构造函数,因为Test函数中的变量p是主函数中类对象p1的别名(本质上是同一个对象)。

2、默认生成的拷贝构造函数

如上图,当用户没有编写拷贝构造函数时,编译器会默认生成拷贝构造函数,对类对象进程拷贝。

3、拷贝构造函数的特点

  1. 内置类型的成员会进行值拷贝(浅拷贝);
  2. 自定义类型的成员会调用这个成员的拷贝构造函数;

如上图,Queue中含有内置类型成员变量和自定义类型成员变量,代码执行过程:光标在62行开始执行,会先进入Stack的构造函数中对s1和s2进程实例化构造,完成后光标从62行进入到63行中,此时代码会进入Stack中的拷贝构造函数中第39行代码,对q1中的s1和s2进行两次拷贝构造函数调用,而对于q1中的_size这种内置类型来说会直接进行值拷贝,也就是直接将_size的值传给s2中的_size。

4、调用拷贝构造函数的情况:

  1. 对象初始化对象;
  2. 函数参数以对象传递;
  3. 函数的返回值以对象返回;

赋值运算符重载

在类对象中,内置类型可以直接使用各种运算符等,但是自定义类型无法直接使用:

如上图,内置类型可以直接使用是否相等,但是自定义类型不能直接使用;为了能让自定义类型也能使用各种运算符,这里就引入了运算符重载的规则。

如上图,使用关键字operator,使用规则是:返回值类型   operator  操作符  (参数);但是类中的operator其实本质是:

bool operator==(Date* this,Date d){
    return this->year == d.year
        && this->month == d.month
        && this->day == d.day
}

其实这里隐藏了一个this指针,因此我们传参的时候也就相应的需要减少一个;

如上图,类对象传参都会涉及到拷贝构造函数,因此一般都使用引用的方式进行传参,别忘记加上const修饰。

如上图,当使用赋值运算符重载时,将d2赋值给d1,这里的d1=d2在编译器中会帮助我们直接处理成d1.operator(d2),注意:

  • 在operator中隐藏一个Date*类型的this指针参数,在这里也就是d1的地址;
  • d2对应的就是第二个参数,其类型是Date;

赋值运算符在连续赋值的情况下

在开始之前先了解连续赋值:

//代码一:
int a = 88,b;
b = a;

//代码二:
int i = 99,j,k;
j = k = i;

如上图,在代码一中:b=a的返回值是b;而在代码二中:j = k = i的返回值先是k再是j,因为只有当k作为返回值才能将k的值赋值给j。

如上图,当我使用同样的赋值运算重载函数时代码就会出现问题,这是因为:正如刚才所述,当d1=d2时,此时会调用operator函数,但是该函数并没有返回值,因此想要给d3赋值时就会报错(operator=没有返回值,连续赋值就会报错)。

如上图,当给赋值运算符重载函数添加返回值后,代码就不会报错。但是这里的运算符重载函数时传值返回,传值返回会涉及到拷贝函数的问题,也就是说这里还会调用该类对应的拷贝构造函数,因此为避免这个问题,将运算符重载函数的返回值改为引用类型返回即可避免。

Date& operator=(const Date& d) {
	_year = d._year;
	_month == d._month;
	_day = d._day;
	return *this;
}

还需要注意的一处:

在刚才的代码中,如果d4和d5的实例化按照上述所示Date d4();Date d5();这样不会实例化对象,这种做法实际上会被解析成函数声明,不接受参数且返回值类型是Date的函数。因此正确声明方法是Date d4;并且类中要声明无参数的构造函数。

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值