1、构造函数
任意一个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)
如果不显式定义这些函数,则编译器为类产生四个缺省函数:缺省的无参数构造函数、缺省的拷贝构造函数、缺省的析构函数、缺省的赋值函数
举例:
A(void); // 缺省的无参数构造函数
A(const A& a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A& operate =(const A& a); // 缺省的赋值函数
手动编写这四个函数的原因:这四个默认的函数均采用“位拷贝”的方式来实现,倘若类中含有指针变量,相当于只把地址赋给另一个变量,会出现内存泄露和野指针的情况。
拷贝时指针可能遇到的问题:
(1) 左值中的指针指向的地址没有被释放,导致内存泄露
(2) 赋值后,左值和右值指向同一块空间,任何一方对内存的改变都会影响另外一方,结果难以预料。
(3) 调用析构函数时,会对同一块内存释放两次。
2、构造函数的初始化表
格式:
类名::构造函数名(参数表):(数据成员名1(初始值1),数据成员名2(初始值2),…… )
{
函数体
}
举例:
class A
{
public:
A(int x,int y):m_x(x),m_y(y)
{ }
private:
int m_x;
int m_y;
};
书写位置:函数参数表之后,却在函数体 {} 之前
调用时间:该表里的初始化工作发生在函数体内的任何代码被执行之前
构造函数初始化表的使用规则:
(1) 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数.
(2) 类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化
(3) 非内部数据类型的数据成员的初始化可以采用初始化表实现,效率高
(4) 内部数据类型的数据成员的初始化可以采用函数体内赋值两种方式,效率一样,当更加直观
注意:只有构造函数才可以使用初始化表对成员初始化
举例:说明非内存成员使用初始化表实现的效率
使用初始化表初始化
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A::A()"<<endl;
}
A(const A& other)
{
cout<<"A::A(const A& other)"<<endl;
}
A& operator=(const A& other)
{
cout<<"A::operator="<<endl;
return *this;
}
};
class B
{
public:
B(const A& a):m_a(a)
{
cout<<"B::B()"<<endl;
}
private:
A m_a;
};
int main()
{
A a; //调用A的构造函数
B b(a); //调用类B的构造函数和A的拷贝构造函数(A m_a = a;)
/*
输出结果:
A::A() //调用A的构造函数
A::A(const A& other) //执行类B的构造函数的初始化列表-调用A的拷贝构造函数(A m_a = a;)
B::B() //执行类B的构造函数的大括号里面的
*/
system("pause");
return 1;
}
不使用初始化表初始化类成员
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A::A()"<<endl;
}
A(const A& other)
{
cout<<"A::A(const A& other)"<<endl;
}
A& operator=(const A& other)
{
cout<<"A::operator="<<endl;
return *this;
}
};
class B
{
public:
B(const A& a)
{
m_a = a;
cout<<"B::B()"<<endl;
}
private:
A m_a;
};
int main()
{
A a;
B b(a);
/*
输出结果:
A::A() //执行A a;
A::A() //执行A m_a;
A::operator= //执行B的构造函数中的m_a = a;
B::B() //执行B的构造函数中的 cout<<"B::B()"<<endl;;
*/
system("pause");
return 1;
}
例子说明:
在类中有对象成员时,
当使用初始化列表初始化该对象成员时,编译器会使用A m_a(a)的方式定义病初始化对象成员,此时会调用拷贝构造函数
当使用函数体内赋值初始化对象成员时,编译器会先定义A对象 A m_a,之后使用 m_a = a 的方式初始化对象成员m_a,此时会调用A的构造函数和赋值函数。
如果不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?
可以将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。当写的程序调用私有的拷贝构造函数和赋值函数时,编译器将指出错误,因为外界不可以操作A的私有函数。
举例:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A::A()"<<endl;
}
private:
A(const A& other)
{
cout<<"A::A(const A& other)"<<endl;
}
A& operator=(const A& other)
{
cout<<"A::operator="<<endl;
return *this;
}
};
int main()
{
A a; //调用A的构造函数
//A aa = a; //报错:A::A”: 无法访问 private 成员
A aaa;
//aaa = a; //报错:A::operator =”: 无法访问 private 成员
system("pause");
return 1;
}
注意:如果把A的构造函数也设为私有函数,则在执行语句A a时也会报错,即类将不能定义对象。
3、如何在派生类中实现类的基本函数
注意:基类的构造函数、析构函数、赋值函数都不能被派生类继承。
如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
(1) 派生类的构造函数应在其初始化表里调用基类的构造函数。
(2) 基类与派生类的析构函数应该为虚函数(实现多态)
(3) 编写派生类的赋值函数和拷贝构造函数时,注意不要忘记对基类的数据成员进行重新赋值。
注意:调用基类的拷贝构造函数和赋值函数时,传参都是派生类对象。
写派生类的构造函数时,为了给基类传参,需要在初始化表中传如派生类对象。
写派生类的赋值函数时,显式调用基类的赋值函数,参数也是派生类对象。
举例:说明注意事项 1 和 注意事项 3
#include <iostream>
using namespace std;
class A
{
public:
A(int x)
{
m_x = x;
}
A(const A& other)
{
m_x = other.m_x;
}
A& operator=(const A& other)
{
//检测自赋值
if (this == &other)
{
return *this;
}
//对自身成员进行赋值
m_x = other.m_x;
//返回本对象的引用
return *this;
}
void display()
{
cout<<"m_x = "<<m_x<<endl;
}
private:
int m_x;
};
class B : public A
{
public:
B(int x,int y): A(x)
{
m_y = y;
}
B(const B& other) : A(other) //***别忘了***,而且传参是派生类的对象
{
m_y = other.m_y;
}
B& operator=(const B& other)
{
//检测自赋值
if (this == &other)
{
return *this;
}
//对派生类成员进行赋值 ***别忘了***
A::operator=(other);
//static_cast<A&>(*this) = other;//也可以,详细见effective c++
//对自身成员进行赋值
m_y = other.m_y;
//返回本对象的引用
return *this;
}
void display()
{
A::display();
cout<<"m_y = "<<m_y<<endl;
}
private:
int m_y;
};
int main()
{
B b(1,2);
b.display();
/*输出:m_x = 1,m_y = 2*/
B bb(3,4);
bb.display();
/*输出:m_x = 3,m_y = 4*/
bb = b;
bb.display();
/*输出:m_x = 1,m_y = 2*/
system("pause");
return 1;
}
注意事项:如果在赋值语句 和 构造函数中不对基类进行赋值,基类中的变量将变成随机数。
4、总结:
1、一个空类,编译器会默认为其生成四个缺省函数:缺省的无参数构造函数、缺省的拷贝构造函数、缺省的析构函数、缺省的赋值函数。
2、只有构造函数才能使用初始化表为成员赋值
3、类中的对象成员使用初始化表为其赋值,效率高;而内部成员在函数体内为其赋值,更加直观。
4、基类的构造函数、析构函数、赋值函数都不能被派生类继承。
5、在有派生的类中,写派生类的拷贝构造函数和赋值函数时,不要忘了基类成员赋值。