C++继承

文章目录

C++继承

继承的概念和定义

继承的概念

继承机制是面向对象程序设计使代码可以复用 的最重要的手段,它允许程序员在保持原有类特性的基础上扩展 ,增加功能,这样产生新的类,称 派生类 。继承呈现了面向对象程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

cpp 复制代码
class Person
{
public:
	Person(string name = "张三", int age = 18)
		:_name(name),
		_age(age)
	{}

	void Print()
	{
		cout << "name: " << _name << endl;
		cout << "age: " << _age << endl;
	}
protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student(int stdid = 1001)
		:_stdid(stdid)
	{ }
protected:
	int _stdid;
};

class Teacher : public Person
{
public:
	Teacher(int jobid = 10001)
		:_jobid(jobid)
	{ }
protected:
	int _jobid;
};


int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

继承定义

定义格式
继承关系和访问限定符

继承基类成员访问方式的变化

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  • 基类private成员在派生类中无论怎么继承都不可见。
  • 基类private成员仍然被继承到了派生类对象中,只是语法上不允许派生类直接访问。
  • 如果希望基类成员在类外不能访问,但派生类中可以访问,就定义为 protected。
  • 基类其他成员在派生类中的访问方式 = min(基类访问限定符, 继承方式),权限严格程度:public > protected > private。
  • 实际中几乎只用public继承,很少用protected/private继承。

注意:"不可见 "不等于"不存在 "。基类private成员仍然在派生类对象内存中,构造、析构、基类成员函数仍然可以访问它。这是因为如果基类private成员在派生类中完全不存在,那么基类子对象的布局就不完整,基类函数也无法正常工作。所以它必须被继承下来,只是语法上禁止派生类直接访问,以保护封装。

基类和派生类对象赋值转换

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这叫切片 / 切割:把派生类中属于基类的那一部分切出来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类指针/引用可以指向派生类对象,但只能访问基类部分。基类指针向下强制转换为派生类指针,只有它实际指向派生类对象时才安全。多态类型(在多态章节再详细介绍)可以用dynamic_cast 做安全向下转型。

为什么派生类可以赋值给基类而基类不能赋值给派生类?

  • 因为派生类对象赋值给基类对象时发生切片,派生类就会将特有成员被丢弃 。比如:上述图片中将Student类赋值给它的父类Person类就会丢弃成员变量_No;
  • 而基类对象不能赋值给派生类对象是因为基类没有派生类的特有成员。

这里我们解释一下第三点是什么意思:

  • 派生类对象内存布局中,前面通常是基类子对象。所以基类指针/引用可以自然指向派生类对象中的基类部分。
  • 那么为什么 基类指针向下强制转换为派生类指针,只有它实际指向派生类对象时才安全。 这是因为编译器只改变指针的类型,并不会检查这个指针实际指向的到底是不是派生类对象。只有当你确定它实际指向的是一个派生类对象时,转换后通过它访问派生类特有的成员才是合法的;否则就是越界访问,属于未定义行为。
cpp 复制代码
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; // 可以转换,但访问 _No 越界,未定义行为
    // ps2->_No = 10;
}

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问 ,这种情况叫隐藏 ,也叫重定义 。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系 里面最好不要定义同名的成员。
cpp 复制代码
class Person {
protected:
    string _name = "小李子";
    int _num = 111; // 身份证号
};

class Student : public Person {
public:
    void Print() {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl; // 显式访问基类
        cout << "学号:" << _num << endl;             // 访问自己的
    }
protected:
    int _num = 999; // 学号
};

访问被隐藏的基类成员:

cpp 复制代码
Person::_num

注意:只要基类和派生类的成员函数同名,哪怕参数不同依旧构成隐藏而不是重载。因为重载是在同一个域里面同名函数才构成重载,基类和派生类各自都有独立的域。比如下面这个代码:

cpp 复制代码
class A {
public:
    void fun() { cout << "A::fun()" << endl; }
};

class B : public A {
public:
    void fun(int i) {
        A::fun();
        cout << "B::fun(int)" << endl;
    }
};

这里的B::fun(int) 和A::fun()不是重载,因为不在同一作用域;它们构成隐藏。

总结:

  • 隐藏:不同作用域中,派生类同名成员屏蔽基类成员,函数名相同即可。
  • 重载:同作用域中,函数名相同,参数不同。

派生类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类构造函数必须调用基类构造函数初始化基类部分。
  2. 基类没有默认构造时,必须在派生类初始化列表显式调用。
  3. 派生类拷贝构造要调用基类拷贝构造。
  4. 派生类 operator= 要调用基类 operator=。
  5. 派生类析构后会自动调用基类析构。
  6. 构造顺序:基类构造 → 派生类构造。
  7. 析构顺序:派生类析构 → 基类析构。
  8. 析构函数名会被特殊处理,父类析构不加 virtual(多态章节详细介绍) 时,子类析构与父类析构构成隐藏。

构造函数

派生类构造函数必须调用基类构造函数初始化基类部分。

如果基类没有默认构造函数,必须在派生类构造函数初始化列表阶段显式调用。

  • 这是因为派生类中包含了基类的成员变量,当使用派生类时就必须先调用基类的构造函数。
cpp 复制代码
class Person {
public:
    Person(const char* name = "peter") : _name(name) {
        cout << "Person()" << endl;
    }
protected:
    string _name;
};

class Student : public Person {
public:
    Student(int id, const char* name)
        : Person(name)  // 显式调用基类构造
        , _id(id)
    {}
protected:
    int _id;
};

拷贝构造函数

派生类拷贝构造必须调用基类拷贝构造完成基类部分的拷贝初始化。

和构造函数同理你要拷贝不能只拷贝你需要的必须拷贝完整,简单的说就是权责对等。

cpp 复制代码
Student(const Student& s)
    : Person(s)   // 调用基类拷贝构造
    , _id(s._id)
{}

赋值运算符重载

派生类 operator= 必须调用基类 operator= 完成基类部分的复制。和上面两个同理

cpp 复制代码
Student& operator=(const Student& s) {
    if (this != &s) {
        Person::operator=(s); // 调用基类赋值
        _id = s._id;
    }
    return *this;
}

析构函数

派生类析构函数执行完后,会自动调用基类析构函数。

顺序:

  • 构造:先基类构造,再派生类构造。
  • 析构:先派生类析构,再基类析构。

构造就比较好理解,就像必须先有你爸妈才能有你一个道理,析构就不能怎么理解。析构我们可以认为继承的析构就相当于你在上学的时候去当兵,你首先要跟你的辅导员说一下:'导员,我去当兵了。'然后你去宿舍里面搬行李,这一步就相当于派生类析构了。后续学校的操作比如保留学籍退住宿费等操作就相当于基类在析构。

cpp 复制代码
class Person {
public:
    Person() { cout << "Person 构造" << endl; }
    ~Person() { cout << "Person 析构" << endl; }
};

class Student : public Person {
public:
    Student() { cout << "Student 构造" << endl; }
    ~Student() { cout << "Student 析构" << endl; }
};

int main() {
    Student s;
    return 0;
}

继承与友元

友元在重载 operator>> 和 operator<< 时经常使用。我们知道友元可以让类访问外部函数接口,那么基类的友元可以被派生类继承吗?

答案是:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

那么友元为什么不能被继承呢?

我们可以简单理解为你父母的朋友不是你的朋友。

cpp 复制代码
class Student;

class Person {
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name;
};

class Student : public Person {
protected:
    int _stuNum;
};

void Display(const Person& p, const Student& s) {
    cout << p._name << endl;   // 可以,Display 是 Person 友元
    // cout << s._stuNum << endl; // 不可以,友元不能继承
}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个static成员实例 。

我们用一个小故事来解释为什么只有一个static成员实例。

有一个美国的千亿富豪,他有4个子女,大儿子到上班的年龄,二儿子在准备考大学,三女儿在上初中,小儿子在上小学。有一天大儿子给富豪老爸打电话说我想买一辆车需要30万美元你听二话不说就给大儿子打过去了,二儿子也给你打电话说老爸你给我一点钱我想请个家教老师辅导我富豪老爸将钱给过去了并叮嘱二儿子要好好学习努力考上一个好大学,三女儿也打来了电话跟爸爸说我想买一个彩虹小马富豪将钱打过去了问女儿还想买一起买三女儿说不用了爸爸,最后小儿子跟爸爸打电话说爸爸我想买一套赛车玩具富豪也将钱打过去了。

这个故事说明了什么呢?

故事里面的大儿子、二儿子、三女儿和小儿子买东西的钱都是富豪的钱,本质上他们4个并没有使用自己的钱而是富豪的钱,即基类定义了 static 静态成员,则整个继承体系里只有一个这样的成员实例。无论派生出多少子类,都只有一个 static 成员实例。

cpp 复制代码
class Person {
public:
    Person() { ++_count; }
    static int _count;
};

int Person::_count = 0;

class Student : public Person {
    int _stuid = 0;
};

class Graduate : public Student {
    string _course;
};

void TestPerson() {
    Student s1, s2;
    Graduate g1;

    cout << Person::_count   << endl;  // 3
    cout << Student::_count  << endl;  // 3
    cout << Graduate::_count << endl;  // 3

    cout << &Person::_count   << endl;
    cout << &Student::_count  << endl;
    cout << &Graduate::_count << endl; // 三个地址完全一样
}

注意 :如果你在派生类中声明了同名static变量,那么基类的static变量就会被隐藏,只要派生类不重新定义,整个继承体系就只有基类那一个静态成员实例。

下一章我们来了解一下继承中的多继承是怎么一回事。

完

相关推荐
汉克老师2 小时前
GESP2026年9月认证C++七级( 第三部分编程题(2、括号序列))精讲
c++·gesp·小学生·学c++编程
jimy13 小时前
虚函数vtable,链接时看到构造函数,linker把符号地址写入vtable(三)
开发语言·c++
潼心1412o4 小时前
C++初阶(长期更新)第9讲:string(上)
开发语言·c++
朝朝辞暮i5 小时前
C++ 第 39 章: 阶段性总复习——类、对象、构造、继承、this、指针与智能指针
开发语言·c++·算法·ros2
wuminyu5 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
All for pursuit.5 小时前
【回溯-6】79.单词搜索
数据结构·c++·算法·leetcode
Benny_Tang6 小时前
AT_arc180_d [ARC180D] Division into 3 题解
数据结构·c++·算法
殷色玫瑰6 小时前
C++ STL:stack、queue、priority_queue 与容器适配器详解
java·开发语言·数据结构·c++·算法·visualstudio
努力努力再努力wz7 小时前
【CUDA入门系列】CUDA编程实战:设备选择、显存管理、Kernel、Stream 与 Event 全流程梳理
开发语言·c++