构造函数
构造和析构函数都为public的
构造函数:主要作用于创建函数时对对象成员的属性赋值。
虚函数定义:
成员函数前加入virtual,可以实现多态。
实现多态通过虚函数表
1、在C++的类中,一旦成员函数中有虚函数,这个类中就会多一个虚函数表指针,这个指针指向一个虚函数表,表里面记录了这个类中所有的虚函数,当这个类被继承,它的子类中也会有一个虚函数表(不管子类中有没有虚函数),如果子类的成员函数中的有函数签名与父类的虚函数一样就会用子类中的函数替换它在虚函数表中的位置,这样就达到了覆盖的效果。
2、在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
构造函数不可以为虚函数,原因:
1、从存储空间方面:虚函数对应一个指向vtable虚函数表的指针,这个指向vtable的指针其实是存储在对象的内存空间的,对象没有实例化就没有vtable。
2、从使用方面:虚函数主要为了实现多态,构造函数是为了实例化某个确定的对象,没有必要使用虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数,构造函数不需要这个特性。
3、当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。vptr是对于当前类的vtable,构造的时候是不知道是基类还是派生类的。当更晚的派生类的构造函数被调用的时候,vptr又指向新的vtable,因此,VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)
构造函数可以有多个
基本构造函数:
#include<iostream>
using namespace std;
class Person
{
public:
//默认构造函数
Person();
//有参构造
Person(int age, int height);
//拷贝构造
Person(const Person& p);
int m_Age;
int* m_Height;
};
//默认构造
Person::Person() :Person(0, 0)
{
cout << "委托构造函数的调用!" << endl;
}
//有参构造,把age赋值给m_Age,身高用m_Height指向
Person::Person(int age, int height)
{
cout << "有参构造函数的调用!" << endl;
this->m_Age = age;
this->m_Height = new int(height);
}
//拷贝构造函数调用
Person::Person(const Person& p)
{
cout << "拷贝构造函数的调用!" << endl;
this->m_Age = p.m_Age;
this->m_Height = new int(*p.m_Height);
}
int main()
{
Person p;
cout << "p的年龄是: " << p.m_Age << endl;
cout << "p的身高是: " << *(p.m_Height) << endl;
return 0;
}
重载等号
Person& operator=(const Person& p) //重载赋值操作符
{
this->m_Age = p.m_Age;
this->m_Height = new int(*p.m_Height);
cout << "Person & operator=(const Person & p)" << endl;
return *this;
}
变量初始化
声明时初始化->初始化列表->构造函数初始化
类对象初始化
主要注意初始化列表初始化
1、const成员变量只能用成员初始化列表来完成初始化,而不能在构造函数内赋值
2、初始化的数据成员是对象
class Test
{
public:
Test (int, int){
cout <<"Test" << endl;
};
private:
int x;
int y;
};
class Women{
public:
Women() :xiaowang(1,2){
//xiaowang(18,170); //因为Test有显示带参的构造函数
cout << "Women" << endl;
};
Test xiaowang;
};
int main()
{
Women women;
return 0;
}
3、需要初始化引用成员数据
4、子类初始化父类的私有成员
class Women:public Person{
public:
Women() :Person(18,170){
//Person(18,170); // 构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用
};
};
int main()
{
Person *p = new Women();
cout << "p的年龄是: " << p->m_Age << endl;
cout << "p的身高是: " << p->m_Height << endl;
return 0;
}
拷贝构造函数
拷贝构造函数是传递一个对象,把对象的个属性拷贝到此对象中,是用对象构造对象的方法。
调用拷贝构造函数的情况:
1. 对象需要通过另外一个对象进行初始化
2. 对象以值传递的方式传入函数参数
3. 对象以值传递的方式从函数返回
- 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
Complex c2(c1);
Complex c2 = c1;
这两条语句是等价的。
注意,第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
Complex c1, c2; c1 = c2 ;
c1=c2;
这条语句不会引发复制构造函数的调用,因为 c1 早已生成,已经初始化过了。
- 如果函数 F 的参数是类 A 的对象,那么当 F 被调用时,类 A 的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
#include<iostream>
using namespace std;
class A{
public:
A(){};
A(A & a){
cout<<"Copy constructor called"<<endl;
}
};
void Func(A a){ }
int main(){
A a;
Func(a);
return 0;
}
程序的输出结果为:
Copy constructor called
这是因为 Func 函数的形参 a 在初始化时调用了复制构造函数。
前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 a 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。
以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。
如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。例如:
void Function(const Complex & c)
{
...dosomething
}
这样,Function 函数中出现任何有可能导致 c 的值被修改的语句,都会引发编译错误。
思考题:在上面的 Function 函数中,除了赋值语句,还有什么语句有可能改变 c 的值?例如,是否允许通过 c 调用 Complex 的成员函数?
- 如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:
#include<iostream>
using namespace std;
class A {
public:
int v;
A(int n) { v = n; };
A(const A & a) {
v = a.v;
cout << "Copy constructor called" << endl;
}
};
A Func() {
A a(4);
return a;
}
int main() {
cout << Func().v << endl;
return 0;
}
程序的输出结果是:
Copy constructor called
第19行调用了 Func 函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是第 16 行 return 语句所返回的 a。复制构造函数在第 9 行确实完成了复制的工作,所以第 19 行 Func 函数的返回值和第 14 行的 a 相等。
深拷贝和浅拷贝
浅拷贝:只是简单的赋值操作。
深拷贝:重新分配空间,存储要拷贝的值。
区别在于有没有重新分配空间。
默认的拷贝构造函数:只拷贝对象中的值,浅拷贝。
防止默认拷贝发生——声明一个私有拷贝构造函数
private:
Person(const Person & C);
如果类里面有其他的对象或者对象的指针(有动态内存)的时候需要写拷贝构造函数,防止浅拷贝造成重复释放内存的问题。
析构函数
析构函数:主要作用于在对象销毁前,清理分配的内存。
析构函数是虚函数
析构函数为虚函数,是为了防止内存泄露,为了防止只析构了父类的没有析构子类的。
Person::~Person()
{
cout << "析构函数的调用!" << endl;
if (this->m_Height != NULL)
{
delete this->m_Height;
}
}
构造析构顺序
构造先构造父类,再构造子类,析构先析构子类,再析构父类。
参考链接:https://blog.youkuaiyun.com/weixin_44788542/article/details/126187645