C++ 入门基础之四

大纲

学习目标

  • C++ 面向对象的基础模型
  • C++ 编译器管理类和对象的机制
  • 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 的区别

struct 和 class 关键字的区别如下:

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

析构函数的调用:

  • 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++ 编译器提供了构造函数(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 {

private:
int _a;
int _b;

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;
}

};

默认的构造函数

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 = 3;
}

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


public:

int getA() const {
return _a;
}

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

int main() {
// 第一种: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());

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

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

return 0;
}

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

1
2
3
4
a = 5, b = 3
a = 10, b = 20
a = 1, b = 1
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
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 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
析构函数被调用了
析构函数被调用了
第四种调用场景
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
#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;
}

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

// 若将函数functionA()返回的匿名对象,赋值给另外一个同类型的对象,那么匿名对象会被析构
// 此时有参构造函数和析构函数被调用两次
Location A;
A = functionA();

// 若使用函数functionA()的匿名对象,来初始化另外一个同类型的对象,那么匿名对象会直接转成B对象
// 此时有参构造函数与析构函数各被调用一次
// Location B = functionA();
return 0;
}

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

1
2
3
4
有参构造函数被调用了
有参构造函数被调用了
析构函数被调用了
析构函数被调用了

思考:在上述的代码中,在 main() 函数内直接调用 functionA() 函数时,为什么拷贝构造函数没有被调用呢?是否跟 C++ 编译器的版本有关系呢?

构造函数的使用规则

  • 当类中没有定义任何一个构造函数时,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
#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;
}
构造函数和析构函数的调用顺序
  • 当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同,之后再调用类自身的构造函数
  • 析构函数的调用顺序与对应的构造函数调用顺序相反