C++之类和对象

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之类和对象相关的知识,希望对你有一定的参考价值。

类和对象(二)

在一个空类中,我们都知道一个空类的大小是一字节,那么空类真的什么都没有吗?答案是并不是,在我们写任何一个空类时,编译器其实都会自动生成6个默认的成员函数。那么这6个默认的成员函数是什么呢?在这篇博客中,我将讲解构造函数、析构函数、拷贝构造函数、运算符重载函数、取地址以及const取地址操作符重载6个函数


构造函数

我们前面在初始化成员变量是怎么初始化的呢?是定义一个Init成员函数来初始化,如下:

class Date
{
public:
    //一般写
    void Init(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    return 0;
}

这种方式不好,我们可能有时候会忘记调用Init函数,而且如果每次创建对象都调用该方法设置信
息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

可以,C++增加了一个构造函数,他是在对象定义的时候就调用,那么我们就来看看构造函数是什么样子的:

构造函数概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

构造函数特征

构造函数的规则:

  • 函数名和类名相同
  • 没有返回值
  • 对象实例化的时候编译器自动调用对应的构造函数

首先我们来定义一个日期类,并且写一个构造函数:

class Date
{
public:
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    return 0;
}

我们进行调式发现对象在被定义后就自动被初始化了:

  • 构造函数可以重载

构造函数是可以重载的,还记得重载的概念吗?函数名相同,参数的列表不同(参数的顺序、类型、个数)

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;

    }
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    private:
        int _year;
        int _month;
        int _day;
};
int main()
{
    Date d1;//不传参调用没参数的构造函数
    Date d2(2021, 10, 9);//传参调用有参数的构造函数
    return 0;
}

我们进行调试:

我们发现不传参的调用了没参数的构造函数,传参的调用了有参数的构造函数

所以构造函数可以重载这个特性提供了多种的初始化的方式,参数怎么写决定于你想怎么初始化

  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

当我们显式的定义构造函数时:

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    void Print()
    {
        cout << _year <<"/"<< _month <<"/"<< _day << endl;
    }
    private:
        int _year;
        int _month;
        int _day;
};
int main()
{
    Date d1;
    d1.Print();
    Date d2(2021, 10, 9);
    d2.Print();
    return 0;
}

这时我们写了构造函数:

那么当我们不写的时候:

class Date
{
public:
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.Print();
    return 0;
}

我们运行程序:

我们发现初始化成员变量都是随机值了,这时其实是生产了默认的构造函数,很多同学疑惑,这默认的构造函数有什么用呢?下面我们来看这样一个场景:

class A
{
public:
    A()
    {
        _a1=1;
        _a2=2;
    }
private:
    int _a1;
    int _a2; 
};
class Date
{
public:
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
    A aa;
};
int main()
{
    Date d1;
    d1.Print();
    return 0;
}

我们定义了两个类,一个Date类,一个A类,其中A类的一个对象是Date的成员变量。

在C++中,把变量分为内置类型和自定义类型,哪些是内置类型呢?比如说int、char、double,指针类型等等,哪些是自定义类型呢?struct、class;当我们不写构造函数,编译器默认生成构造函数,编译器做了一个偏心的处理,对于内置类型不会初始化,自定义类型会调用它的无参构造函数初始化

我们可以看到aa这个对象的成员变量已经被初始化了,但是内置类型并没有被初始化:

这是早期C++语法设计缺陷,这种偏心的处理导致了语法复杂了,但是因为要向前兼容,不能改动,C++11,语法委员会为这里打了一个补丁:

class A
{
public:
    A()
    {
        _a1=1;
        _a2=2;
    }
private:
    int _a1;
    int _a2; 
};
class Date
{
public:
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }    
private:
    int _year = 0;
    int _month = 1;
    int _day = 1;
    A aa;
};
int main()
{
    Date d1;
    d1.Print();
    return 0;
}

解决方案是在类里面的成员变量后面加缺省值,当我们不写构造函数,编译器默认生成构造函数,编译器做了一个偏心的处理,对于内置类型不会初始化,自定义类型会调用它的无参构造函数初始化,这里本来不处理内置类型,但是这里会处理了:

这里的成员变量只是定义,这里不是初始化,这里就像函数缺省参数一样,我们给了参数就用给的,不给就用缺省值,这里我们没写构造函数,也就没有初始化成员变量,所以全部用缺省值,故这里内置类型进行处理了,但是当我们写构造函数初始化成员变量时,初始化了的成员变量就用初始化的值,没初始化的就用默认值:

  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Date
{
public:
    //本来是构成函数重载的,但是他们两不能同时存在
    //Date d;会出现歧义,编译器不知道调用谁了,全缺省还是无参呢?
    Date(int year = 0,int month = 0,int day = 0)
    {
        _year=year;
        _month=month;
        _day=day;

    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    void Print()
    {
        cout<<_year<<_month<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;   
};
int main()
{
   Date d;
   return 0;
}

这里会报错,为什么呢?

本来是构成函数重载的,但是他们两不能同时存在

Date d;会出现歧义,编译器不知道调用谁了,全缺省还是无参呢?

误区:有的人会认为,我们不写构造函数,编译器默认生成的构造函数,叫做默认构造函数。这个是不对的。

总结:不用传参数就能调用的构造函数就是默认构造函数

  • 书写构造函数风格
class Date
{
public:
    Date(int year)
    {
        year=year;
    }
    private:
    int year;
}
int main()
{
    Date d(1);
}

这里的year是什么值呢?

这里是随机值,为什么呢?因为成员变量没有成功被初始化,这里的year是成员变量还是局部变量呢?答案是局部变量,因为编译器坚持局部优先,在看到year时,它会先看它的局部范围内有没有这个变量。

所以我们一般都建议这样写:

class Date
{
public:
    Date(int year)
    {
    	_year = year;
    }
private:
	int _year;
};

或者这样写:

class Date
{
public:
    Date(int year)
    {
        m_year = year;
    }
private:
	int m_year;
};

学习了构造函数之后,我们知道了一个对象被定义的时候会做些什么,那么一个对象的声明周期结束的时候又会做什么呢?析构函数就是做这个事情的,下面我们来看析构函数。


析构函数

析构函数概念

析构函数的功能与析构函数恰好相反,析构函数不是完成对象的销毁,局部对象销毁的工作是由编译器完成的。而在对象在销毁的时候会自动调用析构函数,完成类的一些资源清理工作

对象出了它的声明周期,就调用析构函数:

class Date
{
public:
    Date(int year)
    {
    	_year = year;
    }
    ~Date()
    {
        cout<<"~Date()"<<endl;
    }
private:
	int _year;
};
int main()
{
    Date d1(2021);
    return 0;
}

我们发现析构函数调用了,这里我们只是进行演示一下会自动调用析构函数,实际中像Date这样的类是不需要析构函数的,因为没有需要清理的资源。

析构函数特征

析构函数是特殊的成员函数。其特征如下:

  • 析构函数名是在类名前加上字符 ~。

  • 无参数无返回值。

  • 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

class Stack
{
public:
Stack(int capacity = 4)
  {
      _a = (int*)malloc(sizeof(int)*capacity);
      if(_a==nullptr)
      {
          cout<<"malloc fail"<<endl;
          exit(-1);
      }
      _top = 0;
      _capacity = capacity;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack s1;
  return 0;
}

上面这段代码编译器就会自动生成一个析构函数进行处理

但是跟构造函数类似,我们如果不写,编译器默认生成的析构函数做了偏心处理:

1、内置类型不处理

2、自定义类型成员会去调用它的析构函数

请看下面这个例子:

class A
{
public:
    A()
    {}
    ~A()
    {
        cout << "~A()" << endl;
    }
};
class Stack
{
public:
    Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
private:
    int* _a;
    int _top;
    int _capacity;
    A aa;
};
int main()
{
    Stack s1;
    return 0;
}

那么构造函数和析构函数有什么价值呢?

在我们通过两个栈实现队列的时候,会体现它的价值:

class Stack
{
public:
    Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int)*capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        free(_a);
        _top=_capacity=0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};
class MyQueue
{
private:
    Stack _pushST;
    Stack _popST;
};
int main()
{
    MyQueue mq;
    return 0;
}

在MyQueue没有写构造函数的情况下,我们调用了自定义类型的构造函数,这时相当于完成了两个栈的初始化

当我们对象在被销毁的时候:

在对象被销毁的时候,自动的完成了两个栈的析构函数的调用

这里可以看到编译器默认生成构造函数和析构函数也是有价值的

  • 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
	Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int)*capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        //完成对象中的资源清理工作
        free(_a);
        _a=nullptr;
        _top = _capacity = 0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack s1;
    return 0;
}

在前面的日期类中没有需要清理的地方,但是这个类就有需要清理的地方

可以看到它会自动的调用析构函数:

下面我们来看拷贝构造函数:


拷贝构造函数

拷贝构造函数概念

在现实生活中,我们ctrl cv就可以复制粘贴一个东西,那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?答案是可以的。

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象
创建新对象时由编译器自动调用。

拷贝函数特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  • 拷贝构造函数是构造函数的一个重载形式。

  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

在看来概念后,你肯定一脸懵逼,小白肯定会看不懂,这是啥意思呢?下面我将说一说拷贝构造函数:

我们首先创建一个日期类:

class Date
{
public:
    Date(int year = 0,int month = 0,int day = 0)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;   
};
int main()
{
   	Date d1(2021,10,9);
    d1.Print();
    
   	return 0;
}

这里我们创建了一个d1对象,并给它初始化,那么现在想拷贝复制一个d1对象出来怎么拷贝呢?

Date d2(d1);
//等价于
Date d2 = d1;

我们可以这样写,这两种都可以,是一样的,但是这样写是调用的是构造函数吗?不是的,这里调用的是拷贝构造函数,并不是我们前面讲的构造函数,对象初始化调用构造函数,但是d1是同类型,调用拷贝构造函数。

那么拷贝构造函数怎么写呢?这样写可以吗?

Date(Date d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}

我们把代码写进去看一看可不可以:

在vs2019中你都不运行它就会报错,那么为什么不可以呢?

这里会无穷递归调用拷贝构造,因为得传参:

那么怎么解决呢?

用引用来接收,这样就不会在传参时拷贝,操作引用就相当于操作它本身:

Date(Date& d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}

引用传参解决了这里传值无穷递归的问题

更加安全的写法,保证被拷贝的类对象不会被改变,在前面加const增加代码的健壮性:

Date(const Date& d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}
  • 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

我们不写,编译器默认生成的拷贝构造和构造和析构又不太一样,不会去区分内置类型和自定义类型成员,而是这两者都会处理:

1、内置类型,字节序的浅拷贝(就像memcpy完成拷贝)

2、自定义类型,会去调用它的拷贝构造完成拷贝

那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

class Stack
C++之类和对象的特性

C++之类和对象

C++之类和对象的使用

C++之类和对象

C++基础之类和对象二

C++之类和对象的使用