C++ 入门基础之五

浅拷贝与深拷贝

  • 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
#include <iostream>
#include "string.h"

using namespace std;

class Name {

private:
char *p;
int len;

public:

Name(const char *name) {
cout << "有参构造函数被调用了" << endl;
int length = strlen(name);
p = (char *) malloc(length + 1);
strcpy(p, name);
len = length;
}

~Name() {
cout << "析构函数被调用了" << endl;
if (p != NULL) {
free(p);
p = NULL;
len = 0;
}
}

char *getP() const {
return p;
}

int getLen() const {
return len;
}
};

int main() {
Name obj1("Peter");
Name obj2 = obj1; // 自动调用C++提供的默认拷贝构造函数,属于浅拷贝
cout << "obj1.name: " << obj1.getP() << ", obj1.len: " << obj1.getLen() << endl;
cout << "obj2.name: " << obj2.getP() << ", obj2.len: " << obj2.getLen() << endl;
return 0;
}

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

1
2
3
4
5
6
7
有参构造函数被调用了
obj1.name: Peter, obj1.len: 5
obj2.name: Peter, obj2.len: 5
析构函数被调用了
析构函数被调用了

Process finished with exit code 134 (interrupted by signal 6: SIGABRT)

问题分析

由于在上述的代码中,没有自定义拷贝构造函数,使用的是 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
#include <iostream>
#include "string.h"

using namespace std;

class Name {

private:
char *p;
int len;

public:

Name(const char *name) {
cout << "有参构造函数被调用了" << endl;
int length = strlen(name);
p = (char *) malloc(length + 1);
strcpy(p, name);
len = length;
}

// 深拷贝的实现
Name(const Name &name) {
cout << "拷贝构造函数被调用了" << endl;
int length = name.getLen();
p = (char *) malloc(length + 1);
strcpy(p, name.getP());
len = length;
}

~Name() {
cout << "析构函数被调用了" << endl;
if (p != NULL) {
free(p);
p = NULL;
len = 0;
}
}

char *getP() const {
return p;
}

int getLen() const {
return len;
}
};

int main() {
Name obj1("Peter");
Name obj3 = obj1; // 自动调用自定义的拷贝构造函数(深拷贝)
cout << "obj1.name: " << obj1.getP() << ", obj1.len: " << obj1.getLen() << endl;
cout << "obj3.name: " << obj3.getP() << ", obj3.len: " << obj3.getLen() << endl;
return 0;
}

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

1
2
3
4
5
6
有参构造函数被调用了
拷贝构造函数被调用了
obj1.name: Peter, obj1.len: 5
obj3.name: Peter, obj3.len: 5
析构函数被调用了
析构函数被调用了

特别注意:

在以下的代码中,obj3 = obj1; 依旧属于浅拷贝(这里不会自动调用拷贝构造函数),最终程序也会异常终止运行。若希望解决该问题,需要重载 C++ 的 = 操作符,这里暂时不展开讨论。

1
2
3
4
5
6
7
8
int main() {
Name obj1("Peter");
Name obj3("Tom");
obj3 = obj1; // 浅拷贝,不会自动调用拷贝构造函数
cout << "obj1.name: " << obj1.getP() << ", obj1.len: " << obj1.getLen() << endl;
cout << "obj3.name: " << obj3.getP() << ", obj3.len: " << obj3.getLen() << endl;
return 0;
}

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

1
2
3
4
5
6
7
8
有参构造函数被调用了
有参构造函数被调用了
obj1.name: Peter, obj1.len: 5
obj3.name: Peter, obj3.len: 5
析构函数被调用了
析构函数被调用了

Process finished with exit code 134 (interrupted by signal 6: SIGABRT)

对象的动态建立和释放

使用类名定义的对象都是静态的(如 Teacher t(30);),在程序运行过程中,对象所占的内存空间是不能随时释放的,只有在程序运行结束之后才会被释放。但有时候用户希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用,这样可提高内存空间的利用率。在 C++ 中,可以用 new 运算符动态建立对象,用 delete 运算符动态撤销对象。

new 和 delete 介绍

在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在 C 语言中是利用库函数 malloc()free() 来分配和撤销内存空间的。C++ 提供了较简便而功能较强的运算符 newdelete 来取代 malloc()free() 函数。值得注意的是,newdelete 是运算符,不是函数,因此执行效率更高。虽然为了与 C 语言兼容,C++ 仍保留 malloc()free() 函数,但建议用户不要使用 malloc()free() 函数,而是使用 newdelete 运算符。

new 和 delete 的基础语法

cplusplus-new
cplusplus-delete

new 运算符的简单使用例子如下:

  • new int;:开辟一个存放整数的内存空间,返回一个指向该内存空间的地址(即指针)
  • new int(100);:开辟一个存放整数的空间,并指定该整数的初值为 100,返回一个指向该内存空间的地址(即指针)
  • new char[10];:开辟一个存放字符数组(包括 10 个元素)的空间,返回首元素的地址(即指针)
  • new int[5][4];:开辟一个存放二维整型数组(大小为 5*4)的空间,返回首元素的地址(即指针)
  • float *p = new float (3.14159);:开辟一个存放单精度数的空间,并指定该实数的初值为 3.14159,将返回的该空间的地址赋给指针变量

值得注意的是,用 new 分配数组内存空间时不能指定初值,如果由于内存不足等原因而导致无法正常分配内存空间,那么 new 会返回一个空指针 NULL,用户可以根据该指针的值判断内存空间是否分配成功。

new 和 delete 的使用案例

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
91
92
93
#include <iostream>

using namespace std;

class Teacher {
private:
int _age;

public:
Teacher(int age) {
this->_age = age;
cout << "构造函数被调用" << endl;
}

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

void setAget(int age) {
this->_age = age;
}

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

// C语言分配基础类型
void functionA() {
int *p = (int *) malloc(sizeof(int));
*p = 3;
cout << "functionA -> p = " << *p << endl;
free(p);
}

// C++分配基础类型
void functionB() {
int *a = new int;
*a = 3;
cout << "functionB -> a = " << *a << endl;
delete a;

int *b = new int(30);
cout << "functionB -> b = " << *b << endl;
delete b;
}

// C语言分配数组类型
void functionC() {
char *p = (char *) malloc(sizeof(char) * 3);
p[0] = 'a';
p[1] = 'b';
p[2] = 'c';
cout << "functionC -> p = " << p[0] << p[1] << p[2] << endl;
free(p);
}

// C++分配数组类型
void functionD() {
char *p = new char[3];
p[0] = 'e';
p[1] = 'f';
p[2] = 'g';
cout << "functionD -> p = " << p[0] << p[1] << p[2] << endl;
delete []p;
}

// C语言分配对象
void functionE() {
// 这里不会自动调用类的构造函数和析构函数
Teacher *p = (Teacher *) malloc(sizeof(Teacher));
p->setAget(33);
cout << "functionE -> age = " << p->getAge() << endl;
free(p);
}

// C++分配对象
void functionF() {
// new和delete会分别自动调用类的构造函数和析构函数
Teacher *p = new Teacher(35);
cout << "functionF -> age = " << p->getAge() << endl;
delete p;
}

int main() {
functionA();
functionB();
functionC();
functionD();
functionE();
functionF();
return 0;
}

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

1
2
3
4
5
6
7
8
9
functionA -> p = 3
functionB -> a = 3
functionB -> b = 30
functionC -> p = abc
functionD -> p = efg
functionE -> age = 33
构造函数被调用
functionF -> age = 35
析构函数被调用

上面的 Teacher *p = new Teacher(35); 这种写法,是将两个语句(定义指针变量和使用 new 建立新对象)合并为一个语句,并指定初值,在调用对象时,既可以通过对象名,也可以通过指针。在执行 new 运算符时,如果内存空间不足,无法开辟所需的内存空间,目前大多数 C++ 编译器都会返回一个 0 指针值。只要检测返回值是否为 0,就可判断内存空间是否分配成功。ANSI C++ 标准提出,在执行 new 出现故障时,就抛出一个异常,用户可根据异常进行相关处理,但 C++ 标准仍然允许在出现 new 故障时返回 0 指针值。值得注意的是,不同的编译器对 new 故障的处理方法是不同的。当不再需要使用由 new 建立的对象时,可以用 delete 运算符予以释放,此后程序不能再使用该对象。如果用一个指针变量先后指向了不同的动态对象,应注意指针变量的当前指向,以避免释放错了对象。在执行 delete 运算符时,在释放内存空间之前,会自动调用类的析构函数,完成有关善后清理工作。

静态成员变量

静态成员变量的概念

  • 静态成员局部于类,它不是对象成员
  • 在类外访问静态成员变量时,可以使用 类名 :: 作为限定词,或通过对象访问
  • 关键字 static 可以用于声明一个类的成员,静态成员提供了一个同类对象的共享机制
  • 将一个类的成员声明为 static 时,这个类无论有多少个对象被创建,这些对象都共享这个 static 成员

cplusplus-static

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

using namespace std;

class Counter {
private:
// 声明静态成员变量
static int num;

public :

// 成员函数访问静态成员变量
void setNum(int i) {
num = i;
}

void showNum() {
cout << num << endl;
}
};

// 定义静态成员变量,这里不是简单的变量赋值,更重要的是告诉C++编译器,给静态成员变量分配内存
int Counter::num = 0;

int main() {
Counter a, b;
a.showNum();
b.showNum();
a.setNum(10);
a.showNum();
b.showNum();
return 0;
}

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

1
2
3
4
0
0
10
10

静态成员变量的使用

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

using namespace std;

class Counter {
public:
int mem; // 公有成员变量
static int smem; // 公有静态成员变量

public :
Counter(int num) {
mem = num;
}
};

// 定义静态成员变量,这里不是简单的变量赋值,更重要的是告诉C++编译器,给静态成员变量分配内存
int Counter::smem = 0;

int main() {
Counter c(5);
for (int i = 0; i < 5; i++) {
// 访问静态成员变量的方法1(通过类名直接访问)
Counter::smem += i;
cout << "Counter::smem = "<< Counter::smem << endl;
}

// 访问静态成员变量的方法2(通过对象访问)
cout << "c.smem = " << c.smem << endl;
return 0;
}

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

1
2
3
4
5
6
Counter::smem = 0
Counter::smem = 1
Counter::smem = 3
Counter::smem = 6
Counter::smem = 10
c.smem = 10

静态成员函数

静态成员函数的概念

  • 静态成员函数、静态成员变量都属于类的
  • 静态成员函数都是以关键字 static 声明
  • 在类外调用静态成员函数时,可以使用 类名 :: 作为限定词,或通过对象访问
  • 静态成员函数提供不依赖于类数据结构的共同操作,它没有 this 指针,而普通成员函数包含一个指向具体对象的 this 指针

静态成员函数的使用

值得一提的是,在静态成员函数中,不能访问普通成员变量和调用普通成员函数。这是因为静态成员函数属于整个类的,它没办法区分普通成员变量和普通成员函数是属于哪个具体的对象;同时在静态成员函数内,不能使用 this 指针。

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 Counter {
private:
int num;

public:
// 声明静态成员函数
static int getNum(Counter *p);

static void setNum(int i, Counter *p);
};

// 定义静态成员函数
int Counter::getNum(Counter *p) {
return p->num;
}

void Counter::setNum(int i, Counter *p) {
p->num = i;
}

int main() {
Counter obj;

// 访问静态成员函数的方法1(通过类名直接访问)
Counter::setNum(1, &obj);
cout << "num = " << Counter::getNum(&obj) << endl;

// 访问静态成员函数的方法2(通过对象访问)
obj.setNum(3, &obj);
cout << "num = " << obj.getNum(&obj) << endl;
return 0;
}

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

1
2
num = 1
num = 3

C++ 面向对象模型初探

对象模型概述

C++ 对象模型可以概括为以下两部分:

  • 对于各种特性支持的底层实现机制
  • 语言中直接支持面向对象程序设计的部分,主要涉及如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等

在 C 语言中,“数据” 和 “处理数据的操作(函数)” 是分开来声明的,也就是说,语言本身并没有支持 “数据和函数” 之间的关联性。在 C++ 中,通过抽象数据类型 ADT(Abstract Data Type),在类中定义数据和函数来实现数据和函数直接的绑定。概括来说,在 C++ 类中有两种成员数据:staticnonstatic,三种成员函数:staticnonstaticvirtual

cplusplus-class

属性和函数的处理机制

C++ 中的 Class 从面向对象理论出发,将变量(属性)和函数(方法)集中定义在一起,用于描述现实世界中的类。从计算机的角度,程序依然由数据段和代码段构成。

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

using namespace std;

struct S1 {
int i;
int j;
int k;
};

struct S2 {
int i;
int j;
int k;
static int m;
};

class C1 {
public:
int i;
int j;
int k;
};

class C2 {
public:
int i;
int j;
int k;
static int m;

public:
int getK() const {
return k;
}

void setK(int val) {
k = val;
}
};

int main() {
printf("s1:%d \n", sizeof(S1));
printf("s2:%d \n", sizeof(S2));
printf("c1:%d \n", sizeof(C1));
printf("c2:%d \n", sizeof(C2));
return 0;
}

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

1
2
3
4
s1:12
s2:12
c1:12
c2:12

通过上面的案例,可以得知 C++ 类对象中的成员变量和成员函数是分开存储的,C 语言中的内存四区模型仍然有效。C++ 中类的普通成员函数都隐式包含一个指向当前对象的 this 指针。

  • 静态成员变量:存储于全局数据区中
  • 普通成员变量:存储于对象中,与 struct 变量有相同的内存布局和字节对齐方式
  • 成员函数:存储于代码段中

this 指针的使用

cplusplus-this

值得一提的是,当使用 const 修饰类成员函数时,成员函数不能修改被调用对象的值,这是因为此时 const 本质上修饰的是 this 指针,间接也说明了 conststatic 关键字不能同时修饰类成员函数,示例代码如下:

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>

using namespace std;

class Test {
private:
int _cm;

public:
Test() {}

Test(int _m) : _cm(_m) {}

int get_cm() const {
// _cm = 10; 是错误写法,对象的_cm属性值不能被改变
return _cm;
}
};


void Cmf(const Test & _tt) {
cout << _tt.get_cm();
}

int main() {
Test t(8);
Cmf(t); // 打印结果为8
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
#include <iostream>
using namespace std;

class Test
{
public:
int a;
int b;

public:
Test(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
}

~Test()
{

}

public:
void printT()
{
cout << "a:" << a << " b: " << b << endl;
}

Test testAdd(Test& t2)
{
Test tmp(this->a + t2.a, this->b + t2.b);
return tmp;
}

//t1.testAdd2(t2);
//返回一个引用,相当于返回自身
//返回t1这个元素,this就是&t1
Test& testAdd2(Test& t2)
{
this->a = this->a + t2.a;
this->b = this->b + t2.b;
return *this; //把 *(&t1) 又回到了 t1元素
}
};

// 全局函数
Test testAdd(Test& t1, Test& t2)
{
Test tmp;
tmp.a = t1.a + t2.a;
tmp.b = t1.b + t2.b;
return tmp;
}

// 全局函数
void printT(Test* pT)
{
cout << "a:" << pT->a << " b: " << pT->b << endl;
}

int main()
{
Test t1(1, 2);
Test t2(3, 4);

// 调用全局函数
Test t3;
t3 = testAdd(t1, t2);
printT(&t3);

// 调用成员函数
Test t4 = t1.testAdd(t2); // 将匿名对象直接转化成t4
t4.printT();

Test t5;
t5 = t1.testAdd(t2); // 将匿名对象复制给t5
t5.printT();

t1.testAdd2(t2); // 函数内部使用了this指针
t1.printT();

return 0;
}

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

1
2
3
4
a:4 b: 6
a:4 b: 6
a:4 b: 6
a:4 b: 6