1. 派生类的访问权限
- 类成员(使用的访问限定符)有public、protected、private三种,继承方式也有public、protected、private三种。public的类成员可以在类外进行访问,而protected、private的类成员无法在类外访问。类成员访问权限顺序为:public > protected > private。
- 基类的私有成员都会被派生类继承,但是在派生类中无法使用。定义成保护的成员在派生类中可以访问,但是在类外不可以访问。基类的成员在派生类的访问方式为类成员的访问限定符和该类的继承方式中访问权限最小的。实际中一般将基类的成员定义成public或protected,而派生类在继承时通过public方式继承。
2. 函数隐藏
- 若派生类(子类)中含有与基类(父类)同名的变量(子类可以访问这个变量),子类会优先访问自己局部域中的该变量,若要访问父类的该同名变量需要指定作用域。同名成员函数也具有类似的性质,这种方式称为隐藏或称重定义。只要函数名相同,就构成隐藏(无论参数和返回值是否相同)。不构成重载,重载需要有共同的作用域,当然若在子类指定父类的类域,仍可构成重载。
#include <iostream>
#include <string>
class Animal
{
public:
Animal(std::string period = "Moderm Times")
:_period(period)
,_age(1)
{}
void PrintPeriod()
{
std::cout << _period << std::endl;
}
protected: //基类中保护的成员变量和成员函数在子类中可以访问,而在类外不可以使用。
int _age;
private: //基类中私有的成员变量和成员函数在子类中和在类外都不可以访问。(都会被子类继承,但是用不了)
std::string _period;
};
class Person :public Animal
{
public:
Person(std::string AnimalClass = "Primates")
:_AnimalClass(AnimalClass)
,_age(10)
{}
void PrintAnimalClass()
{
std::cout << _AnimalClass << std::endl;
std::cout << _age << std::endl; //成员变量的隐藏
std::cout << Animal::_age << std::endl; //指定父类作用域访问父类同名变量
}
void PrintPeriod(std::string str) //成员函数的隐藏
{
std::cout << str << ", hello world" << std::endl;
}
protected:
int _age;
private:
std::string _AnimalClass;
};
int main()
{
Person teacher;
teacher.PrintPeriod("Person");
// teacher.PrintPeriod(); //看似调用的是父类的成员函数,但是实际无法成功。子类中该同名的函数被隐藏。
teacher.Animal::PrintPeriod(); // 访问子类中隐藏的父类函成员数可以指定作用域。
teacher.PrintAnimalClass();
return 0;
}
运行结果:
Person, hello world
Moderm Times
Primates
10
1
3. 继承的默认成员函数
-
构造和析构:
- 对于从父类继承下来的成员变量,会调用父类自己的析构和构造
- 对于子类自定义的,处理方式和定义普通类一样:标准库内置类型的变量不处理,自定义类型变量调用其默认构造、析构函数。
-
拷贝构造和赋值运算符重载:
- 从父类继承下来的成员变量,调用父类自己的拷贝和operator=
- 子类自定义的,处理方式和定义普通类一样。
-
以下情况需要自子类自己写构造、析构、拷贝、赋值重载:
- 子类自定义的变量需要申请内存执行深拷贝,结束后要释放资源
- 父类中没有相应的构造等函数可用
#include <iostream>
#include <string>
class A
{
public:
A(std::string str = "I am A")
:_str(str)
,_p(new int)
{
std::cout << "A Constructor" << std::endl;
}
~A()
{
std::cout << "A Deconstructor" << std::endl;
delete _p;
}
A(const A& a)
{
std::cout << "A Copy Constructor" << std::endl;
}
A& operator=(const A& a)
{
std::cout << "A operator=" << std::endl;
return* this;
}
protected:
std::string _str;
int* _p;
};
class B :public A
{
public:
B(const char* str = "I am B")
:A(str) //调用A的构造函数初始化A的成员变量。不能这样:_str(str),因为_str不是B的成员变量也不是静态变量。
,_valB(0)
{
std::cout << "B Constructor" << std::endl;
}
B(const B& b) //拷贝构造
:A(b) // 调用父类的拷贝构造,会自动切片
, _valB(b._valB)
{
std::cout << "B Copy Constructor" << std::endl;
}
B& operator=(const B& b)
{
if (this != &b)
{
A::operator=(b); //指定父类作用域,否则会就近无限调用自己的赋值
}
std::cout << "B operator=" << std::endl;
return *this;
}
~B()
{
//A::~A(); //调用父类的析构,注意需要指定作用域
//原因:子类的析构函数名字会被统一处理成destructor()这个名字。子类的析构函数和父类的析构函数就构成隐藏。
//注意:这里不能显示调用,因为子类析构函数结束时,会自动调用父类的析构函数,这样同一份资源就被析构了两次
std::cout << "B Deconstructor" << std::endl;
}
private:
int _valB;
};
int main()
{
B b;
B b1 = b;
B b2;
b2 = b;
return 0;
}
运行结果:
A Constructor # 继承类创建的对象,现有父类对象后有子类,所以析构先析构子类,后析构父类(先析构最后创建的对象)
B Constructor
A Copy Constructor
B Copy Constructor
A Constructor
B Constructor
A operator=
B operator=
B Deconstructor
A Deconstructor
B Deconstructor
A Deconstructor
B Deconstructor
A Deconstructor
4. 继承的切片
- 对于公有(public)继承的类,派生类对象可以给基类对象赋值(基类 = 派生类,为了叙述方便可以定义成父 = 子),这种赋值方式称为切片(实际上是将子类中含有父类的成员变量赋值给父类对象);但是基类无法给派生类对象赋值(使用指针或引用可以强制操作,但是通过父类对象访问子类自定义的成员变量时会越界)。对于protected 和 private的继承方式,实际上会将成员变量的访问权限缩小,若执行切片会有"权限放大"的问题。
#include <iostream>
#include <string>
class Base
{
public:
Base(std::string strBase = "I am Base")
:_strBase(strBase)
, _BaseVar1(0)
, _BaseVar2(0)
{}
void Greeting()
{
std::cout << "hello, i am your daddy!" << std::endl;
}
protected:
int _BaseVar1;
int _BaseVar2;
void PrintBase()
{
std::cout << _strBase << std::endl;
}
private:
std::string _strBase;
};
class DerivedA :public Base
{
public:
DerivedA(std::string strDerivedA = "I am DerivedA")
:_strDerivedA(strDerivedA)
, _DerivedAVar1(0)
{}
void PrintDerivedA()
{
Greeting();
PrintBase();
std::cout << _strDerivedA << std::endl;
}
protected:
int _DerivedAVar1;
private:
std::string _strDerivedA;
};
int main()
{
DerivedA DA1;
// 父 = 子 -> 切片
Base B = DA1; //调用拷贝构造
Base* pB = &DA1; // pB的成员实际指向的是DA1中,含有父类成员的部分
Base& refB = DA1; //refB的成员实际是DA1中,含有父类成员的别名。
// 子 = 父 (越界)
DerivedA* pDA = (DerivedA*)&B;
//将B强制看成了DerivedA那么大的空间;不能访问_DerivedAVar1,会越界。(c++11 dynamic cast)
DerivedA& refDA = (DerivedA&)B;
DA1.PrintDerivedA();
return 0;
}
5. 菱形继承
- 一个子类只有一个直接父类——单继承;若有两个以上的父类——多继承。
- 在菱形继承中,对于子类从父类中继承的变量二义性问题,可以通过指定作用域进行访问进行解决。
- 对于数据冗余问题:可使用虚继承解决,在DerivedA、DerivedB继承的时候使用virtual。
- 继承数据的存储:先继承的数据在前,后继承的在后;
#include <iostream>
#include <string>
class Base
{
public:
int _BaseVar;
};
class DerivedA : virtual public Base
{
public:
int _DerivedAVar;
};
class DerivedB : virtual public Base
{
public:
int _DerivedBVar;
};
class Grandchild :public DerivedA, public DerivedB
{
public:
int _GrandchildBVar;
};
int main()
{
Grandchild ch;
printf(" ch._DerivedAVar: %p\n", &ch._DerivedAVar); //先继承类成员变量的地址在前
printf(" ch._DerivedBVar: %p\n", &ch._DerivedBVar);
printf(" ch._GrandchildBVar: %p\n", &ch._GrandchildBVar);
printf(" ch._BaseVar: %p\n", &ch._BaseVar); // 虚继承的成员变量放在最后,同一个地址
printf(" ch.DerivedA::_BaseVar: %p\n", &ch.DerivedA::_BaseVar);
printf(" ch.DerivedB::_BaseVar: %p\n", &ch.DerivedB::_BaseVar);
return 0;
}
运行结果:
6. 其他
- 父类的友元函数无法被子类继承。
- 对于父类的static静态成员,子类和父类中只有一个这样的成员,访问时会共同访问这个静态成员。