C++中的继承是类与类之间的关系,是一个很简单很直观的概念,例如:儿子继承爸爸的财产。
继承是一个类获取另一个类的成员函数和变量的过程,分为三种
类继承格式
class 派生类 : [ public | protected | private ] 基类
{
...
}
public继承(公有继承)
1. 基类的private、public和 protected 成员的访问属性在派生类中保持不变;
2. 派生类中继承的成员函数可以直接访问基类中的所有成员,派生类中新增的成员函数只能访问基类的 public和protected 成员,不能访问基类的 private 成员;
3. 通过派生类的对象只能访问基类的 public 成员。
protected继承(受保护继承)
1. 基类的 public 和 protected 成员都以protected 身份出现在派生类中;
2. 派生类中新增的成员函数可以直接访问基类中的 public 和 protected 成员,但不能访问基类的 private 成员;
3. 通过派生类的对象不能访问基类中的任何成员。
privated继承(私有继承)
1. 基类的 public 和 protected 成员都以 private 身份出现在派生类中;
2. 派生类中新增的成员函数可以直接访问基类中的 public和 protected 成员,但不能访问基类的private成员;
3. 通过派生类的对象不能访问基类中的任何成员。
测试代码
#include<iostream>
using namespace std;
//基类
class Person
{
int m_age;
protected:
char m_name[20];
public:
Person(int age = 0, const char* name = nullptr)
{
m_age = age;
strcpy_s(m_name, name);
}
~Person() {}
void set_age(int age) { m_age = age; }
void set_name(char* name) { strcpy_s(m_name, name); }
};
//派生类
class Student :public Person
{
int m_no;
public:
Student(int age = 0, const char* name = nullptr, int no = 0): Person(age, name), m_no(no) {}
void set_no(int no) { m_no = no; }
void get_name() { cout << m_name << endl; }//新增的成员函数可以访问public和protected
//void get_age() { cout << m_age << endl; } //不可访问
};
//派生类
class Teacher :protected Person
{
string m_addr;
public:
//void show() { cout << m_name << m_age << endl; } //报错
void set_add(string addr) { m_addr = addr; }
};
class Boss :private Person
{
string m_job;
public:
void set_job(string job) { m_job = job; }
//void show() { cout << m_name << m_age << endl; } //报错
};
int main()
{
return 0;
}
继承的对象模型
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void* operator new(size_t size) // 重载new运算符。
{
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << ",大小是:" << size << endl;
return ptr;
}
void operator delete(void* ptr) // 重载delete运算符。
{
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
cout << "释放了内存。\n";
}
class A { // 基类
public:
int m_a = 10;
protected:
int m_b = 20;
private:
int m_c = 30;
public:
A() {
cout << "A中this指针是: " << this << endl;
cout << "A中m_a的地址是:" << &m_a << endl;
cout << "A中m_b的地址是:" << &m_b << endl;
cout << "A中m_c的地址是:" << &m_c << endl;
}
void func() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c << endl; }
};
class B :public A // 派生类
{
public:
int m_d = 40;
B() {
cout << "B中this指针是: " << this << endl;
cout << "B中m_a的地址是:" << &m_a << endl;
cout << "B中m_b的地址是:" << &m_b << endl;
//cout << "B中m_c的地址是:" << &m_c << endl;
cout << "B中m_d的地址是:" << &m_d << endl;
}
void func1() { cout << "m_d=" << m_d << endl; }
};
int main()
{
cout << "基类占用内存的大小是:" << sizeof(A) << endl;
cout << "派生类占用内存的大小是:" << sizeof(B) << endl;
B* p = new B;
p->func(); p->func1();
// memset(p, 0, sizeof(B));
delete p;
}
我们可以发现派生类中private成员虽然不可见,但是仍占有内存空间,并且可以得出以下结论
1. 创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,this指针相同的。
2. 创建派生类对象时,先初始化基类对象,再初始化派生类对象
3. 对派生类对象用sizeof得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小。
4. 用指针可以访问到基类中的私有成员(内存对齐)
5. 在C++中,不同继承方式的访问权限只是语法上的处理
在讲第五点时,首先要讲一下在VS中查看类的内存模型的方法
打开VS开发者命令行后,我们输入
cl 文件名 /d1 reportSingleClassLayout类名
例如我们需要查继承详解.cpp中的类A的内存模型
cl 继承详解.cpp /d1 reportSingleClassLayoutA
我们通过内存模型可以看出来成员变量的相对路径是连续的,通过运行结果也可以看出来,这里存在内存对齐问题,感兴趣的话可以去我的另一篇文章C语言之深入理解内存对齐简单了解内存对齐。
接下来讲《在C++中,不同继承方式的访问权限只是语法上的处理》
C++语法说,public继承时,派生类访问基类私有成员的方法只有通过基类的公有成员函数访问,假如我们使用memset(p, 0, sizeof(B));我们发现private成员被修改了,看来也是可以修改的啊!!那么就简单了,memset是直接操作内存的,那我们能否通过指针直接修改基类private的成员,
下面再上面代码中加入以下代码
*((int*)p + 2) = 31; // 把基类私有成员m_c的值修改成31。
//根据类B的内存模型,m_c是分配在相对的第12-16个内存单元,所以我们不妨把p指针强制转换成int*,然后往后偏移两个内存单元就指向m_c,然后再进行修改。
p->func(); p->func1();
我们奇迹的发现基类的private成员竟然被修改了!!!!
创建派生类时,系统自动分配一片基类+派生类大小的连续的空间,虽然private成员在派生类中不可见,但是也占有一定的空间;创建派生类对象时,先调用基类构造函数,再调用派生类构造函数;销毁派生类对象是,先调用派生类的析构函数,再调用基类的析构函数。
但是这种方法再实际开发中不建议。
如何构造基类
1)创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
2)如果没以指定基类构造函数,将使用基类的默认构造函数。
3)可以用初始化列表指明要使用的基类构造函数。
4)基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
5)派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a;
private:
int m_b;
public:
A() : m_a(0) , m_b(0) // 基类的默认构造函数。
{
cout << "调用了基类的默认构造函数A()。\n";
}
A(int a,int b) : m_a(a) , m_b(b) // 基类有两个参数的构造函数。
{
cout << "调用了基类的构造函数A(int a,int b)。\n";
}
A(const A &a) : m_a(a.m_a+1) , m_b(a.m_b+1) // 基类的拷贝构造函数。
{
cout << "调用了基类的拷贝构造函数A(const A &a)。\n";
}
// 显示基类A全部的成员。
void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
class B :public A // 派生类
{
public:
int m_c;
B() : m_c(0) , A() // 派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)。
{
cout << "调用了派生类的默认构造函数B()。\n";
}
B(int a, int b, int c) : A(a, b), m_c(c) // 指明用基类的有两个参数的构造函数。
{
cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";
}
B(const A& a, int c) :A(a), m_c(c) // 指明用基类的拷贝构造函数。
{
cout << "调用了派生类的构造函数B(const A &a,int c) 。\n";
}
// 显示派生类B全部的成员。
void showB() { cout << "m_c=" << m_c << endl << endl; }
};
int main()
{
B b1; // 将调用基类默认的构造函数。
b1.showA(); b1.showB();
B b2(1, 2, 3); // 将调用基类有两个参数的构造函数。
b2.showA(); b2.showB();
A a(10, 20); // 创建基类对象。
B b3(a, 30); // 将调用基类的拷贝造函数。
b3.showA(); b3.showB();
}
名字遮蔽与类作用域
名字遮蔽
注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。
类作用域
当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域中已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a = 10;
void func() { cout << "调用了A的func()函数。\n"; }
};
class B :public A { // 子类
public:
int m_a = 20;
void func() { cout << "调用了B的func()函数。\n"; }
};
class C :public B { // 孙类
public:
int m_a = 30;
void func() { cout << "调用了C的func()函数。\n"; }
void show() {
cout << "C::m_a的值是:" << C::m_a << endl;
cout << "B::m_a的值是:" << B::m_a << endl;
cout << "A::m_a的值是:" << B::A::m_a << endl;
}
};
int main()
{
C c;
cout << "C::m_a的值是:" << c.C::m_a << endl;
cout << "B::m_a的值是:" << c.B::m_a << endl;
cout << "A::m_a的值是:" << c.B::A::m_a << endl;
c.C::func();
c.B::func();
c.B::A::func();
}
继承的特殊关系
1)如果继承方式是公有的,派生类对象可以使用基类成员。
2)可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。
3)基类指针可以在不进行显式转换的情况下指向派生类对象。
4)基类引用可以在不进行显式转换的情况下引用派生类对象。
注意:
1) 基类指针或引用只能调用基类的方法,不能调用派生类的方法。
2)可以用派生类构造基类。
3)如果函数的形参是基类,实参可以用派生类。
4)C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。