面向对象程序设计的基本特点
抽象
面向对象首先要对事物进行抽象概括,基本抽象为“特征”、“行为”两个方面,特征在类中表现为属性(变量),行为在类中表现为方法(函数)。
封装
将抽象得到的数据和行为(功能)相结合,形成“类”。其中数据和(操作数据的)函数都是类的成员。
继承
继承机制,在保持原有类特性的基础上,进行更具体更详细的说明。
多态
对人类思维方式的模拟,例如:打球。可以是各种球。则需要多态。
类和对象
类的定义
class Animal{
public:
void run();
void eat();
void love();
private:
int sex;
char name;
int age;
protect:
}
这段代码封装了动物的数据和行为,分别成为Animal类的数据成员和函数成员。
- Cpp中函数的形参可以有默认值,基本规范是:
(1)指定了默认值参数的右边不允许出现未指定默认值的参数
(2)不允许在函数原型和定义中同时指定默认值,一般在定义中指定 - 函数重载
** 相同函数名下实现不同操作
** 实现原理,编译器通过函数参数个数或参数类型找到对应函数
** PS:注意避免二义性,即编译器无法区分
例如
- this自引用指针,实例化对象时系统将自动创建指向该对象的指针this。
- 对象数组
** 对象数组的初始化:
(1)对象的构造函数只有一个参数时,与普通数组初始化格式一致
像这样
(2)对象构造函数参数多于一个时,需完整写出
像这样
ps:数组后两个对象用缺省构造函数初始化即ArrayType(0, 0)
静态数据成员:实现多个对象之间数据共享
** 也遵循public、protect、private访问规则常数据成员:只能通过成员初始化列表对常数据成员进行初始化、而后值不能再改变
** 常成员函数:不可改变函数中数据的值
常成员函数说明方式(类内类外)
PS:1.const对象只能访问const成员函数,而不能访问非const成员函数
2.const成员函数只能调用const成员对象,而不能调用非const成员对象(编译时检查)
类成员的访问控制
访问控制属性(与继承)
- 私有类型 private:私有成员智能被本类的成员函数访问,来自类外部的任何访问都是非法的。
** 私有继承使用较少,因其在进一步(二次)派生后,基类的成员将无法在新派生类中访问,相当于终止了基类的派生。(通常采用下文提到的保护继承)
- 保护类型 protect:该类型是私有类型的拓展,在基类以及直接派生类中,protect类型和private没什么区别。区别 主要体现在多层继承中。
** 当间接继承时,若祖父类到父类采用私有继承,祖父类在父类中是私有类型父类内部可以访问,但是子类继承时因为祖父类特性在父类中的私有性而无法继承,因而子类将无法访问祖父类的特性
** 若采用保护继承,祖父类在父类中是保护类型,不仅父类可以访问,子类继承父类后也能够访问祖父类特性。
- 公有类型 public:类的外部接口,类外通过public中的成员访问类
** 公有继承时,除基类的私有成员不可访问外,其余两种访问属性在派生类中不变
对象
声明一个对象和声明一个一般变量相同:
Animal cat;
就声明了一个“动物类型”的对象“猫”。(声明的对象所占据内存只用于存放数据成员,函数成员在类定义时就存在代码区中)
类的成员函数
成员函数的实现
函数的原型声明要写在类体中,具体实现类外类内均可。但通常写于类体之外(写在类体内相当于内联函数的隐式声明),且在函数类型后加上类名:
void Animal::eat(){...}
成员函数调用中的目的对象
调用(非静态)成员函数时需要使用“.”操作符指出针对对象。该对象称为目的对象。
带默认形参值的成员函数
内联成员函数
函数体实现时,在函数返回值类型前加上inline
inline void Animal eat(){...}
内联成员函数的函数体会在编译时被插入到每个调用它的地方。
优点:减少调用开销,提高执行效率
缺点:增加编译后代码长度
综上,内联函数通常用于实现相对简单的成员函数。
构造函数和析构函数
构造函数
默认构造函数
如果没有显示定义构造函数,那么编译器就会隐式地定义一个默认地构造函数。
通常要定义默认的构造函数,原因有三:
由于编译器只在类不包含任何构造函数的情况下才会替我们生成默认构造函数,一定我们定义了其他的构造函数,需要我们自己定义默认构造函数,否则构造类将没有默认构造函数
(存疑)合成的默认构造函数可能会执行错误的操作。
编译器不能为某些类合成默认的构造函数。如:类中包含一个其他类 类型的成员,并且这个类没有默认构造函数,那么编译器就无法初始化该成员。
复制构造函数
形参为本类的对象的引用。作用是使用一个已经存在的对象(即复制构造函数参数所指),去初始化同类的一个新对象。
实现:
Animal::Animal(Animal &a){
age = a.age;
sex = a.sex;
cout<<"Calling the copy constructor"<<endl;
}
复制构造函数在以下情况会被调用:
- 当用类的一个对象去初始化该类的另一个对象时
Animal cat_b(cat);
Animal cat_c = cat;
(以上两种写法等价)
- 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时
void function(Animal p){...}
int main(){
Animal cat;
function(cat); //相当于p = cat;
}
- 如果函数的返回值是类的对象,函数执行完成返回调用者时
Animal function(){Animal a; return a;}
int main(){
Animal cat;
cat = function(); //相当于cat = a
}
析构函数
不接受任何参数。
作用是用来完成对象被删除前的一些清理工作(释放相应的内存空间),在对象的生存期即将结束的时刻被自动调用。
析构函数与构造函数作用几乎正好相反。
类的继承
基类与派生类
继承就是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。
原有类成为基类、父类,产生的新类称为派生类、子类。
一个派生类可以同时有多个基类,称为多继承。派生类只有一个直接基类则称为单继承。
直接基类:直接参与派生出某类的基类
间接基类:基类的基类甚至更高层的基类。
访问控制
继承方式(见类成员的访问控制属性)
类型兼容规则(*)
指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
派生类的对象可以隐含转换为基类对象
派生类的对象可以初始化基类的引用
派生类的指针可以隐含转换为积累的指针
在替代之后,** 派生类对象就可以作为基类的对象使用 **,但只能使用从基类继承的成员。
派生类的构造和析构函数
派生类的构造和析构函数与基类相同,但基类的构造和析构函数不会继承下来
构造函数
构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化。
派生类对于基类的很多成员对象是不能直接访问的,因此要完成对基类成员对象的初始化工作,需要通过调用基类的构造函数。
- 执行次序:调用基类构造函数、初始化新增成员对象、派生类构造函数函数体
(ps:区别于构造函数的执行顺序:“父母->客人->自己”)
- 一般语法形式:
派生类名::派生类名(参数表):基类1(基类1参数表),... ,基类名n(基类n参数表),成员对象(成员对象参数表)...基本类型成员初始化
{
派生类构造函数的其他初始化操作;
}
复制构造函数
对于存在继承关系的类,编译系统会在必要时生成一个隐含的复制构造函数,这个复制构造函数会自动调用基类的复制构造函数然后对派生类新增的成员对象一一执行复制。
若要为派生类编写复制构造函数,一般需要** 为基类相应的复制构造函数 **传递参数。
析构函数
与其他普通类(或理解为没有继承关系的类)相同,声明析构函数即可。系统会自动调用基类及对象成员的析构函数来对基类及对象成员进行清理。与构造函数的执行次序正好严格相反。
派生成员的标识与访问
作用域分辨符“::”
如果子类中定义的函数与父类的函数同名但具有不同的参数表,不属于函数重载,这是自类中的函数将隐藏父类中的同名函数,调用父类中的同名函数必须使用父类名称来限定。只有在相同的作用域中定义的函数才可以重载
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
总而言之,当继承过程中派生类出现同名成员,产生“二义性”时,习惯性用作用域分辨符唯一标识。
虚基类
上文讲到可以用作用域分辨符唯一标识基类中拥有相同名称的成员。
也可以将共同基类设置为虚基类
- 将基类设为虚基类,这时从不同路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射。
虚基类的声明是在派生类(图中派生类1、2)的定义过程中进行的,其语法形式为:
class cat: virtual protect Animal;
- 虚基类及其派生类构造函数
虚基类声明有非默认形式(带形参)的构造函数,且没有声明默认形式的构造函数时:
** 在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化 **
多态性
概述
所谓多态性,就是**不同对象**收到相同的消息时,产生不同的动作。
简单来说就是用一个相同的名字定义多个不同的函数,从而实现“一个接口,多种方法”。
预备知识
- 虚函数(具体见下文)
- 联编:将函数调用链接至相应的函数体代码的过程
** 静态联编:系统在编译时就决定如何实现某一动作。自然要求在编译时就知道调用函数的全部信息。 其优点是调用速度快
*** 例如:函数重载、模板
重载函数通常表现为:
(1)有不同的参数类型
(2)有不同的类对象
(3)常用作用域分辨符区分
** 动态联编:系统在运行时动态实现某一动作。 优点是更灵活更抽象、程序更易维护
*** 常通过继承和虚函数来实现。通常用指向基类的指针(或引用)来调用派生类的虚函数
如(代码接下段):
int main()
{
//分别用基类和派生类创建两个对象
Base base_obj;
Derived1 one_obj;
//创建基类指针指向基类对象
Base *p = &base_obj;
p->Who();
//基类指针指向派生类对象
Base *p = &one_obj;
p->Who();
return 0;
}
上述代码运行结果应该是
I am base class!
I am Derived1 class!
运行结果说明,通过虚函数和指向不同对象的基类指针,C++系统能判别应该调用同一类族中哪一个类对象的成员函数,即同一个语句“p->Who()”,由于*赋给p的对象地址不同*,使得能够实现调用不同Who()函数版本的方法。
由于所调用函数Who()的版本实在陈鼓型运行时确定的,因此称为动态联编,这种多态性也称为运行时的多态性。
运算符重载
- 运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用域不同类型的数据导致不同类型的行为。
** 方法:定义一个重载运算符的函数,在需要执行重载的运算符时,系统会自动调用该函数,以实现相应的运算。(实质就是函数重载)
** 运算符重载有两种形式(统称为运算符函数):
(1)重载为类的成员函数(必须放在public段)
(2)重载为类的友元函数
运算符函数一般语法形式:
函数类型 operator 运算符(形参列表)
{
函数体;//对运算符的重载处理
}
(1)“函数类型”指定了重载运算符的返回值类型
(2)“operator”时定义运算符重载函数的关键字
(3)“运算符”给定了要重载的运算符(必须是可重载运算符)
对复数加减的运算符重载
** 运算符函数的操作数
一、作为(public段)成员函数时
(1)重载一元运算符。参数表为空。调用该函数的对象作为运算符唯一操作数
(2)重载二元运算符。参数表中有一个参数。当前对象作为左操作数,参数作为右操作数
二、作为友元函数时
(1)因为不是成员函数,所以没有this指针。因此,必须在参数表中显示列出每一个操作数(即比用成员函数重载运算符时多一个参数)。
** 运算符重载规则
(1)下列四个运算符只能用成员函数重载
"="、"()"、"[]"、"->"
(2)下列两个运算符只能用友元函数重载
"<<"、">>"
(3)下列五个运算符不能重载
1.成员访问运算符 ".";
2.成员指针访问运算符 ".*";
3.作用域符 "::";
4.长度运算符 "sizeof";
5.条件运算符 "?:";
(4)重载时,运算符的优先级、结合性以及操作数的个数都不能改变
(5)重载运算符不能有默认参数
(6)除赋值运算符"="外,其他通过成员函数重载的运算符都可以被派生类继承,但要在派生类再次重载定义,因为参数要一致(this指针)
虚函数
- 虚函数:
虚函数在基类中定义,在基类成员函数声明的前面加上virtual关键字(派生类中重定义该函数时不加),即可把该函数声明为虚函数。
即:
(1)在基类中声明虚函数
(2)派生类中该函数与基类中原型完全相同(包括函数名、返回类型、参数个数及类型、顺序)
** 不允许在子类中定义 与基类虚函数仅返回类型不同的成员函数
** 仅仅函数名相同但参数特征不同的函数,系统视为函数重载(不同函数)
** 通过虚函数使用多态性机制时,派生类应该从基类公有派生
(3)虚函数的限制
** 只有类的成员函数才能说明为虚函数(仅适用于有继承关系的类对象)
** 只有非静态成员函数才能说明为虚函数(虚函数调用要靠特定对象)
** 内联函数不能是虚函数,因为内联函数不能再运行中动态确定其位置
** 构造函数不能是虚函数
** 析构函数可以是虚函数。一个类如果定义了虚函数,一般(或必须)也将析构函数定义成虚函数(具体原因在另一篇文章中说明《C++的虚析构函数》)
如:
class Base{
public:
virtual void Who(){cout<<"I am base class!"<<endl;}//①定义虚函数
virtual ~Base(){};//(基类)析构函数通常说明为虚函数
}
class Derived1: public Base{
public:
void Who(){cout<<"I am Derived1 class!"<<endl;}//②派生类中只修改函数体
}
......//③必须用指向基类的指针(或引用访问虚函数)
纯虚函数与抽象类
- 纯虚函数:为了强制派生类重新定义其基类的虚函数,C++引入了纯虚函数
一般格式:virtual 函数原型 = 0;
virtual void Who() = 0;
- 抽象类:若一个类中至少说明了一个纯虚函数,则把该类称为抽象类
class Base
{
public:
virtual void Draw() = 0;
virtual double Perimeter() = 0;
}
** 抽象类没有完整地实现,故不能实例化、不能声明抽象类的实例、不能创建它的对象
** 抽象类只能作为其他类的基类
** 但可以定义它的指针变量
** 常由(一个或多个)抽象类作为基类派生出具体类。但不允许从具体类派生出抽象类
** 抽象类中也可以定义普通成员函数或虚函数。派生类虽然不能创建对象,但仍然可以通过派生类对象来调用这些普通函数或虚函数
模板(类型参数化)
定义:所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理不同类型的对象
函数模板
(1)所有函数模板都是用template关键字开始,后<>括起“模板参数表”
(2)class(或typename)接受类型参数,可以代表预定义类型或自定义类型(结构体、类等)
模板的实例化:便能一起会从实参类型推导出函数模板的类型函数。当类型参数确定后,编译器将以函数模板为样板生成函数,这一过程称为函数模板的实例化
模板的特化:当函数模板功能不能满足某一实例的需求时,需要特别定义实现一个“特别的模板”,称为模板的特化。例如:
主函数中,我们希望char*z接收比较两个字符串大小的返回值。但原有的函数模板比较的是字符串的首地址。因此我们需要一个“特化的模板”加入strcmp后比较字符串大小,而不影响模板原来的作用。
类模板
//类模板定义体外定义成员函数
template<模板参数表>
类型名 类名<模板“形参”列表>::函数名(参数表)
定义类模板:(1)定义类;(2)在类定义体外定义成员函数
- 类模板的派生
(1)类模板派生出类模板
* 派生指出父类(此处为Base)时要加上模板参数(此处为<T>)
(2)类模板派生出普通类
- 作为普通类的父类(模板),必须是类模板实例化后的(即类型参数确定)
- 派生(普通)类前不需要声明template<class T>
(3)普通类派生出类模板
模板相关
- 强制类型转换——显式给出模板实参
- 函数模板、重载的普通函数同时存在,调用顺序问题
- PS:
(1)模板本身在编译时不会生成任何目标代码,只有由模板生成的实例会生成目标代码
(2)被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数只将声明放在头文件中
(3)函数指针只能指向模板实例,而不能指向模板本身
参考书籍:
《C++语言程序与设计》(清华大学出版社)