C++ 入门基础之五

大纲

explicit 关键字

explicit 介绍

在 C++ 中,explicit 关键字用于声明类的单参数构造函数为显式构造函数,防止其被隐式调用。简而言之,explicit 可以显式地禁止类构造函数的隐式转换,这样可以避免一些意外的类型转换和编译器自动调用构造函数的情况。

使用注意事项

  • explicit 关键字只能用于类内部的构造函数声明上。
  • explicit 关键字作用于单个参数的构造函数。

explicit 使用

当一个类有一个单参数的构造函数,该构造函数接受一个参数时,C++ 默认允许对参数进行隐式转换(隐式类型转换)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {

public:
explicit MyClass(int x) : num(x) {

}

private:
int num;

};

int main() {
MyClass obj = 42; // 错误写法:使用 explicit 后不允许隐式转换
MyClass obj2(42); // 正确写法:显式转换
}

在上面的例子中,MyClass 类有一个单参数构造函数,使用 explicit 关键字修饰后,这意味着不允许隐式转换。尝试使用 MyClass obj = 42 来初始化 obj 会导致编译错误,只有显式地使用 MyClass(42) 来初始化 obj2 才是允许的。总结,当想要禁止某个构造函数的隐式转换,可以将该构造函数标记为 explicit。这样,当该构造函数被用于隐式转换时,编译器会报错。这在多参数或标准布局的类中特别有用,可以防止不正确的转换和意外的转换。

深拷贝与浅拷贝

提示

  • C++ 提供的默认拷贝构造函数,可以完成对象的数据成员值简单的复制,属于浅拷贝。
  • 对象的数据资源是由指针指向的堆,C++ 提供的默认拷贝构造函数仅对指针的值(内存地址)进行复制,不会对指针指向的内容进行复制,属于浅拷贝。

概念介绍

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是涉及到对象拷贝的概念。在 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>

using namespace std;

class ShallowCopyExample {

public:
ShallowCopyExample(int val) {
data = new int;
*data = val;
}

ShallowCopyExample(const ShallowCopyExample &other) {
// 浅拷贝
data = other.data;
}

void setData(int val) {
*data = val;
}

int getData() {
return *data;
}

~ShallowCopyExample() {
delete data;
}

private:
int *data;

};

int main() {
ShallowCopyExample shallowObj1(10);
ShallowCopyExample shallowObj2 = shallowObj1;

shallowObj2.setData(20);

cout << "Shallow Copy Example:\n";
cout << "Obj1 Data: " << shallowObj1.getData() << endl;
cout << "Obj2 Data: " << shallowObj2.getData() << endl;

return 0;
}

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

1
2
3
Shallow Copy Example:
Obj1 Data: 20
Obj2 Data: 20

深拷贝的概念

深拷贝会为目标对象分配一块新的内存,并将源对象的数据复制到新的内存空间中。这样可以确保每个对象拥有自己的数据副本,互不影响。

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>

class DeepCopyExample {

public:
DeepCopyExample(int val) {
data = new int;
*data = val;
}

DeepCopyExample(const DeepCopyExample &other) {
// 深拷贝
data = new int;
*data = *other.data;
}

void setData(int val) {
*data = val;
}

int getData() {
return *data;
}

~DeepCopyExample() {
delete data;
}

private:
int *data;

};

int main() {
DeepCopyExample deepObj1(10);
DeepCopyExample deepObj2 = deepObj1;

deepObj2.setData(20);

std::cout << "Deep Copy Example:\n";
std::cout << "Obj1 Data: " << deepObj1.getData() << std::endl;
std::cout << "Obj2 Data: " << deepObj2.getData() << std::endl;

return 0;
}

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

1
2
3
Deep Copy Example:
Obj1 Data: 10
Obj2 Data: 20

浅拷贝问题剖析

问题提出

思考:下述代码为什么会异常终止运行?

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)

new 与 delete 运算符

C 语言的动态分配内存

为了在运行时支持动态分配内存,C 语言在它的标准库中提供了一些函数,比如 malloc 以及它的变种 callocrealloc,还有释放内存的 free 函数。这些函数都是有效的,不过都是原始的,需要程序员理解和小心使用。为了使用 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
#include <iostream>
#include <string.h>

struct Person {

int mAge;
char *pName;

void init() {
mAge = 20;
pName = (char *) malloc(strlen("Peter") + 1);
if (pName != NULL) {
strcpy(pName, "Peter");
}
}

void destroy() {
if (pName != NULL) {
free(pName);
pName = NULL;
}
}

void toString() {
printf("name = %s, age = %d", pName, mAge);
}

};

int main() {
struct Person person;

// 初始化对象
person.init();

// 调用成员函数
person.toString();

// 销毁对象
person.destroy();

return 0;
}

上述代码有以下诸多弊端,而且动态分配内存函数太复杂,容易令人混淆,所以在 C++ 中推荐使用 newdelete 运算符。

  • 程序员必须确定对象的长度
  • malloc 函数返回一个 void * 指针,C++ 不允许将 void * 赋值给其他任何指针,必须强制转换类型
  • malloc 函数可能申请内存失败,所以必须判断返回值来确保内存分配成功
  • 在使用对象之前必须要对它初始化,用户有可能忘记调用初始化函数,导致出现意想不到的结果

对象的动态创建和释放

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

在栈上创建对象

在 C++ 中,使用类名定义的对象如 Teacher t(30); 是在栈上创建对象的一种方式,对象的生命周期与作用域相关。当这个对象跳出其所在的作用域时,比如函数结束、代码块结束或者程序结束,对象会被自动销毁,其析构函数会被调用,从而释放对象占用的内存空间。所以,对于 Teacher t(30); 这样在栈上创建的对象,在其作用域结束时会自动释放内存空间,无需手动管理内存。这是 C++ 的 RAII(资源获取即初始化)机制的体现,能够帮助避免内存泄漏和资源泄漏问题。

new 和 delete 的介绍

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

delete 运算符与 free () 函数的区别

在 C++ 中,delete pfree(p) 都可以用来释放动态分配的内存,但它们之间有一些关键的区别:

  • delete p 是 C++ 中的删除动态分配的对象的操作符,它不仅会释放内存,还会调用对象的析构函数(如果有的话)来做清理工作,所以对于类类型的对象,应该始终使用 delete 运算符来释放内存。
  • free(p) 是 C 标准库的函数,它只是简单地释放内存,不会调用任何析构函数。在 C++ 中使用 delete 运算符会更安全,因为它确保了对象的析构函数被正确调用,避免了内存泄漏和悬挂指针等问题。
  • 因此,在 C++ 中,推荐使用 delete 运算符来释放使用 new 运算符分配的内存,避免使用 free() 来释放内存。

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 运算符创建对象时,如果由于内存不足等原因而导致无法正常分配内存空间,那么大多数 C++ 编译器都会让 new 返回一个 0 指针值,用户可以根据该指针的值来判断内存空间是否分配成功。

new 和 delete 的使用案例

在 C++ 中解决动态内存分配的方案是将创建一个对象所需要的操作都结合在一个称为 new 的运算符里。当使用 new 创建一个对象时,C++ 会在堆里为对象分配内存,并调用构造函数完成对象的初始化。使用 new 运算符后,会发现在堆里创建对象的过程变得简单多了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和类型安全检查,这样在堆里创建一个对象就和在栈里创建对象一样简单。

使用案例一

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

using namespace std;

class Person {

public:

Person() {
cout << "默认构造函数调用" << endl;
}

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

};

int main() {
Person p1; // 创建对象(静态),会在栈上开辟内存空间,对象所在的作用域结束后或者程序结束后都会自动释放内存空间,会自动调用析构函数

Person *p2 = new Person(); // 通过 new 运算符创建对象(动态),会在堆上开辟内存空间,对象所在的作用域结束后或者程序结束后都不会自动释放内存空间,不会自动调用析构函数
delete p2; // 通过 delete 运算符释放对象所在的堆内存空间,会自动调用析构函数

return 0;
}

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

1
2
3
4
默认构造函数调用
默认构造函数调用
析构函数调用
析构函数调用

使用案例二

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

使用案例三

使用 void * 类型指针 p 来接收 new 运算符返回的 Person 对象的地址,这样做会导致编译器无法知道 p 所指向的内存应该如何处理。此时,使用 delete 运算符释放一个 void * 指针的内存是不安全的,因为编译器不知道该指针指向的内存分配时使用的具体类型,最终导致内存空间没有被正确释放,同时析构函数也没有被自动调用。

警告

尽量不要使用 void * 类型指针去接收 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
#include <iostream>

using namespace std;

class Person {

public:

Person() {
cout << "默认构造函数调用" << endl;
}

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

};

int main() {
void *p = new Person(); // 当使用 void * 类型接收 new 出来的指针时,会出现内存释放问题
delete p; // 这里无法正确释放内存空间,并且不会自动调用析构函数
return 0;
}

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

1
默认构造函数调用

使用案例四

使用 new 运算符创建对象数组时,编译器一定会自动调用类的默认构造函数,所以必须提供类的默认构造函数,否则代码编译不通过。如果要释放对象数组的内存空间,可以使用 delete[] pArray 的写法,这样就可以释放 pArray 指针指向的对象数组的内存空间,避免内存泄漏。

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

using namespace std;

class Person {

public:

Person() {
cout << "默认构造函数调用" << endl;
}

Person(int age) : mAge(age) {
cout << "有参构造函数调用" << endl;
}

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

private:
int mAge;

};

void test01() {
Person *pArray = new Person[3]; // 通过 new 运算符创建对象数组时(动态),一定会自动调用类的默认构造函数,所以必须提供类的默认构造函数,否则编译不通过
delete[] pArray; // 通过 delete [] 运算符释放对象数组的内存空间,会自动调用析构函数
}

void test02() {
Person pArray2[2] = {Person(1), Person(2)}; // 创建对象数组(静态),会在栈上开辟内存空间,可以指定有参构造函数
}

int main() {
cout << "------- test01() -------" << endl;
test01();
cout << "------- test02() -------" << endl;
test02();
return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
------- test01() -------
默认构造函数调用
默认构造函数调用
默认构造函数调用
析构函数调用
析构函数调用
析构函数调用
------- test02() -------
有参构造函数调用
有参构造函数调用
析构函数调用
析构函数调用

静态成员变量

在一个类中,若将一个成员变量声明为 static,这种成员称为静态成员变量。它与一般的成员变量不一样,无论创建多少个对象,都只有一份静态数据的拷贝。静态成员变量,是属于某个类的,被所有对象所共享。

静态成员变量的概念

  • 静态成员变量作用于类,它不是对象成员。
  • 静态成员变量都是以关键字 static 声明。
  • 静态成员变量拥有访问控制级别,比如可以声明为 private
  • 在类外访问静态成员变量时,可以使用 类名 :: 作为限定词,或者通过对象名访问。
  • 关键字 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
31
#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++) {
// 访问静态成员变量的第一种方法(通过类名直接访问)
Counter::smem += i;
cout << "Counter::smem = "<< Counter::smem << endl;
}

// 访问静态成员变量的第二种方法(通过对象名访问)
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 声明。
  • 静态成员函数拥有访问控制级别,比如可以声明为 private
  • 在类外调用静态成员函数时,可以使用 类名 :: 作为限定词,或者通过对象名访问。
  • 静态成员函数提供不依赖于类数据结构的共同操作,它没有 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
37
38
#include <iostream>

using namespace std;

class Counter {

private:
int num;

public:

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

// 声明静态成员函数
static void setNum(int i, Counter *p);

};

// 定义静态成员函数
void Counter::setNum(int i, Counter *p) {
p->num = i;
}

int main() {
Counter obj;

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

// 访问静态成员函数的第二种方法(通过对象名访问)
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 变量具有相同的内存布局和字节对齐方式
  • 成员函数(静态与普通):存储在代码段中,虽然内含在 class 声明之内,却不会出现在对象中
  • 每一个非内联函数(non-inline member function)都只会诞生一份函数实例,也就是说多个同类型的对象会共用同一块函数代码

总结:只有普通成员变量(非静态成员变量),才存储在对象中。

字节对齐方式的介绍

  • 在 C++ 中,可以使用 #pragma pack(n) 预处理指令实现字节对齐,它用于指示编译器按照指定的字节对齐方式对结构体、类等数据类型进行打包(或者称为紧凑化)。具体来说,#pragma pack(n) 中的 n 表示指定的对齐字节数,通常取 1、2、4、8 等值,表示数据在内存中的对齐方式。
  • 使用 #pragma pack(n) 可以让编译器以字节对齐的方式来排列结构体或类的成员,从而减少内存占用。这对于一些特定的应用场景非常有用,例如与硬件交互时,需要确保数据结构的布局与硬件的预期一致,或者需要在网络中传输结构化数据时,确保数据的字节对齐方式与协议要求一致。
  • 特别注意的是,使用 #pragma pack(n) 可能会导致性能下降,因为对齐不当可能会增加内存访问的时间。因此,在使用时需要权衡考虑。另外,#pragma pack 是编译器相关的特性,不同的编译器可能会有不同的行为,因此在跨平台开发时需要谨慎使用。

this 指针的使用

在上述的介绍中,每一个非内联函数(non-inline member function)都只会诞生一份函数实例,也就是说多个同类型的对象会共用同一块函数代码,如下图所示。那么问题是:这块代码是如何区分哪个对象调用自己的呢?

C++ 规定,this 指针是隐含在成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都有一个由编译器自动生成的隐含指针 this,用以保存这个对象的地址。也就是说,虽然程序员没有写上 this 指针,但编译器在编译的时候会自动加上去。因此 this 也称为 “指向本对象的指针”,而且 this 指针并不是对象的一部分,不会影响 sizeof(对象) 的执行结果。this 指针是 C++ 实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起。在外部看起来,每一个对象都拥有自己的成员函数,其实这是一种假象。一般情况下,并不需要主动写 this,而是让编译器进行默认设置。成员函数通过 this 指针就可以知道自己需要操作的是哪个对象的数据。this 指针是一种隐含指针,它隐含在每个类的非静态成员函数中。静态成员函数的内部没有 this 指针,因为静态成员函数不能操作非静态成员变量

当形参和成员变量同名时,可用 this 指针来区分。在类的非静态成员函数中返回对象本身时(链式编程),可以使用 return *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
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>

using namespace std;

class Person {

public:

Person() {

}

// 当形参和成员变量同名时,可用 this 指针来区分
Person(int age) {
this->age = age;
}

// 编译器会自动加上一个 this 指针参数:Person * const this
void showAge() {
cout << "age = " << this->age << endl;
}

// 在类的非静态成员函数中返回对象本身时(链式编程),可以使用 return *this
Person &addAge(const Person &p) {
this->age += p.age;
return *this;
}

private:
int age = 0;

};


int main() {
Person p1(18);
p1.showAge();

Person p2(26);
p2.showAge();

Person p3(23);
p1.addAge(p2).addAge(p3);
p1.showAge();

return 0;
}

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

1
2
3
age = 18
age = 26
age = 67

当使用 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;
}

空指针访问成员函数

  • 如果成员函数的内部没有使用到 this 指针,那么空指针可以直接访问成员函数。
  • 如果成员函数的内部使用到 this 指针,那么就要注意加 if 判断 this 指针是否为 NULL,否则成员函数在调用期间会异常退出。
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
using namespace std;

class Person {

public:

void showPerson() {
cout << "Show person" << endl;
}

void showAge() {
cout << "Show age is " << mAge << endl;
}

void printAge() {
// 判断 this 指针是否为空
if (this == NULL) {
return;
}
cout << "Print age is " << this->mAge << endl;
}

private:
int mAge;

};

int main() {
Person *p = NULL;
p->showPerson(); // 空指针可以访问 showPerson() 成员函数
p->printAge(); // 空指针可以访问 printAge() 成员函数
// p->showAge(); // 空指针不可以访问 showAge() 成员函数,代码编译会通过,但运行期间会异常退出
return 0;
}

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

1
Show person

常函数与常对象的使用

  • 常函数

    • 定义语法:void func() const {},即需要在函数的小括号后面加上 const 关键字
    • 常函数修饰的是 this 指针,即 const Type * const this
    • 常函数不能修改 this 指针指向的内存内容,即在常函数内不允许修改成员变量的值
    • 如果希望某个成员变量的值可以在常函数内被修改,那么可以使用 mutable 关键字修饰该成员变量
  • 常对象

    • 定义语法:const Type t;,即在定义对象时,需要在最前面加上 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

using namespace std;

class Person {

public:
Person() {
this->m_A = 0;
this->m_B = 0;
}

// 定义常函数
int getA() const {
// 错误写法,常函数内不允许修改成员变量的值
// this->m_A = 10;

// 常函数内可修改被 mutable 修饰的成员变量的值
this->m_B = 20;

return this->m_A;
}

// 定义普通函数
int getB() {
return this->m_B;
}

public:
int m_A;
mutable int m_B;
};

void test01() {
Person p1;
int m_A = p1.getA();
cout << m_A << endl;
}

void test02() {
// 定义常对象
const Person p2;

// 错误写法,不允许修改常对象的成员变量的值
// p2.m_A = 10;

// 错误写法,常对象不可以调用普通的成员函数
// p2.getB();

// 常对象可以调用常函数
int m_A = p2.getA();
cout << m_A << endl;
}

int main() {
test01();
test02();
return 0;
}

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

1
2
0
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