目录
对象的动态建立和释放
前面我们知道了 C++ 语言中可以用 new 运算符动态地分配内存,用 delete 运算符释放这些内存空间。这两个运算符也同样适用于对象的动态建立和撤销。
如果已经定义了一个 Time 类 ,可以用下面的方法动态的建立一个对象:
new Time ;
在执行这个语句时,系统开辟了一段内存空间,并在此内存空间中存放一个 Time 类对象,同时调用该类的构造函数,以使该对象初始化,但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。这种对象称为无名对象,它确实是存在的,但它没有名字。
用 new 运算符动态地分配内存后,将返回一个指向新对象的指针,即所分配的内存空间的起始地址,用户可以获得这个地址,并通过这个地址来访问这个对象,这样就需要定义一个指向本类的对象的指针变来存放该地址,这样就可以通过指针去访问这个新建的对象。如:
Time *p ;
p = new Time ;
p->show();
另系统允许在执行 new 时,对新建的对象进行初始化。如:
Time *q = new Time ;
q->show();
Time *p_1 = new Time(1,1,1);
p_1->show();
Time *p =new Time(1,1,1);
p->show();
调用对象既可以通过对象名,也可以通过指针。用 new 建立的动态对象一般是不用对象名,而是通过指针访问,一般主要应用于动态的数据结构,如链表。在执行 new 运算符时,如果内存量不足,则无法开辟所需的内存空间,此时大多系统都使 new 返回一个 0 指针值(NULL)。只要检测返回值是否为 0 ,就可判断分配内存是否成功,这在数据结构方面会经常用到。而在不需要使用 new 建立的对象时,可以用 delete 运算符予以释放,如:
Time *p =new Time(1,1,1);
p->show();
delete p ;
//p->show();
这就撤销了 p 指向的对象,此后程序不能再使用该对象。如果用一个指针变量 p 先后指向不同的动态对象,应注意指针变量的当前指向,以免删除错了对象。
对象的赋值和复制
1、对象的赋值
同类的对象之间可以相互复=赋值,这里说的对象的值是指对象中所有的数据成员的值。对象之间的赋值也是通过赋值运算符 “=” 进行的,一般形式为:对象名1 = 对象名2 ; 注意需要是同一类中的对象。
Time t(1,1,1) , t1 ;
t1=t ;
t1.show();
对象的赋值只是对其中数据成员的赋值,而不对成员函数赋值。数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一对象的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段,不需要、也无法对它们赋值。
注意在对象的赋值过程中,类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果(在此不作详细分析,只需记住这一结论即可)
2、对象的复制
对象的复制机制:用一个已有的对象快速地复制出多个完全相同的对象。其一般形式是: 类名 对象2(对象1); 如:
Time t ;
Time t1(t) ;
其作用是用已有的对象 t 去克隆出一个新对象 t1 。这里和赋值不一样的是,在建立对象时调用了一个特殊的构造函数—复制构造函数,这个函数的形式如下:
Time :: Time(const Time &t){
hour=t.hour;
min=t.min;
sec=t.sec;
}
复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加 const 声明。使参数值不能改变,以免在调用此函数时因不慎而使实参对象被修改)。此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。
回顾复制对象的语句:Time t1(t);这实际上也是建立对象的语句,建立一个新对象 t1 ,由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参是对象),而不会去调用其他构造函数,实参 t 的地址传递给形参 t (t 就是实参 t 的引用),在执行复制构造函数的函数体时,将对象 t 中各数据成员的值赋给 t1 中各数据成员。
C++还提供另一种复制形式,用赋值号代替括号,其一般形式为: 类名 对象名1 = 对象名2 ;可以在一个语句中进行多个对象的复制,如:
Time t ;
Time t1=t , t2=t1 ;
t2.show();
对象的复制和赋值的区别:对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值;而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。
普通构造函数和复制构造函数的区别:
- 在形式上:
类名(形参表列); // 普通构造函数的声明,如 Time (int h, int m ,int s);
类名(类名 &对象名); //复制构造函数的声明,如 Time(Time &t);
- 在建立对象时,实参类型不同,系统会根据实参的类型决定调用普通构造函数或复制构造函数:
Time t (1,1,1); // 实参为整数,调用普通构造函数
Time t1(t); //实参为对象名,调用复制构造函数
- 在什么情况下被调用:
普通构造函数在程序中建立对象时被调用。
复制构造函数在用已有对象复制一个新对象时被调用,在以下三种情况下需要复制对象:
- 程序中需要建立一个对象,并用另一个同类的对象对它初始化,像前边的例子。
- 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传给形参,也就是需要建立一个实参的拷贝,按实参复制一个形参,系统通过调用复制构造函数来实现。
void set(Time t){
cout << "hour=" << t.hour << endl;
}
int main(){
Time t;
set(t);
}
- 函数的返回值是类的对象。
Time set(){ //函数的返回类型是 Time 类型
Time t_1(1,1,1);
return t_1; //返回值是 Time 类的一个对象
}
int main(){
Time t;
t=set(); //调用函数,返回Time的临时对象,并将它赋值给 t 。
t.show();
}
对于上边函数的解释: 由于 t_1 是在函数 set 中定义的,在调用 set 函数结束时, t_1 的生命周期就结束了,因并不是将 t_1 带回 main 函数,而是在函数 set 结束前执行 return 语句时,调用 Time 类中的复制构造函数,将 t_1 复制一个新的对象,然后将它赋值给 t 。
静态成员
前边了解到全局变量,它能够实现数据共享。如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数所共享。但是用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。因此在实际工作中很少会用到全局变量。
如果想在同类的对象之间实现数据共享,也不要用全局变量,可以用静态的数据成员。
1、静态数据成员
静态数据成员是一种特殊的数据成员,它以关键字 static 开头。为各对象所占有,而不只属于某个对象的成员,所有的对象都可以引用它,静态的数据成员在内存中只占一份空间(而不是每个对象都分别为它保留一份空间)。静态数据成员的值对所有的对象都是一样的,如果改变它的值,则在各对象中这个数据成员的值都同时改变了。
前边已提到。如果只声明了类而没有定义对象,则类的一般数据成员是不占用内存空间的,只有在定义对象才为对象的数据成员分配空间。但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员是在所有对象之外单独开辟空间,只要在类中指定了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放),静态数据成员是在程序编译时分配空间,到程序结束时才释放空间。
静态数据成员可以被初始化,但只能在类体外进行初始化,注意也不能用参数初始化表对其初始化,其一般形式为 数据类型 类名 :: 静态数据成员 = 初值 ; 在类体中声明静态数据成员时加 static ,不必在初始化语句中加 static 。 静态数据成员可以通过对象名引用,也能通过类名引用。下面举个例子:
class Time{
int hour;
int min;
public:
static int sec;
//Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){} // 初始化表不能对静态数据成员初始化。
Time(int h=0,int m=0):hour(h),min(m){}
void show(){
cout<<"时间是: "<<hour<<":"<<min<<":"<<sec<<endl;
}
};
int Time::sec = 1 ;
int main(){
Time t , t1 ;
t.show();
cout<<t.sec<<endl;
t1.sec=2;
cout<<Time::sec<<endl;
}
需要注意的是,静态数据成员的访问属性,根据自己后续程序中是如何引用来设置他的访问属性。
有了静态数据成员后,各对象之间的数据有了沟通的渠道,实现数据共享,因此就不需要再用全局变量,全局变量破坏了封装的原则,不符合面向对象程序的要求。
注意全局静态数据成员和全局变量的不同,静态数据成员的作用域只限于定义的该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内),在此作用域内,可以通过类名和域运算符 “::” 引用静态数据成员,不论对象是否存在。
2、静态成员函数
在类中声明函数的前面加上 static 就成了静态成员函数,和静态数据成员一样,静态成员函数是类的一部分而不是对象的一部分,同样可以通过对象名和类名去调用静态成员函数。但与静态数据成员不同的是,静态成员函数的作用不是为了对象之间的沟通,而是为了去处理静态数据成员(当然一般的成员函数也可引用和修改静态数据成员)。静态成员函数可以直接引用本类中的静态成员,因为静态成员同样是属于类的,故能直接饮用,但静态成员函数不能访问非静态成员。(此处说的不能访问是指不能默认访问,因为无法知道去找哪个对象,如果一定要引用本类中的非静态成员,应该加对象名和成员运算符 “. ” )。在前边我们已提出,当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的 this 指针 ,而静态成员函数不属于某个对象,它与任何对象都无关,因此静态成员函数没有 this 指针(这也是静态成员函数和普通函数的区别),既然它不能指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。
class Time{
int hour;
int min;
public:
static int sec;
//Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){} // 初始化表不能对静态数据成员初始化。
Time(int h=0,int m=0):hour(h),min(m){}
void show(){
cout<<"时间是: "<<hour<<":"<<min<<":"<<sec<<endl;
}
static void set();
static void set_1(Time t){
cout<<t.hour<<endl; //访问非 static 数据成员
}
};
int Time::sec = 1 ;
void Time::set(){
sec++ ;
// cout<<hour<<endl; 找不到
// cout<<Time::hour<<endl;
cout<<sec<<endl;
}
int main(){
Time t ;
t.show();
t.set();
t.set_1(t);
}
下面是一个使用静态成员函数统计学生的平均成绩的例子,挺有代表性:
#include <iostream>
using namespace std;
class Student{
int num ;
int age ;
float score ;
static int count ;
static float sum ;
public:
Student(int n,int a,float s):num(n),age(a),score(s){}
void total(){
sum += score ;
count++;
}
static float average();
};
int Student::count=0; //静态数据成员的初始化
float Student::sum=0;
float Student::average(){
return(sum/count);
}
int main(){
Student std[3]={ //对象数组
Student(1,18,90), //注意对象数组元素是如何初始化的
Student(2,18,80),
Student(3,19,70)
}; //分号别忘了!!!
for(int i=0;i<3;i++) //注意数组是从0开始的
std[i].total();
cout<<"他们的平均分是: "<< Student::average()<<endl; // 其实也不用非要静态成员函数,用对象也可直接引用,只是看着不舒服。
}
用静态成员函数也只是比较方便一点,读起来也比较容易理解,但并不是要非用不可。
友元
前边在介绍类的访问权限时提到过友元(friend),friend 的意思就是朋友,朋友显然要比一般人关系要亲密一些。友元可以访问与其有好友关系的类中的私有成员,这种关系以关键字 friend 声明。友元包括友元函数和友元类。
1、友元函数
友元函数又分为两类:友元普通函数和友元成员函数。一个函数(包括普通函数和成员函数)可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据。
- 友元普通函数就是将普通函数声明为友元函数,进而可以让该函数去访问类中的私有成员。
class Time{
int hour;
int min;
int sec;
public:
Time(int h=0,int m=0,int s=0):hour(h),min(m),sec(s){}
void show(){
cout<<"时间是: "<<hour<<":"<<min<<":"<<sec<<endl;
}
friend void display(Time ) ;
};
void display(Time t){
cout<<t.hour<<":"<<t.min<<":"<<t.sec<<endl;
}
int main(){
Time t ;
display(t);
}
需要注意的是,在用友元函数引用该类的私有数据成员时,要加上对象名(即要使对象作为形参),因为普通函数并不是该类的成员函数,没有 this 指针,不能默认引用 类的数据成员,必须指定要访问的对象,毕竟友元函数也只是能够访问私有成员。就比如有一个人是两家人的邻居,被两家人都确认为好友,可以访问两家的各房间,但他在访问时理所当然要指出他要访问的是哪家。
- 友元成员函数是将另一个类中的成员函数声明为友元函数。
成员函数和普通函数声明为友元函数时略微有所不同,成员函数需要指出它是属于哪个类中的成员函数。
此处涉及到类的提前引用声明,在一般情况下,对象必须先声明然后才能使用它,但是在特殊情况下(正式声明类之前)需要使用类名,此时就该作提前引用声明。如下:
#include <iostream>
using namespace std;
class Time;
class Date{
int year ;
int month ;
int day ;
public:
Date(int y,int m,int d):year(y),month(m),day(d){}
void display(Time );
};
class Time{
int hour;
int min ;
int sec;
public:
Time(int h, int m,int s):hour(h),min(m),sec(s){}
void show(){
cout<<"时间: "<< hour<<":" <<min <<":" <<sec<<endl;
}
friend void Date::display(Time );
//此处定义这个函数,编译是出错的,想一下为什么?
};
void Date::display(Time t){
cout<<"日期: "<< year <<"/"<<month <<"/"<<day<<endl;
t.show();
}
int main(){
Time t(10,47,20);
Date d(2018,12,31);
d.display(t);
}
类的提前声明的使用范围是有限的,只有在正式声明一个类后才能用它去定义类对象(比如上方如果在第三行后边加上 Time t 是出错的),因为在定义对象时是需要为这些对象分配存储空间的,在正式声明类之前编译系统不知道应为该对象分配多大的空间,只有 “见到” 类体后,才能确定具体的分配大小。仍用上边那个例子再解释一下:
class Date;
class Time{
int hour;
int min ;
int sec;
public:
Time(int h, int m,int s):hour(h),min(m),sec(s){}
void show(){
cout<<"时间: "<< hour<<":" <<min <<":" <<sec<<endl;
}
friend void Date::display(Time );
};
class Date{
int year ;
int month ;
int day ;
public:
Date(int y,int m,int d):year(y),month(m),day(d){}
void display(Time );
};
这段代码仅仅将两个类的定义换了一下位置,然后会发现编译是出错的,出错的位置就是Time类中友元函数的位置,我们也已经在Time 类上边对 Date 作了提前引用声明了,那为什么还是会出错呢?这是因为在定义时,系统并不知道 Date 类中有什么成员,我们想让Date类中的成员函数指定为Time 类的友元函数,语法上是没错的,但在前边仅仅是做了提前引用声明,我们通过提前引用声明也只能使用其类名。
在对一个类做提前引用声明后,可以用该类的名字去定义一个指向该类型对象的指针变量或者对象的引用,这是因为指针变量(四个字节大小)和引用(不需要分配空间)与它所指向的类对象大小无关。另外需要注意:友元成员函数必须在类内部声明,在类外部定义。因为在内部定义成员函数,要用到其对象,此刻必须定义完整的类,但是类完整的定义在右 中括号出现后才是,故此刻会编译出错(这就是第一段代码注释中的那个问题)。这是我所知道的知识,但至于正确不正确我没有去考证。
2、友元类
如果一个类 B 声明为 类 A 的友元类,那么 B 类中的所有成员函数都是 A 类的友元函数,即可以访问 A 类的所有成员。其声明一般形式为:friend 类名 ; 需要注意的是:友元的关系是单向的而不是双向的(B 是 A 的友元类 其内的成员函数可以访问 A 中所有的成员,但不意味着 A 也是 B 的友元 , A 中的成员函数无权访问 B 中的私有成员)。友元的关系也不能传递(如果 B 是 A 的友元类 , C 是 B 的友元类,不等于 C 是 A 的友元类)。
class Time;
class Date{
int year ;
int month ;
int day ;
public:
Date(int y,int m,int d):year(y),month(m),day(d){}
void display(Time t);
void show_hour(Time);
};
class Time{
int hour;
int min ;
int sec;
public:
Time(int h, int m,int s):hour(h),min(m),sec(s){}
void show(){
cout<<"时间: "<< hour<<":" <<min <<":" <<sec<<endl;
}
friend Date ;
};
void Date::display(Time t){
cout<<"日期: "<<year<<"/"<<month<<"/"<<day<<endl;
t.show();
}
void Date::show_hour(Time t){
cout<<"hour="<<t.hour<<endl;
}
类模板
前边提到过函数模板,是对于功能一样而仅数据类型不一样的一些函数,可以定义一个任何类型变量都能进行操作的函数模板,在调用时系统根据实参的类型取代函数模板中的类型参数,得到具体的函数。同样也有相同功能的类模板。声明形式也是需要加一个关键字 template 表示模板的含义。
#include <iostream>
using namespace std;
template <class numtype> //注意无分号,和函数模板的声明一样。
//声明模板,template后面的尖括号内的内容为模板的参数表,关键字calss表示后面的是虚拟类型参数名numtype
class Compare{ //类模板名为 Compare
numtype a;
numtype b;
public:
Compare(numtype a1,numtype b1){ //构造函数
a=a1;
b=b1;
}
numtype max(){
return (a>b)?a:b ;
}
numtype min();
};
//注意这是在类外定义模板时的正确写法,不能用一般定义类成员函数的形式。
template <class numtype> //必须有
numtype Compare <numtype> :: min(){ // Compare <numtype> 是一个整体,是带参的类
if(a<b)
return a;
else
return b;
}
int main(){
Compare <int> c(1,3); //和普通定义对象不一样,这是正确表示
cout<<"c_max="<<c.max()<<endl;
Compare <double> c1(1.67,3.89);
cout<<"c1_min="<<c1.min()<<endl;
}
在上边代码中,Compare是类模板名,而不是一个具体的类,Conpare <numtype> 是一个整体 ,numtype 也并不是一个实际的类型,只是虚拟的,无法用它去定义对象,必须用实际类型名去取代虚拟的类型,即在类模板名后在尖括号中指定实际的类型名,这样系统在编译时就会用实际类型名取代类模板中的类型参数numtype ,也就是实例化。另外类模板和函数模板一样,它的类型参数同样可以有一个或多个,每个类型参数前面都必须加上 class ,定义对象时也要分别代入实际的类型名。
template <class numtype_1,class numtype_2>
class Compare{
};
int main(){
Compare <int,double> c1(1,1.69) ;
}
最后多说一点,使用类模板时也要注意其作用域,只能在其有效作用域内用它定义对象。另外模板可以有层次,一个类模板可以作为基类,派生出派生模板类。这方面查阅相关资料可以了解到,我具体也不是很了解,很少会用到。