【C++初阶】二、类与对象(中篇)

简介: 目录一、类的6个默认成员函数二、构造函数2.1 构造函数概念 2.2 构造函数的特性三、析构函数3.1 析构函数的概念3.2 析构函数的特性四、拷贝构造函数 4.1 拷贝构造函数概念4.2 拷贝构造函数的特性 五、赋值运算符重载5.1 运算符重载5.2 赋值运算符重载六、const 成员七、取地址及const取地址操作符重载

目录

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

二、构造函数

2.1 构造函数概念

2.2 构造函数的特性

三、析构函数

3.1 析构函数的概念

3.2 析构函数的特性

四、拷贝构造函数

4.1 拷贝构造函数概念

4.2 拷贝构造函数的特性

五、赋值运算符重载

5.1 运算符重载

5.2 赋值运算符重载

六、const 成员

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


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

如果一个类中什么成员都没有,简称为空类

class Date {};

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

       默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

image.png

其中,构造函数、析构函数,拷贝构造函数、赋值重载 重点学,最后两个关于取地址重载不重要,简单了解即可。

这些函数都不是普通函数,不能以普通函数的定义和调用规则去理解它们

-------------------我是分割线------------------

二、构造函数

2.1 构造函数概念

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该函数方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

Date类代码如下:

classDate{
public:
voidInit(intyear, intmonth, intday)
    {
_year=year;
_month=month;
_day=day;
    }
voidPrint()
    {
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1;
d1.Init(2022, 9, 29);
d1.Print();
Dated2;
d2.Init(2022, 9, 30);
d2.Print();
return0;
}

上面的初始化每次都要调用 Init 进行初始化,这样很不方便,接下来构造函数就要登场了

构造函数的概念:

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

如何创建构造函数,请看构造函数的特性,方便说构造函数的特性

2.2 构造函数的特性

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

构造函数其特征如下 :

  • (1)函数名与类名相同
  • (2)无返回值
  • (3)对象实例化时编译器自动调用对应的构造函数

注:构造函数是可以有多个的,有多个构造函数也说明可以有多种初始化方式,其他情况下面有解释

classDate{
public:
//1、无参构造函数Date()
    {}
// 2.带参构造函数Date(intyear, intmonth, intday)
    {
_year=year;
_month=month;
_day=day;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1;// 自动调用无参构造函数Dated2(2022, 9, 30);// 自动调用带参的构造函数return0;
}

        写了没有参数的构造函数(如上代码注释的第一点),创建类类型无参,如d1,则自动调用无参的构造函数进行初始化;写了有参数的构造函数(如上代码注释的第二点),创建类类型有参,如d2,则自动调用有参的构造函数进行初始化

image.png

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如

1. 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
2. Date d3();

简单说就是创建对象是无参的,构造函数也是无参的,对象后不用加 (),这是语法规定的,否则就会报出警告

image.png

(4)构造函数可以重载

如上面代码中的 Date ,参数个数不同

image.png

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

测试代码:

classDate{
public:
如果用户显式定义了构造函数,编译器将不再生成//Date()//{}private:
int_year;
int_month;
int_day;
};
intmain()
{
//没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数//如果将上面定义的构造函数放开,则调用自己创建的构造函数Dated3;
return0;
}

(6)关于编译器生成的默认成员函数,我们很多会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是 d对象 _year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

测试代码:

classDate{
public:
//没有定义构造函数,编译器默认调用自动生成的构造函数private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated;
return0;
}

image.png

解答:

       C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...还包括所有类型的指针,自定义类型就是我们使用class/struct/union 等自己定义的类型,如栈、队列...


    C++ 对内置类型不作处理,也就是不调用构造函数,自定义类型则自动调用它的默认构造函数

看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数

测试代码:

classTime{
public:
Time()
    {
cout<<"Time()"<<endl;
_hour=0;
_minute=0;
_second=0;
    }
private:
int_hour;
int_minute;
int_second;
};
classDate{
private:
// 基本类型(内置类型)int_year;
int_month;
int_day;
// 自定义类型Time_t;
};
intmain()
{
Dated;
return0;
}

_t 自动调用它的默认成员函数

image.png

 对内置类型不作处理,是 C++ 在早期设计时的语法缺陷,后面也发现了这个问题,因为 C++ 要向前兼容,对内置类型就不作处理了,而是打了个补丁

        注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值 

测试代码:

classDate{
private:
// 基本类型(内置类型)int_year=2022;//不是定义,依旧是声明,给的值是缺省值int_month=9;
int_day=30;
};
intmain()
{
Dated;
return0;
}

image.png

注意:这里给的默认值是缺省值,不是定义,依旧是声明

这个补丁雀实会给很多初学者造成困扰,很多人会认为它是初始化,其实它是缺省值

(7)无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

总结:不传参数就可以调用的构造函数,就叫默认函数

以下测试代码能通过编译吗

classDate{
public:
//无参构造函数Date()
    {
_year=1900;
_month=1;
_day=1;
    }
//全缺省的构造函数Date(intyear=1900, intmonth=1, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
private:
int_year;
int_month;
int_day;
};
// 以下测试函数能通过编译吗?intmain()
{
Dated;
return0;
}

答案是不能,因为默认构造函数只能有一个,有多个调用是会造成歧义,编译器不知道调用哪一个

image.png

-------------------我是分割线------------------

三、析构函数

3.1 析构函数的概念

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

析构函数的功能与之前数据结构中使用的顺序表、栈销毁函数Destory 功能相似。

3.2 析构函数的特性

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

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

测试代码,栈类:

typedefintDataType;
classStack{
public:
//构造函数Stack(size_tcapacity=3)
    {
_array= (DataType*)malloc(sizeof(DataType) *capacity);
if (NULL==_array)
        {
perror("malloc申请空间失败!!!");
return;
        }
_capacity=capacity;
_size=0;
    }
voidPush(DataTypedata)
    {
// CheckCapacity();_array[_size] =data;
_size++;
    }
//析构函数~Stack()
    {
if (_array)
        {
free(_array);
_array=NULL;
_capacity=0;
_size=0;
        }
    }
private:
DataType*_array;
int_capacity;
int_size;
};
intmain()
{
Stacks;
s.Push(1);
s.Push(2);
return0;
}

对象生命周期结束时,自动调用析构函数,对空间资源进行释放清理

image.png

(5) 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数

测试代码:

classTime{
public:
~Time()
    {
cout<<"~Time()"<<endl;
    }
private:
int_hour;
int_minute;
int_second;
};
classDate{
private:
// 基本类型(内置类型)int_year=2022;
int_month=9;
int_day=30;
// 自定义类型Time_t;
};
intmain()
{
Dated;
return0;
}

运行结果打印了 ~Time ,说明了 _t 调用了自己的析构函数(套娃操作)

在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类生成的默认析构函数

注意创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

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

-------------------我是分割线------------------

我怎么知道要不要写析构函数和构造函数?

看面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写

       比如,Date Stack的构造函数需要自己写,不写就会调用编译器默认生成的构造函数,初始化成随机数,这随机数不是我们想要的初始数据,所以需要自己写

比如,用栈实现队列,MyQueue 构造函数就不需要自己写,默认生成就可以用

比如,Stack栈的析构函数,需要我们自己写,不写就会造成内存泄漏

比如,MyQueue Date就不需要自己写析构函数,默认生成就可以用,没有需要清理的空间

构造函数和析构函数极大方便了我们,特别是有时候忘记初始化数据和忘记释放空间, 构造函数和析构函数就帮我们擦屁股了,很方便

   -------------------我是分割线------------------

四、拷贝构造函数

4.1 拷贝构造函数概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

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

4.2 拷贝构造函数的特性

拷贝构造函数也是特殊的成员函数,其特征如下:

(1)拷贝构造函数是构造函数的一个重载形式

(2)拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

测试代码:

classDate{
public:
Date(intyear=2022, intmonth=10, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
Date(Date&d) // 正确写法//Date(Date d) // 错误写法:编译报错,会引发无穷递归    {
_year=d._year;
_month=d._month;
_day=d._day;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1;
Dated2(d1);
return0;
}

运行结果,d2 拷贝了 d1

image.png

如果是传值传参的方式,编译器直接报错,因为使用传值传参的过程中,编译器会自动调用拷贝构造函数,使用拷贝构造函数就必须得先传参,传参又会调用拷贝构造函数,一直调用下去就会造成无穷递归(图中的const 先无视,下面讲,有没有const都差不多)

image.png

要理解这里,必须要清楚 以传值的方式会自动调用拷贝构造函数,往这点想就可以想明白了,下面进行一下测试,证明一下以传值的方式会自动调用拷贝构造函数

测试代码:

classDate{
public:
Date(intyear=2022, intmonth=10, intday=1)
        {
_year=year;
_month=month;
_day=day;
        }
Date(Date&d) 
    {
cout<<"测试谁自动调用了拷贝构造函数"<<endl;
    }
private:
int_year;
int_month;
int_day;
};
voidFunc1(Dated)
{
//预测:传值传参会调用拷贝构造函数cout<<"Func1()"<<endl;
}
voidFunc2(Date&d)
{
//预测:引用传参不会调用拷贝构造函数cout<<"Func2()"<<endl;
}
intmain()
{
Dated1(2022, 9, 22); // 构造 - 初始化Func1(d1);
cout<<"------我是分割线------"<<endl;
Func2(d1);
return0;
}

运行结果说明传值方式会自动调用拷贝构造函数

image.png

说一下 对类类型对象的引用一般常用const修饰的原因

image.png

就是为了防止有些人头脑发热把代码写反了,比如把_day = d._day 写成 d._day = _day;

classDate{
public:
Date(intyear=2022, intmonth=10, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
Date(Date&d) 
    {
_year=d._year;
_month=d._month;
d._day=_day;//错误,写反了//_day = d._day;//正确    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1;
Dated2(d1);
return0;
}

运行结果,d2 反而把 d1的值改掉了

image.png

加上 const ,编译器直接检测报错

image.png

image.png

(3)若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

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

classTime{
public:
Time()
    {
_hour=1;
_minute=1;
_second=1;
    }
Time(constTime&t)
    {
_hour=t._hour;
_minute=t._minute;
_second=t._second;
cout<<"Time::Time(const Time&)"<<endl;
    }
private:
int_hour;
int_minute;
int_second;
};
classDate{
private:
// 基本类型(内置类型)int_year=2022;
int_month=10;
int_day=1;
// 自定义类型Time_t;
};
intmain()
{
Dated1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数//Date类里面的自定义类型 _t 则调用它自己拷贝构造函数Dated2(d1);
return0;
}

运行结果说明了自定义类型是调用其拷贝构造函数完成拷贝的

image.png

(4)编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的,默认生成的就够用了。那么下面的栈类呢?验证一下试试?

测试代码:

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决typedefintDataType;
classStack{
public:
Stack(size_tcapacity=10)
    {
_array= (DataType*)malloc(capacity*sizeof(DataType));
if (nullptr==_array)
        {
perror("malloc申请空间失败");
return;
        }
_size=0;
_capacity=capacity;
    }
voidPush(constDataType&data)
    {
// CheckCapacity();_array[_size] =data;
_size++;
    }
~Stack()
    {
if (_array)
        {
free(_array);
_array=nullptr;
_capacity=0;
_size=0;
        }
    }
private:
DataType*_array;
size_t_size;
size_t_capacity;
};
intmain()
{
Stacks1;
s1.Push(1);
s1.Push(2);
Stacks2(s1);
return0;
}

运行时崩溃的,因为这种默认生成的拷贝构造函数,拷贝都是浅拷贝,没有进行深拷贝,进行浅拷贝的结果就是两个栈共用同一块空间,进行析构清理的时候同一块空间被释放了两次,运行自然就崩溃了(深拷贝后面会用一个大章节去讲,这里先知道即可)


image.png

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

总结两点:

  1. 需要写析构函数的类,都需要写深拷贝的拷贝构造  如 Stack
  2. 不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用  如Date/MyQueue

(5)拷贝构造函数典型调用场景:

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

  -------------------我是分割线------------------

五、赋值运算符重载

在学习赋值运算符重载之前,首先学习运算符重载,帮助理解

5.1 运算符重载

       C++为了增强代码的可读性引入了运算符重载,用户可以让编译器按照指定的规则对自定义类型对象直接进行运算符操作。运算符重载是具有特殊函数名的函数也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似


函数名字为:关键字operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)

参数:操作符有几个操作数,它就有几个参数注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*   ::   sizeof   ?:   .   注意以上5个运算符不能重载。这个经常在笔试选择题中出现

比如要两个类日期 d1/d2 要进行比较是否相等, 必须要对 == 进行运算符重载,否则就无法比较,默认情况C++是不支持自定义类型对象使用运算符

测试代码:

// 全局的operator==classDate{
public:
Date(intyear=2022, intmonth=10, intday=2)
    {
_year=year;
_month=month;
_day=day;
    }
//private://私有int_year;
int_month;
int_day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。//关键字operator 后面接需要重载的运算符符号,如 operator==booloperator==(constDate&d1, constDate&d2)
{
returnd1._year==d2._year&&d1._month==d2._month&&d1._day==d2._day;
}
intmain()
{
Dated1(2022, 10, 2);
Dated2(2022, 10, 3);
//cout << d1 == d2 << endl;//error,要加括号使 d1 == d2 优先进行运算cout<< (d1==d2) <<endl;//d1 == d2 编译器自动转换成 operator==(d1, d2);return0;
}

运行结果是 false,说明两个日期不相等

image.png

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

这里其实可以用后面学习的友元函数解决,或者干脆重载成成员函数,但是都不推荐

这里可以把 operator== 放到类里面去,这样就可以直接使用类里面的私有成员了,没有破坏类的封装,但是放到类里面有一个问题,如下:

测试代码:

classDate{
public:
Date(intyear=2022, intmonth=10, intday=2)
    {
_year=year;
_month=month;
_day=day;
    }
booloperator==(constDate&d1, constDate&d2)
    {
returnd1._year==d2._year&&d1._month==d2._month&&d1._day==d2._day;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1(2022, 10, 2);
Dated2(2022, 10, 3);
//cout << d1 == d2 << endl;//error,要加括号使 d1 == d2 优先进行运算cout<< (d1==d2) <<endl;
return0;
}

运行是无法运行的,会显示参数过多

image.png

这是为啥,代码也没有错,怎么解决?

别忘了,类成员函数还有一个默认的 this 指针,参数加起来有三个,而 = 只有两个操作数,参数明显超了,所以报错会显示参数太多

不要忘记了这句话,参数:操作符有几个操作数,它就有几个参数

修改后,默认隐藏的 this 指针可以当作第一个操作数,再补一个参数就可以了

修改后代码:

classDate{
public:
Date(intyear=2022, intmonth=10, intday=2)
    {
_year=year;
_month=month;
_day=day;
    }
// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象booloperator==(constDate&d2)
    {
return_year==d2._year&&_month==d2._month&&_day==d2._day;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1(2022, 10, 2);
Dated2(2022, 10, 3);
//cout << d1 == d2 << endl;//error,要加括号使 d1 == d2 优先进行运算cout<< (d1==d2) <<endl;
return0;
}

运行就成功了

image.png

5.2 赋值运算符重载

(1)赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回 *this :要符合连续赋值的含义

测试代码:

classDate{
public:
Date(intyear=2022, intmonth=1, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
//拷贝构造Date(constDate&d)
    {
_year=d._year;
_month=d._month;
_day=d._day;
    }
//赋值运算重载Date&operator=(constDate&d)
    {
if (this!=&d)//极端情况下自己给自己赋值        {
_year=d._year;
_month=d._month;
_day=d._day;
        }
return*this;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1(2022, 10, 2);
Dated2(2022, 11, 3);
// 拷贝构造:一个已经存在的对象拷贝初始化一个马上创建实例化的对象Dated3(d1);  // 拷贝构造Dated4=d1; // 拷贝构造// 赋值运算符重载:两个已经存在的对象之间进行赋值拷贝d1=d2; // 赋值运算符重载:编译器自动转换d1.operator=(d3)return0;
}

运行结果,调试看 d1 = d2

image.png

注意区分拷贝构造和赋值运算重载,上面已经注释有了,不再多说。

-------------------我是分割线------------------

(2)赋值运算符只能重载成类的成员函数不能重载成全局函数

意思就是说赋值运算符重载只能写在类里面,不能写到全局域里面

测试代码:

classDate{
public:
Date(intyear=2022, intmonth=10, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
//private:int_year;
int_month;
int_day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数Date&operator=(Date&left, constDate&right)
{
if (&left!=&right)
    {
left._year=right._year;
left._month=right._month;
left._day=right._day;
    }
returnleft;
}

编译失败

image.png

原因:

      赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中

生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

image.png

(4)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,但是深拷贝依旧要我们自己实现,因为编译器生成的默认赋值运算符重载函数只是进行了浅拷贝

测试代码:

classTime{
public:
Time()
    {
_hour=1;
_minute=1;
_second=1;
    }
Time&operator=(constTime&t)
    {
if (this!=&t)
        {
_hour=t._hour;
_minute=t._minute;
_second=t._second;
        }
return*this;
    }
private:
int_hour;
int_minute;
int_second;
};
classDate{
private:
// 基本类型(内置类型)int_year=2022;
int_month=10;
int_day=1;
// 自定义类型Time_t;
};
intmain()
{
Dated1;
Dated2;
d1=d2;
return0;
}

这里编译器默认生成的赋值运算符重载函数就够用了

image.png

但是涉及到空间资源管理的默认的不够用,要自己写深拷贝的赋值运算符重载函数

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。typedefintDataType;
classStack{
public:
Stack(size_tcapacity=10)
    {
_array= (DataType*)malloc(capacity*sizeof(DataType));
if (nullptr==_array)
        {
perror("malloc申请空间失败");
return;
        }
_size=0;
_capacity=capacity;
    }
voidPush(constDataType&data)
    {
// CheckCapacity();_array[_size] =data;
_size++;
    }
~Stack()
    {
if (_array)
        {
free(_array);
_array=nullptr;
_capacity=0;
_size=0;
        }
    }
private:
DataType*_array;
size_t_size;
size_t_capacity;
};
intmain()
{
Stacks1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stacks2;
s2=s1;
return0;
}

这里就会运行崩溃

image.png

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现

 -------------------我是分割线------------------

前置++和后置++重载

前置++:返回+1之后的结果

后置++:后置++是先使用后+1,前置++和后置++都是一元运算符

为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

测试代码:

classDate{
public:
Date(intyear=2021, intmonth=1, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date&operator++()
    {
_day+=1;
return*this;
    }
// 后置++:// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Dateoperator++(int)
    {
Datetemp(*this);
_day+=1;
returntemp;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated;
Dated1(2022, 10, 1);
d=d1++; // d:2022,10,1    d1:2022,10,2d=++d1; // d:2022,10,3   d1:2022,10,3return0;
}

只截了 d = d1++ 这一步完成的图,下一步 d = ++d1 自行调试查看

image.png

-------------------我是分割线------------------

总结一下:

       默认生成的四个默认成员函数,构造函数和析构函数的处理机制是基本类似的,拷贝构造和赋值重载处理机制也是类似的

-------------------我是分割线------------------  

六、const 成员

       将 const修饰的“成员函数”称之为 const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的 this指针,表明在该成员函数中不能对类的任何成员进行修改

基本的修饰方法如下,在类成员函数的括号后加 const即可

1. void Print()const
2. {}

const 实际修饰的是该函数隐含的 this指针

       this 指针本身是 类名* const 类型的,比如日期类的this指针类型是 Date* const,用 const修饰后变为 const 类名* const 类型,比如日期类的 this指针被const修饰后,类型变成了 const Date* const,该this指针是隐藏的,不显示。

测试代码:

classDate{
public:
Date(intyear=0, intmonth=1, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
voidPrint1()//void Print(Date* this)    {
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
int_year;
int_month;
int_day;
};
intmain()
{
Dated1(2022, 10, 3);
d1.Print1();//okreturn0;
}

该段代码是可以正常运行的,如果再创建一个变量用 const修饰就不能运行了,因为权限被放大了,权限只能缩小和平行,不能被放大,这个在C++入门中引用讲过了,自行翻阅

1.     const Date d2(2022, 10, 3);
2.  d2.Print1();//error,

image.png

这时候如果我们在函数后面加了const,就可以避免此种权限放大问题。这样不管是上面的 d1/d2 就都可以正常打印了!

image.png

总结:

  1. const对象不可以调用非const成员函数(权限放大)
  2. 非const对象可以调用const成员函数(权限缩小)
  3. const成员函数内不可以调用其他非const成员函数(权限放大)
  4. 非const成员函数可以独调用其他const成员函数(权限缩小)

    -------------------我是分割线------------------  

const 修饰成员函数什么时候需要使用?

成员函数加 const,变成 const成员函数是有好处的,这样 const对象可以调用,非 const对象也可以调用

众所周周知,const修饰指针有下面两种形式

  • 在*之前修饰,代表该指针指向对象的内容不能被修改(地址里的内容不能改)
  • 在*之后修饰,代表该指针指向的对象不能被修改(指向的地址不能改)

this指针本身就是 类型名* const 类型的,它本身不能被修改。加上 const之后,this指向的内容,既类里面的成员变量也不能被修改了,知道了这一点后,我们可以合理的判断出:只要是需要修改类中成员变量的函数,就不需要在()后面加 const修饰,如果一个函数中不需要修改成员变量,就可以加const进行修饰

注意:如果你用了声明和定义分离的写法,那么声明和定义的函数都需要加上 const修饰

  -------------------我是分割线------------------

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

最后两个默认成员函数,编译器会自动生成。这两个函数一般都不需要重载

测试代码:

classDate{
public:
Date(intyear=1, intmonth=1, intday=1)
    {
_year=year;
_month=month;
_day=day;
    }
Date*operator&()
    {
returnthis;
    }
constDate*operator&()const    {
returnthis;
    }
private:
int_year; 
int_month; 
int_day; 
};
intmain()
{
Dated1(2022, 10, 1);
constDated2(2022,10, 2);
cout<<&d1<<endl;
cout<<&d2<<endl;
return0;
}

说白了就是用来取类对象地址的

image.png说明:

       一般不需要写,编译器生成的就够用(上面写出来方便理解)。如果非要写,比如不想让别人获取对象的地址,就可以自己实现,返回 nullptr

-------------------我是分割线------------------

文章先到这,下篇即将更新

相关文章
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
60 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
111 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
111 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
148 4
|
3月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
35 4
|
3月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
33 4
|
3月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
30 1
|
3月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
23 0
|
3月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
3月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)