C++面向对象程序设计-北京大学-郭炜【课程笔记(三)】
- 1、构造函数(constructor)
- 1.1、基本概念
- 2、赋值构造函数
- 2.1、基本概念
- 2.1、复制构造函数起作用的三种情况
- 2.2、常引用参数的使用
- 3、类型转换构造函数
- 3.1、什么事类型转换构造函数
- 4、析构函数
- 4.1、什么是析构函数
- 4.2、析构函数和数组
- 4.3、析构函数和运算符 delete
- 5、构造函数析构函数调用时机
开始课程:P7 2_2. 构造函数
课程链接:程序设计与算法(三)C++面向对象程序设计 北京大学 郭炜
课程PPT:github提供的对应课程PPT
1、构造函数(constructor)
1.1、基本概念
1、成员函数的一种
- 名字与类名相同,可以有参数,不能有返回值(void 也不行)
- 作用是对对象进行初始化,如给成员变量赋初值
- 如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数
- 默认构造函数无参数,不做任何操作
- 如果定义了构造韩素,则编译器不生成默认的无参数的构造函数
对象生成时,构造函数自动调用。对象一旦生成,就再也不能在其上执行构造函数
- 一个类可以有多个构造函数
2、为什么需要构造函数
- 构造函数执行必要的初始化工作,有了构造函数,就不必再写初始化函数,也不用担心忘记调用初始化函数。
- 有时对象没被初始化就使用,会导致程序出错。
例1:
// 类中没有写构造函数
class Complex{
private:
double real, imag;
public:
void Set(double r, double i);
}; // 编译器自动生成默认构造函数
Complex c1; // 默认构造函数被调用
Complex * pc = new Complex; // 默认构造函数被调用
例2:
class Complex{
private:
double real, imag;
pubilc:
Complex(double r, double i = 0); // 构造函数
}
Complex::Complex(double r, double i){
real = r; imag = i;
}
Complex c1; //error,缺少构造函数的参数
Complex * pc = new Complex; // error,没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);
例3:可以有多个构造函数,参数个数或类型不同
class Complex{
private:
double real, imag;
pubilc:
// 函数重载
Complex(double r, double i = 0); // 构造函数
Complex(double r, double i);
Complex(double r);
Complex(Complex c1, Complex c2);
}
Complex::Complex(double r){
real = r; imag = 0;
}
Complex::Complex(double r, double i){
real = r; imag = i;
}
Complex::Complex(Complex c1, Complex c2){
real = c1.real + c2.real;
imag = c1.imag + c2.imag;
}
// 构造函数初始化
Complex c1(3), c2(1,0), c3(c1,c2);
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};
例4-1:构造函数在数组中的使用
#include<iostream>
class CSample
{
int x;
public:
CSample()
{
std::cout << "Constructor 1 Called" << std::endl;
}
CSample(int n)
{
x = n;
std::cout << "x = " << x << std::endl;
std::cout << "Constructor 2 Called" << std::endl;
std::cout << "====================" << std::endl;
}
};
int main()
{
CSample array1[2]; // 无参数构造函数会被调用两次
std::cout << "step1" << std::endl;
CSample array2[2] = {4, 5};
std::cout << "step2" << std::endl;
CSample array3[2] = {3}; // array3[0]:用的是有参构造函数初始化;array3[1]:用的是无参构造函数初始化;
std::cout << "step3" << std::endl;
CSample * array4 = new CSample[2];
delete []array4;
return 0;
}
// OUT
Constructor 1 Called
Constructor 1 Called
step1
x = 4
Constructor 2 Called
x = 5
Constructor 2 Called
step2
x = 3
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called
zhangbushi@zhangbushideair beida_lesson % g++ 04.cpp -o 04
zhangbushi@zhangbushideair beida_lesson % ./04
Constructor 1 Called
Constructor 1 Called
step1
x = 4
Constructor 2 Called
====================
x = 5
Constructor 2 Called
====================
step2
x = 3
Constructor 2 Called
====================
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called
例4-2:构造函数在数组中的使用
class Test
{
public:
Test(int n) {} //(1)
Test(int n, int m) {} //(2)
Test() {} //(3)
};
Test array1[3] = {1, Test(1,2)};
// 三个元素分别(1),(2),(3)初始化
Test array2[3] = {Test(2,3), Test(1,2), 1};
// 三个元素分别用(2),(2),(1)初始化
Test * pArray[3] = {new Test(4), new Test(1,2)}; // new的返回值是指针类型
//两个元素分别用(1),(2)初始化
2、赋值构造函数
2.1、基本概念
只有一个参数,即对同类对象的引用。
形如X::X( X& )
或X::X(const X &)
, 二者选一,后者能以常量对象作为参数
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
注意事项:无参构造函数不一定存在,但赋值构造函数一定存在;
例1:
class Complex
{
private:
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;
cout << “Copy Constructor called”;
}
};
Complex c1;
Complex c2(c1);//调用自己定义的复制构造函数,输出 Copy Constructor called
不允许有形如 X::X( X )的构造函数。(必须要加上引用)
class CSample {
CSample( CSample c ) {
} //错,不允许这样的构造函数
};
2.1、复制构造函数起作用的三种情况
- 1、当用一个对象去初始化同类的另一个对象时。
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句
- 2、如果某函数有一个参数是类 A 的对象,那么该函数被调用时,类A的复制构造函数将被调用。
class A
{
public:
A() { };
A( A & a) {
cout << "Copy constructor called" <<endl;
}
};
void Func(A a1){ }
int main(){
A a2; // 通过无参构造函数初始化
Func(a2); // 调用复制构造函数(复制构造函数,形参是实参的拷贝,不一定)
return 0;
}
// 程序输出结果为: Copy constructor called
- 3、如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用:
# include <iostream>
class A
{
public:
int v;
A(int n) { v = n; };
A( const A & a)
{
v = a.v;
std::cout << "Copy constructor called" << std::endl;
}
};
A Func()
{
A b(4); // 调用A(int n) { v = n; }; v = 4
return b;
}
int main()
{
std::cout << Func().v << std::endl;
return 0;
}
// 输出结果:
Copy constructor called
4
- 4、注意:对象之间复制并不导致复制构造函数被调用
#include<iostream>
class CMyclass
{
public:
int n;
CMyclass() {};
CMyclass( CMyclass & c) { n = 2 * c.n ;}
};
int main()
{
CMyclass c1, c2;
c1.n = 5;
c2 = c1; // 对象间赋值
CMyclass c3(c1); // 调用复制构造函数
std::cout << "c2.n = " << c2.n << ",";
std::cout << "c3.n = " << c3.n << std::endl;
return 0;
}
// 输出
c2.n = 5,c3.n = 10
2.2、常引用参数的使用
void fun(CMyclass obj_). {cout << “fun” << endl; }
- 这样的函数,调用时生成形参会引发复制构造函数调用,开销比较大。
- 所以考虑使用CMyclass & 引用类型作为参数
- 如果希望确保实参的值在函数中不应该被改变,那么可以加上const关键字
3、类型转换构造函数
3.1、什么事类型转换构造函数
- 定义转换构造函数的目的是实现类型的自动转换。
- 只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。
- 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
实例:
#include<iostream>
class Complex
{
public:
double real, imag;
Complex( int i ) // (1)
{
std::cout << "IntConstructor called" << std::endl;
real = i; imag = 0;
}
Complex(double r, double i) {real =r; imag = i;} //(2)
};
int main ()
{
Complex c1(7, 8);
Complex c2 = 12;
c1 = 9; // 解释如下
/*
c1 = 9; 解释如下
1、首先9会被自动转化成一个临时Complex对象,即:Complex Linshi = 9;
2、c1 = linshi;
*/
std::cout << c1.real << "," << c1.imag << std::endl;
return 0;
}
4、析构函数
4.1、什么是析构函数
实例:
class String{
private :
char * p;
public:
String () {
p = new char[10]; //动态分配的内存空间,需要释放,在析构函数中释放。
}
~ String ();
};
String ::~ String() {
delete [] p;
}
4.2、析构函数和数组
对象数组生命结束时,对象数组的每个元素的析构函数都会被调用。
#include<iostream>
class Ctest
{
public:
~Ctest()
{
std::cout << "destructor called" << std::endl;
}
};
int main ()
{
Ctest array[2];
std::cout << "End Main" << std::endl;
return 0;
}
// OUT
End Main
destructor called
destructor called
4.3、析构函数和运算符 delete
delete 运算导致析构函数调用
若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)
Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest; //析构函数调用
------------------------------------------------------------------
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次
析构函数在对象作为函数返回值返回后被调用
/*
日期:2024.02.17
作者:源仔
*/
#include<iostream>
class CMyclass
{
public:
~CMyclass() {std::cout << "destructor" << std::endl;}
};
CMyclass obj; // 全局对象
CMyclass fun(CMyclass sobj)
{
return sobj;
/*
1、参数对象消亡也会导致析构函数被调用。
2、函数调用返回时,生成临时对象返回
*/
}
int main()
{
obj = fun(obj); // 函数调用的返回值(临时对象)被
return 0; // 用过后,该临时对象析构函数被调用
}
// OUT
destructor //指的是CMyclass fun(CMyclass sobj)中的CMyclass sobj形参使用结束,调用析构函数
destructor //指的是fun(obj)临时变量使用结束,调用析构函数
destructor //指的是CMyclass obj;全局对象消完,调用析构函数
5、构造函数析构函数调用时机
#include<iostream>
class Demo
{
int id;
public:
Demo(int i)
{
id = i;
std::cout << "id = " << id << " constructor " << std::endl;
}
~Demo()
{
std::cout << "id = " << " destructed " << std::endl;
}
};
Demo d1(1); // 1、全局对象,在main函数之前就初始化了,就会引发构造函数,输出:id = 1 constructor
void Func()
{
static Demo d2(2); // 静态的局部变量,整个程序结束,静态变量才会消完
Demo d3(3);
std::cout << "func" << std::endl;
}
int main()
{
Demo d4(4); // 2、输出:id = 4 constructor
d4 = 6; // 3、调用类型转换构造函数,构建为6的临时构造函数,输出:id = 6 constructor,临时构造函数调用完就会直接销毁,引发析构函数调用,输出:id = destructed
std::cout << "main" << std::endl; // 输出:main
{
Demo d5(5); // 4、局部对象,输出:id = 5 constructor
} // 5、局部变量销毁,引发析构函数调用。输出:id = destructed
Func(); // 6、如下
/*
6、输出:id = 2 constructor
7、输出:id = 3 constructor
8、输出:Func
9、静态的局部变量,整个程序结束,静态变量才会消完,所以不会先引发 static Demo d2(2)的析构函数
10、先引发Demo d3(3);的析构函数,输出:id = destructed
*/
std::cout << "main ends" << std::endl; // 11、输出:main ends
/*
12、引发d4 = 6;中d4的析构函数调用(注意:之前引发的析构函数是 6 创建临时构造函数引发的析构函数调用),输出:id = destructed
13、引发static Demo d2(2);的析构函数调用,输出:id = destructed
14、引发Demo d4(4);的析构函数调用,输出:id = destructed
*/
return 0;
}
/*
id = 1 constructor
id = 4 constructor
id = 6 constructor
id = destructed
main
id = 5 constructor
id = destructed
id = 2 constructor
id = 3 constructor
func
id = destructed
main ends
id = destructed
id = destructed
id = destructed
*/
实例5:
假设A是一个类的名字,下面的程序片段会类A的调用析构函数几次?
答案:
调用3次。
解释:
new创建的动态变量,必须要释放,才能引发析构函数的调用。
int main()
{
A * p = new A[2];
A * p2 = new A;
A a;
delete [] p;
}