继承——子承父业,代码复用
较难20继承:让代码“子承父业”,轻松复用
你知道吗?子女往往跟父母长得像,而且会继承父母的一些本领,比如走路、说话。在C++里,一个类可以“继承”另一个类的成员,这个被继承的类叫“基类”(父类),继承的类叫“派生类”(子类)。子类自动拥有父类的所有公开成员,还可以添加新增的属性和方法,就像孩子除了继承父母的,还能自己学画画、弹琴。
继承的目的很简单:避免重复写相同的代码。比如,你要描述猫、狗、鸟等各种动物,它们都有名字,都会叫。如果每种动物都单独写一个类,每个类里都要写名字、叫声,那就太啰嗦了。把共同的属性和行为放在一个“父类”里,子类“继承”过来,只写自己特有的东西就行。
一、基础:定义一个动物基类
我们先定义一个“动物”父类,它包含所有动物共有的东西:名字和发出声音的能力。
#include <iostream>
#include <string>
using namespace std;
class Animal {
protected: // 受保护的成员,子类可以访问,但类外部不能直接用
string name; // 名字
public:
// 构造函数:给名字赋值
Animal(string n) : name(n) {}
// 一个通用方法:“发出声音”
void speak() const {
cout << name << " 发出声音" << endl;
}
};
protected是访问权限的一种,它允许子类直接访问父类中的name,但其他函数(比如main)不能直接修改。这就像家庭内部的秘密,家人可以知道,外人不行。Animal(string n) : name(n){}是构造函数初始化列表,把参数n赋给成员变量name。后面派生类必须调用这个构造函数来设置名字。
二、创建派生类:猫和狗
现在让 Dog 和 Cat 继承 Animal,并覆盖(重写) speak 方法,因为每种动物的叫声不同。
class Dog : public Animal { // 公有继承:父类的公有成员在子类中仍是公有的
public:
// 调用父类构造函数,传入名字
Dog(string n) : Animal(n) {}
// 重写父类的 speak,实现狗叫
void speak() const {
cout << name << " 汪汪叫" << endl;
}
};
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
// 重写 speak 实现猫叫
void speak() const {
cout << name << " 喵喵叫" << endl;
}
};
int main() {
Dog dog("旺财"); // 创建一个狗对象
Cat cat("咪咪"); // 创建一个猫对象
dog.speak(); // 输出:旺财 汪汪叫
cat.speak(); // 输出:咪咪 喵喵叫
return 0;
}
你看,Dog 和 Cat 没有写 name 和构造函数里的名字赋值,这些已经从 Animal 继承过来了。我们只需要专注于它们不同的部分:叫声。继承让代码不再重复,就像同一个家族的基因可以传给后代一样,大大提高了代码的复用率。
三、继承的更多细节
1. 继承方式:公有、保护、私有
上面的 class Dog : public Animal 中的 public 叫继承方式。常用的有三种:
| 继承方式 | 父类中的 public 成员 | 父类中的 protected 成员 | 父类中的 private 成员 |
|---|---|---|---|
| public | 变为子类的 public | 变为子类的 protected | 不可访问 |
| protected | 变为子类的 protected | 变为子类的 protected | 不可访问 |
| private | 变为子类的 private | 变为子类的 private | 不可访问 |
- 公有继承最常用,相当于“保持原样”:父类能公开的,子类也能公开;父类保护给家族的,子类也保护给家族。
- 保护继承:把父类的公开成员也变成保护,子类能访问,但外部不能。
- 私有继承:所有父类成员(除了私有的)都变成子类的私有,子类可以使用,但子类的子类就不能用了。
初学者只要记住:一般情况下用 public 继承,这样最自然。
2. 派生类的构造函数——必须初始化父类
子类对象在创建时,必须先调用父类的构造函数来初始化继承下来的成员(比如 name)。如果父类没有默认构造函数(比如 Animal 要求带一个 string 参数),那么子类必须在自己的构造函数初始化列表中显式调用父类的构造函数,就像我们写的 Dog(string n) : Animal(n) {}。
如果父类有一个默认构造函数(没有参数或参数都有默认值),子类也可以不写初始化列表,系统会自动调用父类的默认构造函数。例如:
class Person {
protected:
string name; // 名字
public:
Person() : name("未知") {} // 默认构造函数
Person(string n) : name(n) {}
};
class Student : public Person {
int score; // 分数
public:
Student(string n, int s) : Person(n), score(s) {} // 调用带参数的父类构造函数
// 如果不写 : Person(n),就会默认调用 Person(),name 会被设为 "未知"
};
3. 访问权限:protected 的妙用
为什么用 protected 而不是 public 来存放 name?因为 protected 让父类成员既能被子类使用,又不会被外面的代码随意修改。比如:
int main() {
Dog dog("小黑");
// dog.name = "小白"; // 错误!name 是 protected,不能从外部直接修改
cout << dog.name; // 也错误,外部不能访问
}
这样就保护了数据的安全性。如果希望外部也能读但不能写,可以在父类中提供公有的 getName() 方法。
四、生活中的更多继承例子
除了动物,我们还可以用“学生”和“老师”来理解继承。有一个基类 Person(人),有名字、年龄。派生出 Student(学生)和 Teacher(老师)。
#include <iostream>
#include <string>
using namespace std;
class Person {
protected:
string name; // 名字
int age; // 年龄
public:
Person(string n, int a) : name(n), age(a) {}
void introduce() const {
cout << "我是" << name << ",今年" << age << "岁。" << endl;
}
};
class Student : public Person {
int grade; // 年级
public:
Student(string n, int a, int g) : Person(n, a), grade(g) {}
void study() {
cout << name << "正在学习。" << endl;
}
};
class Teacher : public Person {
string subject; // 教授科目
public:
Teacher(string n, int a, string sub) : Person(n, a), subject(sub) {}
void teach() {
cout << name << "正在教" << subject << "。" << endl;
}
};
int main() {
Student stu("小明", 12, 6);
Teacher tea("张老师", 28, "数学");
stu.introduce(); // 输出:我是小明,今年12岁。
stu.study(); // 输出:小明正在学习。
tea.introduce(); // 输出:我是张老师,今年28岁。
tea.teach(); // 输出:张老师正在教数学。
return 0;
}
Person 中的 name、age 和 introduce() 被 Student 和 Teacher 自动继承,不用重复写。这就是继承的威力:公共代码在父类写一次,所有子类共享。
五、新手常见错误
错误1:忘记调用父类构造函数导致编译错误
class Dog : public Animal {
public:
Dog(string n) { // 错误:没有调用 Animal(n)
// 编译报错:Animal 没有默认构造函数
}
};
解决办法:在初始化列表中调用 Animal(n)。
错误2:在子类中试图访问父类的私有成员
假设我们把 Animal 中的 name 从 protected 改为 private:
class Animal {
private:
string name; // 私有,子类不能直接访问
// ...
};
那么 Dog::speak() 里写 cout << name 就会报错。所以想把成员留给子类用,应该用 protected。
错误3:继承方式写错导致访问权限变化
class Cat : protected Animal { ... }; // 保护继承
这时在 main 中调用 cat.speak(),如果 speak() 原来是 public,经过保护继承后变为 protected,外部就不能调用了。所以要仔细想好继承方式。
错误4:子类重写函数时忘记加 const
父类 speak() 声明为 const,子类重写时也必须加 const,否则编译器会认为是不同的函数(重载而非覆盖)。例如:
void speak() const; // 正确
void speak(); // 错误:不加 const 就是另一个函数了
六、完整可运行示例:动物园里的叫声
下面是一个综合示例,我们把上面讲的内容放在一起,并增加了一个 Bird 类。
#include <iostream>
#include <string>
using namespace std;
// 基类:动物
class Animal {
protected:
string name; // 名字
public:
Animal(string n) : name(n) {} // 构造函数
void speak() const { // 通用叫声
cout << name << " 发出声音" << endl;
}
string getName() const { return name; } // 外部获取名字
};
// 派生类:狗
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void speak() const { // 重写叫声
cout << name << " 汪汪叫" << endl;
}
};
// 派生类:猫
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() const {
cout << name << " 喵喵叫" << endl;
}
};
// 派生类:鸟
class Bird : public Animal {
public:
Bird(string n) : Animal(n) {}
void speak() const {
cout << name << " 叽叽喳喳" << endl;
}
void fly() { // 鸟独有的方法
cout << name << " 飞起来了" << endl;
}
};
int main() {
Dog dog("旺财"); // 狗
Cat cat("咪咪"); // 猫
Bird bird("小翠"); // 鸟
// 调用各自的 speak
dog.speak(); // 旺财 汪汪叫
cat.speak(); // 咪咪 喵喵叫
bird.speak(); // 小翠 叽叽喳喳
// 调用继承来的 getName
cout << "这只鸟叫" << bird.getName() << endl; // 这只鸟叫小翠
// 调用独有的 fly
bird.fly(); // 小翠 飞起来了
return 0;
}
运行结果:
旺财 汪汪叫
咪咪 喵喵叫
小翠 叽叽喳喳
这只鸟叫小翠
小翠 飞起来了
七、相关指引
继承只是面向对象的三大特性之一(继承、封装、多态)。学会继承后,你可以继续学习:
- 多态:用父类指针或引用调用子类的重写方法,实现“一套接口,多种行为”。
- 虚函数(
virtual):实现多态的关键。 - 抽象类(含有纯虚函数的类):只定义接口,强制子类实现。
- 多重继承:一个子类可以继承多个父类(但容易产生歧义,谨慎使用)。
从“子承父业”开始,慢慢你就会发现 C++ 的面向对象世界有多么强大。不妨试着用继承来设计你自己的“班级”或“游戏角色”吧!
例题精讲
在C++中,关于公有继承(public inheritance)的说法,下列哪一项是正确的?
已知基类Base和派生类Derived,定义如下: class Base { public: Base() { cout << "Base"; } }; class Derived : public Base { public: Derived() { cout << "Derived"; } }; 执行"Derived d;"后,输出的结果是?
在C++中,派生类会自动继承基类的构造函数。
请补全派生类Derived的构造函数,使其能正确初始化基类Base的成员a和自身成员b。
class Base {
public:
Base(int x) : a(x) { }
int a;
};
class Derived : public Base {
public:
Derived(int x, int y) : ___(x), b(y) { }
private:
int b;
};当在派生类中定义一个与基类同名的成员函数(无论参数列表是否相同)时,基类中的该函数将被隐藏。