C++ 基础系列

您所在的位置:网站首页 c加加类中不能被派生类继承的有 C++ 基础系列

C++ 基础系列

2023-12-19 23:18| 来源: 网络整理| 查看: 265

1. 继承和派生入门

继承可以理解为一个类在另一个类的成员变量和成员函数上继续拓展的过程。这个过程站的角度不同,概念也会不同,继承是儿子接收父亲,派生是父亲传承给儿子。

被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类” 和“派生类”通常放在一起称呼。

// 基类 People class People { public: void set_name(string name); void set_age(int age); string get_name(); int get_age(); private: string m_name; int m_age; }; // 派生类 Studeng // public 表示公有继承,下一节会讲继承方式问题 class Student:public People { public: void set_score(float score); float get_score(); private: float m_score; }; int main() { Student stu; stu.set_name("小明"); // 继承基类 stu.set_score(99.9); return 0; }

继承方式有 public、private、protected,如果不写,默认 private。(结构struct 默认继承方式是 public)

2. 三种继承方式

继承方式限定了基类成员在派生类中的访问权限,三种方式分别是:public、private、protected。

类的public 成员可以通过对象来访问,private 成员不能通过对象和派生类访问,而 protected 也不能通过对象访问,但基类的 protected 成员可以在派生类中访问。

不同的继承方式会影响基类成员在派生类中的访问权限:

public 继承 基类中 public、protected 成员在派生类保持基类的属性。(基类private 成员不能在派生类中使用) protected 继承 基类的 public、protected 成员在派生类中均为 protected 属性。 private 继承 基类的 public、protected 成员在派生类中均为 private 属性。

对于基类中既不向外暴露(不能通过对象访问),还能在派生类中使用的成员,只能声明为 protected。

注意,基类的 private 成员是能够被继承的,并且成员变量一样会占用派生类的内存,只是在派生类中不可见。

public 成员 protected 成员 private 成员 public 继承 public protected 不可见 protected 继承 protected protected 不可见 private 继承 private private 不可见 由于 private 和 protected 继承方式会改变基类成员在派生类的访问权限,导致继承关系复杂,实际开发中通常使用 public。 派生类中访问基类 private 成员的唯一方法是借助基类的非 private 成员函数。如果基类未提供,则派生类中无法访问。 改变访问权限

使用 using 关键字可以改变基类成员在派生类的访问权限。只能改变基类中 public 和 protected 成员的访问权限。

// 基类 class People { public: void show(); protected: string m_name; int m_age; }; // 派生类 class Student: public People { public: using People::m_name; // 将 protected 改为 public float m_score; private: using People::m_age; // 将 protected 改为 private using People::show; // 将 public 改为 private }; 3. 继承时名字遮蔽问题

名字遮蔽指的是,当派生类成员与基类成员重名时,派生类使用的是该派生类新增的成员,基类的成员会被遮蔽。

对于成员函数来说,只要派生类成员函数与基类名字一样,就会造成遮蔽,遮蔽与参数无关,只与函数名称有关。也就是说,基类的成员函数与派生类成员函数不会构成函数重载。

// 基类 class People { public: void show(); protected: string m_name; int m_age; }; class Student: public People { public: Student(string name, int age, float score); void show(); // 基类 show 函数遮蔽 private: float m_score; }; int main() { Student stu("小明",16,99,9); stu.show(); // 派生类 show 注意:如果派生类没有覆写基类的 show 函数,此时会调用基类的 show,下一节有讲具体原因。 stu.People::show(); // 基类 show }

如果派生类要访问基类中被遮蔽的函数,需要加上类名和域名解析符。

4. 继承时作用域的嵌套

类其实也是一种作用域,每个类都有自己的作用域,在这个作用域内再定义类的成员。当存在继承关系时,派生类的作用域嵌套在基类的作用域之内。

当派生类对象访问成员时,会在作用域链中寻找最匹配的成员。对于成员变量,会直接查找,但是对于成员函数,编译器仅仅根据函数名字来查找,当内层作用域有同名函数时,不管有几个,编译器都不会再到外层作用域中查找,而是将这些同名函数作为一组重载候选函数。

// 基类 class Base { public: void func(); void func(int); }; // 派生类 class Derived: public Base { public: void func(string); void func(bool); }; int main() { Derived d; d.func("test"); // 派生类 Derived 域中匹配 d.func(true); // 派生类 Derived 域中匹配 d.func(); // 编译错误,在派生类中找到了同名函数,因此不会再去基类匹配,但派生类中无法匹配 d.func(10); // 编译错误,在派生类中找到了同名函数,因此不会再去基类匹配,但派生类中无法匹配 d.Base::func(); d.Base::func(100); return 0; } 5. 继承时的对象内存模型

对于没有继承时的对象内存模型很简单,成员变量和成员函数会分开存储:对象的内存中只包含成员变量,存储在栈区或堆区(new),成员函数与对象内存分离,存储在代码区。

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,所有成员函数仍存储在代码区,由所有对象共享。

在派生类的对象模型中,会包含所有基类的成员变量,这种设计方案的优点是访问效率高,能直接访问。当存在遮蔽问题时,被遮蔽的成员变量仍然会留在内存中,只是对于存在遮蔽问题的成员变量,会增加类名和域名解析符::。

6. 基类和派生类的构造函数

类的构造函数不能被继承,并且对于继承过来的基类的成员变量的初始化,需要派生类的构造函数完成,通常是通过调用基类的构造函数完成。

class People { public: People(string, int); protected: string m_name; int m_age; }; class Student:public People { public: Student(string name, int age, float score); private: float m_score; }; // 派生类构造函数,调用基类的构造函数完成基类成员变量的初始化 // 基类构造函数的调用只能放在函数头部,不能放在函数体中。 Student::Student(string name, int age, float score): People(name, age), m_score(score){ } 构造函数调用顺序

在创建派生类对象时,会先调用基类构造函数, 再调用派生类构造函数。构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

基类构造函数调用规则

通过派生类创建对象时必须调用基类构造函数,如果没有指明基类构造函数,会调用基类的默认构造函数,如果基类默认构造函数不存在,会编译错误。

7. 基类和派生类的析构函数

析构函数也不能被继承。并且在派生类的析构函数中不用显式调用基类的析构函数,因为每个类只有一个析构函数。

此外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

创建派生类时,构造函数调用顺序自顶向下、从基类到派生类; 销毁派生类时,析构函数执行顺序是自下向顶,从派生类到基类。

8. 多继承

多继承语法:

class D : public A, private B, protected C { };

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。

D(形参列表): A(实参列表), B(实参列表), C(实参列表){ //其他操作 }

基类构造函数的调用顺序和它们在派生类构造函数的出现顺序无关,只和声明派生类时基类出现的顺序相同。

当多个基类拥有同名的成员时,派生类调用时需要加上类名和域解析符::。

9. 指针突破访问权限的限制

C++不能通过对象来访问 private、protected 属性的成员变量,但是通过指针,能够突破这种限制。

class A{ public: private: int m_a; int m_b; int m_c; }; A::A(int a, int b, int c): m_a(a), m_b(b), m_c(c){ } int main(){ A obj(10, 20, 30); int a = obj.m_a; // 编译错误,无法访问 protected、private 成员 A *p = new A(40, 50, 60); int b = p -> m_b; // 编译错误 return 0; }

在对象的内存模型中,成员变量和对象的开头位置会有一定的距离。以上面的 obj 对象为例,它的内存模型:

一旦知道了对象的起始地址,再加上偏移就能求得成员变量的地址,如果知道了成员变量的类型,就能轻易获得其值。

实际上,通过对象访问成员变量时,编译器也是通过这种方式来取得它的值:(假设 m_b 成员变量此时为 public)

int b = p->m_b; 此时编译器会将其转换为: int b = *(int*) ((int)p + sizeof(int));

p 是对象 obj 的指针,(int)p 将指针转换为一个整数,这样才能进行加法运算;sizeof(int)用来计算 m_b 的偏 移;(int)p + sizeof(int)得到的就是 m_b 的地址,不过因为此时是 int 类型,所以还需要强制转换为 int*类型,开头的*用来获取地址上的数据。

// 通过指针突破访问权限限制访问 private 成员 int main(){ A obj(10, 20, 30); int a1 = *(int*)&obj; // 10 int b = *(int*)( (int)&obj + sizeof(int) ); // 20 A *p = new A(40, 50, 60); int a2 = *(int*)p; // 40 int c = *(int*)( (int)p + sizeof(int)*2 ); // 60 cout


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3