📖基础知识
1,什么是继承
- 继承就是用户从现有的类(基类)创建一个新的类(派生类)。
- 派生类继承了基类的所有功能,并且可以拥有自己的其他功能。
举个例子。
一个父类Animal,两个子类cat和dog。
以下是代码实现。
class Animal
{
public:
void eat()
{
cout<<"Animal can eat"<<endl;
}
void sleep()
{
cout << "Animal can sleep" << endl;
}
string name;
string clocur;
protected:
int age;
};
class dog :public Animal
{
public:
void dog_speak()
{
cout << "wang-wang-wang" << endl;
}
};
class cat :public Animal
{
public:
void cat_speak()
{
cout << "mi-mi-mi" << endl;
}
};
int main()
{
cat c;
c.clocur = "red";
c.name = "cat";
//c.age = 3;这样会报错,因为age是protected类型的,不能被继承
c.eat();//调父类的函数
c.cat_speak();//调子类的函数
}
在上面的示例中,Animal类是基类,而dog和cat类则是从Animal派生出来的。
- 派生的类与类的声明一起出现,后跟冒号,关键字public和派生该类的基类名称。
- 由于cat和dog是从Person派生的,因此可以从它们访问Person的所有成员数据和成员函数。
有图可以看出,我们实例化的cat类的对象c继承了Animal的成员。但是我们不能继承被保护的成员“age”。“age”虽然在c中可见,但是不可用。也就是不可修改。
2,继承的访问说明符
-
从基类创建派生类时,可以使用不同的访问说明符来继承基类的数据成员。
-
这些可以是public, protected 或 private.。
-
在上面的实例中,基类Animal被dog公开地继承了。
还是刚才的例子,我门这次实现private和protected继承
class Animal
{
//....
};
class dog :private Animal//private继承
{
//....
};
class cat :private Animal//protected继承
{
//....
};
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
3,继承的成员函数重写
-
假设基类和派生类的成员函数具有相同的名称和参数。
-
如果创建派生类的对象并尝试访问该成员函数,则只能调用派生类中的成员函数。
-
派生类的成员函数将覆盖基类的成员函数。
class Animal
{
public:
//animal类的名字的默认缺省值是dongwu
string name="dongwu";
void speak()
{
cout << "animal can speak" << endl;
}
protected:
int age;
};
class dog :public Animal
{
public:
//dog类的名字的默认缺省值是gou
string name="gou";
void speak()
{
cout << "dog can speak" << endl;
}
protected:
int age;
};
int main()
{
dog d;
cout << d.name << endl;
d.speak();
return 0;
}
运行结果
在上述代码中,基类和派生类有同名的函数speak,和相同的成员name,我创建了派生类的对象d,并且想要输出d(派生类)的speak()和name。结果是输出了派生类的值,没有输出基类的值,也就是说派生类对基类形成了“重写”。(也叫隐藏)
📖继承的访问权限
1,访问控制和继承
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
-
一个派生类继承了所有的基类方法,但下列情况除外:
-
1,基类的构造函数、析构函数和拷贝构造函数。
-
2,基类的重载运算符。
-
3,基类的友元函数。
接下来我们用代码证明上述的结论。
class Person
{
public:
//构造函数
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
//拷贝构造函数
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
//运算符重载
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
//析构函数
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
//构造函数
Student(const char* name = "", int num = 0)
:_num(num)
, Person(name)
{
cout << "Student(const char* name = "", int num = 0)" << endl;
}
//拷贝构造函数
Student(const Student& s)
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
// 运算符重载 s1 = s3
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
// 父子类的析构函数构成隐藏关系
// 原因:下一节多态的需要,析构函数名统一会被处理成destructor()
// 为了保证析构顺序,先子后父,
// 子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
//析构函数
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}// -》自动调用父类析构函数
protected:
int _num; //学号
};
/*子类构造函数原则:
a、调用父类构造函数初始化继承自父类成员
b、自己再初始化自己的成员 -- 规则参考普通类
析构、拷贝构造、复制重载也类似*/
int main()
{
Student s1("李四", 1);
cout << "------" << endl;
Student s2(s1);
cout << "------" << endl;
Student s3("王五", 2);
s1 = s3;
return 0;
}
运行结果如下图
由图可以看出,
创建派生类对象s1,先后调用了父类的构造函数,子类的构造函数
拷贝构造派生类对象s2,先后调用了父类的拷贝构造函数,子类的拷贝构造函数
赋值函数也就是运算符重载,先后调用了父类的operator=,子类的operator
我们可以总结结论
子类构造函数原则:
a、调用父类构造函数初始化继承自父类成员
b、自己再初始化自己的成员 – 规则参考普通类
析构、拷贝构造、复制重载也类似
- 这也就证明了最开始给出的结论,派生类可以继承基类的所有方法,但是构造函数,析构函数,拷贝构造函数,重载不可以被继承,这些函数还要再派生类中重新实现。(因为不能继承,所以必须再写一次)
📖多重继承
1,多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
我们来看个例子
class animal
{
//...
};
class dog
{
//...
};
//erha继承了2个父类
class erha :public animal, public dog
{
//...
};
多继承是C++独有的一种继承方式,这种继承方式是c++的一个大坑!!!如果不是特殊情况,最好不要使用多继承!!!
接下来我举个多继承大坑的例子
class base1
{
public:
void someFunction()
{
//.... ... ....
}
};
class base2
{
void someFunction()
{
// .... ... ....
}
};
class A : public base1, public base2
{
};
int main()
{
A a;
a.someFunction() // 错误!
}
上述的代码不能正常运行,因为a是派生类A的实例化对象,A继承的两个基类有同名的函数someFunction,a去调用a.someFunction()时,编译器不知道要调用那个函数,所以报错了。
- 要想解决这个问题,可以在调用函数时写清楚要调用那个函数所类。
2,多层继承
在C ++编程中,不仅可以从基类派生一个类,还可以从派生类派生一个类。这种继承形式称为多层继承。
class A
{
//...
};
class B:public A
{
//...
};
class C :public B
{
//...
};
这里,类B派生自基类A,类C派生自派生类B。
然后我们实际运行一个程序
以下是代码
class A
{
public:
void test()
{
cout << "这是A的test函数" << endl;
}
};
class B:public A
{
//...
};
class C :public B
{
//...
};
int main()
{
C c;
c.test();
return 0;
}
以下是运行结果
在这个程序中,C类是从B类派生出来的(B类是从基类A派生出来的)。
C类的c对象在main()函数中定义。
调用test()函数时,将执行类A中的test()。 这是因为在C类和B类中没有test()函数。
编译器首先在类C中查找test()函数。由于该函数在该类中不存在,因此它将在类B中查找该函数(因为C是从B派生的)。
这个test()函数在B类中也不存在,所以编译器在A类中寻找它(因为B是从A派生出来的)。
如果C中存在test()函数,则编译器将覆盖类A的test()(因为成员函数覆盖)。
📖继承和组合
我先举一个例子,让大家看看什么是组合
//这是组合
class A
{
//...
};
class B
{
A a;
};
//这是继承
class C
{
};
class D :public C
{
//...
};
继承和组合都完成了类的复用
-
组合是一种黑盒测试,A对B是不透明的,A保持着他的封装
-
继承是一种白盒测试(透明),C对D是透明的,在一定程度上破坏了父类的封装性。
-
如果要比较谁更好,结果是组合更好。因为程序追求的是“高内聚,低耦合”,组合的封装性更强,程序间的耦合度就越低,更符合程序的要求。
📖虚函数
讲虚函数之前,我们先来看一个例子
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : 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;
return 0;
}
以下是内存的存储结构
改变成虚函数后的代码
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;
return 0;
}
以下是内存存储结构图
然后我们画一下这几个类的继承关系