• c++中类的特殊成员函数


    C++内部自己定义了一些类的成员函数,这些成员函数我们是可以重构或者说覆盖的,具体地说,C++自动提供了下列的成员函数

    一、默认构造函数

    如果没有提供任何构造函数,C++将创建默认构造函数。也就是说,编译器将提供一个不接收任何参数,不执行任何操作的构造函数。这是因为创建对象式总会调用构造函数。

    如果定义了构造函数,C++将不在提供默认构造函数,如果希望在创建对象时不显式的对他进行初始化,则必须显式的定义默认构造函数。

    带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值。

    Klunk(int n = 0) {n_ = n;}
    

    但是只能有一个默认构造函数,也就是说,这两种定义不能同时存在

    Klunk(int n = 0) {n_ = n;}
    Klunk() {n_ = 0;}
    

    因为下面的调用会不知道调用哪个构造函数,两个都可以。

    Klunk bus;
    

    二、默认析构函数

    如果没有提供任何析构函数,C++将创建默认析构函数。也就是说,编译器将提供一个不接收任何参数,不执行任何操作的构造函数。这是因为销毁对象式总会调用析构函数。

    在下述情况中析构函数将被调用

    • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。

    • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数

    • 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。

    三、复制构造函数

    复制构造函数用于将一个对象复制到新建的对象中,也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中,类的复制构造函数原型通常如下

    Class_name(Const Class_name&)
    

    它接受一个指向类的对象的常量引用作为参数,对于复制构造函数我们需要知道两件事,什么时候调用,有什么功能。

    3.1、何时调用复制构造函数

    新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,这在很多情况下都可能发生,例如将新对象显式的初始化为现有对象

    Stringbad ditto(motto);
    Stringbad metto = motto;
    Stringbad also = Stringbad(metto);
    Stringbad *pStringBad = new Stringbad(metto);
    

    其中中间的两种声明,可能会使用复制构造函数直接创建metto,also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metto,also,这取决于编译器具体的实现,最后一个声明是使用metto初始化一个匿名对象,并且将新对象的地址赋值给一个指针。

    每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。记住,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。例如,将3个Vector对象相加时,编译器可能生成临时的Vector对象来保存中间结果。何时生成临时对象随编译器而异,但无论是哪种编译器,当按值传递和返回对象时,都将调用复制构造函数。

    3.2、默认赋值构造函数(浅复制)

    默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。

    下面两段代码其实是等效的

    // code1
    String str1 = str2;
    
    // code2
    String str1;
    str1.len = str2.len; 
    

    只是由于私有成员无法通过对象直接访问,所以编译错误。

    3.3、浅复制存在的问题

    要是我们复制一个字符串呢

    class String{
    private:
        char *str_;
    public:
        String(char* str);
    }
    
    String::String(char* str){
        str_ = str;
    }
    

    我们创建并且复制这个对象

    String str1
    String str1 = str2;
    

    但是这里存在一个问题,我们是把字符串复制了嘛,并没有,我们只是复制了字符串的指针地址,也就是说,第一个对象销毁的时候,一旦把这个字符串也销毁了,那我们第二个对象内的地址就失去了意义,或者说我们第一个对象对字符串作出了一些操作,那么第二字符串不也就改变了嘛。如果我们在第一个对象中销毁了这个字符串指针,那么也会报错。

    还有一种情况,假如我们有一个静态变量用于统计有多少个这种对象

    class String{
    private:
        char *str_;
    public:
    	static int numString; 
    	String();
        ~String();
    }
    
    String::numString = 0;
    String::String(){
        numString ++;
    }
    String::~String(){
        numString --;
    }
    

    这样我们在进行复制操作的时候就不能准确的统计创建对象的多少,这个错误我们修改一下

    String::String(const String & s){
    	numString++;
    }
    
    3.4、显式构造函数解决问题

    在解决类设计中这种问题的解决方案是进行深度复制,也就是说,复制构造函数应当复制字符串并将副本地址赋值,也就是下面的程序。

    String::String(const String & s){
    	numString++;
    	len = s.len;
    	str = new char[len+1];
        std::strcpy(str,st.str);
    }
    

    注意:浅复制只复制指针地址,深复制自己new内存空间,手动将所有的数据备份。

    四、赋值运算符

    ANSI C允许结构赋值,而C++允许类对象赋值。这是用过自动为类重载赋值运算符实现的,这种运算符原型如下

    Class_name & Class_name::operator=(const Class_name &)
    

    他接受并返回一个指向类对象的引用

    4.1、赋值运算符的功能及如何使用

    将已有对象赋值给另一个对象时,将使用重载的复制运算符

    Srting line("hello");
    String knot;
    knot = line;
    

    初始化对象时不一定会使用赋值运算符

    String metto = line;
    

    这里,metto是—个新创建的对象,被初始化为knot的值,因此使用复制构造函数。然而,正如前面指出的,实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。

    与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员;但静态数据成员不受影响。

    4.2、解决赋值运算符的问题

    对于由于默认赋值运算符不合适导致的问题,解决方法是提供赋值运算符(进行深度复制),其实现和复制构造函数相似,也有差别

    • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。

    • 函数应当避免将对象赋给自身否则,给对象重新赋值前,释放内存操作可能删除对象的内容。

    • 函数返回一个指向调用对象的引用

    通过返回一个对象,我们可以进行连续的赋值

    s0 = s1 = s2;
    
    // 相当于这样
    s0.operator=(s1.operator(s2));
    

    下面的代码编写了一个赋值运算符

    String & String::operator=(const String & st){
    	if(this == & st){ return *this;}
        delete [] str;
        len = st.len;
        str = new char[len+1];
        std::strcpy(str,st.str);
        return *this;
    }
    

    代码首先检查自我复制,这是通过查看赋值运算符右边的地址是否与接受对象的地址相同来完成的,如果相同,程序返回*this,结束,前面说过,赋值运算符只能有类成员函数重载。

    五、地址运算符

    六、移动构造函数

    七、移动赋值运算符

  • 相关阅读:
    Python+Vue实现简单的前后端分离
    CSS-clamp 函数说明
    原理:用UE5制作一个2D游戏
    MySQL数据库之事务
    Elasticsearch进阶篇(三):ik分词器的使用与项目应用
    图文精灵升级离线版本
    jvm 程序计算器 程序计数器是否溢出 程序计数器是做什么的 java程序计数器会内存溢出吗 程序计数器作用与用处 jvm内存模型 jvm合集(一)
    技术路线扩容,韶音的雄心不止于骨传导耳机?
    Generator—导出种子数据
    深度之眼(六)——矩阵的逆(附:logistic模型一些想法)
  • 原文地址:https://blog.csdn.net/weixin_43903639/article/details/127095659