C++面向对象程序设计
参考书目:《C++面向对象程序设计》—— 谭浩强 《C++程序设计:思想与方法》—— 翁惠玉
第三章:怎样使用类和对象
一、构造函数
1.对象的初始化
在定义一个类时,不能对其数据成员赋初值,因为类是一种类型,系统不会为它分配内存空间。在建立一个对象时,需要对其数据成员赋初值。如果一个数据成员未被赋初值,则它的值是不确定的。因为系统为对象分配内存时,保持了内存单元的原状,它就成为数据成员的初值,这个值是随机的。C++ 提供了构造函数机制,用来为对象的数据成员进行初始化。如果你未设计构造函数,系统在创建对象时,会自动提供一个默认的构造函数,而它只为对象分配内存空间其他什么也不做。
如果类中的所有数据成员是公有的,可以在定义对象时对其数据成员初始化。
class Time
{
public:
int hour;
int minute;
int sec;
};
Time t1={15,36,26}; //列出各个公有数据成员的值,在两个值之间用逗号分隔。
2.构造函数的作用
构造函数用于为对象分配空间和进行初始化,它属于某一个类,可以由系统自动生成,也可以由程序员编写。程序员根据初始化的要求设计构造函数及函数参数。构造函数是一种特殊的成员函数,在程序中不需要写调用语句,在系统建立对象时由系统自觉调用执行。
构造函数的特点:
- 构造函数的名字与它的类名必须相同。
- 它没有类型,也不返回值。
- 它可以带参数,也可以不带参数。
- 一般被定义为公有成员。
例:定义构造成员函数
class Time
{
public:
Time(){ hour=0;minute=0;sec=0;}
void set_time();
void show_time();
private:
int hour;
int minute;
int sec;
};
void Time::set_time()
{
cin>>hour;
cin>>minute;
cin>>sec;
}
void Time::show_time()
{
cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
int main()
{
Time t1;
t1.set_time(); t1.show_time();
Time t2;
t2.show_time();
return 0;
}
程序运行时首先建立对象t1,并对t1中的数据成员赋初值0,然后执行t1.set_time函数,从键盘输入新值给对象t1的数据成员,再输出t1的数据成员的值。接着建立对象t2,同时对t2中的数据成员赋初值0,最后输出t2的数据成员的初值。
注:在类Time中定义了构造函数Time,它与所在的类同名。在建立对象时自动执行构造函数,该函数的作用是为对象中的各个数据成员赋初值0。只有执行构造函数时才为数据成员赋初值。
构造函数的使用说明:
- 当函数执行到对象定义语句时建立对象,此时就要调用构造函数,对象就有了自己的作用域,对象的生命周期开始了。
- 构造函数没有返回值,因此不需要在定义中声明类型。
- 构造函数不需要显式地调用,构造函数是在建立对象时由系统自动执行的,且只执行一次。构造函数一般定义为public。
- 在构造函数中除了可以对数据成员赋初值,还可以使用其他语句。
- 每个类都必须至少有一个构造函数。如果用户没有定义构造函数,C++系统会自动生成一个构造函数,而这个函数体是空的,不执行初始化操作。
- 若构造函数没有参数,则称为无参构造函数;带参数的构造函数称为有参构造函数。
- 构造函数可以重载。
3.带参数的构造函数
可以采用带参数的构造函数,在调用不同对象的构造函数时,从外边将不同的数据传递给构造函数,实现不同对象的初始化。
构造函数的首部一般格式:构造函数名(类型 形参1,类型 形参2,…)
定义对象格式:类名 对象名(实参1,实参2,…);
例:有两个长方柱,其长、宽、高分别为:(1)12,25,30(2)15,30,21 编写程序,在类中用带参数的构造函数,计算它们的体积。
//分析:可以在类中定义一个计算长方体体积的成员函数计算对象的体积。
#include <iostream>
using namespace std;
class Box
{
public:
Box(int,int,int);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len) // 长方体构造函数
{
height=h;
width=w;
length=len;
}
int Box::volume() // 计算长方体的体积
{
return(height*width*length);
}
int main()
{
Box box1(12,25,30); // 定义对象box1
cout<< " box1体积=" << box1.volume() <<endl;
Box box2(15,30,21); // 定义对象box2
cout<< " box2体积= " << box2.volume()<<endl;
return 0;
}
注:(1)带形参的构造函数在定义对象时必须指定实参。(2)用这种方法可以实现不同对象的初始化。
4.参数初始化表的使用
C++提供了参数初始化表的方法对数据成员初始化。这种方法不必在构造函数内对数据成员初始化,在函数的首部就能实现数据成员初始化。
构造函数的首部格式:类名::构造函数名(类型1 形参1,类型2 形参2):成员名1(形参1),成员名2(形参2) { }
定义对象格式:类名 对象名( 实参1,实参2);
例:定义带形参初始化表的构造函数
Box::Box( int h, int w, int len): height (h),width(w), length(len){ }
定义对象:
Box box1(12,25,30);
…
Box box2(15,30,21);
5.构造函数的重载
构造函数也可以重载。一个类可以有多个同名构造函数,函数参数的个数、参数的类型各不相同。
例:定义两个构造函数,其中一个无参数,另一个有参数。
#include <iostream>
using namespace std;
class Box
{
public:
Box();
Box(int h,int w ,int len): height(h), width(w), length(len) { }
int volume();
private:
int height;
int width;
int length;
};
Box::Box()
{
height=10;
width=10;
length=10;
}
int Box::volume()
{
return(height*width*length);
}
int main()
{
Box box1;
cout<<"box1 体积= "<<box1.volume()<<endl;
Box box2(15,30,25);
cout<<"box2 体积= "<<box2.volume()<<endl;
return 0;
}
//系统根据定义对象的格式决定调用哪个构造函数。
注:(1)不带形参的构造函数为默认构造函数。每个类只有一个默认构造函数,如果是系统自动给的默认构造函数,其函数体是空的。(2)虽然每个类可以包含多个构造函数,但是创建对象时,系统仅执行其中一个。
6.默认参数的构造函数
C++允许在构造函数里为形参指定默认值,如果创建对象时,未给出相应的实参时,系统将用形参的默认值为形参赋值。
格式:函数名(类型 形参1=常数,类型 形参2=常数,…);
例:将构造函数改用带默认值的参数,长、宽、高的默认值都是10。
#include <iostream>
using namespace std;
class Box
{
public:
Box(int w=10,int h=10,int len=10);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int w,int h,int len)
{
height=h;
width=w;
length=len;
}
int Box::volume()
{
return(height*width*length);
}
int main()
{
Box box1;
cout<<"box1 体积= "<<box1.volume()<<endl;
Box box2(15);
cout<<"box2 体积 "<<box2.volume()<<endl;
Box box3(15,30);
cout<<"box3 体积 "<<box3.volume()<<endl;
Box box4(15,30,20);
cout<<"box4 体积"<<box4.volume()<<endl;
return 0;
}
//构造函数也可以改写成带参数初始化表的形式:
Box::Box( int h, int w, int len): height ( h),
width(w), length(len){ }
注:(1)如果在类外定义构造函数,应该在声明构造函数时指定默认参数值,在定义函数时可以不再指定默认参数值。(2)在声明构造函数时,形参名可以省略。例如:Box(int =10,int =10,int =10); (3)如果构造函数的所有形参都指定了默认值,在定义对象时,可以指定实参也可不指定实参。由于不指定实参也可以调用构造函数,因此全部形参都指定了默认值的构造函数也属于默认构造函数。为了避免歧义,不允许同时定义不带形参的构造函数和全部形参都指定默认值的构造函数。(4)同样为了避免歧义性,如定义了全部形参带默认值的构造函数后,不能再定义重载构造函数。反之亦然。
- 对局部对象,静态对象,全局对象的初始化对于局部对象,每次定义对象时,都要调用构造函数。
- 对于静态对象,是在首次定义对象时,调用构造函数的,且由于对象一直存在,只调用一次构造函数。
- 对于全局对象,是在main函数执行之前调用构造函数的。
class A
{
float x,y;
public:
A(float a, float b){x=a;y=b;cout<<"初始化自动局部对象\n";}
A(){ x=0; y=0; cout<<"初始化静态局部对象\n";}
A(float a){ x=a; y=0; cout<<"初始化全局对象\n"; }
void Print(void){ cout<<x<<'\t'<<y<<endl; }
};
A a0(100.0);//定义全局对象
void f(void)
{
cout<<"进入f()函数\n";A a2(1,2);
static A a3; //初始化局部静态对象
}
void main(void)
{
cout<<"进入main函数\n";
A a1(3.0, 7.0);//定义局部自动对象
f(); f();
}
二、析构函数
析构函数也是个特殊的成员函数,它的作用与构造函数相反,当对象的生命周期结束时,系统自动调用析构函数,收回对象占用的内存空间。析构函数的函数名与类名相同!
格式:类名::~类名(){ 函数语句}
执行析构函数的时机:
- 在一个函数内定义的对象当这个函数结束时,自动执行析构函数释放对象。
- static局部对象要到main函数结束或执行exit命令时才自动执行析构函数释放对象。
- 全局对象(在函数外定义的对象)当main函数结束或执行exit命令时自动执行析构函数释放对象。
- 如果用new建立动态对象,用delete时自动执行析构函数释放对象。
析构函数的特征:
- 析构函数名以~符号开始后跟类名
- 析构函数没有数据类型、返回值、形参。由于没有形参所以析构函数不能重载。一个类只有一个析构函数。
- 如果程序员没有定义析构函数,C++编译系统会自动生成一个析构函数。
析构函数除了释放对象(资源)外,还可以执行程序员在最后一次使用对象后希望执行的任何操作。
调用构造函数和析构函数的顺序
在一般情况下,调用析构函数的次序与调用构造函数的次序恰好相反:最先调用构造函数的对象,最后调用析构函数。而最后调用构造函数的对象,最先调用析构函数。可简记为:先构造的后析构,后构造的先析构,它相当一个栈,后进先出。
注:(1)在全局范围中定义的对象(在所有函数之外定义的对象),在文件中的所有函数(包括主函数)执行前调用构造函数。当主函数结束或执行 exit 函数时,调用析构函数。(2)如果定义局部自动对象(在函数内定义对象),在创建对象时调用构造函数。如多次调用对象所在的函数,则每次创建对象时都调用构造函数。在函数调用结束时调用析构函数。(3)如果在函数中定义静态局部对象,则在第一次调用该函数建立对象时调用构造函数,但要在主函数结束或调用 exit 函数时才调用析构函数。
例:调用构造函数和析构函数
#include<iostream>
using namespace std;
class Complex //复数类
{
public:
Complex(double r, double i); //构造函数声明
~Complex(); //析构函数声明
private:
double real;
double imag;
};
Complex::Complex(double r, double i) //构造函数实现
{
cout << "constructor..."<<endl;
real = r;
imag = i;
cout << "real=" << real << ",imag=" << imag << endl;
}
Complex::~Complex() //析构函数的实现
{
cout << "destructor..."<<endl;
cout << "real=" << real << ",imag=" << imag << endl;
}
int main()
{
Complex A(1.2, 1.8);
Complex B(2.2, 2.8);
return 0;
}
class A
{
float x,y;
public:
A(float a,float b) { x=a;y=b;cout<<"调用非缺省的构造函数\n";}
A() { x=0; y=0; cout<<"调用缺省的构造函数\n" ;}
~A() { cout<<"调用析构函数\n";}
void Print(void) { cout<<x<<'\t'<<y<<endl; }
};
void main(void)
{
A a1;
A a2(3.0,30.0);
cout<<"退出主函数\n";
}
不同存储类型的对象调用构造函数及析构函数:
- 对于全局定义的对象(在函数外定义的对象),在程序开始执行时,调用构造函数;到程序结束时,调用析构函数。
- 对于局部定义的对象(在函数内定义的对象),当程序执行到定义对象的地方时,调用构造函数;在退出对象的作用域时,调用析构函数。
- 用static定义的局部对象,在首次到达对象的定义时调用构造函数;到程序结束时,调用析构函数。
class A
{
float x, y;
public:
A(float a, float b) { x = a; y = b; cout << "初始化自动局部对象\n"; }
A() { x = 0; y = 0; cout << "初始化静态局部对象\n"; }
A(float a) { x = a; y = 0; cout << "初始化全局对象\n"; }
~A() { cout << "调用析构函数" << endl; }
};
A a0(100.0);//定义全局对象
void f(void)
{
cout << "进入f()函数\n";
A ab(10.0, 20.0);//定义局部自动对象
static A a3; //初始化局部静态对象
}
void main(void)
{
cout << "进入main函数\n";
f(); f();
}
对对象成员的构造函数的调用顺序取决于这些对象成员在类中说明的顺序,与它们在成员初始化列表中的顺序无关。
三、对象数组
类是一种特殊的数据类型,它是 C++的合法类型,也可以被定义为对象数组。在一个对象数组中各个元素都是同类对象。例如一个班级有50个学生,每个学生具有学号、年龄、成绩等属性,可以为这个班级建立一个对象数组。
格式:student st[n]={student ( 实参1,实参2,实参3 );…student ( 实参1,实参2,实参3 );};
例:长方体数组的使用
class Box
{
public:
Box( int h=10, int w=12, int len=15 ):height(h), width(w), length(len) { } // 带默认参数值和参数表
int volume();
private:
int height;
int width;
int length;
};
int Box::volume()
{ return(height*width*length); }
int main()
{
Box a[3]={ Box(10,12,15),
Box(15,18,20),
Box(16,20,26) };
cout<<"a[0]的体积是 "<<a[0] .volume()<<endl;
cout<<"a[1]的体积是 "<<a[1] .volume()<<endl;
cout<<"a[2]的体积是 "<<a[2] .volume()<<endl;
return 0;
} // 每个数组元素是一个对象
四、对象指针
指针的含义是内存单元的地址,可以指向一般的变量,也可以指向对象。
1.指向对象的指针
对象要占据一片连续的内存空间,CPU实际都是按地址访问内存,所以对象在内存的起始地址是CPU确定对象在内存中位置的依据,这个起始地址称为对象指针。C++的对象也可以参加取地址运算。
格式:&对象名
定义对象的指针变量与定义其他的指针变量格式:类名 * 变量名表
例:定义类的指针并用指针来引用对象
class A
{
float x,y;
public:
float Sum(void) { return x+y; }
void Set(float a,float b) { x=a;y=b;}
void Print(void) { cout<<"x="<<x<<'\t'<<"y="<<y<<endl; }
};
void main(void)
{
A a1,a2;
A *p; //定义类的指针
p=&a1; //给指针赋值
p->Set(2.0, 3.0); //通过指针引用对象的成员函数
p->Print();
cout<<p->Sum()<<endl;
a2.Set(10.0, 20.0); a2.Print();
}
2.指向对象成员的指针
对象由成员组成。对象占据的内存区是各个数据成员占据的内存区的总和。对象成员也有地址,即指针。这指针分指向数据成员的指针和指向成员函数的指针。
1)指向对象公有数据成员的指针
①定义数据成员的指针变量格式:数据类型 * 指针变量名
②计算公有数据成员的地址格式:&对象名.成员名
2)指向对象成员函数的指针
①定义指向成员函数的指针变量格式:数据类型 ( 类名::*变量名 )( 形参表 );
②取成员函数的地址格式:&类名::成员函数名
③给指针变量赋初值格式:指针变量名= & 类名::成员函数名 ;
④用指针变量调用成员函数格式: (对象名.*指针变量名)([实参表]);
例:对象指针的使用
class Time
{
public:
Time(int,int,int);
int hour;
int minute;
int sec;
void get_time();
};
Time::Time(int h,int m,int s)
{
hour = h;minute = m;sec = s;
}
void Time::get_time()
{ cout<<hour<<":"<<minute<<":"<<sec<<endl; }
int main()
{
Time t1(10,13,56);
int *p1=&t1.hour; // 定义指向成员的指针p1
cout<<*p1<<endl;
t1.get_time(); // 调用成员函数
Time *p2=&t1; // 定义指向对象t1的指针p2
p2->get_time(); // 用对象指针调用成员函数
void (Time::*p3)(); // 定义指向成员函数的指针
p3=&Time::get_time; // 给成员函数的指针赋值
(t1.*p3)(); // 用指向成员函数的指针调用成员函数
return 0;
}
程序采用了三种方法输出t1的hour,minute,sec的值。
说明:
(1)成员函数的起始地址的正确表示是:&类名::成员函数名。
不要写成: p3=&t1.get_time;
(2)主函数的第8和9行可以合并写成:
void (Time::*p3) = &Time::get_time;
注:对指向成员函数的指针变量的使用方法说明以下几点: (1)指向类中成员函数的指针变量不是类中的成员,这种指针变量应在类外定义。(2)不能将任一成员函数的地址赋给指向成员函数的指针变量,只有成员函数的参数个数、参数类型、参数的顺序和函数的类型均与这种指针变量相同时,才能将成员函数的指针赋给这种变量。(3)使用这种指针变量来调用成员函数时,必须指明调用那一个对象的成员函数,这种指针变量是不能单独使用的。用对象名引用。(4)由于这种指针变量不是类的成员,所以用它只能调用公有的成员函数。若要访问类中的私有成员函数,必须通过类中的其它的公有成员函数。(5)当一个成员函数的指针指向一个虚函数,且通过指向对象的基类指针或对象的引用来访问该成员函数指针时,同样地产生运行时的多态性。(6)当用这种指针指向静态的成员函数时,可直接使用类名而不要列举对象名。这是由静态成员函数的特性所确定的。
3.this指针
一个类的成员函数只有一个内存拷贝。类中不论哪个对象调用某个成员函数,调用的都是内存中同一个成员函数代码。每个成员函数中都包含一个特殊的指针,它的名字是 this 。它是指向本类对象的指针,当对象调用成员函数时,它的值就是该对象的起始地址。
调用成员函数的格式:对象名.成员函数名(实参表)
this指针是隐式使用的,在调用成员函数时C++把对象的地址作为实参传递给this指针。
int Box :: volume(){ return (height * width * length) ; }
C++编译成:
int Box :: volume(*this){ return (this->height*this->width * this->length); }
对于计算长方体体积的成员函数volume,当对象a 调用它时,就把对象 a地址给 this 指针,编译程序将a 的地址作为实参调用成员函数:a.volume( &a );
实际上函数是计算:
(this->height)*(this->width)*(this->length)
这时就等价计算(a.height)*(a.width)*(a.length)
例:使用this指针调用成员函数
class Test
{
public:
Test(){ b=c=0; }
Test(double x,double y){ b=x; c=y; }
void copy(Test &t);
void Display(){ cout<<"this:"<<this<<endl;cout<<"b="<<b<<",c="<<c<<endl; }
private:
double b,c;
};
void Test::copy(Test &t)
{
if(this==&t) return;
*this=t;
}
void main()
{
Test t1,t2(22,3);
t1.Display();
t1.copy(t2); //对象t1调用成员函数
t1.Display();
t2.Display();
}
C++ 通过编译程序,在对象调用成员函数时,把对象的地址赋予this 指针,用this 指针指向对象,实现了用同一个成员函数访问不同对象的数据成员。
五、共用数据的保护
如果即希望数据在一定范围内共享,又不愿它被随意修改,从技术上可以把数据指定为只读型的。C++提供const手段,将数据、对象、成员函数指定为常量,从而实现了只读要求,达到保护数据的目的。
1.常对象
格式:const 类名 对象名(实参表);
或:类名 const 对象名(实参表);
注:如果一个常对象的成员函数未被定义为常成员函数(除构造函数和析构函数外),则对象不能调用这样的成员函数。
把对象定义为常对象,对象中的数据成员就是常变量,在定义时必须带实参作为数据成员的初值,在程序中不允许修改常对象的数据成员值。如果在常对象中要修改某个数据成员,C++提供了指定可变的数据成员方法。
格式: mutable 类型 数据成员;
2.常对象成员
可以在声明普通对象时将数据成员或成员函数声明为常数据成员或常成员函数。
1)常数据成员
格式: const 类型 数据成员名
注:只能通过带参数初始表的构造函数对常数据成员进行初始化。
2)常成员函数
格式:类型 函数名 (形参表) const
常成员函数的使用:
- 如果类中有部分数据成员的值要求为只读,可以将它们声明为const,这样成员函数只能读这些数据成员的值,但不能修改它们的值。
- 如果所有数据成员的值为只读,可将对象声明为const,在类中必须声明const 成员函数,常对象只能通过常成员函数读数据成员。
- 常对象不能调用非const成员函数。
注:如果常对象的成员函数未加const,编译系统将其当作非const成员函数;常成员函数不能调用非const成员函数。
常成员函数使用注意:
- const是函数类型的一个组成部分,因此在函数的实现部分也要使用关键字const。
- 常成员函数不能修改对象的数据成员,也不能调用该类中没有由关键字const修饰的成员函数,从而保证了在常成员函数中不会修改数据成员的值。
- 如果一个对象被说明为常对象,则通过该对象只能调用它的常成员函数。
- 静态函数不能声明为常成员函数!
3.指向对象的常指针
如果在定义指向对象的指针时,使用了关键字 const , 它就是一个常指针,必须在定义时对其初始化。并且在程序运行中不能再修改指针的值。
格式:类名 * const 指针变量名 = 对象地址
注:指向对象的常指针,在程序运行中始终指的是同一个对象。即指针变量的值始终不变,但它所指对象的数据成员值可以修改。当需要将一个指针变量固定地与一个对象相联系时,就可将指针变量指定为const。往往用常指针作为函数的形参,目的是不允许在函数中修改指针变量的值,让它始终指向原来的对象。
const放在指针变量的类型之前,表示声明一个指向常量的指针。此时,在程序中不能通过指针来改变它所指向的数据值,但可以改变指针本身的值。
格式: const 类名 *指针名
注:(1)如果一个对象已被声明为常对象,只能用指向常对象的指针变量指向它, 而不能用一般的 ( 指向非 const 型对象的 ) 指针变量去指向它。 (2)如果定义了指向常对象的指针,并指向非const的对象,则指向的对象不能通过指针变量来修改。(3)指向常对象的指针最常用于函数的形参,目的是在保护形参指针所指向的对象,使它在函数执行过程中不被修改。 (4)如果定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量本身的值是可以改变的。 (5)const 类名 * const 指针名,表示声明一个指向常量的指针常量,指针本身的值不可改变,而且它所指向的数据的值也不能通过指针改变。
4.对象的常引用
引用是传递参数的有效办法,用引用形参时,形参变量与实参变量是同一个变量,在函数内修改引用形参也就是修改实参变量。如果用引用形参又不想让函数修改实参,可以使用常引用机制。
格式: const 类名 & 形参对象名
const 使用总结
六、对象的动态建立和释放
C++提供了new和delete运算符,实现动态分配、回收内存。它们也可以用来动态建立对象和释放对象。
动态建立对象格式:类名 * 指针变量名;
new 类名;
Box *pt;
pt = new Box;
当不再需要使用动态对象时,必须用delete运算符释放内存。
格式: delete 指针变量
可以使用new运算符来动态地建立对象。建立时要自动调用构造函数,以便完成初始化对象的数据成员。最后返回这个动态对象的起始地址。用new建立类的对象时,可以使用参数初始化动态空间。
class A
{
float x,y;
public:
A(float a=0, float b=0){x=a; y=b;cout<<"调用了构造函数\n";}
void Print(void){ cout<<x<<'\t'<<y<<endl; }
~A() { cout<<"调用了析构函数\n"; }
};
void main(void)
{
cout<<"进入main()函数\n";
A *pa1;
pa1=new A[3];//开辟数组空间
cout<<"\n完成开辟数组空间\n\n";
delete [ ]pa1; //必须用[]删除开辟的空间
cout<<"退出main()函数\n";
}
注:用new运算符为对象分配动态存储空间时,调用了构造函数,用delete删除这个空间时,调用了析构函数。当使用运算符delete删除一个由new动态产生的对象时,它首先调用该对象的析构函数,然后再释放这个对象占用的内存空间。
七、对象的赋值和复制
1.对象的赋值
如果一个类定义了两个或多个对象,则这些同类对象之间可以互相赋值。这里所指的对象的值含义是对象中所有数据成员的值。
格式:对象1 = 对象2;
class Box
{
public:
Box(int=10,int=10,int=10);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len)
{height=h;width=w;length=len;}
int Box::volume()
{return(height*width*length); }
int main()
{
Box box1(15,30,25),box2;
cout<<"box1 体积= "<<box1.volume()<<endl;
box2=box1;
cout<<"box2 体积= "<<box2.volume()<<endl;
return 0;
}
注:(1)对象的赋值只对数据成员操作。(2)数据成员中不能含有动态分配的数据成员。
2.对象的复制
对象赋值的前提是对象1和对象2是已经建立的对象。C++ 还可以按照一个对象克隆出另一个对象(从无到有)。这就是复制对象。创建对象必须调用构造函数,复制对象要调用复制构造函数。
格式:
class 类名
{
public:
类名(参数表); //构造函数
类名(const 类名 &对象名); //复制构造函数
┇
};
类名::类名(const 类名 &对象名)
{ 函数语句 }
注:(1)复制构造函数也是一种特殊的成员函数。(2)它的功能是用一个已知的对象初始化一个被创建同类新对象。(3)复制构造函数的参数是本类对象的引用。(4)C++为每一个类定义了一个默认的复制构造函数。(5)可以根据需要定义自己的复制构造函数,从而实现同类对象之间数据成员的值传递。
复制构造函数的特点:
- 复制构造函数名与类名相同,并且也没有返回值类型。
- 复制构造函数可写在类中,也可以写在类外。
- 复制构造函数有且仅有一个参数,即是同类对象的引用。
- 如果没有显式定义复制构造函数,系统自动生成一个默认形式的复制构造函数。
复制对象有两种格式:
- 类名 对象2(对象1);
- 类名 对象2=对象1,对象3=对象1,…;
class Box
{
public:
Box(int=10,int=10,int=10);
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len)
{ height=h;width=w;length=len;}
int Box::volume()
{ return(height*width*length); }
int main()
{
Box box1(15,30,25);
cout<<"box1的体积= "<<box1.volume()<<endl;
//Box box2=box1,box3=box2;
Box box2(box1),box3(box2);
cout<<"box2的体积= "<<box2.volume()<<endl;
cout<<"box3的体积= "<<box3.volume()<<endl;
return 0;
}
复制构造函数的自动调用:
- 声明语句中用类的一个已知对象初始化该类的另一个对象时。
- 当对象作为一个函数实参传递给函数的形参时,需要将实参对象去初始化形参对象时,需要调用复制构造函数。
- 当对象是函数的返回值时,由于需要生成一个临时对象作为函数返回结果,系统需要将临时对象的值初始化另一个对象,需要调用复制构造函数。
1)浅复制
- 在用一个对象初始化另一个对象时,只复制了数据成员,而没有复制资源,使两个对象同时指向了同一资源的复制方式称为浅复制。
- 默认复制构造函数所进行的是简单数据复制,即浅复制。
2)深复制
- 通过一个对象初始化另一个对象时,不仅复制了数据成员,也复制了资源的复制方式称为深复制。
- 自定义复制构造函数所进行的复制是深复制。
#include <iostream>
using namespace std;
class Ex
{
public:
Ex(const char*s) //构造函数
{len = strlen(s); p = new char[len + 1]; strcpy(p, s);}
Ex(){p = new char[8]; cout << "****" << endl;}
Ex(const Ex& st) //复制构造函数
{len = strlen(st.p); p = new char[len + 1]; strcpy(p, st.p);}
~Ex() { delete[] p; }
void outdata()
{cout << &len << ":" << len << endl << &p << ":" << p << endl;}
private:
int len;
char *p;
};
int main()
{
Ex x("first"), y=x, z; x.outdata(); y.outdata();
}
八、静态成员
通常,每当说明一个对象时,把该类中的有关成员数据拷贝到该对象中,即同一类的不同对象,其成员数据之间是互相独立的。当我们将类的某一个数据成员的存储类型指定为静态类型时,则由该类所产生的所有对象,其静态成员均共享一个存储空间,这个空间是在编译的时候分配的。换言之,在说明对象时,并不为静态类型的成员分配空间。在类定义中,用关键字static修饰的数据成员称为静态数据成员。
class A
{
int x,y; static int z;
public:
void Setxy(int a, int b){ x=a; y=b;}
};
A a1, a2;
1.静态数据成员
静态数据成员定义格式:static 类型 数据成员名
静态数据成员的说明:
- 由于一个类的所有对象共享静态数据成员,所以不能用构造函数为静态数据成员初始化,只能在类外专门对其初始化。
格式:数据类型 类名::静态数据成员名 = 初值;
如果程序未对静态数据成员赋初值,则编译系统自动用0 为它赋初值。必须在文件作用域中,对静态数据成员作一次且只能作一次定义性说明。 - 即可以用对象名引用静态成员,也可以用类名引用静态成员。
类名::公有静态数据成员
对象名.公有静态数据成员 - 静态数据成员在对象外单独开辟内存空间,只要在类中定义了静态成员,即使不定义对象,系统也为静态成员分配内存空间,可以被引用。
- 在程序开始时为静态成员分配内存空间,直到程序结束才释放内存空间。它们不随对象的建立而分配空间,也不随对象的撤销而释放。
- 静态数据成员作用域是它的类的作用域(如果在一个函数内定义类,它的静态数据成员作用域就是这个函数)在此范围内可以用“类名::静态成员名”的形式访问静态数据成员。
例:引用静态数据成员
class Box
{
public:
Box(int,int);
int volume();
static int height;
int width;
int length;
};
Box::Box(int w,int len)
{ width=w;length=len; }
int Box::volume()
{ return(height*width*length); }
int Box::height=10; //必须对静态成员作一次定义性说明
int main()
{
Box a(15,20),b(20,30);
cout<<a.height<<endl;
cout<<b.height<<endl;
cout<<Box::height<<endl;
cout<<a.volume()<<endl;
return 0;
}
注:(1)静态数据成员具有全局变量和局部变量的一些特性。静态数据成员与全局变量一样都是静态分配存储空间的,但全局变量在程序中的任何位置都可以访问它,而静态数据成员受到访问权限的约束。必须是public权限时,才可能在类外进行访问。(2)为了保持静态数据成员取值的一致性,通常在构造函数中不给静态数据成员置初值,而是在对静态数据成员的定义性说明时指定初值。
2.静态成员函数
C++ 提供静态成员函数,用它访问静态数据成员,静态成员函数不属于某个对象而属于类。类中的非静态成员函数可以访问类中所有数据成员;而静态成员函数可以直接访问类的静态成员,不能直接访问非静态成员。
静态成员函数定义格式:static 类型 成员函数( 形参表 ){…}
调用公有静态成员函数格式:类名::成员函数( 实参表 )
例:引用非静态成员和静态成员
class Student
{
private:
int num;
int age;
float score;
static float sum;
static int count;
public:
Student(int, int, int);
void total();
static float average();
};
Student::Student(int m, int a, int s)
{num = m;age = a;score = s;}
void Student::total()
{sum += score;count++;}
float Student::average()
{return(sum / count);}
float Student::sum = 0;
int Student::count = 0;
int main()
{
Student stud[3] = {
Student(1001,18,70),
Student(1002,19,79),
Student(1005,20,98) };
int n;
cout << "请输入学生的人数:";
cin >> n;
for (int i = 0; i < n; i++)
stud[i].total();
cout << n << "个学生的平均成绩是"<<Student::average() << endl;
return 0;
}
注:(1)静态成员函数的实现部分在类定义之外定义时,其前面不能加修饰词static。这是由于关键字static不是数据类型的组成部分,因此,在类外定义静态成员函数的实现部分时,不能使用这个关键字。(2)不能把静态成员函数定义为虚函数。静态成员函数也是在编译时分配存储空间,所以在程序的执行过程中不能提供多态性。(3)可将静态成员函数定义为内联的(inline),其定义方法与非静态成员函数完全相同。
例:静态成员函数、静态数组及其初始化
#include<iostream>
using namespace std;
#include<stdio.h>
class A
{
static int a[20];
int x;
public:
A(int xx=0){ x=xx; }
static void in();
static void out();
void show(){ cout<<"x="<<x<<endl; }
};
int A::a[20]={0,0};
void A::in()
{
cout<<"input a[20]:"<<endl;
for(int i=0;i<20;++i)
cin>>a[i];
}
void A::out()
{
for(int i=0;i<20;++i)
cout<<"a["<<i<<"]="<<a[i]<<endl;
}
void main()
{
A::in(); //调用静态成员函数in(),初始化静态数组a[20]
A::out(); //调用静态成员函数out(),显示静态数组a[20]
A a; //定义A类的对象a
a.out(); //与A::out()同
a.show(); //显示对象a的数据成员值
}
九、友元
1.友元
1)友元
除了在同类对象之间共享数据外,类和类之间也可以共享数据。类的私有成员只能被类的成员函数访问,但是有时需要在类的外部访问类的私有成员,C++ 通过友元的手段实现这一特殊要求。友元可以是不属于任何类的一般函数,也可以是另一个类的成员函数,还可以是整个的一个类(这个类中的所有成员函数都可以成为友元函数)。
注:(1)友元是C++提供的一种破坏数据封装和数据隐藏的机制。(2)为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。
2)友元函数
如果在 A 类外定义一个函数(它可以是另一个类的成员函数,也可以是一个普通函数),在A类中声明该函数是A的友元函数后,这个函数就能访问A类中的所有成员。友元函数需要在类体内进行说明,在前面加上关键字friend。
友元函数声明格式:friend 类型 函数y( 类2 &对象 );
友元函数内访问对象的格式:对象名. 成员名
因为友元不是成员函数,它不属于类,所以它访问对象时必须冠以对象名。定义友元函数时形参通常定义引用对象,这样在友元函数内就能访问实参对象了。
例:将普通函数声明为友元函数 。
class Time
{
public:
Time(int h,int m,int s){ hour = h;minute = m;sec = s; }
friend void display(Time &);
private:
int hour;
int minute;
int sec;
};
void display(Time &t)
{
cout<<t.hour<<":"<<
t.minute<<":"<<t.sec<<endl;
}
int main()
{
Time t1(10,13,56);
display(t1);
return 0;
}
友元函数的使用说明:
- 友元函数不是类的成员函数
- 友元函数近似于普通的函数,它不带有this指针,因此必须将对象名或对象的引用作为友元函数的参数,这样才能访问到对象的成员。
友元函数与一般函数的不同点在于:
- 友元函数必须在类的定义中说明,其函数体可在类内定义,也可在类外定义;
- 它可以访问该类中的所有成员(公有的、私有的和保护的),而一般函数只能访问类中的公有成员。
- 友元函数不受类中访问权限关键字的限制,可以把它放在类的私有部分,放在类的公有部分或放在类的保护部分,其作用都是一样的。换言之,在类中对友元函数指定访问权限是不起作用的。
谨慎使用友元函数!
3)友元成员函数
友元成员函数声明格式:friend 类型 类1::成员函数x(类2 &对象);
大多数情况是友元函数是某个类的成员函数,即A类中的某个成员函数是B类中的友元函数,这个成员函数可以直接访问B类中的私有数据。这就实现了类与类之间的沟通。
class A
{
...
void fun( B &);
};
class B
{
...
friend void fun( B &);
};
注:一个类的成员函数作为另一个类的友元函数时,应先定义友元函数所在的类。
例:将成员函数声明为友元函数
class Date;
class Time
{
private:
int hour;
int minute;
int sec;
public:
Time(int h,int m,int s){ hour=h;minute=m;sec=s; }
void display(const Date&);
};
class Date
{
private:
int month;
int day;
int year;
public:
Date(int m,int d,int y){ month=m;day=d;year=y;}
friend void Time::display(const Date &);
};
void Time::display(const Date &da)
{
cout<<da.month<<"/"<<da.day<<"/"<<da.year<<endl;
cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
int main()
{
Time t1(10,13,56);
Date d1(12,25,2004);
t1.display(d1);
return 0;
}
注:友元是单向的,此例中声明Time的成员函数display是Date类的友元,允许它访问Date类的所有成员。但不等于说Date类的成员函数也是Time类的友元。
2.友元类
C++允许将一个类声明为另一个类的友元。假定A类是B类的友元类,A类中所有的成员函数都是B类的友元函数。
在B类中声明A类为友元类的格式:friend A;
实际中一般并不把整个类声明友元类,而只是将确有需要的成员函数声明为友元函数。
//a.h
class A
{
public:
A(){ cout<<"Input x=";cin>>x;y=x*x; }
void display(){cout<<"x="<<x<<",y="<<y<<endl;}
friend class B;
private:
double x,y;
};
//b.h
#include “a.h”
class B
{
public:
void display1(){ obj1.display(); obj1.x=5.5;obj1.y=6.8;obj1.display();}
private:
A obj1;
};
#include “b.h”
void main()
{
B obj1; obj1.display1();
B obj2; obj2.display1();
}
注:(1)友元关系是单向的,不是双向的。若B是A的友元,若没有特别声明,则A不是B的友元。(2)友元关系不能传递。若B是A的友元,C是B的友元,若没有特别声明,则C不是A的友元。
十、类模板
对于功能相同而只是数据类型不同的函数,不必须定义出所有函数,我们定义一个可对任何类型变量操作的函数模板。对于功能相同的类而数据类型不同,不必定义出所有类,只要定义一个可对任何类进行操作的类模板。
1.函数模板
函数模板定义格式:
template <模板形参表>
返回值类型 函数名(参数表)
{
函数体
}
函数模板只是一种说明,并不是一个具体的函数,C++编译系统不会产生任何可执行代码当遇到具体的函数调用时,才根据调用处的具体参数类型,在参数实例化以后才生成相应的代码,此时的代码称为模板函数。
2.类模板
例如定义比较两个整数的类和比较两个浮点数的类,这两个类做的工作是相似的所以可以用类模板,减少工作量。
定义类模板的格式:
template < class 类型参数名>
class 类模板名
{ … … }
用类型参数作为数据类型,用类模板名作为类。
使用类模板时,定义对象格式:
类模板名 <实际类型名> 对象名;
类模板名 <实际类型名> 对象名(实参表);
例:实现两个整数、浮点数和字符的比较,求出大数和小数。
template<class numtype>
class Compare
{
private:
numtype x,y;
public:
Compare(numtype a,numtype b){x=a;y=b;}
numtype max(){ return (x>y)?x:y;}
numtype min(){ return (x<y)?x:y;}
};
int main()
{
Compare<int> cmp1(3,7);
cout<<cmp1.max()<<endl;
cout<<cmp1.min()<<endl;
Compare<float> cmp2(45.78,93.6);
cout<<cmp2.max()<<endl;
cout<<cmp2.min()<<endl;
Compare<char> cmp3('a','A');
cout<<cmp3.max()<<endl;
cout<<cmp3.min()<<endl;
return 0;
}
声明和使用类模板步骤:
- 先写出一个实际的类。
- 将此类中准备改变的类型名 ( 如 int 要改变为 float 或 char )改用一个自己指定的虚拟类型名 ( 如上例中的 numtype) 。
- 在类声明前面加入一行,格式为 template <class 虚拟类型参数 > ,如 template class Compare{…};
- 用类模板定义对象时用以下形式: 类模板名 < 实际类型名 > 对象名 ; 类模板名 < 实际类型名 > 对象名 ( 实参表列 ); 如Compare cmp; Compare cmp(3,7);
- 如果在类模板外定义成员函数,应写成函数模板形式:template <class 虚拟类型参数 > 函数类型 类模板名 < 虚拟类型参数 >:: 成员函数名 ( 函数形参表列 ) {…}
注:(1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加class。(2) 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用内用它定义对象。 (3) 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。
3.类模板的友元
类模板的友元和类的友元的特点基本相同,但也具有自身的特殊情况。
以类模板的友元函数为例,可以分为以下三种情况:
- 友元函数无模板参数。
- 友元函数含有类模板相同的模板参数。
- 友元函数含有与类模板不同的模板参数。
例:类模板的友元函数
template <class T>
class A
{
public:
A(T i=0){num=i;}
void add(T i) { num=num+i; }
friend void inttodouble(A<int>& n1, A<double>& n2);
friend void display(A<T> n);
friend void success();
private:
T num;
};
template <class T1,class T2>
void inttodouble(A<T1>& n1, A<T2>& n2)
{n2.num=double(n1.num);}
template <class T>
void display(A<T> n)
{cout<<n.num<<endl;}
void success()
{ cout<<"success"<<endl; }
int main()
{
A<int> a1; A<double> a2;
a1.add(3); a2.add(5.4);
display(a2);
inttodouble(a1,a2);
success(); display(a2);
return 0;
}