继承
1.继承
简单理解:子类从父类继承成员变量和成员函数
父类中所有非静态成员属性都会被子类继承下去,和java一致
父类中的private私有成员,是被编译器隐藏了,但是被继承了。
#include <iostream>
using namespace std;
class People
{
private:
string name;
int age;
public:
void setName(string name);
void setAge(int age);
string getName() const;
int getAge() const;
};
void People::setName(string name)
{
this->name = name;
}
void People::setAge(int age)
{
this->age = age;
}
string People::getName() const
{
return this->name;
}
int People::getAge() const
{
return this->age;
}
class Student : public People
{
public:
void setScore(int score);
int getScore() const;
private:
int score;
};
void Student::setScore(int score)
{
this->score = score;
}
int Student::getScore() const
{
return this->score;
}
void test()
{
Student stu;
stu.setScore(90);
stu.setName("zhangsan");
stu.setAge(20);
cout << stu.getName() << stu.getAge() << stu.getScore() << endl;
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
2.三种继承方式,public/protected/private
三种方式差异:
1) public继承方式
- 基类中所有 public 成员在派生类中为 public 属性;
- 基类中所有 protected 成员在派生类中为 protected 属性;
- 基类中所有 private 成员在派生类中不能使用。
2) protected继承方式
- 基类中的所有 public 成员在派生类中为 protected 属性;
- 基类中的所有 protected 成员在派生类中为 protected 属性;
- 基类中的所有 private 成员在派生类中不能使用。
3) private继承方式
- 基类中的所有 public 成员在派生类中均为 private 属性;
- 基类中的所有 protected 成员在派生类中均为 private 属性;
- 基类中的所有 private 成员在派生类中不能使用。
简单,通过下面图来看下差异:
基类成员属性 | public继承方式 | protected继承方式 | private继承方式 |
---|---|---|---|
public | public | -> protected | -> private |
protected | protected | protected | -> private |
private | -> 不可访问 | -> 不可访问 |
在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。
3.通过using改变访问的权限
可以改变基类成员在子类中的访问权限,将 public 改为 private、将 protected 改为 public。
注意:private不能修改,private修饰的基类在子类中无法使用和查询到。
#include <iostream>
using namespace std;
class People
{
protected:
string name;
int age;
public:
void show() const;
void learning() const;
};
void People::show() const
{
cout << this->name << this->age << endl;
}
void People::learning() const
{
cout << this->name << this->age << endl;
}
class Student : public People
{
public:
using People::age; //将protected改为public
using People::name; //将protected改为public
float score;
private:
using People::show; //将public改为private
public:
void learning() const;
void show() const;
};
void Student::learning() const
{
cout << this->name << this->age << this->score << endl;
}
void test()
{
Student stu;
stu.name = "zhangsan";
stu.age = 20;
stu.score = 30.0f;
// stu.show(); //错误,权限调整为private
//使用的是派生类新增的成员函数,而不是从基类继承的
stu.learning();
//使用的是从基类继承来的成员函数
stu.People::learning();
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
4.子类成员与基类成员重名
与java一致,默认会使用自己的成员,而不是基类的成员。
//使用的是派生类新增的成员函数,而不是从基类继承的
stu.learning();
//使用的是从基类继承来的成员函数
stu.People::learning();//需要加作用域
注意:基类成员函数和派生类成员函数不构成重载。 同名就会被覆盖,默认执行自己的成员
5.类继承时的作用域嵌套
1.子类是嵌套在父类中的,首先会在自己的类中查找,如果找到,结束,如果找不到,则去父类中查找,
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class A{
public:
virtual void funA(){
cout<<"A"<<endl;
}
};
class B {
public:
virtual void funA(int a){
cout<<"B"<<endl;
}
};
class C: public A,public B{
};
int main() {
C c;
c.funA(7);//报错,不知道找A还是B
}
int main(){
C c;
A *a = &c;
a->funA();//正确,找A
}
a的静态类型对应的类(也就是类A)的作用域中查找funA
6.构造函数和析构顺序
#include <iostream>
using namespace std;
class Base
{
public:
Base();
~Base();
};
Base::Base()
{
cout << "Base创建了" << endl;
}
Base::~Base()
{
cout << "Base被析构了" << endl;
}
class A :public Base
{
public:
A();
~A();
};
A::A()
{
cout << "A创建了" << endl;
}
A::~A()
{
cout << "A被析构了" << endl;
}
void test()
{
A a;
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
Base创建了
A创建了
A被析构了
Base被析构了
请按任意键继续. . .
总结:与类中其他类对象一致,都是其他类对象先创建,本身再创建, 析构时,是本身类先析构,其他类对象后析构。
另外注意,创建A对象时,其实父类也会被创建,不然怎么析构了。
7.同名静态重名
#include <iostream>
using namespace std;
class Base
{
public:
static int age;
static void func()
{
}
static void func(int num)
{
}
};
int Base::age = 200;
class A : public Base
{
public:
static int age;
static void func()
{
}
};
int A::age = 10;
void test()
{
//类对象
A a;
a.age;
a.Base::age;
//类名
A::age;
Base::age;
A::Base::age;
//类静态函数重名
a.func();
a.Base::func();
A::func();
A::Base::func();
Base::func(20);
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
8.多继承语法
容易产生二义性。不仅仅成员会出现二义性,而且子类会出现二义性,是骡子还是马就搞不清楚了。
1.成员二义性
#include <iostream>
using namespace std;
class Tiger
{
public:
static int age;
};
int Tiger::age = 20;
class lion
{
public:
static int age;
};
int lion::age = 30;
class Dog :public Tiger,public lion
{
};
void test()
{
Dog d;
// d.age;//会产生二义性,不清楚导是Tiger还是lion的
d.Tiger::age;
d.lion::age;
}
int main(int argc, char const *argv[])
{
system("pause");
return 0;
}
2.类二义性,特别是菱形继承
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WG1J9ALD-1625408289973)(G:\markdown\C++\C++基础\草泥马.png)]
这就是所谓的菱形继承,动物有年龄,骡子也有,马也有,当马修改了年龄,则骡子的年龄修不修改。
#include <iostream>
using namespace std;
class Animal
{
public:
int age;
};
class Mule : public Animal
{
};
class horse : public Animal
{
};
class GrassHorse : public Mule, public horse
{
};
void test()
{
GrassHorse g;
g.Mule::age = 15;
g.horse::age = 30;
cout << g.Mule::age << endl;
cout << g.horse ::age << endl;
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
15
30
为什么会出现这种情况,从构造函数和析构顺序,我们知道,子类是会继承父类所有成员,所以本质上骡子保存了一份Animal的年龄,horse也保存了一份Animal的年龄,再修改骡子的年龄时,马的年龄并不会因此改变。
3.如何解决多继承带来地是菱形二义性问题
class Mule : virtual public Animal
{
};
class horse : virtual public Animal
{
};
骡子和马都加上virtual虚继承,
为什么添加virtual虚继承之后,就不会出现问题了。
直接给出结论: Mule加上了virtual,那么Mule只会保存一份vbptr地指针,该指针指向了父类,同样,horse也是如此, 二者修改数据时,其实修改地是它们父类Animal地年龄,此时不会产生二义性。
总结:再开发中,尽量不要使用多继承,再java/go等面向对象开发语言中,多继承是被抛弃地概念。
9.借助指针突破访问权限private的限制
通过上面继承的讲解,我们知道子类会继承父类所有成员,即使private也会继承,子类是嵌入到父类中,创建子类,其实本质父类也会创建。父类private是被编译器隐藏的,导致子类无法访问,那么有没有办法突破private的限制。
答案是有的,还记得前面说过,如何计算一个类的字节大小,这里不在介绍了。我们根据字节的计算,来知道各个属性的位置。然后通过修改该段内存空间调整值。
++x有的编译器直接指向下一个成员,有的不是。跟编译器有关
#include <iostream>
using namespace std;
class A
{
private:
int a;
int b;
char e;
int c;
public:
A(int a, int b, char e, int c);
int getA() const;
};
A::A(int a, int b, char e, int c)
{
this->a = a;
this->b = b;
this->e = e;
this->c = c;
}
int A::getA() const
{
return this->a;
}
void test()
{
A d(10, 20, 'a', 30);
int *x = (int *)&d; //指向d首地址
cout << *x << endl;
*x = 100;
cout << d.getA() << endl;
++x;//自动指向像一个成岩
int *y = x;
cout << *y << endl;
char *z = (char *)++x;
cout << *z << endl;
++x;
int *w = x;
cout << *w << endl;
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
10.虚基类必须派生类初始化
规定:C++干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。避免产生二义性.
构造函数执行顺序:虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。
总结:虚基类必须有派生类初始化,而且必须第一个初始化,不按照构造顺序.
#include <iostream>
using namespace std;
//虚基类A
class A{
public:
A(int a);
protected:
int m_a;
};
A::A(int a): m_a(a){ }
//直接派生类B
class B: virtual public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//直接派生类C
class C: virtual public A{
public:
C(int a, int c);
public:
void display();
protected:
int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}
//间接派生类D
class D: public B, public C{
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
B b(10, 20);
b.display();
C c(30, 40);
c.display();
D d(50, 60, 70, 80);
d.display();
return 0;
}
D:😄(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ } // A(a)必须要构建
11.派生类赋值给基类
1.案例
#include <iostream>
using namespace std;
class Base
{
public:
int a;
public:
Base(int a);
void display();
};
void Base::display()
{
cout << this->a << endl;
}
Base::Base(int a)
{
this->a = a;
}
class B : public Base
{
public:
int b;
public:
B(int a, int b);
void display();
};
B::B(int a, int b) : Base(a), b(b)
{
}
void B::display()
{
cout << this->a << this->b << endl;
}
void test()
{
Base base(10);
B b(20, 30);
base.display();
b.display();
base = b;//赋值,
base.display();//执行的还是Base类,修改了Base类的值
b.display();
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
10
2030
20
2030
请按任意键继续. . .
2.本质分析
**赋值的本质:**赋值的本质是将现有的数据写入已分配好的内存空间中,对象的内存只包含了成员,不包含成员函数(所有函数只有一份,可以前面的章节详细了解),所以不存在成员函数赋值问题.
Base:内存空间四字节;
B:内存空间8字节.
base = b;// 该过程,是将B的8字节内存空间数值写入到Base四字节空间,显然是装不下的.
此时,编译时会自动将B的基类成员进行赋值,舍弃B的成员.所以最后只是将B的基类成员写入到Base的内存空间
注意:这种舍弃,是不可逆的.
总结:派生类不能为赋值给基类,否则派生类会舍弃自身的成员, 基类可以赋值给派生类.
12.将派生类指针赋值给基类指针时到底发生了什么?
案例:
#include <iostream>
using namespace std;
class Base
{
public:
int a;
public:
Base(int a);
void display();
};
void Base::display()
{
cout << this->a << endl;
}
Base::Base(int a)
{
this->a = a;
}
class B : public Base
{
public:
int b;
public:
B(int a, int b);
void display();
};
B::B(int a, int b) : Base(a), b(b)
{
}
void B::display()
{
cout << this->a << this->b << endl;
}
void test()
{
Base *base = new Base(10);
B *b = new B(20, 30);
base->display();//10
b->display();//2030
base = b;//base指针指向b
base->display();//20,原来是10
cout << base->a << endl;//20,值修改了
}
int main(int argc, char const *argv[])
{
test();
system("pause");
return 0;
}
10
2030
20
20
发现:通过上面,我们发现base->display()调用的Base的成员函数,而base->a却是使用的B b的成员,为什么会发生这种现象.
结论:
- 1.指针base = b;与对象赋值不同,指针的赋值,是指针改变了方向,指向另外一个内存空间.base指针指向b的内存空间. 隐式指针 this 发生了变化
- 2.编译器是通过类型来访问成员函数的,而不是所指的具体对象.
- 3.成员函数,漏掉了一点,访问的成员函数属于父类,则首先调用父类的成员,反之,成员函数属于子类,则首先调用子类的成员。(多态时,解答该问题)。
所以,base指针指向b的内存空间,可以访问b的成员,但是base的类型没有变化,编译器依旧会让器指向Base类型的成员函数.
se(a), b(b)
{
}
void B::display()
{
cout << this->a << this->b << endl;
}
void test()
{
Base *base = new Base(10);
B *b = new B(20, 30);
base->display();//10
b->display();//2030
base = b;//base指针指向b
base->display();//20,原来是10
cout << base->a << endl;//20,值修改了
}
int main(int argc, char const *argv[])
{
test();
system(“pause”);
return 0;
}
10
2030
20
20
**发现:通过上面,我们发现base->display()调用的Base的成员函数,而base->a却是使用的B b的成员,为什么会发生这种现象.**
结论:
- 1.指针base = b;与对象赋值不同,指针的赋值,是指针改变了方向,指向另外一个内存空间.base指针指向b的内存空间. 隐式指针 this 发生了变化
- 2.编译器是通过类型来访问成员函数的,而不是所指的具体对象.
- 3.成员函数,漏掉了一点,访问的成员函数属于父类,则首先调用父类的成员,反之,成员函数属于子类,则首先调用子类的成员。(多态时,解答该问题)。
所以,base指针指向b的内存空间,可以访问b的成员,但是base的类型没有变化,编译器依旧会让器指向Base类型的成员函数.