默认成员函数

默认成员函数:程序员没有写,但编译器会自动生成;程序员写了,编译器就不会生成。
4个重要的默认成员函数:构造函数、析构函数、拷贝构造、赋值重载。本文主要介绍围着这四个默认成员函数介绍

构造函数(用来初始化成员变量)

其特征如下:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时自动调用
  4. 可以重载(即:有多种初始化方式)
  5. 自己不写构造函数,编译器会自动生成一个构造函数
#include<iostream>
#include<cstdlib>
using namespace std;

class Stack
{
public:
//构造函数,与类名相同,无返回值,不需要写void
    Stack()
    {
        cout << "Stack()" << endl;
        arr = (int*)malloc(sizeof(int) * 100);
        top = 0, capacity = 100;
    }
private:
    int* arr;
    int top;
    int capacity;
};

int main()
{
    Stack st1; // 对象实例化会自动调用构造函数。可以单步调试更清晰地观察函数调用
    return 0;
}

构造函数重载

#include<iostream>
#include<cstdlib>
using namespace std;

class Stack
{
public:
    Stack()
    {
        cout << "Stack()" << endl;
        _arr = (int*)malloc(sizeof(int) * 100);
        _top = 0, _capacity = 100;
    }
	//开n个空间
    Stack(int n)
    {
        cout << "Stack(int n)" << endl;
        _arr = (int*)malloc(sizeof(int) * n);
        _top = 0, _capacity = n;
    }
	//开n * cnt个空间
    Stack(int n, int cnt)
    {
        cout << "Stack(int n, int cnt)" << endl;
        _arr = (int*)malloc(sizeof(int) * n * cnt);
        _top = 0, _capacity = n * cnt;
    }

    void Print()
    {
        printf("_arr: %p, _top: %d, _capacity: %d\n", _arr, _top, _capacity);
    }
private:
    int* _arr;
    int _top;
    int _capacity;
};

int main()
{
    Stack st1;		// 调用Stack()
    st1.Print();

    Stack st2(999); // 调用Stack(int n)
    st2.Print();

    Stack st3(12, 2);   // 调用Stack(int n, int cnt)
    st3.Print();
    // st3.Stack(); // 注意:构造函数不能用这样方式调用:对象.构造函数
    return 0;
}

运行结果如下
在这里插入图片描述

编译器实现的构造函数

上述例子中我们自己实现了构造函数,如果不实现,编译器会自动生成。介绍之前先引入一些小知识点:(可能会有点绕)

  1. 内置类型(也叫基本类型):int/char/double.../指针 等系统内置的类型
  2. 自定义类型class/struct 等需要自己定义的类型
  3. 默认构造函数:指不传参就能调用的那个构造函数,例如全缺省构造函数、没有参数的构造函数,默认构造函数只能有一个

对于第三点,举个例子:

class A
{
public:
    // 无参构造函数
    A()
    {
        cout << "A()" << endl;
        _t = 0;
    }

    // 全缺省构造函数
    A(int t = 22)
    {
        cout << "A(int t = 22)" << endl;
        _t = t;
    }
// 上面两个构造函数都是默认构造函数
private:
    int _t;
};

int main()
{
    A a; // A存在多个默认构造函数,构造时会产生歧义
    return 0;
}

编译器生成的构造函数性质:

  1. 没有参数(所以编译器生成的构造函数也是默认构造函数
  2. 对于内置类型的成员变量,不做处理。(C++没规定标准,有的编译器不做处理,有的会初始化为0。这里我们默认不会处理内置类型)
  3. 对于自定义类型的成员变量,调用其默认构造函数
class A
{
public:
    // 无参构造函数(是默认构造函数)
    A()
    {
        cout << "A()" << endl;
        _t = 0;
    }
private:
    int _t;
};

class C
{
public:
    // 全缺省构造函数(是默认构造函数)
    C(int c = 999)
    {
        cout << "C(int c = 999)" << endl;
        _t1 = c;
    }
private:
    int _t1;
};

class B
{
public:
    void Print()
    {
        printf("_b: %d\n", _b);
    }
private:
    // 我们没有写B的构造函数,编译器会自动实现
    A _a;   // 自定义类型,会调用其默认构造函数
    int _b; // 不做处理
    C _c;   // 自定义类型,会调用其默认构造函数
};

int main()
{
    B b;
    b.Print();
    return 0;
}

运行结果如下
在这里插入图片描述

小练习:

下列代码会编译错误吗?

#include<iostream>
#include<cstdlib>
using namespace std;
class A
{
private:
    int _a;
};

class B
{
private:
    int _b;
    A _a;
};
int main()
{
    B b;
    return 0;
}

上述代码编译正常,编译器会生成B构造函数去初始化其成员变量,对于_b不做处理,对于_a调用其默认构造函数(即编译器生成的构造函数)

编译器生成的构造函数对内置类型如何处理C++没有规定,但C++11打了个补丁:内置类型可以给缺省值

class Time
{
public:
    Time(int h = 10, int m = 24, int s = 0)
    {
        _hour = h;
        _minute = m;
        _second = s;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
public:

private:
    int _year = 2025;  // 内置类型给缺省值
    int _month = 9;
    int _day = 10;
    Time _t;
};

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

调试观察结果:
在这里插入图片描述
构造函数补充:
全局对象在main函数之前构造,局部静态对象第一次调用时构造

class A
{
    int _a;
public:
    A() { cout << "A()" << endl; }
};


void func()
{
//局部静态对象第一次调用时构造。局部静态对象的生命周期是全局的
    static A a2;
}

A a1; // 全局对象在main函数之前构造

int main()
{
    func();
    cout << "-------------" << endl;
    func();
    cout << "-------------" << endl;
    return 0;
}

调试观察
在这里插入图片描述
全局对象确实在main函数前构造

运行结果
在这里插入图片描述

析构函数(对象生命周期结束时释放其资源)

其特征如下:

  1. 函数名:~类名
  2. 无返回值,无参数(不支持重载,一个类只有能一个析构函数)
  3. 对象生命周期结束时自动调用
  4. 自己不写析构函数,编译器会自动生成一个析构函数
#include<iostream>
#include<cstdlib>
using namespace std;

class Stack
{
public:
//构造函数
    Stack(int n = 10)
    {
        cout << "Stack(int n = 10)" << endl;
        _arr = (int*)malloc(sizeof(int) * n);
        _top = 0, _capacity = 100;
    }

//析构函数
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_arr);
        _arr = nullptr;
        _top = _capacity = 0;
    }
private:
    int* _arr;
    int _top;
    int _capacity;
};

int main()
{
    {
        Stack st1; // st1的生命周期就在第33~34行
    }
    
    Stack st2;
    st2.~Stack(); // 析构函数支持显示调用。虽然st2声明周期结束后会再次调用析构free(_arr),但第一次析构时把_arr置为nullptr,free(nullptr)相当于无事发生
    return 0;
}

运行结果
在这里插入图片描述
调试观察:
在这里插入图片描述
调试可以看出,刚跳到Stack st2;那行代码,st1就已经调用析构函数了

编译器实现的析构函数

自己不实现析构函数,编译器会自动实现。其特性如下:

  1. 内置类型的成员变量:不做处理
  2. 自定义类型的成员变量:调用它的析构函数
class Stack
{
public:
//构造函数
    Stack(int n = 10)
    {
        cout << "Stack(int n = 10)" << endl;
        _arr = (int*)malloc(sizeof(int) * n);
        _top = 0, _capacity = 100;
    }

//析构函数
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_arr);
        _arr = nullptr;
        _top = _capacity = 0;
    }
private:
    int* _arr;
    int _top;
    int _capacity;
};

class Test
{
private:
    Stack _st;
};

int main()
{
    Test t; // Test没写构造跟析构,其自定义类型成员变量_st会调用Stack类的默认构造和析构
    return 0;
}

运行结果
在这里插入图片描述
如果只将上述代码的Test类改为:

class Test
{
private:
    Stack* _st;
};

将不会输出任何值,因为指针都是内置类型,编译器实现的析构函数对内置类型不做处理

拷贝构造

拷贝构造其特性如下:

  1. 是一种特殊的构造函数
  2. 参数只能有一个,且参数类型必须是类本身的引用
  3. 自己不写拷贝构造,编译器会自动生成一个拷贝构造。(若自己写了拷贝构造,编译器就不会生成拷贝构造,而且也不会生成构造函数;但若写了构造函数没写拷贝构造,编译器会生成拷贝构造)
class Date
{
public:
    Date(int year = 2025, int month = 9, int day = 10)
    {
        cout << "Date(int year = 2025, int month = 9, int day = 10)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }
    // Date(Date d) // 错误写法:编译报错,会引发无穷递归
    Date(Date& d)   // 正确写法,但是一般我们都会写成const Date& d
    {
        cout << "Date(const Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    void Print()
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2000, 1, 1);
    d1.Print();
    
    Date d2(d1);    //d2 拷贝 d1
    d2.Print();

    Date d3 = d1;   //这种写法也是拷贝构造。d3 拷贝 d1
    d3.Print();

    // // 下面的写法不是拷贝构造:
    // Date d4;    //调用默认构造
    // d4 = d1;    //赋值重载,接下来会讲
    return 0;
}

运行结果
在这里插入图片描述

编译器生成的拷贝构造

编译器生成的拷贝构造性质如下:

  1. 内置类型成员变量:按字节方式直接拷贝(值拷贝)
  2. 自定义类型成员变量:调用其拷贝构造完成拷贝
class Stack
{
public:
    Stack(int n = 100)
    {
        cout << "Stack(int n = 100)" << endl;
        _arr = (int*)malloc(sizeof(int) * n);
        _top = 0, _capacity = n;
    }

    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_arr);
        _top = _capacity = 0;
    }

    void Print()
    { printf("_arr: %p, _top: %d, _capacity: %d\n", _arr, _top, _capacity); }

    
private:
    int* _arr;
    int _top;
    int _capacity;
};

int main()
{
    Stack st1; 
    st1.Print();
    Stack st2 = st1; // 没写拷贝构造,编译器会自动生成
    st2.Print();
    return 0;
}

该程运行错误,调试发现:
在这里插入图片描述
st1st2的成员变量完全一致,会导致析构时free同一位置两次,这时候我们需要自己写拷贝构造
加上下面的代码,再重新运行

    Stack(const Stack& st)
    {
        cout << "Stack(const Stack& st)" << endl;
        _arr = (int*)malloc(sizeof(int) * st._capacity);
        _top = st._top;
        _capacity = st._capacity;
        for(int i = 0; i < st._top; i++)
            _arr[i] = st._arr[i];
    }

运行结果
在这里插入图片描述

运算符重载

介绍复制重载之前先简单介绍一下运算符重载
下面用一个日期类的比较大小引入:

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    // 比较类的大小. 用d1表示当前类
    // d1 > d2返回true
    bool greater(const Date& d2)
    {
        if (_year != d2._year) return _year > d2._year;
        if (_month != d2._month) return _month > d2._month;
        return _day > d2._day;
    }
    // d1 == d2返回true
    bool equal(const Date& d2)
    {
        return _day == d2._day && _year == d2._year && _month == d2._month;
    }

    // d1 < d2返回true
    bool lesser(const Date& d2)
    {
        if (_year != d2._year) return _year < d2._year;
        if (_month != d2._month) return _month < d2._month;
        return _day < d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2025, 9, 10);
    Date d2(2025, 9, 11);
    
    if(d1.greater(d2)) cout << "d1 > d2" << endl;
    else cout << "d1 <= d2" << endl;
    return 0;
}

按照上面的写法,一个比较函数就要定义一个新的函数名。C++引入运算符重载用d1 > d2 替代d1.greater(d2),简化代码提高可读性

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    // 运算符重载具有特殊的函数名,写法为:operator + 运算符
    // 比较类的大小. 用d1表示当前类
    // d1 > d2返回true
    bool operator> (const Date& d2) // 函数名为: operator>
    {
        cout << "bool operator> (const Date& d2)" << endl;
        if (_year != d2._year) return _year > d2._year;
        if (_month != d2._month) return _month > d2._month;
        return _day > d2._day;
    }
    // d1 == d2返回true
    bool operator== (const Date& d2)
    {
        cout << "bool operator== (const Date& d2)" << endl;
        return _day == d2._day && _year == d2._year && _month == d2._month;
    }

    // d1 < d2返回true
    bool operator< (const Date& d2)
    {
        cout << "bool operator< (const Date& d2)" << endl;
        if (_year != d2._year) return _year < d2._year;
        if (_month != d2._month) return _month < d2._month;
        return _day < d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2025, 9, 10);
    Date d2(2025, 9, 11);

    // 这里的 d1 > d2 等价于 d1.operator>(d2)
    if(d1 > d2) cout << "d1 > d2" << endl;
	//当然你也可以将上面那行代码直接写成:if(d1.operator>(d2)) cout << "d1 > d2" << endl;
		
    // 这里同上,d1 == d2 等价于 d1.operator==(d2)
    if(d1 == d2) cout << "d1 == d2" << endl;

    // d1.operator<(d2)
    if(d1 < d2) cout << "d1 < d2" << endl;
    return 0;
}

运行结果
在这里插入图片描述

运算符重载详细解析点这里

赋值重载

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    Date (const Date& d)
    {
        cout << "Date (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
    }

    void operator= (const Date& d)
    {
        cout << "Date operator= (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2020, 10, 10);
    Date d2 = d1; // 调用拷贝构造

    Date d3;    // 调用默认构造
    d2 = d1;    // 调用赋值重载 d2.operator=(d1);
    return 0;
}

运行结果
在这里插入图片描述
void operator= (const Date& d) 返回值为void,就不能连续赋值,例如:d3 = d2 = d1;
若想连续赋值,返回值应改为:Date / Date&,具体如下:

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    Date (const Date& d)
    {
        cout << "Date (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
    }
	// 返回值改为Date&, 若为Date会调用不必要的拷贝构造
    Date& operator= (const Date& d)
    {
        cout << "Date operator= (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2020, 10, 10);
    Date d2, d3;

    d2 = d3 = d1; // 支持连续赋值
    // 等价于 d2.operator=(d3.operator=(d1));
    return 0;
}

运行结果
在这里插入图片描述
若将赋值重载函数的返回值改为Date,会进行不必要的拷贝。原理详情传值返回与引用返回
赋值重载返回值改为Date的运行结果
在这里插入图片描述

编译器生成的赋值重载函数

如果自己不实现赋值重载,编译器会自动生成,生成的赋值重载性质如下:

  1. 内置类型成员变量:直接赋值
  2. 自定义类型成员变量:调用其赋值重载函数
class A
{
public:
    A& operator=(const A& a1)
    {
        cout << "A& operator=(const A& a1)" << endl;
        _a = a1._a;
        return *this;
    }
private:
    int _a = 1;
};

class T
{
public:
    T(int x = 1)
    {
        _t = x;
    }
private:
    int _t;
    A _aa;
};

int main()
{
    T t1(222);
    T t2;
    t2 = t1;	// T类里的_t是内置类型,直接赋值;_aa是自定义类型,调用其赋值重载
    return 0;
}

运行结果
在这里插入图片描述

传值返回与引用返回

传值返回

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    Date (const Date& d)
    {
        cout << "Date (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

Date func() // 返回Date值
{
    Date d;
    return d;
}

int main()
{
    func();
    return 0;
}

运行结果
在这里插入图片描述
原因:传值返回会调用拷贝构造,func()返回d时,会创建一个临时对象来储存d的值,将其返回
但是你们的运行结果可能什么都不输出,这是因为编译器进行了返回值优化点击查看关闭返回值优化

传引用返回

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }

    Date(const Date& d)
    {
        cout << "Date (const Date& d)" << endl;
        _year = d._year, _month = d._month, _day = d._day;
    }

    void Print()
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }
private:
    int _year;
    int _month;
    int _day;
};

Date& func() // 返回Date引用
{
    Date d(2022, 10, 10);
    return d;	// 引用返回不会调用拷贝构造
}
int main()
{
    Date d1 = func(); // 这里会调用拷贝构造
    d1.Print();
    return 0;
}

运行结果
在这里插入图片描述
上述代码不同的编译器上可能有不同的结果,但原因都类似
func()返回的是d的引用,但是d被析构了,这是个“野引用”,其结果是未知的

什么时候可以用传引用返回?
答: 返回的对象没有析构就可以。例如将上述代码的func()改为

Date& func()
{
    static Date d(2022, 10, 10); // 函数调用完不会析构
    return d;
}

练习:实现日期类的比较函数

Date.h文件(可以根据函数声明自己实现函数定义)

#pragma once
#include<iostream>
using namespace std;

class Date
{
public:
    Date(int y = 2025, int m = 9, int d = 11)
    {
        _year = y, _month = m, _day = d;
    }
    bool operator< (const Date& d);
    bool operator> (const Date& d);
    bool operator== (const Date& d);
    bool operator<= (const Date& d);
    bool operator>= (const Date& d);
    bool operator!= (const Date& d);
private:
    int _year;
    int _month;
    int _day;
};

Date.cpp 文件

#include"Date.h"

// 声明跟定义分离,要指定类域
bool Date::operator< (const Date& d)
{
    if (_year != d._year) return _year < d._year;
    if (_month != d._month) return _month < d._month;
    return _day < d._day;
}

bool Date::operator== (const Date& d)
{
    return _day == d._day && _month == d._month && _year == d._year;
}

bool Date::operator<= (const Date& d)
{
    return *this < d || *this == d;
}

bool Date::operator> (const Date& d)
{
    return !(*this <= d); // 不<=就是大于
}

bool Date::operator>= (const Date& d)
{
    return !(*this < d);
}

bool Date::operator!= (const Date& d)
{
    return !(*this == d);
}

关闭返回值优化

这里建议用老版编译器详细观察。
Dev-C+:工具->编译选项->加入指令-fno-elide-constructors关闭返回值优化
在这里插入图片描述
vscode:在tasks.json"args"里添加指令"-fno-elide-constructors",
在这里插入图片描述
Linuxg++ -fno-elide-constructors yourcode.cpp
其他编译器:暂时未知

更多推荐