本期Blog素材来源: 北京大学郭炜老师——C++面向对象程序设计
链接戳这里
基本概念
- 构造函数——名字为类名,可以有参数,不能有返回值,void也不行。
- 作用:对对象进行初始化。
- 如果类定义时没有写构造函数,则编译器自动生成一个默认的无参构造函数。
- 对象生成的那一刻,构造函数就自动被调用;对象一旦生成,就再也不能在其上执行构造函数了。对象所占用的存储空间不是由构造函数分配的。当调用构造函数时,对象的内存空间已经被分配好了(房子已经盖好了),构造函数只是做一些初始化工作(装修)。
- 一个类可以有多个构造函数。
- 在继承方面,子类不能继承父类的构造函数,只能调用父类的构造函数。
为什么需要构造函数
- 构造函数可以执行必要的初始化工作。有了构造函数,我们就不用专门写初始化函数,也不用担心忘了调用初始化函数。
- 请注意,初始化非常重要!未初始化的对象使用起来很有可能会出错。打个比方,在做LeetCode的时候,我经常因为忘记初始化而WA…郁闷。
子类构造函数
- 父类为CStudent,子类为Cundergraduate。继承的写法为:class Cundergraduate :public CStudent{};
- 子类不能继承父类的构造函数,只能调用父类的构造函数。
- 如果子类也要调用构造函数,程序格式为:子类::子类(参数):父类(父类参数,但不包含数据类型)
class CStudent
{
public:
CStudent();
~CStudent() {
cout << "调用了析构函数" << endl;
}
CStudent(string a, int b);
protected:
string name;
int age;
};
CStudent::CStudent()
{
cout << "缺省构造函数" << endl;
}
CStudent::CStudent(string a, int b) {
name = a;
age = b;
cout << "调用这个构造函数" << endl;
cout << age << endl;
}
class Cundergraduate :public CStudent
{
public:
string course;
Cundergraduate() { course = "大物"; cout << "大物" << endl; }
Cundergraduate(string a, int b, string c);
};
Cundergraduate::Cundergraduate(string a, int b, string c):CStudent(a, b)//子类调用父类构造函数,同时定义自己的构造函数
{
cout << "子类构造函数" << endl;
}
int main()
{
//Cundergraduate aa("eee", 20);//不能这么写。子类不能继承父类的构造函数,只能调用。
Cundergraduate aa("栈", 500, "ss");
Sleep(1);
return 0;
}
运行程序,输出结果为:
还有一个直观的例子,代码如下:
class A
{
public:
A();
A(int a);
~A() { cout << "父类析构函数" << endl; }
};
A::A()
{
cout << "父类无参构造函数" << endl;
}
A::A(int a) {
cout << "父类有参构造函数" << endl;
}
class B :public A
{
public:
B(){ cout << "子类无参构造函数" << endl; }
//B() : A() { cout << "子类无参构造函数" << endl; }//也可以这么写
B(int a) :A(a) { cout << "子类有参构造函数" << endl; }
~B() { cout << "子类析构函数" << endl; }
};
int main() {
//A a;
//子类会先调用父类的构造函数,再调用子类自己的构造函数。
B b;
//而析构函数则相反。先调用子类析构函数,再调用父类析构函数。
//有参数的同理
//B c(4);
return 0;
}
运行程序,输出结果为:
拷贝构造函数
调用常见场景
一个类有且仅有一个拷贝构造函数。
拷贝构造函数会在三种情况下起作用:
- 用一个对象去初始化同类的另一个对象时;
- 如果某函数有一个参数是类A的对象,那么该函数被调用时,类A的拷贝构造函数将被调用。有点绕,试着理解一下,很好理解的!
- 如果函数的返回值为类A的对象时,则函数返回时,A的拷贝构造函数被调用。
//第2条的解释
class A
{
public:
A(){}
A(A & a)
{
cout << "已调用拷贝构造函数" << endl;
}
};
void func(A a1) {} //这个a1也是个对象。他是如何被初始化的呢?调用拷贝构造函数
int main()
{
A a2;
func(a2);
return 0;
}
//因此上述程序的运行结果就是:已调用拷贝构造函数
//第3条的解释
class A
{
public:
int v;
A(int n)
{
v = n;
}
A(const A & a)
{
v = a.v;
cout << "已调用拷贝构造函数" << endl;
}
};
A Func()
{
A b(4);
return b;
}
int main()
{
cout << Func().v << endl;
return 0;
}
拷贝构造函数缺省
class Complex
{
private:
double real, imag;
};
Complex c1; //自动调用缺省无参构造函数
Complex c1(c2); //调用缺省无参拷贝构造函数,将c2初始化地和c1一样
不调用的场景
请注意,对象间的赋值并不导致拷贝构造函数被调用!
class CMyclass
{
public:
int n;
CMyclass() {}
CMyclass( CMyclass & c)
{
n = 3 * c.n;
}
};
int main()
{
CMyclass c1, c2;
c1.n = 5;
c2 = c1; //注意,这是赋值语句,拷贝构造函数此时没调用
CMyclass c3(c1);
cout << "c2.n = " << c2.n << ",";
cout << "c3.n = " << c3.n << endl;
return 0;
}
上述程序执行结果为:c2.n = 5. c3.n = 10
拷贝构造函数已定义
class Complex
{
private:
double real, imag;
Complex() {}
Complex(const Complex &c)
{
real = c.real;//把real初始化成和c一样
imag = c.imag;
cout << "已调用拷贝构造函数" << endl;
}
};
Complex c1; //
Complex c2(c1); //调用自己定义的拷贝构造函数,输出"已调用拷贝构造函数"。
Complex c3 = c2; //这条语句和上一条完全等价。
类型转换构造函数
- 定义转换构造函数的目的当然是为了实现类型的自动转换。
- 只有一个参数,而且不是拷贝构造函数的构造函数,一般就可以看成转换构造函数;
- 如需要,编译系统会自动调用转换构造函数,建立一个无名的临时对象/临时变量。
析构函数
- 名字与类名相同,在前面加”~”即可。没有参数和返回值。
- 一个类最多只能有一个析构函数。
- 在对象消亡时自动调用析构函数。可以自己定义析构函数用来在对象消亡前做善后工作(如释放内存)。这个时候,系统就不自己生成析构函数了;
- 如果定义class的时候没有写析构函数,那么编译器自己生成缺省析构函数。这个缺省析构函数只是个空壳子,它什么也不做。
析构函数的应用场景
- 对于对象数组而言(即数组中每个元素均为对象时),对象数组生命周期结束时,每个元素的析构函数均会被调用。如果自己没有写析构函数,那么有几个元素,缺省析构函数就调用几次。示例如下:
class Ctest
{
public:
~Ctest()
{
cout << "调用析构函数!" << endl;
}
};
int main()
{
Ctest arr[2];
cout << "结束主函数" << endl;
return 0;
}
上述程序的输出结果如下: 结束主函数 调用析构函数! 调用析构函数!
- 析构函数在对象作为函数返回值返回后被调用
Talk is cheap. 先来看代码:
class CMyclass
{
public:
~CMyclass()
{
cout << "调用析构函数" <<endl;
}
};
CMyclass obj; //对象1
/*下面这个函数参数是个对象,返回值也是个对象*/
/*这个sobj是临时对象,会调用拷贝构造函数*/
CMyclass fun(CMyclass sobj) //参数对象消亡也会导致析构函数被调用
{
return sobj; //函数调用返回时生成临时对象返回
}
//函数在结束时,局部变量和参数都会消亡。因此返回时,sobj就消亡了
int main()
{
obj = fun(obj); //函数调用的返回值(临时对象)消亡后,
return 0; //该临时对象析构函数被调用
}
上述程序的输出结果如下:
调用析构函数 调用析构函数 调用析构函数
第一次调用析构函数——函数形参对象sobj消亡时调用; 第二次调用析构函数——函数调用的返回值(临时对象)消亡时调用; 第三次调用析构函数——整个程序结束后,全局对象obj消亡时调用。
构造函数和析构函数什么时候被调用
- 请参考下述函数:
class Demo
{
int id;
public:
Demo(int i)
{
id = i;
cout << "id = " << id << "的构造函数" << endl;
}
~Demo()
{
cout << "id = " << id << "的析构函数" << endl;
}
};
Demo d1(1); //这是个全局对象,在main之前就被初始化了。全局对象就会引发构造函数,因此这里是第1次调用构造函数。
void Func()
{
static Demo d2(2); //静态的局部对象。这里是第4次调用构造函数.这不是在主函数里,所以还不需要调用析构函数。
Demo d3(3); //普通局部对象。这里是第5次调用构造函数.
cout << "func" << endl;
}
//Func函数结束后,静态局部对象不会消亡,整个程序结束后才消亡。因此只调用d3的析构函数。这里是第3次调用析构函数
int main()
{
//一般来说程序从这里开始。但由于前面有个全局对象d1,所以先执行那个。
Demo d4(4); //这是局部对象。这里是第2次调用构造函数。
d4 = 6; //按理来说类型不匹配,但系统自动分配了类型转换构造函数。6会自动转化为一个临时对象,赋给d4。这里是第3次调用构造函数。赋值之后,这个临时对象6就消亡了,这时会调用析构函数。这里是第1次调用析构函数
cout << "main" << endl;
{
Demo d5(5); //还是一个局部对象,它的生成会引发构造函数被调用,遇到后花括号它的生命周期结束,调用析构函数。这里是第2次调用析构函数
}
Func(); //接下来去执行上面定义的这个函数体内的语句。
cout << "主函数结束!" << endl; //执行完Func()后,输出本语句
return 0;
}
//main函数结束后,先消亡的是对象d4。由于对象d4已被赋为6,因此id = 6.这里是第4次调用析构函数.
//先初始化的后消亡,所以静态对象d2先消亡,全局对象d1最后消亡。这是第5次和第6次调用析构函数.
输出结果如下:
id = 1的构造函数
id = 4的构造函数
id = 6的构造函数
id = 6的析构函数
main
id = 5的构造函数
id = 5的析构函数
id = 2的构造函数
id = 3的构造函数
func
id = 3的析构函数
主函数结束!
id = 6的析构函数
id = 2的析构函数
id = 1的析构函数\
- 请注意,new出来的对象如果没有delete,可以认为它是不消亡的..请看案例:
int main()
{
A *p = new A[2];
A *p2 = new A; //如果只会没有delete这个p2,那么他是不会调用析构函数的,因为是new出来的。
A a; //局部对象在main函数结束时自动调用1次析构函数
delete []p; //delete []p调用了2次析构函数
}
//因此上述程序共调用了3次析构函数。