class="hide-preCode-box">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
从中得到结论 :
1.局部对象 (后定义先析构)
2.局部的静态
3.全局对象 (后定义先析构)
析构函数清理细节 :
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 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
提问 :默认析构函数对内置类型不处理,我就想让析构函数对内置类型进行处理,怎么办?
对于这个问题,我们可以采用显式析构函数,里面的逻辑是自己设计的,可以要求对内置类型进行操作,但是这样子没有价值。 内置类型不需要进行资源清除,同时将内置类型全部设置为0,同样没有完成清除的任务,对此在程序结束后,系统会自动回收内置类型的空间,不需要我们多此一举
3.4 调用类中类的析构函数细节
d
对象的销毁时,要将其内部包含的Time类的_t对象销毁,但是这里不是直接调用Time类的析构函数。因为实际要释放的是Date类对象,对此调用Date类对象对应的析构函数(编译器默认生成的析构函数),目的是在其内部调用Time。(没有直接调用Time类析构函数,通过Date类中析构函数间接调用)
小结:
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收 创建哪个类的对象,则调用该类的析构函数,销毁那个类的对象,则调用该类的析构函数 关于析构函数是否显示写,主要是看是否存在资源申请,并不是每个类都需要析构。 析构函数可以显示调用,但是可能会用引发不安全行为,需要小心调用
四、拷贝构造函数
4.1 拷贝构造函数概念
拷贝构造函数指只存在单个形参,该形参是本类类型对象的引用(一般常用const
修饰),用已存在的类类型对象创建新对象 (该过程编译器自动调用)
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;
}
private :
int _year;
int _month;
int _day;
} ;
int main ( )
{
Date d1;
Date d2 ( d1) ;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
拷贝构造函数特性:
拷贝构造函数本身属于构造函数一种重载,同类型对象进行初始化 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用 (编译器可能会强制检查)
4.2 关于对拷贝构造疑问
1.拷贝构造函数为什么只有一个参数?
拷贝构造函数需要拷贝对象参数即可,由于存在this指针,将调用对象地址传进来(编译器会自动处理)
2.为什么传值会引发无穷递归调用呢?是否可以提前写个返回条件进行拦截呢?可以使用指针类型进行接收吗?
通过函数栈帧中学习,传值过程需要开辟空间去拷贝实参数据,这里就需要调用拷贝函数。导致了传值需要调用拷贝构造,调用拷贝构造需要传值
的套娃当中。
对于返回条件拦截,实际上这里压根没有进去函数体,返回条件都用不上。指针是可以,但是指针不适合这里。使用引用给实参取别名,指向对象共占用一块内存空间,就不需要拷贝数据去调用拷贝函数,减少拷贝次数
3.使用const
修饰引用
使用const
修饰的引用意味着我们不会修改传入的对象。保证被拷贝对象不会被修改,可以及时地报错检查是否位置放反。 如果拷贝构造传的是const
修饰的变量,并且拷贝构造函数 参数部分没使用const
修饰,就会造成权限放大
4.4 浅拷贝与深拷贝
4.4.1 浅拷贝
若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)
class Time
{
public :
Time ( )
{
_hour = 1 ;
_minute = 1 ;
_second = 1 ;
}
Time ( const Time& t)
{
_hour = t. _hour;
_minute = t. _minute;
_second = t. _second;
cout << "Time::Time(const 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 d1;
Date d2 ( d1) ;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
拷贝构造是构造函数一种特殊形式,如果存在拷贝构造,编译器不会默认生成构造函数 。但是可以使用函数名 = default
强制编译器生成,但是此场景在大多数在类中类。
拷贝构造对于内置类型和自定义类型处理方式:
内置类型按照字节方式直接拷贝 自定义类型是调用其他拷贝构造函数完成拷贝
4.4.2 深拷贝
既然编译器默认生成的默认拷贝构造,本身可以实现内置类型按照字节方式直接拷贝,那么自己是否还需要实现显式拷贝构造吗?
在不同场景下,有不同场景的处理办法。接下来如果继续使用浅拷贝程序就会崩溃掉,就需要使用深拷贝解决。
typedef int DataType;
class Stack
{
public :
Stack ( size_t capacity = 10 )
{
_array = ( DataType* ) malloc ( capacity * sizeof ( DataType) ) ;
if ( nullptr == _array)
{
perror ( "malloc申请空间失败" ) ;
return ;
}
_size = 0 ;
_capacity = capacity;
}
void Push ( const DataType& data)
{
_array[ _size] = data;
_size++ ;
}
~ Stack ( )
{
if ( _array)
{
free ( _array) ;
_array = nullptr ;
_capacity = 0 ;
_size = 0 ;
}
}
private :
DataType * _array;
size_t _size;
size_t _capacity;
} ;
int main ( )
{
Stack s1;
s1. Push ( 1 ) ;
s1. Push ( 2 ) ;
s1. Push ( 3 ) ;
s1. Push ( 4 ) ;
Stack s2 ( s1) ;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
报错原因 :从图中来看,两个对象同时指向同一块空间,当对同一块空间重复析构就会报错。
由于浅拷贝是按照字节方式直接拷贝,所以只需对于array深拷贝处理,开辟等大空间,更换指向。对此解决两个对象指向同一块空间的问题,在生命周期结束时,会自动调用对应析构函数释放资源(数据拷贝到新空间,将指向转为指向新空间)
Stack ( const Stack& st)
{
_array = ( DataType* ) malloc ( st. _capacity * sizeof ( DataType) ) ;
if ( nullptr == _array)
{
perror ( "malloc申请空间失败" ) ;
return ;
}
memcpy ( _array, st. _array, st. _size * sizeof ( DataType) ) ;
_size = st. _size;
_capacity = st. _capacity;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
关于是否显示写拷贝构造函数
类中没有涉及资源申请,拷贝构造是否写都是可以 类中一旦涉及资源申请,拷贝构造一定要写,否则就是浅拷贝
4.5 拷贝构造函数典型调用场景
使用已存在对象 创建新对象 函数参数类型为类类型对象 函数返回值类型为类类型对象
class Date
{
public :
Date ( int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date ( const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~ Date ( )
{
cout << "~Date():" << this << endl;
}
private :
int _year;
int _month;
int _day;
} ;
Date Test ( Date d)
{
Date temp ( d) ;
return temp;
}
int main ( )
{
Date d1 ( 2022 , 1 , 13 ) ;
Test ( d1) ;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
为了提高程序效率,一般对象传参时,尽量使用引用类型 ,返回时根据实际场景,能用引用尽量使用引用。
五、运算符重载
关于给函数取名这件事。如果需要实现一个函数,就需要为函数起名字,这件事情很依赖写代码人的素养。
5.1 运算符重载函数概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数(不用我们取名字),也是具其返回值类型,函数名字以及参数列表,其返回值类型与参数列表于普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
该函数注意点:
不能通过连接其他符号来创建新的操作符 :比如operator@(需要是C/C++语法中存在)重载操作符必须有一个类类型参数 (不能去重载运算符改变内置类型的行为)用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义 作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针 .* :: sizeof ?: .
注意以上运算符不能重载。这个经常在笔试选择题中出现 (注意第一个不是*, *是可以重载的)并不是运算符都是需要重载的,需要看是否有存在的意义,参数部分需要对应顺序
5.2 运算符重载使用场景
祖师爷设置运算符重载的长期目标 :自定义类型也可也使用运算符,同时这里编译器可以调用这两个对象,是原因存在this指针。但是可以更简单就是下面的写法(效果是等价的,同时注意优先级的问题)
class Date
{
public :
Date ( int year = 2024 , int month = 3 , int day = 9 )
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
} ;
bool operator == ( const Date& x, const Date& y)
{
return x. _year == y. _year
&& x. _month == y. _month
&& x. _day == y. _day;
}
bool operator < ( const Date& x, const Date& y)
{
if ( x. _year < y. _year)
{
return true ;
}
else if ( x. _year == y. _year)
{
if ( x. _month < y. _month)
{
return true ;
}
else if ( x. _month == y. _month)
{
return x. _day < y. _day;
}
}
return false ;
} ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
在 C++ 中,d1 < d2
和 operator<(d1, d2)
之间,它们涉及不同的概念和语法。d1 < d2
是调用d1.operator(d2)
,operator<(d1, d2)
调用非成员函数。
5.3 访问类内成员
以上可以访问到类内成员,是由于注释了private访问限定符。如果类外面不能随便访问成员,有什么办法可以解决呢?
两种解决办法:
在类中提供Get函数 int Getname(){return _name}; 在类里面定义该函数,就可以使用类内成员
这里采用第二种方式:
在类里面定义该函数,这样子该函数有隐含this指针 ,只需要传一个参数就行。就可以这样子写d1.operator<(d2)等价于operator(const this*d1,d2)
,同时在类内部定义就可以使用private去保护成员变量,完成了封装。
关于这两种写法都是可以的,编译器知道会调用这个函数。并且第一种写法不会转为第二种写法再调用 ,而是直接调用对应的函数,中间步骤省略
六、 赋值运算重载
赋值运算符重载格式:
参数类型 :const typename &
传递引用可以提高传参效率返回值类型 :typename&
返回引用可以减少拷贝,提高返回的效率,有返回值目的是为了支持连续赋值检查是否存在自己给自己赋值 **返回*this:**要复合连续赋值的含义
6.1 判断拷贝构造函数与赋值运算重载
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;
}
void operator = ( const Date& d)
{
_year = d. _year;
_month = d. _month;
_day = d. _day;
}
private :
int _year;
int _month;
int _day;
} ;
int main ( )
{
Date d1 ( 2024 , 1 , 23 ) ;
Date d2 = ( 2024 , 2 , 28 ) ;
d2 = d1;
Date d3 = d1;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
无论是赋值运算符重载还是拷贝构造函数都可以完成对内置类型处理,那么如何识别属于赋值还是拷贝呢?那么d2 = d1
是赋值还是拷贝,Date d3 = d1;
赋值还是拷贝?
判断方法:
拷贝构造:同类型定义对象需要初始化要创建对象 赋值运算符重载:已经存在的对象,一个拷贝赋值给另一个
6.2 关于连续赋值
int main ( )
{
Date d1 ( 2024 , 1 , 23 ) ;
Date d2 ( 2024 , 2 , 28 ) ;
Date d3;
int i; int j = 10 ;
i = j = 20 ;
d3 = d2 = d1;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
内置类型:连续赋值是从右到左,这里先处理j=20
表达式,返回临时变量存储返回值,再跟i赋值。 自定义类型:连续赋值,先处理d2=d1
这里就会调用赋值运算符重载。连续赋值返回值需要改为Date,并且返回对象*this
Date operator = ( const Date& d)
{
_year = d. _year;
_month = d. _month;
_day = d. _day;
return * this ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
这种写法不推荐 。由于返回值传值返回先存储到寄存器中,传值不会返回对象本身,而是会返回他的拷贝。如果是同类,就需要调用拷贝构造 。无论如何会导致浪费,不如使用引用做返回值,减少拷贝次数。这也是指针跟引用差异。
Date& operator = ( const Date& d)
{
_year = d. _year;
_month = d. _month;
_day = d. _day;
return * this ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
6.3 自己给自己赋值
可能会存在一种情况,自己给自己赋值(一般人都不这么干,主要是白干),对此一般会加给判断语句
Date& operator = ( const Date& d)
{
if ( this != & d)
{
_year= d. _year;
_month= d. _month;
_day= d. _day;
}
return * this ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
6.4 赋值运算符重载不能重载为全局函数
赋值运算符重载跟拷贝构造类似,如果不显式实现,编译器会生成一个默认的赋值运算符重载 ,此时用户再类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突 ,故而赋值运算符只能是类的成员函数(其他运算符函数可以重载为全局函数)
特性 :用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
默认生成赋值运算符重载对于内置类型与自定义类型处理方式
内置类型成员变量直接赋值的 自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
6.5 赋值运算符中深拷贝
既然编译器生成的默认赋值运算符重载已经可以完成字节序的值拷贝,还需要自己实现吗?
接下来如果继续使用浅拷贝程序就会崩溃掉,就需要使用深拷贝解决。因为这里两个对象调用同一个函数,对同一块空间进行free,重复free会报错
typedef int DataType;
class Stack
{
public :
Stack ( size_t capacity = 10 )
{
_array = ( DataType* ) malloc ( capacity * sizeof ( DataType) ) ;
if ( nullptr == _array)
{
perror ( "malloc申请空间失败" ) ;
return ;
}
_size = 0 ;
_capacity = capacity;
}
void Push ( const DataType& data)
{
_array[ _size] = data;
_size++ ;
}
~ Stack ( )
{
if ( _array)
{
free ( _array) ;
_array = nullptr ;
_capacity = 0 ;
_size = 0 ;
}
}
private :
DataType * _array;
size_t _size;
size_t _capacity;
} ;
int main ( )
{
Stack s1;
s1. Push ( 1 ) ;
s1. Push ( 2 ) ;
s1. Push ( 3 ) ;
s1. Push ( 4 ) ;
Stack s2;
s2 = s1;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
小结:
如果类中未涉及到资源管理,赋值运算符是否实现都是可以的 如果类中涉及到资源管理,赋值运算符则必须实现
七、前置++与后置++运算符重载
前置++和后置++ 都这样子写,编译器是无法区分的。对此需要进行特殊处理(解决语法逻辑不自洽)。虽然++operator可以解决问题,但是C++给出其他解决方式
Date operator ++ ( ) ;
Date operator ++ ( ) ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
C++规定后置++重载时,多增加一个int类型的参数(用来完成重载,没有实际意义),但是调用函数时该参数不用传参,编译器会自动传递。
Date& operator ++ ( )
{
_day += 1 ;
return * this ;
}
Date operator ++ ( int )
{
Date temp ( * this ) ;
_day += 1 ;
return temp;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
八、const成员函数
将const
修饰的"成员函数"称之为const
成员函数,const
修饰类成员函数,实际修饰改成员隐含的this指针 ,表明在该成员函数中不能对类的任何成员进行修改
问题:
cosnt
对象可以调用非const
成员函数吗?非const
对象可以调用const
成员函数吗?const
成员函数内可以调用其他的非const
成员函数吗?非const
成员函数内可以调用其他的const
成员函数吗?
接下来,带着我们的问题进行下去
int main ( )
{
Date d1 ( 2024 , 1 , 31 ) ;
d1. Print ( ) ;
const Date d2 ( 2024 , 3 , 31 ) ;
d2. Print ( ) ;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
这里原因很显然是d2
的权限被放大了(权限可以缩小,但是不能放大 )
既然const
修饰类成员函数,实际修饰改成员隐含的this指针 。那么只要const
修饰this指针就可以解决权限问题。但是对于我们需要修饰this指向的内容,但是规定在this形参和实参位置不允许显式,也是不能修改的 ,那么怎么修改呢?
8.1 函数定义添加const修饰
祖师爷给出解决措施,在函数定义地方加个const
。void fname() const
,至于为什么不是 const void fname()
还是那一句,我们是语法的学习者。这样处理完了之后,对于const
对象和非const
对象都可以调用该函数
8.2 const修饰全部函数可行?
如果对成员变量只进行读访问 的函数建议加const
,这样const
对象和非const
对象都可以使用; 如果对成员变量进行读写访问的函数,不能加上const
,否则不能修改成员变量(需要修改读写权限)
补充:全局实现的运算符重载函数不存在this指针,而const
修饰成员函数是修饰this指针的 。那么流插入与流提取不是在类中实现,没有隐含的this指针,不能使用const
修饰。
对于上面的几个问题的答案:
cosnt
对象可以调用非const
成员函数吗?(不可以,权限放大)非const
对象可以调用const
成员函数吗?(可以,权限缩小)const
成员函数内可以调用其他的非const
成员函数吗?(不可以,权限放大)非const
成员函数内可以调用其他的const
成员函数吗?(可以,权限缩小)
九、&取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成的
class Date
{
public :
Date* operator & ( )
{
return this ;
}
const Date* operator & ( ) const
{
return this ;
}
private :
int _year;
int _month;
int _day;
} ;
int main ( )
{
int a = 0 ;
const int b = 10 ;
cout << & a << endl;
cout << & b << endl;
return 0 ;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
那么对于返回地址具有选择性,可以指定返回空或者返回一个像模像样的地址 (传了假地址都很难发现,写bug小妙招~)
class Date
{
public :
Date* operator & ( )
{
return null;
}
const Date* operator & ( ) const
{
return ( const Date* ) 0xeeffee ;
}
private :
int _year;
int _month;
int _day;
} ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!
data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/2302_79177254/article/details/140932152","extend1":"pc","ab":"new"}">>
评论记录:
回复评论: