- 构造函数
- 对象的初始化:
- 类的数据成员是不能在声明类时初始化的。
- 如果一个类中所有的成员都是
public
的,则可以在定义对象时对数据成员进行初始化。如果类中有private
或protected
的成员,就不能用这种方法初始化。 - 可以用成员函数来对对象中的数据成员赋初值。但是如果对一个类定义了多个对象,而且类中的数据成员比较多,那么,程序就显得非常臃肿烦琐。
- 对象的初始化:
!为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。
-
构造函数的作用:函数体内通过赋值语句对数据成员实现初始化。
- 不需要用户来调用它,而是在建立对象时自动执行。
- 构造函数的**名字必须与类名同名,**而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。
- **它不具有任何类型,不返回任何值。**构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。
- 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。
-
不带参数的构造函数:
类名();
在函数体中对数据成员赋初值。这种方式使该类的每一个对象都得到同一组初值。 -
带参数的构造函数:
类名(类型 1 形参1,类型2 形参2,…);
调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现不同的初始化。- 用户是不能调用构造函数的,因此无法采用常规的调用函数的方法给出实参。**实参是在定义对象时给出的。**定义对象的一般格式为
类名 对象名(实参1,实参2,…);
- 用户是不能调用构造函数的,因此无法采用常规的调用函数的方法给出实参。**实参是在定义对象时给出的。**定义对象的一般格式为
-
用参数初始化表对数据成员初始化:不在函数体内对数据成员初始化,而是在函数首部实现。
Box∷Box(int h,int w,int len):height(h),width(w),length(len){ }
只能用参数初始化列表初始化的情况!!!
常对象的数据成员是常数据成员,因此常对象的构造函数只能用参数初始化表对常数据成员进行初始化。
常数据成员:
const int hour;
//声明hour为常数据成员,不能采用在构造函数中对常数据成员赋初值的方法。
- 在类外定义构造函数,应写成:
Time∷Time(int h):hour(h){}
静态数据成员不可以用这种方式初始化
-
构造函数的重载
- 在一个类中可以定义多个构造函数,这些构造函数具有相同的名字,而参数的个数或参数的类型不相同
- 对于每一个对象来说,建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。
-
默认构造函数(default constructor):调用构造函数时不必给出实参的构造函数。一个类只能有一个默认构造函数。
-
使用默认参数的构造函数:构造函数中参数的值指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值。
-
说明
- 应该在声明构造函数时指定默认值,而不能只在定义构造函数时指定默认值。
- 形参名可以省略。
- 如果构造函数的全部参数都指定了默认值,则在定义对象时可以给一个或几个实参,也可以不给出实参。
- 在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数。
-
析构函数:(destructor)当对象的生命期结束时,会自动执行析构函数,释放资源,使这部分内存可以被程序分配给新对象使用。
-
析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。
- ①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象 释放前 自动执行析构函数。
- ②
static
局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main
函数结束或调用exit
函数结束程序时,才调用static
局部对象的析构函数。 - ③如果定义了一个全局对象,则在程序的流程离开其作用域时(如
main
函数结束或调用exit
函数) 时,调用该全局对象的析构函数。 - ④如果用
new
运算符动态地建立了一个对象,当用delete
运算符释放该对象时,先调用该对象的析构函数。
-
析构函数的作用
- 并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。
- 可以被用来执行“用户希望在最后一次使用对象之后所执行的任何操作”,例如输出有关的信息。
- 如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。
-
调用构造函数和析构函数的顺序
- 在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。但是凡事都有例外:
- (1) 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。
- (2) **如果定义的是局部自动对象(**例如在函数中定义对象),则在建立对象时调用其构造函数。**如果函数被多次调用,则在每次建立对象时都要调用构造函数。**在函数调用结束、对象释放时先调用析构函数。
- (3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
- 在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。但是凡事都有例外:
-
复制构造函数:见下面
-
转换构造函数:当一个构造函数只有一个参数,而且该参数又不是本类的const引用时
-
对象数组:数组中每个元素是对象
- 如果构造函数只有一个参数,在定义数组时可以直接在等号后面的花括号内提供实参。如
Student stud[3]={60,70,78};
- 如果构造函数有多个参数,则不能用在定义数组时直接提供所有实参的方法,很容易造成实参与形参的对应关系不清晰,出现歧义性。可以在花括号中分别写出构造函数并指定实参。
Student Stud[3]={Student(1002,19,76), Student(1000,20,86)};
- 如果构造函数只有一个参数,在定义数组时可以直接在等号后面的花括号内提供实参。如
-
对象指针
- 可以通过对象指针访问对象和对象的成员。
Time t1,*pt; pt=&t1; *pt pt所指向的对象,即t1。 (*pt).hour pt所指向的对象中的hour成员,即t1.hour pt->hour pt所指向的对象中的hour成员,即t1.hour
-
指向对象 数据成员 的指针:和定义指向普通变量的指针变量方法相同。
p1=&t1.hour; cout<<*p1<<endl;
注:hour
为公用的整型数据。 -
指向对象 成员函数 的指针:和定义指向普通函数的指针变量方法有所不同。
- 成员函数与普通函数有一个最根本的区别: 它是类中的一个成员。编译系统要求在上面的赋值语句中,指针变量的类型必须与赋值号右侧函数的类型相匹配,要求在以下3方面都要匹配(函数原型=返回值类型+函数名+参数表):
- ①函数参数的类型和参数个数;
- ②函数返回值的类型;
- ③所属的类。
- 定义指向公共成员函数的指针变量应该采用下面的形式:
void (Time∷*p2)( ); //定义p2为指向Time类中公用成员函数的指针变量。
定义指针变量一般形式为数据类型名 (类名∷*指针变量名)(参数表列);
- 把公用成员函数的入口地址赋给一个指向公用成员函数的指针变量:
p2=&Time∷get_time;
使指针变量指向一个公用成员函数的一般形式为:指针变量名=&类名∷成员函数名;
- 成员函数与普通函数有一个最根本的区别: 它是类中的一个成员。编译系统要求在上面的赋值语句中,指针变量的类型必须与赋值号右侧函数的类型相匹配,要求在以下3方面都要匹配(函数原型=返回值类型+函数名+参数表):
-
共用数据的保护:
private ,const
- 既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用
const
,即把有关的数据定义为常量。
- 既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用
-
常对象:其数据成员为常量,但成员函数不一定都是。
- 常对象必须要有初值,如
Time const t1(12,34,46); //t1是常对象
。这样,在所有的场合中,对象t1中的所有成员的值都不能被修改。 - 定义常对象的一般形式为:
类名 const 对象名[(实参表列)]; 或者 const 类名 对象名[(实参列表)];
- 如果一个对象被声明为常对象,则不能调用该对象的
非const
型的成员函数。为了防止这些函数会修改常对象中数据成员的值。 - 引用常对象中的数据成员很简单,只需将该成员函数声明为const即可。
- 常对象必须要有初值,如
-
常对象成员
-
常数据成员:
int const hour;
-
常成员函数:
void get_time( ) const;
/将函数声明为const,注意const的位置在函数名和括号之后。- 常成员函数可以访问常对象中的数据成员,但仍然不允许修改常对象中数据成员的值。
- 一定要修改常对象中的某个数据成员的值,对该数据成员声明为
mutable
,如mutable int count;
把count
声明为可变的数据成员,这样就可以用声明为const
的成员函数来修改它的值。 - 常成员函数不能调用另一个非const成员函数。
-
- const成员函数可以引用const数据成员,也可以引用非const的数据成员。
- const数据成员可以被const成员函数引用,也可以被非const的成员函数引用。
-
指向对象的常指针:将一个指针变量固定地与一个对象相联系
- 一般形式为:
类名 * const 指针变量名;
。这样指针值始终保持为其初值,不能改变。即只能指向一个对象,不能指向其他的对象,但是可以改变指向对象的值
- 一般形式为:
-
指向常变量的指针变量 :
const char *ptr;
- 注意
const
的位置在最左侧,它与类型名char
紧连,表示指针变量ptr
指向的char变量
是常变量,不能通过ptr
来改变其值的。 const 类型名 *指针变量名;
- 常变量只能用指向常变量的指针变量指向它。如
const i=2; const char *ptr; ptr = &2;
- 指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为
const
的变量。此时不能通过此指针变量改变该变量的值。 - 如果函数的形参是指向
非const
型变量的指针,实参只能用指向非const
变量的指针,而不能用指向const
变量的指针 - 如果函数的形参是指向
const
型变量的指针,在执行函数过程中显然不能改变指针变量所指向的变量的值,因此允许实参是指向const
变量的指针,或指向非const
变量的指针
- 注意
请记住这样一条规则: 当希望在调用函数时对象的值不被修改,就应当把形参定义为指向常对象的指针变量,同时用对象的地址作实参(对象可以是const
或非const
型)。如果要求该对象不仅在调用函数过程中不被改变,而且要求它在程序执行过程中都不改变,则应把它定义为const
型。
-
const型数据的小结
-
对象的动态建立和释放
new Box;
编译系统开辟了一段内存空间,并在此内存空间中存放一个Box类对象,同时调用该类的构造函数,以使该对象初始化(如果已对构造函数赋予此功能的话)。但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。这种对象称为无名对象,它确实是存在的,但它没有名字。- 用
new
运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。用户可以获得这个地址,并通过这个地址来访问这个对象。需要定义一个指向本类的对象的指针变量来存放该地址。如Box *pt; pt = new Box;
- 在执行
new
时,对新建立的对象进行初始化。如Box *pt=new Box(12,15,18);
- 调用对象既可以通过对象名,也可以通过指针。用
new
建立的动态对象一般是不用对象名的,是通过指针访问的,它主要应用于动态的数据结构,如链表。访问链表中的结点,并不需要通过对象名。 - 在不再需要使用由new建立的对象时,可以用
delete
运算符予以释放。如delete pt;//释放pt指向的内存空间
。在执行delete
运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。
-
对象的赋值:
- 赋值运算符
“=”
只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,这是通过对赋值运算符的重载实现的。实际这个过程是通过成员复制来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。 对象名1 = 对象名2;
注意对象名1和对象名2必须属于同一个类。- 对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。
- 类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。
- 赋值运算符
-
对象的复制: 用一个已有的对象快速地复制出多个完全相同的对象。是一个从无到有的过程。
- 需要用到多个完全相同的对象。
- 将对象在某一瞬时的状态保留下来
- 第一种一般形式为:
类名 对象2(对象1);
用对象1复制出对象2。Box box2(box1);
- 另一种一般形式为:
类名 对象名1 = 对象名2;
。用赋值号代替括号,如:Box box2=box1;
//用box1初始化box2
。可以在一个语句中进行多个对象的复制。如:Box box2=box1,box3=box2;
-
复制构造函数(copy constructor)
第一种赋值的形式与定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。在建立对象时调用一个特殊的构造函数,即复制构造函数。即这个函数的形式是这样的:
//The copy constructor definition.
Box∷Box(const Box& b)
{
height = b.height;
width = b.width;
length = b.length;
}
**复制构造函数也是构造函数,**但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。
第一种形式实际上也是建立对象的语句,建立一个新对象box2。由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参box1的地址传递给形参b(b是box1的引用),因此执行复制构造函数的函数体时,将box1对象中各数据成员的值赋给box2中各数据成员。
-
全局变量可以实现数据共享,但是在每一个函数中都可以改变全局变量的值,全局变量的安全性得不到保证。破坏了类的封装原则,不符合面向对象程序的要求。
-
静态成员:
- 类类型(上)中有讲过,不同的对象使用的是同一个函数代码段,但是每一个对象都分别有自己的数据成员,而有时人们希望**有某一个或几个数据成员为所有对象所共有。**这样可以实现数据共享。
-
静态数据成员:同类的多个对象之间实现数据共享,其值对所有对象都是一样的,改一次,则全改。
- 在内存中只占一份空间,在为对象所分配的空间中不包括静态数据成员所占的空间,不属于某一个对象。
- 每个对象都可以引用这个静态数据成员。
- 不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
- 初始化:只能在类体外进行。
int Box∷height=10;
数据类型 类名∷静态数据成员名=初值;
不必在初始化语句中加static
- 既可以通过对象名引用,也可以通过类名来引用。
Box∷height
。这说明静态数据成员并不是属于对象的,而是属于类的,但类的对象可以引用它。 - 公用的(
public
)静态数据成员,在类外可以直接引用。private
的则必须通过公用的成员函数引用。
-
静态数据成员和全局变量的区别?
- 静态数据成员的作用域只限于定义该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内)。
-
静态成员函数:不是为了对象之间的沟通,而是为了能处理静态数据成员
- 是类的一部分,而不是对象的一部分。
- 类外调用公用的静态成员函数,要用类名和域运算符
“∷”
。如Box∷volume( );
- 也允许通过对象名调用静态成员函数,如
a.volume( );
- 当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。因为无法知道应该去找哪个对象。
静态成员函数与非静态成员函数的根本区别是: 非静态成员函数有this
指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。
- 如果一定要引用本类的非静态成员,应该加对象名和成员运算符“.”。如
cout<<a.width<<endl;
//引用本类对象a中的非静态成员
在类外可以访问公用成员,只有本类中的函数可以访问本类的私有成员。现在,我们来补充介绍一个例外——友元(friend
)。
- 友元函数
如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend
对其进行声明,此函数就称为本类的友元函数。友元函数可以访问这个类中的私有成员。(我声明了你是我的好朋友,所以你可以访问我的私有数据,但是你不一定认为我是你的好朋友,我还是无法访问你的私有成员。即友元这个是单方面的)
- 将普通函数声明为友元函数
friend void display(Time &); //在Time类中声明display函数为Time类的友元函数 void display(Time& t) //这是友元函数,形参t是Time类对象的引用 {cout<<t.hour<<″:″<<t.minute<<″:″<<t.sec<<endl;} //hour,minute,sec是Time类的私有成员 引用这些私有数据成员时,必须加上对象名,因为display函数不是Time类的成员函数,不能默认引用Time类的数据成员,必须指定要访问的对象。
- 友元成员函数:
friend 函数
是另一个类中的成员函数。
- 对类的“提前引用”的声明(前向声明)
- 即在正式声明一个类(如:
class Date{};
)之前,先声明一个类名,表示此类将在稍后声明。只包含类名,不包括类体。 如class Date;
- 只有在正式声明一个类以后才能用它去定义类对象。只提前引用声明是不行的。
因为在定义对象时是要为这些对象分配存储空间的,在正式声明类之前,编译系统无法确定应为对象分配多大的空间。编译系统只有在“见到”类体后,才能确定应该为对象预留多大的空间。
- 在对一个类作了提前引用声明后,可以用该类的名字去定义指向该类型对象的指针变量或对象的引用变量
指针变量和引用变量本身的大小是固定的,与它所指向的类对象的大小无关。
- 作用:
- 不必在
include
头文件了,相对会省点编译时间。 - 方便的解决两种类类型互相使用的问题。针对接口编程常常会遇到这种互相使用类型的场景。
- 不必在
- 即在正式声明一个类(如:
class Date; //对Date类的提前引用声明
class Time{ void display(Date &); } ; //display是成员函数,形参是Date类对象的引用
class Date { friend void Time∷display(Date &); };//声明Time中的display函数为友元成员函数
void Time∷display(Date &d) //display的作用是输出年、月、日和时、分、秒
{
cout<<d.month<<″/″<<d.day<<″/″<<d.year<<endl; //引用Date类对象中的私有数据
cout<<hour<<″:″<<minute<<″:″<<sec<<endl; //引用本类对象中的私有数据
}
int main( )
{
Time t1(10,13,56); //定义Time类对象t1
Date d1(12,25,2004); //定义Date类对象d1
t1.display(d1); //调用t1中的display函数,实参是Date类对象d1
return 0;
}
-
注意:程序是在定义
Time∷display
函数之前正式声明Date
类的。 -
**本程序中调用友元函数访问有关类的私有数据方法: **
- 在函数名
display
的前面要加display
所在的对象名(t1
);(这就是一般的引用成员函数前加对象名) display
成员函数的实参是Date
类对象d
1,否则就不能访问对象d1
中的私有数据;- 在
Time∷display
函数中引用Date
类私有数据时必须加上对象名,如d.month
。
- 在函数名
-
一个函数(包括普通函数和成员函数)可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据
-
友元类
- 一个类(例如B类)声明为另一个类(例如A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
- 在A类的定义体中用以下语句声明B类为其友元类:
friend B;
- 说明:
- 友元的关系是单向的而不是双向的。
- 友元的关系不能传递。
-
友元优点:能有助于数据共享,能提高程序的效率
-
友元缺点:友元可以访问其他类中的私有成员,对封装原则的一个破坏。
-
类模板:功能是相同的,仅仅是数据类型不同
- 声明类模板:
template <class 虚拟类型参数名1,class 虚拟类型参数名2...>
,把原有类中具体的类型名都换成类型参数名。当有多个时,每个前面都要有class
- 类模板包含类型参数,因此又称为参数化的类。
- 如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。
- 利用类模板可以建立含各种数据类型的类。
- 用类模板定义对象:
类模板名<实际类型名[1,2,...]> 对象名[(实参表列)];
如`Compare cmp(4,7);- 即在类模板名之后在尖括号内指定实际的类型名,在进行编译时,编译系统就用
int
取代类模板中的类型参数类型参数名
,这样就把类模板实例化了。
- 即在类模板名之后在尖括号内指定实际的类型名,在进行编译时,编译系统就用
- 声明类模板:
-
注:如果类模板中的成员函数在类模板外定义,不能用一般定义类成员函数的形式:
numtype Compare∷max( ) {…}
//不能这样定义类模板中的成员函数。而应当写成类模板的形式:
template<class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>∷成员函数名(函数形参表列) {…}
如:
template<class numtype>
numtype Compare<numtype>∷max( ){...}
- 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
- 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。