继承中的类的作用域
每个类定义自己的作用域,在这个作用域中定义自己的成员。当存在继承关系时,派生类的作用域嵌套在基类的作用域之中。如果一个名字在派生类的作用域中无法解析,那么编译器将继续在外层的基类中寻找该名字的定义。
继承关系如下:
我们看下面代码:
Child obj;
obj.fun();
我们通过Child类型的obj去调用fun函数,所以我们首先在Child类域中查找,如果没找到。
因为Child继承于Teacher,Child是Teacher的派生类,所以接下来我们继续在Teacher的类域中查找fun函数,如果没找到。
因为Teacher继承于Person,Teacher是Person的派生类,所以接下来我们继续在Person的类域中查找。一直找到最终的基类。
编译时的名字搜索
一个对象、引用、指针的静态类型决定了该对象的哪个成员是可见的,即使它的静态类型与动态类型可能不一致,但是它能使用哪个类型依然是由静态类型决定的
举个例子:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Teacher : public Person
{
public:
int _No; // 学号
void fun() const
{
cout << _No;
}
};
我们只能通过Teacher及其派生类的对象、指针、引用去访问fun函数
Teacher obj;
Teacher* obj_Tea = &obj; //静态类型与动态类型一致
Person* obj_Per = &obj; //静态类型与动态类型不一致
obj_Tea->fun(); //正确、类型为Teacher
obj_Per->fun(); //错误、类型为Person
虽然obj之中确实是有一个名字为fun的函数,但是这个成员对于obj_Per是不可见的。obj_Per的类型是一个Person类型,那么就意味着对于fun的的搜索是从Person开始的,显然Person类中没有fun函数,所以我们无法通过Person的对象、指针、引用去调用fun。
名字冲突与继承
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
运行结果:
子类成员中有_num,父类成员中也有_num,所以正常在子类里面访问_num,会隐藏父类继承来的_num,而访问子类本身有的成员,如果要访问这个隐藏的成员,需要在前面加上Person::
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();//调用父类的
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
运行结果:
B中的fun和A中的fun不是构成重载,因为不是在同一作用域
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
如果有隐藏默认调用自身所在类的,如果需要调用父类的,就加上 父类::(A::)
这种方式叫:使用作用域运算符来使用隐藏的基类成员
作用域运算符将覆盖掉原有的查找规则,并指示编译器从指定类的作用域开始查找成员
派生类的成员将隐藏同名的基类成员
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
理解函数调用的解析过程对于理解C++继承至关重要:
如果我们需要调用一个函数:obj.fun();
1.我们首先要确定obj的静态类型,因为我们调用的是一个成员,所以该类型必然是类类型
2.在obj静态类型对应的类中查找fun,如果找不到,则依次在直接基类中不断查找直到到达继承链的顶端,如果找完了还找不到,编译器则会报错
3.一旦找到了fun,先进行常规的类型检查,以确定找到的fun合法
4.调用合法,编译器将根据调用的是否是虚函数而产生不同的代码
- 如果fun是虚函数且是通过指针或引用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据的是对象的动态类型
- 如果不是虚函数,是通过对象(而非引用或指针)进行调用,编译器将产生一个常规函数调用
名字查找优先类型检查
声明在内层作用域的函数并不会重载声明在外层作用域的函数、因此,定义在派生类中的函数也不会重载其基类中的成员
如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,那么派生类将在其作用域内隐藏该基类成员,即使该派生类成员与基类成员的形参列表不一致,基类成员仍然会被隐藏。
派生类的默认成员函数
派生类的构造函数
派生类对象中含有从基类继承过来的成员,但是派生类并不能直接初始化这些成员,派生类必须使用基类的构造函数初始化它的基类部分。
派生类对象的基类部分与它自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。派生类构造函数同样是通过构造函数初始化列表来讲实参传递给基类的构造函数。
且看下面分析:
class Person
{
public:
Person(const string name,const string sex,int age)
:_name(name),_sex(sex),_age(age){}
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Teacher : public Person
{
public:
Teacher(const string name, const string sex, int age,int No)
:Person(name,sex,age),_No(No)
{}
int _No; // 学号
};
Teacher自己的构造函数,将前三个参数传递给Person的构造函数,Person的构造函数负责初始化Teacher的基类部分,接下来初始化派生类自己定义的成员,最后运行Teacher空的函数体。
除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化,如果你想使用其他基类的构造函数,我们需要以类名加圆括号内的实参列表的形式来为构造函数提供初始值。这些实参将告诉编译器到底使用哪一个构造函数来初始化派生类的基类部分。
首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员
1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
2、子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构
默认成员函数规则总结:
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
派生类的声明
派生类的声明与其他类差别不大,声明包含类名但是不包含它的派生列表
class Student : Person; //错误:派生列表不能出现在声明
class Student; //正确声明方式
一条声明语句的目的是令程序知晓某个名字的存在,以及该名字表示一个什么样的实体。派生列表以及定义有关的细节必须与类的主体一起出现。
用作基类的类
如果我们想将某个类用作基类,那么这个类必须已经定义而非只声明
一个类是基类,同时它也可以是一个派生类,但是一个类不能派生它自己。
class Person{...};
class Teacher : private Person{...};
class Child : public Teacher{...};
在这个继承关系中,Person是Teacher的直接基类,同时也是Child的间接基类。
每个类都会继承直接基类的所有成员。最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。
继承与友元
就像友元关系不能传递一样,友元关系同样不能继承。基类的友元在访问派生类的成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
Display是基类Person的友元,cout << s._stuNum << endl;这条语句想要访问Students的受保护成员_stuNum,显然是不可以的。基类友元不能访问派生类私有和保护成员。
不能继承友元关系,每个类负责控制各自成员的访问权限
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不论从基类中派生多少个派生类出来,对于每个静态成员来说都只存在唯一实例。
class Person
{
public:
static string _age;
};
静态成员遵循通用的访问规则,如果基类中成员是private,那么派生类无权访问它。假设某静态成员是可访问的,那么我们既可以通过基类也能通过派生类使用它。
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀