C++——类继承
类库:类库由类声明和实现构成。类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。
类继承:从已有的类派生出新的类,派生类继承了原有类(称为基类)的特征,包括方法。
通过类继承可以完成的工作:
*可以在已有类的基础上添加功能;
*可以给类添加数据;
*可以修改类的行为。
继承机制只需要提供新特性,甚至不需要访问源代码就可以派生出类。
一、一个简单的基类
首先我们定义一个简单的基类Person,其设计如下:
Person.h
#include <iostream>
#include <string>
using std::string;
class Person{
private:
string name_;
int age_;
public:
Person(const string & name = "none", int age = );//形参类型声明为const string &,那么实参既可以是string对象,也可以是字符串常量。
void setName(const string &name);
void setAge(int age);
string getName()const;
int getAge() const;
friend std::ostream & operator<<(std::ostream & os, const Person & p);
};
Person.cpp
#include "Person.h"
Person::Person(const string & name, int age){
name_ = name;
age_ = age;
}
std::ostream & operator<<(std::ostream & os, const Person & p){
os << "name:" << p.name_ << ", age:" << p.age_;
return os;
}
void Person::setName(const string &name){
name_ = name;
}
void Person::setAge(int age){
age_ = age;
}
string Person::getName()const{
return name_;
}
int Person::getAge()const{
return age_;
}
提示:在设计一个类的时候,我们需要考虑一下几个问题:
*是否需要显式提供默认构造函数;
*是否需要显式提供析构函数;
*是否需要显式提供复制构造函数;
*是否需要显式提供赋值运算符重载函数;
*是否需要显式提供地址运算符函数;
一般来说,如果在类的构造函数中使用了new运算符,或者在其他成员函数中使用了new运算符来修改类的成员,那么就需要考虑显式提供复制构造函数、赋值运算符重载函数、析构函数。在Person类中,我们使用编译器提供的默认析构函数、默认复制构造函数和默认的赋值运算符重载函数即可。
1、派生一个类
下面我们设计一个Teacher类继承自Person类。首先将Teacher类声明为从Person类派生而来:
#include <iostream>
#include "Person.h" class Teacher:public Person{
// ...
};
冒号指出Teacher类的基类是Person类。上述特殊的生命头表明Person是一个公有基类,这杯称为公有派生。派生类对象包含基类对象。
使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
派生类将具有以下特征:
*派生类对象存储了基类的数据成员(派生类继承了基类的实现);
*派生类对象可以使用基类的方法(派生类继承了基类的接口)。
接下来,我们就可以在继承特性中添加下面的内容:
*派生类需要自己的构造函数;
*派生类可以根据需要添加额外的数据成员和成员函数。
在我们设计的Teacher类需要一个数据成员来存储工作的单位、工资以及所教授的课程。还应包括检查这些信息和重置这些信息的方法:
#include <iostream>
#include "Person.h" class Teacher:public Person{
private:
string workUnit_;//工作单位
float salary_;//工资
string course_;//教授的课程
public:
Teacher(const string & , int , const string &, float, const string &);
Teacher(const Person &, const string &, float, const string &);
Teacher();
void setWorkUnit(const string & );
void setSalary(float );
void setCourse(const string &);
string getWorkUnit()const;
float getSalary()const;
string getCourse()const;
friend std::ostream & operator<<(std::ostream & os , const Teacher &);
};
构造函数必须给新成员(如果有新成员)和继承的成员提供数据。
2、构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如,派生类构造函数不能直接设置继承来的成员,而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类的构造函数。
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如,下面是第一个Teacher类的构造函数代码:
Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(name,age){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此下面的两段代码是等效的:
Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
除非要使用默认的构造函数,否则应显式调用正确的基类构造函数。
下面来看第二个构造函数的代码:
Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
由于per的类型为Person,因此调用基类的复制构造函数。在这里,基类Person没有定义复制构造函数,如果需要复制构造函数但又没有定义,编译器将生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配。
同样,也可以对派生类使用成员初始化列表语法。在这种情况下,应在列表中使用成员名,而不是类名。所以,第二个构造函数可以按照下面的方式编写:
Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per),workUnit_(workUnit),salary_(salary),course_(course){}
有关派生类构造函数的要点有如下几点:
*首先创建基类对象;
*派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
*派生类构造函数应初始化派生类新增的数据成员。
这个例子没有提供显式析构函数,因此使用隐式析构函数。释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
3、使用派生类
要使用派生类,程序必须要能够访问基类声明。可以将基类和派生类的声明置于同一个头文件中,也可以将每个类放在独立的头文件中,但由于这两个类是相关的,所以把其类声明放在一起更合适。
下面是Teacher的完整方法实现文件:
#include "Teacher.h"
Teacher::Teacher(const string & name, int age, const string & workUnit, float salary, const string & course):Person(name,age){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
Teacher::Teacher(const Person & per, const string & workUnit, float salary, const string & course):Person(per){
workUnit_ = workUnit;
salary_ = salary;
course_ = course;
}
Teacher::Teacher(){
workUnit_ = "none";
salary_ = .;
course_ = "none";
}
void Teacher::setCourse(const string & course){
course_ = course;
}
void Teacher::setWorkUnit(const string & workUnit){
workUnit_ = workUnit;
}
void Teacher::setSalary(float salary){
salary_ = salary;
}
string Teacher::getWorkUnit()const{
return workUnit_;
}
string Teacher::getCourse()const{
return course_;
}
float Teacher::getSalary()const{
return salary_;
}
std::ostream & operator<<(std::ostream & os,const Teacher & te){
os << "name:" << te.getName() << ",age:" << te.getAge() << ", workUnit:" << te.workUnit_ << ", salary:" << te.salary_ << ", course:" << te.course_;
return os;
}
4、派生类和基类之间的特殊关系
派生类和基类之间有一些特殊关系。
*派生类可以使用基类的方法,条件是方法不是私有的。
*基类指针可以在不进行显式类型转换的情况下指向派生类对象;
*基类引用可以在不进行显式类型转换的情况下引用派生类对象。
但是,基类指针或引用只能调用基类方法。
通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针。
二、继承:is-a关系
派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际上,C++有3种继承方式:共有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生对象也是一个基类对象,可以对基类执行的操作,也可以对派生类对象执行。
但是公有继承不具有下列关系:
*公有继承不建立has-a关系;
*公有继承不建立is-like-a关系;
*公有继承不建立is-implemented-as-a(作为....来实现)关系。
三、多态公有继承
多态:方法的行为取决于调用该方法的对象,即同一个方法的行为随上下文而异。
有两种重要的机制可用于实现多态公有继承:
*在派生类中重新定义基类的方法;
*使用虚方法。
下面我们重新设计Person类和Teacher类,
Person.h
#ifndef __Demo__Person__
#define __Demo__Person__ #include <iostream>
#include <string>
using namespace std; class Person{
private:
string name_;
int age_;
public:
Person(const string & name = "无名氏", int age = );
virtual ~Person(){};
void setName(const string & name);
void setAge(int age);
const string & getName()const;
int getAge()const;
virtual void showMessage()const;
void setMessage(const string & name, int age);
friend ostream & operator<<(ostream & os, const Person & per); };
#endif /* defined(__Demo__Person__) */
Person.cpp
#include "Person.h"
Person::Person(const string & name, int age){
name_ = name;
age_ = age;
}
void Person::setAge(int age){
age_ = age;
}
void Person::setName(const string &name){
name_ = name;
}
const string & Person::getName()const{
return name_;
}
int Person::getAge()const{
return age_;
}
void Person::showMessage()const{//虚方法
cout <<"调用了Person对象的showMessage()方法:"<< *this;
}
void Person::setMessage(const string &name,int age){
cout << "调用了Person对象的setMessage()方法\n";
name_ = name;
age_ =age;
}
ostream & operator<<(ostream & os, const Person & per){
os << "name:" << per.name_ << ", age:" << per.age_;
return os;
}
Teacher.h
#ifndef __Demo__Teacher__
#define __Demo__Teacher__ #include <iostream>
#include "Person.h" class Teacher:public Person{
private:
string school_;
float salary_;
public:
Teacher(const string & name = "无名氏", int age = , const string & school = "无", float salary = .);
void setSchool(const string &);
void setSalary(float salary);
const string & getSchool()const;
float getSalary()const;
virtual void showMessage()const;
void setMessage(const string & school, float salary);
friend ostream & operator<<(ostream & , const Teacher &);
}; #endif /* defined(__Demo__Teacher__) */
Teacher.cpp
#include "Teacher.h"
Teacher::Teacher(const string & name , int age, const string & school, float salary ):Teacher(name, age){
school_ = school;
salary_ = salary;
}
void Teacher::setSchool(const string & school){
school_ = school;
}
void Teacher:: setSalary(float salary){
salary_ = salary;
}
const string & Teacher:: getSchool()const{
return school_;
}
float Teacher:: getSalary()const{
return salary_;
}
void Teacher:: showMessage()const{
cout << "调用了Teacher对象的showMessage()方法:" << *this;
}
void Teacher:: setMessage(const string & school, float salary){
cout << "调用了Teacher的setMessage()方法\n";
school_ = school;
salary_ = salary;
}
ostream & operator<<(ostream & os, const Teacher & per){
os <<"调用了Teacher对象的<<运算符方法,"<< "name:" << per.getName() << ", age:" << per.getAge() << ", school:" << per.school_ << ", salary:"<< per.salary_;
return os;
}
main.cpp
#include <iostream>
#include "Teacher.h" using namespace std; int main(int argc, const char * argv[]) {
Person *per = new Person{"王晓红",};
Person *per2 = new Teacher{"刘晓东",,"成都七中",5000.0};
per->showMessage();
per2->showMessage();
per->setMessage("王晓玲", );
per2->setMessage("刘翔情", );
per->showMessage();
per2->showMessage();
return ;
}
输出结果:
调用了Person对象的showMessage()方法,name:王晓红, age:
调用了Teacher对象的showMessage()方法,name:刘晓东, age:, school:成都七中, salary:
调用了Person对象的setMessage()方法
调用了Person对象的setMessage()方法
调用了Person对象的showMessage()方法,name:王晓玲, age:
调用了Teacher对象的showMessage()方法,name:刘翔情, age:, school:成都七中, salary:
说明:
首先,在上面的代码中,在基类Person和Teacher类声明中声明showMessage()方法时都使用了C++关键字virtual,这些方法方法叫做虚方法。从输出结果中可以看出,虽然在main.cpp函数中Person对象和Teacher对象都是用Person指针指向的,但是在调用showMessage()方法的时候,都调用了对象各自的方法,即继承类Teacher对象没有调用基类的showMessage()方法。
其次,在基类Person和Teacher类声明中声明setMessage()方法的时候没有使用关键字virtual。从输出结果可以看出,用Person指针指向的Person对象和Teacher对象在调用setMessage()方法的时候,都是调用的基类Person类的setMessage()方法。Teacher类虽然重载了setMessage()方法,但是在用指向Teacher对象的基类Person指针或引用调用该方法的时候并没有调用Teacher对象本身的setMessage()方法。
有上述可以得出以下结论:
如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象类型来选择方法。
因此,我们需要在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。
另外,基类声明一个虚析构函数,可以确保释放派生对象的时候,按正确的顺序调用析构函数。
注意,关键字virtual只用于类声明的方法原型中,而不能用于方法定义中。
非构造函数不能使用成员初始化列表语法,但是派生类方法可以调用公有的基类方法。
在重定义派生类继承方法的代码中调用基类中被继承的同名方法时,如果不使用作用域解析运算符很有可能带来不必要的麻烦,将会创建一个无限递归函数,为避免这种错误必须对基类被继承的同名方法使用作用域解析运算符。例如下面的代码将会创建一个无限递归函数:
void Teacher::showMessage()const{
.....
showMessage();//这样将会创建一个无限递归函数, 因为该函数的默认调用对象是自己本身,即该语句与this->showMessage();等效
.....
}
但是下面的不会出错:
void Teacher::showMessage() const{
....
Person::showMessage();//这将调用基类的showMessage()方法,在这里并不会出现任何错误。
.....
}
虚析构函数
在上面代码中,在基类Person声明中,我们使用了虚析构函数,即virtual ~Person();这样做的理由在于:
如果析构函数不是虚的,则将调用对应于指针或引用类型的析构函数;如果析构函数是虚的,将调用相应对象类型的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。
四、静态联编和动态联编
静态联编:在编译过程中进行联编,又称为早期联编;
动态联编:编译器在程序运行时生成选择正确虚方法的代码,称为动态联编,又称为晚期联编。
1、指针和引用类型的兼容性
在C++中,动态联编与通过指针和引用调用方法相关,从某种成都上说,这是由继承控制的。公有继承建立的is-a关系的一种方法是如何处理指向对象的指针和引用。通常,C++不允许将一种类型的地址赋给另一种类型的指针,也不允许将一种类型的引用指向另一种类型。
指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。
将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。该规则是is-a关系的一部分。向上强制转换是可以传递的,即A是B的基类,B是C的基类,则A引用或指针可以引用A对象、B对象和C对象。
将基类指针或引用转换为派生类指针或引用称为向下强制转换。如果不使用显式类型转换,则向下强制类型转换是不允许的。原因是is-a关系是不可逆的。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。
对于使用基类引用或指针作为参数的函数调用,将进行向上转换。隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。
2、虚成员函数和动态联编
编译器对非虚方法使用静态联编,对虚方法使用动态联编。
编译器将静态联编设为默认联编方案,原因如下:
(1)静态联编效率更高。仅当在程序设计时确实需要虚函数时,才使用它们。提示:如果要在派生类中重定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
(2)使用虚方法时,在内存和执行速度方面将有一定的成本,包括:
*每个对象都将增大,增大量为存储地址的空间;
*对于每个类,编译器都将创建一个虚函数地址表(数组);
*对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
3、有关虚函数的注意事项
*在基类方法的声明中使用关键字virtual可以使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的;
*如果使用指向对象的指针或引用来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种类型非常重要,因为这样基类指针或引用可以指向派生类对象。
*如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
对于虚方法,还需要了解下面的知识:
(1)构造函数
构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数。
(2)析构函数
析构函数应当是虚函数,除非类不用做基类。即使基类不需要显式析构函数提供服务,也不应依赖于默认的析构函数,而应提供虚析构函数,即使它不做任何操作。因此,通常应该给基类提供一个虚析构函数,即使它并不需要析构函数。
(3)友元
友元不能是虚函数,因为友元不是类成员,而只有成员函数才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
(4)没有重新定义
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
(5)重新定义将隐藏方法
假设创建了如下的代码:
class Dwelling{
public:
virtual void showperks(int a)const;
....
};
class Hovel:public Dewlling{
public:
virtual void showperks()const;
...
};
这将导致问题,可能会出现类似于下面这样的警告:
Warning :Hovel::showperks(void) hides Dewlling::showperks(ing)
也可能不出现警告。但不管怎么样,代码将具有如下含义:
Hovel trump;
trump.showperks();//允许
turmp.showperks(5);//不允许
新定义将showperks()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。
这里引出了两条经验规则:
第一、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化:
class Dwelling{
public:
virtual Dewlling& build(int a);
....
};
class Hovel:public Dewlling{
public:
virtual Hovel& build(int a);
...
};
注意,这种例外只适用于返回值,而不适用于参数。
第二、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
五、访问控制:protected
关键字protected与private类似,在类外只能用公有类成员函数来访问protected部分中的类成员。private与protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部来说,保护成员的行为与私有成员类似;但对于派生类来说,保护成员的行为与公有成员类似。
警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能直接使用的内部函数。
六、抽象基类
抽象基类(abstract base class, ABC)
C++通过使用纯虚函数来提供未实现的函数。纯虚函数声明的结尾处为=0.
当类声明中包含纯虚函数时,则不能创建该类的对象。这里的概念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。纯虚函数可以有函数定义,也可以没有函数定义。
ABC理念
设计ABC之前,首先应开发一个模型——指出编程问题所需的类以及他们之间的相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会被用作基类的类设计为具体的类。
可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用ABC使得组件设计人员能够制定“接口约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。
七、继承和动态内存分配
一般来说,在设计类的时候,我们会根据类是否使用了动态内存分配来考虑是否需要提供显式析构函数、复制构造函数和赋值运算符,对于派生类同样需要考虑这些因素。一般在设计派生类的时候会有一下两种情况:
1、派生类不使用new
(1)析构函数
派生类的默认析构函数总是要执行下面的操作:执行自身的代码后调用基类的析构函数。因此,对于没有使用动态内存分配的派生类来说,默认析构函数是合适的。
(2)复制构造函数
默认复制构造函数执行成员复制,成员复制将根据类成员类型采用相应的复制方式;并且在复制类成员和继承的类组件时,则是使用该类的复制构造函数完成的。因此,对于没有使用动态内存分配的派生类来说,默认复制构造函数是合适的。
(3)赋值运算符
类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值,因此对于没有使用动态内存分配的派生类来说,默认的赋值运算符是合适的。
2、派生类使用new
下面的讨论都是基于A是B的基类,并且A和B使用了动态内存分配。
(1)析构函数
派生类的析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。
(2)复制构造函数
派生类B的复制构造函数只能自身的数据,因此它必须调用基类A的复制构造函数来处理共享的基类数据,派生类的复制构造函数的基本形式如下:
B::B(const B & b):A(b){
//复制基类自身的数据
....
}
需要注意的一点是,成员初始化列表将一个派生类B的引用传给基类A的复制构造函数,这里使用了向上强制类型转换(基类引用或指针可以指向派生类对象),这样基类A的复制构造函数将使用派生类B引用对象参数共享的基类数据部分来构造新对象的共享基类数据部分。
(3)赋值运算符
派生类的复制运算符应遵循下面的基本格式:
B & B::operator=(const B & b){
if (this == & b)
return *this;
A::operator=(b);
//.....
return *this;
}
在派生类的赋值运算符中,必须使用作用域解析运算符显式调用基类的赋值运算符,不然将会导致无限递归。同时,给基类赋值运算符提供参数的时候只需要提供派生类对象引用即可,这里会自动使用向上强制类型转换,这样基类赋值运算符就只会使用派生类共享的基类数据部分来进行赋值操作。
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数和赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同方式来满足的。对于析构函数,这是自动网承德。对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的,如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
3、使用动态内存分配和友元的继承示例
由于友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。在这里,假设类A是类B的基类,operator<<(ostream &, const A &)为基类A的<<重载函数原型,那么派生类B的<<运算符重载函数应使用下面的定义:
ostream & operator<<(ostream & os, const B & b){
os << (const A &)b;//必须显式使用向上强制类型转换,这样将会调用基类A的友元<<运算符重载函数;否则将会导致无限递归
//......
return os;
}
八、类设计回顾
1、编译器生成的成员函数
(1)默认构造函数
默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数。
自动生成的默认构造函数的一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
另外,如果派生类的构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有过构造函数,将导致编译阶段错误。
如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。
(2)复制构造函数
复制构造函数接受其所属类的对象作为参数。
在下述情况下将使用复制构造函数:
*将对象初始化为另一个同类对象;
*按值将对象传递给函数;
*函数按值返回对象;
*编译器生成临时对象。
如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
在某些情况下,成员初始化是不合适。例如,使用new初始化的成员指针通常要求深度复制,或者类可能包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。
(3)赋值运算符
默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值和初始化混淆了。如果语句创建新的对象,则用初始化;如果语句修改已有对象的值,则是赋值。
默认赋值为成员赋值。如果成员为类对象,则默认赋值运算符将使用相应类的赋值运算符。如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。
编译器不会生成将一种类型赋给另一种类型的赋值运算符。
2、其他的类方法
(1)构造函数
构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。
(2)析构函数
一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
(3)转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。
在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换。
要将类对象转换为其他类型,应定义转换函数。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。即使没有声明返回类型,函数也应返回所需的转换值。
但是,对于某些类,包含转换函数将增加代码的二义性。可以将关键字explicitshiyong1于转换函数,这样将禁止隐式转换,但仍允许显式转换。
(4)按值传递对象与传递引用
通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间多得多。如果函数不修改对象,应将参数声明为const引用。
按引用传递传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
(5)返回对象和返回引用
有些类方法返回对象。有些成员函数直接返回对象,而另一些返回引用。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。来具体看一下:
首先,在编码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头:
Star noval1(const Star &);//返回Star对象
Star noval2(const Star &);//返回Star引用
其次,应返回引用而不是返回对象的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可以节省时间和内存。直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。
然而,并不总是可以返回引用。函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。
通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。
如果函数返回的是通过引用或指针传递给他的对象,则应按引用返回对象。
(6)使用const
使用const时应特别注意。可以用它来确保方法不修改参数:
Star:: Star(const char * s){.....}
使用const可以来确保方法不修改调用它的对象:
void Star::show()const{....}//这里const表示const Star* this,而this指向调用的对象。
通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象。但可以使用const确保引用或指针的值不能用于修改对象中的数据:
const Stock & Stock::topval(const Stock & s)const{
if(s.total_val > total_val)
return s;
else
return *this;
}
该方法返回对this或s的引用。因为this和s被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const。
注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
3、公有继承的考虑因素
通常,在程序中使用继承时,有很多问题需要注意。下面来看其中的一些问题。
(1)is-a关系
要遵循is-a关系。如果派生类不是一种特殊的基类,则不要使用公有派生。
在某些情况下,最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。
表示is-a关系的方式之一是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。另外,反过来是行不通的,即不能在不进行显式类型转换的情况下,将派生类指针或引用指向基类对象。这种显式类型转换(向下强制转换)可能有意义,也可能没有,这取决于类声明。
(2)什么不能被继承
构造函数是不能被继承的,也就是说,创建派生类时,必须调用派生类的构造函数。然而,派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生类对象的基类部分。如果派生类构造函数没有使用成员初始化列表语法显式调用基类构造函数,将使用基类的默认构造函数。在继承链中,每个类都可以使用成员初始化列表将信息传递给相邻的基类。C++11新增了一种能够继承构造函数的机制,但默认仍不继承构造函数。
析构函数也是不能继承的。然而,在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚的。
赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。
(3)赋值运算符
如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符。这个运算符的默认或隐式版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。然而,如果对象属于派生类,编译器将使用基类赋值运算符来处理派生类对象中基类部分的赋值。如果显式地为基类提供了赋值运算符,将使用该运算符。于此类似,如果成员是另一个类的对象,则对于该成员,将使用其所属类的复制运算符。
正如多次提到,如果类构造函数使用new来初始化指针,则需要提供一个显式赋值运算符。因为对于派生类对象的基类部分,C++将使用基类的赋值运算符,所以不需要为派生类重新定义赋值运算符,除非它添加了需要特别留意的数据成员。
然而,如果派生类使用了new,则必须提供显式复制运算符。必须给类的每个成员提供赋值运算符,而不仅仅是新成员。
另外,将派生类对象赋给基类对象,将调用基类赋值运算符,基类赋值运算符的参数为一个基类引用,它可以指向派生类对象。只是,赋值运算符只处理基类成员,而忽略派生类新增加的成员(如果派生类新增加了成员)。总之,可以将派生类对象赋给基类对象,但这只涉及基类的成员。
相反,如果把基类对象赋给派生类对象,除非派生类有将基类对象转换为其类型的转换构造函数(可以接受一个类型为基类的参数和其他参数,条件是其他参数有默认值);否则,将会导致错误(派生类引用不能自动引用基类对象)。
总之,问题“是否可以将基类对象赋给派生类对象?”的答案是“也许”。如果派生类包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生类对象。如果派生类定义了将基类对象赋给派生类对象的赋值运算符,则也可以这样做。如果上述两个条件都不满足,则不能这样做,除非使用显式强制类型转换。
(4)私有成员与保护成员
对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问基类的私有成员。因此,将基类成员设置为私有成员可以提高安全性,而将他们设置为保护成员则可以简化代码的编写工作,并提高访问速度。
(5)虚方法
设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编);如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但是却表达了这样的意思:不希望它被重新定义。
注意,不适当的代码将阻止动态联编。例如,请看下面的两个函数:
void show(const Brass &rba){
rba.ViewAcct();
cout << endl;
}
void inadequate(Brass ba){
ba.ViewAcct();
cout << endl;
}
第一个函数按引用传递对象,第二个按值传递对象。
现在假设派生类参数传递给上述两个函数:
BrassPlus buzz(....);
show(buzz);
inadequate(buzz);
show()函数调用使rba成为BrassPlus对象buzz的引用,因此,rba.ViewAcct()被解释为BrassPlus版本,正如应该的那样。但在inadequate()函数中(它是按值传递参数的),ba是Brass(const Brass &)构造函数创建的一个对象(自动向上强制转换使得构造函数可以引用一个BrassPlus对象)。因此,在indaquate()中,ba.ViewAcct()是Brass版本,所以只有buss的Brass部分被显示。
(6)析构函数
正如前面介绍的,基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生类对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。
(7)友元函数
由于友元函数并非类成员,因此不能继承。然而,我们可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。
(8)有关使用基类方法的说明
以公有方式派生的类的对象可以通过多种方式来使用基类的方法。
*派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法;
*派生类的构造函数自动调用基类的构造函数;
*派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数;
*派生类构造函数显式地调用成员初始化列表中指定的基类构造函数
*派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法;
*派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换成基类引用或指针,然后使用该引用或指针调用基类的友元函数。
4、类函数小结
C++类函数有很多不同的变体,其中有些可以继承,有些不可以。有些运算符函数既可以是成员函数,也可以是友元,而有些运算符函数只能是成员函数。下面的表总结了这些特征,其中op=表示诸如+=、*=等格式的赋值运算符。注意,op=运算符的特征与“其他运算符”类别并没有区别。单独列出op=旨在指出这些运算符与=运算符的行为不同。
函数 | 能够继承 | 成员还是友元 | 默认能否生成 | 能否为虚函数 | 是否可以有返回类型 |
构造函数 | 否 | 成员 | 能 | 否 | 否 |
析构函数 | 否 | 成员 | 能 | 能 | 否 |
= | 否 | 成员 | 能 | 能 | 能 |
& | 能 | 任意 | 能 | 能 | 能 |
转换函数 | 能 | 成员 | 否 | 能 | 否 |
() | 能 | 成员 | 否 | 能 | 能 |
[] | 能 | 成员 | 否 | 能 | 能 |
-> | 能 | 成员 | 否 | 能 | 能 |
op= | 能 | 任意 | 否 | 能 | 能 |
new | 能 | 静态成员 | 否 | 否 | void* |
delete | 能 | 静态成员 | 否 | 否 | void |
其他运算符 | 能 | 任意 | 否 | 能 | 能 |
其他成员 | 能 | 成员 | 否 | 能 | 能 |
友元 | 否 | 友元 | 否 | 否 | 能 |
C++——类继承的更多相关文章
- javascript类继承的一些实验
其实一开始编js没怎么用过对象,一般都用func,func,func···但是用多了,感觉代码一点都不美观,还要这里包一个函数,那里包一个函数,或者一直都是函数调用,不好看,而且一些重用的都要重写的话 ...
- C++ 类继承的对象布局
C++多重继承下,对象布局与编译器,是否为虚拟继承都有很大关系,下面将逐一分析其中的差别,相同点为都按照类继承的先后顺序布局(类内按照虚表.成员声明先后顺序排列).该类情况为子类按照继承顺序排列,如c ...
- C++中public,protected,private派生类继承问题和访问权限问题
C++中public,protected,private派生类继承问题和访问权限问题 当一个子类从父类继承时,父类的所有成员成为子类的成员,此时对父类成员的访问状态由继承时使用的继承限定符决定. 1. ...
- Javascript类继承-机制-代码Demo【原创】
最近看到<Javascript设计模式>,对js模拟的”继承方式“有了更深一步的了解,虽然之前也总是用到prototype.new ,但只是知其然不知所以然,现在将类继承的方法整理如下,暂 ...
- smartjs 0.2 OOP讲解 - Klass 类继承
SmartJS2.0加入OOP的功能.OOP包括klass与factory两个对象. Klass 类继承 与其他的类继承相比,smartjs使用了执行指针的概念(后面例子中会介绍),另外提供base基 ...
- cocos2dx中常见的类及类继承关系
场景:CCScene,继承自CCNode,几乎完全等于CCNode类 CCNode继承自CCObject,CCObject是真正意义上的父类,CCObject又继承自CCCopying类,CCCopy ...
- C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类
类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. //单继承的定义 class B:public A { ...
- Android(java)学习笔记118:类继承的注意事项
/* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. C:不要为了部分功能而去 ...
- Lua类和类继承实现
Lua本身是不能像C++那样直接实现继承,但我们可以用万能的table表来实现. 以下我总结了三种方式的类以及继承的实现 第一.官方的做法,使用元表实现 原理参照<Programming in ...
随机推荐
- poj1785 Binary Search Heap Construction
此题可以先排序再用rmq递归解决. 当然可以用treap. http://poj.org/problem?id=1785 #include <cstdio> #include <cs ...
- C语言typeof详解 offsetof
http://blog.chinaunix.net/uid-28458801-id-4200573.html 前言: typeof关键字是C语言中的一个新扩展,这个特性在linux内核中应用非常 ...
- CountDownLatch的原理学习
转载:http://blog.csdn.net/yanyan19880509/article/details/52349056 前言 前面介绍了ReentrantLock,又叫排他锁,本篇主要通过Co ...
- [Django_1_0]初次见面
Django 初次见面 文章将写安装和第一次使用时候的操作.文章是照着文档做的,但是以后的内容会有不一样. 安装 pip install django 我这里是使用python3的,也可以使用 pip ...
- sudo详解
一. sudo的特点 sudo扮演的角色注定了它要在安全方面格外谨慎,否则就会导致非法用户攫取root权限.同时,它还要兼顾易用性,让系统管理员能够更有效,更方便地使用它.sudo设计者的宗旨是:给用 ...
- ContentProvider官方教程(7)3种访问形式:批处理、异步访问、intent间接访问(临时URI权限)
Alternative Forms of Provider Access Three alternative forms of provider access are important in app ...
- Zabbix监控交换机设置
说明: Zabbix监控服务端已经配置完成,现在要使用Zabbix对交换机进行监控. 具体操作: 以下操作在被监控的交换机上进行,这里以Cisco交换机为例. 一.登录到Cisco交换机,开启snmp ...
- Eclipse / android studio 添加第三方jar包 步骤
eclipse 将第三方包放到libs文件夹后并没有引用. 基本步骤分为3步,具体介绍如下: 打开自己的Eclipse,在自己的Android工程上名上右键->Build Path ->C ...
- UVA 437 十九 The Tower of Babylon
The Tower of Babylon Time Limit:3000MS Memory Limit:0KB 64bit IO Format:%lld & %llu Subm ...
- 将NuGet配置到环境变量中
https://docs.nuget.org/consume/command-line-reference Installing The NuGet command line may be insta ...