C++面对对象的总结

C++面对对象的总结

​ 面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些

属性变量的函数打包成一个类来表示

​ 面向对象:将数据与函数绑定到一起,进行封装,加快开发程序,减少重复代码的重写过程

面向对象的三大特征

面向对象的三大特征是封装、继承、多态。

封装

​ 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和

对象进行 交互。封装本质上是一种管理,不想给别人看到的,我们使用protected/private把成员

封装起来。开放一些共有的成员函数对成员合理的访问。

继承

​ 可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

三种继承方式

继承方式private继承protected继承public继承
基类的private成员不可见不可见不可见
基类的protected成员变为private成员仍为protected成员仍为protected成员
基类的public成员变为private成员变为protected成员仍为public成员
多态

​ 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现

多态,有二种方式,重写,重载。

C++ 的重载和重写

重写

​ 指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重

写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会

调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

​ 实现:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类

型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调

用基类的函数。

代码示例

class A
{
public:
	virtual void fun(){
		cout << "A";
	}
};
class B :public A
{
	public:
	virtual void fun(){
		cout << "B";
	}
};
int main(void)
{
	A* a = new B();
	a->fun();//输出B,A类中的fun在B类中重写
}
重载

​ 我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。函数重载是

指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根

据参数列表确定调用哪个函数,重载不关心函数返回类型。

​ 实现:C++利用命名倾轧(name mangling)技术,来改名函数名,区分参数不同的同名函数。命

名倾轧是在编译阶段完成的。编译时将参数类型加入以区分不同。

代码示例

class A
{
	void fun() {};
	void fun(int i) {};
	void fun(int i, int j) {};
	void fun1(int i,int j){};
};

C++的构造函数

​ C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。

默认构造函数、初始化构造函数

默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。

注意:有了有参的构造了,编译器就不提供默认的构造函数。

代码示例

class Student
{
public:
    //默认构造函数
    Student()
    {
        num=1001;
        age=18;
    }
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
    int num;
    int age;
};
int main()
{
    //用默认构造函数初始化对象S1
    Student s1;
    //用初始化构造函数初始化对象S2
    Student s2(1002,18);
    return 0;
}
拷贝构造函数

**浅拷贝:**又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份

实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名

叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中

去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,深拷贝情况下,不会出现

重复释放同一块内存的错误。

代码实现

class Test
{
    int i;
    int *p;
public:
    Test(int ai,int value)
    {
        i = ai;
        p = new int(value);
    }
    ~Test()
    {
        delete p;
    }
    Test(const Test& t)
    {
        this->i = t.i;
        this->p = new int(*t.p);
    }
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
    Test t1(1,2);
    Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同
    return 0;
}
拷贝构造函数的参数必须使用引用传递的原因

​ 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采

用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地

调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass*

c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

移动构造函数。

用于将其他类型的变量,隐式转换为本类对象。下面的转换构造函数,将int类型的

r转换为Student类型的对象,对象的age为r,num为1004.

代码实现

Student(int r)
{
    int num=1004;
    int age= r;
}

C++的析构函数

​ 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使

用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的

析构函数不是虚函数,在特定情况下会导致派生类无法被析构。

  1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常

析构

  1. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基

类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时

候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对

象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就

要根据指针绑定的对象来调用对应的析构函数了。

​ C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。

C++ 类对象的初始化顺序

  1. 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);

  2. 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)

  3. 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们

在成员初始化表中的顺序;

  1. 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而

不是它们出现在成员初始化表中的顺序;

  1. 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传

递给适当的基类构造函数。

  1. 综上可以得出,初始化顺序:

父类构造函数–>成员类对象构造函数–>自身构造函数

其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。

析构顺序和构造顺序相反。

深入了解C++的多态

​ 由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多

态。 多态分为静态多态和动态多态:

静态多态

​ 编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应

的函数,就调用,没有则在编译时报错。

比如一个简单的加法函数:

int Add(int a,int b)//1
{
	return a+b;
}
char Add(char a,char b)//2
{
	return a+b;
}
int main()
{
    cout<<Add(666,888)<<endl;//1
    cout<<Add('1','2');//2
    return 0;
}

显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信

你可以将顺序调过来试试。

动态多态

其实要实现动态多态,需要几个条件——即动态绑定条件:

  1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。

  2. 通过基类类型的指针或引用来调用虚函数。

C++虚函数和纯虚函数

虚函数

​ 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在

这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应

实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们

用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指

明了实际所应该调用的函数。

代码示例

class Person{
public:
    //虚函数
    virtual void GetName(){
    	cout<<"PersonName:xiaosi"<<endl;
	};
};
class Student:public Person{
public:
    void GetName(){
    	cout<<"StudentName:xiaosi"<<endl;
    };
};
int main(){
    //指针
    Person *person = new Student();
    //基类调用子类的函数
    person->GetName();//StudentName:xiaosi
}
纯虚函数

​ 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现

方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。在很多情

况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,

但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在

派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这

样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以

覆盖的接口,但是这个类中的函数绝不会调用。声明了纯虚函数的类是一个抽象类。所以,用户不

能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0)

否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派

生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执

行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在

告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

代码示例

//抽象类
class Person{
public:
	//纯虚函数
	virtual void GetName()=0;
};
class Student:public Person{
public:
    Student(){
    };
    void GetName(){
    	cout<<"StudentName:xiaosi"<<endl;
    };
};
int main(){
	Student student;
}
虚函数和纯虚函数的区别
  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类

不能被称为抽象类。

  1. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实

现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

  1. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  2. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  3. 虚函数的定义形式: virtual{} ;纯虚函数的定义形式: virtual { } = 0 ;在虚函数和纯虚函数

的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数

却是动态绑定,而且被两者修饰的函数生命周期也不一样。

C++ 中不能被声明为虚函数的有哪些?

​ 常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函

数,友元函数。

  1. 为什么C++不支持普通函数为虚函数?

普通函数(非成员函数)只能被overload,不能被override,声明为虚函数无意义,因此编译器会

在编译时绑定函数。

  1. 为什么C++不支持构造函数为虚函数?

这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象

成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另

外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数

来完成你想完成的动作。(这不就是典型的悖论)

构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚

未形成,所以不能将构造函数定义为虚函数

  1. 为什么C++不支持内联成员函数为虚函数?

其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在

继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展

开,虚函数在运行时才能动态的绑定函数)

内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数

为虚函数

  1. 为什么C++不支持静态成员函数为虚函数?

这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没

有要动态绑定的必要性。

静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

  1. 为什么C++不支持友元函数为虚函数?

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

类模板和模板类

  1. 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数

  2. 模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值