C++ 类(中)

Posted 玄鸟轩墨

tags:

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

@toc

写在前面

我们已经初步的认识到到类的内容了,但是那些都是基础的,今天我们来分享一些比较细节的内容,也是对我知识的一个梳理,难度比较大,所以这个博客我可能写的不太好,到时候有什么问题,可以直接在评论区留言,我看到立马回复,要是我也不懂的,就去给你们查资料,还请谅解.

类的6个默认成员函数

这个博客主要就是和大家分享成员函数(方法),里面的内容比较多,我一个一个来吧,我们现在来看道例题,作为今天的开篇.

class Person


;

这不就是一个空类吗?里面什么都没有.要是你这么想,就有些简单了,是的,我们看到里面什么都没有,但是实际上编译器会自动生成六个默认的成员函数,他们存在这个类里面,至于如何验证,单机先不要着急.

我们先来看看这六个成员函数,但是我们只详细说其中的四个,另外的两个不太重要,简略的谈谈就可以了.

  • 构造函数 完成对象的初始化.不是构造对象
  • 析构函数 完成资源的清理
  • 拷贝构造 对象拷贝必须调用拷贝函数
  • 赋值重载 重新定义运算符


构造函数

构造函数又称构造方法,在C++中是帮助我们实例化对象的作用,记住是帮助我实例化对象.我们先来看看这个东西.

class Person

public:
    void Print()
    
        cout << "我叫" << _name << ",今年" << _age << "岁了" << endl;
    
    void Set(const char* name, const int age)
    
        strcpy(_name, name);
        _age = age;
    

private:
    int _age;
    char _name[20];
;

int main()

    Person per1;
    per1.Set("张三", 18);  //每次都要  set
    per1.Print();
    return 0;

构造函数的特性

现在我们就可以正式认识构造函数了,构造函数存在下面的几个特性.

  • 没有返回值 记住返回值是没有 不是void
  • 函数名和类名一样
  • 支持重载
  • 对象实例化时编译器自动调用对应的构造函数

我们先来把前面的代码优化一下,写一个构造函数

class Person

public:
    void Print()
    
        cout << "我叫" << _name << ",今年" << _age << "岁了" << endl;
    
    // 带有 两个  参数(编译器的那个 this 没算)的 构造函数
    Person(const char* name, const int age)
    
        strcpy(_name, name);
        _age = age;
    

private:
    int _age;
    char _name[20];
;

int main()

    Person per1("张三", 18);

    per1.Print();
    return 0;

默认构造函数

这个属于构造函数的特别的一类,我们需要先看看下面的东西,只是初步认识,后面细谈.

  • 编译器自动生成
  • 无参的构造函数
  • 全缺省的构造函数

编译器自动生成

一般情况下,构造函数谈到上面那里就可以了,但是对于我们还远远不够.我们需要理解一些东西.再刚开始的代码中,我想问问类里面存在构造函数吗?这个问题,我一开始就回答了,存在的编译器自动生成一个构造函数.

我们验证一下,假如我们在类类里面自己写了构造函数,看看会什么?

class A

public:
    A(int a)
    

    
;

int main()

    A _a;

    return 0;

我们开始疑惑,我们不是写了一个构造函数,为何会报错,.我们可以一眼看出,我们实例化对象的时候没有传参,也就是问题所在,从这里我们就可以看出,实例化对象的时候编译器会自动调用相匹配的构造函数,也就是说,实例化对象的时候定会存在构造函数的参与.

但是看看下面的代码,为何不报错? 原因就是编译器自动生成了一个无参的构造函数,至于它的结构是啥,就不需要了解了.

class B


;

int main()

    B b;
    return 0;

无参的构造函数

我们自己写的无参的构造函数也是属于默认的构造函数,这里和大家说一下.

这里面就比较简单了,至于代码里面的那个问题,不要急,最后我会说的.

class A

public:
    A()
    
        a = 0;
    
    void Print()
    
        cout << "无参构造函数" << endl;
        cout << "a = " << a << endl;
    
public :
    int a;
;

int main()

    A a;    //  为哈  不是 写   A a()?
    a.Print();
    return 0;

全缺省的构造函数

这是最后一个了,一般到这里而言我们就对默认构造函数很了解了,理解这个就比较简单了.我们看看现象基本可以了.

class A

public:
    // 全缺省
    A( int a = 10)
    
        _a = a;
    
    void Print()
    
        cout << "无参构造函数" << endl;
        cout << "a = " << _a << endl;
    
public :
    int _a;
;

int main()

    A a;    //  为哈  不是 写   A a()?
    a.Print();
    return 0;

为哈是 Aa

这个就要来解决上面遗留的问题了,这个说实话是语法规定的,我也很难搞懂,但是我们可以通过现象来搞懂一些东西.

先解决编译器自己生成的那个,这里我们解决不了,这里现象也不看了,记住就行.

再开始解决自己写的自己写的无参的构造函数,我们看看现象.

class A

public:

    A()
    
    
    void Print()
    
        cout << "你好,世界" << endl;
    
;

int main()

    A a();
    a.Print();
    return 0;

最后一个就是全缺省构造函数

class A

public:
    // 全缺省
    A(int a = 10)
    
        _a = a;
    
    A()
    
    
    void Print()
    
        cout << "无参构造函数" << endl;
        cout << "a = " << _a << endl;
    
public:
    int _a;
;

int main()

    A a();    
    a.Print();
    return 0;

这个报错和上面一样,这个我们就不理解了,全缺省不是允许我们可以这么做吗?是的,但是这里就不允许了.

默认构造函数的优先级

这个我想和大家谈谈,也不知道我的标题名字写的准确不准确,我也不知道这个知识点的名字叫什么,这里先给大家描述一下,

假如 一个类里面存在无参的构造函数,也存在一个全缺省的构造函数,我们使用 A a;调用的是哪个构造方法?这里用现象来得到答案.

class A

public:
    // 全缺省
    A(int a = 10)
    
        cout << "全缺省" << endl;
    
    //无参
    A()
    
        cout << "无参" << endl;
    
;

int main()

    A a;
    return 0;

默认构造函数的作用

我们前面就提了,构造函数是为了我们进行初始化的.但是这个初始化也是有很大的问题的,这里C++在之前有很大的缺陷,直到C++11才弥补了一部分.

对内置类型不做改变

C++这个特性很让人苦恼,在使用默认构造的时候对内置类型竟然不做处理,是在是有点难以让人接受.我们一看就可以明白了.

class A

public:
    A()
    

    
private:
    int _a;
    double _d;
;

int main()

    A a;
    return 0;

C++11填坑

这个坑太大了,反正我是相对这个问题骂骂咧咧,幸好C++11把这个坑给填了,但是这个方法也会出初学者造成很大的问题,我先说方法

class A

public:
    A()
    

    
private:
    int _a = 0;    // 在这 声明
    double _d = 0.0;
;

对自定义类型进行初始化

上面的标题描述的也不太准确,这句话可以这样说,对于自定类型,编译器会调用这个自定义类的==默认构造函数==,来帮助进行初始化.

class A

public:
    A()
    
        cout << "辅助 进行  初始化" << endl;
    
private:
    int _a;
    double _d;
;

class B

public:
    B()
    

    
    A _aa;
;

int main()

    B b;
    return 0;

我们需要看看里面的结果,再来一次调试.

有人可能眼见,看到_aa里面的内容也没有进行初始化,这是由于我们在A类中没有把默认初始化给写好,我重新写一下A类,在调试一下.这样就可以了.

class A

public:
    A()
    
        //在这写好
        _a = 0;
        _d = 0.0;
    
private:
    int _a;
    double _d;
;

总结

说了这么多,现在需要来个总结,我们学习了构造函数,知道了默认构造函数,也明白了构造函数的作用,这些都是比较有难度的.


析构函数

如果说构造函数是为了初始化,那么析构函数就是为了资源的清理工作,对于一些比较用以忘得程序员,这是一个福音,比如我们使用malloc开辟了一款空间,有的时候容易忘记free掉,这就会造成内存泄漏.这就会有一定得问题.但是析构函数可以在对象生命周期结束后,会自动调用这个析构函数.我们只需要在这析构函数free里面就可以了.

析构函数得特性

我们先来看看析构函数得特性,这是我们得基础.

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

编译器自动调用

我们先看看个例子.

class A

public:
    A(int cap = 4)
    
        int* arr = (int*)malloc(sizeof(int) * cap);
        assert(arr);
        a = arr;
        memset(arr, 0, sizeof(int) * cap);
        _cap = cap;
    
    ~A()
    
        _cap = 0;
        free(a);
        a = nullptr;
    
private:
    int* a;
    int _cap;
;

int main()

    A a;
    return 0;

从上面得动图我们就可以知道了,在对象a得声明周期结束后,编译器会自动调用析构函数,完成资源得清理.

默认生成得析构函数

我们需要看看默认生成得析构函数会怎样,这样可以帮助我们更高效得写出代码.

析构函数会对内置类型进行资源清理吗

很抱歉,并不能帮助我们把内置类型给清理掉.

class A

public:
    A(int cap = 4)
    
        int* arr = (int*)malloc(sizeof(int) * cap);
        assert(arr);
        a = arr;
        memset(arr, 0, sizeof(int) * cap);
        _cap = cap;
    

private:
    int* a;
    int _cap;
;

析构函数可以清理自定义类型吗

这个是可以得,不过需要调用自定义类型得析构函数,这个和默认构造函数的初始化一样的.

会调用自定义了类型的析构函数

class A

public:

    ~A()
    
        cout << "自定义类型的析构函数" << endl;
    
;

class B

private:
    A _aa;
;

int main()

    B b;

    return 0;

我们也可以看看是如何调用的析构函数.

class A

public:

    A(int cap = 4)
    
        int* arr = (int*)malloc(sizeof(int) * cap);
        assert(arr);
        a = arr;
        memset(arr, 0, sizeof(int) * cap);
        _cap = cap;
    

    ~A()
    
        _cap = 0;
        free(a);
        a = nullptr;
    

private:
    int* a;
    int _cap;
;

class B

public:
    B()
    

    

private:
    A _aa;
;

int main()

    B b;

    return 0;

总结

这样我们也可以得到一个结果,对于自定类型我们不需要写析构函数,对于内置类型需要进行资源清理,避免内存泄漏.


拷贝构造

那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?拷贝构造是构造函数的一种,也是我们未来写类比较关键的内容,我们需要了解一下。

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

值拷贝

我们起初在学习函数的时候,大多时候都会给函数传入参数,也就是编译器另开辟一块空间,把要出传入的内容拷贝一份放到这块空间里面.这就是简单的值拷贝.

我们确实需要好好看看这个值拷贝,我们发现它们的地址是不一样的.

void func(int b)

    cout << "&a" << &b << endl;

int main()

    int a = 10;
    func(a);
    cout <<"&a" << &a << endl;
    return 0;

对于一些简单的类型,这个拷贝是没有问题的,但是现在我要和大家看看这个.

void func(int* pb)

    free(pb);

int main()

    int* arr = nullptr;
    arr = (int*)malloc(sizeof(int) * 4);
    func(arr);
    free(arr);
    return 0;

我们就会发现一个问题,对于一些类型,简单的值拷贝完全不够,上面为何会报错?原因就是我们把数组名作为参数,编译器简单的把它当做了一个指针,拷贝给了pb,但是pb的所指向的内容是没有变的,所以我们free掉了两次,程序会中断.

拷贝构造的特性

我们认识到了值拷贝,现在我们就可以说拷贝构造了,拷贝构造也是编译器默认生成的构造函数,函数名和类名一样.

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用

第二个话题我们先不谈,最后分享.

默认生成的拷贝构造

我们先来看看默认生成的拷贝构造作用如何,来看看我们何时需要自己写拷贝构造

class A

public:
    A(int a = 0,double d = 0.0)
    
        _a = a;
        _d = d;
    
    ~A()
    
        _a = 0;
        _d = 0.0;
    

public:
    int _a;
    double _d;
;

int main()

    A _aa(1, 3.0);
    cout<< "_aa._a = " << _aa._a;
    cout<< "   _aa._d = " << _aa._d << endl;

    A _bb(_aa);

    cout << "_bb._a = " << _bb._a;
    cout << "   _bb._d = " << _bb._d << endl;
    return 0;

这个构造可以可以说是一样,所以我们不用担心编译器这一次不会不管内置类型了,这一点很好,但是也出现问题了,下面来看.

默认构造函数是值拷贝吗

这个问题很严重,要知道我们对象在生命周期结束后是会调用析构函数的,要是出现两次free这个情况,我想谁都会骂娘.

看看吧

class A

public:
    A(int cap = 4)
    
        int* pa = (int*)malloc(sizeof(int) * cap);
        assert(pa);
        _array = pa;
    
    ~A()
    
        free(_array);
        _array = nullptr;
    
public:
    int* _array;
;
void func(A _bb)

    cout << _bb._array << endl;


int main()

    A _aa;
    func(_aa);

    cout << _aa._array << endl;
    return 0;

int main()

    A _aa;
    A _bb(_aa);
    return 0;

手写构造函数

分享了这么多,我们好象还没有手写构造函数,这个来个普通的,但是里面的细节也很多.

class A

public:
    A(int a = 0, double d = 0.0)
    
        _a = a;
        _d = d;
    
    A(const A& a)
    
        _a = a._a;
        _d = a._d;
    
private:
    int _a;
    double _d;
;

我们开始抠细节了.

为何用 const 修饰

很好,我们可以不用const修饰,但是有时候会写出这样的代码.

A(const A& a)

    a._a = _a; //写反了 
    _d = a._d;

用const修饰就可以避免这种失误,因为它编译不过,可以很快的查出问题所在.

为何使用引用

你发现了最为重要的东西,首先要记住一点,<font color = red>自定义类型要实现拷贝,必须先调用构造函数</font>,如果你写的是普通传参,那也要进行拷贝,需要构造函数,编译器开始寻找构造函数,找到构造函数发现要进行拷贝,寻找构造函数.....出现死循环,所以我们要使用别名,避免拷贝.

构造函数对内置类型怎么办

这个我们前面已经说的很详细了,这里就给出一个结论,如果你的类里面没有指向同一片空间的这种类似的属性,用默认的就可以了,但是要是存在,就需要自己来写,至于如何写这涉及到深浅拷贝的知识了,这里就不谈了.这个规律适合大部分情况.

构造函数对自定义类型怎么办

这个我就不放动图了,它和构造函数以及析构函数一样,去寻找自定义类型自己的构造函数.


赋值运算符重载

本来想分两篇来写的,这个也是一个很大的内容,我们可以自己创造赋值运算符的实现规则,我们先来看看什么是赋值运算符重载.

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

为何出现赋值运算符重载

我们先来看一种请况.

为啥会报错,我就想让他们比较一下,我有什么错?可编译器却不允许,今天我必须让它给我允许了,这就是赋值运算符重载 为何会出现的原因

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(2022, 5, 18);
    Date d2(2022, 5, 18);

    if (d1 == d2)
    
        cout << "==" << endl;
    
    return 0;

赋值运算符重载

多的不说,我们现在就来看看如何使它变得合理.

我们来看看这个函数,现在出现了一个问题,我们得不到类的属性,它被封装了,我们先把属性改成public,后面在解决这个问题.

bool operator==(Date d1, Date d2)

    //在这里 年月日都相等  才是 相等
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;

这里就可以了,我们调用一下这个函数

int main()

    Date d1(2022, 5, 18);
    Date d2(2022, 5, 18);

    if (operator==(d1,d2))
    
        cout << "==" << endl;
    
    return 0;

我们可能会疑惑,我随便取一个函数名就可以把这个函数的功能给写出来,还用弄得这样花里胡哨,但是你写的函数可以被这样调用吗?但是我的就可以.

if (d1 == d2)

    cout << "==" << endl;

这个就是运算符重载的魅力,现在我需要把这个函数给完善下,传引用,没必要在开辟空间了,用const修饰,避免被不小心修改

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

    //在这里 年月日都相等  才是 相等
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;

解决不能得到属性的问题

这个我给两个解决方法,一个是在类里面写一些get函数,得到这些属性的值,另一个是使用友元,但是这种方法破坏了封装,不太建议.

在类里面写运算符重载

我们还不如直接在类里面写这个函数呢,简单快捷,这样就可以避免破坏封装.

class Date

public:
    Date(int year = 1900, int month = 1, int day = 1)
    
        _year = year;
        _month = month;
        _day = day;
    
    bool operator==(const Date& d1, const Date& d2)
    
        //在这里 年月日都相等  才是 相等
        return d1._year == d2._year
            && d1._month == d2._month
            && d1._day == d2._day;
    
public:
    int _year;
    int _month;
    int _day;
;

bool operator==(const Date& d) //默认添加一个  this  指针

    //在这里 年月日都相等  才是 相等
    return _year == d._year
        && _month == d._month
        && _day == d._day;

这样函数的调用就变成这样

int main()

    Date d1(2022, 5, 18);
    Date d2(2022, 5, 18);

    if (d1 == d2)    //   d1 == d2 默认  变成   d1.operator==(d2) 
    
        cout << "==" << endl;
    

    return 0;

class Date

public:
    Date(int year = 1900, int month = 1, int day = 1)
    
        _year = year;
        _month = month;
        _day = day;
    
    bool operator==(const Date& d)
    
        cout << "this" << this << endl;
        return true;
    
public:
    int _year;
    int _month;
    int _day;
;

int main()

    Date d1(2022, 5, 18);
    Date d2(2022, 5, 18);
    cout << "d1" << &d1 << endl;
    cout << "d2" << &d2 << endl;
    if (d1 == d2)
    

    
    return 0;

重载 运算符 "="

本来我想和大家分享一个日期类,但是如果现在这这里写了,至少还需要5000字,我把它单独放到了一个博客,作为我们这些天学习类一个小总结,在这个日期类里面,你会发现我们上面谈的所有的知识点,这里先来点小菜,这个很简单,目的是为了引出下面的知识点.

Date& operator=(const Date& d)

    if (this != &d)
    
        _year = d._year;
        _month = d._month;
        _day = d._day;
    
    return *this;

我们先来调用一下.

int main()

    Date d1(2022, 10, 18);
    Date d2;
    d2 = d1;
    d2.Print();
    return 0;

d2 = d1

细心的朋友可能发现我们写的是d2 = d1;而不是在开辟d2的时候给他赋值,这里我要重点谈下.我们通过调试来看看吧.

这个调用的是运算符重载.

int main()

    Date d1(2022, 10, 18);
    Date d2;
    d2 = d1;
    d2.Print();
    return 0;

Date d2 = d1

这个调用的是拷贝构造,不是那个运算符重载.

int main()

    Date d1(2022, 10, 18);
    Date d2 = d1;
    d2.Print();
    return 0;

以上是关于C++ 类(中)的主要内容,如果未能解决你的问题,请参考以下文章

C++ 动态内存开辟

C++ 动态内存开辟

C++:new操作符

C++中new的用法

C++内存管理+模板

C++中new的用法