[C++]——带你学习类和对象

学习类和对象——中

  • 目录:
  • 一、类的6个默认成员函数
  • 二、构造函数
    • 2.1 构造函数的特性
  • 三、析构函数
    • 3.1析构函数的特性
  • 四、拷贝构造函数
    • 4.1拷贝构造函数的特性
  • 五、赋值运算符重载
    • 5.1运算符重载
    • 5.2 赋值运算符重载
    • 5.3 前置++和后置++重载
  • 六、const 成员函数
  • 七、取地址及const取地址操作符重载

目录:

在c++的学习过程中,类和对象是比较难啃的知识点,希望大家坚持学下去,勇于迎难而上,一起加油。

一、类的6个默认成员函数

一个类中什么成员都没有,简称空类。空类,并不是类里面什么都没有,编译器会生成6个默认成员函数。
默认成员函数:
没有成员函数时,编译器会自己生成的成员函数称为默认成员函数。

二、构造函数

构造函数是一种特殊的成员函数,名字和类名相同,创建类类对象时由编译器自动调用,在对象整个生命周期里只调用一次。(c++规定对象实例化,必须有构造函数)

2.1 构造函数的特性

特征:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
#include <iostream>
using namespace std;
class Date
{
public://公有
    Date()//无参构造函数
    {}
    ;
    Date(int year, int math, int day)//有参构造函数
    {
        _year = year;
        _math = math;
        _day = day;
    }

private://私有
    int _year;
    int _math;
    int _day;

};
int main()
{
    Date d1;//调用无参构造函数
    Date d2(2024,10,13);//调用有参构造函数
  /* Date(); 通过无参构造函数初始化对象时,
  对象后面不要跟括号,否则就是函数声明*/ 
    return 0;
}
  1. 如果类中没有自定义构造函数,c++会自动生成一个无参的默认构造函数(类中有自定义构造函数,编译器就不生成默认构造函数)。
  2. 默认构造类型的用处。
    c++把类型分为内置类型和自定义类型
    内置类型:内置类型不确定(看编译器),建议当成不处理。
    自定义类型:会调用这个成员的默认构造函数。
    c++11打了一个补丁,支持声明时给缺省值。
    总结:
    1.一般情况下,我们都要写构造函数。
    2.成员都是自定义类型,或者声明时给了缺省值,可以考虑使用默认构造函数。(无论什么类型的指针都是基本类型)
    如:默认构造函数的情况
#include <iostream>
using namespace std;
class Time
{
public://公有
    Time()//无参构造函数是默认构造函数
    {
        _hour = 23;
        _minute=44;
        _second=59;
    }

private://私有
    int _hour;
    int _minute;
    int _second;
};
class Date
{
public://公有



private://私有
    //声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
    int _year=2008;//给内置类型赋缺省值
    int _math=10;
    int _day=1;
    //自定义类型
    Time _t;//调用默认构造函数
};
int main()
{
    Date d1;//调用无参构造函数
    return 0;
}
  1. 可以不传参就可以调用构造函数,叫默认构造函数。
    1).编译器生成的默认构造函数。
    2).无参构造函数也可以叫默认构造函数。
    3).全缺省参数也可以叫默认构造函数。
    三个函数不能同时存在,只能存在一个。
#include <iostream>
using namespace std;
class Date
{
public://公有
    //1.编译器自己生成构造函数
    /*
    空
    */


  /*2.无参构造的函数也可以叫默认构造
  Date()
    {
        _year = 2023;
        _math = 11;
        _day = 21;
    }*/
    /*3.全缺省参数也可以叫默认构造
 Date(int year=2021,int math=12,int day=1)
   {
       _year = year;
       _math = math;
       _day = day;
   }*/


private://私有
    int _year;
    int _math;
    int _day;

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

三、析构函数

析构函数:
析构函数与构造类型相反,析构函数不是对对象本身进行销毁,局部对象销毁工作是编译器完成的 ,对象在销毁时会自动调用析构函数,完成对象中资源的清理

3.1析构函数的特性

特征:

  1. 析构函数名是在类名前面加上字符~。
  2. 无参数无返回值类型。
  3. 一个类只有一个析构函数。如果没有析构函数,系统会自动生成一个默认的析构函数。(析构函数不能重载)
  4. 对象生命周期结束时,c++编译系统自动调用析构函数。
    析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生命作用域之后,会放在局部对象之后进行析构。
  5. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
    内置类型类型不作处理。
    自定义类型成员要做处理。
#include <iostream>
using namespace std;
class Time
{
public://公有
    ~Time()//
    {
  cout<<"~Time"<<endl;
    }

private://私有
    int _hour;
    int _minute;
    int _second;
};
class Date
{
public://公有
private://私有
    //声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
    int year=1970;//给内置类型赋缺省值
    int math=1;
    int day=1;
    //自定义类型
    Time t;
};
int main()
{
    Date d1;
    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类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。

  1. 如果类中没有申请资源时,析构函数可以不写,编译器直接生成默认的析构函数,比如Date类;有资源时,一定要写析构函数,否则会造成资源泄漏,比如Stack类。

四、拷贝构造函数

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

4.1拷贝构造函数的特性

特征:

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型的引用。使用传值的方式编译器会直接报错,因为引发了无穷递归调用。
  3. 如果没有显示定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象内存存储按字节序完成拷贝,这种拷贝叫浅拷贝,也叫值拷贝。
#include <iostream>
using namespace std;
class Time
{
public://公有
    Time()//构造函数
    {
        _hour=1;
        _minute=1;
        _second=1;
}
    Time(const Time& d)//拷贝构造函数
    {
        _hour = d._hour;
        _minute = d._minute;
        _second = d._second;
    }
private://私有
    int _hour;
    int _minute;
    int _second;
};
class Date
{
public://公有

private://私有
    //声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
    int _year = 1970;//给内置类型赋缺省值
    int _math = 1;
    int _day = 1;
    //自定义类型
    Time _t;
};
int main()
{
    Date d1;
    //已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数,但是Date类并没有显示定义拷贝构造函数,则编译器会自动给Date类生成一个拷贝构造函数。
    Date d2(d1);
    return 0;
}


注意: 编译器生成默认拷贝构造函数,内置类型按照字节方式直接拷贝,自定义类型调用拷贝构造函数完成拷贝。

  1. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以,一旦涉及到资源申请,拷贝构造函数就一定要写,否则就是浅拷贝。
#include <iostream>
using namespace std;
typedef int DateType;
class Stack
{
public://公有
    //构造函数
    Stack(  int capacity = 10)
    {
        _a = (DateType*)malloc(sizeof(DateType) * capacity);
        if (_a == nullptr)
        {
            perror("  _a");
            exit(-1);
        }
        _size = 0;
        _capacity = capacity;
    }
    //压栈
    void push(const DateType& data)
    {
        _a[_size] = data;
        _size++;
    }
    //析构函数
    ~Stack()
    {
        if (_a)
        {
            free(_a);
                _a = nullptr;
            _size = 0;
            _capacity = 0;
}
    }
private://私有
    DateType* _a;//指向的内容
    int _size;//下标
    int _capacity;//容量
};
int main()
{
    Stack d1;
    d1.push(1);
    d1.push(2);
    d1.push(3);
 Stack d2(d1);
    return 0;
}

解释:

运行上面程序,程序崩溃
d1对象调用了构造函数,申请了10个元素空间,里面存了3个元素1,2,3;
d2对象使用d1拷贝构造,Stack类中没有显示定义拷贝构造函数,编译器就自己生成一份默认拷贝构造函数,默认拷贝构造函数是按照值拷贝得,将d1的内容原封不动拷贝到d2中。因此d1和d2指向同一块空间;
当程序退出时,d1和d2要销毁,先销毁d2,d2销毁时,调用析构函数,将调用的空间释放了,但d1不知道,d1销毁时,会将空间在释放一次,一块空间多次释放,肯定就造成了内存泄漏。

  1. 拷贝构造函数典型调用场景

使用已存在的对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象

深拷贝:
本质是拷贝指向的资源,让我跟你有一样大的空间,一样的值。
浅拷贝也是值拷贝:
默认生成拷贝构造函数。

#include<iostream>
using namespace std;
class Date
{
    public://公有
        Date(int year,int month,int day)//构造函数
        {
            cout << "Date(int year,int month,int day)" << endl;
        }
        Date(const Date& d)//拷贝构造函数
        {
            cout << "Date(const Date& d)" << endl;
        }
        ~Date()//析构函数
        {
            cout << "~Date()" << endl;
        }
private://私有
    int _year;
    int _month;
    int day;
};
Date test(Date d2)
{
    Date temp(d2);
    return temp;
}
int main()
{
    Date d1(2024, 11, 1);
    test(d1);
    return 0;
}

为了提高程序效率,一般对象传参时,尽量使用引用类型。

五、赋值运算符重载

赋值运算符重载使得自定义类型也能直接使用运算符。(c语言里只有内置类型才能直接使用运算法)
c++为了增强代码的可读性使用了运算符重载,运算符重载是具有特殊函数名的函数,它具有返回值类型、函数名字、参数列表、其中返回值类型和参数列表类似。
函数名为:operator后面加需要重载的运算符符号。

5.1运算符重载

  1. 不能通过连接其他的符号来创建新的操作符:如operator @
  2. 重载操作符必须有一个类类型参数。
  3. 不能通过重载运算符去改变内置类型的运算规则:内置整型+,不能改变其含义。
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数第一个参数为隐藏的this。
  5. 内置类型对象可以直接用各种运算符,因为内置类型都是简单类型,语言自己定义,编译直接就可以转换成指令;自定义类型对象不可以直接用各种运算符。
  6. .* 、::sizeof?:.(五个运算符) 。

实现一个全局的operator==
方法1:

//定义一个日期类
class Date_1
{
public://公有
    //构造函数
    Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
    {
         _year_1= year_1;
         _month_1= month_1;
         _day_1= day_1;
    }

//private://私有
    int _year_1;
    int _month_1;
    int _day_1;
};
//实现自定义类型的==判断,返回值为布尔类型
bool operator ==(const Date_1& d1, const Date_1& d2)
{
    return d1._year_1 == d2._year_1 &&
        d1._month_1 == d2._month_1 &&
        d1._day_1 == d2._day_1;
}

void test_1()
{
    Date_1 d1(2023, 11, 2);
    Date_1 d2(2023, 11, 1);
    cout << (d1 == d2) << endl;
}

int main()
{
    test_1();//运算符重载为全局时——成员变量为公有才能实现
}

方法2:

#include <iostream>
using namespace std;
//实现一个全局的operator==
//定义一个日期类
class Date_1
{
    //使用友元,全局的那个函数可以访问类的私有成员
    friend bool operator ==(const Date_1& d1, const Date_1& d2);
public://公有
    //构造函数
    Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
    {
        _year_1 = year_1;
        _month_1 = month_1;
        _day_1 = day_1;
    }

    private://私有
    int _year_1;
    int _month_1;
    int _day_1;
};
//实现自定义类型的==判断,返回值为布尔类型
bool operator ==(const Date_1& d1, const Date_1& d2)
{
    return d1._year_1 == d2._year_1 &&
        d1._month_1 == d2._month_1 &&
        d1._day_1 == d2._day_1;
}

void test_1()
{
    Date_1 d1(2023, 11, 2);
    Date_1 d2(2023, 11, 1);
    cout << (d1 == d2) << endl;
}
int main()
{
    test_1();//运算符重载为全局时——用友元,可以访问类的私有成员变量
}

方法三:调用成员函数

#include <iostream>
using namespace std;
//实现一个全局的operator==
//定义一个日期类
class Date_1
{
public://公有
    //构造函数
    Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
    {
        _year_1 = year_1;
        _month_1 = month_1;
        _day_1 = day_1;
    }
    //实现自定义类型的==判断,返回值为布尔类型
    //成员函数都有一个隐藏的this指针,实际是:bool operator ==( Date_1* this,const Date_1& d2)
    bool operator ==( const Date_1& d2)//默认d1.operator ==(d2)——>d1.operator ==(&d1,d2)
    {
        return _year_1 == d2._year_1 &&
            _month_1 == d2._month_1 &&
            _day_1 == d2._day_1;
    }
    private://私有
    int _year_1;
    int _month_1;
    int _day_1;
};


void test_1()
{
    Date_1 d1(2023, 11, 2);
    Date_1 d2(2023, 11, 1);
    cout << (d1 == d2) << endl;
}
int main()
{
    test_1();//运算符重载在类里做成员函数的方法
}

注意:
运算符重载和函数重载没有任何关系。
运算符重载:使自定义类型可以直接使用运算符
函数重载:可以允许使用参数不同的同名函数

5.2 赋值运算符重载

  1. 赋值运算符重载格式

参数类型:
返回值类型:
检测是否自己给自己赋值
返回* this:成员函数返回 *this,成员函数调用结束后,*this不销毁。(可以连续赋值)

  1. 赋值运算符只能重载成类的成员函数,不能重载成全局函数。

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载, 就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

注意:

我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。

  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
#include <iostream>
 //定义一个类
class Time
{
public://公有
    //构造函数
    Time()
    {
        _hour = 19;
        _minute = 1;
        _second =30;
    }
    //赋值运算符重载
    Time& operator =(const Time& d)
    {
        if(this!=&d)
        {
            _hour = d._hour;
            _minute = d._minute;
            _second = d._second;
        }
         return *this;
    }
private://私有
    int _hour;
    int _minute;
    int _second;
};
//定义一个类
class Date
{
public://公有
private://私有
    //内置类型
    int _year=1970;
    int _month=1;
    int _day=1;
    //自定义类型
    Time _t;
};
int main()
{
    Date d1;
    Date d2;
    d2 = d1;//两个已经初始化的对象进行赋值运算
    return 0;
}

注意:

1.operator =我们不写,编译器会直接生成默认的operator =。
2.跟拷贝构造的行为类似,内置类型值拷贝,自定义类型调用他的赋值。
3.Date和MyQuece也可以不写,默认生成的operator =就可以用。
4.Stack必须自己实现operator =,才能深拷贝。

如果类中未涉及到资源管理,赋值运算是否实现都可以,一旦涉及到资源管理,就必须实现赋值运算。

5.3 前置++和后置++重载

为了让前置++和后置++能形成正确的重载。需要对后置++进行特殊处理(语法设计有时无法逻辑闭环,那么这时候就要进行特殊处理,后置++重载时多增加一个int类型的参数)

#include <iostream>
using namespace std;
//创建一个类
class Date
{
public://公有
    //构造函数
    Date(int year = 1970, int month = 1, int day = 1)
    {
         _year= year;
        _month= month;
       _day= day;
    }
    //获取某月的天数
    int GetMonthDay(int year,int month)
    {
        int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
        //判断是否为闰年
        if (month==2&&(year % 400 == 0 || year % 4 == 0 && year % 100 != 0))
            return 29;
        return arr[month];
    }
    //赋值运算符重载
    //前置++:返回+1后的结果
    //注意:this指向的对象在函数结束后不会销毁,用引用方式返回提高效率
    /*也可以
  Date& operator ++()
  {
 _day+=1;
 return *this;
}
*/
    Date operator ++()
    {
        if(GetMonthDay(this->_year,this->_month) <++this->_day)
        { 
            this->_month++;
            this->_day = 1;
            if (this->_day == 13)
            {
                this->_year++;//年加一
                this->_month = 1;
            }
        }
        return *this;
    }
    //后置++,特殊处理加一个形参
 // 前置++和后置++都是一元运算符,为了让前置++与后置++形成正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
//注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份, 然后给this+1
 //而temp是临时对象,因此只能以值的方式返回,不能返回引用
/*也可以
  Date operator ++(int)
  {
 Date tmp(*this);//拷贝构造函数
 _day+=1;
 return tmp;
}
*/
    Date operator ++(int)
    {
        Date tmp(*this);//拷贝构造函数
        if (GetMonthDay(this->_year, this->_month) < ++this->_day)
        {
            this->_month++;
            this->_day = 1;
            if (this->_day == 13)
            {
                this->_year++;//年加一
                this->_month = 1;
            }
        }
        return tmp;
    }

//private://私有
    //声明内置变量
    int _year;
    int _month;
    int _day;
};
int main()
{
    //定义两个对象
    Date d;//默认构造函数
    Date d1(2023,11,3);//构造函数
    d = d1++;//d(2023,11,3)d1(2023,11,4)
    cout << d._day << endl;
    d = ++d1;//d(2023,11,5) d1(2023,11,5)
    cout << d._day << endl;
    return 0;
}

六、const 成员函数

将const修饰的“成员函数”称为const成员函数,const修饰成员函数,实际上是修饰该成员函数隐藏的this指针,表明在该成员函数中不能对类的任何成员进行修改。const修饰的是*this
权限可以平移也可以缩小
成员函数定义的规则:

  1. 能定义成const的成员函数都应该定义成const,这样const对象和非const对象都能调用。
  2. 要修改变量的成员函数,不能定义成const。

问题:

  1. const对象可以调用非const成员函数吗?权限放大
  2. 非const对象可以调用const成员函数吗?权限缩小
  3. const成员函数内可以调用其它的非const成员函数吗?权限放大
  4. 非const成员函数内可以调用其它的const成员函数吗?权限缩小

七、取地址及const取地址操作符重载

这两个默认的成员函数一般不用重新定义,编译器默认会生成。

#include <iostream>
using namespace std;
class Date
{
public://公有
    //构造函数
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    //取地址和const取地址操作符重载
    //取地址重载
    Date* operator&()
    {
        return this;
    }
    //const取地址重载
    const Date* operator&()const
    {
        return this;
    }
private://私有
    int _year;
    int _month;
    int _day;
};
int main()
{
   const Date d1;
    Date d2(2023, 11, 6);
    //自动匹配,找到更匹配自己的成员函数
    cout << &d1 << endl;
    cout << &d2 << endl;
    return 0;
}

这两个运算符一般不需要重载,编译器会自动生成成员函数,只有特殊情况,才需要重载(想让别人获取到指定的内容)。

补充:
双操作数的运算符,第一个参数是左操作数,第二个参数为右操作数。
写流插入流提取的赋值运算符重载时,定义的函数为全局函数(就可以自己定义函数参数的位置)
流插入——‘<<’
流提取——‘>>’
其他的运算符一般是实现成员函数>> <<流运算符必须实现到全局,这样才能让流对象做第一个参数,才符合可读性。
以日期类为例:

全局函数:
ostream& operator<<(ostream& out,const Date& d2)
{
out<<d2._year<<“年”<<d2._month<<“月”<<d2._day<<“日”<<endl;
return out;
}
在类里面定义友元函数:
friend void operator<<(ostream& out,const Date& d2);

流本质上是解决自定义类型的输入输出问题。
printf和scanf无法解决自定义类型的输入输出问题。
c++用面向对象+运算符重载来解决问题。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年12月7日
下一篇 2023年12月7日

相关推荐