类和对象(中)—-第一部分

  • 前言
  • 1. 类的6个默认成员函数
  • 2. 构造函数
    • 2.1 概念
    • 2.2 特性
    • C++11中有关默认构造函数的补丁
  • 3. 析构函数
    • 3.1 概念
    • 3.2 特性

前言

哈喽大家好,我是 鹿 九 丸 \color{red}{鹿九丸} 鹿,今天给大家带来的是类和对象(中)的第一部分,因为这一篇的内容实在是太多了,补充了很多细节,相信大家能够在学习的过程中能够有所收获!
如果大家在看我的博客的过程中或者学习的过程中以及在学习方向上有什么问题或者想跟我交流的话可以加我的企鹅号: 2361038962 \color{red}{2361038962} 2361038962,或者寄邮件到相应的邮箱里: 2361038962 @ q q . c o m \color{red}{2361038962@qq.com} 2361038962@qq.com,我会尽量帮大家进行解答!

1. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情 况下,都会自动生成下面6个默认成员函数。

class Date {};

学习上面这几个函数分为下面两个大方面:

1、基本语法特性。函数名、参数、返回值、什么时候调用…

2、我们不写编译器默认生成的这个函数干了些什么。

2. 构造函数

2.1 概念

对于下面的日期类:

class Date{public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main(){Date d1, d2;d1.Init(2022, 5, 15);d1.Print();Date d2;d2.Init(2022, 5, 16);d2.Print();return 0;}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信 息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 出于这个目的,出现了构造函数

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

2.2 特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主 要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

    Date()//函数名和类名相同且没有返回值{}
  3. 对象实例化时编译器自动调用对应的构造函数。不需要在构造函数前加void,因为我们一旦加上之后,编译器一方面会认为我们定义的是普通的成员函数,但是编译器同时又认为与其类名相同的成员函数一定是构造函数,此时就出现了命名冲突现象,即我们定义的函数名,一定不可与编译器的6个默认成员函数名相同

  4. 构造函数可以重载。

    class Date{public://1.无参构造函数Date(){_year = 0;_month = 0;_day = 0;}//2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}//3.全缺省构造函数,作用相当于前面两种构造函数加起来,所以,有了第三种,第一种和第二种就可以都不使用了,所以我们习惯上写这种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;//无参d1.Print();Date d2(2022, 5, 15);//带参d2.Print();    //问:可不可以这样写?Date d3();    //答:这样是不可以的,因为编译器无法区分这是一个函数声明(函数名为d3,返回类型为Date)还是一个对象定义return 0;}

    输出结果:(保留第一种和第二种或者只保留第三种的结果)

    注意:我们一般在使用构造函数的时候使用全缺省,当然,我们也可以使用半缺省。

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

  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
    注意:无参构造函数、全缺省构造函数、我们没写时编译器默认生成的构造函数,都可以认为是默认成员函数。
    注意:并不是我们不写构造函数时系统自动调用的才是默认构造函数,而是我们不需要传参数就能调用的才是默认构造函数。
    比如下面的代码中程序就会报错:

    class Date{public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}//拷贝构造函数的使用Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;};int main(){Date d1;return 0;}

    此时程序就会出现问题,如下图所提示:

    因为我们自己定义了构造函数,所以编译器将隐藏或者说不再调用编译器自己的默认构造函数,此时我们再像上面那样去调用默认构造函数程序将无法正确运行了,除非这样进行修改:Date d1(2022, 5, 15);

    //三种默认构造函数//1.没写时编译器默认生成的构造函数类名()//实际不可见{    //1.对于内置类型会将其初始化为随机值,    //2.对于自定义类型会调用其构造函数来进行初始化}//2.无参构造函数类名(){    //可以对成员变量进行初始化    _year = 0;    _month = 0;    _day = 0;    //默认对自定义类型调用其构造函数进行初始化}//3.全缺省构造函数类名(year = 0, month = 0, day = 0){    //对成员变量进行初始化    _year = year;    _month = month;    _day = day;    //默认对自定义类型调用其构造函数进行初始化}
  7. 关于编译器生成的默认成员函数,很多人会有疑惑:在我们不实现构造函数的情况下,编译器会生成 默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

    解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

    class Time{public:Time()//Time构造函数{_hour = 0;_minute = 0;_second = 0;}int _hour;int _minute;int _second;};class Date{public:    //当我们不写构造函数的时候,编译器会生成一个默认构造函数    //内置类型/基本类型:int/char/double/指针    //自定义类型:class/struct    //默认生成构造函数对于内置类型成员变量不会做处理,但是对于自定义类型的成员变量就会调用它们的构造函数进行初始化构造    //在下面的这个例子中,因为Date类型的变量中有自定义类型的成员变量_time,所以会调用_time即Time类型的构造函数,然后将_hour、_minute、_second初始化为0    //_year、_month、_day都是内置类型,所以不做处理,打印出来是随机数void Print(){cout << _year << "-" << _month << "-" << _day << endl;cout << _time._hour << '-' << _time._minute << "-" << _time._second << endl;}private:int _year;int _month;int _day;Time _time;};int main(){Date d1;d1.Print();return 0;}

    经过打印发现输出结果为_year、_month、_day为随机数,而_time._hour、_time._minute、_time._second为0。

    打印截图:

    总结:一般情况下一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成。
    1、类里面都是自定义类型成员,并且这些成员都提供了默认构造函数。
    2、如果还有内置类型成员,声明时给了缺省值,也可以使用默认构造函数,当然这种情况相对来说比较少。
    3、使用C++库里的STL数据结构时可以使用编译器自动生成的默认构造函数。

    注意:构造函数必须定义在public权限中,否则我们无法在主函数中定义相关的变量,因为如果我们将其放在private权限中,我们在类外是无法访问的,自然也就无法进行定义该种类型的变量。

    注意:看下面的例子:

    class Time{private:int _hour;int _minute;int _second;};class Date{public:Date(){_year = 0;_month = 0;_day = 0;_time._hour = 0;_time.minute = 0;_time.second = 0;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;cout << _time._hour << '-' << _time._minute << "-" << _time._second << endl;}private:int _year;int _month;int _day;Time _time;};

    在上面的例子中,我们试图在Date的构造函数中将_time._hour、_time._minute、_time._second进行初始化,但这是无法实现的,因为在Time类中,我们将这三个变量定义为私有权限,在类外无法进行访问,所以自然也就不能在Date的构造函数中进行初始化。

    结论:各个类中应当有自己的构造函数来初始化自己的成员变量,而不应该越俎代庖,即使另一个类中定义的成员变量的权限是public,我们也不应该如此去做,因为我们无法保证我们通常使用的类类型的成员变量是public,而且一般来说,成员变量的权限也都是private,这是一个好习惯。

  8. 在上面的第6点中,默认构造函数有3种,需要注意的是,无论是三种中的哪一种默认构造函数,都能够对类中定义的自定义类型种的构造函数进行调用,例如下面的例子:

    class Time{public:Time()//会自动进行调用{_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "-" << _minute << "-" << _second << endl;}private:int _hour;int _minute;int _second;};class Date{public:Date()//在Date类的构造函数中,我们没有对Time构造函数进行显式调用(当然我们本身也无法进行显式调用),但实际上编译器会默认调用Time的构造函数的{_year = 0;_month = 0;_day = 0;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;_time.Print();}private:int _year;int _month;int _day;Time _time;};int main(){Date d1;d1.Print();return 0;}

    打印结果:

  9. 注意看下面这段代码:

    class Time{public:Time(int hour)//自己定义的构造函数,有形参,且不是全缺省,所以不再属于默认构造函数了,所以无法被自动调用{_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "-" << _minute << "-" << _second << endl;}private:int _hour;int _minute;int _second;};class Date{public:    //没有定义默认构造函数,所以编译器生成默认的构造函数,对内置类型赋随机值,对自定义类型调用其默认构造函数进行初始化void Print(){cout << _year << "-" << _month << "-" << _day << endl;_time.Print();}private:int _year;int _month;int _day;Time _time;};int main(){Date d1;return 0;}

    此时程序会报错,因为Date类型中的Time类型的变量没有默认构造函数(即我们自己手动定义的构造函数把编译器默认生成的构造函数给掩盖了),所以程序无法通过。那么这种情况下有没有解决方案呢?答案是有的,解决方案就是我们需要初始化列表,后面我们会学到。

  10. 成员变量的命名风格:我们一般习惯在成员变量的前面加一个_,或者在后面加也可以,或者在成员变量的前面加一个m(member的意思)。这是为了防止下面情况的出现:

    class Date{public:Date(int year, int month, int day)    {        year = year;//注意:此时的year是形参,而不是成员变量,此时符合就近原则,哪个近是哪个,不是成员对象        month = month;        day = day;    }private:int year;int month;int day;Time _time;};

    此时编译器将无法区分year是形参还是Date类中的成员变量了。

    当然,我们可以像下面这样定义构造函数来避免上面存在的问题:

    class Date{public:Date(int year, int month, int day)    {        this -> year = year;        this -> month = month;        this -> day = day;    }private:int year;int month;int day;Time _time;};

    虽然上面的这种方法能够很好的解决问题,但是我们还是推荐在成员变量的前面或后面加上_的方式或者在前面加上一个m的方式来避免上面存在的这种问题。

C++11中有关默认构造函数的补丁

我们知道C++的默认构造函数会对内置类型初始化为随机值,这常常会使我们无法或者很难真正使用到默认构造函数,所以C++11中出现了相关的补丁:在成员变量(内置类型)的定义时进行初始化。

class Date{private:int _year = 0;int _month = 0;int _day = 0;Time _time;};

这样我们在定义Date类型的变量调用默认构造函数时会自动将_year、_month、_day初始化为0。

==注意:这个地方我们不加初始化(因为那个地方是声明,而不是定义,所以不能称为初始化,只有在定义的时候赋值才成叫初始化),而是叫做缺省值,默认构造函数会进行使用。即等同于下面的代码:

class Date{public:    Date(int year = 0, int month = 0, int day = 0)    {        _year = year;        _month = month;        _day = day;    }private:int _year = 0;int _month = 0;int _day = 0;Time _time;};

问:C++11中的初始化和构造函数中给的缺省值哪个优先级更高呢?

答:构造函数中给的缺省值优先级更高,如果有缺省值就按照缺省值,如果没有,就按照定义时给的初始化。例如下面的代码:

class Date{public:Date(int year = 1)//year既在初始化中给出了,又在构造函数参数的缺省值给出了,此时就遵循构造函数参数中的缺省值{_year = year;        //_month和_day都没有给缺省值,所以均按照声明时给的值,即0}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year = 0;int _month = 0;int _day = 0;};int main(){Date d1;d1.Print();return 0;}

输出结果为:

3. 析构函数

3.1 概念

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作(比如释放堆区上的空间)。

问:为什么要有析构函数?为什么析构函数有时候我们还要自己进行定义?

答:因为我们定义的类的变量有时候还会和一些空间产生勾连,比如我们将类变量中的某个成员变量(指针类型)指向了堆区上的一块空间,在这个类变量被销毁后,这块空间依然存在,这就造成了内存泄漏问题,且我们将再也无法找到这块空间,这块空间编译器是无法自己进行回收的,默认的析构函数也无法回收,这个只能由程序员自己来实现。其实相当于我们在面向过程中写的Destory函数,构造函数有点类似于Init函数。

3.2 特性

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

  1. 析构函数名是在类名前加上字符 ~

  2. 无参数无返回值。

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

  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

    class Date{public:Date(int year = 0,int month = 0,int day = 0){_year = year;_month = month;_day = day;arr = (int*)malloc(sizeof(int) * 10);}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "~Date()函数的调用" << endl;free(arr);//将堆区上的那块空间释放回收        arr = NULL;}private:int _year;int _month;int _day;int* arr;};int main(){Date d1;return 0;}
  5. 构造与析构的顺序:符合栈的顺序,后定义的先析构,先定义的后析构(析构顺序和构造顺序是相反的)。
    例如:

    int main(){    Date d1;    Date d2;    return 0;}

    像上面这样进行定义,d2先析构,d1后析构。

    嵌套的类的析构顺序也是如此,我们来看下面这个代码:

    class Time{public:~Time(){cout << "~Time()函数的调用" << _hour << endl;}int _hour;};class Date{public:~Date(){cout << "~Date()函数的调用" << endl;}Time _time1;Time _time2;};int main(){Date d1;d1._time1._hour = 1;d1._time2._hour = 2;return 0;}

    运行截图:

    接下来看下一段代码:

    class Time{public:~Time(){cout << "~Time()函数的调用" << _hour << endl;}int _hour;};class Date{public:~Date(){cout << "~Date()函数的调用" << endl;}Time _time2;Time _time1;};int main(){Date d1;d1._time1._hour = 1;d1._time2._hour = 2;return 0;}

    运行截图:

  6. 编译器生成的默认析构函数,自定义类型将不做处理,但是对会自定类型成员调用它的析构函数。

    class Time{public:Time(){_hour = 0;_minute = 0;_second = 0;}~Time(){cout << "~Time()函数的调用" << endl;}private:int _hour;int _minute;int _second;};class Date{public:Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;arr = (int*)malloc(sizeof(int) * 10);}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "~Date()函数的调用" << endl;free(arr);arr = NULL;}private:int _year;int _month;int _day;int* arr;Time _time;};int main(){Date d1;return 0;}

    输出结果:

    此时我们也能够发现一个规则:在上面的例子中,Date类中定义了Time类类型的成员变量,所以要先调用Date析构函数,然后再调用Time析构函数,这是为什么呢,这是因为编译器生成的默认析构函数,自定义类型将不做处理,但是对会自定类型成员调用它的析构函数。这样,调用的顺序就出来了!