目录
[一 . 类的默认成员函数](#一 . 类的默认成员函数)
[二 . 构造函数](#二 . 构造函数)
[三 . 析构函数](#三 . 析构函数)
[四 . 拷贝构造函数](#四 . 拷贝构造函数)
[4.1 写法以及相关问题](#4.1 写法以及相关问题)
[4.2 自动生成拷贝构造](#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;
}