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、在有派生的类中,写派生类的拷贝构造函数和赋值函数时,不要忘了基类成员赋值。