C++类中四大隐者
小编想说的不是一灯、独孤求败、扫地僧、风清扬,而是C++类中的构造函数、析构函数、拷贝构造函数、赋值构造函数,初学C++功法的小编可是被这四大隐者打的“头破血流”
构造函数
1)构造函数的三大基本形式:类名(形参列表) : 初始化列表
class A
{
public:
int a;
A(void)
{
cout << "我是无参构造函数" << endl;
}
A(int _a)
{
a = _a;
cout << "我是有参构造函数" << endl;
}
A(int num):a(num)
{
cout << a << endl;
cout << "我是带初始化列表的构造函数" << endl;
}
};
构造函数具有多个版本的功劳在于C++的函数重载功法
2)构造函数的作用与特点
在知道了构造函数的基本形式后,我们需要了解我们为什么需要构造函数,以及构造函数有什么特点:
① 构造函数无需返回值
② 构造函数主要负责成员变量的初始化、分配相关资源、设置对象的初始状态
③ 构造函数在每每创建类对象时会被自动调用一次,在对象的生命周期中只会被调用一次
④ 当该类涉及继承与包含成员类时,构造函数有特定的调用顺序:先按照继承链依次调用父类中的构造函数 → 根据类成员变量的顺序依次调用类成员变量的构造函数 → 调用自身的构造函数
3)缺省构造函数
缺省构造函数(相当于无参构造)是编译器自动生成的一个什么都不做的构造函数,目的是为了避免系统错误
所谓编译器自动生成的函数并不是语法意义上的函数,而是功能意义上的函数,编译器往往直接生成具有特定功能的二进制指令,而不需要借助高级语言(C++代码)
需要注意的是当我们在类中实现一个有参构造后,无参构造函数变不再自动生成,如果需要使用到无参构造函数,必须显示的写在类中
class A
{
public:
int a;
A(int _a)
{
a = _a;
cout << "我是有参构造" << endl;
}
};
int main()
{
A a;
}
上述在
class A
中定义了一个有参构造A(int _a)
后,在main
中定义对象A a
时未提供参数,由于此时已经不会自动生成无参构造,会产生错误
4)无参构造函数
隐者喜欢自问自答
问:无参构造就是没有参数的构造函数吗?
答:并非如此
问:无参构造还有此等玄机?
答:默认形参罢了
class A
{
public:
int a;
A(int _a = 10)
{
a = _a;
cout << "我也算得上是无参构造" << endl;
}
};
int main()
{
A a;
}
当我们的
class A
中存在class B
的类对象成员时,根据构造函数的调用顺序我们必须得保证class B
中具备无参构造函数,因为在class A
中定义B b
时是无法初始化的
5)有参构造函数
单参构造:形如A(int a) {}
,A a = 10
这种语法也可能会成功,这是因为单参构造函数私底下实现了类型转换的功能(导致不调用赋值操作符,而是优先调用单参构造),这样的写法会给程序带来一定的危险,往往会在单参构造函数前加上explicit
禁止该情况
6)带初始化列表的构造函数
① 为类成员初始化
② 执行初始化列表时,类成员变量在之前仍未成功定义
③ 成员的初始化顺序与初始化列表中的顺序无关,而是与定义顺序相关
class A
{
public:
A(int _a)
{
cout << "A的有参构造函数" << endl;
}
};
class B
{
public:
B(int _b)
{
cout << "B的有参构造函数" << endl;
}
};
class C
{
public:
C(int _c)
{
cout << "C的有参构造函数" << endl;
}
};
class All
{
A a;
B b;
C c;
All(int _a, int _b, int _c):c(_c),a(_a),b(_b)
{
}
};
int main()
{
All all;
}
此时构造函数的调用顺序为A → B → C
析构函数
1)析构函数的基本形式:~类名(void)
2)析构函数的作用与特点
析构函数处于构造函数的对立面,二者之间存在着不同,但是它们犹如阴阳两极,缺一不可
① 析构函数无返回值、无参数 → 无参数的规定导致其不能重载
② 析构函数会在销毁对象时自动调用,在对象的生命周期中最多被调用一次
class A
{
public:
int a;
A(void)
{
cout << "有参构造函数" << endl;
}
~A(void)
{
cout << "析构函数" << endl;
}
};
int main()
{
//会调用析构函数
A a1;
//不会调用析构函数
A* a2 = new A;
}
析构函数不一定被调用的原因在于还未来得及执行析构函数,进程便结束了
③ 析构函数主要负责释放在构造函数中获得的资源。当该类涉及继承与包含成员类时,析构函数有特定的调用顺序:调用自身的析构函数 → 根据类成员变量的顺序逆序调用类成员变量的析构函数 → 按照继承链逆序调用父类中的析构函数
注意:析构函数的调用次序与构造函数的调用次序正好相反
3)缺省的析构函数
① 如果一个类中没有实现析构函数,编译器会自动生成一个具有析构函数功能的二进制指令,用来负责释放编译器所能看见的资源(成员变量、类成员、父类成员)
② 什么是编译器看不见的资源?天下武功唯快不破,看不见的资源便是动态资源(我们可以初步将其理解为堆内存),缺省的析构函数无法释放动态资源,当类中存在有关动态资源的分配时,我们必须定义一个专门的析构函数来进行资源管理与善后处理
赋值构造
1)赋值构造的基本形式
class Student
{
public:
char* name;
//构造函数
Student(const char* name)
{
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
}
//析构函数
~Student(void)
{
delete[] name;
}
//赋值构造
Student& operator=(const User& that)
{
if(this != &that)
{
//释放原来的空间
delete[] name;
//申请新空间
name = new char[strlen(that.name)+1];
//复制内容
strcpy(name,that.name);
}
}
}
知道运算符重载的同学们不难发现,赋值构造其实便是对
=
操作符的重载,返回值设置成Student&
的目的是为了能够连续赋值
2)赋值构造的作用与特点
① 当某一个类的旧对象给该类的另一个旧对象赋值时,调用赋值构造
② if(this != &that)
是为了能够自己给自己赋值,若缺少该判断语句,则会导致逻辑错误,将自己释放后如何再给自己赋值呢?
③ 问题又来了,为什么要先释放内存呢?因为当我们调用赋值构造时,that
对象中的成员可能在之前通过new
申请的字节数少小于我们this
对象所需申请的字节数,所以需要重新设置申请内存的大小
3)缺省的赋值构造
① 缺省时编译器会生成一个赋值构造函数,它负责把一个对象的内存拷贝给另一个对象
② 当需要深拷贝时,缺省的赋值构造无法满足需求,此时必须手动实现赋值构造
浅拷贝:复制了对象的引用地址,使得两个对象指向同一块内存地址,修改任意一边都会
同时变化
深拷贝:复制了对象与其值,拥有自己的内存地址,修改任意一边不会影响彼此
拷贝构造
1)拷贝构造的基本形式
class Student
{
public:
char* name;
//构造函数
Student(const char* name)
{
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
}
//析构函数
~Student(void)
{
delete[] name;
}
//拷贝构造
Student(Student& that)
{
name = new char[strlen(that.name)+1];
strcpy(name,that.name);
}
2)拷贝构造的作用和特点
① 拷贝构造也称为复制构造
② 当某一类的旧对象给该类的新对象赋初值的时候调用拷贝构造
③ 当将对象作为函数参数时,调用函数将会一起调用拷贝构造(给形参赋初值)
3)缺省的拷贝构造
① 缺省时,编译器会自动生成一个拷贝构造函数,它负责把旧对象中的所有数据给新创建的对象
② 当需要深拷贝时,即类中有指针成员,缺省的拷贝构造函数便无法完成任务,此时必须手动实现拷贝构造
因此不难发现,当涉及到深拷贝时,拷贝构造与赋值构造同时需要我们手动实现
总结
小编认为其实C++中最大的隐士还是C语言,小编整理的四个隐藏函数仍停留在基础上,涉及到各种不同的继承后它们还有一些独特之处。若有不正确的地方,希望大佬们及时指出。共勉!