在原有类上扩展。
首先,假设在一个系统里有老师、学生的信息。
class Student
{
public:
void identity()
{
// ...
}
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
void identity()
{
// ...
}
void teaching()
{
// ...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};但是,如姓名、地址等都是两者共同的,我们就可以将相同的放在一个类 Person 里,师生可以共同继承这个类。
class Person
{
public:
void identity()
{
// ...
}
protected:
std::string _name = "张三"; // 姓名
int _age = 18; // 年龄
std::string _address; // 地址
std::string _tel; // 电话
};
class Student : public Person
{
public:
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
void teaching()
{
// ...
}
protected:
std::string _title; // 职称
};继承的基础写法:class Student : public Person,在类之后加一个类。
Person 叫父类(基类),Student 叫子类(派生类)。
我们将 protected 改为 private,会报错:"Person::_name": 无法访问 private 成员(在"Person"类中声明)。
Protected 大概率为专门为了继承而来的。

下面是表格。

由于栈是容器适配器,因此可以直接继承,复用 vector。
template <class T>
class Stack : public std::vector<T>
{
public:
void push(const T& val)
{
push_back(val);
}
void pop()
{
pop_back();
}
const T& top()
{
return back();
}
bool empty()
{
return empty();
}
};但是,这样写会报错:"pop_back": 找不到标识符。
原因:按需实例化
Stack<int> st; 初始化 vector 的构造函数。
st.push(1); 仅初始化了 push,在 push 里没有找到 push_back,就报错。
解决方案: 在开始时就告诉编译器:push_back 是 vector 里的,叫它一起实例化掉。
template <class T>
class Stack : public std::vector<T>
{
public:
void push(const T& val)
{
std::vector<T>::push_back(val);
}
void pop()
{
std::vector<T>::pop_back();
}
const T& top()
{
return std::vector<T>::back();
}
bool empty()
{
return std::vector<T>::empty();
}
};用宏修改容器
#define CONTAINER std::list<T>
void push(const T& val)
{
CONTAINER::push_back(val);
}Student s1;
Person p1 = s1;
Person& p2 = s1;
Person* p3 = &s1;三种写法都可以。
但是,不是强制类型转换。如果是的话,Person& p2 = s1; 语句将会报错,因为 s1 强转后变为临时对象,有常性,因此要用 const 引用,而代码中不用。
父对象可以强转给子对象
Person p1;
Student* s1 = (Student*)&p1;class Father
{
protected:
int a = 2;
};
class Child : public Father
{
public:
void print()
{
std::cout << a << std::endl; // 输出 1,调用子类的 a
}
protected:
int a = 1;
};就近调用变量。

std::cout << Father::a << std::endl; 加上以上语句能打印父类的 a。
class Father
{
public:
void print()
{
std::cout << "father" << std::endl;
}
};
class Child : public Father
{
public:
void print(int a)
{
std::cout << "child" << std::endl;
}
};如果调用不传参版本的,会报错。
父类如果有构造函数,可以在子类初始化列表调用。
class Father
{
public:
Father(int a1, const char* s1)
: _a1(a1)
, _s1(s1)
{}
protected:
int _a1;
string _s1;
};
class Child : public Father
{
public:
Child(int a1, int a2, const char* s1, const char* s2)
: _a2(a2)
, _s2(s2)
, Father(a1, s1)
{}
protected:
int _a2;
string _s2;
};如果没有构造函数,想用编译器生成的,那也要在初始化列表写空括号,此时自定义类型调用默认构造,内置类型随机。
class Father
{
public:
//Father(int a1, const char* s1)
// : _a1(a1)
// , _s1(s1)
//{}
protected:
int _a1;
string _s1;
};
class Child : public Father
{
public:
Child(int a1, int a2, const char* s1, const char* s2)
: _a2(a2)
, _s2(s2)
, Father()
/*, Father(a1, s1)*/
{}
protected:
int _a2;
string _s2;
};在我们的 Father、Child 类中,自动生成的够用,其实可以不写。
Child(const Child& c)
: _a2(c._a2)
, _s2(c._s2)
, Father(c)
{}会切片,将 Child 类里的 Father 类的元素切出来构造。
要是父类没写拷贝构造,编译器就会自动生成,进行浅拷贝。
Child& operator=(const Child& c)
{
_a2 = c._a2;
_s2 = c._s2;
Father::operator=(c);
return *this;
}和拷贝函数差不多。
注意: 加上 Father:: 指定作用域,否则会无限递归。
~Child()
{
Father::~Father();
}也记得加作用域 Father::,否则两个函数的析构底层名字一样,会构成重载而报错。
但如果这样直接调用,Father 明明只有 2 个元素,却析构了 4 次。

原因: 父类的析构会在子类析构完后自动调用。因此不用写。
这么做可以确保先定义后析构。
同理,子类初始化先调用父再调用子,和构造列表出现的顺序无关。
总之,将父类看成一个自定义类型,子类调用。
父类私有成员在子类不可见,子类无法调用父类构造函数。
缺点: 不去定义派生类对象不会报错。
class Father
{
protected:
int _a1;
string _s1;
private:
Father(int a1, string s1)
: _a1(a1)
, _s1(s1)
{}
};
class Child : public Father
{
public:
//Child(int a1, string s1, int a2, string s2)
// : Father(a1, s1)
// , _a2(a2)
// , _s2(s2)
//{}
protected:
//int _a2;
//string _s2;
};比如这样写,是不会报错的。
在父类最后写上 final 关键字。
class Father final
{
// ...
};父类的友元不是子类的友元。
解决方式: 再声明一次友元。
class Student;
class Person
{
public:
Person(string name = "sss")
: _name(name)
{}
friend void Display(const Person& p, const Student& s); // 友元关系不能被继承
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(string name = "ppp", int stuNum = 1)
: Person(name)
, _stuNum(stuNum)
{}
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}由于以上代码无论是 Person 还是 Student 都要提到对方,因此要加前置声明。
父类和子类为同一个。
class Person
{
public:
static int _a;
};
int Person::_a = 1;
class Student : public Person
{};
int main()
{
Person p;
Student s;
cout << &p._a << endl;
cout << &s._a << endl;
// 地址相同
}代码中,&p._a 和 &s._a 地址是一样的,为同一个。

即普通的继承。
一个子类有 2 个及以上的父类。
class Student
{
protected:
int _num;
};
class Teacher
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};学生和老师都算人员。
如果在学生和老师上面加一个 Person,就变成了菱形继承。
class Person
{
protected:
string _name;
};
class Student : public Person
{
protected:
int _num;
};
class Teacher : public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};会出现问题: 数据冗余,二义性。
Assistant 对象包含:
Student 继承来的 Person::_name
Teacher 继承来的另一个 Person::_name
_name 时,编译器不知道使用哪个版本。
冗余解决方式: 加虚拟继承(virtual)。
规则: 谁有冗余数据就加谁(不能只加一个)。
在图中,要给 B、C 加上虚拟继承。

先继承的在前面。
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;
// p1 == p3 != p2
}P1、p2、p3 指针的关系。
由于 Derive 先继承 Base1,因此 d 的指针和 b1 的指针是一个位置,b2 在 b1 后。
因此 p1 == p3 != p2。
class Stack : public std::list<T>
class Stack { std::list<T> _lt; }
is-a 关系,每个子类对象都是一个父类对象。
has-a 关系,每个 B 都有一个 A 对象。
最好低耦合高内聚,因此优先组合。
必须条件:
virtual 会继承下来)。
写法:
class Person
{
public:
virtual void ticket()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
void ticket()
{
cout << "打折" << endl;
}
};
void func(Person* p)
{
p->ticket();
}
int main()
{
Student* s = new Student;
Person* p = new Person;
func(s); // 输出:打折
func(p); // 输出:全价
}这样子,只需要传入父或子类的指针,就可以自动"识别",调用对应的函数。

若不满足多态,调用父类;满足,看指向对象。
去掉 virtual,调用父类。

class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
B* p = new B;
p->test();
return 0;
}结果: B->1

原因:
test 调用 func 虚函数,此时由于 p 是 B 的指针,因此 this 指的是 B,调用 B 的 func。
A 中调用 func 默认值为 1,而此时 B 调用 func 本质上是在 A 中调用的,因此值为 1,不为 B 的默认值 0。
class A {};
class B : public A {};
class Person
{
public:
virtual A* ticket()
{
cout << "全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* ticket()
{
cout << "半价" << endl;
return nullptr;
}
};
void buyTicket(Person* p)
{
p->ticket();
}有两组继承。
这样一样能有多态的效果。
在前面,讲到了父类和子类的析构函数名称会被处理成相同。 因此只要父类的析构为虚,子类和父类的析构就构成重写。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
delete _p;
}
protected:
int* _p = new int[10];
};上文中,我们提到:子类的析构函数会自动调用父类的,以确保先子后父的析构。
但是正常父类指针不会调用子类的析构。
因此在上面的程序中写:A* a2 = new B; delete a2; 会发现只调用了 A 的析构,没有调用 B 的析构,这样就会造成内存泄露。

解决方案: 在 A 的析构函数上加 virtual。
这样就会调用子类的析构。
那上文父类子类的析构函数名字处理相同的原因就知道了:为了让析构函数可以同时调用。
由于有时不确定是否函数被重写,因此可以加入这个关键字。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B() override // 确保构成重写
{
cout << "~B()" << endl;
delete _p;
}
protected:
int* _p = new int[10];
};这样就能确保先构成重写,再运行,降低内存泄露风险。
final:不可被重写。
class A
{
public:
virtual void func() = 0; // 纯虚函数
};在函数后面加一个 =0 就代表它是纯虚函数,必须在派生类中实现。
抽象类: 有纯虚函数的类。
使用方式: 在子类定义这个函数。
class A
{
public:
virtual void func() = 0;
};
class B : public A
{
public:
virtual void func()
{
cout << "可以" << endl;
}
};class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};这个类的大小为(32位):12。
由:整型 _b,字符 _ch,以及一个指针构成。

这个指针为:函数指针数组的指针。 人话,即为虚函数表的指针。
在继承中,无论父类子类的虚函数都会被整合成一个虚函数表,里面存着函数指针。 运行时,根据指向对象的虚函数表中找到虚函数地址,进行调用,因此,只有在运行时才能确定地址。
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
B* b1 = new B;
printf("虚表指针:%p\n", *(int**)b1); // 取b1对象的前4/8个字节(虚表指针)结果看,b1 的前 4 个字节的地址小于常量区,大于静态区,因此可以推测在 VS 下,处于常量区。
