目录
一 . 类的默认成员函数
二 . 构造函数
三 . 析构函数
四 . 拷贝构造函数
4.1 写法以及相关问题
4.2 自动生成拷贝构造
一 . 类的默认成员函数
默认成员函数就是用户没有显示实现 , 编译器会自动生成的成员函数称为默认成员函数 。 一个类 , 在不写的情况下 , 编译器会默认生成以下的 6 个 默认成员函数 , 需要注意的是这 6个中最重要的是前 4 个 , 最后两个取地址重载不重要 , 稍微了解就好 。 其次 C++11 以后还会增加两个默认成员函数 , 移动构造 和 移动赋值 , 后续更新 ...
二 . 构造函数
构造函数是特殊的成员函数 , 需要注意的是 ---> 构造函数虽然名称为 构造 , 但 是构造函数的主要任务并不是开辟空间,创造对象(我们常使用的局部对象是栈帧创建时 , 空间就开好了) , 而是对象实例化时 初始化对象 。
构造函数的本质是要替代我们以前 Stack 和 Date 类中写的 Init 函数的功能 , 构造函数自动调用的特点就完美的替代了 Init 。
构造函数的特点 :
- 函数名 与 类名相同 。
- 无返回值 。 ( 返回值啥都不需要给 , 也不需要写 void )
- 对象实例化时 系统 会 自动调用 对应的构造函数 。
- 构造函数可以重载
- 如果类中没有显示定义构造函数 , 则C++编译器会自动生成一个无参的默认构造函数 ,一旦用户显示定义 ----> 编译器将不再生成 。
- 无参构造函数 , 全缺省构造函数 , 在不写构造函数时编译器默认生成的构造函数都叫做默认构造函数 。 但是这三个函数有且只有一个存在 , 不能同时存在 。无参构造函数和全缺省构造函数虽然构成函数重载 , 但是调用时会存在歧义 。注意 !注意 !注意 ! 默认构造函数并不只有编译器默认生成的那个叫默认构造 , 实际上无参构造函数 , 全缺省构造函数也是默认构造函数 , 总结以下 ----> 就是不传实参就可以调用的构造就叫默认构造 。
- 不写构造函数时 , 编译器默认生成的构造 , 对内置类型成员变量的初始化没有要求 , 也就是是否初始化是不确定 , 看编译器 。 对于自定义类型成员变量 , 要求调用这个成员变量的默认构造函数初始化 。 如果这个成员变量 , 没有默认构造函数 , 那么就会报错 , 需要初始化这个成员变量 , 需要使用初始化列表才能解决 , 初始化列表是啥 ? 后续更新 ....
接下来使用 日期类 对 构造函数的特点 详细讲解 --->
实现日期类的构造函数 :
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Date
{public://无参构造函数Date(){_year = 1;_month = 1;_day = 1;}//带参的构造函数Date(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.Print();Date d2(2024 , 11 , 16);d2.Print();return 0;
}
构造函数是放在public 里面 , 不然的话 , 调用不了 :
构造函数可以重载 ,为啥 ?
----> 因为函数可以有不同的初始化的方式 ,对象实例化的时候会 调用对应的构造函数
思考1 :
思考 :当全缺省函数添加进来时 , 是否可以正常调用 ? ---> 不可以噢~
class Date
{public://无参构造函数Date(){_year = 1;_month = 1;_day = 1;}//带参的构造函数Date(int year,int month , int day){_year = year;_month = month;_day = day;}//全缺省构造函数Date(int year = 1,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;
};
思考 : 什么情况下需要自己写构造 ?
//日期类没有写构造函数时
// -->发现并没有成功初始化!!!#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Date
{
public:void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();return 0;
}
所以成员变量为内置类型的时候需要自己写构造函数 , 仅对于某些编译器可能会初始化 , 但是没啥保障 , 还得自己来 !
默认构造函数不是只有 编译器自动调用!!!
啥时候不需要自己写构造 ?
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}
private:STDataType * _a;size_t _capacity;size_t _top;};
//两个栈实现一个队列
class MyQueue
{
private:Stack _pushst;Stack _popst;
};int main()
{//调用了栈的初始化 -- MyQueue不需要写构造,默认生成的就够用了MyQueue mq;return 0;
}
三 . 析构函数
析构函数与构造函数功能相反 , 析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的 , 函数结束栈帧销毁 , 它就释放了 , 不需要我们管 , C++规定对象再销毁时会自动调用析构函数 , 完成对对象中的资源清理释放工作 。 析构函数的功能类比我们之前Stack 实现的Destory 的功能 , 而像Date 没有 Destory , 其实就是没有资源需要释放 , 所以严格说Date , 是不需要析构函数的 。
析构函数的特点 :
- 析构函数名是在类名前加上字符~ 。
- 无参数无返回值 。 (这里与构造类似 , 也不需要加void)
- 一个类只能有 一个 析构函数 。 若未显式定义,系统会自动生成默认的析构函数 。
- 对象生命周期结束时 , 系统会自动调用析构函数 。
- 与构造函数类似 , 我们不写构造函数时 , 编译器会自动生成析构函数 , 但是对内置类型成员不做处理 , 自定类型成员会调用他的析构函数 。
- 需要注意的时 , 显示写析构函数时 , 对于自定义类型成员也会调用自定义类型中的析构 , 也就是说自定义类型成员无论什么情况都会自动调用析构函数 。
- 如果类中 没有 申请资源时 , 析构函数可以不写 , 直接使用编译器生成的默认析构函数 , 如 Date ; 如果默认生成的析构就可以用 , 也就不需要显示写析构函数 , 如MyQueue ; 但是有资源申请时 , 一定要自己写析构 , 否则会造成资源泄漏 , 如Stack 。
- 一个局部域的多个对象 , C++规定 后定义 的先析构 。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个栈实现一个队列
class MyQueue
{// 不需要写构造,默认生成就可以用// 不需要写析构,默认生成就可以用
private:Stack _pushst;Stack _popst;
};int main()
{MyQueue mq1;return 0;
}
四 . 拷贝构造函数
如果一个构造函数的 第一个参数 是 自身类类型的引用 , 且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数 , 也就是说拷贝构造 ( 拷贝初始化 ) 是一个特殊的构造函数 。
拷贝构造的特点 :
- 拷贝构造函数 是构造函数的一个重载 。
- 拷贝构造函数的参数 只有一个且必须是类 类型对象的引用 , 使用传值方式编译器直接报错 , 因为语法上会引发 无穷递归调用 。
- C++ 规定自定义类型对象进行拷贝行为必须调用 拷贝构造 , 所以这里自定义类型传值传参 和 返回都会调用拷贝构造完成 。
- 若未显式定义拷贝构造 , 编译器会 自动生成 拷贝构造函数 。 自动生成的拷贝构造对内置类型成员变量会完成 值拷贝/浅拷贝(一个字节一个字节拷贝) , 对自定义类型成员变量会调用他的拷贝构造 。
4.1 写法以及相关问题
举 日期类为例 :
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
什么时候需要用到拷贝构造函数(拷贝初始化,完成对象的拷贝) ?
-----> 通过一个对象 初始化 新创建的对象 ( 以下代码是通过d1 初始化 d2)
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷贝构造函数//构造函数一种形式的重载(函数名相同,参数不同)//Date d2(d1)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,11,16);d1.Print();Date d2(d1);d2.Print();return 0;
}
C++ 规定自定义类型对象进行拷贝行为必须调用 拷贝构造 , 所以这里自定义类型 传值传参 和 传值返回 都会调用拷贝构造完成 。
可以通过控制台 , 调试观察 , 是否调用了 拷贝构造 , 何时调用的拷贝构造 。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1,int day = 1){_year = year;_month = month;_day = day;}//拷贝构造 -- 构造函数的一个重载//Date d2(d1)Date(const Date& d){cout << "Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};void func1(Date d)
{}//传返回值 -- 也会调用拷贝构造
//传值返回不是返回 d , 而是d 的拷贝
//d 拷贝的临时对象 返回d
Date func2()
{Date d;//...return d;
}
int main()
{Date d1(2024, 11, 16);func1(d1);func1(d1);//C++规定 -- 无论传参还是直接初始化,只要是一个自定义类型对象去初始化另一个自定义类型对象的时候//要调用拷贝对象//调用func1的时候 , 先传参 , 调用拷贝构造return 0;
}
思考 :
1 ) 为什么 必须 加 &
---> 不加 & , 会发生无穷递归 !!!
注 : 如果编译器此时没报错 , 但也运行不过去 , 因为发生了无穷递归 (没有返回条件)。
传值返回 会产生 一个临时对象 调用拷贝构造 ;
传值引用返回 , 返回的是返回对象的别名(引用) , 没有产生拷贝!
但是如果返回对象是一个当前函数局部域的局部对象 ,函数结束就销毁了 ,那么使用引用返回是有问题的 ,这时的引用相当于也引用 , 类似一个野指针一样 。 传引用返回可以减少拷贝 , 但是一定要确保返回对象 , 在当前函数结束后还在 ,才能用 引用返回 。
语法上 , 引用没有开空间 ,是 取别名 。
2 ) 为什么建议加上 const
举个例子 : 如果我给了一个张三的蓝本 , 给你去造一个张三出来 , 但是因为某一个不小心的错误 , 把张三 变成了 李四了 , 给我造了个李四出来 , 还把我原先给的张三的蓝图 改成了 李四的蓝图 , 这就和本意不符合了。
加上const 的好处 :
1 ) 程序更健壮了 ,防止对象被错误修改
2 ) 避免因为权限扩大而报错
注意 : 拷贝构造的第一个参数必须是 类 类型对象的引用 , 可以再后面加参数 , 但此时的参数必须是缺省的!
Date(const Date& d,int x= 1){_year = d._year;_month = d._month;_day = d._day;}
4.2 自动生成拷贝构造
1 ) 啥是深拷贝 ? 啥是浅拷贝 ?
浅拷贝 : 只拷贝对象的数据 , 资源不进行拷贝
深拷贝 : 不仅拷贝对象的数据 , 而且拷贝资源
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
2 ) 如果自定义类型 , 使用自动生成的拷贝构造 --> 浅拷贝 , 会怎样 ?
会崩 ~
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}void Pop(){_a[_top - 1] = -1;--_top;}int Top(){return _a[_top - 1];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);Stack st2(st1);//st1.Pop();//st1.Pop();//cout<<st2.Top()<<endl;return 0;
}
会导致新对象的修改影响原对象 !
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}void Pop(){_a[_top - 1] = -1;--_top;}int Top(){return _a[_top - 1];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);Stack st2(st1);st1.Pop();st1.Pop();cout<<st2.Top()<<endl;return 0;
}
3 ) 为啥要用深拷贝 ?
1 )避免空间被释放多次
2 ) 避免新对象的修改 , 影响原对象
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// st2(st1)Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}void Pop(){_a[_top - 1] = -1;--_top;}int Top(){return _a[_top - 1];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);Stack st2(st1);return 0;
}
3 ) 需要写拷贝构造的小 tip
如果一个类显示实现了析构 并 释放资源 , 那么它就需要显示写拷贝构造 , 否则就不需要。
下面列举三个类 :
1 ) 日期类 (Date) : 成员变量全是内置类型的 , 且没有指向什么资源 , 编译器自动生成的拷贝构造就可以完成需要的拷贝 。不需要再额外写拷贝构造 。
2 )Stack 类 : 编译器自动生成的拷贝构造 --- 值拷贝/浅拷贝 , 不符合需求 , 所以需要自己写深拷贝 ( 对指向的资源也拷贝) 。
3 ) MyQueue 类 : 内部主要是自定义类型Stack 成员 , 编译器自动生成的拷贝构造会调用 Stack 的拷贝构造 , 也不许要我们显示实现MyQueue 的拷贝构造 。
MyQueue 类 :
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// st2(st1)Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}void Pop(){_a[_top - 1] = -1;--_top;}int Top(){return _a[_top - 1];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
private:Stack _pushst;Stack _popst;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);Stack st2(st1);MyQueue q1;MyQueue q2(q1);return 0;
}