C++(多态,虚函数,构造函数,static)

多态

  多态性可以简单的概括成“一个接口多种方法”,这是面向对象编程的核心概念,多态性指相同对象收到不同消息或不同对象收到相同消息时产生的不同的实现动作,C++主要有两种多态性:编译时多态性(静态多态性,通过重载函数实现)和运行时多态性(动态多态性,通过虚函数实现)。虚函数允许子类重新定义成员函数,而子类重新定义父类的做法成为覆盖或者是重写。C++三大特性中另外两个封装使得代码模块化,继承可以扩展已存的代码,这两个的目的都是为了代码重用,而多态是为了“接口重用”——不管传递的是类的哪个对象,函数都能通过同一个接口调用到适应各自对象的实现方法。多态最常用的做法就是声明基类类型的指针,利用这个指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法,这是如果没有虚函数就没有利用C++多态性,使用基类指针的时候就只会被限制在基类函数本身,无法调用子类被重写过的函数。
  需要注意:虚函数只适用于有继承关系的类对象普通函数不能声明虚函数静态成员函数不能是虚函数,因为静态成员函数是属于一个类的不能局限余一个对象,內联函数不能是虚函数,因为內联函数不能在运行中动太确定位置,构造函数不能是虚函数析构函数可以是虚函数并且推荐变为虚函数。
  函数重写,重载和隐藏是有区别的,重写可以用来重写虚函数和成员函数,只有重写了虚函数才能体现多态的特性,而重载是运行有多个同名的函数,但是这些函数的参数列表是不同的,重载允许参数个数不同或是参数类型不同,编译器会根据不同的函数列表生成不同名称的预处理函数,来实现同名函数调用时的重载问题,但是重载其实本质上并没有体现多态性。隐藏是指当派生类和基类函数重名但是参数不同时,无论有无virtual关键字基类函数都会被隐藏(与重载不同,重载是发生在同一个类中的),如果派生类和基类函数名相同,但是参数也相同,并且没有virtual那么基类函数也会被隐藏(与重写区分,重写是有virtual关键词)。
参考资料:浅谈C++ 多态c++多态

虚函数

  虚函数是多态的一种实现方式,而多态是:“多态(英语:polymorphism),是指计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。简单来说,所谓多态意指相同的消息给予不同的对象会引发不同的动作称之。”其实更简单地来说,就是“在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数”。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的,这点在上文也有讲过。而虚函数则是加了virtual修饰词的类的成员函数。

#include <iostream>
using namespace std;
int main(int argc, char const *argv[]){
class base{
public:
	virtual void vir_func(){ cout<<"virtual function, this is class base"<<endl;}
	void func() {cout<<"normal function, this is class base"<<endl;}
};
class A:public base{
	public:virtual void vir_func(){ cout<<"virtual function, this is class A"<<endl;}
	void func(){cout<<"normal function, this is class A"<<endl;}
};
class B:public base{
	public:virtual void vir_func(){ cout<<"virtual function, this is class B"<<endl;}
	void func() {cout<<"normal function, this is class B"<<endl;}
};
base * Base = new(base);base * a = new(A);base * b = new(B);
Base->func();a->func();b->func();
cout<<"##########################"<<endl;
Base->vir_func();a->vir_func();b->vir_func();
cout<<"##########################"<<endl;
((A *)b)->vir_func();((A *)b)->func();
return 0;
}
//运行后输出
normal function,this is classbase
normal this is classfunction,base
normal function, this is class base
###################################
virtual function, this is class base
virtual function,this is class A
virtual function, this is class B
###################################
virtual function, this is class B
normal function,this is class A

  从上面的例子可以看出当使用这三个指针调用func函数时,调用的都是基类base的函数。而使用这三个指针调用虚函数vir_func时,调用的是指针指向的实际类型的函数。最后,我们将指针b做强制类型转换,转换为A*类型,然后分别调用func和vir_func函数,发现普通函数调用的是类A的函数,而虚函数调用的是类B的函数。以上,我们可以得出结论“当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定”。虚函数不会受强制类型转换影响,指针指向虚函数就会运行虚函数实际完成的逻辑,不会运行父类的逻辑。
  对于虚函数,构造函数不可以是虚函数,析构函数可以且最好设置为虚函数,因为虚函数是通过对象内存中的堆中的一部分数据(vptr,虚函数表指针)来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,所以构造函数只能作为普通函数存放在类所指定的代码区中。那么为什么析构函数推荐最好设置为虚函数呢?如上面的例子中,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数,而我们希望派生类的析构函数对新的成员也能进行析构,所以最好使用虚函数来实现。
  纯虚函数和虚函数的区别:定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。纯虚函数是在基类中声明的虚函数,在基类中没有定义,但要求任何派生类都要定义自己的实现方法,在基类中实现纯虚函数的方法是在函数原型后加“=0”比如virtual void funtion1()=0,引入纯虚函数还有个原因是为了解决基类本身能够生成对象的问题(因为基类本身能够生成对象是不合理的),因为含有纯虚函数的类称为抽象类(只作为基类来使用,虚函数的实现由派生类给出,如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类),它不能生成对象,能够很好的解决这个问题,所以定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
参考资料:虚函数详细介绍纯虚函数余虚函数

C++类对象的内存布局

  C++程序的内存格局通常分为四个区:全局数据区(也有把这个分为全局静态区和常量区的),代码区,栈区,堆区。全局数据区存放全局变量,静态数据和常量。所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。在类的定义时,类的成员函数被放在代码区。类的静态成员变量在全局数据区。非静态成员变量在类的实例内,实例在栈区或者堆区。虚函数指针、虚基类指针在类的实例内,实例在栈区或者堆区。类的实例如果是定义的类变量就在栈内存储,如果是new出来的类指针就在堆内存储,同时引用会保存在栈里。
  C++中的虚函数是通过虚函数表(vtbl)来实现,每一个类为每一个virtual函数产生一个指针,放在表格中,这个表格就是虚函数表。每一个类对象会被安插一个指针(vptr),指向该类的虚函数表。vptr的设定和重置都由每一个类的构造函数、析构函数和复制赋值运算符自动完成。C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

构造函数

  C++构造函数有很多种,主要是六种分别是:无参构造函数,初始化构造函数(有参数),拷贝构造函数,移动构造函数,委托构造函数,转换构造函数。
  无参构造函数:就是在没有显式提供初始化式时调用的构造函数。如果定义某个类的变量时没有提供初始化时就会使用默认构造函数。但是只要自定义了一个没有参数的构造函数那么系统就不会自动生成这样的构造函数。
  有参构造函数:这种构造函数内部有参数,一般有两种写法,分别是初始化列表方式(以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化值)和内部赋值方式:

#include<iostream>
using namespace std;
class Student{
public:
	int m_age;
	int m_score;
	//初始化列表方式
	Student(int age, int score) :
		m_age(age),
		m_score(score)
		{}
	//内部赋值方式
	Student(int age, int score){
		m_age = age;
		m_score = score;
		}
	};

  由此就引出了一些问题,为什么需要初始化列表的方式,它的优点和使用场景是什么,先说结论:以下四种情况必须要用到成员列表初始化① 当初始化一个引用成员时;② 当初始化一个常量成员时;③ 当调用一个基类的构造函数,而它拥有一组参数时;④ 当调用一个成员类的构造函数,而它拥有一组参数时;而使用它的好处则是相比于赋值初始化,初始化列表的方式执行完效率会更高,因为对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。但是列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前就会给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。函数体内的初始化中有赋值操作,会产生临时对象,临时对象会降低程序的效率。在初始化列表中,初始化的顺序是按照成员的声明顺序决定的,而不是由列表的排列顺序。
  派生类构造函数的执行顺序是按照这个关系实现的:① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。③ 类类型的成员对象的构造函数(按照初始化顺序)④ 派生类自己的构造函数:

public class IoTest {
	public static void main(String[] args) throws IOException {
		new Son();
	}
}
class Sup{
	public Sup(){
		System.out.println("Sup构造函数");
	}
}
class Son extends Sup{
	//成员对象的构造函数,会优先于类的构造函数执行
	private AA aa = new AA();
	public Son(){
		System.out.println("Son构造函数");
	}
}
class AA{
	public AA(){
		System.out.println("AA构造函数");
	}
}

结果如下图:
在这里插入图片描述
析构函数的执行顺序和构造函数的顺序是相反的。

拷贝构造函数

  一般直接初始化调用实参匹配的构造函数,拷贝初始化调用拷贝构造函数,什么是拷贝函数,一个例子如下所示:

class A
{
private:
    int b;
public:
    A(int c=0);
    A(const A &a);
    ...
};

  拷贝函数就是用已存在的对象来初始化另一个对象。拷贝函数的使用情况,主要有三种,分别是:1). 一个对象以值传递的方式传入函数体 ;2). 一个对象以值传递的方式从函数返回 ;3). 一个对象需要通过另外一个对象进行初始化。4)如果我们需要对数据进行深拷贝,那么只有显示的定义拷贝构造函数才能够实现。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的,拷贝构造函数不可以改变它所引用的对象。拷贝构造函数,用一个对象来初始化另一个对象,是在类对象定义的时候起作用,赋值函数,通过把一个对象赋值给另一个对象来改变对象,是在被赋值对象定义以后通过调用赋值函数起作用,2个函数的结果一样,但使用的地方不一样。赋值和初始化是有区别的在定义的同时进行赋值叫做初始化(Initialization),定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值(Assignment)。初始化只能有一次,赋值可以有多次。
  如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会私下里为你制定一个函数来进行对象之间的位拷贝(bitwise copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对于成员的关联方式。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数除非另外一个构造函数在类初始化或者在构造列表的时候被调用。
  不管是显示调用还是隐式调用创建的对象都是在栈上分配的内存,而使用new是在堆上分配的内存,系统上堆和栈会有区别:1、栈内存是系统分配,一般不超过8M,而堆内存可达到4G;2、栈是系统数据结构,创建和释放都是系统操作,而堆是开发者自己使用malloc,free,或者new delete来进行创建和释放操作的;3、栈内存分配是系统操作,有专门的寄存器存放地址效率非常高,而堆分配是按照C++库函数操作的,从寻址到分配是非常复杂的,效率也比较低。
  一般以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。所以拷贝构造函数和赋值运算符重载会有很大的区别:拷贝构造函数是函数,赋值运算符是运算符重载,拷贝构造函数会生成新的类对象,赋值运算符不行,使用赋值运算符如果原来的对象有内存分配要先把原来的内存释放掉。
  在拷贝时深拷贝和浅拷贝也会有区别,浅拷贝 (shallow copy) 只是对指针的拷贝, 拷贝够两个指针指向同一个内存空间,并没有开辟一个地址,如果原来的地址释放了那么再释放浅拷贝的资源就会错误。深拷贝 (deep copy) 不但对指针进行拷贝, 而且对指针指向的内容进行拷贝,开辟了一个新的内存空间即使原来拷贝的原内存释放了,也不会影响深拷贝的值,经过深拷贝后的指针是指向两个不同地址的指针。

移动构造函数

  移动构造函数设置的初衷是当我们把对象a初始化为b后就不能再使用了但是a的空间在析构前还存在,因为使用了拷贝构造函数,相当于把a的对象复制到了b中,如果我们能直接使用a的空间就能避新的空间分配,所以就有了移动构造函数。移动构造函数中的指针采用的是浅层复制,移动实现的是对象真实值的转移(从源对象到目的对象):源对象丢失其内容,被目标对象占有,移动操作发生时条件是移动值的对象是未命名的对象(临时变量,函数返回值,类型转换的对象),只有移动构造函数和移动赋值可以完成上述的操作:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作。

static

  static的作用:1、有隐藏的作用,同时编译多个文件时,所有未加static的前缀的全局变量和函数有全局可见性。比如在a中写的函数和变量,在b中使用了extern后就可以使用,所以加了static的话就会对其他源文件隐藏。
  2、static可以保持变量内容的持久,这种使用的较少,因为存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化,简单的说就是静态变量在函数内定义,始终存在(函数退出后扔存在),并且只进行一次初始化,有记忆性。总共有两种变量存储在静态存储区:全局变量和static变量,只是和static相比它的可见范围更小(第一个特性),虽然static的生命周期更长,但是使用范围并不会收到影响,比如定义了一个static的局部变量,它的作用域和一般的自动变量仍然是相同的,只能在定义该变量的函数内使用这个变量。所以基于上面两个特性可以对出一个结论把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。
  3、static的默认初始化为0,这个和全局变量一样,因为两者都是存储在静态数据区内。
  4、在C++中类的成员声明static,有几个作用:(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。(2)不能将静态成员函数定义为虚函数,const和volatile。(5)静态成员变量初始化与一般数据成员初始化不同:必须要在类外进行初始化不能在构造函数内初始化,因为静态成员变量是属于类的不是对象的,如果在类中初始化所有类的对象都会包含,这是矛盾的。且在类外定义不用加stastic修饰。以免与一般静态变量或对象相混淆;初始化时不加该成员的访问权限控制符private,public等;初始化时使用作用域运算符来标明它所属类;静态成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。 然而,普通成员变量则随着对象的创建而创建,销毁随之释放。
const与static的区别:1、在不考虑类的情况下const常量是在定义时就初始化,之后不能再更改,定义形参指在形参中不能更改。如果const声明的是 const char* p 表示指向的内容不可修改(这是常量指针,指向的是一个指针,他的指针内容是不能更改的),如果是char* const p 表示指向的地址不能改变,内容可以改变。(这是一个指针常量因为指向的是常量,地址无法改变)2、考虑类的情况,const对于成员变量,不能在类定义外部初始化,,只能通过构造函数初始化,所以必须有构造函数,因为不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。对于成员函数,const对象不可以调用非const的成员函数而非const对象却都可以调用,并且不可以改变不含mutable关键词(使用这个关键词可以在const成员函数中被修改)的数据。两者的共同点是const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用, 因此在头文件中声明const变量是没问题的。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值