1、构造函数(constructor)是什么
答:类里面定义一个函数, 和类名一样, 这样在我们生成一个对象之后,就会默认调用这个函数,初始化这个类。
子类B继承父类A的情况, 当你调用子类的对象,构造函数会怎么样运行?
-
1、创建子类对象时,程序首先调用父类的构造函数,然后再调用子类的构造函数
- 父类构造函数负责初始化继承的数据成员
- 子类构造函数主要用于初始化新增的数据成员
-
2、子类构造函数总是需要调用一个父类构造函数;默认就是调用父类无参数构造函数,如果父类没有这个构造函数,程序会默认生成一个无参数构造的,如果父类又 有参数构造函数,但是没有无参数的构造函数,就会报错。
- 所以当父类没有无参数的构造函数时,就必须明确指明调用哪一个构造函数,需要用到初始化列表这个方式。
Person(int age, char *address): age(age), address(address)
2、final用于禁止被重写和继承,确保自己的唯一,是最后一个。
- 1 、禁止类自己被继承
final 用于禁止 虚函数被重写和 类被继承class Person final {}
,然后这个Person就不可以被继承了, - 2、 一个类, 自己继承了别人后,禁止别人继承自己。
class Person {};class Student final: public Person {};class Middel_School_Student : public Student{ // 失败,因为父类Student是final修饰的};
就是意味着继承了Person。但是Student不能被别人继承。
- 3、final用于禁止函数被重写
class Person { virtual void run() final { // 表示该方法是最终方法,无法被重写 }
};class Student : public Person{// 编译错误,方法无法被重写。void run(){ }
};
final总是放在定义类的后面,和方法的后面, 类是 class A final {}. 和 class A :final public B {}.方法是
class Person { virtual void C() final { // 表示该方法是最终方法,无法被重写 }
};
也是在后面,但是在()前。
3、 友元 friend ,目的是为了使用私有属性的函数和变量。 类似一个白名单
如果一个类A里面有私有的方法和属性,当类生成一个对象 a ,肯定是无法调用这些方法和属性的,如果在类A里面,加上 友元函数 或者友元类 之后,这个友元函数内部可以无视private的影响。 友元类里也可以无视private的影响。
5、重载
C++中,一个类里,如果有两个函数名一样的函数,但是入参不同,就属于重载
4、关于 继承
1、继承是什么
继承就是把一个类里的变量和方法 “继承” 给另一个类。如果B类 继承 A类,我们就叫B类是子类, A类是B类的父类,B类有A类的所有功能和方法(先这样粗浅的理解)。
2、继承的写法
继承有三种方式 pubulic prtected private 和类的封装时候的三种方式一样。不写默认就是private
class 子类名:[继承方式]父类名{ 子类新增加的成员
};
2、继承的用法
- 子类继承父类之后,拥有了一套和父类一样的变量和方法,即可以使用父类的方法和变量的,如果想要使用父类中继承下来的变量和函数。就在子类中使用 this-> 【方法和变量】 ,就可以调用。
3、继承需要注意
-
类本身是一个抽象的概念,类本身不占用内存空间;它仅仅是一个蓝图或模板,用于创建对象。只有当类被实例化为对象时,才会分配内存空间。每个对象都有其独立的地址。所以当我们用类生成对象时,每个对象都是一个独立的个体。
-
比如:如果C++中,一个类B,继承于类A, 则类A的对象中的变量var_a, 在类B的对象中也会有,如果类B生成一个对象obj_b, 那么这个对象obj_b里面也有var_a。 这个时候,使用类A的方法调用var_a和 使用类B的方法调用var_a,都可以调用到var_a, 并且这两个var_a是同一个,即地址是一样的。
#include <iostream>using namespace std;class A {
public:int var_a = 4;void get_addr() {cout << "IN A : var_a ==" << var_a << "\n";cout << "IN A : var_a address ==" << &var_a;}
};class B : public A {
public:int var_b;void get_addr1() {cout << "IN B : var_a ==" << var_a << "\n";cout << "IN B : var_a address ==" << &(this->var_a) << "\n";}void get_addr2() {A::get_addr();}};int main() {B obj_b;obj_b.var_a = 10;//打印在对象obj_b里, 调用类B变量 var_a的值和地址obj_b.get_addr1();cout<<"================================================="<<"\n";//打印在对象obj_b里, 调用类A变量 var_a的值和地址obj_b.get_addr2();return 0;
}
- 4、继承之后怎么用( 等重写,多态知道了之后再看)
继承之后,子类的对象,自然可以调用父类的一切(在子类的函数中,调用从父类继承而来的成员变量或成员函数,直接使用this),那么父类的对象,可以调用子类么 ?
-
子类对象可以调用父类的方法:
当一个类(子类)继承自另一个类(父类)时,子类对象可以调用从父类继承来的所有公有(public)和保护(protected)成员(包括方法和属性)。如果父类的方法或属性是私有的(private),则子类无法直接访问它们,但可以通过父类的公有或保护方法来间接访问。 -
父类对象不能调用子类的方法:
父类对象只能调用自己定义的公有和保护成员,不能调用子类特有的方法或属性。这是因为父类对象在内存中的布局并不包含子类的任何额外成员或方法。换句话说,父类对象对子类扩展的部分一无所知。 -
多态性:
虽然父类对象不能直接调用子类的方法,但可以通过指向子类对象的父类指针或引用来实现多态性。这通常是通过在父类中定义虚函数(virtual function)来实现的。当子类重写了父类的虚函数时,通过父类指针或引用调用的将是子类版本的函数。
示例代码:
#include <iostream>class Parent {
public:void parentMethod() {std::cout << "Parent method called" << std::endl;}virtual void virtualMethod() {std::cout << "Parent virtual method called" << std::endl;}
};class Child : public Parent {
public:void childMethod() {std::cout << "Child method called" << std::endl;}void virtualMethod() override {std::cout << "Child virtual method called" << std::endl;}
};int main() {Parent parent;Child child;// 子类对象调用父类方法child.parentMethod();// 子类对象调用自己的方法child.childMethod();// 通过父类指针调用子类重写的虚函数Parent* parentPtr = &child;parentPtr->virtualMethod(); // 输出:Child virtual method called// 父类对象不能调用子类方法// parent.childMethod(); // 编译错误return 0;
}
在这个例子中:
child
对象可以调用parentMethod()
和childMethod()
。- 通过父类指针
parentPtr
调用virtualMethod()
时,实际调用的是子类的virtualMethod()
,展示了多态性。 - 尝试通过父类对象
parent
调用childMethod()
会导致编译错误,因为父类对象不知道子类的任何扩展。
总结:子类对象可以调用父类的公有和保护成员,但父类对象不能调用子类的成员。多态性允许通过父类指针或引用来间接调用子类的重写方法。
4 、 那么 ,是不是通过父类指针,调用子类的成员,一定是父类也拥有的成员才可以?
是的,通过父类指针调用子类的成员时,只能调用父类也拥有的成员。这是因为多态是基于父类指针或引用来实现的,编译器在编译时期会根据指针或引用的类型来确定要调用的成员函数。如果子类有额外的成员,且这些成员在父类中没有对应的声明,那么通过父类指针或引用是无法调用这些成员的。
5、重写
重写就是子类继承父类之后,子类中,定义了父类相同名字的函数。
重写之后,子类直接调用,就是调用的子类自己的函数。 父类中所有的同名的函数(为啥说所有,是因为父类中可能有重载的函数,所以会有同名的函数 ),都被子类的同名函数覆盖了,即父类中所有的同名函数,子类都不会继承,子类只用自己重写的那个,如果你想在子类中调用这个被你重写的,就需要用父类名去调用 【父类名】::【父类的重写的方法】 。
#include <iostream>using namespace std;class A {
public:void display() {cout << "A::display()" << "\n";}// 1、重载一个display函数void display(int num) {cout << "A::display(int num)" << "\n";}
};class B : public A {
public: // 2、重写一个display. 重写之后,自然而然的,父类的void display() 方法被顶替了,并且 void display(int num)也被顶替了(重载的全被顶替了)// 综上 父类的两个display都不不能直接调用了。void display() {cout << "B::display()" << "\n"; //3、 this->display(100); // 编译失败,原因如上面解释的一样 ,但是如果想使用父类的函数也是可以的, 方法1,需要用父类,类A来调用方式如下cout <<"---------------------------------------------------------"<< "\n";A::display(); A::display(100); }};int main() { class B b; b.display(); // 调用子类的函数 //4、同2表示的一样 // b.display(100); // 编译失败 return 0;
}
6、 多层继承和多继承
- 多层继承(Hierarchical Inheritance)就是 继承了 很多层,比如 爷爷继承给父亲,父亲继承给儿子,继承了3层。
- 多继承(Multiple Inheritance)就是 同时继承了多个, 比如 父亲母亲,同时继承给儿子。
7、 多态
1、多态就是,有多个相同名字的函数,当调用时,会根据调用时的方式不同,调用不同的函数。比如一个人,同样是说话这个行为,对有的人态度好,对有的人态度差,展现了不同的“态度”
2、C++中通过 visual (虚函数)来实现多态。
-
通过 virtual可以实现真正的多态
virtual (虚函数 )可以在父类的指针指向子类对象的前提下,通过父类的指针调用子类的成员函数
这种技术让父类的指针或引用具备了多种形态,这就是所谓的多态 -
最终形成的功能:
- 如果父类指针指向的是一个父类对象,则调用父类的函数
- 如果父类指针指向的是一个子类对象,则调用子类的函数
3、怎么使用 visual
- 定义 visual ( 虚函数) 非常简单,只需要在函数声明前,加上 virtual 关键字即可
- 在父类的函数上添加 virtual 关键字,可使子类的同名函数也变成虚函数,可以子类父类都写上visual
#include<iostream> using namespace std; class Father {
public: virtual void show() { cout << "father show" << endl; }
};class Children : public Father {
public: virtual void show() { cout << "children show" << endl; }
};int main() { Father *father = new Father(); father->show(); // 调用父类的show函数 Children *children = new Children();children->show(); // 调用子类的show函数Father *p = new Children();p->show(); // 调用哪个类中的show函数? 就看父类的指针,指向了哪个对象,指向的是子类,所以调用的就是子类的函数return 0;
}
总结: 多态需要以下三个条件
1、有继承关系
2、子类重写了父类的函数
3、定义一个父类的指针变量,指向子类调用子类,指向父类,调用父类。
8、 初始化列表的前序
- 先说一下初始化列表作用和构造函数一样,构造函数是有默认的逻辑的,但很多时候,默认的逻辑不太像,需要我们指定,就用到了初始化列表。初始化列表,就用来明确指出,到底用哪个构造函数。
之前说构造函数是类生成一个对象之后,自动调用用来初始化的函数,如果一个类A里有一个 构造函数,类B继承了类A,那么当类B生成一个对象后,有几种情况。
1、类B 在初始化一个对象之后,会调用A里面这个构造函数么 ?
会
2、类B 里面 如果有一个自己的构造函数,类B的对象会调用哪个构造函数呢 ?
都会调用,且顺序是先调用父类的构造函数,即 A 里面的构造函数,再调用B里面的构造函数。
#include <iostream>using namespace std;class A {
public:A() {cout << this << "父类A的 无参数构造函数被调用\n";}
};class B : public A {
public:B() {cout << this << "父类B的 无参数构造函数被调用\n";}
};int main( ) { B obj_1; return 0;
}
3、构造函数在类中,可以有多个,区别只是参数不同,那么我们默认的调用顺序是什么呢?
- 首先还是先调用父类的构造函数,再调用子类的构造函数
- 当父类有 无参构造函数 时,在自动调用子类构造函数之前,一定会自动调用父类的无参构造函数
- 父类没有无参构造函数,那么子类必须指定父类调用哪一个,即使父类只有一个带参数的构造函数,否则编译会报错。这个和子类调用自己的构造函数不同,调用自己的还是和单个类生成对象一样, 即 :看传入的参数和哪个构造函数入参类型一样
//当父类有无参构造函数时,在自动调用子类构造函数之前,一定会自动调用父类的无参构造函数
#include <iostream> using namespace std; class A {
public: A() { cout << this << "父类A的 无参数构造函数被调用\n"; }A(int temp) { cout << this << "父类A的 有参数构造函数被调用\n"; }
};class B : public A {
public:B() {cout << this << "父类B的 无参数构造函数被调用\n";}B(int temp) {cout << this << "父类B的 有参数构造函数被调用\n";}
};int main() {B obj_1;cout << "---------------\n";B obj_2(11);return 0;
}
//父类没有***无参构造函数***,那么子类**必须指定**父类调用哪一个,即使父类只有一个带参数的构造函数,否则编译会 报错 Constructor for 'B' must explicitly initialize the base class 'A' which does not have a default constructor “B”的构造函数必须显式初始化没有默认构造函数的基类“A”#include <iostream>using namespace std;class A {
public:
// A() {
// cout << this << "父类A的 无参数构造函数被调用\n";
// }A(int temp) {cout << this << "父类A的 有参数构造函数被调用\n";}
};class B : public A {
public:B() {cout << this << "父类B的 无参数构造函数被调用\n";}B(int temp) {cout << this << "父类B的 有参数构造函数被调用\n";}
};int main(int argc, char **argv) {B obj_1;cout << "---------------\n";B obj_2(11);return 0;
}
4、C++为什么要设计成,子类对象生成的时候,父类A中的构造函数也会被调用呢?
简单点理解,因为,如果父类中如果有private修饰的成员变量的话,在子类中是无法被调用的,也就是意味着子类不能够对这些private成员变量进行初始化,因此就需要调用父类的构造函数
AI解释一下:
C++设计成在子类对象生成时,父类(或基类)A中的构造函数也会被调用的原因,主要基于面向对象编程(OOP)的继承机制和对象构造的逻辑。以下是对这一设计的详细解释:
-
对象构造的完整性:
在C++中,一个子类对象是由其基类部分和自身特有的部分组成的。为了确保子类对象在构造时能够完整地表示其应有的状态,基类部分必须被正确初始化。这包括基类成员变量的初始化和基类构造函数中可能执行的任何其他初始化操作。 -
资源管理和依赖关系:
基类构造函数通常负责初始化基类对象所需的资源,如内存分配、文件打开、网络连接等。如果基类构造函数不被调用,那么这些资源可能不会被正确分配或初始化,从而导致资源泄漏、访问违规或程序崩溃。此外,基类构造函数还可能设置基类对象与其他对象之间的依赖关系,这些关系在子类对象中也必须得到维护。 -
多态性和接口一致性:
C++支持多态性,允许通过基类指针或引用来操作派生类对象。为了确保这种操作的安全性,基类部分必须被正确初始化。如果基类构造函数不被调用,那么通过基类接口访问派生类对象时可能会遇到未定义的行为。此外,基类构造函数还可能提供了一组通用的初始化逻辑,这些逻辑在派生类中应该被保留和扩展。 -
构造顺序和初始化链:
C++规定了对象的构造顺序:首先调用基类的构造函数(如果有多个基类,则按照它们在派生类中的声明顺序依次调用),然后按照成员变量的声明顺序调用成员变量的构造函数(如果有的话),最后调用派生类的构造函数。这种顺序确保了对象在构造时的逻辑一致性和可预测性。 -
代码重用和减少冗余:
如果每个派生类都需要手动复制基类的初始化代码,那么这将导致大量的代码冗余和难以维护。通过自动调用基类构造函数,C++允许派生类重用基类的初始化逻辑,从而简化代码并提高可维护性。
综上所述,C++设计成在子类对象生成时自动调用父类构造函数,是为了确保对象构造的完整性、正确管理资源、维护多态性和接口一致性、保证构造顺序的逻辑一致性以及减少代码冗余和提高可维护性。这是面向对象编程中继承机制的一个基本特性,也是C++语言设计的一个重要方面。
总结:
-
创建子类对象时,程序首先调用父类的构造函数,然后再调用子类的构造函数
-
父类构造函数负责初始化继承的数据成员
-
子类构造函数主要用于初始化新增的数据成员
-
-
子类构造函数总是需要调用一个父类构造函数;当父类没有无参数的构造函数时,就必须显式指明调用哪一个构造函数
5、怎么显示的指明调用哪个构造函数呢,就需要用到 初始化列表 了。