02 类和对象:构造函数和析构函数

类 & 对象

  • 面向对象的程序设计:

    • 面向对象的程序=+++面向对象的程序 = 类 + 类 + …+ 类 设计程序的过程,就是设计类的过程。
  • 对象的内存分配:对象所占用的内存空间的大小,等于所有成员变量的大小之和。

    • 求法:sizeof(类名)
  • 对象之间的运算:

    • 对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。
1.类的定义

类中的数据称为成员变量,函数称为成员函数。

类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。

定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

以下实例我们使用关键字 class 定义 Box 数据类型,包含了三个成员变量 length、breadth 和 height:

1
2
3
4
5
6
7
class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};

关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。您也可以指定类的成员为 private 或 protected,这个我们稍后会进行讲解。

2.定义对象

类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。下面的语句声明了类 Box 的两个对象:

1
2
Box Box1;          // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box

对象 Box1 和 Box2 都有它们各自的数据成员。

下面程序定义了一个roommate类,记录了几个人的睡觉时间和起床时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class roommate
{
public:
int name,getup,sleep;
int average_sleeptime(void);
void Init(int _name,int _getup,int _sleep)
{
name=_name;getup=_getup;sleep=_sleep;
}
};
int roommate ::average_sleeptime(void)
{
if(sleep<24 && sleep>12)return getup+24-sleep;
else return getup-sleep;
}
3.访问数据成员

用法1:对象名.成员名

1
2
3
CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4);

用法2:指针->成员名

1
2
3
4
5
CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上

用法3:引用名.成员名

1
2
3
4
CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了,r2的值也变

类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问。

4.类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:

  • private: 私有成员,只能在成员函数内访问

  • public : 公有成员,可以在任何地方访问

  • protected: 保护成员

注:三种关键字出现的次数和先后次序都没有限制

如果某个成员前面没有上述关键字,则缺省地被认为是私有成员。

1
2
3
4
5
6
7
8
9
10
class Man 
{
    int nAge; //私有成员
    char szName[20]; // 私有成员
    public:
    void SetName(char * szName)
    {
     strcpy( Man::szName,szName);
    }
};

在类的成员函数内部,能够访问:

  • 当前对象的全部属性、函数;

  • 同类其它对象的全部属性、函数。

注:用struct定义类和用"class"的唯一区别,就是未说明是公有还是私有的成员,就是公有

5.成员函数的重载和参数缺省

成员函数也可以重载 成员函数可以带缺省参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Location {
private :
int x, y;
public:
void init( int x=0 , int y = 0 );
void valueX( int val ) { x = val ;}
int valueX() { return x; }
};
void Location::init( int X, int Y)
{
x = X ;
y = Y;
}
int main() {
Location A,B;
A.init(5);
A.valueX(5);
cout << A.valueX();
return 0;
}
6.构造函数

定义:成员函数的一种:

  • 名字与类名相同,可以有参数,不能有返回值(void也不行)

  • 作用是对对象进行初始化,如给成员变量赋初值。

  • 如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数。默认构造函数无参数,不做任何操作

  • 如果定义了构造函数,则编译器不生成默认的无参数的构造函数。对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数

  • 一个类可以有多个构造函数

系统自动生成的无参构造函数:

1
2
3
4
5
6
7
8
9
10
11
class Complex {
private :
double real, imag;
public:
void Set( double r, double i);
//相当于系统自动在这里写了一个:(无参构造函数)
// Complex();
}; //编译器自动生成默认构造函数

Complex c1; //默认构造函数被调用
Complex * pc = new Complex; //默认构造函数被调用

手写的带参构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);

带参构造函数的重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
using namespace std;

class Complex
{
public:
double real, imag;
void Set(double r, double i);
Complex(double r, double i);
Complex(double r);
Complex(Complex c1, Complex c2);
};
Complex::Complex(double r, double i)
{
real = r;
imag = i;
}
Complex::Complex(double r)
{
real = r;
imag = 0;
}
Complex::Complex(Complex c1, Complex c2)
{
real = c1.real + c2.real;
imag = c1.imag + c2.imag;
}
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};
int main()
{
Complex c1(3), c2(1, 0), c3(c1, c2);
cout<<c1.real<<endl;
return 0;
}

注:构造函数最好是public的,private构造函数不能直接用来初始化对象

1
2
3
4
5
6
7
8
class CSample{
    private:
        CSample() {}
};
int main(){
CSample Obj; //err. 唯一构造函数是private
return 0;
}
7.构造函数在数组中的使用

构造一个对象,就调用一次构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
using namespace std;
class CSample
{
int x;
public:
CSample()//(1)无参构造函数
{
cout << "Constructor 1 Called" << endl;
}
CSample(int n)//(2)带参构造函数
{
x = n;
cout << "Constructor 2 Called" << endl;
}
};
int main()
{
CSample array1[2];
//构造两个无参对象,都使用(1)
cout << "step1" << endl;

CSample array2[2] = {4, 5};
//构造两个有参对象,都使用(2)
cout << "step2" << endl;

CSample array3[2] = {3};
//构造1个有参对象和1个无参对象,先使用(1),再使用(2)
cout << "step3" << endl;

CSample *array4 =new CSample[2];
//构造两个无参对象,都使用(1)

delete[] array4;
return 0;
}

以上程序输出:

1
2
3
4
5
6
7
8
9
10
11
Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called

单对象多参数的构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test
{
public:
Test(int n) {} //(1)
Test(int n, int m) {} //(2)
Test() {} //(3)
};
// 三个元素分别用(1),(2),(3)初始化
Test array1[3] = { 1, Test(1,2) };
//等价于:Test array1[3] = {Test(1), Test(1, 2),Test()};

//没有申请内存的不能算数
Test * pArray[3] = { new Test(4), new Test(1,2) };
//两个元素(注意:是两个元素,不是三个)分别用(1),(2) 初始化
拓展:使用初始化列表来初始化字段

使用初始化列表来初始化字段:

1
2
3
4
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}

上面的语法等同于如下语法:

1
2
3
4
5
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}

假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:

1
2
3
4
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}
8.复制构造函数(copy constructor)
  • 只有一个参数,即对同类对象的引用。

  • 形如 X::X( X& )或X::X(const X &), 二者选一 后者能以常量对象作为参数

  • 如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能

    注意:

    不允许有形如 X::X( X ) or X ( X ) 的构造函数

1
2
3
class CSample {
    CSample( CSample c ) {} //错,不允许这样的构造函数
};
9.复制构造函数起作用的三种情况

1)当用一个对象去初始化同类的另一个对象时。

1
2
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句

2)如果某函数有一个参数是类 A 的对象,那么该函数被调用时,类A的复制构造函数将被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A 
{
public:
A() { };
A( A & a) {
cout << "Copy constructor called" <<endl;
}
};
void Func(A a1){ }
int main(){
A a2;
Func(a2);
return 0;
}
1
2
程序输出结果为: 
Copy constructor called

3)如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;
class A
{
public:
int v;
A(int n) { v = n; };
A(const A &a)
{
v = a.v;
cout << "Copy constructor called" << endl;
}
};
A Func()
{
A b(4);
return b;
}
int main()
{
cout << Func().v << endl;
return 0;
}

这段程序理论上应该输出:

1
2
Copy constructor called
4

但实际上只输出了:

1
4

这是因为:

C++ 函数返回值为对象时调用复制构造函数的问题
知识点:
C++中调用复制构造函数的三种情况:

1.通过一个对象构造另一个对象
2.调用参数为对象的函数
3.调用返回值为对象的函数
– 上述知识点在各种书籍、博客都无不同,属于C++的标准
– 但是实际测试的时候,当调用返回值为对象的函数时,并未按预想地调用复制构造函数。
– 在查阅了很多博客资料后,原因如下:

当调用返回值为对象的函数时,系统消耗调用复制构造函数、调用构造函数,调用析构函数的代价,为了减少消耗,编译器使用了一项名为返回值优化的技术,使得调用函数时不需要调用复制构造函数
具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
string name;
int age;
Person(string name,int age){
this->name = age;
this->age = age;
}
//复制构造函数
Person(const Person& p){
cout<<"调用复制构造函数";
}
};

//返回一个Person对象
Person create(string name,int age){
return new Person(name,age);
}
int main(){
Person p = create("张三",20);
}

优化前:调用create函数时,先根据传入的参数生成一个临时对象 t1(栈中),然后拷贝生成临时对象 t2(栈外),函数执行完毕,返回 t2的地址,t1被回收;p根据返回的 t2 地址构造对象

优化后:调用create函数时,编译器偷偷地增加了一个参数,传入了p的地址,直接在函数内部构造了p对象

10.转换构造函数(Intconstructor)
  • 定义转换构造函数的目的是实现类型的自动转换。 只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。

  • 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

class Complex
{
public:
double real, imag;
explicit Complex(int i)//显式类型转换构造函数
{
cout << "IntConstructor called" << endl;
real = i;
imag = 0;
}
Complex(double r, double i)
{
real = r;
imag = i;
}
};
int main()
{
Complex c1(7, 8);
Complex c2 = Complex(12);
c1 = 9; // error, 9不能被自动转换成一个临时Complex对象
c1 = Complex(9); // ok
cout<< c1.real << "," << c1.imag << endl;
return 0;
}

隐式类型转换构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class Complex
{
public:
double real, imag;
Complex(int i)//区别1
{
cout << "IntConstructor called" << endl;
real = i;
imag = 0;
}
Complex(double r, double i)
{
real = r;
imag = i;
}
};
int main()
{
Complex c1(7, 8);
Complex c2 = 12;//区别2
c1 = 9; // 9被自动转换成一个临时Complex对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}
11.析构函数(destructors)
  • 名字与类名相同,在前面加 ~ , 没有参数和返回值,一个类最多只能有一个析构函数。

  • 析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。

  • 如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。

  • 如果定义了析构函数,则编译器不生成缺省析构函数。

析构函数实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class String
{
private:
char *p;

public:
String()
{
p = new char[10];
}
~String();
};
String ::~String()
{
delete[] p;
}

析构函数被调用的几种情况:

1.对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
class Ctest
{
public:
~Ctest() { cout << "destructor called" << endl; }
};
int main()
{
Ctest array[2];
cout << "End Main" << endl;
return 0;
}

2.delete 运算导致析构函数调用。

1
2
3
4
5
6
7
8
int main()
{
Ctest *pTest;
pTest = new Ctest;//构造函数调用
delete pTest;//析构函数调用
pTest = new Ctest[3]; //构造函数调用3次
delete[] pTest; //析构函数调用3次
}
  • 若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)

3.析构函数在对象作为函数返回值返回后被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
class CMyclass
{
public:
~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj)
{ //参数对象消亡也会导致析构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main()
{
obj = fun(obj); //函数调用的返回值(临时对象)被
return 0; //用过后,该临时对象析构函数被调用
}

输出:

1
2
3
destructor
destructor
destructor