目录
拷贝构造函数的参数只有一个且一必须使用引用传参,使用传值方式会引发无穷递归调用
若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?(那我们写拷贝构造和operator=的意义在哪?)
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面的6个默认成员函数
构造函数
为什么要有构造函数
通常情况下我们调用Init函数进行初始化,但是不可避免会出现忘记初始化或者在初始化之前就进行访问这些值,就会产生随机值的现象,出现随机值的危害是非常大的,所以就有了构造函数
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 7, 18);
d1.Print(); //2024-7-18
return 0;
}
如果忘记初始化
概念
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,不是真的去构造函数,构造函的主要任务并不是开空间创建对象,而是初始化对象。构造的意思是给对象开空间,但是并没有而是初始化,如果构造函数叫初始化函数,是不是更好理解,但是C++没有这样做
特性
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
5,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了构造函数,编译器不再生成
6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。 特点:不需要传参
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
class Date
{
public:
//构造函数
Date(int year, int month, int day) //无返回值
{
_year = year;
_month = month;
_day = day;
}
//构造函数 (构造函数可以重载)
Date() //无返回值
{
_year = 0;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,7,18); //不需手动调用构造函数Date,编译器会自动调用构造函数完成初始化
d1.Print();
Date d2; //对象创建出来会自动调用无参的,如果不传参数不能写成 Date d2();这个是语法规定
d2.Print();
return 0;
}
我们可以把无参的和需要传参的合并成一个构造函数,之前我们学过的缺省参数就在这里运用到了
如果函数传参了就使用传过来的参数,如果函数没有传参就使用缺省参数
class Date
{
public:
//构造函数,使用缺省参数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024,7,18);
d1.Print();
Date d2;
d2.Print();
return 0;
}
5,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了构造函数,编译器不再生成
构造函数的定义里面不是说会是完成初始化化工作的吗,为什么还是随机值,那我要你构造函数有何用。
原因:是因为默认生成的构造函数,针对内置类型没有处理,针对自定义类型做了处理
通常我们都是自己写构造函数
针对内置类型没有处理,针对自定义类型做了处理
class Time
{
public:
Time()
{
_hours = 0;
_mintue = 1;
_seconed = 3;
}
private:
int _hours;
int _mintue;
int _seconed;
};
class Date
{
public:
//1,针对内置类型的成员变量没有做处理
//2,针对自定义类型的成员变量,调用它的默认构造函数初始化
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time t; //实例化出 t 对象 自动完成对 t 对象的初始化工作
};
int main()
{
Date d1;
d1.Print(); // 打印的年月日还是随机值,对内置类型不做处理
return 0;
}
对自定义类型做处理
综上所述:当我们不写构造函数,生成的默认的构造函数大部分情况下其实并没什么用 ,通常情况下手动编写构造函数能更好地控制对象的初始化过程,保证对象的正确性和稳定性。
6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。 特点:不需要传参
小结
创建出对象的时候直接传参
调用无参的构造函数
调用的时候 Date d2,d2 后面不能加括号
析构函数
概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁
局部对象销毁工作是由编译器完成的,出了作用域会自动销毁对象
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
特性
析构函数是特殊的成员函数。
1.析构函数是在类名前加上字符 ~
2.无参数无返回值。
3.一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。不是所有的都需清理,主要针对动态开辟的,或者文件的打开关闭
析构函数不是销毁对象,对象在作用域中创建出了作用域自动销毁,而析构函数是将对象内动态开辟的空间释放,以避免内存泄漏,不仅仅只是释放空间,还有文件的关闭、以及释放其它网络资源都可以使用析构函数。
对象在生命周期到了的时候自动调用析构函数
当对象要被销毁,已经处于销毁的这个过程中,此时就会调用析构函数。可以认为是对象已经“走到了生命的尽头”,在这最后的时刻执行析构函数来做最后的清理工作。
比如,一个局部对象在其所在的作用域结束时,就在那个确切的点,对象被销毁,同时析构函数被调用。
该日期类可以不清理,因为该类仅仅包含一些基本数据类型(如整数表示年、月、日),并且没有涉及到动态分配的内存,但是编译器还是会去调用默认的析构函数,如果写了就调用写的,如果没有写就调用编译器自己生成的,但是在栈或者其它动态开辟出了的就需要清理
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//析构函数,在对象完全消失之前,系统会自动调用其析构函数
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2;
d2.Print();
cout << "------------------" << endl;
return 0;
}
运行结果:
1,栈类需要自己动手清理 ,不仅仅是栈,像我们以前学的链表、顺序表、队列都需要手动清理
2,如果类中没有显式定义析构函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了构造函数,编译器不再生成。未显式定义析构函数的时候:析构函数针对内置类型不会做处理,针对自定义类型做处理(和构造函数同理) , 析构函数会去调用自定义类型的析构函数,对自定义类型进行最后的清理工作。
class Stack
{
public:
//构造函数
Stack(int n = 10)
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
//析构函数,完成清理工作
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
return 0;
}
析构函数的析构顺序
创建对象就像函数建立栈桢,不断进行压栈操作、栈桢满足先进后出的原则,在对象的析构过程中,确实遵循这个原则。先创建的对象后析构,后创建的对象先析构,这与栈的“先进后出”特性相符。
class Date
{
public:
//构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << ":";
cout << this << endl;
}
//析构函数
~Date()
{
cout << "~Date:" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 18);
Date d2(2024, 7, 20);
//先创建的对象后析构,后创建的对象先析构,先析构d2,再析构d1
d1.Print();
d2.Print();
return 0;
}
运行结果: 先析构d2,后析构d1
析构函数与构造函数的总结
- 构造函数名称和类名相同,析构函数的名称是在类名前加 ~
- 自动调用,需要在程序中显式的通过函数名来调用
- 没有返回值
- 都是默认的成员函数
- 构造函数和析构函数针对内置类型不会做处理,针对自定义类型会做处理
拷贝构造函数
引出
那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
如果我们拷贝一份d1给d2,当d1修改的时候,d2也得跟着改,这样就不好,所以就有了拷贝构造函数
概念
拷贝构造函数是一种特殊的构造函数。
拷贝构造函数用于创建一个新对象,并使用另一个已存在对象的数据来初始化这个新对象。
创建新对象(拷贝的对象)时由编译器自动调用。
它具有以下特点:
- 函数名与类名相同,并且参数是该类类型的引用。
- 通常用于对象的复制操作,例如按值传递对象、从一个对象初始化另一个对象等。
回顾类和对象上,我们都知道编译器会在背后偷偷处理,以下就是编译器处理的结果
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且一必须使用引用传参,使用传值方式会引发无穷递归调用
- 若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
- 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?(那我们写拷贝构造和operator=的意义在哪?)
拷贝构造函数的参数只有一个且一必须使用引用传参,使用传值方式会引发无穷递归调用
我们都知道,函数在调用之前,都需要传参,但是传参过去,又是一个拷贝构造,调用拷贝构造之前又需要传参,传参又是拷贝构造,不断在重复这种过程,类似于无穷递归
如图所示:
在创建新对象 d2 的时候,会自动调用默认的拷贝构造函数,调用之前先传参,对象 d1 传给 d
传参的过程就是 Date d = d1,这又是拷贝构造,我函数还没调上呢,相当于 Date d(d1),用d1去拷贝构造 d,d1传给d又是拷贝构造,这样就会引发无穷递归
注意:Date d2 = d1 和 Date d2(d1) 编译器都会自动识别成自动去调用拷贝构造函数,这个两者是等价的,记住就行,拷贝构造时指用一个存在的对象去拷贝给一个新对象,和后面的赋值运算符重载是有区别的,赋值运算符重载是指两个对象都存在
如何解决呢,我们可以使用引用传参就不会出现这种传参又是拷贝构造的情况
调用拷贝构造的前提是创建新对象,这里是引用,没有创建新对象,就不会调用了,也就没有了无穷递归拷贝构造了
我们解决之后,还不过完善,如果我们拷贝构造函数里面,把要赋值和被赋值的对象写反了就会出现随机值,因为d2还没构造出来,默认是随机值,我们把原来d1的值覆盖成了随机值,为了防止这种情况,我们加上 const,防止传过来的值被修改
以下是拷贝构造函数的实现:
class Date
{
public:
//构造函数
Date(int year = 0, 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 Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 18);
Date d2(d1); // d1 拷贝给 d2,d2 是 d1 的拷贝
Date d3 = d1; //这里不是赋值,编译器会自动识别为拷贝构造
d1.Print();
d2.Print();
d3.Print();
return 0;
}
若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
屏蔽掉拷贝构造函数之后,发现拷贝构造仍然可以完成,为什么会存在这种情况???
原因如下:
因为这个是默认成员函数,我们不实现的时候,编译器会自动实现一份
1,构造函数和析构函数,针对内置类型不处理,针对自定义类型会去调用这个成员对象的构造函数或者析构函数
2,我们不实现时,编译器生成的 拷贝构造 和 operator=, 会完成按字节的值拷贝(浅拷贝),也就是说有些类,我们是不需要去实现拷贝构造和operator=的,因为编译器默认生成就可以用,比如:Date日期类就是 (operator= 下一个默认成员函数会提到)
那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?(那我们写拷贝构造和operator=的意义在哪?)
在栈类中的浅拷贝问题,会存在两次析构,如果不写析构就不会存在这样的两次释放问题,但是不析构(没有清理资源),会造成内存泄漏,析构了浅拷贝的崩溃就会出来,但是对于日期类这种完全没毛病。这就是它存在的意义,顺序表,链表,队列等都存在深浅拷贝的问题。
解决方法:去自己实现深拷贝的拷贝构造和operator= ,这是后面的知识点(内存管理,我们现在暂时只需知道存在深浅拷贝的问题)。
突发奇想,加上判断条件是不是可以解决这个两次释放的问题?
还是不行,因为 st2._a 置成了空指针,不会影响 st1._a
赋值运算符重载
引出
比如日期类Date的对象想比较大小相等,d1==d2 或者 d1< d2,自定义类型不能直接比较,内置类型才可以直接比较
运算符重载
运算符的重载是为了让自定义类型像内置类型一样去使用运算符
C++为了增强代码的可读性引入了运算符重载(而不是写一个函数去比较比如 IsDateEqual ),运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为: 关键字operator后面接需要重载的运算符符号 。
函数原型: 返回值类型 operator操作符(参数列表)
注意:
1,不能通过连接其他符号来创建新的操作符:比如operator@
2,重载操作符必须有一个类类型或者枚举类型的操作数
3,用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
4,作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
5,操作符有一个默认的形参this,限定为第一个形参,
6,注意以下5个运算符不能重载。
(1). (成员访问运算符)。
(2).* (成员指针访问运算符)。
(3)∷(域运算符)。
(4)sizeof(长度运算符)。
(5)?: (条件运算符)。
运算符重载的使用
1,运算符有几个操作数,operator重载的函数就有几个参数,如果在类外面重载,就需要把private改成public,因为外面无法访问私有成员,所以我们一般不这样写,运算符重载就是为了增强可读性,外面重载可读性不强
2,隐含了this指针
3,d1 > d2的重载
赋值运算符重载
返回类型是void,不能连续赋值
优化之后:
返回 *this, this 出了这个作用域还在,可以使用引用返回,在可以的情况下,尽量使用引用,减少拷贝构造
拷贝构造时另一个对象不存在,赋值运算是两个对象都存在
取地址及const取地址操作符重载
这是两个默认成员函数:一个是取地址的重载,一个是const 取地址的重载
const 成员
对象调用 const 成员函数
const 修饰类的成员函数
this指针是隐含的,需要在前面加 const 如何加呢??? const 加在这里的意义在上一节已经介绍:常量指针和指针常量
如果是隐含的this指针,我们就直接把const加在函数的后面
当被调用的函数都成了const,这属于权限的缩小,那么 const 可以调用 const,而非const 也可以调用 const,权限可以缩小,但是不能放大
成员函数调用const成员函数
const 和 非const 调用的区别
综上所述:
- const 对象不可以调用 非 const 成员函数
- 非 const 对象可以调用 const 成员函数
- const 成员函数内不可以调用其它 非const 成员函数
- 非 const成员函数内 可以调用其它的 const 成员函数