欢迎光临
我们一直在努力

【C++】C++继承入门(上):继承语法与基本特性详解

📌 相关专栏

  • 【Linux专栏】
  • 【C语言专栏】
  • 【测试专栏】
  • 【MySQL专栏】
  • 【C++ 专栏】

📌 相关文章推荐

  • 【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南
  • 【C++】模板初阶: 解析模板原理、实例化与特化
  • 【C++】STL:掌握STL容器特性与实战用法
  • 【Linux】 传输层协议UDP:从端口号到传输机制

举爪爪打招呼

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • 1. 继承的基本概念
    • 1.1 定义格式
    • 1.2 继承方式
    • 1.3 代码示例
    • 1.4 test
  • 2. 继承方式与访问权限
    • 2.1 公有继承(public)
    • 2.2 保护继承(protected)
    • 2.3 私有继承(private)
    • 2.4 访问权限总结
  • 3. 继承中的作用域与隐藏
    • 3.1 隐藏规则
    • 3.2 成员函数隐藏示例
  • 4. 基类与派生类的转换
    • 4.1 转换规则
    • 4.2 代码示例
  • 5. 派生类的默认成员函数
    • 5.1 构造函数
    • 5.2 拷贝构造
    • 5.3 赋值运算符重载
    • 5.4 析构函数
    • 5.5 构造与析构顺序
    • 5.6 成员函数总结
  • 6. 不能被继承的类
    • 6.1 C++98 方法:构造函数私有化
    • 6.2 C++11 方法:final 关键字
  • 7. 完整测试用例
    • 7.1 基础继承测试
    • 7.2 派生类成员函数测试
    • 7.3 继承方式测试(适配器模式)
  • 8. 总结
  • 本文全部代码
    • 🐾test.cpp

前言

继承是面向对象程序设计的三大特性之一(封装、继承、多态),它允许在保持原有类特性的基础上进行扩展,实现代码复用。

🐾为什么需要继承?

场景:Student 和 Teacher 都需要 “姓名、地址、身份认证” 但 Student 有学号、Teacher 有职称

  • 如果不使用继承:每个类都要重复定义姓名、地址、身份认证 -> 代码冗余
  • 使用继承:将公共部分抽成 Person 父类,子类直接复用

🐶 🐾 ✨ 🐾 🐶


1. 继承的基本概念

继承 :是面向对象程序设计使代码可以复用的手段,他允许在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),通过这样产生新的类——派生类

  • 父类(基类) :存放公共成员的类(如 Person)
  • 子类(派生类): 继承父类并扩展专属成员的类(如 Student、Teacher)

继承的本质 : 子类是父类的“扩展”,复用父类成员


1.1 定义格式

定义格式:继承方式+父类名——>class Student : public Person
派生类 继承方式 基类


1.2 继承方式

  • class 默认继承方式是 private
  • struct 默认继承方式是 public

1.3 代码示例

////基类/父类:Person(人类:作为学生和老师的通用父类)
class Person
{

// 公共成员:子类和类外都能访问
public:

//身份认证方法:进入校园等场景
void identity()
{
cout << "void identity()" << _name << endl;
}

//年龄访问方法:间接暴露私有成员 _age
void age()
{
cout << _age << endl;
}

// 保护成员:子类能访问,类外不能访问(专门为继承设计)
protected:
string _name = "张三"; //姓名
string _address;//地址
string _tel = "888888"; //电话

// 私有成员:子类和类外都不能直接访问,只能通用父类公有方法间接访问
private:
int _age = 18; //年龄
};

//// 子类Student:公有继承->Person

//class默认是私有继承
//struct默认是公有继承

// class Student:Person (私有继承)
class Student : public Person//公有继承
{
public:
void study()
{
cout << "void study(): " << _tel << endl;

//通过父类公有函数能间接访问私有成员_age
age();
}
protected:
int _stuid; //学号
};

// 子类Teacher:公有继承Person
class Teacher : public Person
{
public:
void teaching()
{
cout << "void teaching() " << _tel << endl;
}
protected:
string title; //职称
};


1.4 test

int main()
{
Student s;
Teacher t;

s.identity(); // 调用父类函数
t.identity(); // 调用父类函数

s.study(); // 调用子类函数
t.teaching(); // 调用子类函数

return 0;
}

🐶 🐾 ✨ 🐾 🐶


2. 继承方式与访问权限

继承方式决定了父类成员在子类中的访问级别


2.1 公有继承(public)

父类成员子类访问权限类外访问权限
public public 可访问
protected protected 不可访问
private 不可访问 不可访问

class Student : public Person
{
// Person::public -> public
// Person::protected -> protected
// Person::private -> 不可访问
};


2.2 保护继承(protected)

父类成员子类访问权限类外访问权限
public protected 不可访问
protected protected 不可访问
private 不可访问 不可访问

class Student : protected Person
{
// Person::public -> protected
// Person::protected -> protected
// Person::private -> 不可访问
};


2.3 私有继承(private)

父类成员子类访问权限孙子类访问权限类外访问权限
public private 不可访问 不可访问
protected private 不可访问 不可访问
private 不可访问 不可访问 不可访问

2.4 访问权限总结

  • 公有继承(public): 保持父子关系,类外可调父类 public
  • 保护继承(protected):父类 public -> protected,类外不能调任何父类成员
  • 私有继承(private): 所有能继承的成员变成 private,孙子类无法继续继承

🐶 🐾 ✨ 🐾 🐶


3. 继承中的作用域与隐藏

3.1 隐藏规则

当子类和父类有同名成员时,子类成员会隐藏父类成员

class Person
{
protected:
string _name = "张三";
int _num = 123456; // 父类:身份证号
};

class Student : public Person
{
public:
void Print()
{
// 默认访问子类的 _num
cout << "子类的_num:" << _num << endl; // 输出 111

// 显式访问父类的 _num
cout << "父类的_num:" << Person::_num << endl; // 输出 123456
}
protected:
int _num = 111; // 子类:学号(与父类同名,构成隐藏)
};

隐藏规则 :

  • 成员变量:同名构成隐藏
  • 成员函数:只需要函数名相同就构成隐藏(不看参数列表)

3.2 成员函数隐藏示例

class A
{
public:
void fun() { cout << "func()" << endl; }
};

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

int main()
{
B b;
b.fun(10); // 调用子类的 fun(int)
// b.fun(); // 父类的 fun() 被隐藏,不能直接调用
b.A::fun(); // 显式调用父类的 fun()
return 0;
}

🐶 🐾 ✨ 🐾 🐶


4. 基类与派生类的转换

4.1 转换规则

  • 子类 -> 父类指针/引用: 安全, 隐式转换,指向子类中的父类部分
  • 子类 -> 父类对象: 安全, 调用父类拷贝构造,只拷贝父类部分
  • 父类 -> 子类 :不安全, 编译报错,父类没有子类的成员

4.2 代码示例

void Test3()
{
Student st;

// 1. 子类对象 -> 父类指针/引用(安全)
Person* ppe = &st; // 父类指针指向子类对象的"父类部分"
Person& rpe = st; // 父类引用引用子类对象的"父类部分"

// 2. 子类对象 -> 父类对象(调用父类拷贝构造)
Person pe = st;

// 3. 父类对象 -> 子类对象(编译报错)
// st = pe; // 错误
// st = (Student)pe; // 强制转换也不行
}

区别 :这种转换不同于普通隐式类型转换(不会产生临时对象),是 C++ 为继承专门规定的特例。

🐶 🐾 ✨ 🐾 🐶


5. 派生类的默认成员函数

5.1 构造函数

规则 :子类必须在初始化列表中显式调用父类构造函数

public:
// 父类全缺省构造(基类有默认构造函数的情况)
//Person(const char* name = "张三")
//:_name(name) // 初始化父类的_name
//{
//cout << "Person()" << endl;
//}

// 父类带参构造(基类没有默认构造函数的情况)
Person(const char* name)
:_name(name) // 初始化父类的_name
{
cout << "Person()" << endl;
}

class Student : public Person
{
public:
Student(const char* name = "张三", int num = 18, const char* address = "北京")
: Person(name) // 必须显式调用父类构造
, _num(num)
, _address(address)
{
cout << "Student()" << _name << endl;
}
// …
};

🐾 为什么不能直接在初始化列表初始化父类成员?

  • 父类成员属于父类,应该由父类构造函数初始化
  • 如果父类成员很多,逐一手动初始化非常麻烦

5.2 拷贝构造

Student(const Student& s)
: Person(s) // 调用父类拷贝构造(s 可隐式转换为 Person)
, _num(s._num)
, _address(s._address)//(s是子类,能隐式转父类:通过切割把子类中父类那部分成员变量切出来进行拷贝)
{
// 如果有深拷贝资源,这里需要手动处理
}


5.3 赋值运算符重载

Student& operator=(const Student& s)
{
if (this != &s)
{
// 不能直接写 operator=(s),会死循环
Person::operator=(s); // 显式调用父类赋值重载
_num = s._num;
_address = s._address;
}
return *this;
}

🐾 子类的 operator= 与父类构成隐藏,必须通过 父类:: 显式调用。


5.4 析构函数

~Student()
{
cout << "~Student()" << endl;
// 不需要显式调用父类析构
// 子类析构结束后,编译器会自动调用父类析构

//Person::~Person();
//这样写如果存在有动态开辟的空间反而会导致对同一块空间析构两次而程序崩溃

cout<<"student()'<<endl;
}


5.5 构造与析构顺序

  • 构造顺序:先父类 -> 后子类
  • 析构顺序:先子类 -> 后父类

原因 :如果先析构父类,子类成员访问父类成员就会出问题


5.6 成员函数总结

成员函数是否需要显式调用父类注意事项
构造函数 必须(初始化列表) 父类无默认构造时尤其重要
拷贝构造 必须(初始化列表) 子类对象可切片传给父类
赋值重载 必须(函数体内) 用 父类::operator= 避免隐藏
析构函数 不需要 编译器自动调用

🐶 🐾 ✨ 🐾 🐶


6. 不能被继承的类

6.1 C++98 方法:构造函数私有化

class Base
{
public:
void func() { cout << "Base::func" << endl; }
protected:
int a = 1;
private:
Base() {} // 构造函数私有,子类无法调用
};

// class Derive : Base {}; // 编译错误


6.2 C++11 方法:final 关键字

class Base final // final 关键字禁止继承
{
public:
void func() { cout << "Base::func" << endl; }
protected:
int a = 1;
};

// class Derive : Base {}; // 编译错误

🐶 🐾 ✨ 🐾 🐶


7. 完整测试用例

7.1 基础继承测试

void Test1()
{
Student s;
Teacher t;

s.identity(); // 调用父类函数
t.identity(); // 调用父类函数

s.study(); // 调用子类函数(内部调用父类的 age())
t.teaching();
}


7.2 派生类成员函数测试

int main()
{
// 构造顺序:先 Person(name),再 Student()
Student s1("李四", 20, "北京");

// 拷贝构造
Student s2(s1);

// 赋值重载
Student s3("王五", 18, "上海");
s1 = s3;

// 析构顺序:先 ~Student(),再 ~Person()
return 0;
}


7.3 继承方式测试(适配器模式)

namespace MyStack
{
template<class T>
class stack : public std::vector<T> // 私有继承 vector
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定类域
vector<T>::push_back(x);
}

void pop() { vector<T>::pop_back(); }

const T& top() { return vector<T>::back(); }

bool empty() { return vector<T>::empty(); }
};
}

🐶 🐾 ✨ 🐾 🐶


8. 总结

C++ 继承的核心知识点:

  • 继承方式: public、protected、private 三种,影响成员访问权限

  • 作用域与隐藏 :子类同名成员隐藏父类成员,可通过 父类:: 显式访问

  • 转换规则 :子类 -> 父类安全,父类 -> 子类不安全

  • 不可继承类:C++98(私有构造)和 C++11(final)两种实现方式


构造/析构顺序总结

  • 构造:先父类 -> 后子类
  • 析构:先子类 -> 后父类

隐藏 vs 重载 vs 重写

  • 隐藏 :子类与父类同名成员 ,不看参数列表
  • 重载 :同一作用域,同名不同参, 与继承无关
  • 重写 :多态中的虚函数覆盖 ,需要 virtual 关键字

🐶 🐾 ✨ 🐾 🐶


本文全部代码

🐾test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
#include<deque>

using namespace std;

//继承:是面向对象程序设计使代码可以复用的手段,他允许在保持原有类特性的基础上进行扩展,增加方法(成员函数)
//和属性(成员变量),通过这样产生新的类——派生类

/*理解:Student 和 Teacher 都需要 “姓名、地址、身份认证”,但 Student 有学号、Teacher 有职称。如果各自写一遍,
很多代码会变得冗余 —— 继承就是把 “公共部分” 抽成父类(基类),子类(派生类)直接复用。*/

/*核心概念:
父类(基类):存放公共成员的类,比如 person 类(包含姓名、地址、identity 身份认证函数)
子类(派生类):继承父类并扩展专属成员的类,比如 Student (加学号)、Teacher (加职称)
*/

/*本质:子类是父类的 “扩展”,能直接用父类的公共 / 保护成员,不用重复定义*/

/*定义格式:继承方式+父类名——>class Student : public Person*/
/* 派生类 继承方式 基类*/

/*继承方式:public 继承、protect 继承、private 继承*/

////基类/父类:Person(人类:作为学生和老师的通用父类)
class Person
{

// 公共成员:子类和类外都能访问
public:

//身份认证方法:进入校园等场景
void identity()
{
cout << "void identity()" << _name << endl;
}

//年龄访问方法:间接暴露私有成员 _age
void age()
{
cout << _age << endl;
}

// 保护成员:子类能访问,类外不能访问(专门为继承设计)
protected:
string _name = "张三"; //姓名
string _address;//地址
string _tel = "888888"; //电话

// 私有成员:子类和类外都不能直接访问,只能通用父类公有方法间接访问
private:
int _age = 18; //年龄
};

//// 子类Student:公有继承->Person

//class默认是私有继承
//struct默认是公有继承

// class Student:Person (私有继承)
class Student : public Person//公有继承
{
public:
void study()
{
cout << "void study(): " << _tel << endl;

//通过父类公有函数能间接访问私有成员_age
age();
}
protected:
int _stuid; //学号
};

// 子类Teacher:公有继承Person
class Teacher : public Person
{
public:
void teaching()
{
cout << "void teaching() " << _tel << endl;
}
protected:
string title; //职称
};

int main()
{
//测试子类是否能用父类的函数
student s;
teacher t;

s.identity();
t.identity();

s.study();
t.teaching();
return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/*访问权限:
1.公有继承(public): 父类 public -> 子类 pubilc
父类 protect -> 子类 protect
父类 private -> 子类 不可访问

父子关系不变,外面可以调用父类的 public 成员,子类可以用父类 protected,子类不能直接用父类 private

2.保护继承(protect): 父类 public -> 子类 protect
父类 protect -> 子类 protect
父类 private -> 子类 不可访问

父类的公有成员在子类变成保护,类外不能调用父类的任何成员,只允许子类内部、孙子类使用

3.私有继承(private): 父类 public -> 子类 private
父类 protect -> 子类 private
父类 private -> 子类 不可访问

父类所有能继承的,在子类都变成私有,子类能用,但孙子类不能再继承,类外完全不能访问父类成员

*/

#define CONTAINER vector
//#define CONTAINER list
//#define CONTAINER deque

namespace MyStack
{
template<class T>
class stack : public std::CONTAINER<T>
{
public:
void push(const T& x)
{
//push_back(x);
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,调用哪个成员函数就实例化哪个,
// 由于 push_back 等成员函数未实例化,所以找不到
CONTAINER<T>::push_back(x);
}

void pop()
{
CONTAINER<T>::pop_back();
}

const T& top()
{
return CONTAINER<T>::back();
}

bool empty()
{
return CONTAINER<T>::empty();
}
};
}

void Test1()
{
// 测试:子类能直接用父类的函数
Student s;
Teacher t;
s.identity();
t.identity();
s.study(); // 用子类的 study,调用父类的 age(),输出了18
t.teaching();
}

void Test2()
{
MyStack::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
}

int main()
{
Test2();
return 0;
}

void Test3()
{
//基类与派生类的转换
Student st;
// 1.派生类对象可以赋值给基类的指针/引用
// 子类对象 → 父类指针 / 引用(隐式转换,安全)
Person* ppe = &st; //父类指针 指向子类对象的“父类部分”
Person& rpe = st; //父类引用 引用子类对象的“父类部分”
//这里会有人误以为就是前面所学的简单的隐式类型转换
//但其实不是,从第三个引用来看就知道了:
int i = 1;
double d = i;
//double& rd = i;
const double& rd = i;
//如果是隐式类型转换的话会产生临时对象,临时对象显常性,如果不用const修饰就会报错
//但是上面的转换并没有const修饰也不会报错,
//所以和之前学习的隐式类型转换是有区别的,可以理解为是C++规定的特例

// 2.子类对象 → 父类对象(调用父类拷贝构造,只拷贝父类部分)
// 生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的
Person pe = st;

// 3.基类对象不能赋值给派生类对象,这里会编译报错
// 父类对象 → 子类对象(编译报错,不安全)
//st = pe;
//st = (Student)pe;// 强制转换也不行
}

int main()
{
//Test1();
//Test2();
Test3();
return 0;
}

//继承中的作用域
class Person
{
protected:
string _name = "张三"; // 姓名
int _num = 123456; // 父类的_num:身份证号
};

class Student : public Person
{
public:
void Print()
{
// 同名变量:默认访问子类的_num(学号)
// 同名成员构成隐藏
//如果是成员函数的隐藏,只需要函数名相同就构成隐藏
cout << "子类的_num:" << _num << endl;//输出111

// 想访问父类的_num:必须加“父类::”,即“基类::基类成员”显式访问
cout << "父类的_num:" << Person::_num << endl;//输出123456
}
protected:
int _num = 111; // 学号
};

int main()
{
Student s;
s.Print();
}

//继承作用域相关选择题
class A
{
public:
// 父类无参函数
void fun()
{
cout << "func()" << endl;
}
};

class B : public A
{
public:
// 子类有参函数:函数名和父类相同,构成隐藏
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};

int main()
{
B b;
b.fun(10); // 调用子类的fun(int),输出“func(int i)10”
// b.fun(); // 编译报错:父类的fun()被隐藏了,不能直接调用
b.A::fun(); // 想调用父类的fun():加“父类::”,输出“func()”
return 0;
}


//派生类的默认成员函数
class Person
{
public:
// 父类全缺省构造(基类有默认构造函数的情况)
//Person(const char* name = "张三")
//:_name(name) // 初始化父类的_name
//{
//cout << "Person()" << endl;
//}

// 父类带参构造(基类没有默认构造函数的情况)
Person(const char* name)
:_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;// 姓名
//int _age; //年龄
//string _gender; //性别
//string _title; //职业
};

class Student :public Person
{
public:
// 子类构造:必须在初始化列表显式调用父类构造
Student(const char* name = "张三", int num = 18, const char* address = "北京")
//这里可以显式写一下(不能直接用_name),其实就把他当成一个自定义类型成员变量就可以了
//: _name(name) //error C2614: “Student”: 非法的成员初始化:“_name”不是基或成员
:Person(name) // 先初始化父类(必须写在前面)
//这样要求也是变相的为我们节省构造的实现,如果父类的成员变量非常多,
//难道子类为了实现构造要把每个父类的成员变量都初始化一遍吗?显然太麻烦了
//所以初始化列表通过调用父类构造就可以直接对父类把部分的成员变量全部进行初始化
, _num(num) // 再初始化子类自己的成员
, _address(address)
{
cout << "Student()" << _name << endl;
}

//子类拷贝构造
Student(const Student& s)
:Person(s) //调用父类拷贝构造,拷贝父类部分
//(s是子类,能隐式转父类:通过切割把子类中父类那部分成员变量切出来进行拷贝)
, _num(s._num) // 拷贝子类自己的学号
, _address(s._address) // 拷贝子类自己的地址
{
// 如果有深拷贝资源(比如int*),这里要手动处理
}

//子类的赋值重载
Student& operator=(const Student& s)//一定要注意子类的同名函数 operator= 与父类构成隐藏
{
if (this != &s)
{
//operator=(s); //不能直接调用operator=否则就是调用子类本身的赋值重载导致死循环而栈溢出
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
}

//子类的析构
~Student()
{
//~Person(); //error
//由于析构函数都会被特殊处理成destructor()
//所以子类的析构和父类的析构也是会构成隐藏关系(虽然表面上不是同名的)
//规定:不需要显式调用父类析构,子类析构结束后,编译器会自动调用父类析构
//Person::~Person();
//这样写如果存在有动态开辟的空间反而会导致对同一块空间析构两次而程序崩溃
cout << "~Student()" << endl;
}

protected:
int _num; // 学号
string _address; // 地址
};

//实现一个不能被继承的类
//方法一:基类的构造函数私有(C++98)
//class Base
//{
//public:
//void func()
//{
//cout << "Base::func" << endl;
//}
//protected:
//int a = 1;
//private:
////C++98的方法:构造函数私有的类不能被继承
//Base()
//{
//}
//};

//方法二:添加 final 关键字(C++11)
//class Base final
//{
//public:
//void func5() { cout << "Base::func5" << endl; }
//protected:
//int a = 1;
//};
//
//class Derive : Base
//{
//};

int main()
{
// 构造顺序:先调用Person(name),再调用Student()
Student s1("李四", 20, "北京");
////拷贝构造
//Student s2(s1);
////赋值重载
//Student s3("王五", 18, "上海");
//s1 = s3;

// 构造顺序:先Person(),再Student()
// 先父后子:我们联想一下之前初始化列表按声明顺序来的原理
// 析构顺序:先~Student(),再~Person()
// 先子后父:我们可以想一下如果先析构父类,那么子类的成员如果需要访问父类就出问题了

//Derive d;
return 0;
}

  • 欢迎留言交流 ​
  • 期待你的评论与建议 ​
  • 留下你的想法吧

  • 举爪爪求关注

    谢谢你看到这里呀

    如果喜欢这篇内容,点个关注,下次更新不迷路✨

    👍 点赞 ⭐ 收藏 💬 评论

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】C++继承入门(上):继承语法与基本特性详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址