继承派生析构多态与花式计算的顺序

继承 析构 派生 多态

就是class 然后public ,主要是执行逻辑顺序

  • 在构造层面,先有基类,再按照一次的派生顺序排到对应的派生类,
    父类有带参数的构造函数,也有默认构造函数,子类未显示调用父类的构造函数时,子类会调用父类的构造函数。要是父类没有写默认,那么子类继承也默认执行隐藏的默认构造函数,对应的含参是需要明显调用的。
    类似于看洋葱的内核,只有里面的洋葱核可以吃,所以需要从内部挖掘
  • 在析构方面,就是相反,析构是脱衣服,也就是外部的先拆,然后留下里面的
  • 函数执行,也就是普通的函数对应执行,先明确,构造是构造的思路,
    A a=new A () 是a 为A申明下的对象,然后用A的构造
    如果有派生的B, public了A,
    B a=new A();//也许你认为可以这样,但不能以下犯上,所以是不存在的
    B a=new B()也就是执行b是派生的构造,首先要执行基类然后是派生
    A a=new B(),new的时候也就是先基类然后是派生
    ,A申明还是A
  • 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数;
  • 当父类的指针new一个子类的对象时,,父类析构不是虚析构,则delete的时候不调用子类的,只是调用父类的析构函数,如果是virtual的析构函数,则先子类之后父类
    在这里插入图片描述
// 基类
class Parent {
public:
	Parent() { cout << "Struct Parent" << endl; };
	virtual ~Parent() {
		cout << "Destructor Parent" << endl;
	}
	virtual void fn() {
		cout << "Call Parent fn" << endl;
	}
};

// 派生类
class Child :public Parent {
public:
	Child() {
		cout << "Struct Child" << endl;
	}
	~Child() {
		cout << "Destructor Child" << endl;
	}
	virtual void fn() {
		cout << "Call Child fn" << endl;
	}
};
void fun() {
	Parent* ob1 = new Parent;
	ob1->fn();
	Parent* obj3 = new Child;
	obj3->fn();
	Child *ob2 = new Child;
	ob2->fn();
}

如果一个类有多个基类,基类的构造函数在继承类的构造函数之前被调用。基类的构造函数以被声明的顺序被调用。下面是一个例子:

class Y {...}
class X : public Y {...}
X one;
构造函数的调用顺序是下面的顺序:
Y(); // 基类的构造函数
X(); // 继承类的构造函数

对于多基类的情况,下面是一个例子:

class X : public Y, public Z
X one;
构造函数以声明的次序调用。
Y(); // 基类构造函数首先被调用
Z();
X();
  • 虚基类的构造函数在任何非虚基类构造函数前调用。如果构造中包括多个虚基类,它们的调用顺序以声明顺序为准。
  • 如果虚类是由非虚类派生而来,那非虚类的构造函数要先被调用。
  • 如果类继承中包括多个虚基类的实例,基类只被初始化一次,也就是虚函数的继承只执行一次
class X : public Y, virtual public Z
X one;
调用顺序如下:
Z(); // 虚基类初始化
Y(); // 非虚基类
X(); // 继承类

下面是一个复杂的例子:

class base;
class base2;
class level1 : public base2, virtual public base;
class level2 : public base2, virtual public base;
class toplevel : public level1, virtual public level2;
toplevel view;
构造函数调用顺序如下:
base(); // 虚基类仅被构造一次
base2();
level2(); // 虚基类
base2();
level1();
toplevel();

虚基类l2执行,再执行虚基类base,虚基类执行且执行一次,然后根据虚基类优先,所以是先base然后base2,level2,再public l1项目,l1构造系列里因为base虚过了,所以直接base2,再level1,最后top。

例子代码

#include <iostream>
#include<bits/stdc++.h>
using namespace std; 
class Base {
public:
	Base(int i) { 
		cout << "执行base" << endl;
		cout << i; }
	~Base() {}
};
class Base1 : virtual public Base {
public:
	Base1(int i, int j = 0) : Base(j) { 
		cout << "执行base1" << endl;
		cout << i; }
	~Base1() {}
};
class Base2 : virtual public Base {
public:
	Base2(int i, int j = 0) : Base(j) { 
		cout << "执行base2" << endl;
		cout << i; }
	~Base2() {}
};
class Derived : public Base2, public Base1
{
public:
	Derived(int a, int b, int c, int d) : mem1(a), mem2(b), Base1(c), Base2(d), Base(a)
	{
		cout << "执行der" << endl;
		cout << b;
	}
private:
	Base2 mem2; //显式
	Base1 mem1;
};
int main() {
	Derived obj(1, 5, 3, 8);
	return 0;
    /* 
	执行base   虚基类首先初始化
    1执行base2   按照顺序的虚基类初始化
    8执行base1     base1在base1后,根据derive的虚基类顺序
    3执行base  因为mem2是由base2命名的,属于复制构造函数,就是按照显式执行即可,虚函数可以直接多次
    0执行base2
    5执行base  mem1同理
    0执行base1
    1执行der
    5
    */
}
  • 初始化是显式的调用拷贝构造 类似于mem1
  • 当使用对象进行值传递的时候,会发生隐式的调用拷贝构造,理解为拷贝就是转移,并不是重新生成。 下面的a new

构造指针与对象

#include <iostream>
#include<string>
using namespace std; 
class AA{
private:
	int aa;
public:
	static int count;
	AA(int a = 0) :aa(a) {
		count++;
	}
	~AA() {
		count--;
	}
};
int AA::count;
int main() {
	AA e(5);
	cout << AA::count << endl;
	AA a, b(3), c(2), * d;
	cout << a.count << endl;
	d = new AA(b);
	cout << d->count << endl;
	delete d;
	cout << a.count << endl;
	return 0;
}
  • 在初始化的时候,执行A a,b(3),c(2),*d;时候,只有前三种是可以的,第四种因为是指针但是没有指向对象,所以不能初始化。
  • 在使用隐式调用拷贝构造函数的时候,count是没有变换的//隐式
  • 类的全局对象,在进入主函数之前就会被初始化,单纯定义类指针而没有指向则不会调用类的构造函数。
#include<bits/stdc++.h>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 0, int month = 0, int day = 0){
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)            //这里必须使用引用,否则就会变成 值传递对象
	{
		this->_year = d._year;
		this->_day = d._day;
		this->_month = d._month;
		cout << "拷贝构造函数被调用" << endl;
	}
};
void operator=(Date d)      //注意这里是 值传递
//这里定义了一个 赋值运算符重载函数,参数使用的是 值传递
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
//如果  void operator=(Date d)  改为  void operator=(Date& d)
//此时就是引用传递,不会调用拷贝构造函数
Date operator=(Date& d)      //注意这里是 值传递
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
 
    return *this;
}
Date &operator=(Date& d)  {}
/*
同理 Date operator=(Date& d) 改为  Date& operator=(Date& d)
传值返回 改为 引用返回就能避免拷贝构造函数被调用
*/
int main() {
	Date d1;
	Date d2(d1);    //使用已经存在的d1对象初始化d2对象
	//Date d2; d2=d2;
	return 0;
}
  • 由于值传递会触发调用拷贝构造函数,这就是上面在定义拷贝构造函数的时候,形参必须添加引用的原因,否则会触发拷贝构造函数的无限调用

执行函数

  • 每一个除构造外的本身对象类型要执行自己的类对象下命名的函数,根据命名确定自己是属于什么对象。自己对象下的构造后。
    也就是正常的什么命名 就是构造,正常逻辑
    而因为继承出现了多态定义。
    例如
    Parent* ob1 = new Parent;
	ob1->fn();
	Child* ob2 = new Child;
	ob2->fn();
	Parent* obj3 = new Child;
	obj3->fn();

多态定义下的默认情况

  • 在基类中虚函数的默认参数会在编译过程就被保存,再调用子类的函数后发生多态,编译器会使用基类的默认参数,由于虚函数会因为被继承而重构,执行的是重构的函数,如果重构里没有,那么只能执行基类中的虚函数

  • 基类内部函数是实际存在的函数情况下,以基类为首,如果出现了多态,那么就是类对象是需要执行的类别,而不是去考虑派生重新构造的函数,如果基类是虚函数的执行,那么就看看继承的有无重构声明

  • 如果是派生类,那么执行自己的,如果没有就执行基类的,如果基类是虚函数,自己也是虚函数,那么也是执行自己的,如果基类是虚函数,自己是实函数,那么还是自己的

class  派生类名(参数总表) : 访问控制  基类名1 (参数表1),  访问控制  基类名2 (参数表2),, 访问控制  基类名n (参数表n)
{
         数据成员和成员函数声明
         // 派生类新增成员的初始化语句
}

多继承方式下构造函数的执行顺序:
处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的基类顺序,与派生类构造函数中所定义的成员初始化列表顺序没有关系。
●先执行所有基类的构造函数
●再执行对象成员的构造函数
●最后执行派生类的构造函数

运算符号

等级关系是,!>算数运算符>关系运算符>&&>||>赋值运算符
在这里插入图片描述

正反补码

在这里插入图片描述

&与*的爱恨情仇

C++提供了两个与地址相关的运算符——“”和“&”。“”是指针运算符,表示获取指针所指向的变量的值。“&”表示取地址运算符,用来得到一个对象的地址。

  • 指针类型:在C++中专门用来存放内存单元地址的变量类型。
  • 指针变量:具有指针类型的变量,指针变量是用来存放内存单元地址的。
  • 声明指针的语法:数据类型 *指针名;
  • 在这里*表示的是声明的是一个指针类型的变量,而数据类型表示的是此指针指向的对象(包括变量和类的对象)的类型。

第一种是数据的引用传递

int i,j;
int &ri = i;//建立一个int型的引用ri,并将其初始化为变量i的一个别名
也就是说,当我输出ri和i的时候,它俩不冲突,同一个人的大小名
j = 10;
ri = j; //相当于i=j;
赋值上表面是j赋给ri,因为是大小名的缘故,所以i也被改值了

运算&* 从执行与声明角度

  1. “ * ”从声明角度
    在被声明变量名之前,作为的是指针
int *p; //声明p是一个int类型的指针

比如结构体typedef struct之后的*作为指针连接。

  1. “ * ”从执行角度
    当作为执行侧的时候,是要获取这个指针里的内容,也就是指针里的数据,并不是指针的地址
cout<<*p; //输出指针p所指向的内容

  1. “ & ”从声明角度
    在变量声明语句中位于被声明的变量左边时,作为引用,等价于我把右侧的东西引用为这个名字
int i;
int &ri = i;//表示建立一个int型的引用ri,并将其初始化为变量i的一个别名

  1. “ & ”从执行角度
    右侧的时候,我要拿取这个对象的地址,或者是这个变量存放的地址address
int a,b;
int *pa,*pb;
pa = &a;//表示pa中存储的地址是a的地址

融合

  • 在定义指针的同时对其进行初始化赋值。语法形式: 存储类型 数据类型 *指针名 = 初始地址;
  • 在定义之后,单独使用赋值语句。赋值语句的语法形式为:指针名 = 地址;

注:一个数组可以用它的名称来直接表示它的初始地址。数组名称实际上就是一个指针常量。例:

int a[10];//一维* ,二维** 或者 * a[],关于维度的操作
int *ptr = a;//用数组的首地址初始化int型指针,只是首地址,不是全部交接
/* int *ptr;
ptr = a; */  //同样也是用数组的首地址初始化int型指针

注:①可以声明为指向常量的指针,此时不能通过通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象。(如果在函数体中不需要通过指针改变指针所指向的内容,可以将其声明为指向常量的指针)例:

int a;
const int *p1 = &a ; //p1是指向常量的指针
int b;
p1 = &b ;//正确,p1本身的值可以改变,将其改为指向另一个对象
*p1 = 1//错误,不能通过p1这个指针改变其所指向的对象

②可以声明为指针类型的常量,这是指针本身的值不能被改变。(意思就是始终指向同一个地址)例:

int* const p2 = &a;
p2 = &b;//错误,p2是指针常量,值不能改变,始终指向a的地址

③一般情况下,指针的值只能赋给相同类型的指针。但是void类型的指针,可以存储任何类型的对象地址,经过使用类型显式转换,通过void类型的指针就可以访问任何类型的数据。

  1. 参数执行
  • (* a)作为参数传入,那么在函数中便可以对a所指向的内存空间的值指直接进行修改;在函数的形参里要对a指针下的数组或者其他结构体进行内部修改。
  • (*&a)作为参数传入,我们可以对指针本身这个数据,以及它所指向的数据进行修改。
  • (&a)就是对a的数据直接修改
  • 形参直接a 就只是这个形参,不会改变这个变量在整体main里的作用,除非特地return了
    用题目检测一下
#include<bits/stdc++.h>
using namespace std;
int sub(int x, int& y, int* z) {
	int t = y;
	y = x > y ? x : y;
	if (y < *z)y = *z;
	x = x + t + *z;
	*z = x / 3;
	return x;
}
int main()
{
	int a = 10, b = 5, c = 22, * pa = &c;
	int s = sub(a, b, pa);
	cout << s << " " << a << " " << b << " " << c << endl;
	return 0;
}

答案是 37 10 22 12
分析,(pa获取c地址)后加*也就是取值为22;
sub函数要的是a的值,y的地址上值,操作更改会进行覆盖,而pa地址上的值。进行操作
y的值给了t,先进行x>y就取大,此时y 的值变成了10;y<22,此时y又变成22,
x=x自身10+t5+z22=37;
z在计算变换后为37/3=12;
返回了x=37;
s就是37,a还是原来的10没有任何变化,b因为&同步变化是22,c就是pa地址上的值是函数里的z是12;

static静态 public全局 extern外接

c语言中,static用来说明静态变量。

  1. 如果是在函数外面定义的,那么其效果和全局变量类似,即static说明的变量可以在当前c程序文件中使用。
  2. 如果是在函数内部定义的,那么这个变量只初始化一次,即使再次调用这个函数,这个static变量也不会再次被初始化,于是,这个变量的取值就会一直保存着,我们再次调用该函数时,仍是保存的上一次函数调用时保存的结果。
  3. 当全局变量与局部变量重名的时候,起作用的是局部变量,全局变量被屏蔽掉。
  4. extern在函数外对全局变量声明,使全局变量的作用域从声明处到文件的结束。

bool计算

  1. a && b,当a为假时,整个逻辑表达式的结果必为假,评估表达式b的值是不必要的。
  2. 同样的情况也发生在表达式c || d中,当c为真时,d的真假不影响整个表达式的值。在上述情形下,编译器会略过表达式b和d的运算,
  3. 这种行为就好比电路“短路”时,电流总是沿阻抗最低的“近路”行进,称之为“布尔运算的短路”。常见

default

(1)既无成功匹配,又无default子句,那么swtich语句块什么也不做;

(2)无成功匹配,但有default,那么swtich语句块做default语句块的事;

(3)有成功匹配,没有break,那么成功匹配后,一直执行,直到遇到break。

全局静态

静态构造函数在一个类中只能有一个,且只能被执行一次,而实例构造函数则无此限制。即在加载类的时候执行静态构造函数,而在创建实例时执行实例构造函数。

public class AddClass
{
    public static int a { get; }
    /// <summary>
    /// 静态构造函数会在属性a被调用之前执行,用于初始化静态属性a
    /// </summary>
    static AddClass()
    {
        a = 10;
    }
}

静态构造函数的作用是初始化类的静态字段和静态属性,且在类的任何实例创建之前和静态成员被引用之前。
这是由于CLR不能确保在明确时间点执行静态构造函数,同时也不能预计不同类的静态构造函数执行顺序,所以,只能确定静态构造函数会在第一次调用类的其他成员之前被调用。

静态构造函数只会被CLR调用,不能显示调用,所以静态构造函数不会有访问修饰符和参数;静态构造函数也不能访问类的实例成员。
析构
析构函数只对类的实例起作用,因此没有静态析构函数,只有直接的析构函数命名

实例构造函数 在创建类的每个实例时都会调用
静态构造函数 只调用一次,在类的任意静态变量第一次被访问之前或者类的任何实例被创建之前,没有的话就跳过

class MyClass {
	int x;
public:
	MyClass(int a) {
		x = a;
		cout << "construction" << x << endl;
	}
	~MyClass() {
		cout << "Destruct" << x << endl;
	}
};
MyClass globlabody(9);
int main() {
	MyClass commonobity(0);
	static MyClass staticnbody(1);
	return 0;
}
//
construction9
construction0
construction1
Destruct0
Destruct1
Destruct9
  • 构造逻辑
    首先注意的是主函数外的全局变量和静态全局变量,对其先调用构造函数
    主函数内遇到局部变量与静态局部变量的时候,按照顺序对其进行构造函数
    遇到花括号中的局部变量,调用构造函数,当花括号结束的时候,也就是表明他的寿命结束了,进行对应的析构函数
    遇到局部变量,对其进行构造函数

  • 析构逻辑
    首先按照构造函数执行的相反次序执行析构,是正常的auto变量
    其次执行主函数中的静态局部变量析构
    最后按照构造函数的相反次序执行析构函数,不再强调全局外的auto和static顺序

namespace ConsoleApp1{
    class A{
        static A(){
            Console.WriteLine("1");
        }
        public A() {
            Console.WriteLine("2");
        }
    }
    class B : A{
        static B() {
            Console.WriteLine("a");
        }
        public B(){
            Console.WriteLine("b");
        }
    }
    class Program {
        static void Main(string[] args) {
            //a 1 2 b
            A ab = new B();
            Console.WriteLine("--------");
            //2 b
            ab = new B();
            Console.ReadKey();
        }
    }
}

A ab = new B();
A. 实例化B, 执行B的静态构造函数 == 输出a
B. 执行B的构造函数,因为B继承自A,所以先进入A
C. 实例化A, 执行A的静态构造函数,== 输出 1
D. 执行A的构造函数 ==输出2
E. 最后回到B的构造函数 == 输出b
2、ab = new B();
因为静态构造函数只创建一次,所以不会进入静态构造函数
A. 因为B继承自A,所以先进入A的构造函数 == 输出2
B. 再进入B的构造函数 == 输出b

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

磊哥哥讲算法

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

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

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

打赏作者

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

抵扣说明:

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

余额充值