Cpp04 — 默认成员函数

前言:本文章主要用于个人复习,追求简洁,感谢大家的参考、交流和搬运,后续可能会继续修改和完善。

因为是个人复习,会有部分压缩和省略。

C++中将数据类型进行了分类,分为内置类型(原生类型(int/double/bool/char)+指针)

和自定义类型(struct/class等,主要包含类类型)

类是一个整体,类内互相使用时是会上下搜索的,所以写成员函数时顺序可以打乱。

成员变量在类里的是声明,不是定义,成员函数在类里可以是声明,可以是定义,主要看我们怎么写。

封装本质上是一种管理,将数据和操作数据的方法有机结合,隐藏对象的属性和实现细节。数据和方法都封装到了类中,管理了起来,想给你访问的定义成公有,不想给的定义成私有或保护

类对象的大小计算不算成员函数,因为类和对象中的函数(成员函数)是存在公共代码段的。

目录

 一、默认成员函数

1.构造函数(主要完成初始化工作)

explicit关键字 

2.析构函数(主要完成清理工作)

关于调用顺序

3.拷贝构造函数(使用同类对象初始化创建对象)

4.赋值运算符重载(主要是把一个对象赋值给另一个对象)

运算符重载

赋值运算符重载

 一、默认成员函数

当类里面成员函数什么都不写的时候,编译器会自动生成6个默认成员函数。

六个成员函数包括:

构造函数(主要完成初始化工作)

析构函数(主要完成清理工作)

拷贝构造函数(使用同类对象初始化创建对象)

赋值重载(主要是把一个对象赋值给另一个对象)

取地址重载(主要是普通对象和const对象取地址,后面这两个很少需要自己实现)

默认成员函数对于内置类型成员不处理,对于自定义类型成员,它会去调用它的构造函数、析构函数。我们主要探讨前四个。

1.构造函数(主要完成初始化工作)

因为我们有时候可能会忘记调用初始化函数,C++为了解决这个问题,引入了构造函数来初始化,构造函数不可能没有调用,它会在对象实例化时自动调用,保证对象一定有初始化流程、保证对象一定被初始化。我们写了构造函数,编译器就不会生成构造函数了。

构造函数的特点:

1.函数名与类名相同

2.无返回值

3.对象实例化时编译器自动调用对应的构造函数(保证了对象一定会初始化)

4.构造函数可以重载(我们就可以有多种初始化方式)

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

6.不传参数就可以调用

 给一个全缺省的初始化是最好的

注意:调试的时候,只能看当前作用域里的变量

当全缺省构造函数和无任何参数的构造函数同时存在时,编译会出错,因为无法判断是要调用哪一个构造函数。语法上是可以的,但是实际使用时是不可以的。

我们不写,编译器会生成一个无参的构造函数,我们写了编译器就不会生成了。所以说构造函数是默认构造函数。虽然构造函数默认生成了,但是其初始化时,不会把值初始为0,而是随机值

对于内置类型(基本类型)语言原生定义的类型,如char、int、double、指针等等编译器不会初始化为0。对于自定义类型:class、struct等定义的类型,编译器会去调用它们的默认构造函数初始化为0。构造函数还是自己写靠谱,绝大多数情况下,编译器生成的默认构造函数并不好。(不管什么类型的指针,都是内置类型)

构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

默认构造函数:我们不写,编译器自动生成,我们写了,编译器自动生成。这个理解有一些地方不对。我们在写构造函数时,最好写不用传参就可以调用的构造函数,全缺省的构造函数是最好的,它可以适应各种场景。

1.我们不写,编译器默认生成的

2.我们自己写的无参的

编译器默认生成的并不是什么都不做,而是有区分的。默认生成的构造函数,对内置类型成员不做处理,对自定义类型成员会去调用它的默认构造函数。

默认构造只能有一个

class Date
{
    private:
        int _year;
        int _month;
        int _day;
};

int main()
{
    Date d1(2022,5,15);//可以
    Date d2;//可以
    Date d3();//不可以,未调用原型函数。没有调用到构造函数,对象没有被构造出来
    return 0;
}

 那么我们如何验证呢?如下:

class Date
{
    public:
        void Print()
        {
            cout << "cout" << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
};

int main()
{
    Date d1(2022,5,15);//可以
    Date d2;//可以
    Date d3();//不可以,未调用原型函数。没有调用到构造函数,对象没有被构造出来
    d1.Print();
    d2.Print();
    d3.Print();//这里会出错,会显示"Print"的左边必须有类/结构/联合
    return 0;
}

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

虽然有些构造函数调用之后,对象已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,不能称作初始化,因为初始化只能初始化一次,而构造函数体内可以多次赋值。

自定义类型调用它的构造函数,内置类型不处理

C++11打的补丁 

 

 

如果这么写

我们会发现,默认构造函数把全部都没处理,不论是_size还是_st1,_st2。

初始化列表

初始化列表:以一个冒号开始,接着是以逗号分隔的数据成员列表,每个成员变量后跟一个放在括号中的初始值或表达式。

注意,每个成员变量在初始化中只能出现一次。类中包含以下成员时,必须在初始化列表位置初始化。(引用成员变量、const成员变量、自定义类型变量(且该自定义类型变量没有默认构造函数),因为引用、const必须在定义时就初始化,而自定义类型成员推荐使用初始化列表初始化)

class Date

{

public:

Date(int year, int month, int day)

: _year(year)

, _month(month)

, _day(day)

{}

private:

int _year;

int _month;

int _day;

};

注意:

        1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

        2.类中包含以下成员,必须放在初始化列表位置进行初始化:

                引用成员变量、const成员变量、自定义类型成员(该类没有默认构造函数)

        3.尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化

        4.成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A

{

public:

A(int a)

:_a1(a)

,_a2(_a1)

{}

void Print() {

cout<<_a1<<" "<<_a2<<endl;

}

private:

int _a2;

int _a1;

}

int main() {

A aa(1);

aa.Print();

}

A. 输出1 1

B.程序崩溃

C.编译不通过

D.输出1 随机值

选D

初始化列表初始化后,还会去调用自己写的构造函数吗?

初始化列表可以认为时成员变量定义的地方。我们尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

class A

{

public:

    A(int a)
    :_a(a)
    {}

private:

    int _a;

};

class B
{
public:
    B(int a, int ref)
    :_aobj(a)
    ,_ref(ref)
    ,_n(10)
    {}

private:
    A _aobj;      // 没有默认构造函数
    int& _ref;    // 引用
    const int _n; // const
};

成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

在对象中,谁先声明谁就先初始化(与在函数中出现的顺序无关,与定义的顺序有关)

class A

{

public:

    A(int a)
    :_a1(a)
    ,_a2(_a1)
    {}

    void Print() 
    {}

private:
    int _a2;
    int _a1;
};

int main() 
{
    A aa(1);
    cout<<_a1<<" "<<_a2<<endl;
    aa.Print();
    return 0;
}

A. 输出1  1
B.程序崩溃
C.编译不通过
D.输出1 随机值

选D

我们不写构造函数,编译器会自己生成,我们写了,编译器就不会生成了,所以说构造函数叫默认成员函数

构造函数小总结:实际情况中,大多数情况下都要我们自己写构造函数完成初始化,并且一般情况下写一个全缺省的构造函数,这种方式更能适应多种场景

explicit关键字 

构造函数不仅可以初始化对象,对于单个参数的构造函数,还具有类型转换地作用

class Date

{

public:

Date(int year)

:_year(year)

{}

explicit Date(int year)

:_year(year)

{}

private:

int _year;

int _month:

int _day;

};

void TestDate()

{

Date d1(2018);

// 用一个整形变量给日期类型对象赋值

// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值

d1 = 2019;

}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换

 

 MyQueue用默认生成的构造函数就挺好

 

 我们会发现_size没有初始化

2.析构函数(主要完成清理工作)

对象的构造和销毁是编译器干的。构造函数的作用是对于资源的清理,会在对象销毁时(生命周期到时)自动调用

析构函数的特性:

1.析构函数需要在类名前加上~

2.没有参数没有返回值(无法重载)

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

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

默认生成的构造函数与默认生成的析构函数都是对内置类型成员不做处理,对自定义类型成员做处理。

有些类的析构函数才有意义,例如类中有malloc的。

如何判断要不要我们自己显式写出析构函数?

一般有动态开辟资源,交给内置类型存储起来的,需要我们自己写一个析构函数,否则会存在内存泄漏。(如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄露,比如Stack类)

class Time
{
public:
    ~Time()
    {
        cout << "~Time()" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};

int main()
{
    Date d;
    return 0;
}

// 程序运行结束后输出:~Time()
// 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是
//内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数



关于调用顺序

先定义的先构造后析构(构造时排在前,析构时排在后)。因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出

调用构造函数的顺序:先全局变量,因为全局变量在main函数之前就要初始化。接下来处理局的所定义的静态变量,静态变量只在第一次初始化,多次调用时,它不会再初始化了。

3012412

析构:2121403

同一块空间如果析构两次的话程序会崩溃掉。

       编译器默认生成的析构函数,对于内置类型成员不处理,对于自定义类型成员,会去调用它的析构函数
       构造函数和析构函数都不处理内置类型成员(构造不把它初始成0),但是会处理自定义类型成员
       注意,对象销毁不归析构函数管,像malloc等资源类型则很需要析构函数     

3.拷贝构造函数(使用同类对象初始化创建对象)

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

拷贝构造函数要注意深浅拷贝的问题,拷贝构造对于内置类型和自定义类型成员都会拷贝。

拷贝构造函数在我们不写时会自动生成,会对内置类型完成浅拷贝或者值拷贝。(注意,只是参数带了个引用并不构成重载)

拷贝构造若使用传值,会陷入无限传值递归中,因为会一直拷贝形参并传值。所以要调用拷贝构造,就要先传参,传参使用传值,又是对象拷贝构造,循环往复 的过程。所以用传引用完成拷贝及初始化

Date d4(d1);

这样之后d4和d1的值就是一样的。即使用d1的值初始化创建出来的d4.

拷贝构造也是一种构造函数

其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个必须是类类型对象的引用传参,使用传值方式会引发无穷递归调用。

也就是说这样写是不对的:

Date(Date d)
{
    _year = year;
    _month = month;
    _day = day;
}

 非法的拷贝构造函数,要把传参括号里的Date d写成Date& d,不然传值会一直进行拷贝构造,这里先省一个图。为了避免这个,我们有两种解决方式:1.传引用2.用指针

这里用指针比较麻烦,推荐使用传引用

拷贝构造建议加const,这样写反了才能报错

Date(const Date& d)
{
    _year = year;
    _month = month;
    _day = day;
}

Date d4(d1);

如果拷贝构造了一块空间,会出很多错误

 那么拷贝构造函数对于自定义类型呢?

自定义类型会调用它自己的拷贝构造

对于浅拷贝,默认生成的拷贝构造就够用了,像Stack这样的类,需要的是深拷贝,需要自己写

拷贝构造函数传值传参和传引用传参的区别

浅拷贝存在的问题:拷贝出来的指针和原来的指针会指向同一块空间。并且
1.调用析构函数时,这块空间被free了两次。多次释放可能把别人的释放了
2.其中一个对象插入删除数据,都会导致另一个对象也插入删除了数据

默认的拷贝构造函数有什么用?

如果没有显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象的内存存储按字节序完成拷贝,这种拷贝叫浅拷贝/值拷贝(对内置类型是按照字节方式直接拷贝的,对于自定义类型是调用其拷贝构造函数(即我们自己写的)完成拷贝)

Date ret = *this;//这里是拷贝构造。两个已经存在的对象才是赋值。

总结:一般的类都不会去用编译器默认生成的构造函数,都会自己显式地写一个全缺省的,非常好用,只有某些特殊情况下才放心使用默认生成的(栈、队列)

引用传参需要拷贝构造吗?

4.赋值运算符重载(主要是把一个对象赋值给另一个对象)

运算符重载

内置类型可以直接使用运算符运算,因为编译器知道是如何运算的。

自定义类型无法直接使用运算符,因为编译器并不知道要如何运算,如果想支持自定义类型使用运算符,就要自己实现运算符重载。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。

具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。 
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@ 
2.重载操作符必须有一个类类型或者枚举类型的操作数

3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不  能改变其含义 
4.作为类成员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参
5.   .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。(即点号.三目运算符?:作用域访问符::运算符sizeof以及.*)

注意运算符的优先级:<<、>>大于==

1.赋值运算符重载格式:

参数类型:constT&,传递引用可以提高效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为s了支持续赋值

检测是否自己给自己赋值

返回*this:要符合连续赋值的含义

成员函数中,编译器处理以后在成员(成员变量/成员函数)前面都会加this->。

2.赋值运算符只能重载成类的成员函数不能冲重载成全局函数。因为重载成全局函数时,就没有那个this指针了(我们可以重载赋值运算符,不论形参的类型是什么,赋值运算符都必须定义为成员函数)。

3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成拷贝。

其实对于任何一个类,只需要写一个>=重载或<=重载,剩下的比较运算符重载复用它们即可。

// 全局的operator==

class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

//private:

int _year;

int _month;

int _day;

};

// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?

// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。

bool operator==(const Date& d1, const Date& d2)

{

return d1._year == d2._year;

&& d1._month == d2._month

&& d1._day == d2._day;

}

void Test ()

{

Date d1(2018, 9, 26);

Date d2(2018, 9, 27);

cout<<(d1 == d2)<<endl;

}

class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

// bool operator==(Date* this, const Date& d2)

// 这里需要注意的是,左操作数是this指向的调用函数的对象

bool operator==(const Date& d2)

{

return _year == d2._year;

&& _month == d2._month

&& _day == d2._day;

}

private:

int _year;

int _month;

int _day;

};

void Test ()

{

Date d1(2018, 9, 26);

Date d2(2018, 9, 27);

cout<<(d1 == d2)<<endl;

}

让自定义类型可以像内置类型一样使用运算符需要哪个运算符,就重载哪个运算符

运算符重载跟函数重载,都有用了重载这个词,但是两个地方之间没有关联。

1.函数重载时支持定义同名函数

2.运算符重载时为了让自定义类型可以像内置类型一样取使用运算符

赋值是把值赋给变量,拷贝构造是创建一个对象时,拿同类对象初始化它。赋值拷贝时两个对象都已经存在

赋值重载:两个已经存在的对象拷贝

拷贝构造:拿一个已经存在的对象取构造初始化另一个要创建的对象

赋值运算符重载

class Date

{

public :

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

Date (const Date& d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

Date& operator=(const Date& d)

{

if(this != &d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

}

private:

int _year ;

int _month ;

int _day ;

};

赋值运算符主要有四点:

1.参数类型

2.返回值

3.检测是否自己给自己赋值

4.返回*this

5.一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象俺字节序的值拷贝

class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d1;

Date d2(2018,10, 1);

// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。

d1 = d2;

return 0;

}

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

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。

class String

{

public:

String(const char* str = "")

{

_str = (char*)malloc(strlen(str) + 1);

strcpy(_str, str);

}

~String()

{

cout << "~String()" << endl;

free(_str);

}

private:

char* _str;

};

int main()

{

String s1("hello");

String s2("world");

s1 = s2;

}

前置++和后置++的重载

Date& operator++();//前置++

Date& operator++(int);//后置++

后置++重载增加了一个int参数来与前置构成函数重载进行区分。

上述情况在调用时实际是:d1.operator++(&d1, 0);这里0的位置,只要传的是整型都可以,其作用仅仅是做区分。

delete和free是检查空指针的,如果是空,是不错的。

以上两种写法都可以

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

列宁格勒的街头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值