C++ 入门基础之四

大纲

类和对象

基本概念

  • (a) 类、对象、成员变量、成员函数
  • (b) 面向对象四大概念:封装、继承、多态、抽象

类的封装

封装(Encapsulation):

  • (a) 封装,是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,对数据和函数进行访问控制,这在计算机世界中是用类与对象实现的。
  • (b) 封装,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

C++ 中类的封装:

  • 成员变量:C++ 中用于表示类属性的变量
  • 成员函数:C++ 中用于表示类行为的函数

类成员的访问控制

在 C++ 中可以给成员变量和成员函数定义访问级别:

  • private:修饰的成员变量和成员函数,只能在类的内部被访问
  • public:修饰的成员变量和成员函数,可以在类的内部和类的外部被访问
  • protected:修饰的成员变量和成员函数,可以在派生类(继承的子类)的内部访问,不能在派生类的外部被访问
  • 特别注意:若在类中声明了没有访问控制级别的成员变量或者成员函数,那么它们默认都是 private 访问级别的

基于类成员的访问控制,计算圆形面积的示例代码如下:

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 Circle {

private:
double m_r; // 圆形的半径
double m_s; // 圆形的面积

public:
void setR(double r) {
m_r = r;
}

double getR() {
return m_r;
}

double getS() {
m_s = 3.14 * m_r * m_r;
return m_s;
}

};

int main() {
double r;
cout << "请输入圆形的半径:";
cin >> r;

Circle circle;
circle.setR(r);
cout << "圆形的面积是:" << circle.getS() << endl;
return 0;
}

struct 和 class 的区别

在 C++ 中可以简单地认为 structclass 两者是一样的,唯一的区别如下:

  • 在使用 class 定义类时,所有成员的默认属性为 private
  • 在使用 struct 定义类时,所有成员的默认属性为 public

类的声明与类的实现一起写

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 Circle {

private:
double m_r; // 圆形的半径
double m_s; // 圆形的面积

public:
void setR(double r) {
m_r = r;
}

double getR() {
return m_r;
}

double getS() {
m_s = 3.14 * m_r * m_r;
return m_s;
}

};

int main() {
double r;
cout << "请输入圆形的半径:";
cin >> r;

Circle circle;
circle.setR(r);
cout << "圆形的面积是:" << circle.getS() << endl;
return 0;
}

程序运行输出的结果如下:

1
2
请输入圆形的半径:30
圆形的面积是:2826

类的声明与类的实现分开写

在企业开发中,由于项目结构比较庞大,一般都会将类的声明和类的实现分开写在不同的源文件中。

Teacher.h 头文件,声明了 Teacher 类的成员变量和成员函数。使用 #ifndef#define#endif 指令,是为了防止 Teacher.h 头文件被多次引用时 C++ 编译器编译失败,也可以直接使用 #pragma once 指令来替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef TEACHER_H
#define TEACHER_H

class Teacher {

private:
char *_name;
int _age;

public:
const char *getName() const;

void setName(char *name);

int getAge() const;

void setAge(int age);
};

#endif

Teacher.cpp 源文件,实现了在 Teacher.h 头文件中定义的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include "Teacher.h"

using namespace std;

const char *Teacher::getName() const {
return this->_name;
}

void Teacher::setName(char *name) {
this->_name = name;
}

int Teacher::getAge() const {
return this->_age;
}

void Teacher::setAge(int age) {
this->_age = age;
}

Main.cpp 源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "Teacher.h"

using namespace std;

int main() {
char name[32] = "Peter";
Teacher teacher;
teacher.setAge(10);
teacher.setName(name);
cout << "age: " << teacher.getAge() << endl;
cout << "name: " << teacher.getName() << endl;
}

程序运行的输出结果如下:

1
2
age: 10
name: Peter

对象的构造和析构

构造函数介绍

创建一个对象时,常常需要做某些初始化的工作,例如对数据成员赋初值。必须注意,类的数据成员是不能在声明类时初始化的。为了解决这个问题,C++ 编译器提供了构造函数(Constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动被调用。

构造函数的定义

构造函数的定义:

  • C++ 中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数
  • 构造函数在定义时可以有参数
  • 构造函数没有任何返回类型的声明
  • 构造函数可用于为某些成员变量设置初始值

构造函数的调用:

  • 自动调用:一般情况下 C++ 编译器会自动调用构造函数
  • 手动调用:在一些特定的情况下,需要手工调用构造函数

构造函数的分类

  • 按类型分类

    • 普通构造函数(包括无参、有参)
    • 拷贝构造函数(又叫赋值构造函数)
  • 按参数分类

    • 有参构造函数
    • 无参构造函数(又叫默认构造函数)

构造函数的声明

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
#include <iostream>

using namespace std;

class Test {

public:

// 无参构造函数(默认构造函数)
Test() {
_a = 1;
_b = 2;
}

// 有参构造函数
Test(int a, int b) {
_a = a;
_b = b;
}

// 拷贝构造函数(赋值构造函数)
Test(const Test &obj) {
_a = obj._a;
_b = obj._b;
}

private:
int _a;
int _b;

};

默认的构造函数

C++ 中有两个特殊的构造函数:

  • 默认构造函数:当类中没有定义构造函数时,编译器默认会提供一个无参构造函数(即默认构造函数),并且其函数体为空
  • 默认拷贝构造函数:当类中没有定义拷贝构造函数时,编译器默认会提供一个拷贝构造函数,用于简单地进行类成员变量的值复制

析构函数介绍

析构函数的定义

析构函数的定义:

  • C++ 中的类可以定义一个特殊的成员函数来清理对象,这个特殊的成员函数叫做析构函数
  • 析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号 ~ 作为前缀,它没有任何参数,也没有任何返回类型的声明
  • 析构函数有助于在关闭程序前释放资源(比如关闭文件、释放内存等)
  • 析构函数在对象销毁时会自动被调用

析构函数的调用:

  • C++ 编译器会自动调用析构函数

析构函数的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

class Teacher {

public:

// 析构函数
~Teacher() {
cout << "调用析构函数" << endl;
}

};

int main() {
Teacher teacher;
return 0;
}

程序运行输出的结果如下:

1
调用析构函数

默认的析构函数

C++ 中有一个特殊的析构函数:

  • 默认析构函数:当类中没有定义析构函数时,编译器默认会提供一个析构函数,这个默认析构函数会删除类中的所有成员变量

构造函数使用

构造函数的调用方式

普通构造函数的调用

普通构造函数(包括无参、有参)常用的调用方式分为以下三种。特别注意,无参构造函数的调用不要加括号,否则 C++ 编译器会认为是函数的声明,导致构造函数不会被调用。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>

using namespace std;

class Test {

private:
int _a;
int _b;

public:

Test() {
_a = 1;
_b = 1;
}

Test(int a) {
_a = a;
_b = 8;
}

Test(int a, int b) {
_a = a;
_b = b;
}

int getA() const {
return _a;
}

int getB() const {
return _b;
}
};

int main() {
// C++编译器调用无参构造函数
Test t0;
printf("a = %d, b = %d\n", t0.getA(), t0.getB());

// 以下是错误写法,无参构造函数的调用不要加括号,否则 C++ 编译器会认为是函数的声明,导致构造函数不会被调用
// Test t0();

// 第一种:C++编译器调用有参构造函数(等号法)
Test t1 = (1, 2, 3, 4, 5);
printf("a = %d, b = %d\n", t1.getA(), t1.getB());

// 第二种:C++编译器调用有参构造函数(括号法)
Test t2(10, 20);
printf("a = %d, b = %d\n", t2.getA(), t2.getB());

// 第三种:手动调用有参构造函数,生成一个匿名对象(显式法)
Test t3 = Test(100, 200);
printf("a = %d, b = %d\n", t3.getA(), t3.getB());

return 0;
}

程序运行输出的结果如下:

1
2
3
4
a = 1, b = 1
a = 5, b = 8
a = 10, b = 20
a = 100, b = 200
拷贝构造函数的调用

拷贝构造函数常用的调用方式分为以下三种。

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
37
38
39
#include <iostream>

using namespace std;

class Person {

public:

Person(string name, int age) {
this->name = name;
this->age = age;
}

Person(const Person &p) {
this->name = p.name;
this->age = p.age;
cout << "拷贝构造函数" << endl;
}

private:
string name;
int age;

};

int main() {
Person p2("Jim", 23);

// 第一种:自动调用拷贝构造函数(等号法)
Person p3 = p2;

// 第二种:自动调用拷贝构造函数(括号法)
Person p4(p2);

// 第三种:自动调用拷贝构造函数
Person p5 = Person(p2);

return 0;
}

程序运行输出的结果如下:

1
2
3
拷贝构造函数
拷贝构造函数
拷贝构造函数

构造函数的使用规则

  • 当类中没有定义任何一个构造函数时,C++ 编译器会提供默认无参构造函数和默认拷贝构造函数
  • 当类中定义了拷贝构造函数时,C++ 编译器不会提供默认无参构造函数
  • 当类中定义了任意的非拷贝构造函数(即当类中定义了有参构造函数或无参构造函数),C++ 编译器不会提供默认无参构造函数
  • C++ 提供的默认拷贝构造函数,只负责给类成员变量简单赋值
  • 必要的时候,需要手动编写拷贝构造函数
  • 构造函数和普通成员函数都遵循函数重载规则

构造函数的初始化列表

初始化列表出现的原因

有的时候必须用带有初始化列表的构造函数:(1)没有默认无参构造函数的成员类对象;(2)const 成员或引用类型的成员,必须要通过初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的就是对它们赋值,这样是不被允许的。值得一提的是,构造函数中有着比我们所看见的还要多的细节,构造函数可以调用其它的构造函数来初始化对象中的基类对象和成员对象的构造函数。类的数据成员中的其它类对象,若该成员对象是没有默认无参构造函数,则必须进行显式初始化;因为编译器会隐式调用成员对象的默认无参构造函数,而它又没有默认无参构造函数,则编译器会编译失败。

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
37
38
39
40
#include <iostream>

using namespace std;

class Teacher {

private :
int _age;

public:

Teacher(int age) {
_age = age;
}

int getAge() const {
return _age;
}

};

class Student {

private :
int _age;
Teacher teacher;

public:

int getAge() const {
return _age;
}

};

int main() {
Teacher t(20);
Student s; // C++编译器编译不通过
return 0;
}

上述示例代码无法通过编译,Student 的类数据成员中有一个 Teacher 类的对象 teacher,创建 Student 类时,要先创建其成员对象 teacher;由于 Teacher 类有一个自定义的有参构造函数,C++ 编译器不会再提供默认无参构造函数,因此 teacher 对象无法被自动创建。使用构造函数初始化列表改写后,正确的示例代码如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>

using namespace std;

class Teacher {

private :
int _age;

public:

Teacher(int age) {
_age = age;
}

int getAge() const {
return _age;
}

};

class Student {

private :
int _age;
Teacher teacher;

public:

// 使用构造函数的初始化列表来初始化Teacher类对象
// 这里会自动调用Teacher类的有参构造函数,并将age2作为构造函数的参数传递过去
Student(int age1, int age2) : teacher(age2) {
_age = age1;
}

int getAge() const {
return _age;
}

Teacher getTeacher() {
return teacher;
}

};

int main() {
Student s(20, 35);
cout << "student.age: " << s.getAge() << ", teacher.age: " << s.getTeacher().getAge() << endl;
return 0;
}

程序运行的输出结果如下:

1
student.age: 20, teacher.age: 35
初始化列表使用的语法规则

构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。

1
2
3
4
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)
{

}

在下述的示例代码中,两个构造函数的最终效果是一样的。使用初始化列表的构造函数是显式地初始化类的成员;而没有使用初始化列表的构造函数是对类的成员赋值,并没有显式地初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A
{
public:
int a;
float b;
A(): a(0),b(9.9) {} //构造函数初始化列表
};

class A
{
public:
int a;
float b;
A() //构造函数内部赋值
{
a = 0;
b = 9.9;
}
};
初始化 const 成员和引用成员

构造函数初始化列表是初始化 const 成员和引用成员的唯一方式。因为 const 成员或引用类型的成员只能被初始化,不能对它们赋值。示例代码如下:

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
#include <iostream>

using namespace std;

class A {

private:
int i;
int &j;
const int c;

public:

// 构造函数初始化列表
A(int x, int y) : c(x), j(y) {
i = -1;
}

};

int main() {
int m;
A a(5, m); // C++编译可以通过
return 0;
}

若不通过初始化列表来对 const 成员或引用类型的成员进行初始化,那么缺省情况下,在构造函数被执行之前,对象中的所有成员都已经被它们自己的默认无参构造函数初始化了。由于这两种数据成员要在声明后马上初始化,而在构造函数中,做的就是对它们赋值,这样是不被允许的。示例代码如下:

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 {
private:
int i;
int &j;
const int c;

public:

A(int x) {
i = -1;
c = 5; // C++编译不通过,必须通过初始化列表来初始化
j = x; // C++编译不通过,必须通过初始化列表来初始化
}
};

int main() {
A a(3);
return 0;
}

当类中某个数据成员本身也是一个类对象时,应该尽量避免使用赋值操作来对该成员进行初始化,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Person
{
private:
string name;

public:
Person(string & n)
{
name = n;
}
}

虽然这样的构造函数也能得到正确的结果,但这样写效率并不高。当一个 Person 对象创建时,string 类成员对象 name 先会被默认无参构造函数进行初始化,然后在 Person 类的自定义有参构造函数中,它的值又会因赋值操作而再改变一次。这里可以通过初始化列表来显示地对 name 对象进行初始化,这样就可以将前面的两步骤(初始化和赋值)合并成一个步骤了。示例代码如下:

1
2
3
4
5
6
7
8
9
10
class Person
{
private:
string name;

public:
Person(string& n): name(n){

}
}
初始化与赋值的使用区别

初始化与赋值是两个完全不同的概念,两者的区别如下:

  • 初始化:被初始化的对象正在创建
  • 赋值:被赋值的对象已经存在
  • 初始化列表优先于构造函数的执行
  • 成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关

在宏观代码上,两者的作用类似,但对于数组和结构体来说,初始化和赋值的形式是不同的,如下:

  • 对于数组,可以使用花括号一起初始化,如果是赋值的话,就只能单个元素进行赋值。
  • 对于结构体,可以使用花括号初始化,否则只能通过 . 符号来访问变量进行赋值。
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
#include <iostream>
#include <string>

using namespace std;

struct MyStruct {
int aa;
float bb;
string cc;
};

int main() {
// 数组的初始化与赋值
int a[3] = {1, 2, 3};
int b[3];
b[0] = 1;
b[1] = 2;
b[2] = 3;

// 结构体的初始化与赋值
MyStruct stu1 = {1, 3.14f, "hello world"};
MyStruct stu2;
stu2.aa = 1;
stu2.bb = 3.14f;
stu2.cc = "we are csdn";

cout << stu1.aa << endl;
cout << stu1.bb << endl;
cout << stu1.cc << 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <iostream>
#include <string>

using namespace std;

class Phone {

public:

Phone() {
cout << "手机的无参构造函数被调用" << endl;
}

Phone(string &number) : m_number(number) {
cout << "手机的有参构造函数被调用" << endl;
}

~Phone() {
cout << "手机的析构函数被调用" << endl;
}

const string &getNumber() const {
return m_number;
}

void setNumber(const string &mNumber) {
m_number = mNumber;
}

private:
string m_number;

};

class Student {

public:

Student() {
cout << "学生的无参构造函数被调用" << endl;
}

Student(string &name) : m_name(name) {
cout << "学生的有参构造函数被调用" << endl;
}

Student(string &name, string &phone) : m_name(name), m_phone(phone) {
cout << "学生的有参构造函数被调用" << endl;
}

~Student() {
cout << "学生的析构函数被调用" << endl;
}

const string &getName() const {
return m_name;
}

void setName(const string &mName) {
m_name = mName;
}

const Phone &getPhone() const {
return m_phone;
}

private:
string m_name;
Phone m_phone;
};

void test01() {
Student s;
}

void test02() {
string name = "Jim";
string phone = "110";
Student s(name, phone);
}

int main() {
cout << "-------- test01() --------" << endl;
test01();

cout << "-------- test02() --------" << endl;
test02();

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
-------- test01() --------
手机的无参构造函数被调用
学生的无参构造函数被调用
学生的析构函数被调用
手机的析构函数被调用
-------- test02() --------
手机的有参构造函数被调用
学生的有参构造函数被调用
学生的析构函数被调用
手机的析构函数被调用

拷贝构造函数的调用场景

第一种调用场景
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
37
38
39
40
41
42
43
44
45
#include <iostream>

using namespace std;

class Test {

private :
int _a;

public:
Test() {
cout << "无参构造函数自动被调用了" << endl;
}

Test(int a) {
_a = a;
cout << "有参构造函数被调用了" << endl;
}

Test(const Test &obj) {
_a = obj._a + 10;
cout << "拷贝构造函数被调用了" << endl;
}

~Test() {
cout << "析构函数被调用了" << endl;
}

int getA() {
return _a;
}
};

void functionA() {
Test t1(1);
Test t0(2);
t0 = t1; // 普通的赋值操作,拷贝构造函数不会被调用
Test t2 = t1; // 类的初始化操作(等号法),拷贝构造函数会被调用
cout << "a = " << t2.getA() << endl;
}

int main() {
functionA();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
有参构造函数被调用了
有参构造函数被调用了
拷贝构造函数被调用了
a = 11
析构函数被调用了
析构函数被调用了
析构函数被调用了
第二种调用场景
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
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

class Test {

private :
int _a;

public:
Test() {
cout << "无参构造函数自动被调用了" << endl;
}

Test(int a) {
_a = a;
cout << "有参构造函数被调用了" << endl;
}

Test(const Test &obj) {
_a = obj._a + 10;
cout << "拷贝构造函数被调用了" << endl;
}

~Test() {
cout << "析构函数被调用了" << endl;
}

int getA() {
return _a;
}
};

void functionA() {
Test t1(3);
Test t2(t1); // 类的初始化操作(括号法),拷贝构造函数会被调用
cout << "a = " << t2.getA() << endl;
}

int main() {
functionA();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
有参构造函数被调用了
拷贝构造函数被调用了
a = 13
析构函数被调用了
析构函数被调用了
第三种调用场景
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
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

class Test {

private :
int _a;

public:
Test() {
cout << "无参构造函数自动被调用了" << endl;
}

Test(int a) {
_a = a;
cout << "有参构造函数被调用了" << endl;
}

Test(const Test &obj) {
_a = obj._a + 10;
cout << "拷贝构造函数被调用了" << endl;
}

~Test() {
cout << "析构函数被调用了" << endl;
}

int getA() {
return _a;
}
};

void functionA() {
Test t1(3);
Test t2 = Test(t1); // 类的初始化操作,拷贝构造函数会被调用
cout << "a = " << t2.getA() << endl;
}

int main() {
functionA();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
有参构造函数被调用了
拷贝构造函数被调用了
a = 13
析构函数被调用了
析构函数被调用了
第四种调用场景

以值传递的方式给函数参数传值时,会自动调用拷贝构造函数,因此一般推荐使用常引用参数来避免拷贝构造函数的调用,以此提高代码的执行效率,如 void functionA(const Person & p) {}

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
37
38
39
40
41
42
43
#include "iostream"

using namespace std;

class Location {
private :
int X, Y;

public:
Location(int xx = 0, int yy = 0) {
X = xx;
Y = yy;
cout << "有参构造函数被调用了" << endl;
}

Location(const Location &p) {
X = p.X;
Y = p.Y;
cout << "拷贝构造函数被调用了" << endl;
}

~Location() {
cout << "析构函数被调用了" << endl;
}

int getX() {
return X;
}

int getY() {
return Y;
}
};

void functionA(Location b) {
cout << b.getX() << "," << b.getY() << endl;
}

int main() {
Location a(1, 2);
functionA(a); // 拷贝构造函数会被调用,这里会使用实参变量(a)初始化形参变量(b),同时会多创建一个Location对象(匿名对象),所以最后析构函数会被调用两次
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
有参构造函数被调用了
拷贝构造函数被调用了
1,2
析构函数被调用了
析构函数被调用了
第五种调用场景

在 Debug 模式下,当使用函数的返回值(局部对象)来初始化另外一个同类型的对象时,会自动调用拷贝构造函数。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>

using namespace std;

class Location {

private:
int x, y;

public:
Location(int xx = 0, int yy = 0) {
x = xx;
y = yy;
cout << "有参构造函数被调用了" << endl;
}

Location(const Location& p) {
x = p.x;
y = p.y;
cout << "拷贝构造函数被调用了" << endl;
}

~Location() {
cout << "析构函数被调用了" << endl;
}

int getX() {
return x;
}

int getY() {
return y;
}
};

Location functionA() {
Location l(1, 2);
return l;
}

void test01() {
// 第一种情况
// 在 VS 的 Debug 模式下,若使用函数 functionA() 的返回值(匿名对象),赋值给另外一个同类型的对象时,会自动调用拷贝构造函数
// 在 VS 的 Release 模式下,若使用函数 functionA() 的返回值(匿名对象),赋值给另外一个同类型的对象时,不会自动调用拷贝构造函数(编译器优化的结果)
Location A;
A = functionA();
}

void test02() {
// 第二种情况
// 在 VS 的 Debug 模式下,若使用函数 functionA() 的返回值(匿名对象),来初始化另外一个同类型的对象时,会自动调用拷贝构造函数
// 在 VS 的 Release 模式下,若使用函数 functionA() 的返回值(匿名对象),来初始化另外一个同类型的对象时,不会自动调用拷贝构造函数(编译器优化的结果)
Location B = functionA();
}

int main() {
// 匿名对象的去与留,关键是看返回匿名对象时如何被接收,一般有以下两种情况

cout << "----------call test1()----------" << endl;
test01();

cout << "----------call test2()----------" << endl;
test02();

return 0;
}

程序运行输出的结果(使用 VS 的 Debug 模式)如下:

1
2
3
4
5
6
7
8
9
10
11
12
----------call test1()----------
有参构造函数被调用了
有参构造函数被调用了
拷贝构造函数被调用了
析构函数被调用了
析构函数被调用了
析构函数被调用了
----------call test2()----------
有参构造函数被调用了
拷贝构造函数被调用了
析构函数被调用了
析构函数被调用了

程序运行输出的结果(使用 VS 的 Release 模式)如下:

1
2
3
4
5
6
7
8
----------call test1()----------
有参构造函数被调用了
有参构造函数被调用了
析构函数被调用了
析构函数被调用了
----------call test2()----------
有参构造函数被调用了
析构函数被调用了

运行结果分析

C++ 编译器存在一种对函数返回值优化的技术 - RVO (Return Value Optimization)。在 VS 的 Debug 模式下并没有进行这种优化,所以在函数 functionA 中创建 l 对象时,调用了一次构造函数,当编译器发现要返回这个局部的对象时,编译器通过调用拷贝构造函数创建一个临时的 Loacation 对象并返回,然后调用 l 的析构函数。从常理来分析的话,这个匿名对象和这个局部的 p 对象是相同的两个对象,那么如果能直接返回 p 对象,就会省去一个拷贝构造函数和析构函数的开销。在程序中一个对象的拷贝是非常耗时的,如果可以减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。所以在 VS 的 Release 模式下,编译器偷偷做了一层优化。