C++ 构造函数&运算符重载

C++ 构造函数&运算符重载

构造函数:创建对象时调用的类成员函数 构造函数可以重载
默认构造函数:无参数构造函数 名称和类名相同
有参构造函数:名称和类名相同 参数列表的参数表示的不是类成员 而是要赋给类成员的值
拷贝(复制)构造函数:一种特殊的构造函数 函数名称和类名称相同 拷贝构造函数的参数列表里必须有一个参数是本类型的一个引用变量
析构函数:删除对象时调用的类成员函数 没有参数 在程序关闭前调用析构函数释放资源 析构函数无法重载
C++重载:C++允许在同一作用域存在多个功能相似的同名函数和运算符
运算符重载:重载的运算符是带有特殊名称的函数,函数名是由关键字operator和其后要重载的运算符符号构成的

拷贝构造函数

拷贝构造函数,就是以一个对象作为另一个类对象初值的构造函数。在下面三种情况下会调用拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Chess{};
//-------------第一种---------------//
//用一个对象对另一个对象进行显式的初始化
Chess chess_one;
Chess chess_two = chess_one;

//-------------第二种---------------//
//将一个对象作为函数参数 以值传递的方式传给函数
void getName(Chess a){}
getName(chess_one);

//-------------第三种---------------//
//把一个对象作为函数的返回值 以值传递的方式从函数返回
Chess setName(){
Chess chess_one;
//...
return chess_one;
}

程序实例1:

程序文件constructor存放在文件目录中

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
49
50
#include <iostream>
#include <string>
using namespace std;
class MyStr{
private:
string name;
public:
MyStr();//无参构造函数
MyStr(string n);//有参数构造函数
MyStr(const MyStr &s);//复制构造函数 因为数据成员中无指针 所以浅拷贝可行 后面会讨论浅拷贝深拷贝
~MyStr();//析构函数
MyStr &operator=(const MyStr &s){//把运算符=重载 因为我们下面要用到对象=对象来进行赋值 记住这个&
//加上const 对于const和非const的实参函数都能接受 如果不加const 函数就只能接受非const的实参
cout<<"重载赋值运算符= "<<endl;
this->name=s.name;
return *this;//返回值是被赋值者的引用 即*this 这样在函数返回时候避免一次拷贝提高效率
//这样可以实现连续赋值(a=b=c)
}
friend ostream &operator<<(ostream &os,const MyStr &s){
os<<s.name<<endl;//因为name是私有成员 所以函数要声明友元friend
}
};
MyStr::MyStr(){
name="syh";
cout<<"默认无参构造函数"<<endl;
}
MyStr::MyStr(string n){
name=n;
cout<<"有参数构造函数"<<endl;
}
MyStr::~MyStr() {
cout<<"调用析构函数"<<endl;
}
MyStr::MyStr(const MyStr &s) {
name=s.name;
cout<<"拷贝构造函数"<<endl;
}
MyStr::MyStr(const char *c) {//有了这个构造函数可以直接MyStr str="sdas"来进行初始化
cout<<"拷贝构造函数(指针字符串)"<<endl;
name=c;
}
int main() {
MyStr str1;//调用无参构造函数
MyStr str2("outlook");//调用有参数构造函数
MyStr str3=str2;//调用拷贝构造函数
MyStr str4;//调用无参构造函数
str4=str2;//调用重载运算符=
cout<<str1<<str2<<str3<<str4;//<<被重载设计成能够输出对象的内容
return 0;//调用四次析构函数
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
默认无参构造函数
有参数构造函数
拷贝构造函数
默认无参构造函数
重载赋值运算符=
syh
outlook
outlook
outlook
调用析构函数
调用析构函数
调用析构函数
调用析构函数

程序修改 &

程序实例中第12行代码

1
MyStr &operator=(const MyStr &s);

如果删掉& 运算符=的重载就会发生一些变化
如果不加& 就会在函数调用的时候对实参进行一次拷贝
程序运行结果就变为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
默认无参构造函数
有参数构造函数
拷贝构造函数
默认无参构造函数
重载赋值运算符=
拷贝构造函数 #新增
调用析构函数 #新增
syh
outlook
outlook
outlook
调用析构函数
调用析构函数
调用析构函数
调用析构函数

执行到str4=str2这个赋值传参的时候 在调用运算符=重载之后 会生成一个临时对象 这个临时对象我们暂称pro pro获得了str2的值然后调用了拷贝构造函数将这个值传给str4 然后pro被析构
MyStr str4;str4=str2;代码等价于

1
2
3
4
MyStr str4;
pro=str2;//临时对象运算符=重载赋值
#临时对象pro
MyStr str4=pro;//拷贝构造

浅拷贝与深拷贝

浅拷贝

浅拷贝是系统提供的默认拷贝构造函数
浅拷贝执行MyStr str3=str2;仅仅是将对象str2中各个数据成员的值拷贝给对象str3中的数据成员 而不做其他事情 这样str2与str3的name值是相同的 并且它俩指向内存中的同一区域
当我们修改str2的name值时 str3的name也会被修改
当执行str2和str3的析构函数时 同一内存区域会被释放两次 程序崩溃

深拷贝

程序实例是深拷贝 当然使用浅拷贝也可以 因为成员变量中没有指针
深拷贝是必须显示提供拷贝构造函数 这样执行MyStr str3=str2;为str3的name申请了新的地址空间 再将str2的name值拷贝到str3的name中 这样str2.name与str3.name各自独立

赋值运算符重载

C++规定 赋值运算符重载函数 只能是类的非静态的成员函数 不能是静态成员函数也不能是友元函数

不能是静态成员函数的原因:静态成员函数只能操作类的静态数据成员 不能操作非静态成员
不能是友元函数的原因:C++规定当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时 编译器会自动提供一个 但是友元函数并不属于这个类 当我们提供了一个友元的赋值运算符重载时 系统也会自动提供一个 这样程序执行时会产生二义性 C++强制规定 赋值运算符重载函数只能定义为类的成员函数

赋值运算符重载函数也不能被继承:若子类自己不提供赋值运算符重载函数只是用父类的重载函数 父类的重载函数只能调用父类自己的数据成员 那么子类的数据成员就无法被使用

赋值运算符函数要避免自己给自己赋值 如果出现自赋值 立即return *this;:自己给自己赋值毫无意义 还会调用其他函数开销很大

如果类的数据成员中含有指针p 假设用指针q赋值给p 需要先将p原来指向的空间delete掉(因为指针p的空间通常是之前new来的 如果为p重新分配空间的时候没有将原来的空间delete掉会造成内存泄漏) 如果是自赋值 那么q和p是同一个指针 指向同一个内存空间 在赋值前需要对p原来空间delete 那么q指向的内容也被删除 无法赋值

定义赋值运算符函数时 需要关注如下几点:
1.是否把返回值的类型声明为该类型的引用 并在函数结束前返回对象自身的引用(*this) 只有返回一个引用 才可以允许连续赋值(a=b=c) 否则如果函数的返回值是void 应用该赋值运算符将不能连续赋值 程序不能通过编译
2.是否把传入的参数类型声明为const引用 若传入的参数不是引用而是对象 那么从形参到实参会调用一次复制构造函数 把参数声明为引用可以避免这一次无谓的消耗提高代码效率 同时我们在赋值运算符函数内不会改变传入的对象的窗台 因此加上const关键字
3.是否释放对象自身已有的内存 若忘记在分配新内存之前释放已有空间 会出现内存泄漏
4.是否判断传入的参数和当前对象(*this)是不是同一个对象 如果是同一个 则不进行赋值操作 直接返回 若不判断就进行赋值 那么在释放对象自身的内存的时候会导致:当 *this和传入的参数是同一个对象时 一旦释放了自身的内存 传入参数的内存也同时被释放了 就再也找不到需要赋值的内容了

1
2
3
4
5
6
7
8
9
10
11
12
13
MyStr &operator=(const MyStr &s){//把运算符=重载 因为我们下面要用到对象=对象来进行赋值 记住这个&
//加上const 对于const和非const的实参函数都能接受 如果不加const 函数就只能接受非const的实参
cout<<"重载赋值运算符= "<<endl;
if(this!=&s){
MyStr strT(s);//隐式调用构造函数

char *pTemp=strT.name;
strT.name=name;
name=pTemp;
}
return *this;//返回值是被赋值者的引用 即*this 这样在函数返回时候避免一次拷贝提高效率
//这样可以实现连续赋值(a=b=c)
}

这个函数中 先创建一个临时对象strT 然后把strT.name和对象自身的name做交换 由于strT是一个局部变量 离开了if作用域 自动调用strT的析构函数 由于strT.name指向的内存就是对象之前name的内存 相当于自己调用析构函数释放对象的内存

End of reading! -- Thanks for your supporting