c++软件开发基础知识

3.虚函数和纯虚函数

3.1动态编译

VTABLE实际是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但派生类的VTABLE与基类的有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写。

类的实例对象不包含虚函数表,只有虚指针;

一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态

设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。

3.2 纯虚函数

在普通的虚函数后面加上" =0"这样就声明了一个pure virtual function. 纯虚函数的意思是:一个抽象类,不要实例化,纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,派生类都会有这个函数。

  1.  当在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
    
  2.  避免一个类被实例化,且在编译时就被发现,那使用pure virtual funcion
    
  3.  这个方法必须在派生类(derived class)中被实现;
    

3.3 基类虚析构函数

析构函数也可以是虚的,甚至是纯虚的.

class A {
public:
  virtual ~A()=0; // 纯虚析构函数
};

当一个类打算被用作其它类的基类时,它的析构函数必须是虚的,否则派生类的析构函数用不上,会造成资源的泄漏。
原因是:如析构函数不被声明成虚函数,编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样会造成派生类对象析构不完全

3.4禁止使用缺省参数

避免虚函数重载时,因参数声明不一致带来的困惑和问题,所有虚函数均不允许声明缺省参数值。
示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的:

virtual void Display(const std::string& text  = "Sub!")    {
       std::cout << text << std::endl;    
}

注:
构造函数不能是虚的
禁止在构造函数和析构函数中调用虚函数(只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为), 同样的道理也适用于析构函数。

4、深拷贝和浅拷贝

简单来说,如果一个类拥有资源,当这个类的对象发生复制过程时,如果资源重新分配就是深拷贝;反之没有重新分配资源,就是浅拷贝。

拷贝构造函数
生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的则是拷贝构造函数.

调用情形:
1)用类的一个对象去初始化另一个对象的时候
2)当函数参数是类的对象时,就是值传递的时候;如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候;

代码实例

#include <iostream>
#include <string>

using namespace std;
class A{
    public:
        A(int i){ data = i;}   //自定义构造
        A(A && a);             //拷贝构造
        int getdata(){return data;}
    private:
        int data;
};
A::A(A && a){  //拷贝构造函数
     data = a.data;
     cout <<"拷贝构造函数执行完毕"<<endl;
}

int getdata1(A a){   //参数是对象,值传递,调用拷贝构造
    return a.getdata();
}
int getdata2(A &a){   //参数是引用传递,不调用拷贝构造
    return a.getdata();
}
A getA1(){            //返回值是对象,会调用拷贝构造
    A a(0);
    return a;
}
A& getA2(){    //返回引用,调用拷贝构造,函数内是临时对象,离开就消失
    A a(0);
    return a;
}

int main(){
    A a1(1); 
    A b1(a1);           //用a1初始化b1,调用拷贝构造
    A c1=a1;            //用a1初始化c1,调用
    int i=getdata1(a1);  //函数形参是类的对象,调用拷贝构造
    int j=getdata2(a1);  //函数形参类型是引用,不调用
    A d1=getA1();        //调用拷贝构造
    A e1=getA2();        //调用
    return 0;
}

拷贝构造函数和赋值运算符重载有以下不同:
1) 拷贝构造函数生成新的类对象,而赋值运算符不能
2) 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉
注:当类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

5 单参构造, explicit

1)单参数构造函数
一个参数的构造函数(或除了第一个参数外其余参数都有缺省值的多参构造函数),承担了两个角色。

  • 用于构建单参数的类对象
  • 隐含的类型转换操作符.
#include <iostream>

class Base
{
public :
    Base(const char data)  {
    	std::cout <<"constructor..." <<std::endl;
        this->m_data = data;
    }
protected :
    char m_data;
};

int main(void)
{
    Base base1('a');        // 用于构建单参数的类对象
    Base base2 = 'b';       // 隐含的类型转换操作符
    return 0;
}

这种方法看起来很方便,但有时这并不是我们想要,由此可能引入一些其他问题。触发隐式转换,生成一个临时的对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。

2)避免隐士转换
用来修饰类的构造函数,表明该构造函数是显式的,在某些情况下,要求类的使用者必须显示调用类的构造函数时就需要使用explicit,反之默认类型转换可能会造成无法预期的问题。声明为explicit的构造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象。

class Test2
{
public:
    explicit Test2(int n)  {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};

Test2 t2=3;//编译错误,不能隐式调用其构造函数
Test2 t2(3);//显式调用成功

explicit只针对单个参数的类构造函数, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。

拷贝构造函数X(const X&)不要声明为explicit,如被声明为explicit,则这个类对象不能用于传参和函数返回值,但仍可以直接调用。

6、析构函数,显示析构

C++的最基本惯用法,程序运行到对象作用域之外时,会隐式的调用析构函数,析构函数执行完成后,对象的资源就被释放。
析构函数定义方式为: ~类名(){...}

https://cpp-note.readthedocs.io/zh/latest/docs/%E7%AC%AC11%E7%AB%A0%20C%2B%2B11%E6%96%B0%E6%A0%87%E5%87%86/11.3%20%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.html#unique-ptr

1) 析构事项

A、只有真实存在的对象离开其作用域时才会调用析构函数,对象的引用,指向对象的指针离开其作用域时,不会调用析构函数。建议当对象离开其作用域后,让对象的引用,指向对象的指针失效,或者干脆就不再使用它。

B、使用new运算符创建的对象的资源,只有使用delete运算符删除指向它的指针时,才会调用它的析构函数,释放它的资源。这点要特别注意,当在类中显式定义析构函数时,函数体中通常就包含delete语句。

C、类中的静态成员属于类,不属于类的对象,它们的资源不会被析构函数释放。

通常情况下,我们不需要显式定义析构函数,除非我们需要它完成一些工作。如果一个类需要手动定义一个析构函数,那么通常情况下,这个类也需要手动定义复制构造函数和赋值运算符重载函数。

复制构造函数用于对象的复制,赋值运算符重载函数的功能和复制构造函数几乎一样。通常,将复制构造函数和赋值运算符重载函数绑定,定义了一个,另一个也必须出现。

析构函数、复制构造函数和赋值运算符重载函数,这三个函数是C++类的复制控制(copy-control)成员。复制控制,就是控制类的对象的复制。其中复制构造函数和赋值运算符重载函数是用来复制对象,析构函数是用来删除对象。

通常,使用复制构造函数或者赋值运算符重载函数创建一个对象时,会获得资源,有时必须显式定义析构函数才能释放这样的对象的资源。

2) 显式析构

析构函数的调用与构造函数的调用有明显不同:析构函数可以被显式调用,而构造函数不能。显式调用析构函数和调用类的其它成员函数没什么不同。当析构函数被显式调用时,只执行它的函数体,而不删除对象的资源。也就是说,当析构函数被显式调用时,它就是一个普通的成员函数,没有析构功能。

并没有destroy对象, 只有对象声明周期结束时即对象销毁了再次调用destructor会造成Undefined Behavior;

1.析构函数并不是销毁对象,只是释放构造函数在构造时初始化的资源(包括堆上分配等)

2.只有类对象被销毁后再次调用用析构函数才会引起Undefined Behavior。

显式定义析构函数多用于以下两种情况:

1、用于查看对象在销毁的前一刻保存的内容。有时候为了测试程序,会用到。

2、在类中用new运算符动态分配了内存,可以在析构函数中使用delete运算符释放内存。这种情况是最常用的。因为编译器生成的析构函数是不会销毁new出来的动态对象,这一点是因为new出来的对象保存在内存中的堆(heap)区,而编译器生成的析构函数只会释放内存中的栈(stack)区。

显式定义的析构函数的作用不像显式定义的构造函数那么有用,显示定义的析构函数完全可以用别的函数代替,但是,为了使用方便,为了其它编程人员的使用,在需要显示定义析构函数的情况下,还是定义它比较好,这样符合通用编程风格。

7. 友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1) 友元函数
可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。

friend 类型 函数名(形式参数);

一个函数可以是多个类的友元函数,只需要在各个类中分别声明

2) 友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

friend class 类名;

注:

  1. 友元关系不能被继承。

  2. 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

  3. 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。

8. Return Value Optimization

返回值优化(RVO)是一种编译器优化机制, 当函数需要返回一个对象的时候,如果自己创建一个临时对象返回,那么这个临时对象会消耗一个构造函数的调用、一个复制构造函数的调用以及一个析构函数(Destructor)的调用代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,这样就省去了一次拷贝构造函数的调用和一次析构函数的调用。

在使用GNU/g++编译器时可以使用"-fno-elide-constructors"选项来强制g++总是调用copy构造函数,即使在用临时对象初始化另一个同类型对象的时候。

三种拷贝构造函数调用的时机

最明显的就是用一个类对象初始化另外一个对象的时候
函数的参数是类对象,这个是函数按值传参数的时候,包括指针在内都是对原有的值的拷贝.
函数返回一个类对象,这是一个对象以值传递的方式从函数返回.
这些都只是语义上的分析, 现在编译器的编译策略, 很多情况下会把这些临时对象的创建都隐藏掉.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值