c++程序计划(第3版)系列教程
c++程序计划(第3版)系列笔记准备知识
在c当中,避免字符串被截断的输入为gets(S),但是由于c语言新标准的推行和部分删除,在利用gets(S)时只能通过宏界说#define gets(S) fgets(S,sizeof(S),stdin)处理之后利用。
在c++当中,面对难以处理的字符串,我们不必再利用字符串数组进行存储,可以利用string范例进行操纵,利用cin对于string进行输入时遇到空格会自动截断,可以利用getline(cin,字符串名);对于字符串进行含有空格的输入。
另外对于输入输出流来说,很显然c语言的格式化输入与格式化输出对于格式有着详细限定,这也有了scanf和printf在速率方面优于cin与cout,因为cin与cout在进行输入输出时自动对数据范例进行识别,所以速率较慢。c语言在输出方面利用printf对于格式有着非常省事的操纵方法,但是对于c++来说,若相对于输出的数据格式进行修饰,要调用库函数#include 。比如:
[*]设置列宽在所输出数据之前加上setw(列宽);
[*]保存小数位数在所输出数据之前添加setiosflags(ios::fixed)和setprecision(保存位数)
政府部变量和全局变量名称一样时,对于全局变量的操纵要在变量名之前添加::避免对局部变量的影响。
内联函数
顾名思义现实上就是在所界说的函数范例名之前添加inline,使程序在实行到该函数时不是转移至该函数的接口去调用该函数,而是将该函数于本该的调用位置就此睁开,利用额外的内存空间换取运行速率。当然,在函数体内有循环语句、分支语句、if嵌套语句之内不能界说内联函数,否则将会代码膨胀导致内存爆炸。
引用
即“起外号”“:给某一个变量起了另一个名字。例如int a;int &b=a;那么我们对b进行操纵时,就等价于对a进行操纵,在这里我们特别夸大,此处的a、b共用同一段存储单位。
缺省函数
缺省函数即为缺少部分函数参数的函数,在我们未给出相对应的参数时,参数的值即为界说函数时的初值。譬如:
void dispaly(int a=1;int b=2;int c=3){cout<<a<<' '<<b<<' '<<c;}
[*]当 display(666,66,6)输出为666 66 6
[*]当display(666,66)输出为666 66 3
[*]当display(666)输出为666 2 3
[*]当display()输出为1 2 3
注意,若存在以下界说:
void display(int a,int b,int c=520,int d=1314){cout<<a<<b<<c<<d;}
void display(int a,int b){cout<<a<<b;}
当调用display(1,2);时,系统将无法确认到底是第一个函数还是第二个函数,从而导致编译错误。
重载函数
重载函数即为具有相同的功能,相同名字的函数,但是范例不同。
int getmax(int a,int b,int c);
double getmax(double a,double b,double c);
long long getmax(long long a,long long b,long long c);
在我们设置一个变量去接收函数的值的时候,系统会自动根据我们所设置的担当变量的范例去匹配对应的函数返回范例:
int ans=getmax(int a,int b,int c);//ans为int型
double ans=getmax(double a,double b,double c);//ans为double型
long long ans=getmax(long long a,long long b,long long c);//ans为long long 型
第八章笔记
面向对象程序计划方法概述
凡是以类对象为基本构成单位的程序称为基于对象的程序,面对象对象的程序计划的特点:封装,抽象,继承还有多态性。那么我们来说说,什么是对象?对象有两个基本要素:一是静态特性称为属性,二是动态特性称为举动。任何一个对象都有这两个基本要素所组成,在c++中,对象是由数据和函数这两个部分组成的,数据体现了之条件到的静态特性属性,函数体现了之条件到的动态特性举动,在一个系统的多个对象之间通过肯定的渠道相互联系,要使某一个对象实现某一种举动,应向它传送相应的信息。
封装性
对一个对象进行封装处理,把它的一部分属性和功能对外界屏蔽,也就是说从外界是看不到的甚至是不可知的。好比c++当中的vetor整形数组,我们在对其进行输出,删除的时候只必要调用相关的函数即可,并不用去相识其是如何实现的,我们只负责运用,不负责架构。
封装性:一是将有关的数据和操纵代码封装在一个对象之中,形成一个基本单位,各个对象之间相互独立,互不干扰、二是将对象之中的某些部分对外隐蔽,即隐蔽其内部细节,只留下少量接口,以便与外界联系,接收外界的消息。这种方法被称为信息屏蔽,信息屏蔽有利于数据安全,防止无关的人相识和篡改数据。
抽象性
抽象的作用是表示同一类事物的本质。
类与结构体相似但不同于结构体,它可以大概对自己的私有成员的数据通过自身各种函数访问并修改,从而实现详细环境详细化操纵。举个例子,现在必要对学生的成绩系统进行计划,每个学生呢必要有其成绩,学号,指导老师,各课成绩等等,那么我们此时只必要构建一个类,并实现相应的功能就可以,而我们所计划的这个类,雷同于int和double等,可以直接命名变量,我们命名的变量可以通过调用自身函数实现其自己的各式操纵,我们界说的学生类就是对于学生这个整体的抽象,而我们界说的学生类的对象就是这个学生类的详细体现。即类是对象的抽象,对象是类的特例,也就是类的详细体现情势。
继承性
继承性体现在可以对于一个已经设置好的类进行额外的扩充与扩展,从而可以大概淘汰重新计划类的时间,以及实现更多的新功能。详细实现方式将在以后的学习当中提到。被继承的类称为基类或者称为父类,派生出来的类被称为派生类或者子类。
多态性
由于继承而产生的不同的派生类,其对象对同一消息会作不同的响应,多态性是面向对象程序计划的一个紧张特性,能增加程序的灵活性。
面向对象程序计划的特点
将数据和有关操纵封装成一个对象。程序计划者的任务包含两个方面:一是计划各种所必要的类和对象,决定把哪儿些数据和对象封装在一起;二是思量怎样向有关对象发送消息以完成所需的任务。对于大型的程序计划来说,面向对象的程序计划可以大概大大的低落工作的难度,低落错误风险。
类和对象的作用
类是所有面向对象语言的共同特性,所有面向对象的语言都提供了这种范例。基于对象就是基于类,与基于过程的程序不同,基于对象的程序是以类和对象为基础的,程序的操纵是围绕着对象进行的。在此基础上利用了继承机制和多态性,就是面向对象的程序计划。
一组数据是与一组操纵相对应的,因此人们把相关的数据和操纵放在一起,形成一个整体,与外界相对分隔。这就是面向对象的程序计划中的对象。
在基于过程的结构化的程序计划当中:程序=算法+数据结构,基于对象和面向对象程序计划当中:对象=算法+数据结构 程序=对象s+消息。这里的对象s表示多个对象,消息的作用就是对对象的控制。程序计划的关键是计划好每一个对象以及确定这些对象发出的下令,使各对象完成相应的操纵。
类和对象的关系
类是抽象的不占用内存,对象是详细的,占用内存空间,类的声明的详细格式:
#include <iostream>
using namespace std;
class name//类名
{
private:
//私有的数据和成员函数
protected:
//不可以被类外函数访问但可以被派生类的成员函数访问
public:
//公有的数据和成员函数
};
private意味私有的,public意味公有的,他们被称为成员访问限定符。在类的界说当中,假如没有对数据和成员函数进行限定,那么默认为private。在struct结构体当中,则会默认为public。类名+对象名即可实现对对象的界说:
name OJ;
类的成员函数
类的成员函数和之前我们c中界说的函数基本相同,它可以在private、public、public当中,不同点在于它是一个属于类的成员只能出现在类体当中,成员函数所依照的规则与数据一样。
一般来说,成员函数只是一个工具函数,我们把public中的函数定为类的对外接口。外界可以调用这些接口从而实现对类的详细操纵,而有的不在public中的函数是支持其他函数的。这种函数用于支持其他函数的操纵,被称为工具函数,类的成员函数十分紧张,假如一个类当中没有了成员函数,那么它将与结构体没有差异,它的界说也就体现不出在面向对象程序计划的作用。
在类外界说成员函数
条件是该成员函数已经在类之中进行声明,在类外可以通过函数返回值范例 类名::函数名()对其进行界说。"::"是作用域限定符或称作用域运算符,若在作用域限定符前没有类名,或在函数名之前既无类名又无作用域限定符,那么此函数为全局函数。
内置成员函数
内置函数这个概念在c++第一次课中已经讲到,就不再赘述,对于类来说,在其之中界说的函数默认为inline内置函数,而在类之外界说类的函数时,必要加inline才为内置函数,否则系统不认为其是内置函数。
成员函数的储存方式
同一类的不同对象中的数据成员的值一般是不同的,而不同对象的函数代码是相同的,不论调用哪儿一个对象的函数的代码,其实调用的都是同样内容的代码,在c++之中,**每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,而不包罗函数代码所占用的存储空间。**即一个对象所占的空间大小只取决于该对象中数据成员所占用的空间,与成员函数无关。
无论成员函数在类内界说还是在类外界说,成员函数的代码段的存储方式都是相同的,都不占对象的存储空间,不要将成员函数的这种存储方式和inline函数的概念混淆,**不论是否用inline声明,成员函数的代码段都不占用对象的存储空间,inline函数只影响程序的实行服从,而与成员函数是否占用对象的存储空间无关,**固然成员函数没有放在对象的存储空间当中,但从逻辑上的角度,成员函数是和数据一起封装在一个对象中的,只答应本对象中成员的函数访问同一对象中的私有数据。
对象成员的引用
对象名.成员函数 对象名.成员名 其中.为成员运算符。在一个类当中应当至少有一个公用函数,作为对外的接口,否则就无法对对象实行任何操纵。也可以利用对象的引用,利用引用访问对外接口从而访问对象。
类的封装性与信息屏蔽
共用成员函数可以说是用户利用类的公用接口,或者说是类的对外接口,仅仅调用类而无法相识其内核体现了接口与实现分离,通过成员函数对数据成员进行操纵称为类的功能的实现,类中被操纵的数据是私有的,类的功能的实现细节是对用户隐蔽的,这种实现方式称为私有实现,“类的公用接口与私有实现的分离”形成了信息屏蔽。
类声明和成员函数界说的分离
类声明头文件是用户利用类库的共用接口,包含成员函数界说的文件就是类的实现,类声明和函数界说是放在两个文件中的,一个完成的c++程序由三部分组成(建一个项目):
[*]类声明头文件 .h
[*]类实现文件 .cpp
[*]类的利用文件 .cpp(主文件)
对于类实现文件仅仅必要编译一次即可,在利用时与对应的程序目的文件相连接。在系统提供的头文件中只包罗对成员函数的声明,而不包罗成员函数的界说,只有把对成员函数的界说单独放在另一文件当中,单独编译,才能做到不重复编译。
类库包罗两个部分:
[*]包含类声明的头文件
[*]已经过编译的成员函数的界说,它是目的文件
面向对象程序计划当中的几个名词
类的成员函数在面向对象程序理论中被称为“方法”,很显然,只有被声明为公用的方法才能被外界所激活。外界是通过发“消息”来激活有关方法的。
补充笔记
[*]类中的成员不答应初始化,若初始化要利用构造函数,将在下一章讲到
[*]类中的成员函数默认为private类
[*]派生类的友元函数可以访问类private中的数据
[*]私有private对于变量和函数的限定要求是一样的
[*]可以在类的外部界说类的成员函数,在类中声明函数之后
[*]在类外利用函数返回值范例+类名+::+函数名(参数)即可完成对其界说
[*]类的存储空间仅为内容变量开辟
[*]假如界说了一个对象的引用,那么引用与原本对象共用一段存储单位
第九章笔记
对象的初始化
因在类中,类并不是一个详细的实体,而是一种抽象的范例,所以我们在界说一个类时,对于类中的变量赋初值黑白法的,因为类在界说之后并不占用存储空间,所以也无处容纳数据,若假如一个类当中的所有的成员都是公用的,那它与结构体也就基本等价,此时可以在界说一个对象时对于其中的数据初始化,例如:
#include <iostream>
using namespace std;
class student
{
public:
string num;
string name;
string subject;
};
int main()
{
student stu={"202110900000","朱奕锦","计算机科学与技术"};
cout<<stu.num<<' '<<stu.name<<' '<<stu.subject;
return 0;
}
对于学生类中的学号,名字,专业进行了初始化,即直接赋值,但仅仅对于class类中数据全为public时才可以。否则编译报错。那么对于对象的初始化,我们发现,当类中数据全为public时我们可以对类中数据赋初值,那么假如类中的数据不为public时呢?我们还是和之前那样为了对private或者protected中的数据赋值设置一个一个函数去得到输入值而且return赋予吗?对于数据较小时可以,但对于数据较大时,一个一个调用函数未免也太贫苦了。这时我们引入一个新的概念————构造函数。
构造函数实现数据成员的初始化
构造函数与我们界说的一般函数不同,它是一种特殊的函数,用于处理对象的初始化,它不必要用户来调用,而是在用户建立对象的时候自动实行。构造函数的要求:
[*]它必须与类同名,便于系统识别他为构造函数
[*]它不具有任何范例,不具有任何返回值
#include <iostream>
using namespace std;
class student
{
private:
string num;
string name;
string subject;
public:
student()
{
num="空";
name="空";
subject="空";
}
void setdata();
void display();
};
int main()
{
student stu;
stu.display();
stu.setdata();
stu.display();
return 0;
}
void student::setdata()
{
num="202110900000";
name="朱奕锦";
subject="计算机科学与技术";
}
void student::display()
{
cout<<num<<' '<<name<<' '<<subject<<endl;
}
我们在此学生类未输入任何数据时界说的构造函数对所有数据初始化为“空”,在数据输出后第二次输出对象中的数据发现数据进行了改变,这就是构造函数的作用。注意:
[*] 在建立类的对象的时刻会自动调用构造函数,对象建立几个,构造函数运行几次,构建一个学生库,即开辟一个类对象为数组,我们发现,在数组当中,为输入数据时,相应的学号姓名专业都为"空"。即拥有了初值。
[*] 构造函数的作用仅仅是对对象进行初始化,因此他并不必要声明范例
[*] 构造函数无需调用,也不可以大概被用户调用
[*] 可以用一个类对象初始化另一个类对象
[*] 在构造函数的函数体中不仅仅可以包罗对于数据成员赋初值,还可以包罗其他语句例如cout
[*] 假如用户在界说类的时候没有界说构造函数,那么c++系统将会自动生成一个空的构造函数,此构造函数不进行任何操纵
带有参数的构造函数
带有参数的构造函数声明时:
构造函数名(参数类型);
带有参数的构造函数界说时(在类外界说):
类名::构造函数名(参数类型 参数)
{
某一数据成员=参数;
}
带有参数的构造函数调用时:
类名 对象名(实参);
例如:
#include <iostream>
using namespace std;
class student
{
private:
string num;
string name;
string subject;
public:
student(string,string,string);
void display();
};
int main()
{
student stu1("202110900000","朱奕锦","计算机科学与技术");
stu1.display();
student stu2("202110900000","min","金融学");
stu2.display();
return 0;
}
void student::display()
{
cout<<num<<' '<<name<<' '<<subject<<endl;
}
student::student(string a,string b,string c)
{
num=a;
name=b;
subject=c;
}
在带有参数的构造函数声明是仅仅在括号内添加相应参数的范例即可,在类外界说时,直接对数据成员用形参进行赋值,在带有参数的构造函数调用时要注意,在构建对象时在后面直接加括号与相关的形参以及其范例输出实参并与之一一对应,即可实现带有参数的构造函数赋初值,带有参数的构造函数赋初值在现实应用上比无参的构造函数要好用且实用得多。这种初始化对象的方法利用起来很简便,很直观,可以从界说语句之中直接看到数据成员的初值。
用参数初始化表实现对数据成员的初始化
参数初始化表可以在类体内界说构造函数而不是在类体外,而且可以大概淘汰函数体的长度,使代码变得简短精炼。尤其当必要初始化的数据成员较多时更能凸显出其精良性,此方法被很多c++程序人员喜爱而且利用。
类名 (参数类型 参数):参数(数据成员){}
例如:
#include <iostream>
using namespace std;
class student
{
private:
string num;
string name;
string subject;
public:
student(string a,string b,string c):num(a),name(b),subject(c){}
void display();
};
int main()
{
student stu1("202110900000","朱奕锦","计算机科学与技术");
stu1.display();
student stu2("202110900000","min","金融学");
stu2.display();
return 0;
}
void student::display()
{
cout<<num<<' '<<name<<' '<<subject<<endl;
}
num(a)的意思是:将a的值赋予num,以此类推,在利用参数初始化表时在语句末端为大括号。
构造函数的重载
我们在c++的第一次讲堂上已经提到了重载的概念,很显然,构造函数可实现重载,例如:
#include <iostream>
using namespace std;
class student
{
private:
string num;
string name;
string subject;
public:
student();
student(string a,string b,string c):num(a),name(b),subject(c){}
void display();
};
int main()
{
student stu1;
stu1.display();
student stu2("202110900000","朱奕锦","计算机科学与技术");
stu2.display();
return 0;
}
void student::display()
{
cout<<num<<' '<<name<<' '<<subject<<endl;
}
student::student()
{
num="空";
name="空";
subject="空";
}
不指定实参时,则调用的不含参数的构造函数,指定了实参,调用的为参数初始化表对于数据成员赋值。参数不同,调用的构造函数不同,这就是构造函数的重载。系统可以根据参数个数来决定调用哪一个构造函数。必要注意的是:
[*] 在建立对象时不必给出实参的构造函数称为默认构造函数,在一个类当中有且只能有一个默认构造函数,若用户在界说类的时候没有界说默认构造函数,那么c++系统将会自己界说,空的默认构造函数不具有任何作用
[*] 在界说无参构造函数时肯定要注意其情势,且不能被用户主动调用
[*] 尽管在一个类当中可以界说多个构造函数,但对于每一个对象来说,在建立的过程中仅仅实行其中一个构造函数,并非每个构造函数都被实行
利用默认参数的构造函数
构造函数参数的值既可以通过实参传递,也可以指定为某些默认值,即假如用户不指定实参值,编译系统就是形参取默认值,例如:
#include <iostream>
using namespace std;
class student
{
private:
string num;
string name;
string subject;
public:
student(string a="202110900000",string b="朱奕锦",string c="计算机科学与技术"):num(a),name(b),subject(c){}
void display();
};
int main()
{
student stu0;
stu0.display();
student stu1={"202110916113"};
stu1.display();
student stu2={"202110916113","张毅"};
stu2.display();
student stu3={"202110916113","张毅","物联网工程"};
stu3.display();
return 0;
}
void student::display()
{
cout<<num<<' '<<name<<' '<<subject<<endl;
}
注意:
[*] 应在构造函数声明时指定其默认值,而不可以大概只在界说函数时指定默认值
[*] 在声明构造函数时,形参名可以省略
[*] 一个类只能有一个默认构造函数,即可以不用参数而调用的构造函数,一个类有且只能有一个
[*] 在一个类当中界说了全部是默认参数的构造函数之后,不可以大概再界说重载函数,否则系统将无法判定调用哪儿个函数,轻易产生歧义,因此,一般不应同时利用构造函数的重构和有默认参数的构造函数
析构函数
析构函数在界说的过程之中在函数名头前加上~,析构函数的函数名要和类名相同,析构函数的作用并不是删除一个对象。析构函数的真正作用是在撤销对象占用的内存之前完成一些清算工作,或者是在末了一次利用对象之后所实行一些操纵。析构函数不返回任何值,没有函数范例,也没有函数参数,由于没有函数参数,所以也不可以大概被重载。
在一个类当中可以有多个构造函数,但是有且只能有一个析构函数,与构造函数相同的一点是,若在界说类的过程中未界说析构函数,那么类就会在其自己自己生成一个析构函数,徒有名称与情势,现实什么都不做,当对象的生命周期竣事是,会实行析构函数,简朴来说,有以下四种环境:
[*] 假如在一个函数之中界说了一个对象,当这个函数被调用竣事时,对象应该释放,在对象释放前自动实行析构函数
[*] static静态局部对象在函数调用竣事时对象不释放,因此也不调用析构函数,只有在main函数或者调用exit函数竣事程序时,才调用static局部对象的析构函数
[*] 假如界说了一个全局的对象,则在程序的流程脱离其作用域时(如main函数竣事或调用exit函数)时,调用该全局的对象的析构函数
[*] 假如用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数
调用构造函数和析构函数的顺序
在一般环境下,调用构造函数的顺序恰恰与调用析构函数的顺序相反:先构造的后析构,后构造的先析构,相当于一个栈,先进后出。但是并非所有环境下都按照这一原则来处理的:
[*] 假如在全局范围界说对象,那么它的构造函数在本文件模块中的所有函数(包罗main函数)实行前调用,但假如一个程序包含多个文件,而在不同的文件之中都界说了全局对象,则这些对象的构造函数的实行顺序是不确定的。当main函数实行完毕或调用exit函数时了,调用析构函数
[*] 假如界说的是局部自动对象,则在建立对象时调用其构造函数,假如对象所在的函数被多次调用,则在每次建立对象时都要调用构造函数,在函数调用竣事,对象释放时先调用析构函数
[*] 假如在函数中界说静态局部对象,则只在程序第一次调用该函数界说对象是调用构造函数一次,在调用函数竣事时对象并不释放,因此也不调用析构函数,只在main函数竣事或者调用exit函数竣事程序时才调用析构函数
[*] 利用new开辟的对象在开辟时自动调用其析构函数,只有在其利用delete删除之后才利用析构函数,否则即使程序竣事,所开辟的对象仍旧存在,而且占用肯定的存储空间(内存泄漏)
对象数组
数组不仅可以由简朴的变量组成,也可以由类对象组成。在建立数组时,建立多少个数组对象元素,则必要调用多少次构造函数。假如构造函数仅有一个参数,在界说数组时可以直接在等号后面的花括号内提供实参,若构造函数有多个参数时,每个元素要对应多个实参,假如再思量到构造函数有默认参数的环境,很轻易造成实参与形参的对应关系不清楚,出现歧义性。例如当界说student stu={10,8,3};这三个实参的值并不是第一个对象的三个实参的值,而是前三个对象的首个实参的值,因此一般不如许来写程序。
编译系统只为每个对象元素的构造函数传递一个实参,所以在界说数组的时候提供的实参的个数不可以大概超过数组元素的个数。当然,在花括号中分别写出构造函数名并在括号内指定实参,可以对具有多个参数的构造函数的对象数组实现初始化。在建立对象数组时,分别调用构造函数对于每个元素初始化。每一个元素的实参分别用括号括起来,对应构造函数的一组形参,不会混淆。
补充笔记
[*]对于局部对象,每次界说对象时,都要调用构造函数
[*]对于静态对象,在首次界说对象是,调用构造函数,且由于对象一直存在,只调用一次构造函数
[*]对于全局对象,是在main函数实行之前调用构造函数的
this指针
在类建立多个对象时,如何实现的详细对象的详细数据分开存储的呢?对象中的数据成员的名字都相同,系统通过一个this指针来实现,在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本来对象的指针,它的值是当前被调用的函数所在的对象的其起始地点。例如对象a中有数据成员volume,我们在调用的时候通常操纵为a.volume,就是给了系同一个提示我们调用的是a对象中的数据成员volume而不是其他对象当中的,其实质为this->volume,其中this的值为对象a的起始地点,this指针是作为隐式利用的,它是作为参数被传递给成员函数的。我们在平常的类和对象的利用当中,this指针的实现都是编译系统自动实现的,编程序者不必人为地在形参中增加this指针,也不必将对象a的地点传给this指针。
举个例子,例如一个计算体积的函数,隐式利用this指针为:
int Box::volume()
{
return (height*lenth*width);
}
显式利用this指针为:
int Box::volume(Box *this)
{
return ((*this)->height*(*this)->lenth*(*this)->width);
}
this指针的利用过程,只是为了更好的使读者理解this指针实现的作用和机理。
共用数据的掩护
常对象
界说常对象的一般情势为:类名 const 对象名[(实参表)];
[*] 常成员函数是常对象的唯一对外接口
[*] 常成员函数答应访问常对象中的数据成员,但是不答应修改常对象中数据成员的值
常对象成员
常数据成员
只能通过构造函数的参数初始化表对常数据成员进行初始化,其他任何函数都不能对长数据成员赋初值,因此在界说常对象时,构造函数只能用参数初始化表对长数据函数进行初始化。
常成员函数
在函数的参数表之后加上const即可,一般格式为:范例名 函数名(参数表) const。
数据成员非const的平凡成员函数const成员函数非const的平凡数据成员可以引用并修改其值只读const数据成员只读只读const对象不答应只读
[*] 假如在一个类当中,有些数据成员的值答应改变,有些不答应改变,那么可以将不可以改变的值用const修饰,确保其值不被改变,可以利用非const的成员引用这些数据成员的值并对非const的数据成员进行修改
[*] 假如要求所有的数据成员都不可以改变,那么都对其进行const修饰,然后用const成员函数引用,起到双保险的作用
[*] 常对象只能调用常成员函数,但是常对象里面并不都是常成员函数,常对象只是保证其数据成员不能被修改而已
指向对象的常指针:一般情势为:类名 *const 指针名=&所指对象名;将指针变量声明为const型,会使指针所指向的对象不可以在再改变。
指向常对象的指针变量
[*] 假如一个变量被声明为常变量,那么只能用一个指向常变量的指针来指向它,而不能用非const型的指针指向它
[*] 指向常变量的指针不仅可以用来指向常变量,也可以用来指向非const变量,但仅仅为只读。指向常对象的常指针指向的对象可以改变但是其指向的对象的值不可以改变,一般用作函数的形参,用于掩护函数调用对象的相关数据不被修改
[*] 假如函数的形参为非const型指针,那么实参需为非const型指针,因为要对其所指向的变量进行改变数据的操纵
常对象的注意事项:
[*] 在指向常对象,只可以大概用const型指针指向它
[*] 假如界说了一个指向常对象的const型指针,若它指向了非const的对象,那么该对象的值不可改变
[*] 指向常对象的指针一般用于函数的形参,可以掩护指针所指的对象的数据成员不被修改,当希望在调用函数时对象的值不被修改,就应当把形参界说为指向常对象的指针变量,同时用对象的地点作为实参。
[*] 假如界说了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量自己的值是可以改变的,即该指针指向的常对象可以改变
对象的常引用
对象可以在函数体内引用,例如void display(BOX &A);此时引用的对象A的值其中的数据成员是可以改变的,但是假如在类名后加const,即void display(BOX const &A);此时引用的对象A的值其中的数据成员是不可以改变的。
补充笔记
[*] 常对象只能调用常成员函数,即在成员函数后面加上const即可
[*] 常数据成员,在说明数据成员时用const修饰。
[*] 常数据成员只能通过构造函数参数表对其进行初始化,其他函数都不能对该成员赋值
[*] 若构造函数参数表中对一个const成员进行初始化,因为构造函数参数表的赋值晚于构造函数界说内容时对成员的初始化,将导致对初始化const成员赋值,造成编译错误
[*] 为什么要对const成员肯定用构造函数参数表赋值?因为在是用构造函数参数表赋值的话,const成员的值已经给出了,但假如利用一般的构造函数的话,const成员先创建未赋值,里面将是一个随机值。
利用平凡构造函数对const成员赋值的话,等价于对于带有随机值的const成员重新赋值,导致编译错误
[*] 常成员函数,在一般的函数之后加const即可
[*] 常对象只能调用常成员函数
[*] 常成员函数不能更新对象的数据成员
[*] 指向对象的常指针:类名 *const 指针名=&对象名;
[*] 指向对象的常指针变量的值不能被改变,但其所指对象的数据成员的值可以被改变
[*] 常用来作为函数的形参,避免调用函数时误操纵使指针指向的指的那块数据改变
[*] 指向常变量的指针变量:const 范例名 *指针变量名=&常变量;(只读)
[*] 假如一个变量已被声明为常量,则只能用指向常变量的指针变量指向它
[*] 指向常变量的指针变量也可以指向一个非const变量,即无法更改只读
[*] 假如函数的形参是指向非const型变量的指针,则实参只能用指向非const型变量的指针
[*]const->const只读
[*]const->非const只读
[*]非const->非const修改
[*] 函数的形参为const常引用环境下,表示传递的实参不答应被修改只答应被调用
[*] 类的成员函数当中如有函数含有常量引用作为函数的实参,若该类界说的对象不为常对象则直接毙掉
对象的动态建立与释放
new 类名可以创建一个该类的动态对象,但是此对象没有名字,只能通过指针来访问并利用,在new一个类名的时候,仅仅是创建了一个动态的对象。并不能去访问和利用,因此我们就必要用一个指针来指向它,以便于利用它。
类名 *指针名;
指针名=new 类名;
类名 *指针名=new 类名;
在new动态创建一个对象的时候,这个对象是无名的,无法知道它的对象名,因此无法访问和利用,但是这个对象是的的确确的存在着的,并在动态创建其时调用了自身的构造函数。想要利用它的话,就只能通过指向它的指针来访问。当不再必要利用它的时候,用
delete 指向其的指针名;
即可删除,在删除时我们要注意指针的指向,指针指向谁,删哪儿个指针,哪儿个动态建立的对象就会被删除,不要删错了。在实行delete运算符时,该动态对象首先完成析构函数进行数据的相关善后工作,而后才被删除释放内存空间。
对象的赋值与复制
对象的赋值:对于两个同类的对象来说,假设a,b对象同类a中的所有数据成员都有值而b没有,我们想让b对象中数据成员的值与a相同,那么实行b=a即可实现b对象中所有成员函数的赋值。
对象的赋值仅仅包罗对象的数据成员的赋值,而不包罗对成员函数的赋值:因为对象的数据成员是占据着存储空间的,对象的赋值仅仅为将一个对象的数据成员的空间存储状态赋值给另一个对象,使另一个对象的数据成员的空间存储状态与赋值对象完全相同。成员函数不赋值的缘故原由是因为成员函数自己就是一个代码段,而同一个类当中的成员函数的代码段都是相同的,所以不必要赋值,且无法赋值。
类的数据成员之中绝对不要包罗动态分配的数据,否则将产生及其严重的结果(记住结论即可)。
对象的复制:对象的复制就是快速创建多个与源对象一模一样的新对象,一般情势为:
类名 对象2(对象1);
创建一个对象2,与对象1中的数据完全相同,在类当中,我们必要界说一个复制函数即:
类名::类名(const 类名 &对象名)
{
数据成员1=对象名.数据成员1;
数据成员2=对象名.数据成员2;
数据成员3=对象名.数据成员3;
}
我们可以发现,这个复制函数与构造函数情势非常相似。仅仅使情势参数不同,复制函数的情势参数为对象名,且一般要加const,目的是为了放置在对象的复制过程中因操纵失误修改了源对象的值,本函数称为复制构造函数,假如用户未创建一个复制构造函数,那么系统将会自动创建,不外自动创建的复制构造函数仅仅可以大概实现简朴的数据成员的复制。还有另外一种复制对象的方式:
类名 对象2=对象1;
其作用为调用复制构造函数创立对象2与对象1中的数据成员的值相同,那么重中之重来了,对象的赋值与复制都是将一个对象的数据成员的值复刻给另一个对象,如何区分呢?复制与赋值又有什么区别呢?
对象的赋值与复制的区别
首先我们从对象创立的角度来看:对象的赋值是对一个已经存在的对象赋值,因此必须要先界说一个对象使其存在之后,才可以大概进行赋值操纵。但对象的复制则是一个对象从无到有的复刻的过程。
简朴来说:
对象的赋值是有钱吃饭,有一定的基础存在再实现
对象的复制是赚钱吃饭,创造一定的基础并去实现
平凡构造函数与复制构造函数的区别
我们可以从三方面来详细看:
[*]从构造函数的情势参数方面
平凡构造函数的声明为:
类名(数据类型 数据成员);
复制构造函数的声明为:
类名(类名 &对象名)
明显可以看出,复制构造函数的形参为对象。
[*]在建立对象时的现实参数不同
利用平凡的构造函数创建对象:
类名 对象名(数据类型 数据成员);
利用复制构造函数创建对象:
类名 对象名1(对象名2);
[*]调用的环境
平凡的构造函数在对象创立的时候就已经被调用,复制构造函数在用以有对象复制一个新对象的时候被调用。其详细环境为:
[*]程序当中在建立一个新对象的同时用另外一个对象对新对象进行初始化
[*]当函数的参数为类的对象的时候,系统必要将实参对象完备的传递给形参,因此必要建立一个实参的拷贝,即根据实参复制一个形参供复制构造函数利用,系统是通过调用复制构造函数来实现的,以此保证形参具有与实参完全相同的值
[*]函数的返回值是类的对象时,在函数调用完毕将返回值带回函数的调用处时,此时必要将函数中的一个对象复制一个临时对象并传回函数调用处,返回的对象并不是函数中所利用的对象,而是利用函数中具有肯定周期生命的对象所创建的临时对象
在以上三种环境下,系统会自动调用复制构造函数,谨记即可。
静态成员
在已往c的学习过程当中,我们相识到了static并用其创建静态变量,在c++的学习当中,我们相识到类的利用与数据范例的利用有雷同的地方,是不是代表着我们也可以创建一个静态的类对象中的数据成员或者函数呢?答案是肯定的。
静态数据成员
静态数据成员即该类的对象所能共同拥有的一个数据成员,它在各个对象中的值都是相同的,而且只占一份存储空间,而不是在每个对象当中都占一份。所有对象都可以引用它,若改变它的值,则它在所有对象中的值都将改变,如许可以节流空间,提高服从。
在第八章之中我们知道,在界说一个类的同时,其不占据存储空间的,仅仅在创建对象的时候才会为其数据承运创建存储空间。但假如在一个类当中含有static静态数据成员,无论该对象是否创建,系统都为该静态数据成员开辟存储空间,即静态数据成员不随对象的建立而分配空间,也不随对象的撤销而释放,静态数据成员是在程序开始运行时创建,程序竣事时释放空间。静态数据成员可以初始化,但要在类外进行初始化。在一个类当中,所有的对象共用这些静态数据成员,而且可以引用它们。
在类外进行初始化时:
数据类型 类名::静态数据成员名=值;
仅仅在声明静态数据成员时加static即可,不必在初始化的时候再加,注意:不能通过参数初始化表来对静态数据成员进行初始化。静态数据成员可以通过类名引用,也可以通过对象名引用。
类名::静态数据成员名;
对象名.静态数据成员名;
只有界说为公用的静态数据成员才可以在类外直接引用,若界说为private,则必要通过接口来实现间接的引用。没有界说对象,也可以访问静态数据成员,证明静态数据成员并不是属于对象的,而是属于类的,可以被类和类的对象引用。静态数据成员再某种意义上来说与全局变量作用雷同,但是不完全相同。全局变量的利用破坏了c++的面向对象程序计划的封装性,而静态数据成员恰恰弥补了这一点。
静态成员函数
静态成员函数的性质与静态数据成员相同,即他不属于对象而是属于类,可以大概通过类和类的对象调用,与一般函数的界说的区别在于,在函数的返回值范例前加static即可。
通过类名调用:
类名::函数名(参数);
通过类的对象名调用
对象名.函数名();
在之前我们相识到,当调用一个对象的非静态成员函数时,系统会默认产生一个this指针指向该对象的 起始地点,因为静态成员函数并不属于某一对象,因此其没有this指针,这也决定了静态成员函数不能方位本类当中的非静态数据成员,但也不是绝对的,可以引用非静态数据成员,只是不能进行默认访问,因为无法知道访问哪儿个对象,假如有对象名的指引,则可以引用。
**静态成员函数可以直接引用本类当中的静态成员,因为静态成员也是属于本类的,可以直接引用。**在c++当中,静态成员函数主要用来访问静态数据成员,而不访问非静态数据成员。公用的成员函数可以引用本对对象当中的静态数据成员和非静态数据成员。在c++的操纵当中,为了避免堕落,我们要养成静态成员函数仅仅引用静态数据成员的习惯。静态数据成员与静态成员函数的作用域仅仅限定在界说该类的作用域范围之内。
友元
友元即friend,通过友元函数可以访问某一个类当中private型函数,友元函数和友元类我们可以通过比喻形象的理解,我们可以把public中的数据成员和函数看作自己家的客堂,把private中的数据成员和函数看作自己家的寝室。
众所周知,在我们的传统观念当中,private相当于寝室,public相当于客堂。而友元函数则是相当于好朋友的身份,他可以进入寝室。友元类当中含有多个数据成员以及函数,所以它相当于好朋友的一家子,他们都能进寝室。
平凡函数声明为友元函数,友元成员函数与友元类
[*]将平凡函数声明为友元函数
注意:在声明时仅仅必要在平凡函数的函数范例前加friend,在界说时将这个函数当成平凡函数界说即可,即其不是类的内部函数,不必要加类名::来限定。要求:在本类以外界说的一个函数,声明在类中,且函数范例前加friend。
#include <iostream>
using namespace std;
class NUM
{
private:
string baby;
public:
friend void display(NUM &);
NUM(string);
};
int main()
{
NUM a("王嘉隆");
display(a);
return 0;
}
void display(NUM &a)
{
cout<<"运用友元函数访问类中的私有数据:"<<endl;
cout<<"我的大宝儿是"<<a.baby<<endl;
}
NUM::NUM(string a)
{
baby=a;
}
譬如以上程序,我们可以运用友元函数访问类当中的baby成员函数,注意这一行代码:
friend void display(NUM &);
意思是友元函数的情势参数是一个NUM类界说的对象,如许利用友元函数可以使友元函数的访问对象得以指定,因为当同一个友元函数在多个类的界说当中被声明时,我们不知道要用其访问哪儿一个,那么它可以访问多种类的对象的私有数据成员,我们必要为其指定,因为友元函数不是类的成员函数,没有this指针来供其利用。
[*]友元成员函数
友元函数不仅仅可以作为类外的函数,也可以作为类内的成员函数,用于访问另一个类中的数据。
#include <iostream>
using namespace std;
class Date;
class Time
{
private:
int hour;
int minute;
int second;
public:
void display(Date &);//Time类定义的对象可以访问Date类定义的对象
Time(int,int,int);//构造函数
};
class Date
{
private:
int year;
int month;
int day;
public:
Date(int,int,int);//构造函数
friend void Time::display(Date &);//声明Time类中的友元成员函数display可以对于Date类定义的对象进行访问
};
int main()
{
Time a(19,45,33);
Date b(2022,4,26);
a.display(b);
return 0;
}
Date::Date(int a,int b,int c)
{
year=a;
month=b;
day=c;
}
Time::Time(int a,int b,int c)
{
hour=a;
minute=b;
second=c;
}
void Time::display(Date &A)
{
cout<<A.year<<'-'<<A.month<<"-"<<A.day<<endl;
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
Time类当中界说的display成员函数在Date类当中界说为友元成员函数,即display成员函数在Time类当中为平凡函数,形参为Date类。display成员函数在Date类里作为其的友元成员函数。
[*]友元类
在一个类A的界说时,我们可以把另一个类Bfriend一下,而后这个类B当中的所有函数就都是类A的成员函数了,即类B中的所有函数可以访问类A中的所有数据成员。
#include <iostream>
using namespace std;
class Date;//提前声明类Date
class Time
{
private:
int hour;
int minute;
int second;
public:
void display(Date &);//Time类定义的对象可以访问Date类定义的对象
Time(int,int,int);//构造函数
};
class Date
{
private:
int year;
int month;
int day;
public:
friend Time;
Date(int,int,int);//构造函数
};
int main()
{
Time a(19,45,33);
Date b(2022,4,26);
a.display(b);
return 0;
}
Date::Date(int a,int b,int c)
{
year=a;
month=b;
day=c;
}
Time::Time(int a,int b,int c)
{
hour=a;
minute=b;
second=c;
}
void Time::display(Date &A)
{
cout<<A.year<<'-'<<A.month<<"-"<<A.day<<endl;
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
注意类在未界说时我们在其界说前利用它的时候要声明:即class 类名;
三种友元的特性总结
平凡函数作为友元友元成员函数友元类声明在此平凡函数前加friendA成员函数形参中为(B类对象)注意A类要界说在B类之前界说类外界说,与平凡函数界说格式相同B类成员函数当中加friend+函数返回值范例+A类名+::函数名+(形参)在界说B类的时候friendA类 以上环境为A类可以访问B类数据的环境。
类模板
可以大概实现功能相同,且实用于不同数据的模板通用类。由于类模板包含范例参数,因此又称为参数化的类,类是对象的抽象,对象是类的实例,类模板是类的抽象,类是类模板的实例。例如我们界说一个比较大小的类,实用于整数和浮点数以及字符:
#include <iostream>
using namespace std;
template <class nametype>
class Compare
{
private:
nametype x;
nametype y;
public:
Compare(nametype a,nametype b){x=a,y=b;}
nametype maxitem(){return (x>y)?x:y;}
nametype minitem(){return (x<y)?x:y;}
};
int main()
{
Compare <int> a(3,5);
cout<<"二者最大值为:"<<a.maxitem()<<endl;
cout<<"二者最小值为:"<<a.minitem()<<endl;
Compare <float> b(7.8,9.6);
cout<<"二者最大值为:"<<b.maxitem()<<endl;
cout<<"二者最小值为:"<<b.minitem()<<endl;
Compare <char> c('a','z');
cout<<"二者最大值为:"<<c.maxitem()<<endl;
cout<<"二者最小值为:"<<c.minitem()<<endl;
return 0;
}
第十章
重载运算符函数引言
前面我们介绍过函数的重载,即同样的函数可以大概实现不同的功能,其实运算符也可可以重载。在系统中,我们利用预处理时,#include 中就包含了输入与输出流,即>>可以看成流插入运算符利用也可以看成位移运算符利用,且<<可以看成流提取运算符利用也可以看成位移运算符利用。
假定我们设置了一个专门的复数类,那么我们是可以通过它的成员函数来实现数据的输入和输出以及加减乘除等的基本运算,例如下例我们用类的成员函数实现复数类的加法:
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0,imag=0;}
Complex(double a,double b){real=a,imag=b;}
Complex Complex_add(Complex &B);
void display();
};
int main()
{
Complex a(1.2,2.4),b(2.4,3.6),c;
c=a.Complex_add(b);
c.display();
return 0;
}
void Complex::display()
{
cout<<'('<<real<<'+'<<imag<<'i'<<')'<<endl;
}
Complex Complex::Complex_add(Complex &B)
{
Complex C;
C.real=real+B.real;
C.imag=imag+B.imag;
return C;
}
我们利用类的成员函数实现了类对象的加法,在这个成员函数当中,我们发现,复数加法的成员函数的形参仅为一个,在运算加法的过程当中,我们创建了一个临时类对象,其实我们界说的复数加法成员函数是隐蔽了一个形参,第一个隐蔽的形参为对象自己,所以创建一个临时对象之后,可以将临时对象的实数数据赋值为本对象与传入形参对象的实数的和,虚数的运算也是同样的道理。函数返回临时变量c,即我们在代码中创建了一个新的对象c,这个名字为c的对象担当了临时对象的数据,从而实现了两个复数的相加。
那么运用成员函数实现类的对象的加法运算是可以实现的,但是当代码长度过于长时,代码量会比较大且检查的时候不轻易直接找到堕落点,有没有一种简便的方法可以避免这种环境呢?那就是运算符的重载。
运算符的重载
运算符重载实质上来说就是函数的重载,对于原有的运算符进行重载,也就是对于界说函数实现,使原有的运算符不仅可以实现原有的功能,还可以大概实现我们界说的功能。在利用被重载的运算符时,系统自动调用其所在的函数模块,从而实现相应的功能。
运算符的重载在函数的界说上仅仅有很小的改变,就是在函数名上,函数名变成了关键字operator和所重载的运算符组成,即:函数返回范例 operator 所重载的运算符 (形参表){函数内容}。那么根据运算符重载的概念,我们可以对于上次利用成员函数实现复数和的代码进行修改:
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0,imag=0;}
Complex(double a,double b){real=a,imag=b;}
Complex operator + (Complex &B);//将原有的成员函数声明改为运算符的重载函数的声明
void display();
};
int main()
{
Complex a(1.2,2.4),b(2.4,3.6),c;
c = a + b;//在使用时仅仅用我们重载过的运算符即可
c.display();
return 0;
}
void Complex::display()
{
cout<<'('<<real<<'+'<<imag<<'i'<<')'<<endl;
}
Complex Complex::operator + (Complex &B)//定义运算符重载函数
{
Complex C;
C.real=real+B.real;
C.imag=imag+B.imag;
return C;
}
可以发现, 我们仅仅对与代码进行了很小的修改,就实现了我们所想要达到的结果,而且代码理解方面变得更加轻易,在这里,运算符重载函数的形参环境与上文中的一样,并不是我们故意少设置了一个形参,而是系统默认为我们设置了隐蔽形参,使函数中的real与imag数据自带this指针。
当然对于函数的当中对于临时对象的界说,我们也可以换种方法,反正末了运算竣事都是要返回一个复数型给予一个新创建的对象,那么我们也可以不进行临时对象的创建,可以返回一个无名对象,即将函数修改为:
Complex Complex::operator + (Complex &B)//定义运算符重载函数
{
return Complex(real+B.real,imag+B.imag);
}
以上的情势也可以达到同样的结果。
在我们实现这个复数类对象的加法之后,运算符的重载使系统不但可以大概对int,float,double,long,char进行运算,也可以对于复数范例进行运算,**对于单目运算符,其实行在于一侧的数据范例,对于双目运算符,其实行在于两侧的数据范例。**比如将一个复数与一个整数相加黑白法的,编译会出现错误。运算符重载的利用,贴近c++的基本头脑,即我们仅仅提供了一个类,也提供了所有运算符可以大概自顺应与之对应,用户不必在乎这些如何实现,仅仅关心如何利用,类的计划与其中数据成员与成员函数的计划(包罗运算符的重载)是类的计划人员的事变,与我们无关,实现了数据的封装。
运算符重载的规则
[*] 运算符的重载不答应自界说新的运算符,只可以大概对于系统中的运算符进行界说。
[*] 答应重载的运算符如下:
.和*运算符不可以大概重载的缘故原由是为了保证访问成员的功能不能被改变,与运算符和sizeof运算符的运算对象是范例而不是变量或者是一般的表达式,不具备重载的特性。
[*] 重载不可以大概改变运算符运算对象的个数。
[*] 重载不可以大概改变运算符的优先级别。
[*] 重载不可以改变运算符的结合性。
[*] 重载运算符的函数不可以大概有默认的参数,否则与第三点抵牾。
[*] 重载的运算符必须和用户界说的自界说范例对象一起利用,其参数至少应有一个是类对象(或者类对象的引用)。比如重载一个+运算符,参数为int型,将其重载为两数相减,则于系统标准库当中的运算符发生冲突产生了歧义无法精确编译,是绝对克制的。即参数不可以全部是c++当中的标准范例。
[*] 用于类对象的运算符一般要重载,但是对于=与&则不用。因为=运算符已经可以大概实现数据成员之间的复制和对于对象的复制,也可以实现类的对象的赋值。但是当类中含有动态分配内存的指针成员时,就会发生伤害,仅在在此种环境之下对于=重载。对于&运算符,它可以大概返回类对象在内存中的起始地点,不必重载。
[*] 从理论上来说,可以将一个运算符重载为实行恣意的操纵,但是为了避免重载之后实现的功能让人难以理解,我们应当使运算符重载之后可以大概实现雷同于的原有功能。
运算符重载函数作为类的成员函数和友元函数
在运算符重载函数当中,我们可以将其分为两种范例:第一种是作为类的成员函数,第二种是为平凡函数,在类当中声明其为友元函数。在作为类的成员函数的时候,仅有一个参数,正如上两节之中提到的,在此就不再赘述。那么作为友元函数的时候我们可以如许写:
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0,imag=0;}
Complex(double a,double b){real=a,imag=b;}
friend Complex operator + (Complex &A,Complex &B);//将原有的成员函数声明改为运算符的重载函数的声明
void display();
};
int main()
{
Complex a(1.2,2.4),b(2.4,3.6),c;
c = a + b;//在使用时仅仅用我们重载过的运算符即可
c.display();
return 0;
}
void Complex::display()
{
cout<<'('<<real<<'+'<<imag<<'i'<<')'<<endl;
}
Complex operator + (Complex &A,Complex &B)//定义运算符重载函数
{
return Complex(A.real+B.real,A.imag+B.imag);
}
我们可以发现,在将运算符重载函数声明为友元函数时,我们必须要写两个形参,界说时也是,不可以省略。且往往将第一个实参传入的数据放入运算符左侧,第二个实参传入的数据放入运算符右侧。运算符重载后,对于实参数据的利用有着严酷的位置要求,即不依照加法的交换律。
假如将运算符重载函数作为类的成员函数,可以少写一个参数,但是必须第一个参数是一个类的对象,而且于运算符函数的范例相同。因为必须通过类的对象去调用该类的数据成员,而且只有运算符重载函数返回值与该对象范例相同,运算结果才有意义。 假如运算符左侧是一个其他的类的对象或者是c++的标准范例,那么运算符重载函数不可以大概作为成员函数,而只能作为非成员函数。
由于友元的利用从某些方面来说破坏了类的封装,因此从原则上来说,要尽量将运算符的重载函数设置为类的成员函数,但是当运算符的重载函数必要访问到类的私有数据成员时,则必须为友元函数。
[*] c++规定,=、[]、->、()的重载函数必须为类的成员函数。
[*] 流的插入与流的提取>>、<<的重载函数必须重载为类的友元函数
[*] 一般将单目运算符和复合运算符的重载函数为类的成员函数
[*] 一般将双目运算符的重载函数为类的友元函数
重载双目运算符
上节之中我们已经提到,对于双目运算符的重载函数要为类的友元函数,对于关系运算符的重载我们通常会利用bool,比如界说一个String类,重载>、<、==运算符来判定类的对象之间的关系:
#include <iostream>
#include <string.h>
using namespace std;
class String
{
private:
char *p;//用于指向字符串的指针
public:
String(){p=NULL;}
String(char *str);
friend bool operator == (String &A,String &B);
friend bool operator > (String &A,String &B);
friend bool operator < (String &A,String &B);
void display();
};
void Compare(String &A,String &B);
int main()
{
String A("hello"),B("book"),C("computer"),D("hello");
Compare(A,B);
Compare(B,C);
Compare(A,D);
}
String::String(char *str){p=str;}
bool operator == (String &A,String &B)
{
if(strcmp(A.p,B.p)==0)return true;
else return false;
}
bool operator > (String &A,String &B)
{
if(strcmp(A.p,B.p)>0)return true;
else return false;
}
bool operator < (String &A,String &B)
{
if(strcmp(A.p,B.p)<0)return true;
else return false;
}
void Compare(String &A,String &B)
{
if(operator == (A,B)==1)cout<<"长度相等"<<endl;
else if(operator > (A,B)==1){A.display(),cout<<"长于",B.display(),cout<<endl;}
else if(operator < (A,B)==1){A.display(),cout<<"短于",B.display(),cout<<endl;}
}
void String::display()
{
cout<<p;
}
在这里我们界说运算符的重载函数返回范例为bool型,以便Compare函数调用
重载单目运算符(自增和自减)
类名 operator 需要重载的运算符 ();//前置
类名 operator 需要重载的运算符 ();//后置
在界说时注意要有返回值,对于数据进行操纵之后返回*this指针即可
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0,imag=0;}
Complex(double a,double b){real=a,imag=b;}
Complex operator ++ ();//前置
Complex operator ++ (int);//后置
Complex operator -- ();//前置
Complex operator -- (int);//后置
void display();
};
int main()
{
Complex a(1.2,2.4),b(2.4,3.6),c(3.6,4.8),d(4.8,6.0);
a.display();
b.display();
c.display();
d.display();
++a;
a.display();
b++;
b.display();
--c;
c.display();
d--;
d.display();
return 0;
}
void Complex::display()
{
cout<<'('<<real<<'+'<<imag<<'i'<<')'<<endl;
}
Complex Complex::operator ++ ()
{
++real;
++imag;
return *this;
}
Complex Complex::operator ++ (int)
{
real++;
imag++;
return *this;
}
Complex Complex::operator -- ()
{
--real;
--imag;
return *this;
}
Complex Complex::operator -- (int)
{
real--;
imag--;
return *this;
}
重载插入流运算符和提取流运算符
istream& operator >> (istream&,类名&);//插入流运算符
ostream& operator << (ostream&,类名&);//提取流运算符
istream& operator >> (istream& 插入流名称,类名& 形参对象名);
ostream& operator >> (ostream& 提取流名称,类名& 形参对象名);
注意return插入流名称或者return提取流名称保证连续插入与连续提取的实现
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0,imag=0;}
Complex(double a,double b){real=a,imag=b;}
friend istream& operator >> (istream&,Complex&);
friend ostream& operator << (ostream&,Complex&);
};
int main()
{
Complex a;
cin>>a;
cout<<a;
return 0;
}
istream& operator >> (istream& input,Complex& A)
{
input>>A.real>>A.imag;
return input;
}
ostream& operator << (ostream& output,Complex &A)
{
output<<"("<<A.real<<"+"<<A.imag<<"i"<<")"<<endl;
return output;
}
有关运算符重载的归纳
[*] 运算符的重载体现了面向对象的程序计划的灵活性与可实用性,可以大概结合现实问题计划并方便问题的解决。例如计划的复数类,对于其界说的类的对象的操纵可以通过运算符快速且便捷的实现,同时也可以体现类的封装性,也方便利用者可以大概更好的理解代码。 运算符的重载使类的计划丰富多样,扩大了类的功能和实用范围,使程序易于理解,易于对对象进行操纵,它体现了为用户着想、方便用户利用的头脑。有了好的类,利用者就不必再利用复杂的函数调用来实现,可以大概使主程序更加简朴易读。好的运算符重载可以大概体现面向对象程序计划的头脑。
[*] 利用运算符重载的详细做法:先确定要重载的是哪儿一个运算符,想把它用于哪儿一个类。重载运算符只可以大概将一个运算符用于一个指定的类而不是所有的类。计划运算符重载函数和有关的类。在现实的工作之中,很多环境下并不是我们自己去编写一个类以及其中的成员函数(包罗运算符重载函数等),而是这些工作由其他人完成,我们仅仅必要调用和利用即可,这叫做项目的互助。在利用其他人界说的类时,我们必要包含类的计划者所提供的头文件,如许就可以大概在编码过程当中利用其他人的类。利用者必要相识在该头文件包含哪儿些运算符的重载,实用于哪儿些类,有哪儿些参数。也就是必要相识运算符的重载函数的原型,就可以方便的利用该运算符了。假如有特殊的必要,而且没有现成的重载运算符可以利用,就必要自己计划重载运算符函数。应当注意把每次计划的重载运算符函数保存下来,以免下次用到时要重新计划。
[*] 在本章所举的各个例子当中,我们发现函数不采用以往的虚实结合的方式,而是采取引用的方式,利用这种方式我们可以淘汰时间和空间的开销。假如重载函数的返回值是对象的引用时,返回的不是常量,而是引用所代表的对象,它可以出现在赋值号的左侧而成为左值,可以被赋值或参与其他操纵。但利用引用时要警惕,因为修改了引用就代表修改了它所代表的对象。
[*] c++大多数运算符都可以重载,在本章所举的例子当中仅仅扳连到了部分运算符的重载,希望各人可以通过这些例子举一反三。
不同范例数据间的转换
标准范例数据间的转换:隐式转换如下例,属于系统的自我转换
i = 6;
i = 0.5 + i;
第二行代码运行时i的值为0.5+6=6.5,但是由于i时整形数据,所以自动转换之后i值为6。c++也提供了显示转换,其情势为范例名(数据) ,在c语言当中的情势为(范例名)数据。系统可以相识标砖范例数据之间的转换,那么我们是否可以类比一下,将一个类的对象转换为另一个类的对象呢?答案是可以的,但是我们要用某种方式让系统知道,我们怎么引导它实现范例的转换。
用转换构造函数进行不同范例数据的转换:转换构造函数的作用是将一个其他范例的数据转换成一个类的对象。那么为了避免混淆其他构造函数的概念,我们先来复习一下构造函数的几种范例:
[*]默认构造函数
Complex();
[*]用于初始化的构造函数
Complex(double a,double b);
[*]用于复制对象的复制构造函数
Complex(Complex &c);
[*]转换构造函数
Complex(double r){real=r;imag=0;}
那么上例当中的这个转换构造函数的功能是将一个浮点型转换为复数类的对象,并将r作为复数的实部,虚部为0.用户可以根据必要界说转换构造函数,在函数体中告诉编译系统怎么样去范例转换。在类体当中可以有转换构造函数,也可以没有转换构造函数,视必要而定。
以上的四种构造函数可以同时出现一个类当中,他们是构造函数的重载。编译系统会根据建立对象是给出的实参的个数与范例选择形参与之匹配的构造函数。
Complex c(3.5);
调用转换构造函数使浮点型常数3.5转换为一个名为c的实部为3.5虚部为0的复数类的对象,当然也可以建立一个无名对象,但无法利用:
Complex(3.5);
不外可以创建一个类的有名对象,将无名对象赋值给有名对象:
c=Complex(3.5);
由于运算符重载的界说使重载过后的运算符的运算两侧必须要是同范例,所以当用一个复数的对象与一个浮点数相加时编译会堕落,这时候就可以对于浮点数进行范例转换:
c=c1+Complex(3.5);
类的范例转换于标准范例数据间的转换相同,也存在强制范例转换,如上例。转换构造函数的界说要有意义,转换构造函数只能有一个参数,假如有多个参数,则不为转换构造函数,因为系统无法确定将哪儿个参数转换为类的对象。
利用转换构造函数将一个指定的数据转换为类对象的方法如下:
[*] 先声明一个类。
[*] 在这个类中界说一个只有一个参数的构造函数,参数的范例是必要转换的范例,在函数体中指定转换的方法。
[*] 在该类的作用域内可以用以下情势进行转换:类名(指定范例的数据) ,就可以将指定范例的数据转换为此类的对象。
当然对于转换构造函数的利用不仅仅可以将一个标准范例数据转换为类对象,也可以将另一个类的对象转换成转换构造函数所在的类对象,例如:
Teacher(Student& s){num=s.num;strcpy(name,s.name);sex=s.sex;}
学生毕业之后当了教师,数据的转入,但注意对象中的num,name,sex必须是公用成员,否则不能被类外引用。
范例转换函数:前面介绍了构造转换函数可以大概将一个标准范例的数据转换为一个类的对象、一个类的对象转换为另一个类的对象,那么能不能用一种函数让一个类的对象转换成一个标准范例的数据呢?c++提供了范例转换函数来解决了这个问题,范例转换函数是将一个类的对象转换成另一范例的数据,比如在一个复数类当中:
operator double()
{return real;}
函数返回的是double型real的值,请注意函数名是operator+范例名,即范例转换函数的模板是
operator 类型名()
{实现转换的语句}
**在函数名之前不可以大概指定函数的范例,且函数没有参数,其返回值的范例是由函数名中指定的范例名所确定的。范例转换函数只可以大概作为成员函数,因为转换的主体是本类的对象,不能作为友元函数或者平凡函数。**从函数情势上可以看得出来范例转换函数于运算符重载函数较为相似,其范例名的利用不仅可以使系统可以大概识别原有的double型,还可以将Complex类对象作为double型的数据处理。Complex类对象只有在必要时才转换,并不是所有的类对象都一律转换为double范例数据。
转换构造函数和范例转换函数都有一个共同的功能:在必要时,编译系统会自动调用这些函数,建立一个无名的临时对象或者临时变量。
结合现实来说就是:
[*]转换构造函数是将标准范例数据转换为类对象
[*]范例转换函数是将类对象转换为标准范例数据
所以当在利用加法的时候,一共可以看作有两种环境供我们相识:
第一种环境:在一个复数类当中double d1,d2;Complex c1;运行d1=d2+c1;那么此处的加号若未经过重载且不能对于复数类进行加法运算且该类当中含有对于复数类的范例转换函数使复数范例转换为double范例的函数时,系统就会自动寻找我们界说的范例转换函数将加号左右的范例调整为一致,即将c1调整为double范例。
#include <iostream>using namespace std;class Complex{ private: double real; double imag; public: Complex(){real=0;imag=0;} Complex(double a,double b){real=a;imag=b;} operator double()
{return real;}
//范例转换函数,将复数类对象转换为double范例数据 };int main(){ double d1,d2=2.4; Complex c1(1.2,2.4); d1=d2+c1;//2.4+1.2=3.6 cout<<d1; return 0;} 第二种环境:在一个复数类当中double d1;Complex c1,c2;运行c2=c1+d1;那么此处的加号若经过复数类的重载,能运行复数类对象的加法时,且复数类当中含有构造转换函数时,系统会自动调用构造转换函数使加法双方的数据范例均为复数型,即d1转换为Complex型。
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(){real=0;imag=0;}
Complex(double a,double b){real=a;imag=b;}
Complex(double a){real=a;imag=0;}//转换构造函数
friend Complex operator + (Complex A,Complex B);
void display();
};
int main()
{
Complex c1,c2(3.6,4.8);
double d1=1.2;
c1=c2+d1;//(1.2,0)+(3.6,4.8)=(4.8,4.8)
c1.display();
}
Complex operator + (Complex A,Complex B)
{
return Complex{A.real+B.real,A.imag+B.imag};
}
void Complex::display()
{
cout<<'('<<real;
if(imag>0)cout<<'+'<<imag<<'i'<<')'<<endl;
else cout<<imag<<'i'<<')'<<endl;
}
从其中的隐式调用我们可以发现一个规律,在已界说了相应的转换构造函数的环境下,将运算符’+'重载为友元函数时可以利用交换律。假如运算符重载函数不为友元函数而为类的成员函数时,则结论不成立,因为作为运算符重载函数成员函数时,省略了一个形参,这个形参往往以this指针的情势指向其对象,例如:
c1+2.5
系统把它识别成为c1.operator + (2.5);可以精确运行等价于c1.operator + (Complex(2.5));
2.5+c1
系统则会将它识别为2.5.operator + (c1);很显然是错误的。通常把范例转换函数称为范例转换运算符函数,由于它也是重载函数,因此也称为范例转换运算符重载函数。
结论:假如运算符函数重载为成员函数,它的第一个参数必须是本类的对象。当第一个操纵数不是类对象时,不能将运算符函数重载为成员函数,假如将运算符’+'+函数重载为类的成员函数,则交换律不实用。
所以在一般环境下将双目运算符重载为友元函数,将单目运算符则多重载为成员函数
假如第一个参数不是本类的对象时肯定要将运算符函数重载为成员函数也不是不可,要界说另外一个重载函数,只是形参位置不同,从而实现加法交换律。
同时还有一个小条件要记住:类的转换构造函数、类的运算符重载函数不可以大概与类的范例转换函数同时存在,否则将会出现二义性,使系统无法识别,造成编译错误。
第十一章
引言
通过对于c++第八章、第九章、第十章的开端学习我们可以里相识到面向对象程序计划的两个紧张特性:数据抽象和封装,学会了在程序中利用类和对象,写出了基于对象的程序,这是面向对象程序计划的基础。现在让我们复习一下面向对象程序计划的四个特点:抽象性,封装性,继承性,多态性。
面向对象技术夸大软件的可重用性,极大的方便了程序的开发和人力物力资源的节流。那么继承性是如何做到这一点的呢?我们在详细的程序计划的过程当中,会发现有很多的程序模块拥有相同或者雷同的功能,但我们在编写过程当中假如没有继承性的话还得重头再来写一个实现雷同功能的程序模块, 很显然如许的工作量是极其巨大的。我们利用继承性可以不必重写功能相似的代码模块,只必要在其添加一些额外的功能与数据即可,这就是继承性的应用与作用。
继承与派生的概念
继承在c++当中是一个很紧张的概念。例如在一个student类当中,有学号,名字,性别:
class Student
{
private:
string name;
int num;
char sex;
public:
Student(string a,int b,char c){name=a,num=b,sex=c;}
void display()
{
cout<<"name:"<<name<<endl;
cout<<"num:"<<num<<endl;
cout<<"sex:"<<sex<<endl;
}
};
若在其中加上地点和年事信息,则我们不利用继承的话还得重新创建一个类:
class Student1
{
private:
string name;
int num;
char sex;
int age;
string address;
public:
Student(string a,int b,char c,int d,string e){name=a,num=b,sex=c,age=d,address=e;}
void display()
{
cout<<"name:"<<name<<endl;
cout<<"num:"<<num<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"age:"<<age<<endl;
cout<<"address"<<age<<endl;
}
};
很明显我们仅仅对于原Student类进行了些微的修改,明显有着很多的数据皆一致,却必要重新创建类,很显然是费时费力的。那么我们可以利用继承性,使Student1类继承Student类的数据,从而淘汰工作量。c++的继承机制就是为相识决此类问题的出现。在c++当中所谓的继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类或者父类,新建立的类称为派生类或者子类。
一个新的类从已有的类哪里获得其已有属性,这种现象称为类的继承。通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一个角度来说,从已有的父类产生一个新的子类叫做类的派生。类的继承是用已有的类来建立专用类的编程技术。
其实父类和子类的关系我们可以用数据结构当中的树来比较,把根节点比作祖宗也就是基类,把下面的叶节点比作儿子也就是派生类。不外派生类所拥有的基类不肯定拥有,但是基类拥有的派生类肯定会有。即拿人的一生来比喻,你小时候拥有的技能你长大后也会拥有,但是你长大后拥有的技能你小时候却不肯定拥有,就是这个道理。当一个派生类只从一个基类派生时,这被称作为单继承。一个派生类拥有两个及以上的基类称为多重继承。派生类是基类的详细化,
派生类是基类的详细化,基类是派生类的抽象。
派生类的声明方式
class 派生类名:[继承方式] 基类名
{
派生类新增加的成员
}
在这里继承方式有多样:private,public,protected,若不申明则为private。例如我们把上例当中的Student1类声明为Student的派生类:
class Student
{
private:
string name;
int num;
char sex;
public:
void display()
{
cout<<"name:"<<name<<endl;
cout<<"num:"<<num<<endl;
cout<<"sex:"<<sex<<endl;
}
};
class Student1:public Student
{
private:
int age;
string address;
public:
void display()
{
cout<<"age:"<<age<<endl;
cout<<"address"<<age<<endl;
}
};
派生类的构成
派生类当中的成员包罗从基类继承过来的成员和自己增加的成员两大部分。从基类继承的成员体现了派生类从基类继承而获得的共性,新增加的成员体现了派生类的个性。正是这些新增加的成员体现了派生类与基类的不同,体现了不同派生类之间的区别。究竟上并不是把基类的成员和派生类自己增加的成员简朴地加载一起就成为派生类。构造一个派生类包罗以下3部分工作:
[*]从基类接收成员。
派生类对于基类的所有成员的要求的接收均为强制性的接收,派生类对于基类中的成员不可以选择性的接收,这也就导致了某些类在派生的过程之中有的数据不可以大概利用而占用内存空间,造成数据的传递过程当中也耗费了不少的时间,低落了服从,这是现在c++无法解决的。所以在基类的计划当中我们也要用心,使派生过程中的冗余量最小。
[*]调整从基类担当的成员。
固然对于从基类担当的成员是无法选择的,但是程序员可以对于这些成员作出调整,例如改变成员的属性为private,或者说是直接替代。**我们可以通过设置与基类当中同名的变量,则派生类当中的新成员会覆盖掉原有的变量。**在进行函数的替代时,不仅仅要求函数名要相同,函数的形参表(参数的个数和范例)也要相同。假如不相同,则为函数的重载,而不是覆盖。
[*]在声明派生类时增加的成员。
增加的成员也要经过精心计划,方便利用与理解。在声明派生类时,析构函数和构造函数是不可以大概从基类继承过来的,所以我们还应该手动界说。
从以上三点我们可以看出来:派生类是基类界说的连续,可以先声明一个基类,在此基类中只提供某些最基本的功能,而另外有些功能并未实现,然后在声明派生类时参加某些详细的功能,形成实用于某一特定应用的派生类。通过对基类声明的连续,将一个抽象的基类转化成详细的派生类。因此,派生类是抽象基类的详细实现。
派生类成员的访问属性
对于基类成员和派生类自己增加的成员是按不同原则处理的,比如以下六种环境:
[*] 基类的成员函数访问基类成员
[*] 派生类的成员函数访问派生类自己增加的成员
[*] 基类的成员函数访问派生类的成员
[*] 派生类的成员函数访问基类的成员
[*] 在派生类外访问派生类的成员
[*] 在派生类外访问基类的成员
对于1和2中的环境,经过我个人的实践发现,派生类仅仅可以访问基类中的public和protected成员,对于基类当中的private成员仍必要借助基类中public的对外接口访问。当然对于派生类自己增加的成员来说,可以通过自己的成员函数来访问。对于3中的环境,基类成员函数只能访问基类成员,不可以访问派生类成员。对于5中的环境,在派生类外可以访问派生类的公有成员,而不可以大概访问私有成员。对于4和6当中的环境要详细讨论,比如继承关系。
(1)公用继承
基类的公有成员和掩护成员在派生类当中保持原有的访问属性,其私有成员仍为基类私有。
(2)私有继承
基类的公有成员和掩护成员在派生类中成了私有成员。其私有成员仍为基类私有。
(3)受掩护的继承
基类的公有成员和掩护成员在派生类中成了私有成员,其私有成员仍为基类私有。掩护成员的意思是,不能被外界引用,但可以被派生类的成员引用。
公有继承
即继承方式为public的继承称为公有继承,用公有继承方式建立的派生类称为公有派生类,其基类称为公有基类。在公有继承当中,基类的private数据对于派生类来说仍为private数据,是不可访问的成员,只有基类自己的成员函数可以大概引用。 但派生类可以大概访问基类当中的public和protected数据,且访问private数据的方式仅仅只有通过派生类函数调用基类函数,通过基类函数间接访问这一种方式可以实现。
c++系统的数据的封装性体现在基类的私有成员不可以在派生类当中被派生类的成员函数给直接引用,如许有利于测试、调试和修改系统。掩护私有成员是一条紧张的原则。
私有继承
在声明一个派生类时将基类的继承方式指定为private称为私有继承,用私有方法建立的派生类称为私有派生类,其基类称为私有基类。
私有基类的共用成员和掩护乘员在派生类中的访问属性相当于派生类中的私有成员,及派生类的成员函数可以访问他们,而在派生类外不可以访问,私有基类的私有成员在派生类中称为不可访问成员,只有基类的成员函数可以引用它们。一个基类成员在基类当中的访问属性何在派生类当中的访问属性可能是不同的。私有基类的某些成员可以被基类的成员函数访问,但不能被派生类的成员函数访问。
即对于派生类来说,私有基类的公用成员和掩护成员成为了派生类的私有成员,私有基类的私有成员被隐蔽:
[*] 不能通过派生类对象引用从私有基类继承过来的任何成员
[*] 派生类的成员函数不能访问私有基类的私有成员,但是可以访问私有基类的公用成员和受掩护成员。
固然私有基类的私有成员不可以被派生类访问,不外派生类可以通过派生类的成员函数调用私有基类的公有成员函数,再利用私有基类的公有成员函数对于私有基类的私有数据进行访问。
掩护成员和掩护继承
protected声明的成员为受掩护的成员,简称掩护成员。受掩护的成员不可以被类外访问,但掩护成员可以被派生类的成员函数引用。假如基类声明了私有成员,那么任何的派生类都是不能访问他们的,若希望在派生类中可以大概访问他们,应当把他们声明为掩护成员,假如在一个类当中声明了掩护成员,那么也就意味着此类可能会用作基类。在界说一个派生类时将基类的继承方式指定为protected的称为掩护继承,用掩护继承的方式建立的派生类称为掩护派生类,其基类称为受掩护的基类,简称掩护基类。
掩护基类的特点是:掩护基类的公有成员和掩护成员在派生类中都成了掩护成员,其私有成员仍为基类私有,也就是把基类原有的公有成员也掩护起来,不让类外随意访问。
[*] 掩护基类的所有成员在派生类当中都被掩护起来,类外不能访问,其公有成员和掩护成员可以被派生类的成员函数访问私有成员则不可访问。
[*] 对于私有继承和掩护继承比较来说,当仅仅有一个派生类时,作用相同。如有多个派生类时,私有基类的成员在新的派生类当都成为了不可访问的成员,无论在派生类表里都不可以访问。而掩护继承中的成员在新的派生类当中为掩护成员,可以被新生派生类的成员函数访问。
[*] 基类当中的私有成员被派生类继承后变为不可访问的成员,派生类当中的一切成员均无法访问他们,假如在派生类当中必要引用基类的某些成员,那么应当将基类的这些成员声明为protected而不是private。
[*] 在类的派生中,成员有四种不同的访问属性
(1)公有的,派生类内类外均可访问
(2)受掩护的,派生类内可以访问,派生类外不可访问,下一层派生类可以访问
(3)私有的,派生类内可以放为,派生类外不可访问
(4) 不可访问的,派生类内类外均不可访问
(5)类的成员在不同作用域中有不同的访问属性
多级派生时的访问属性
类B是类A的直接派生类,类C是类A的间接派生类。在类的多级派生中,可以发现,把无论哪儿一种继承方式,在派生类是不能访问基类的私有成员的,私有成员只能被本类的成员函数所访问,毕竟派生类和基类不是同一个类。
假如在多级继承当中都采用公有继承,那么直到末了一级的派生类也可以大概访问基类的公用成员和掩护成员。假如采用的是私有继承,经过多少次继承之后,基类的所有成员已经变成不可访问的了。假如采用掩护继承,在派生类外是无法访问派生类中的任何成员的,而且经过多次派生之后,人们很难记清楚哪儿些成员可以访问哪儿些不可以访问,很轻易堕落。
因此在现实操纵中,最常用的是公有继承。
派生类的构造函数和析构函数
简朴的派生类的构造函数
在对于派生类进行构造函数界说时,若基类无构造函数,我们则必要将派生类中的所有数据包罗从基类当中继承来的数据进行初始化,这就造成了非常大的工作量。那么假如基类当中拥有构造函数而且在派生类当中继承过来的话我们就可以省很多事:
#include <iostream>
using namespace std;
class point
{
protected://便于派生类直接访问避免嵌套调用函数
double x,y;
public:
point(double a=0,double b=0):x(a),y(b){}//参数表构造赋值一体化
};
class circle:protected point//注意保护继承中派生类可访问基类的public与protected
{
protected://便于派生类直接访问避免嵌套调用函数
double r;
public:
circle(double a=0,double b=0,double c=0):point(a,b),r(c){}//参数表构造赋值一体化
};
class cylinder:private circle//注意私有继承派生类可以访问基类的public和protected
{
protected://便于派生类直接访问避免嵌套调用函数
double h;
public:
cylinder(double a=0,double b=0,double c=0,double d=0):circle(a,b,c),h(d){}//参数表构造赋值一体化
friend ostream& operator << (ostream& output,const cylinder& A);//友元运算符重载函数的声明
};
int main()
{
cylinder A(1.2,2.4,3.6,4.8);
cout<<A;
return 0;
}
ostream& operator << (ostream& output,const cylinder& A)
{
//因为protected的使用,在这里对于数据的调用输出就方便了许多
output<<"该圆柱体的高为:"<<A.h<<endl<<"该圆柱体的底面半径为:"<<A.r<<endl<<"该圆柱体底面的一个点的坐标为"<<"("<<A.x<<','<<A.y<<")"<<endl;
return output;
}
派生类构造函数一般情势为:
[*]派生类构造函数名(总参数表) :基类构造函数名(参数表)
[*]{在派生类当中新增的数据成员的初始化语句}
在总参数表中包含所有的所必要用到的参数,在总参数表当中包罗参数的范例和参数名,而在基类构造函数名后面的参数表中仅仅包罗参数名而没有参数范例,因为在这里**不是界说基类构造函数而是调用基类构造函数,因此这些参数是实参而不是形参。**它们可以是常量、全局变量和派生类构造函数总参数表中的参数。
在建立一个对象的时候,若派生类的构造函数中继承了基类的构造函数,那么派生类构造函数则会优先调用基类的构造函数,然后再实行派生类构造函数自己。同样,先实行派生类的析构函数,再实行基类的析构函数。
有子对象的派生类的构造函数
内嵌对象即为子对象,简朴来说就是对象中的对象。即在一个类的界说当中用已经界说过的类作为其数据成员来利用。
#include <iostream>
using namespace std;
class Student
{
protected:
string name;
int num;
public:
Student(string a,int b):name(a),num(b){}
void display(){cout<<name<<' '<<num<<endl;}
};
class Studento:public Student
{
private:
string add;
Student monitor;
public:
Studento(string a,int b,string c,int d,string e):Student(a,b),monitor(c,d){add=e;}
void show_monitor()
{
cout<<"该班长是:";
monitor.display();
}
void show()
{
cout<<"该学生是:";
display();
cout<<"他家住在:"<<add<<endl;
}
};
int main()
{
Studento A("赵铁柱",100,"王大锤",101,"图图幼儿园");
A.show();
A.show_monitor();
return 0;
}
界说派生类构造函数的一般情势为:
[*]派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
[*]{派生类当中新增数据成员初始化语句}
实行派生类构造函数的顺序是
[*] 调用基类构造函数对于基类数据成员初始化
[*] 调用子对象构造函数,对子对象数据成员初始化
[*] 再实行派生类构造函数自己,对派生类数据成员进行初始化
多层派生时的构造函数
一个类不仅可以派生出一个派生类,派生类还可以继承派生,就像人类繁衍一样,生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生生。
#include <iostream>
using namespace std;
class point
{
protected://便于派生类直接访问避免嵌套调用函数
double x,y;
public:
point(double a=0,double b=0):x(a),y(b){}//参数表构造赋值一体化
};
class circle:protected point//注意保护继承中派生类可访问基类的public与protected
{
protected://便于派生类直接访问避免嵌套调用函数
double r;
public:
circle(double a=0,double b=0,double c=0):point(a,b),r(c){}//参数表构造赋值一体化
};
class cylinder:private circle//注意私有继承派生类可以访问基类的public和protected
{
protected://便于派生类直接访问避免嵌套调用函数
double h;
public:
cylinder(double a=0,double b=0,double c=0,double d=0):circle(a,b,c),h(d){}//参数表构造赋值一体化
friend ostream& operator << (ostream& output,const cylinder& A);//友元运算符重载函数的声明
};
int main()
{
cylinder A(1.2,2.4,3.6,4.8);
cout<<A;
return 0;
}
ostream& operator << (ostream& output,const cylinder& A)
{
//因为protected的使用,在这里对于数据的调用输出就方便了许多
output<<"该圆柱体的高为:"<<A.h<<endl<<"该圆柱体的底面半径为:"<<A.r<<endl<<"该圆柱体底面的一个点的坐标为"<<"("<<A.x<<','<<A.y<<")"<<endl;
return output;
}
我们可以发现:当进行多层派生时我们仅仅必要列出上一层派生类的构造函数即可,在多层构造函数的调用当中,构造函数的调用是从基类逐层往上开始调用。
派生类构造函数的特殊情势
[*] 不必要对新增的派生类中的成员进行任何操纵时,{}中为空即可
[*] 假如在基类当中没有界说构造函数,或界说了没有参数的构造函数,那么在界说派生类构造函数时可以不写基类构造函数,系统会优先调用基类默认的构造函数。
假如在基类或者子对象范例的声明中都没有界说带参数的构造函数,而且也不许对派生类自己的数据成员初始化,那么可以不必显式的界说派生类构造函数。
假如在基类或者子对象范例的声明中界说了带参数的构造函数,那么就必须显式地界说派生类构造函数,并在派生类构造函数中写出基类或者子对象范例的构造函数以及其参数表
假如在基类中既界说了无参的构造函数又界说了有参的构造函数,则在界说派生类构造函数时,既可以包含基类构造函数及其参数又可以不包含基类构造函数。
派生类的析构函数
在派生时,派生类是不能继承基类的析构函数的,也必要通过派生类的析构函数去调用基类的析构函数。在派生类中可以根据必要界说自己的析构函数,用来对派生类中所增加的成员进行清算工作,基类的清算工作仍由基类的析构函数负责。在实行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类的子对象进行清算。
调用的顺序恰恰与构造函数相反,先调用派生类的析构函数,对于派生类新增的成员进行清算,在调用子对象的析构函数对于子对象进行清算,末了调用基类的析构函数对于基类进行清算。
多重继承
声明多重继承的方法
为了符合一个类从多个类继承的这种环境,也就有了多重继承的出现,例如class D:public A,protected B,private C{D类增加的数据成员}。
多重继承派生类的构造函数
派生类构造函数名(总参数表):基类1构造函数(参数表), 基类2构造函数(参数表),基类3构造函数(参数表){派生类中新增数据成员初始化语句}。
各个基类的排列顺序恣意,派生类构造函数的实行顺序先调用基类的构造函数,再实行派生类构造函数的函数体。调用基类的构造函数的顺序是按照声明派生类时基类出现的顺序,那让我们实现一下,当一个学生毕业后打算留校当老师。提示将Graduate继承于Student和Teacher :
#include <iostream>
using namespace std;
class Student
{
protected:
string name;
int age;
double score;
public:
Student(string s,int a,double b):name(s),age(a),score(b){}
void display()
{
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"score:"<<score<<endl;
}
};
class Teacher
{
protected:
string name0;
char sex;
string title;//职称
public:
Teacher(string a,char b,string c):name0(a),sex(b),title(c){}
void display1()
{
cout<<"name:"<<name0<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"title:"<<title<<endl;
}
};
class Graduate:public Student,public Teacher
{
private:
double wage;//津贴
public:
Graduate(string a,char b,int c,double d,string e,double f):Student(a,c,d),Teacher(a,b,e){wage=f;}
void show()
{
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"age:"<<age<<endl;
cout<<"score:"<<score<<endl;
cout<<"title:"<<title<<endl;
cout<<"wage:"<<wage<<endl;
}
};
int main()
{
Graduate A("大怨种",'m',24,98,"讲师",888);
A.show();
return 0;
}
在代码的实现过程中我们可以发现,为了避免二义性的出现,我们把该毕业生的名字在Student类中设置为了name,在Teacher类中设置为了name0,此外display函数我们也做了微微的修改,这是为了避免同名时出现二义性导致编译错误,当然这种方法是可行的,不外有的时候会引起思维混论和错误难以检查出来,是最低级的避免二义性的做法。那么什么是二义性呢?
多重继承引起的二义性问题
多重继承可以大概很好的对于现实中的环境进行处理,但是同时也引入了新的问题,如在派生类与多个基类的数据成员或者成员函数重名时将会引起二义性的产生,编译及报错。它也增加了程序的复杂度,是程序的编写和维护变得相对困难,轻易堕落。
我们从三种环境讨论:
第一种环境和第二种环境均是建立在C类继承于A类和B类的环境上的
第一种环境:
当C类的新建的成员函数和数据成员的名称无与A、B类的成员函数或者数据成员的名称一致时,且A、B两个类当中有成员重名,系统则会在编译的时候出现错误,即产生了二义性。解决办法是假如在C类中调用A、B类中的重名成员时要加上“类名::”作为限定来避免二义性的产生。
第二种环境:
当C类中新建的成员函数和数据成员的名称与基类的数据成员和成员函数名相同时,在系统调用时则不会产生二义性,因为在系统之中有条规则如许写道:**基类的同名成员在派生类中被屏蔽,成为不可见的,或者说派生类中新增加的同名成员覆盖了基类当中的同名成员。**因此假如在界说派生类对象的模块中通过对象名访问同名的成员,则访问的时派生类的成员。
注意:不同的成员函数只有在函数名和函数参数个数相同时,范例相匹配的环境下才发生同名覆盖,假如只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。
第三种环境:
假设两个B、C类继承于A类,D类又继承于B、C类,我们在用C类建立对象时,在派生类当中新建的成员会将基类当中的同名旧成员进行覆盖,通过D类对象对于A类的成员进行访问或者对于A类的成员函数进行调用时会出现二义性,因为B、C类继承于A类,系统不知道我们想调用的到底是B类继承的A类成员还是C类继承的A类成员,因此我们在调用的时候也要加上作用域限定符,例如:
D a;
a.A::n=3;
a.B::display();
由此可见我们对于作用于限定符的利用可以避免二义性的产生,但是还是不够精简以及比较贫苦。
虚基类
虚基类的作用
我们知道在界说D的对象时,假设A类当中有一个数据成员n,那么我们可以通过D类界说的对象t来对其进行访问。例如对于由B类继承而来的A类当中的n访问:t.B::n=3;对于由C类继承而来的A类当中的n访问:t.A::n=3;
由此我们可以得出,在不利用虚基类的环境下,我们对于D类的间接基类A中的数据是存了直接基类份的,也就是两份,在现实操纵工程当中,如许的操纵会使存储空间得以浪费且增加了访问这些成员的困难,在现实上我们不必要存这么多份,仅仅一份够。c++提供虚基类是的在继承间接共同基类时只保存一份成员,那么我们如何详细实现呢?
class A
{};
class B:virtual public A
{};
class C:virtual public B
{};
class D:public B,public C
{};
虚基类的界说并不是在基类的创建时,而是在声明派生类时,指定继承方式时声明的。声明虚基类的一般格式为:
class 派生类名:virtual 继承方式 基类名
经过如许的声明之后,当基类通过多条派生路径被某一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保存一次。
基类的初始化
#include <iostream>
using namespace std;
class Person
{
protected:
string name;
int age;
char sex;
public:
Person(string a,int b,char c):name(a),age(b),sex(c){}
};
class Student:virtual public Person
{
protected:
double score;
public:
Student(string a,int b,char c,double d):Person(a,b,c){score=d;}
};
class Teacher:virtual public Person
{
protected:
string title;
public:
Teacher(string a,int b,char c,string d):Person(a,b,c){title=d;}
};
class Graduate:public Student,public Teacher
{
private:
double wage;
public:
Graduate(string a,int b,char c,double d,string e,double f):Person(a,b,c),Student(a,b,c,d),Teacher(a,b,c,e){wage=f;}
void show()
{
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"score:"<<score<<endl;
cout<<"title:"<<title<<endl;
cout<<"wage:"<<wage<<endl;
}
};
int main()
{
Graduate A("朱小花",18,'m',99.9,"鸟不拉屎国教授",888);
A.show();
return 0;
}
我们在多重继承当中对于基类构造函数的继承与单一继承时不同,多重继承当中,在末了的派生类中不仅要负责对直接基类进行初始化,还要负责对虚基类进行初始化,例如上例,各人可能会疑惑在Graduate类的构造函数当中我们调用了虚基类Person的构造函数,也调用了Student和Teacher直接基类中的构造函数,是不是就意味着虚基类被构造了3次呢?nonono,c++编译系统只实行末了的派生类对虚基类的构造函数的调用,而忽略基类的其他派生类如Student和Teacher类对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化
有了虚基类的利用,对于Graduate的公共基类Person中的数据name,sex,age的值就不用再加::,直接的调用也不会再产生二义性
在程序员的不成文的规定当中:不提倡在程序中利用多重继承,只有在比较简朴和不易出现二义性的环境下或实在必要时才利用多重继承,假如能用单一继承解决的问题不要采用多重继承,也是这个缘故原由,有些面向对象的程序计划语言如Java并不支持多重继承
基类与派生类的转换
从之前介绍过的三种继承方式当中我们可以发现,只有公有继承可以大概较好地保存基类的特性,它保存了除了构造函数和析构函数以外的基类所有成员,基类的公有或掩护成员的访问权限在派生类中全部都按原样保存下来了,在派生类外可以调用基类的公有成员函数以访问基类的私有成员。
因此,公用派生类具有基类的全部功能,所有基类可以大概实现的功能,公用派生类都可以大概实现。而非共用派生类(protected、private)不能实现基类的全部功能(例如在派生类之外不能调用基类的公有成员函数以访问基类的私有成员)。
只有公有派生类才是基类真正的子范例,他完备的继承了基类的功能
在c语言的学习当中我们相识到,整型数据可以转换为双精度型数据,但是不能把一个整型数据赋值给一个指针变量。这种不同范例数据之间的自动转换和复制,称为赋值兼容
雷同的,基类对象可以雷同于整型数据,派生类对象可以雷同于双精度型数据,也可以实现赋值兼容,详细可以体现在以下四个方面:
1. 派生类对象对于基类对象赋值
我们知道将双精度型数据转换为整型数据时会丢失部分数据,同样,在利用派生类对象对基类对象赋值的时候也有雷同的结果,与double转int丢失小数点后面的数据不同,派生类对象对基类对象赋值之后会舍弃自己派生类新增的成员
我们假设A为基类B为派生类
A a;
B b;
a=b;
以上语句合乎语法,编译精确,为派生类对象对基类对象赋值,即“大材小用”
注意,在这里的赋值指的是仅仅对于基类中的数据成员赋值,而不包罗成员函数,我们假设A基类中有n,B派生类中有m,那么在a=b之后我们可以调用a.n,但是不可以调用b.m,因为a对象中被赋值的成员仅仅为B类当中从A类继承过来的成员,也就是此时对象a当中并无m
应当注意,子范例的关系是单向的、不可逆的。B是A的子范例,而不能说A是B的子范例。**只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。**理由就是基类对象中不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。
2. 派生类对象可以代替基类对象对基类对象的引用进行赋值或初始化
A a;
B b;
A &r=a;
此时r即为a的别名,r与a共用一段存储单位:
A &r=b;
可以用子类对象初始化r:
r=b;
3. 假如函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象
若num为基类A当中的值:
A a;
B b;
void display(A &a)
{cout<<num<<endl;}
利用display(b);也可以输出num的值。
4. 派生类对象的地点可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象
注意对于赋值来说是大材小用,对于指针的指向来说是小指大:
#include <iostream>
using namespace std;
class Student
{
private:
string name;
int age;
char sex;
public:
Student(string a,int b,char c):name(a),age(b),sex(c){}
void display()
{
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
}
};
class Graduate:public Student
{
private:
double wage;
public:
Graduate(string a,int b,char c,double d):Student(a,b,c),wage(d){}
void display()
{
Student::display();
cout<<"wage:"<<wage<<endl;
}
};
int main()
{
Student A("小芳",18,'f');
Graduate B("土狗",18,'m',888);
Student *p;
p=&A;
p->display();
p=&B;
p->display();
return 0;
}
那么根据输出格式我们可能会产生一些误解,误以为两次调用的都是Student中的display函数,实则不然,第一次我们的基类指针指向的是基类对象,调用的是Student中的display函数,但是第二次基类指针指向的是派生类当中的对象,其能访问的仅仅为基类中的成员而不包罗wage,因此第二次未输出wage且访问的是Graduate的display函数。
继承与组合
即在一个类的界说当中数据成员为其他类的对象,类的组合和继承一样,是软件重用的紧张方式,组合和继承都是有用地利用已有类的资源。但二者的概念和用法不同。通过继承建立了派生类与基类的关系,他是一种“是”的关系,派生类是类的详细化实现,是基类的一种。通过组合建立了成员类与组合类的关系,它们之间不是“是”的关系而是“有”的关系
继承是纵向的,组合是横向的
继承在软件开发中的意义
缩短软件开发过程的关键在于鼓励软件重用,c++的继承就是为了实现这一点,这也是和c语言的关键区别所在。在类库的利用过程中,我们要知道类库并不是c++编译系统的一部分,不同的c++编译系统提供的,由不同厂商开发的类库一般是不同的。因此在一个c++编译系统之中利用类库开发的程序在其他编译系统中不肯定可以大概完全精确运行,除非类库移植。在利用类库时,我们仅仅必要在头文件中声明即可。由于基类时单独编译的,程序在编译时只需对派生类新增的功能进行编译,这就大大的提高了调试程序的服从。假如在必要时修改基类,只要基类的公共接口不变,就不必对派生类进行修改,但基类要重新编译,派生类也要重新编译,否则不起作用。
那么我们为何这么看重c++的继承呢?
[*] 有很多基类是被程序的其他部分或其他程序利用的,这些程序要求保存原有的基类不受破坏。利用继承是建立新的数据范例,他继承了基类的所有特性,但不改变基类自己,基类的名称、构成和访问属性丝毫没有改变,不会影响其他程序的利用。
[*] 用户无法知道基类的源代码。掩护了基类的安全,防止基类被篡改。
[*] 在类库当中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此在类库中的积累是绝对不答应修改的。
[*] 现实上很多基类仅仅为一个框架,并无现实作用,目的只是为了建立通用的数据结构,以便用户在此基础上添加各种功能建立各种功能的派生类。
[*] 在面向对象的程序计划过程中,必要计划类的条理结构,也就是必要一层一层的计划类,从最初的抽象类出发,是不断的从抽象到详细的过程。每一层的派生和继承都必要站在整个系统的角度同一规划。
第十二章
多态性的概念
多态性是面向对象程序计划的一个紧张特性,假如一种语言只支持类而不支持多态,是不可以大概称为面向对象的语言的,只能说是基于对象的,如VB。c++支持多态而且可以大概实现多态性,利用多态性可以计划和实现一个易于扩展的系统。
在面向对象方法中一般是如许来描述多态的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的举动(即方法)。也就是说,每个对象可以用自己的方式去相应共同的消息。所谓消息就是调用函数,不同的举动就是不同的实现,即实行不同的函数。
其实我们之前已经打仗过了多态性,例如运算符的重载、函数的重载等。
在c++中,多态性的体现情势之一是:具有不同功能的函数可以用同一个函数名,如许就可以实现用一个函数名调用不同内容的函数。
多态分为两类:静态多态性和动态多态性:
静态多态性
静态多态性是根据函数重载实现的。有函数重载和运算符重载形成的多态性就是静态多态性,要求在程序编译时就知道调用函数的全部信息,因此,在程序编译时系统就能决定要调用的时哪儿个函数。**静态多态性又称为编译时的多态性。**静态多态性的函数调用速率快、服从高,但缺乏灵活性,在程序运行前就已决定了实行的函数和方法。
动态多态性
动态多态性的特点是不在编译时确定调用的是哪儿个函数,而是在程序运行过程中才动态地确定操纵所针对的对象。它又称运行时的多态性, 动态多态性是通过虚函数实现的。
重载函数与同名覆盖的区别
[*]重载函数在函数范例和参数个数两方面至少有一个不同
[*]同名覆盖则是在参数个数和函数范例两方面全部相同且一个处于基类一个处于派生类
利用虚函数实现动态多态性
虚函数的作用
在同一个类当中是不可以大概界说两个名字相同、 参数个数和范例都相同的函数的,否则就是重复界说。但是在类的继承条理结构中,在不同的条理中可以出现名字相同、参数个数和范例都相同而功能不同的函数,但是要通过虚函数来实现。
c++当中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数是虚拟的,并不是现实存在的函数,然后在派生类中才正式界说函数。在程序运行期间,用指针指向某一派生类中的对象,如许就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。
注意:虚函数的作用是在派生类中重新界说与基类同名的函数,而且可以通过基类指针来访问基类和派生类中的同名函数。
#include <iostream>
using namespace std;
class Student
{
protected:
string name;
int age;
char sex;
public:
Student(string a,int b,char c):name(a),age(b),sex(c){}
virtual void display()//声明为虚函数
{
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
}
};
class Graduate:public Student
{
private:
double wage;
public:
Graduate(string a,int b,char c,double d):Student(a,b,c),wage(d){}
void display()
{
cout<<"name:"<<name<<endl;
cout<<"age:"<<age<<endl;
cout<<"sex:"<<sex<<endl;
cout<<"wage:"<<wage<<endl;
}
};
int main()
{
Student A("小芳",18,'f');
Graduate B("土狗",18,'m',888);
Student *p;
p=&A;
p->display();
p=&B;
p->display();
return 0;
}
例如对于上一章中继承的一个例子进行小小的修改(修改部分为解释那行),我们可以发现指针p指向的display函数可以大概成功的输出Graudate类对象的数据而不是与之前那样仅仅输出姓名、年事以及性别。缘故原由在于多态性的实现:在原有的未加virtual的程序当中,本来我们界说的基类指针p是指向基类对象的,假如用它来指向派生类对象,则会自动进行指针范例转换,将基类指针转换为派生类指针,如许基类指针指向的就是派生类对象中的基类部分,也仅仅能对于派生类中从基类继承来的数据进行输出。在程序修改之前是无法通过基类指针去调用派生类对象中的成员函数的。但是虚函数打破了这一限定,在基类中的display被声明为虚函数后,对于派生类中的display函数进行重界说,这时派生类的同名函数就取代了基类中的虚函数。因此在基类指针指向派生类对象后,调用display函数就是调用的派生类的display函数。要注意的是,只有用virtual声明了函数为虚函数后才具有以上作用。
虚函数的以上功能具有十分强大的现实意义,在面向对象的程序计划当中,常常会用到基类的继承,目的是保存基类的特性,以淘汰基类的开发时间。但是,从基类继承来的某些成员函数不完全顺应派生类的必要,且假如派生的条理过多还要起不同的名字,若名字相同又会出现同名覆盖的现象
虚函数很好的解决了这个问题。可以看到:当把基类当中某个成员函数声明为虚函数后,答应起派生类中对该函数重新界说赋予它新的功能,而且可以通过指向基类的指针指向同一类族中不同的类的对象,从而调用其中的同名函数。
注意:由虚函数实现的动态多态性就是:同一类族中不同类的对象对同一函数调用作出不同的相应
虚函数的利用方法
[*] 在基类当中用virtual来声明成员函数为虚函数。在类外界说虚函数时,不必再加virtual
[*] 在派生类中重新界说此函数,函数名、函数范例、函数参数个数和范例必须与基类的虚函数相同,根据派生类的必要重新界说函数体。**当一个函数被声明为虚函数时,其派生类当中的同名函数都自动改为虚函数。**因此在派生类重新声明该函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清楚
[*] 界说一个指向基类对象的指针变量,并使它指向同一类族中必要调用该函数的对象
[*] 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数
通过虚函数与指向基类对象的指针变量的配合利用,就能实现动态的多态性。假如想调用同一类族中不同类的同名函数,只要先用基类指针指向该类对象即可。假如指针先后指向同一类族当中不同类的对象,就能不断地调用这些对象中的同名函数
必要说明,有时在基类中界说的非虚函数会在派生类中被重新界说,假如用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数,假如用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性举动,没有用到虚函数的功能
我们可以发现,函数重载处理的是同一条理上同名函数的问题,虚函数处理的是不同派生条理上的同名函数问题,前者是横向重载,后者是纵向重载,但虚函数与函数重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或参数范例不同)
静态关联与动态关联
在编译系统当中,编译系统要根据已有的信息对于同名函数的调用作出判定。比如函数重载就是根据函数的参数或参数的范例不同来判定究竟调用哪儿个函数。对于调用同一类族当中的虚函数,我们应带在调用的时候用肯定的方式告诉编译系统要调用的是哪儿一个类当中的函数。
确定调用的详细对象的过程称为关联,一般来说关联指把一个标识符和一个存储地点联系起来。前面提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪儿一个类,其过程称为静态关联,由于是在运行前进行关联的,因此又称为早期关联,函数的重载属于静态关联。
从上面那块代码程序我们可以看出,通过指针p指向的display函数属于对虚函数的调用,且我们在调用的时候并没有为其指定对象名,且在编译过程中通过指针p对于display函数的调用是合乎语法符合编译的,但是我们发现在编译阶段无法确定调用的是哪儿个display函数。在这种环境下,编译系统把它放在运行阶段处理,在运行阶段,基类指针变量先指向某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪儿一个对象无疑是确定的。由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此,此过程称为动态关联。这种多态性是动态的多态性,即运行阶段的多态性
由于在运行阶段,指针可以先后指向不同的类对象,从而在调用同一类族中的不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联。
在什么环境下应该声明虚函数
利用虚函数时要注意两点:
[*] 只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的平凡函数声明为虚函数,虚函数仅仅可以大概用于类的继承条理结构当中,因为虚函数的作用是答应在派生类中对基类虚函数重新界说的
[*] 一个成员函数被声明为虚函数后,在同一类族当中的类就不能在界说一个非虚且与该虚函数具有相同参数(个数和范例)和函数返回值相同的同名函数
利用虚函数的环境:
[*] 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,假如希望更改功能,一般将它声明为虚函数
[*] 假如成员函数在类被继承后功能无需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅思量到其作为基类而把基类所有成员函数都声明为虚函数
[*] 应思量对成员函数的调用是通过基类指针还是引用去访问,假如通过基类指针或引用去访问则应当声明为虚函数
[*] 有时在界说虚函数时,其为空,后面的功能仅仅留给派生类中去重新界说实现添加
注意:
利用虚函数系统要有肯定的空间开销。当一个类带有虚函数时,编译一同会为该类构造一个虚函数表,他是一个指针数组,存放每个虚函数的入口地点。系统在进举措态关联的时间开销是很少的,因此多态性是十分高效的
虚析构函数
虚析构函数的利用也是多态性的一种体现,前面我们相识到派生类可以对于基类的构造函数进行继承,因此在派生类对象的建立过程中首先调用基类的构造函数,而后才调用派生类的构造函数,在此对象被清算的时候先调用派生类的析构函数再调用基类的析构函数
然而假如我们new一个派生类的对象,在对其进行delete时我们会发现其仅仅实行了基类的析构函数,对于动态建立的派生类的析构函数并无丝毫的调用,这就轻易造成内存泄漏
解决这个环境的方式就是对于基类的析构函数进行虚函数处理,与平凡的虚函数不同,在基类进行析构函数的虚函数处理之后,其派生类中的虚构函数无论名字相不相同则全部转化为虚函数
专业人员一般都习惯声明虚析构函数,即使基类并不必要界说虚析构函数,也显式的界说一个函数体为空的虚析构函数,以保证再撤销动态分配空间时可以大概得到精确的处理。
构造函数不能声明为虚函数。这是因为在实行构造函数当中类对象还未完成建立的过程,当然谈不上把函数与类对象绑定:
#include <iostream>
using namespace std;
class Student
{
protected:
string name;
int age;
char sex;
public:
Student(){}
virtual ~Student(){cout<<"完成基类的清理"<<endl;}
};
class Graduate:public Student
{
private:
double wage;
public:
Graduate(){}
~Graduate(){cout<<"完成派生类的清理"<<endl;}
};
int main()
{
Student *p=new Graduate;
delete p;
return 0;
}
纯虚函数与抽象类
纯虚函数
有的环境下,虚函数为空且仅仅为了派生类当中可以大概对其功能重新界说,并无现实意义,我们把如许的函数叫做纯虚函数,有个简便的写法:virtual 函数范例 函数名(参数列表)=0;
注意:
[*] 纯虚函数没有函数体
[*] 末了的=0并不代表函数返回值为0他只是起一个情势上的作用告诉编译系统这时纯虚函数
[*] 这是一个声明函数,末端加分号
纯虚函数仅仅是一个函数名字不具备函数的功能,不能被调用,只是个空壳而已。它只是通知编译系统:“在这界说了一个虚函数留给后面派生类中界说”。纯虚函数的作用是在基类中为其派生类保存一个函数的名字,以便派生类根据必要对它进行界说。假如在基类中没有保存函数名字,则无法实现多态性。假如在一个类中声明了纯虚函数而在派生类当中没有对该函数进行界说,则该虚函数在派生类中仍然为纯虚函数。
抽象类
有了空壳函数作为基础来供派生类中的函数进行重界说,自然也有雷同于空壳类的存在。也就是抽象类。假如声明了一个类,一般可以用它界说对象。但是在面向对象的程序计划过程中往往有一些类它们不用来生成对象。界说这些类的唯一目的是用它作为基类去建立派生类。它们作为一种基本范例提供给用户,用户在这个基础上根据自己的必要界说出功能各异的派生类,用这些派生类去建立对象。
这种不用来界说对象而只作为一种基本范例用作继承的类,称为抽象类,由于它常用作基类,通常称为抽象基类。饭时包含纯虚函数的类都是抽象类。因为纯虚函数是无法调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。
假如在抽象类所派生出来的新类当中对于基类的所有纯虚函数进行了界说,那么这些函数就被赋予了新的功能,可以被调用。这个派生类就不是抽象类,而是可以用来界说对象的详细类,假如在派生类当中没有对于所有纯虚函数进行界说,此派生类仍然是抽象类,不能用来建立对象。
固然抽象类不可以用来建立对象,但是可以界说指向抽象类数据的指针变量,当派生类成为详细类之后。就可以用这种指针指向派生类对象,然后通过该指针调用虚函数实现多态性的操纵。
小结
[*] 一个基类假如包含一个或一个以上的纯虚函数时就是抽象基类,抽象基类是不能也不必要界说对象
[*] 抽象基类于平凡基类不同,它一般不是现实存在的对象的抽象,它可以是没有任何物理上的或者其他现实意义方面的寄义
[*] 在类的条理结构当中,顶层或者最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中公有成员函数会合在抽象基类中声明。
[*] 抽象基类是本类族的公共接口,或者说从同一基类派生出的多个类由同一接口,因此能响应同一情势的消息,但是相应的方式因对象不同而异。在通过虚函数实现动态多态性时,可以不必思量对象是哪儿一个类的,都用同一种方式调用
[*] 假如可以大概通过对象名在编译阶段确定调用的是哪儿个类的虚函数这是静态关联,假如是通过基类的指针p调用虚函数,在编译阶段无法确定调用哪儿一个类的虚函数,只有在运行时p指针指向某一类对象时才确定,那么此为动态关联
[*] 假如在基类中声明了虚函数,那么在派生类中凡是与该函数有相同的函数名、函数范例、参数个数和参数范例的函数均为虚函数。同一虚函数在不同的类当中可以有着不同的界说。纯虚函数是在抽象基类中声明的,只是在抽象基类中才称为纯虚函数,在其派生类中固然继承了该函数,但除非再次用=0把它声明为纯虚函数,否则不能称为纯虚函数
[*] 利用虚函数提高了程序的可扩充性
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]