谭书:谭浩强版《C++程序设计(第3版)》
Cpp:《C++ Primer Plus(第6版)》
EC:《Effective C++(第3版)》
一、类
类的声明【谭书P245 P289】
(1)类是一种抽象的数据类型,并不占用存储空间,因此不能在类的声明中对成员变量进行初始化。
(2)对一个类进行“提前引用声明”后,可以用该类的名字来定义指向该类的对象的指针变量、对象的引用。虽然这时系统还没见到这个类的声明的具体内容,但指针变量和引用与其所指向的类的对象的大小无关,因此是合法的。
二、对象
对象的初始化【谭书P248 Cpp295】
可以用一个类对象初始化同一个类的另一个对象
Time t1; //隐式调用构造函数Time()来创建t1
Time t2 = Time(); //显式调用构造函数来创建t2
Time t3 = t1; //用t1复制出t3来
Time t4 = {1,2}; //搜索参数列表能与{}中内容匹配的构造函数并调用它,即Time(int,int)
Time t5 = 3; //如果Time类有只需要给一个参数(包括只有一个参数,或除了一个参数外都有默认值)、且参数类型与等号右侧量相同的构造函数Time(int),则将调用该构造函数(但这么做会导致一些问题)
对象数组【谭书P259 Cpp300】
Time t[3]; //调用默认构造函数(无参数,或全都有默认值)
Time t[3]={1,2,3}; //调用只需要给1个参数的构造函数
Time t[3]={ //调用不同的构造函数,t[2]调用默认构造函数
Time(1);
Time();
};
(1)对象数组中每个对象的创建,都是先用默认构造函数创建对象,然后调用花括号中指定的构造函数创建临时对象,再用临时对象给数组中对应对象赋值
因此创建对象数组,必须有默认构造函数
常对象、常成员【谭书P267 P270 CppP298 P378】
定义常对象:
类名 const 对象名(实参表)
const 类名 对象名(实参表)
(1)定义常对象时,必须同时对其初始化,之后不能再改变其成员变量的值
(2)常对象只能调用常成员函数
(3)类还是普通的类,只是在定义对象时可以选择普通对象、常对象。因此,常对象中还是会包含普通数据成员、普通成员函数
(4)定义了常对象后,该对象的数据成员都变成了常数据成员,其值不能被修改
————————————————————————————————————————————————————————————————————————————————————————————
声明常成员函数:
void fun() const;
(1)常成员函数中不能调用非const成员函数
(2)常成员函数中能调用普通数据成员、常数据成员
(3)声明、定义常成员函数都要加const,调用时不加const
(4)常函数将this指针限定为const,因此不能使用this来修改对象
————————————————————————————————————————————————————————————————————————————————————————————
定义常数据成员
const int a;
(1)常数据成员只能用参数初始化表进行初始化,因为调用构造函数时对象将在{}括号中的代码执行之前被创建,因此在{}中对a赋值是非法的
在参数初始化表中对a的操作属于创建并初始化常量a,而非赋值(数据成员是引用时,也需要在参数初始化列表中进行初始化)
(2)可以将数据成员声明为mutable int a; 可变的数据成员,则可以用常成员函数来修改它的值
————————————————————————————————————————————————————————————————————————————————————————————
定义指向常对象的指针变量
const 类型名 *指针变量名;
(1)只能用const指针指向const对象
(2)指向const对象的指针一般用于函数的形参
静态数据成员【CppP349】
(1)声明:类声明内部声明static数据成员:static int a;
初始化:包含类方法的文件中初始化:int A::a = 0;
(2)如果静态成员是const整数类型或者枚举类型,则可以在类声明中初始化
对象的赋值和复制【谭书P275 P279 CppP293 CppP350 CppP353】
对象的赋值:调用赋值运算符(一般是自动提供),不是在初始化过程中
【1】对象1 = 对象2;
【2】对象1 = 构造函数(参数列表); //已有对象1时,显式调用构造函数(有无参数的都可以),创建一个临时对象,并给对象1赋值
(1)赋值是对一个已经存在的对象赋值。必须先定义被赋值的对象,才能进行赋值
——————————————————————————————————————————————————————————————————————————————————————————————————————————————
对象的复制:在对象2初始化过程中,由对象2调用以对象1为实参的复制构造函数:对象2.类名(对象1);
【1】类名 对象2(对象1);
【2】类名 对象2 = 对象1;
【3】类名 对象2 = 复制构造函数(对象1);
【4】类名 对象2 = 构造函数(参数列表); //无对象2时,显式调用构造函数(有无参数的都可以)创建一个临时对象,用临时对象复制一个对象2出来
【5】fun(对象2); //对象2作为实参传递给fun()函数的形参,利用复制构造函数复制得到形参对象
【6】类名 *p = 类名(对象1);
(1)复制是新建一个对象,并将其初始化为现有同类对象。复制会调用复制构造函数
(2)【2】【3】都可能因为编译器不同而有两种解释:
1.用复制构造函数直接创建出对象2
2.使用复制构造函数生成一个临时对象,再赋值给对象2
【4】作为调用普通的构造函数,也会有两种解释,见“构造函数和析构函数的调用事项”(7)
(3)3种复制的情况:
1.程序中需要新建立一个对象,并用另一个同类的对象对其进行初始化
2.函数的形参是类的对象。在调用时建立一个实参的拷贝,系统会调用复制构造函数
3.类的对象作为函数的返回值。例如:
Box f(){
Box box1(12,15);
return box1;}
int main(){
Box box2;
box2 = f();
return 0;}
执行的过程: box1——(复制)——>临时对象——(赋值)——>box2
其中,box1在return执行时,先复制得到一个临时对象,再结束f()的调用。
然后,临时对象将数据成员的值赋值给box2。
——————————————————————————————————————————————————————————————————————————————————————————————————————————————
(1)赋值、复制时,若数据成员中包括指向动态分配(new)的地址的指针变量,可能会出现严重后果
因为指针变量所存的地址也会被赋值、复制,使得一个对象析构释放(delete)该地址,另一个对象的指针变量指向的地址同样被释放
此时需要定义显式的复制构造函数和赋值运算符,以复制指向的数据,而非指针,这被称为深复制
继承中、对象成员的复制、赋值、析构【CppP422】
A->B,C对象c1是B类的数据成员
此处“默认”指的是:由编译器implicit声明的函数,而非程序员explicit写好的
【析构函数】子类B析构函数 自动调用 父类A、对象成员c1的析构函数
【复制构造函数】
默认复制构造函数:自动调用父类A、对象成员c1的复制构造函数(A、C类若提供了非默认复制构造函数,则编译器不再提供默认版本,自动调用非默认版本)
非默认复制构造函数:不会自动调用A、C的复制构造函数。需要通过初始化列表显式调用父类A、对象成员c1复制构造函数
B::B(B& b1):A(b1),c1(b1.c1){…} //用子类对象b1的继承部分来复制得到新的子类对象的继承部分
(1)因为复制是创建一个新的对象,复制构造函数也是一种构造函数,因此和构造函数的规则一样,
如果B的非默认复制构造函数不显式调用A、C类任何复制构造函数,则调用A、C类的默认构造函数来完成对应的初始化工作
此时只复制了子类B新增的成员,而从A类继承的成员由A的默认构造函数初始化,c1由C的默认构造函数初始化
【赋值运算符】
默认赋值运算符:自动调用父类A、对象成员c1的赋值运算符(A、C类若提供了非默认赋值运算符,则编译器不再提供默认版本,自动调用非默认版本)
非默认赋值运算符:不会自动调用。需要显式调用父类A、对象成员c1赋值运算符
B& B::operator=(B& b1){
A::operator=(b1); //通过作用域解析运算符显式调用。用子类对象b1的继承部分来赋值得到新的子类对象的继承部分
c1 = b1.c1; //调用C类的赋值运算符 也可以写成c1.operator=(b1.c1);
…
}
(1)若B类的非默认赋值运算符不显示调用A、c1的赋值运算符,则对应的成员保持不变,不进行赋值操作
——————————————————————————————————————————————————————————————————————————————————————————————————————————————
(1)结合构造、析构、复制、赋值中使用new、delete的知识可知:
如果父类、对象成员使用了new、delete,子类没有使用new、delete:子类可以使用默认的复制、赋值,它们会自动调用父类、对象成员对应函数的非默认版本
如果父类、对象成员使用了new、delete,子类也使用了new、delete:子类必须使用相应的复制、赋值,并且显式地调用父类、对象成员对应函数的非默认版本
对于析构函数,子类的析构函数都会自动调用父类、对象成员的析构函数
三、构造函数与析构函数
调用时间:构造函数、析构函数【谭书P246 P248 P254 P258】
(1)构造函数的调用是在建立对象时自动调用的,这时调用的是无参数的构造函数。也可以用A a(1,2);或A a=A(1,2);来调用带参数的构造函数。
(2)全局对象的构造函数在本文件所有函数(包括main)之前调用。若有多个文件中都有全局对象,则这些对象的构造函数的调用顺序不确定。
(3)全局对象和局部静态对象的析构函数在main函数结束或调用exit函数结束程序时被调用
(4)由于局部对象属于自动变量,被放在栈中,后进先出,因此先创建的对象后被析构
构造函数和析构函数的调用事项【谭书P246 P248 P254 P258 CppP290 P378 CppP373】
(1)全部参数都带默认值的构造函数和无形参的构造函数、某些重载构造函数在调用时会发生歧异
(2)建立对象时分配存储空间,然后执行构造函数,把指定的初值存放到存储空间中
(3)构造函数不需也不能被用户调用
(4)构造函数只能执行一次
(5)在类的声明中声明构造函数并带有默认值时,可以省略形参名:
Box(int =10,float =1.5);
(6)一旦定义了非默认的构造函数,编译器不再提供空的默认构造函数。若无全部给定默认参数的构造函数时,不能再使用A a;来定义对象
(7)A a=A(1,2); 显式地调用构造函数来定义对象时,不同的编译器会给出两种解释:
1、和隐式调用A a(1,2);一样,用构造函数直接创建一个对象出来
2、由A(1,2)创建一个临时对象,用临时对象复制一个对象a出来,然后再丢弃临时对象。此时会调用临时对象的析构函数。
(8)调用构造函数时,对象将在{}括号中的代码执行之前被创建,各种数据成员,包括子对象也会被用默认构造函数创建
(9)析构函数一般不显式调用。只当使用定位new运算符时,显式地为使用定位new运算符创建的对象调用析构函数:
p->~A();
几种构造函数【谭书P320】
默认构造函数
Complex();
Complex(double r=0.0,double i=0.0); //所有参数都带默认值
带参数的构造函数
Complex(double r,double i);
复制构造函数
Complex(Complex & c);
转换构造函数
Complex(double r);
(1)转换构造函数只能有一个参数
(2)不仅可以将标准类型转换成该类的对象,还能将其他类的对象转换成该类的对象。转换的效果取决于函数内部怎么写
初始化列表【CppP379 P393】
class A{
private:
const int a; //常数据成员
B &b; //B类对象的引用
public:
A(B b1,int data):b(b1),a(data){…}
}
(1)调用构造函数时,对象将在{}括号之前被创建,因此在{}中对常数据成员a、引用数据成员b赋值是非法的
在参数初始化表中对a、b的操作属于创建并初始化,而非赋值
(2)只能用于构造函数
(3)非静态常const数据成员、引用数据成员都必须通过 初始化列表 或 类内初始化 来初始化
(4)数据成员的初始化顺序与初始化列表里的排列顺序无关,与类中声明的顺序有关(如上述程序中,先初始化a,后初始化b)
(5)初始化列表的这种()方式也可用于常规的初始化,比如:
int a(1);
B &b(b1);
(6)C++11新增的类内初始化,和初始化列表等价,但在初始化列表可以对类内初始化的结果进行覆盖
class A{
private: int a=0;
public: A(int data):a(data){…}
};
(7)因为子对象在构造函数的{}之前就会利用子对象的默认构造函数创建完成,因此下列代码不会调用B类的复制构造函数,而是调用赋值运算符:
A::A(B b){
b1 = b; //调用赋值运算符
}
而如果利用初始化列表,则会调用B类的复制构造函数复制得到b1:
A::A(B b):b1(b){}
四、数据成员与成员函数
私有成员(private)【谭书P224】
能通过对象名、this指针、引用直接用“.”调用私有成员的地方,只能是类内和友元函数、友元类内部
静态数据成员和静态成员函数【谭书P280 CppP360 P478】
静态数据成员:
static int a;
(1)静态数据成员只能在类体外初始化(此时不用加static)。不能用参数初始化表对静态数据成员初始化
数据类型 类名::静态数据成员名 = 初值;
(2)静态数据成员可以用对象名、类名引用
(3)静态数据成员若是private类型,则不能在类外直接引用,而得用public成员函数引用
(4)类模板的静态数据成员属于类模板每种实例各自所有,即A<int>和A<double>的静态数据成员k相互独立,但是初始化的值相同
类外初始化: template<typename T>
int A<T>::k=1; //将模板类A的所有实例的k都初始化为1
静态成员函数:
类内声明:static float average();
类外定义:float average(); //此时不能加static
(1)静态成员函数没有this指针,不能访问本类的非静态数据成员
(2)不能通过对象调用静态成员函数,只能使用类名和作用域解析运算符::来调用它
特殊成员函数【CppP352】
以下成员函数会在一定情况下自动提供:
(1)默认构造函数:如果没有定义任何构造函数
(2)默认析构函数:如果没有定义
(3)复制构造函数:如果没有定义,但有试图调用的语句
(4)赋值运算符:如果没有定义,但有试图调用的语句
(5)地址运算符:如果没有定义,但有试图调用的语句 (返回调用对象的地址,即this指针)
五、重载
运算符重载【谭书P300 P315】
作为类的成员函数:c1+c2解释为:c1.operator+(c2),即由对象c1调用成员函数“operator+”,实参是对象c2(c1的this指针作为隐含的参数)
作为类的友元函数:c1+c2解释为:operator+(c1,c2),即将对象c1 c2作为实参传递给友元函数“operator+”
(1)作为类的成员函数时,左侧的c1必须是该类的对象(c1和函数的返回值都得是这个类的对象)。
c1也可以替换为无名对象,例如:Complex(3,0)+c2,解释为:Complex(3,0).operator (c2),由无名对象Complex(3,0)调用函数“operator+”
(2)作为类的友元函数时,若计算的是c1+1,则要实现交换律,需要重载两种“+”,两个形参类型分别为Complex类的对象和int型
(3)重载的运算符,必须和用户字定义的数据类型一起使用,运算符函数的返回类型也得是该种数据类型。
如果全是C++的标准类型,则会导致和原有的运算符发生歧义,同时也是防止用户修改用于标准类型的运算符的性质。
(4)“++”重载为后加1时,Time Time::operator ++(int),在声明和定义处,都加一个int形参,但不使用,也没有名字。
(5)istream & operator >>(istream &,自定义类 &)
ostream & operator <<(ostream &,自定义类 &)
返回的是istream类和ostream类的对象的引用。此时返回的不是常量,而是引用所代表的对象,它可以出现在赋值号的左侧而成为左值,可以被赋值或参与其他操作。
因此,可以连续输出:cout<<c1<<c2;解释为(cout<<c1)<<c2,其中,(cout<<c1)是ostream类的对象的引用(其实就是这个cout对象的引用)
(6)赋值运算符“=”和地址运算符“&”不用重载
(7)有转换构造函数、友元的运算符“+”重载函数时,满足交换律。
如果运算符“+”重载函数是成员函数时,不满足交换律。
(8)运算符“+”重载为成员函数,第1个操作数不是对象(比如double)时,不会自动调用转换构造函数。此时只能再重载一个“+”函数,令其第1个参数为double型,再声明为友元函数(总之,双目还是作为友元函数比较好)
(9)双目运算符 重载为 友元函数
单目运算符 重载为 成员函数
数据类型转换【谭书P320 P322 CppP336】
其他类型——>对象:转换构造函数
显式调用:c1 = Complex(1.0); //将double型常量转化为Complex类的对象
隐式调用:c1 = 1.0; //自动调用可以只接受一个参数(其他的给默认值)的构造函数,创建一个临时对象给c1赋值。
对象——>其他类型:类型转换函数
operator double(){……}
显式调用:double a=double(c1);//用在标准类型的两种强制转换格式在这里也是可以的
double a=(double)c1;
隐式调用:double a=c1;
(1)函数名是“operator double”
(2)没有参数,没有函数类型。返回值类型由函数名确定
(3)只能作为成员函数,转换的主体是本类的对象
(4)程序中不必显式地调用转换构造函数、类型转换函数,在需要的时候,编译系统会自动调用它们,即隐式调用,建立一个临时对象/临时变量
d1 = d2 + c1;
有类型转换函数,无“+”重载函数:用的是普通的+,左右只能是标准C++数据类型,需要把非标准类型转换为标准类型,再进行计算
自动调用double的重载函数,将对象c1转化为double型临时变量
c2 = c1 + d2;
有转换构造函数,有“+”重载函数:作为友元函数重载的+允许两个Complex对象相加,需要把非Complex对象转换为Complex对象,再进行计算
此时d2作为operator+(Complex &c1,Complex &c2)函数的第二个参数传递进去时,
会隐式调用转换构造函数将d2转化为Complex临时对象,再调用重载后的+,将两个Complex对象相加后赋给c2
(5)关键字explicit可以关闭转换构造函数 、类型转换函数的自动调用。explicit只能加在成员函数声明前面,或成员函数定义在类声明中时
explicit Complex(int i){…}
explicit operator double(){…}
六、继承与派生
父类、对象数据成员的构造函数的调用【谭书P350 P356】
A—public—>B
C类对象cc是B类的一个数据成员
类内声明:B(int a,int b,C c);
类外定义:B(int a,int b,C c):A(a),cc(c),b1(b){……}
(1)声明不需要加A、C的构造函数,因为那是在初始化列表里调用的,相当于要执行的语句,
而这里只是声明B的构造函数,并不承担执行任务
(2)子类B需要在其构造函数中调用父类A的构造函数,也要调用C类的构造函数来对数据成员cc初始化。
(3)调用的顺序为:A构造函数,C构造函数,B构造函数。
A、C的构造函数写的先后顺序可以调换,相应的调用顺序不会改变
(4)如果A还有父类X,则在调用A的构造函数时,会先调用X的构造函数,然后再执行A的构造函数,最后再调用B
(5)父类A无带参数的构造函数,或父类有无参数的构造函数A()时,若B的构造函数中不写A的构造函数,
则会自动调用A的默认构造函数。对于C的构造函数也是如此。
(6)如果A、B、C都用的是默认构造函数,那么定义B类的对象时不需要给任何参数,B类构造函数也不需要写A、C的构造函数。
但如果只有B用的是默认构造函数,那么B类必须定义带A、C类构造函数的构造函数
(总之,B类构造函数的初始化列表决定了对A、C类构造函数的调用情况)
(7)系统会自动调用A、B、C类的析构函数,其顺序与构造函数执行顺序相反,先执行B,再C,最后A
重载与覆盖【谭书P361 CppP413 P461】
成员函数:
重载:函数名相同,参数个数、类型、不同类型的顺序 至少有一项不同
覆盖:函数名,参数个数、类型、不同类型的顺序 均完全相同
重写:子类重写父类的虚函数
(1)作为成员函数,重载只发生在同一个类内部
如果父类和子类有同名的成员函数,则无论参数列表是否相同,都属于覆盖,不会发生父子之间跨类的重载
因此,要重新定义继承而来的函数,需要参数列表完全相同
(2)返回类型协变:子类重新定义继承而来的函数时,如果函数原本返回的是指向基类对象的指针或引用,则可以改为指向子类的指针或引用
(3)如果基类中有多个重载函数:
int fun();
int fun(int);
int fun(double);
如果只重新定义其中某一个,则该函数会覆盖父类的三个函数,导致子类中不再有三个重载函数存在
如果只需要修改其中一个fun(int),则可以在子类中使用:using Base::fun;把父类的3个fun重载版本都加入到子类中来,
但子类定义的fun(int)还是会覆盖父类的fun(int)。(C++11新增内容)
——————————————————————————————————————————————————————————————————————————————————
数据成员:
覆盖:变量名、类型均相同
(1)覆盖时,父类的成员被“隐藏”,在子类中调用,或者子类外用子类的对象调用这个成员时,调用的是子类的成员
(2)多重继承中的同名优先级:子类中的名称优先于直接或间接祖先类中的相同名称
若有A->B->C D->C
1.A、B中均有名称fun,因为A是B的父类,则B的fun覆盖A的fun。C中只写fun则调用B的fun,调用A的fun必须写A::fun
2.A、D中均有名称gun,因为A、D互不为对方的父类,则A、D的gun不会相互覆盖,使用时必须用A::gun,D::gun
虚基类【谭书P364】
class B:virtual public A{……}
钻石继承问题:
A—virtual—>B——>D
A—virtual—>C——>D
构造函数:
A(int a):a1(a){}
B(int a,int b):A(a),b1(c){}
C(int a,int c):A(a),c1(c){}
D(int a,int b,int c):A(a),B(b),C(c){……}
——————————————————————————————————————————————————————————————————————————————————
(1)D在继承间接共同父类A时,只继承A一次,只保留一份A的成员
(2)为了保证D只继承A一次,应当令D的所有从A继承而来的父类B、C都virtual继承A
(3)D的构造函数必须对间接继承的A进行初始化,
系统只执行最后的子类D对虚基类A的构造函数调用,而忽略B、C对A构造函数的调用
这就保证了A只被初始化一次,避免了多条路径初始化导致对A进行不同的初始化产生的矛盾
(4)如果A—virtual—>B——>D,只有这么一条线继承下来时,D也必须对A主动初始化,否则会报错
(5)当然,在定义B、C的对象时,还是会调用A的构造函数
(6)A既可以被virtual继承,也可以被普通继承
基类与派生类的转换【谭书P368】
A——>B
(1)子类对象 给 父类对象 赋值、复制(大材小用)
a1 = b1;
A a1 = b1;
(2)子类对象 给 父类对象的指针、引用 赋值/初始化
A* p = &b1; //指针指向都是B中父类A的那部分
A& a1 = b1; //此时a1只是 B中从A继承过来的那部分 的别名 //a1与b1有相同的起始地址
(3)实参是子类对象,形参是父类对象的引用、指针
void fun1(A& a1);
void fun2(A* a2);
fun1(b1);
fun2(&b1);
重新定义父类方法的访问权限【CppP448】
B任意方式继承A时,B想将A的public、protected方法fun()作为自己的public方法
(1)定义一个使用A类方法fun()的B类同名方法(覆盖),在其中调用A类方法fun():
int B::fun(){ return A::fun(); }
(2)在B类的public中,使用using声明为public:
public:
using A::fun; //只有函数名,没有返回值、参数列表。因此若父类有两个同名函数,则都将被using声明作用
(3)在B类的public中,重新声明A类方法fun()(就像是(2)省去了using,但这种方法即将停止使用):
public:
A::fun;
is-a与has-a【CppP400 P430 P436 P438 P443】
【is-a】
(1)public继承
(2)获得实现,获得接口
(3)因此,由public继承表示的is-a关系,代表子类B是父类A的一种特殊情况。
从类外的角度来看,所有父类A能实现的,子类B都能实现,而子类B能实现A所不能实现的一些新功能(接口)
【has-a】
(1)子对象(包含)、private继承、protected继承
(2)获得实现,不获得接口
(3)子对象为新类提供了显式命名的对象成员。private继承、protected继承则提供了无名称的对象成员
(4)因此,has-a关系,代表子对象、基类作为新类的组成部分,分别为其提供某些实现
但从类外的角度来看,has-a关系并不增加新类的接口功能,但使得新类本身的功能更加完善
——————————————————————————————————————————————————————————————————————————————————
(1)接口:类的公有函数,可被对象调用
(2)继承接口:指原有接口函数是否作为新类的接口,可被新类的对象调用
(3)获得实现:新类的成员函数可以使用父类、子对象的接口来访问和修改父类部分、子对象
子对象、private继承【CppP445】
(1)子对象是被显式命名的对象成员
private继承、protected继承则提供了无名称的对象成员
(2)子对象使用对象名来调用类方法
private继承、protected继承使用类名和作用域解析运算符来调用类方法
(3)private继承、protected继承访问父类对象:
(A) *this; //其中*this表示当前对象,对其强制转换为父类对象
cout<<(const A &)b1; //将子类对象b1强制转换为父类A的一个const引用,由此调用父类A的友元函数 operator<<
七、多态
父类指针、引用指向子类对象【CppP445 P449 P445】
public继承:可以在子类类外、类内,用父类指针、引用指向子类对象
protected继承:只能在子类类内,用父类指针、引用指向子类对象
private继承:任何地方都不能直接指向,必须对子类对象显式强制类型转换:A* p = (A *)&b1; A& q = (A &)b2;
虚函数【谭书P388 CppP411】
类内声明:virtual void fun();
类外定义:void fun(){……} //类外定义不用加virtual
A——>B A、B中都有fun()函数
(1)若A类中fun()函数不是虚函数,则定义A* p=b1,p->fun(),
虽然p指向B类对象b1的地址,但实际调用的是A类的fun()函数,因为p指向的是b1中A类的那部分
(2)若A类中fun()函数是虚函数,分别令A* p=a1和A* p=b1时,执行p->fun(),分别调用的是A类和B类的fun()函数
如此,实现了同样执行p->fun()语句,实现的效果不同的目的。
(3)若A有更多的子类,其中有覆盖的fun()函数,也可以通过p指向不同子类的对象来实现对各个子类fun()函数的调用
(4)若A类中fun()函数是虚函数,(2)中改成A& p=b1,则p.fun()调用的也是B类的fun()函数
(5)虚函数使得子类在覆盖父类的函数时,让新的fun()函数取代了父类中的fun()函数
(6)当在父类中声明一个函数fun()为虚函数后,其子类中用来覆盖的fun()函数都自动成为虚函数,
因此在子类中声明fun()时virtual可加可不加,但为了程序更清晰,建议加上
(7)若在子类中没有对父类的虚函数重新定义,则子类简单地继承其父类的虚函数
(8)虚函数在时间上时高效的。在空间上有一定的开销。
当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,这是一个指针数组,存放每个虚函数的入口地址。
静态关联与动态关联(binding)【谭书P390】
关联:确定调用的具体对象的过程
静态关联:在编译时即可确定调用的虚函数属于哪个类:函数重载、通过对象名调用的虚函数
动态关联:在运行阶段确定调用的具体对象:通过父类指针调用的虚函数
编译时,系统只看到“p->fun()”这样的语句,并不能确定fun()是哪个对象的
运行时,令父类指针p指向类族中某个类对象,再通过指针变量调用这个对象的虚函数fun(),此时调用的对象是确定的
虚析构函数【谭书P392 ECP42】
一般来说,子类B的对象在释放时,先调用子类B的虚构函数,再调用父类A的析构函数(即和构造函数调用顺序相反)
定义:A* p = new B;
delete p;
则只会调用A的析构函数。这是因为p是父类指针,指向子类B对象中父类A的那部分。
此时需要将父类A的析构函数声明为虚析构函数:virtual ~A(){},这样再执行delete p时,会先执行B,再执行A的析构函数
(1)并非所有base class都要声明虚析构函数。只有当其用于多态时,即只有当类内至少含有一个虚函数时,才为其声明虚析构函数
如果一个类不作为base class,或不用于多态,则不应该声明虚析构函数
因为虚函数的存在,会使对象包涵一个vptr指针,指向虚函数表,使得对象所占内存增大
单纯一个虚析构函数说明其并不用于多态,因此也不需要虚析构函数,增加的vptr指针只是徒劳增加内存消耗
纯虚函数与抽象类【谭书P393 CppP421 ECP43】
声明纯虚函数:virtual void fun() =0;
(1)纯虚函数的作用是在父类中为其子类保留一个函数的名字,以便子类根据需要对它进行定义
(2)在父类中声明了纯虚函数,而在其子类中没有对该函数定义,则该函数在子类中还是纯虚函数。子类需要对所有纯虚函数进行定义,否则该子类仍然是抽象类,不能用于建立对象
(3)纯虚函数是一种特殊的虚函数,在子类中被重写后,变为普通的虚函数,可以用于后续子类的再次重写
(4)可以为纯虚函数给出定义,这样就能调用这个纯虚函数。但仍旧保留了其纯虚性,该类还是抽象类。
——————————————————————————————————————————————————————————————————————————————————————————
抽象类:包含纯虚函数的类
(1)抽象类的作用是作为一个类族的共同父类
(2)包含纯虚函数的类都是抽象类,无法用来建立对象,但可以定义指向其的指针变量
(3)可以将析构函数声明为纯虚,同时给出该虚构函数的定义。这样做既可以使得该类成为抽象类,而且其子类的析构函数也能调用到父类的析构函数
(4)ABC(abstract base class)理念,将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则(否则将不能声明对象)。常见于基于组件的编程模式
八、友元
友元类【CppP488 494】
非is-a、has-a关系,但一个类A可以改变另一个类B的状态时,可以将类A声明为类B的友元类
(1)互为友元类,则视情况将某些成员函数的定义放到类声明之后:
class A{
friend class B;
void gun(B b); //由友元声明可知B是一个类
}
class B{
friend class A;
void fun(A a);
void kun();
}
void A::gun(B b){ b.kun(); } //由于要用到kun()函数,需要将gun()函数的定义放到kun()函数的声明(即类B的声明之后)
友元成员函数【CppP492】
有时不必让整个类成为另一个类的友元,只需选择仅让特定的类成员成为另一个类的友元。此时需要谨慎地排列各种声明与定义的顺序:
class A{
friend void B::fun(A a);
}
(1)编译器需要知道类B的声明和函数fun()的声明。因此要将类B的声明放在类A的声明之前。但类B的函数fun()有一个类A的对象作为参数,
因此在类B的声明前给出类A的前向声明:
class A;
class B{
void fun(A a); //由前向声明可知A是一个类
}
class A{
friend void B::fun(A a); //有B的定义可知B类、fun()函数的声明
}
(2)若fun()函数中调用了A类的一个函数gun(),此时若将fun()函数的定义放在B类的声明中,fun()函数将无法看到A类的gun()函数的声明。
因此需要将fun()函数的声明放到B类外面,且放到A类声明之后:
class A;
class B{
void fun(A a); //由前向声明可知A是一个类
}
class A{
friend void B::fun(A a); //有B的定义可知B类、fun函数的声明
void gun();
}
void B::fun(A a){ a.gun(); }
(3)若将B类声明为A类的友元类,则可将类A的声明放在类B的声明之前,此时无需前向声明类B,因为友元类声明已经说明了B是一个类:
class A{
friend class B;
void gun(B b); //由友元声明可知B是一个类
}
class B{
void fun(A a);
}
(4)由以上(1)(3)可知:前向声明、友元类声明都可以起到声明类类型的作用
由以上(2)可知:一个类的成员函数用到了另一个类的成员函数时,务必将其定义放置在用到的那个成员函数的声明之后