C++三大特性—继承“名字搜索与默认成员函数”

文章详细阐述了C++中继承的概念,包括类的作用域、编译时的名字搜索、隐藏与重定义,以及派生类构造函数和析构函数的工作原理。同时讨论了派生类如何处理基类的同名成员,以及继承与友元、静态成员的关系。重点强调了派生类如何初始化和调用基类的部分,以及静态成员的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承中的类的作用域

每个类定义自己的作用域,在这个作用域中定义自己的成员。当存在继承关系时,派生类的作用域嵌套在基类的作用域之中。如果一个名字在派生类的作用域中无法解析,那么编译器将继续在外层的基类中寻找该名字的定义。

继承关系如下:在这里插入图片描述
我们看下面代码:

	Child obj;
	obj.fun();

   我们通过Child类型的obj去调用fun函数,所以我们首先在Child类域中查找,如果没找到。
   因为Child继承于Teacher,Child是Teacher的派生类,所以接下来我们继续在Teacher的类域中查找fun函数,如果没找到。
   因为Teacher继承于Person,Teacher是Person的派生类,所以接下来我们继续在Person的类域中查找。一直找到最终的基类。

编译时的名字搜索

一个对象、引用、指针的静态类型决定了该对象的哪个成员是可见的,即使它的静态类型与动态类型可能不一致,但是它能使用哪个类型依然是由静态类型决定的

举个例子:

class Person
{
protected:
	string _name; // 姓名
	string _sex;  // 性别
	int _age;     // 年龄
};
class Teacher : public Person
{
public:
	int _No; // 学号
	void fun() const
	{
		cout << _No;
	}
};

我们只能通过Teacher及其派生类的对象、指针、引用去访问fun函数

	Teacher obj;              
	Teacher* obj_Tea = &obj;  //静态类型与动态类型一致
	Person* obj_Per = &obj;   //静态类型与动态类型不一致
	obj_Tea->fun();           //正确、类型为Teacher
	obj_Per->fun();           //错误、类型为Person

   虽然obj之中确实是有一个名字为fun的函数,但是这个成员对于obj_Per是不可见的。obj_Per的类型是一个Person类型,那么就意味着对于fun的的搜索是从Person开始的,显然Person类中没有fun函数,所以我们无法通过Person的对象、指针、引用去调用fun。

名字冲突与继承

  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
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; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};

运行结果:在这里插入图片描述
   子类成员中有_num,父类成员中也有_num,所以正常在子类里面访问_num,会隐藏父类继承来的_num,而访问子类本身有的成员,如果要访问这个隐藏的成员,需要在前面加上Person::

  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();//调用父类的
		cout << "func(int i)->" << i << endl;
	}
};
void Test()
{
	B b;
	b.fun(10);
};

运行结果:在这里插入图片描述
B中的fun和A中的fun不是构成重载,因为不是在同一作用域
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。

如果有隐藏默认调用自身所在类的,如果需要调用父类的,就加上 父类::(A::)

这种方式叫:使用作用域运算符来使用隐藏的基类成员
作用域运算符将覆盖掉原有的查找规则,并指示编译器从指定类的作用域开始查找成员

派生类的成员将隐藏同名的基类成员
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

理解函数调用的解析过程对于理解C++继承至关重要:
如果我们需要调用一个函数:obj.fun();
1.我们首先要确定obj的静态类型,因为我们调用的是一个成员,所以该类型必然是类类型
2.在obj静态类型对应的类中查找fun,如果找不到,则依次在直接基类中不断查找直到到达继承链的顶端,如果找完了还找不到,编译器则会报错
3.一旦找到了fun,先进行常规的类型检查,以确定找到的fun合法
4.调用合法,编译器将根据调用的是否是虚函数而产生不同的代码

  • 如果fun是虚函数且是通过指针或引用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据的是对象的动态类型
  • 如果不是虚函数,是通过对象(而非引用或指针)进行调用,编译器将产生一个常规函数调用

名字查找优先类型检查

   声明在内层作用域的函数并不会重载声明在外层作用域的函数、因此,定义在派生类中的函数也不会重载其基类中的成员
   如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,那么派生类将在其作用域内隐藏该基类成员,即使该派生类成员与基类成员的形参列表不一致,基类成员仍然会被隐藏。


派生类的默认成员函数

派生类的构造函数

   派生类对象中含有从基类继承过来的成员,但是派生类并不能直接初始化这些成员,派生类必须使用基类的构造函数初始化它的基类部分。

   派生类对象的基类部分与它自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。派生类构造函数同样是通过构造函数初始化列表来讲实参传递给基类的构造函数。
且看下面分析:

class Person
{
public:
	Person(const string name,const string sex,int age)
		:_name(name),_sex(sex),_age(age){}
protected:
	string _name; // 姓名
	string _sex;  // 性别
	int _age;     // 年龄
};
class Teacher : public Person
{
public:
	Teacher(const string name, const string sex, int age,int No)
		:Person(name,sex,age),_No(No)
	{}
	int _No; // 学号
};

   Teacher自己的构造函数,将前三个参数传递给Person的构造函数,Person的构造函数负责初始化Teacher的基类部分,接下来初始化派生类自己定义的成员,最后运行Teacher空的函数体。
   除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化,如果你想使用其他基类的构造函数,我们需要以类名加圆括号内的实参列表的形式来为构造函数提供初始值。这些实参将告诉编译器到底使用哪一个构造函数来初始化派生类的基类部分。

首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员

在这里插入图片描述
1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
2、子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构

默认成员函数规则总结:
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造
6. 派生类对象析构清理先调用派生类析构再调基类的析构
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系


派生类的声明

  派生类的声明与其他类差别不大,声明包含类名但是不包含它的派生列表

class Student : Person; //错误:派生列表不能出现在声明
class Student;          //正确声明方式

一条声明语句的目的是令程序知晓某个名字的存在,以及该名字表示一个什么样的实体。派生列表以及定义有关的细节必须与类的主体一起出现。

用作基类的类

如果我们想将某个类用作基类,那么这个类必须已经定义而非只声明

一个类是基类,同时它也可以是一个派生类,但是一个类不能派生它自己。

class Person{...};
class Teacher : private Person{...};
class Child : public Teacher{...};

  在这个继承关系中,Person是Teacher的直接基类,同时也是Child的间接基类。
  每个类都会继承直接基类的所有成员。最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。

继承与友元

   就像友元关系不能传递一样,友元关系同样不能继承。基类的友元在访问派生类的成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员

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;
	cout << s._stuNum << endl;
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

Display是基类Person的友元,cout << s._stuNum << endl;这条语句想要访问Students的受保护成员_stuNum,显然是不可以的。基类友元不能访问派生类私有和保护成员。
不能继承友元关系,每个类负责控制各自成员的访问权限


继承与静态成员

  如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不论从基类中派生多少个派生类出来,对于每个静态成员来说都只存在唯一实例。

class Person
{
public:
	static string _age;
};

静态成员遵循通用的访问规则,如果基类中成员是private,那么派生类无权访问它。假设某静态成员是可访问的,那么我们既可以通过基类也能通过派生类使用它。


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

<think>我们正在讨论C++继承中子类的默认成员函数生成规则。根据引用[1]和引用[2],我们可以知道: 1. 子类(派生类)会继承父类(基类)的成员,但子类自己的默认成员函数(构造函数、拷贝构造、赋值重载、析构函数)的生成规则普通类有所不同。 2. 引用[1]提到:“派生类的默认成员函数”一节中详细说明了子类默认生成的成员函数和子类显示写的成员函数。 具体规则如下(参考引用[1]和引用[4]): - **构造函数**:派生类在初始化时,会先调用基类的构造函数来初始化基类部分,然后再初始化派生类自己的成员。如果派生类没有显式定义构造函数,编译器会生成一个默认构造函数,该默认构造函数会调用基类的默认构造函数(如果基类没有默认构造函数,则必须显式调用基类的某个构造函数)。 - **拷贝构造**:派生类的默认拷贝构造函数会调用基类的拷贝构造函数完成基类部分的拷贝,然后按派生类自己的成员变量类型进行拷贝(内置类型按字节拷贝,自定义类型调用其拷贝构造函数)。如果派生类没有显式定义拷贝构造函数,编译器会生成一个。 - **赋值重载**:派生类的默认赋值运算符重载函数会调用基类的赋值运算符重载函数完成基类部分的赋值,然后再对派生类自己的成员进行赋值。如果派生类没有显式定义赋值运算符重载,编译器会生成一个。 - **析构函数**:派生类的析构函数会在函数体执行完后自动调用基类的析构函数(即使我们不显式调用)。注意:编译器生成的派生类析构函数会调用基类的析构函数。另外,析构函数的名字会被统一处理为`destructor()`,所以不会构成重载,且基类的析构函数建议声明为虚函数(特别是在多态使用时)。 引用[1]还提到:子类显示写这些成员函数时,需要注意基类部分的处理。例如: - 在派生类的构造函数初始化列表中,必须显式调用基类的构造函数(如果不显式调用,则基类必须有默认构造函数,否则编译失败)。 - 在派生类的拷贝构造函数初始化列表中,必须显式调用基类的拷贝构造函数,否则基类部分将会调用默认构造函数(而不是拷贝构造),这通常不是我们想要的。 - 在派生类的赋值运算符重载函数中,必须显式调用基类的赋值运算符重载函数,否则基类部分不会被赋值。 - 在派生类的析构函数中,不需要显式调用基类的析构函数(编译器会自动在派生类析构函数体执行完后调用基类析构函数),且注意基类析构函数是否要声明为虚函数。 引用[4]和引用[5]强调了继承组合的差异,以及继承带来的耦合问题,但这不是我们当前问题的重点。 因此,总结规则: 1. 如果派生类没有显式定义这些成员函数,编译器会生成默认的,这些默认成员函数会调用基类对应的成员函数(对于构造函数、拷贝构造、赋值运算符)或自动调用基类析构函数(对于析构函数)。 2. 如果派生类显式定义了这些成员函数,则必须注意基类部分的处理: - 构造函数:在初始化列表中显式调用基类构造函数。 - 拷贝构造:在初始化列表中显式调用基类拷贝构造函数。 - 赋值运算符:在函数体内显式调用基类赋值运算符。 - 析构函数:不需要显式调用基类析构函数(会自动调用),但应确保基类析构函数是虚函数(当需要多态时)。 下面我们用一个简单的代码示例来说明: ```cpp #include <iostream> using namespace std; class Base { public: Base() { cout << "Base()" << endl; } Base(const Base& b) { cout << "Base(const Base&)" << endl; } Base& operator=(const Base& b) { cout << "Base::operator=()" << endl; return *this; } ~Base() { cout << "~Base()" << endl; } }; class Derived : public Base { public: // 默认构造函数:如果不写,编译器会生成,并调用Base的默认构造函数 // 拷贝构造:如果不写,编译器会生成,并调用Base的拷贝构造 // 赋值重载:如果不写,编译器会生成,并调用Base的赋值重载 // 析构函数:如果不写,编译器会生成,并在函数体执行完后自动调用Base的析构函数 }; int main() { Derived d1; // 调用Base的默认构造函数 Derived d2(d1); // 调用Base的拷贝构造函数 d1 = d2; // 调用Base的赋值运算符重载 return 0; } // 自动调用Derived的析构函数(然后自动调用Base的析构函数)两次(d2和d1) ``` 输出: ``` Base() Base(const Base&) Base::operator=() ~Base() ~Base() ``` 如果我们显式定义派生类的成员函数,则必须注意基类部分的处理: ```cpp class Derived : public Base { public: // 显式定义构造函数 Derived() : Base() { } // 显式调用基类默认构造函数,可以省略:Base(),因为默认会调用 // 显式定义拷贝构造 Derived(const Derived& d) : Base(d) { } // 必须显式调用基类拷贝构造函数,否则会调用基类默认构造函数 // 显式定义赋值运算符 Derived& operator=(const Derived& d) { if (this != &d) { Base::operator=(d); // 显式调用基类赋值运算符 } return *this; } // 显式定义析构函数 ~Derived() { // 不需要显式调用~Base(),编译器会自动在析构函数体执行后调用基类析构函数 } }; ``` 注意:在拷贝构造函数中,如果不显式调用基类的拷贝构造函数,则基类部分将调用默认构造函数,这通常是不正确的。同样,在赋值运算符中,如果不显式调用基类的赋值运算符,基类部分将不会被赋值。 最后,引用[3]提到了静态成员在继承中的特性:整个继承体系共享同一个静态成员。 以上内容总结自引用[1][2][3][4][5]。 §§相关问题§§ 1. 在C++继承中,为什么基类的析构函数通常应该声明为虚函数? 2. 如果基类没有默认构造函数,派生类应该如何设计构造函数? 3. 在派生类的拷贝赋值运算符中,为什么要先检查自赋值? 4. 多继承情况下,派生类的默认成员函数生成规则有什么不同?</think>### C++继承中子类的默认成员函数生成规则 在C++继承体系中,派生类(子类)的默认成员函数(构造函数、拷贝构造、赋值重载、析构函数)的生成规则如下,结合引用内容详细说明: --- #### 1. **默认构造函数** - **生成规则**:若派生类未显式定义构造函数,编译器会自动生成**合成的默认构造函数**。 - **工作流程**: 1. 先调用**基类的默认构造函数**(若基类无默认构造函数则编译报错) 2. 再按声明顺序初始化**派生类的成员变量**(内置类型不初始化,自定义类型调用其默认构造) ```cpp class Base { public: Base() { cout << "Base()"; } // 基类默认构造 }; class Derived : public Base { int x; // 内置类型不初始化 public: // 编译器生成:Derived() : Base() {} }; ``` --- #### 2. **拷贝构造函数** - **生成规则**:若派生类未显式定义拷贝构造,编译器生成**合成的拷贝构造函数**。 - **工作流程**: 1. 调用**基类的拷贝构造**(传递派生类对象时会切片为基类引用) 2. 对派生类成员执行**逐成员拷贝** ```cpp Derived(const Derived& d) : Base(d) // 调用 Base(const Base&) [切片发生] , x(d.x) {} // 拷贝派生类成员 ``` --- #### 3. **赋值运算符重载(operator=)** - **生成规则**:若派生类未显式定义,编译器生成**合成的赋值运算符**。 - **工作流程**: 1. 调用**基类的 operator=** 2. 对派生类成员执行**逐成员赋值** ```cpp Derived& operator=(const Derived& d) { Base::operator=(d); // 调用基类赋值 x = d.x; // 赋值派生类成员 return *this; } ``` --- #### 4. **析构函数** - **生成规则**:编译器始终自动生成析构函数(即使派生类未显式定义)。 - **工作流程**: 1. 执行**派生类析构函数体**(若显式定义) 2. **自动销毁派生类成员**(逆序调用成员析构) 3. **自动调用基类析构函数**(无需显式调用) ```cpp ~Derived() { // 此处可添加派生类资源释放 // 编译器自动插入:1. 成员析构 2. Base::~Base() } ``` --- ### 关键注意事项 1. **基类依赖**: - 若基类无默认构造,派生类必须**显式调用基类构造函数**(通过初始化列表)[^1]。 - 基类拷贝构造/赋值运算符需可访问(`public`或`protected`)。 2. **资源管理**: - 当派生类有资源(如动态内存)时,必须**显式定义**拷贝构造、赋值运算符和析构函数(遵循“/五法则”)。 3. **析构函数特殊性**: - 派生类析构函数会**自动调用基类析构函数**(即使不显式写)。 - 多态基类应声明**虚析构函数**(确保通过基类指针删除派生类对象时行为正确)[^4]。 4. **静态成员继承**: - 基类的静态成员在整个继承体系中**共享唯一实例**,派生类可直接访问[^3]。 --- ### 示例说明 ```cpp class Base { protected: string _name; public: Base(const string& s) : _name(s) {} Base(const Base& b) : _name(b._name) {} }; class Derived : public Base { int _id; public: // 编译器生成默认构造:Derived() : Base(), _id(0) {} // 编译器生成拷贝构造:Derived(const Derived& d) : Base(d), _id(d._id) {} // 编译器生成赋值:Derived& operator=(const Derived& d) { // Base::operator=(d); _id = d._id; // } // 编译器生成析构:~Derived() { 自动调用 ~Base() } }; ``` ---
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

侠客cheems

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值