1、继承方式
-
类的一个特征就是封装,public和private作用就是实现这一目的。所以:
类外可以访问public成员而不能访问private成员;private成员只能由类内和友元访问。 -
类的另一个特征就是继承,protected的作用就是实现这一目的。所以:
protected成员可以被派生类对象访问,不能被类外访问。
基类的 x 成员,通过 y 继承,得到派生类的 z 成员。
注意:z = min ( x , y )
例如:x = public , y = protected, z = protected .
特例:x = private , y = private , z = private .
2、切片
切片:派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
切片的时候,把派生类中基类那一部分直接赋值过去。
基类对象不能赋值给派生类对象。
基类的指针可以通过强制类型转换赋值给派生类的指针,但必须是基类的指针是指向派生类对象时才是安全的;如果基类指针指向基类对象,虽然可以,但是会存在越界访问。
举例:
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ; //定义了一个派生类对象
// 1.派生类对象可以赋值给基类的 对象/指针/引用
Person pobj = sobj ; //派生类对象赋值给基类的对象
Person* pp = &sobj; //派生类对象赋值给基类的指针
Person& rp = sobj; //派生类对象赋值给基类的引用
// 2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj; // 基类指针指向派生类对象的条件下:
Student* ps1 = (Student*)pp;
//这种情况转换时可以的:基类的指针可以通过强制类型转换赋值给派生类的指针
ps1->_No = 10;
pp = &pobj; // 基类指针指向基类对象
Student* ps2 = (Student*)pp; //这种情况转换虽然可以,但是会存在越界访问
ps2->_No = 10;
}
3、重载 重写(覆盖) 重定义(隐藏)
重载:
- 同一作用域
- 相同函数名
- 不同参数列表(参数个数 / 类型 / 顺序)
- 返回值可同可不同
只有返回值不同,不能构成重载:
因为调用函数的时候无法确认函数的返回类型,所以仅返回类型不同的函数都可以匹配,这样就造成二义性,所以仅仅是返回值类型不同是不能重载的。
重写(覆盖):
- 一个在基类,一个在派生类;
- 都是虚函数,派生类可以不写virtual,但是基类必须写virtual;
- 其他都相同(函数名、参数列表、返回值)。
特例:协变
派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A
{};
class B : public A
{};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
特例:析构函数的重写
基类与派生类析构函数的名字不同。
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,
// 下面的delete对象调用析构函数,才能构成多态,
// 才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
运行结果:
重定义(隐藏)
- 一个在基类,一个在派生类
- 相同成员名 (成员函数 / 成员变量)
派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫重定义(隐藏)。
例:成员变量的重定义
下面的代码中student和person各自的_num构成了隐藏关系。
class Person
{
protected:
string _name = "李华"; // 姓名
int _num = 610; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名: " << _name << endl;//李华
cout << " 身份证号: " << Person::_num << endl; //610
cout << " 学号: " << _num << endl;//170
}
protected:
int _num = 170; // 学号
};
例:成员函数的重定义
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
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); //func()
//func(int i)->10
};
4、实现一个不能被继承的类
- 构造函数私有化
- final关键字
// C++98中构造函数私有化,派生类中调不到基类的构造函数,则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11给出了新的关键字final禁止继承
class NonInherit final
{};
5、友元关系 不能被继承
友元函数不是类的成员函数,而是类的接口。
如果访问类的非静态成员时,需要友元提供一个对象作为参数,才可以访问;如果访问类的静态成员时,不需要参数。
普通函数作为友元:
class Student
{
public:
Student(string n, int a)
:name(n)
,age(a)
{ }
friend void NoStatic(const Student& s); //打印所有的非静态成员
friend void Static(); //打印所有的静态成员
private:
string name;
int age;
static int num;
};
int Student::num = 170;
void NoStatic(const Student& s)
{
cout << "非静态成员:" << "name: " << s.name << " "<< "age: " << s.age << endl;
}
void Static()
{
cout << "静态成员:" << "num= " << Student::num << endl;
}
int main()
{
Student s("lihua", 18);
NoStatic(s);
Static();
system("pause");
return 0;
}
类成员中的一个成员函数,作为另外一个类的友元函数:
class B; // 因为A中形参是B,需要先声明
class A
{
public:
void change(B& b); // 在A中定义一个成员函数
};
class B
{
public:
B(int b1, int b2)
:_b1(b1)
,_b2(b2)
{ }
friend void A::change(B& b); // 声明该成员函数为B的友元函数
void print()
{
cout << "_b1:" << _b1 << " " << "_b2:" << _b2 << endl;
}
private:
int _b1;
int _b2;
};
void A::change(B& b) // 实现友元函数
{
b._b1 = 20;
}
int main()
{
B b(100, 100);
A a;
cout << "修改前:" << endl;
b.print();
a.change(b);
cout << "修改后:" << endl;
b.print();
system("pause");
return 0;
}
友元类:
如果希望一个类可以访问另一个类的非公有成员在内的所有成员(主要是非公有的成员),可以将一个类指定为另一类的友元类。
将类A定义为类B的友元类:
class B;
class A
{
public:
void print(B& b); // 在A中定义一个成员函数
};
class B
{
public:
B(int b1, int b2)
:_b1(b1)
,_b2(b2)
{ }
friend class A; // 声明A为B的友元类
private:
int _b1;
int _b2;
};
void A::print(B& b)
{
cout << "_b1:" << b._b1 << " " << "_b2:" << b._b2 << endl;
}
int main()
{
B b(100, 100);
A a;
a.print(b);
system("pause");
return 0;
}
6、菱形继承、菱形虚拟继承
菱形继承属于多继承。
// 这样会有二义性无法明确知道访问的是哪一个
D d ;
d._x = 1;
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
d.B::_x = 3;
d.C::_x = 5;
菱形继承的二义性和数据冗余的问题该怎么解决呢?
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
上面的继承关系,在B和C的继承A时使用虚拟继承,即可解决问题。
菱形虚拟继承的中间两个类都有各自的一个指针,这两个指针分别指向两个表,表中存放的是冗余数据的处理;指针是虚基表指针,表是虚基表,虚基表存的是偏移量:
举例:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
system("pause");
return 0;
}
多继承中指针偏移问题:
练习:
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
// A. p1 == p2 == p3
// B. p1 < p2 < p3
// C. p1 == p3 != p2 正确
// D. p1 != p2 != p3
system("pause");
return 0;
}
7、组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
因为继承的耦合度特别高(依赖关系强),内聚度低;
组合的耦合度低,内聚度高,代码维护性好。
继承和组合都是复用的方式:
- 如果是is-a关系,使用继承
- 如果是has-a关系,使用组合
- 如果都能用,优先使用组合