文章目录
1. 构造函数
1.1 构造函数的基本定义与使用
类的构造函数是成员函数的一种,其主要特点有:
- 名字与类名相同,可以有参数,但不能有返回值( 也不行);
- 作用是对对象进行初始化,如给成员变量赋值;
- 如果定义类时没写构造函数,则编译器自动生成一个默认的无参构造函数且不做任何操作。
如果定义了构造函数,则编译器不会生成无参构造函数; - 对象生成时构造函数自动被调用(对象以任何形式生成),而对象一旦生成就再也不能在其上执行构造函数;
- 一个类可以有多个参数列表不同的构造函数,即构造函数的重载。
为什么需要构造函数? 考虑两点:构造函数执行必要的初始化工作,有了构造函数,就不必专门去书写初始化函数,也不用担心忘记调用初始化函数;对象没被初始化就使用会导致程序出错。下面以例子说明构造函数的使用:
class Complex {
private:
double real, imag;
}; // 没有自定义构造函数,编译器自动生成无参构造函数
Complex c1; // 因为对象生成时就会调用构造函数,这里调用默认的构造函数,下同
Complex* c2 = new Complex; // 默认构造函数将被调用
class Complex {
private:
double real, imag;
public:
Complex(double r, double i = 0);
}; // 有自定义构造函数,编译器不会生成构造函数
Complex::Complex(double r, double i) {
real = r;
imag = i;
} // 构造函数给成员变量赋值
Complex c1; // 出错,程序中不存在无参构造函数,下同
Complex* c2 = new Complex; // 类"Complex"不存在默认构造函数
Complex c3(2); // 成功
Complex c4(2, 4), c5(3, 5); // 成功
Complex* c6 = new Complex(3, 4); // 成功
1.2 构造函数的重载
构造函数也是函数,可以进行函数重载,以参数个数或类型区别不同构造函数。以下面例子说明:
class Complex {
private:
double real, imag;
public:
Complex(double r, double i);
Complex(double r);
Complex(Complex c1, Complex c2);
};
Complex::Complex(double r, double i) { # 1
real = r;
imag = i;
}
Complex::Complex(double r) { # 2
real = r;
imag = 0;
}
Complex::Complex(Complex c1, Complex c2) { # 3
real = c1.real + c2.real;
imag = c1.imag + c2.imag;
}
Complex c1(3); // 调用# 1 c1={3,0};
Complex c2(1, 2); // 调用# 2 c2={1,2};
Complex c3(c1, c2); // 调用# 3 c3={4,2};
1.3 构造函数在对象数组中的使用
在使用对象数组时,构造函数的调用形式同一般变量。以下面例子说明:
class C {
public:
int a, b;
public:
C() { // 无参构造函数
cout << "Constructor 1 Called!" << endl;
}
C(int m) { // 有参构造函数 # 1
a = m;
cout << "Constructor 2 Called!" << endl;
}
C(int m, int n) { // 有参构造函数 # 2
a = m;
b = n;
cout << "Constructor 3 Called" << endl;
}
};
C a[2]; // 调用两次无参构造函数
C b[2] = { 1, 2 }; // 调用两次有参构造函数# 1
C c[2] = { 3 }; // 先调用有参数构造函数# 1,再调用无参构造函数
C* d = new C[2]; // 调用两次无参构造函数
C e[3] = { 4, C(5,6) }; // 先调用有参数构造函数# 1,再调用有参数构造函数# 2,
// 最后调用无参构造函数
C f[3] = { C(7,8), C(9,10), 11 }; // 先调用有参数构造函数# 2,再调用有参数构造函数# 2,
// 最后调用有参数构造函数# 1
C* g[3] = { new C(12), new C(13,14) }; // 先调用有参数构造函数# 1,再调用有参构造函数# 2
注意最后一条语句,语句左端声明的是 *类型的指针数组,如果没有右端的初始化,不会生成对象。右端是对指针数组的初始化, 中前两个元素使用 的返回值即指针初始化,则分别调用有参构造函数 和 ;最后一个元素没有指定, 没有初始化,不会调用构造函数。
2. 复制构造函数
2.1 复制构造函数的基本定义与使用
首先来看复制构造函数的特点:
- 只有一个参数,即对同类对象的引用,其形式为
C::C(C& c)
或C::C(const C& c)
; - 如果没有定义复制构造函数,编译器会自动生成默认的构造函数,完成复制功能(这里与前面的构造函数相区别,无参构造函数即默认构造函数不一定存在(当自己声明了其他的有参构造函数时);而默认构造函数一定存在);
- 一个程序中有且只存在一个复制构造函数,要么是编译器自动生成的;要么是自己写的。
class Complex {
public:
double real, imag;
};
Complex c1; // 调用编译器生成的默认构造函数初始化
Complex c2(c1); // 调用编译器生成的默认复制构造函数初始化,将c2初始化为c1
class Complex {
public:
double real, imag;
Complex() { // 自己写的无参构造函数,即默认构造函数
}
Complex(const Complex& c) { // 自己写的复制构造函数
real = c.real;
imag = c.imag;
}
};
Complex c1; // 调用无参构造函数初始化
Complex c2(c1); // 调用自己定义的复制构造函数初始化,将c2初始化为c1
复制构造函数起作用的三种情况:
- 用一个对象去初始化另一个对象时,有以下两种形式:
Complex c2(c1);
和Complex c2=c1;
- 如果函数的某个参数是类的对象,在该函数被调用时,如函数形式为
void fun(Complex c){}
; - 如果函数的返回值是类的对象,在该函数返回时,如函数形式为
void fun(){return c}
。
注意:对象间的复制并不导致复制构造函数的调用。以下面例子说明:
class C {
public:
int n;
C() {
}
C(C& c) { // 自己定义的复制构造函数,
n = 2 * c.n; // 作用是将被初始化对象初始化为当前对象的2倍
}
};
C c1, c2;
c1.n = 1; // 给c1对象赋值
c2 = c1; // 不是对象间的初始化而只是赋值,不会调用复制构造函数
C c3(c1); // 用一个对象初始化另一个对象,调用复制构造函数
最后c2.n
的值是
,c3.n
的值是
。
2.2 深复制和浅复制
前面提到了赋值构造函数的作用是赋值对象,而默认构造函数也完成同样的功能,为什么需要自己写复制构造函数? 首先来看一个关于深拷贝和浅拷贝的例子程序:
class MyString {
public:
char* pName;
MyString(char* pN="noString") { // 构造函数
cout << "name=" << pN << "constructed" << endl;
pName = new char[strlen(pN) + 1];
strcpy(pName, pN);
}
~MyString() { // 析构函数
cout << "name=" << pName << "destructed" << endl;
delete[] pName;
}
};
MyString s1("Shanghai"); // 用"Shanghai"初始化s1对象,调用构造函数
MyString s2(s1); // 用s1初始化s2,调用默认复制构造函数
上述程序的输出:
这个和我们预想的结果不一样,上述程序在初始化 时没有调用构造函数且第二个析构函数也没有正常执行。这里产生该结果的原因是对象调用了 的默认复制构造函数,而默认复制构造函数仅复制了对象本体。如下图:
根据下一部分内容,后构造的先析构,则先析构
,将存在"Shanghai"
的内存空间释放了。 轮到
调用析构函数时,此时"Shanghai"
的内存空间已经不存在,此时执行delete
将发生不可预测的错误。为了实现对对象的整体复制,需要定义自己的复制构造函数,即深复制:
MyString(const MyString& s) { // 深复制
cout << s.pName << "copy constructed" << endl;
pName = new char[strlen(s.pName) + 1];
strcpy(pName, s.pName);
}
对深拷贝和浅拷贝的总结: 对象传递绝不能简单地传递对象本体,尤其是在对象具有指针指向对象时,必须定义自己的复制构造函数; 用 初始化 时不调用构造函数(与上面 和 对比)。
2.3 类型转换构造函数
这一部分来看另一种构造函数,类型转换构造函数。首先介绍类型转换构造函数的特点:
- 定义类型转换构造函数的目的是实现类型的转换;
- 只有一个参数,而且不是复制构造函数;
- 当需要的时候,编译器会自动调用类型转换构造函数。如下面的例子:
class Complex {
public:
double real, imag;
Complex(double r, double i) { // 自己定义的有参构造函数
real = r;
imag = i;
}
Complex(int x) { // 不是复制构造函数,是类型转换构造函数
real = x;
imag = 0;
}
};
Complex c1(1, 2);
Complex c2 = 3; // 调用类型转换构造函数,c2.real=3、c2.imag=0
c1 = 4; // 调用类型转换构造函数,将4转化成类的临时对象并赋值给c1,
// c1.real=4、c1.imag=0
3. 析构函数
3.1 析构函数的基本定义与使用
与构造函数在对象初始化时调用相对应,析构函数在对象消亡时调用。首先来看析构函数的特点:
- 名字与类名相同,同时在类名前加上
~
,没有返回值,一个类有且只能有一个析构函数; - 析构函数在对象消亡时自动被调用,因此我们可以在对象消亡时做一些善后工作,如释放内存;
- 如果没有定义析构函数,编译器自动生成默认析构函数,默认析构函数什么也不做。如下面的例子:
class String {
public:
char* p;
String() { // 在对象初始化时动态分配空间
p = new char[2];
}
~String() { // 在对象消亡时自动释放空间
delete[]p;
}
};
3.2 构造函数和析构函数调用程序示例
class Demo {
public:
int id;
Demo(int i) { // 构造函数用于给成员变量id赋值同时输出当前id
id = i;
cout << "id=" << id << "constructed" << endl;
}
~Demo() { // 析构函数,输出当前id
cout << "id=" << id << "destructed" << endl;
}
};
Demo d1(1); // 全局对象d1
void Func() {
static Demo d2(2); // 静态局部对象d2,函数调用完成后不会消亡,程序结束才消亡
Demo d3(3); // 局部对象d3,函数调用完成后d3消亡
cout << "func" << endl;
}
int main() {
Demo d4(4); // 局部对象d4
d4 = 6; // 将6转化为临时对象并赋值给4,该临时对象赋值完成后会消亡,
// 从而调用析构函数
cout << "main" << endl;
{
Demo d5(5); // 局部对象d5,大括号结束后d5消亡
}
Func();
cout << "main ends" << endl;
return 0;
}
程序输出结果:
最后,再总结析构函数需要注意的几点:
- 在程序中,
new
出来的对象,如果不手动调用delete
的话,该对象不会消亡即不会调用析构函数 - 先构造的后析构,后构造的先析构。如上述程序中的全局对象
d1
静态对象d2
。
4. 总结
最后,借用钱能老师对构造函数、复制构造函数、析构函数的总结:当我们展开类机制的学习时,我们可曾想过语言内部只一个类机制就要做这么多的事。况且,这才刚刚开始,才接触了类机制的一小部分,那都是一个个孤立的类,没有对类之间相互牵扯的事情。后面我们将会看到,一个个孤立的类只是基础,是构架类的层次结构的基础,只有形成了的类的层次,才有真正资源重用的意义。而且,因为类的层次结构,导致了更复杂的类机制和更方便的编程。我们将一针见血地直指面向对象编程的实质。
参考
- 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.
- 钱能. C++程序设计教程(第二版)[M]. 北京:清华大学出版社,2005.9.