前言
个人觉得学习编程最有效的方法是阅读专业的书籍,通过阅读专业书籍可以构建更加系统化的知识体系。 一直以来都很想深入学习一下C++,将其作为自己的主力开发语言。现在为了完成自己这一直以来的心愿,准备认真学习《C++ Primer Plus》。 为了提高学习效率,在学习的过程中将通过发布学习笔记的方式,持续记录自己学习C++的过程。
一、一个简单的基类
Class Person
{
...
}
Class Chinese : public Person
{
...
}
上述代码Chinese类继承了Person类,Chinese对象将具有以下特征:
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
- 派生类对象可以使用基类的方法(派生类继承了基类的接口)。
需要在继承特性中添加:
- 派生类需要自己的构造函数。
- 派生类可以根据需要添加额外的数据成员和成员函数。
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
创建派生类对象时,程序首先创建基类对象。
Chinese::Chinese(int age) : Person(age)
{
...
}
如果不调用基类构造函数,程序将使用默认的基类构造函数。
有关派生类构造函数的要点如下:
- 首先创建基类对象;
- 派生类构造函数应通过成员初始化列表将基类信息床底给基类构造函数;
- 派生类构造函数应初始化派生类新增的数据成员。
释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示转换的情况下引用派生类对象。
二、继承:is-a关系
C++有3种继承方式:
- 公有继承,最常用的方式,它建立一种
is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。 - 保护继承
- 私有继承。
三、多态公有继承
一个方法的行为随上下文而异,这种复杂的行为成为多态——具有多种形态。有两种重要的机制可以实现多态公有继承:
- 在派生类中重新定义基类的方法。
- 使用虚方法。
当派生类和基类有相同名称的方法时,需要使用作用域解析运算符来明确调用的是那一个类对象的方法。
使用虚方法有以下作用:
- 可以在程序运行阶段根据情况选择对应的成员函数
- 可以在派生类中对基类的虚函数进行重写
使用虚析构函数,可以确保正确的析构函数序列被调用(当基类使用虚析构函数时,会采取动态编译,才会在进行delete pointer时根据赋值给指针的对象去调用对应的析构函数)。
四、静态联编和动态联编
在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。 编译器在程序运行时选择正确的虚方法代码,被称为动态联编(dynamic binding),又被称为晚期联编(late binding)。
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。
将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显示类型转换。 将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。 静态联编效率比动态联编高。在考虑是否将函数设置为虚函数,需要结合效率和是否需要重新定义该函数两方面进行考虑。 在使用虚函数时,在内存和执行速度方面都有一定的成本,包括:
- 每个对象都将增大,增大量为存储地址的空间;
- 对于每个类,编译器都创建一个虚函数地址表(数组);
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
构造函数不能是虚函数。 析构函数应当是虚函数,除非类不用做基类。通常应给基类提供一个虚析构函数,即使这个类不用做基类,这是一个效率方面的问题。 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
重新定义时有两条经验规则:
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化:
class A
{
public:
virtual A & MyMethod(int i);
}
class B : public A
{
public:
virtual B & MyMethod(int i);
}
注意:这种例外只适用于返回值,而不使用于参数。
- 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果之定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改,则新定义可只调用基类版本:
void B::MyMethod2() const {A::MyMethod2();}
五、访问控制:protected
关键字protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员。protected和private的区别是在基类派生的类中体现。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
六、抽象基类
纯虚函数声明的结尾处为=0:
Class A
{
public:
virtual double MyMethod2() const = 0;//纯虚函数
}
当类声明中包含纯虚函数时,则不能创建该类的对象。纯虚函数的类只能用作基类。 C++允许纯虚函数有定义,这种情况下,可以将原型声明为虚的:
void Move(int nx , ny ) = 0;
然后提供方法的定义:
void BaseClass::Move(int nx , ny )
{
x = nx;
y = ny;
}
总之,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。
七、继承和动态内存分配
1、第一种情况:派生类不使用new
基类使用动态内存分配派生类不使用,则不需要为派生类定义显式析构函数、复制构造函数和赋值运算符。 程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;删除程序对象时,将首先调用派生类的析构函数,然后调用基类西贡的析构函数。
2、第二种情况:基类和派生类都使用new,这种情况下必须为派生类定义显式析构函数、复制构造函数和赋值运算符。
因为派生类复制构造函数只能访问派生类中的数据,因此必须调用基类复制构造函数来处理基类中的数据:
hasDMA::hasDMA(const hs):baseDMA(hs)
{
...
}
然而,派生类的显示赋值运算符必须负责所有继承的基类对象的赋值,可以通过显示调用基类运算符来完成这项工作:
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
haseDMA::operator=(hs);//通过函数表示法调用基类的赋值运算符
}
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,则是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
3、使用动态内存分配和友元的继承示例
作为派生类的友元,通过使用基类的友元访问基类的成员。通过强制类型转换,解决因为友元不是成员函数,不能使用作用域解决运算符来指出要使用哪个函数的问题。
std::ostream & operator<<(std::ostream & os,const hasDMA & hs)
{
os << (const baseDMA &) hs;
os << "Style:" << hs.style << endl;
return os;
}
八、类设计回顾
1、编译器生成的成员函数
(1)默认构造函数 默认构造函数要么没有参效,要么所有的参数都有默认值。如早没有定义任何构造函数,编译器将定义默认构造函数。 自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。 另外,如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有默认构造函数(因为如果用户定义了某周构造函数,编译器将不再提供默认的构造函数),将导致编译阶段错误。 提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。
(2)复制构造函数 复制构造函数接受其所属类的对象作为参数。 在下述情况下,将使用复制构造函数:
- 将新对象初始化为一个同类对象;
- 按值将对象传递给函数;
- 函数按值返回对象;
- 编译器生成临时对象。 如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
(3)赋值运算符 默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值:
AClass a1;
AClass a2 = a1;//初始化
AClass a3;
a3 = a1;// 赋值
默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。 编译器不会生成将一种类型赋给另一种类型的赋值运算符。如果需要这种转换,除了使用转换函数,还可以显示定义有类型转换功能的赋值运算符。
2、其他的类方法
(1)构造函数
构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。
(2)析构函数
一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
(3)转换
使用一个参数就可以调用的构造函教定义了从参数类型到类类型的转换。
(4)按值传递对象与传递引用
通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
(5)返回对象和返回引用
首先,在编码方面,直接返同对象与返回引用之间唯一的区别在于函数原型和函数头:
Start nova1(const Start &);
Start & nova2(const Start &);
如果函数返回在函数中创建的临时对象,则不要使用引用。如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。
(6)使用const
用它来确保方法不修改参数:
Start::Start(const char * s){...}
可以使用const来确保方法不修改调用它的对象:
void Start::show() const {...}
可以使用const来确保引用或指针返回的值不能用于修改对象中的数据:
const Stock & Stock::topval(const Stock & s) const{...}
3、公有继承的考虑因素
(1)is-a关系
如果派生类不是一种特殊的基类,则不要使用公有派生。
表示is-a关系的方式之一是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。
(2)什么不能被继承
构造函数是不能继承的,也就是说,创建派生类对象时,必须调用派生类的构造函数。C++11新增了一种能够继承构造函数的机制,但默认仍不能继承构造函数。
析构函数也是不能继承的。
赋值运算符是不能继承的。
(3)赋值运算符
如果编译器发现程序将个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符。如果构造函数使用new来初始化指针,则需要提供一个显式赋值运算符。
(4)私有成员与保护成员
对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。
(5)虚方法
设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编);如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但表达了这样的意思,不希望它被重新定义。
(6)析构函数
基类的析构函数应该是虚的。
(7)友元函数
可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。
(8)有关使用基类方法的说明
以公有方式派生的类的对象可以通过多种方式来使用基类的方法。
- 派生类对象自动使用继承而来的基类方法,如果派生类设有重新定义该方法。
- 派生类的构造函数自动调用基类的构造函数。
- 派生类的构造函数自动调用基类的默认构造函数,如果设有在成员初始化列表中指定其他构造南数。
- 派生类构造函数显式地调用成员初始化列表中指定的基类构造函数。
- 派生类方法可以使用作用城解析运算符来调用公有的和受保护的基类方法。
- 派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。
4、类函数小结
| 函数 | 能否继承 | 成员还是友元 | 默认能否生成/能否为虚函数/能否可以有返回类型 |
|---|---|---|---|
| 构造函数 | 否 | 成员 | 能/否/否 |
| 析构函数 | 否 | 成员 | 能/能/否 |
| = | 否 | 成员 | 能/能/能 |
| & | 能 | 任意 | 能/能/能 |
| 转换函数 | 能 | 成员 | 否/能/否 |
| () | 能 | 成员 | 否/能/能 |
| [] | 能 | 成员 | 否/能/能 |
| -> | 能 | 成员 | 否/能/能 |
| op= | 能 | 任意 | 否/能/能 |
| new | 能 | 静态成员 | 否/否/void* |
| delete | 能 | 静态成员 | 否/否/void |
| 其他运算符 | 能 | 任意 | 否/能/能 |
| 其他成员 | 能 | 成员 | 否/能/能 |
| 友元 | 否 | 友元 | 否/否/能 |