C++程序设计教程课件_第1页
C++程序设计教程课件_第2页
C++程序设计教程课件_第3页
C++程序设计教程课件_第4页
C++程序设计教程课件_第5页
已阅读5页,还剩47页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

C++程序设计教程(第二版)9/19/20231第九章 对象生灭第九章内容9/19/20232构造函数设计构造函数的重载类成员初始化构造顺序拷贝构造函数析构函数对象转型与赋值1.构造函数设计9/19/20233

初始化要求定义一个对象,其形式与定义一个变量相似,但更具意义。对象与变量的不同在于对象对应于事物,要求从诞生之时起便有明确的意义.对象必须建立一种初始化机制,以满足在编程中可以针对实际问题所提出的要求而赋给一个有意义的初值。创建一个对象,若创建的是全局对象,则以全0的模式表示对象,若创建局部对象,则以随机值表示对象。对象的初始化不是简单的参数与成员对应,而是联系参数到成员的过程.封装性要求Struct

Point{int

x,y;};Point

d={2,3};class

Point{int

x,y;//…};Point

d={2,3};正确错误封装性的要求杜绝了这种初始化形式,事实上,这种形式根本不能完成对象的初始化过程中的校验和计算工作。对象通常是一个复杂的实体,在构建对象过程中,并不一定是一个初值对应一个对象分量。许多时候,一个初值只是传递一个信息,告诉对象的创建工作应怎么进行。封装性要求对象创建中按传递的信息进10/行1/20一23

个过程化的初始化工作。4

函数形式构造函数(constructor)是与类名同名的特殊的成员函数,当定义该类的对象时,构造函数将被自动调用以实现对该对象的初始化。构造函数不能有返回值,因而不能指定包括void在内的任何返回值类型。构造函数的定义体可与其它成员函数成员一样,放在类内或类外都可。构造函数的定义格式为:类名(形参说明){函数体}构造函数既可定义成有参函数,也可义成无参函数,要根据问题的需要来定。全局变量和静态变量在定义时,将自动赋初值为0;局部变量在定义时,其初始值不固定的。而当对象被定义时,由于对象的意义表达了现实世界的实体,所以一旦定义对象,就必须有一个有意义的初始值,在C++中,在定义对象的同时,给该对象初始化的方法就是利用构造函数。10/1/20235例:类person包括4个数据成员,用来记录人员信息。生成对象obj,并使用构造函数为obj赋予初始值。#

include<windows.h>#

include<iostream.h>class

Person{

private:char

name

[10]

;int

age

;int

salary

;chartel[8];public://定义类//类Person的数据成员//姓名//年龄//薪金//电话//构造函数PersonPerson

(

char

*xname,

int

xage,int

xsalary,

char

*xtel

)

;void

disp

();

};//函数Person的定义Person

::

Person

(

char

*xname,int

xage,

int

xsalary,

char

*xtel

){strcpy

(name,xname);//给各数据成员提供初值

age=xage;salary

=

xsalary

;10/1s/t2r0c2p3y

(tel,

xtel)

;}6//函数disp的定义

void

Person::disp(){

cout<<endl;10/1/20237姓名:"<<name<<endl;年龄:"<<age<<endl;

工资:"<<salary<<endl;电话:"<<tel<<endl<<endl;}cout

<<

"cout

<<

"cout

<<

"cout

<<

"//主函数void

main(

){

//生成对象obj并初始化Person obj("张立三",25,850,"45672314");//显示objobj.disp();}程序的执行结果是:姓名:张立三年龄:25工资:850电话:45672314在主函数中的Person

obj("张立三",25,850,"45672314");中完成了以下几个功能:定义并生成了对象obj。在生成对象obj的同时,自动调用相应类的构造函数Person将初始值"张立三",25,850,"45672314"传递给构造函数Person相应的形参xname,xage,xsalary,

xtel。执行构造函数体,将相应的值赋给相应的数据成员。10/1/20238

一次性对象10/1/20239创建对象时如果不给出对象名,直接以类名调用构造函数,则产生一个无名对象。无名对象经常在参数传递时使用。例如:cout<<Date(2003,12,23);Date(2003,12,23)是一个对象,该对象在做了<<操作后便烟消云散了,所以这种对象一般用在创建后不需要反复使用的场合。2.构造函数重载10/1/202310class

Date{public:Date(const

string&

s);//重载//

...};构造函数毕竟是函数,是函数就可以重载。不但可以重载,还可以设置默认参数。如果一个类中出现了两个以上的同名的成员函数时,称为类的成员函数的重载。intmain(){Date

d(“2006-12-26”);Date

e(2000,

12,

25);Date

f(2001,

10);Date(int

y=2003,

int

m=12,

int

d=1);//参数默认Dateg(2002);Date

h;//

...}Date

h;这行代码似乎是一个错误,对于对象的创建,即对于无参的构造函数的调用,应该是g(),加上类名,即写成:

Dateg();但是,从语法上来讲,这个语句也是名叫g的返回临时Date对象的函数声明!因此,C++语言在设计时,为了区别无初始化的对象定义和返回类对象的无参函数声明的差别,也为了保持无初始化变量定义与无初始化(无参)对象定义的一致性,规定无参对象定义语句为:10/1/202311int

a;int

b;Date

g;Dateg();//变量定义//返回整型值的无参函数声明//无参对象定义,加空括号后就成了无参函数声明//返回类对象的无参函数声明对于返回对象值的有参函数声明和有初始化的对象定义语句,由于函数声明中的参数有类型引导,而对象定义的初始化中无类型引导,它们本身(或在编译器看来)是可以区分的:10/1/202312Date

e(2002);Date e(int

y);//对象定义//函数声明C++规定,每个类必须有一个构造函数。如果在类中没有显式定义构造函数时,则C++编译系统在编译时为该类提供一个默认的构造函数,该默认构造函数是个无参函数,它仅负责创建对象,而不做任何初始化工作。class

Date{public://相当于定义了Date(){}};intmain(){Date

d;

//

ok//

...}与变量定义相似,在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的默认值为0,否则对象的初始值是不定的。10/1/202313只要一个类定义了一个构造函数(不一定是无参构造函数),C++编译系统就不再提供默认的构造函数。任何其他的构造函数定义,都将阻止默认无参空函数的产生:class

Date{public:Date(int

y,

int

m,

int

d){}//

...};int

main(){Date

d;

//

error//

...}10/1/202314当构造函数有缺省参数时,称为具有缺省参数的构造函数,在使用时要防止二义性。10/1/202315//定义类Myclass//构造函数Myclass如:class

Myclass{

private:intmember;public:Myclass();Myclass(int

i);……}Myclass:Myclass(){

member=10;

}Myclass:Myclass(int

i=10)//构造函数Myclass(int

i),该函数//的形参i为缺省参数{

member=i;

}void

main(){

Myclass

x(20);10/1/202316Myclass

y;//产生二义性,无法确定自动调用哪个构造//函数完成对象的构造……}回顾:上节课主要讲了构造函数的概念、意义及定义方式、构造函数的重载。10/1/2023173.类成员初始化10/1/202318

默认调用的无参构造函数在类定义中有数据成员和成员函数,数据成员可以是内部数据类型的变量实体,也可以是对象实体。如果一个类A的对象作为另一个类B的数据成员,则在类B的对象创建过程中,调用其构造函数的过程中,数据成员(类A的对象)会自动调用类A的构造函数。这样就会面临一些问题。例:有一个学号类和一个学生类,学生类中包含了学号类的对象,因此在构造学生类对象时,面临着学号类对象的构造。class

StudentID{int

value;

public:StudentID(){在学生类的构造函数中并没有看到学号类对象初始化的痕迹,而数据成员name倒被赋了初值。从运行的结果看,当学生类对象被构造时,一个学号对象也创建了,而且学号类的构造函数先于学生类对象的构造函数体的执行而执行。static

int

nextStudentID

=

0;value

=

++nextStudentID;cout<<"Assigning

student

id"<<value<<"\n";}};//-----------------------------------class

Student{string

name;StudentID

id;public:Student(string

n

=

"noName"){cout

<<"Constructing

student

"

+

n

+

"\n";name

=

n;}};//-----------------------------------int

main(){S1tu0/d1/2e0n23t

s("Randy");}//==============================19对于Student

s(“Randy”);其内部的执行顺序是这样的:先分配学生类对象s的空间,调用Student构造函数;在Student构造函数体尚未执行时,由于看到了类的对象成员ID,转而去调用学号类的无参构造函数;相当于执行定义语句:StudentID

id;执行了学号类构造函数体,输出结果的第1行信息,返回到Student构造函数;执行Student构造函数体,输出结果的第2行信息,完成全部构造工作。说明:先成员构造,后自身构造.成员构造不见显式调用,而是悄悄调用无参构造函数.对学号类构造函数的调用时

默认的,默认调用便是调用无参构造函数,而正好学号类

设计的构造函数就是无参构造函数。10/1/202320

初始化的困惑在刚刚的实例中,只是初始化了名称,而没有给定学号。如果,在学生对象创建中,既要初始化名称,又要给定一个指定的初始化学号,也就是不要默认调用无参构造函数,那么类该如何操作呢?如果不能在创建StudentID的过程中初始化对象,就不能将对象值设置到位。下面给出一个不正确的初始化尝试,还是对刚才的例子作修改:10/1/202321class

StudentID{intvalue;public:StudentID(int

id=0){value

=

id;cout<<"Assigning

student

id

"<<value<<"\n";}};//class

Student{string

name;StudentID

id;public:Student(string

n

=

"noName",

int

ssID=0){cout

<<"Constructing

student

"

+

n

+"\n";name

=

n;StudentID

id(ssID);}};//运行结果:intmain(){Student

s("Randy",58);10/1/2}0/2/3====================================22S对象初始化的学号为58,重新设计了Student构造函

数,将该值传递给了它,所以形参ssID的值也为58。但是我们看到转而调用的StudentID构造函数执行体却没有输出58,而是默认的0学号。这说明转而调用的仍然是无参构造函数。将StudentID的构造函数写成参数默认是为了让无参构造函

数调用也能通过,以便让整个程序顺利运行。乃至后来,要想在Student类内,通过构造函数体中的对象初始化创建的定义语句来改变id的值,发现这仅仅只是区别于s对象中的id对象的一个局部对象,等到构造函数结束时,该局部对象的局部生命期到期而不销毁。10/1/202323

初始化表调用构造函数如果一个类A的对象作为另一个类B的数据成员,则在类B的对象创建过程中,调用其构造函数的过程中,数据成员(类A的对象)会自动调用类A的构造函数。如果类A的构造函数为有参函数时,则在程序中必须在类B的构造函数的括号后面加一“:”和被调用的类A的构造函数,且调用类A的构造函数时的实参值必须来自类B的形参表中的形参。这种方法称为初始化表的方式调用构造函数。这种在构造函数的参数列表的右括号后面,花括号前面,用冒号引出构造函数的调用表,可以省略类型名称,但却行创建对象之职。10/1/202324类X的构造函数的定义格式应为:

X::X(参数表0):成员1(参数表1),成员2(参数表

2),…,成员n(参数表n){

……}其中,参数表1提供初始化成员1所需的参数,参数表2提供初始化成员2所需的参数,依此类推。并且这几个参数表的中的参数均来自参数表0,另外,初始化X的非对象成员所需的参数,也由参数表0提供。在构造新类的对象过程中,系统首先调用其子对象的构造函数,初始化子对象;然后才执行类X自己的构造函数,初始化类中的非对象成员。对于同一类中的不同子对象,系统按照它们在类中的说明顺序调用相应的构造函数进行初始化,而不是按照初始化表的顺序。10/1/202325例:#include<iostream>using

namespacestd;//class

StudentID{intvalue;public:StudentID(int

id=0)//有参构造函数{value=id;cout

<<"Assigning

student

id

"

<<value

<<endl;}};//对刚才的例子进行改进10/1/202326class

Student{string

name;StudentID

id;public:Student(string

n="no

name",

int

ssID=0):id(ssID),name(n){cout<<"Constructing

student

"<<n<<"\n";}};//intmain(){Student

s("Randy",98);Student

t("Jenny");}//==========================10/1/2023274.构造顺序10/1/202328在程序中,各种作用域的对象很多,有些对象还包含在别的对象里面,有些对象早在main函数开始运行之前就已经建立了。创建对象的惟一途径是调用构造函数。构造函数时一段程序,所以构造对象的先后顺序不同,直接影响程序执行的先后顺序,导致不同的运行结果。C++给构造对象的顺序做了专门的规定。

局部对象局部和静态对象是指块作用域(局部作用域)和文件作用域的对象。它们的声明顺序与它们在程序中出现的顺序是一致的。见课本307页例9.8在C中,所有的局部变量(没有对象)都是在函数开始执行时统一创建的,创建的顺序是根据变量在程序中按语句行出现的顺序。而C++却不同,它是根据运行中定义对象的顺序来决定对象创建的顺序。而且,静态对象只创建一次。10/1/202329

全局对象和全局变量一样,所有全局对象在主函数启动之前,全部已近被构造。因为构造过程是程序语句的执行过程,所以,可以想象在程序启动之前,已经有程序语句(构造函数)在那里被执行过了。同一工程不同代码文件全局对象的创建没有明确顺序规定.对策:不要让不同文件的全局对象互为依赖.因为依赖具有先后性,而其全局对象的创建不能保证该依赖性发挥作用.全局对象在main函数启动之前生成,而调试则在main函数启动之后.对策:调试时,应先将全局对象作为局部对象来运行观察.或者,在构造函数中添加输出语句来观察运行过程.10/1/202330

成员对象成员对象的构造顺序按类定义的出现顺序classA{public:A(int

x){

cout<<"A:"<<x<<"->";

}};//classB{public:B(int

x){

cout<<"B:"<<x<<"->";

}};//class C按A、B的顺序定义对象成员,其构造函数又按b、a的顺序进行构造。到底听谁的,以定义顺序说了算。classC{A

a;B

b;public:C(int

x,int

y):b(x),a(y){

cout<<"C\n";

}};//-----------------------------------intmain(){C

c(15,9);}1/0/=/1=/2=02=3========================31

构造位置全局数据区:全局对象,静态全局对象,静态局部对象,常对象类的静态数据成员也存放在该数据区。目的是为了让对象的生命周期与运行的程序等寿命。栈区:局部对象在函数中定义好、局部对象,随着被调用的返回而析构(销毁)。动态存储区(也称堆区):用new申请的对象用delete销毁。特殊地址空间除此之外,还可以指定特殊地址空间,存放对象10/1/202332回顾10/1/2023335.拷贝构造函数10/1/202334对象本体与对象实体:对象本体也是对象主体,对象实体则还包括属于对象的衍生物,如,某个人体是人类对象的主体,然而某人还拥有父母,房产等属于某人的世系或资产,描述人的属性不仅仅只是人体数据.从形式上看,对象除了包括数据成员,还包括指向数据的指针.对象本体与对象实体是不一致就是说有时我们会需要把a对象的属性全部转给b对象,对象本体与对象实体一致就是你要将a对象的属性赋给a对象本身。对于一个简单的变量的初始化方法是用一个常量或变量初始化另一个变量,例如:double

x=12.36;double

y=x;疑问:前面介绍了用构造函数初始化对象,那么能不能像简单变量的初始化一样,直接用一个对象来初始化另一个对象呢?答案是肯定的。例:以定义的一个Point类为例,说明用一个对象来初始化另一个对象。Point

pt1(10,20);Point

pt2=pt1;那么,后一句也可写成Point

pt2(pt1);它是用pt1初始化pt2,此时,pt2各个成员的值与pt1各个成员的值相同,也就是说,pt1各个成员的值被复制到pt2相应的成员当中。在这个初始化过,实际上调用了一个拷贝构造函数。1程0/1/当202中335拷贝构造函数:拷贝构造函数是C++中引入的一种新的构造函数。定义一个拷贝构造函数的方式是:类名(const

类名

&形式参数){

函数体}由此可看出拷贝构造函数的特点:拷贝构造函数的名称与类的名称相同,且它只有一个参数,该参数就是对该类对象的引用。拷贝构造函数的功能是用于实现对象值的拷贝,通过将一个同类对象的值拷贝给一个新对象,来完成对新对象的初始化,即用一个对象去构造另外一个对象。(3)每个类都有一个拷贝构造函数10/1/202336以本类对象为常量引用参数的构造函数:class

Date{public:Date();Date(const

Date&

d);//

...};Date

x; //调用无参构造函数

Date

y(x);//调用拷贝构造函数10/1/202337例:默认拷贝构造函数:若类中没有定义拷贝构造函数,则系统会悄悄定义一个默认空拷贝构造函数:Date(const

Date&d){}默认拷贝构造函数体一定是空的.空拷贝构造函数负责将传递的对象到新创的对象做对象本体的位对位拷贝.(甚至连指针值都相等,即与参数对象拥有共同的资源)10/1/202338自定义拷贝构造函数:为了达到对象实体也就是对象整体复制目的,就需要另外定义一个拷贝构造函数,以覆盖默认的拷贝构造函数:见课本例315页9.13自定义拷贝构造名也是类名,它是构造函数的重载,一旦自定义了拷贝构造函数,默认的拷贝构造函数就不再起作用了。在自定义拷贝构造函数之前,我们进行拷贝对象构造时,都是在用默认的拷贝构造函数,因为那时的对象本体与对象实体是一致的。所以,自定义拷贝构造函数在对象本体与对象实体不一致时,便是需要的,否则无此需要。10/1/202339自定义拷贝构造函数体的工作不负责位对位对象复制,一般来说,它负责资源分配和由此而来的指针修改.

class

Person{char*

pName;public:Person(char*

pN="noName"){pName

=newchar[strlen(pN)+1];if(pName)

strcpy(pName,pN);}Person(const

Person&

s){pName

=newchar[strlen(s.pName)+1];if(pName)

strcpy(pName,

s.pName);}~Person(){delete[]

pName;}10/1/20}2;3406.析构函数10/1/202341当一个对象被定义时,系统自动调用构造函数为该对象分配相应的资源,当对象使用完毕后,这些系统资源需要在对象消失前被释放。人为的动态内存释放工作由析构函数来完成,析构函数时一种特殊的成员函数,它的意义是做关于对象失效之前瞬间的

善后工作。它的功能与构造函数的工作正好相反,是用来释放

一个对象。我们知道有内存申请,也就有内存释放。一般来说,需要定义构造函数的类也需要定义析构函数,不需要构造函数

的类,也无须定义析构函数。所以析构函数和构造函数是成对

出现的。定义:10/1/202342析构函数的定义方式为:~类名(){函数体}特点:析构函数名与构造函数名相同,但它前面必须加一个“~”,用以与析构函数相区别;在定义析构函数时,不能指定任何返回类型,也没有参数,而且不能重载。因此在一个类中只能有一个析构函数。析构函数可以被用户调用,也可以被系统调用。在下面两种情况下,析构函数会被自动调用a、如果一个对象被定义在一个函数体内,则当这个函数结束时,该对象的析构函数被自动调用。b、当一个对象是使用new运算符动态创建时的,在使用delete运算符自动释放时,delete将会自动调用析构函数。每一个类若没有显示地定义为一个类定义析构函数,编译系统会自动地生成一个默认的析构函数,默认析构函数时一个空函数,实际上什么操作都不进行。10/1/202343例:程序中三个类A、B、C分别有一个析构函数。代码如下:classA{public:A(){cout<<"A->";

}~A(){

cout<<"<-~A";

}};//-------------------------classB{public:B(){

cout<<"B->";

}~B(){

cout<<"<-~B";

}};//-------------------------classC{public:C(){

cout<<"C->";

}~C(){

cout<<"<-~C";

}};//-------------------------void

func(){cout<<"\nfunc:

";A

a;cout<<"ok->";static

B

b;C

c;}//----------------------------intmain(){cout<<"main:

";for(int

i=1;

i<=2;

++i){for(int

j=1;

j<=2;

++j)if(i==2)

C

c;

else

A

a;B

b;}func();func();}//=================10/1/202344注:(1)析构函数总是出现在对象的生命期结束之时。静态对象在程序运行结束时析构。对于一个简单的类来说,大多可以直接使用系统提供的默认析构函数。但是,如果在类的对象中分配有动态内存(如:用new申请分配的内容)时,就必须为该类提供适当的析构函数,完成清理工作。对象被析构的顺序与对象建立时的顺序正好相反。对象析构与构造函数的关系是栈数据结构中的入栈和出栈的关系,即最后构造的对象先被析构。栈底封闭指针10/1/2023457.

对象转型与赋值

用于转型的构造函数5/8与5.0/8的结果是不同的,原因是C++执行了两种不同的操作。编译器会将5.0/8中的整数8自动转换成double,匹配两个

double数的除法操作。这是内部数据类型所具有的转换功能。但是,对于类类型,其自动转换的功能必须编程实现。那就是定义一个含有一个参数的构造函数。例:class

Student{public:Student(const

string&n);};void

fn(Student&

s);intmain(){string

t=“jenny”;fn(t);//参数为string,却能匹配Student类型1}0/1/202346刚才的例子也是在告知,如何将string对象转换成一个Student对象。如果有重载函数:void fn(string

&s){cout<<“ok\n”;}则main函数中的fn(t)函数调用将会马上匹配。否则,匹配了void

fn(Student&

s)函数。从fn(Student&)和Student(conststring)可以推得fn(string)调用,这就是构造函数用来从一种类型转换成另一种类型的能力。对象转型的规则:只会尝试含有一个参数的构造函数如果有二义性,则会放弃尝试推导是一次性的,不允许多步推导10/1/202347

对象拷贝对象赋值即对象拷贝:两个已经存在的对象之间的复制Person

d(“Ranny”)Person

g;d=g; //对象赋值对象赋值便是使用类中的赋值操作符.如果类中没有定义赋值操作符,则系统悄悄地定义一个默认的赋值操作符:Person&

operator=(const

Person&

p){memcpy(*this,

*p,

sizeof(p));}10/1/202348当对象本体与对象实体不同时,则对象赋值操作符与拷贝构造函数一样

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论