C++类
# 类
# 介绍
C++作为一门面向对象语言,可以通过定义新的类来反映各种概念,使编写、调试、修改程序可以更加容易。
类即数据类型,其基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程技术。类的接口用于定义用户所能执行的操作;类的实现则包括接口函数的实现、各种数据成员和私有函数的定义。
封装则用于分离类的接口和实现。封装后的类会隐藏它的实现细节,用户只能使用接口而无法直接访问实现部分。
而想要实现数据抽象和封装,首先还需要定义一个抽象数据类型。在抽象数据类型中,类设计者只需要考虑类的实现过程,抽象的思考类做了什么,而无需关注类的工作细节。
C++中
struct
和class
都能用来自定义数据类型,但在实际中,一般使用struct
定义较为简单的纯数据对象,使用class
定义更为复杂的类对象。另外struct
的成员默认都是公开的,class
的成员默认都是私有的。
# struct定义类型
# 结构体介绍
struct即结构体,C++中可以使用struct关键字来定义结构体类型,它是一种轻量级的类,能够将逻辑上相关的不同类型的数据组合到同一单元中。结构体内可以声明任意类型的成员,包括struct结构体类型,因此结构体也可以嵌套。
# 结构体定义
注意:
- 结构体定义完后要跟";"分号。
- 通过类内初始值进行初始化时,必须以=等号或花括号表示。
定义格式如下:
// 常规定义方式。
struct 结构体名称 {
// 结构体内部定义的名称必须唯一,但可以与结构体外部定义的名称重复。
数据类型 成员名称1;
数据类型 成员名称2 = 初始值;
... ...
};
// 定义的同时声明变量,阅读起来会很混乱,不建议使用。
struct 结构体名称 {
/* ... ... */
} 变量名1, 变量名2, ...;
// 定义并创建类型别名。
typedef struct 结构体名称 {
/* ... ... */
} 别名;
// 定义匿名结构体同时声明变量。匿名结构体不提供名称难以无法复用,但是可以用在声明嵌套结构体时使用。
struct {
/* ... ... */
} 变量名1,变量名2, ...;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用格式如下:
// 以结构体作为类型声明变量,每个变量都会对结构体进行拷贝,因此每个变量都是独立的
结构体名称 变量名1, 变量名2, ...;
// 结构体初始化,使用花括号初始化时,等号可以忽略。没有指定到的成员会使用初始值。
// 直接初始化结构体变量,会将值按顺序传给结构体成员。
结构体名称 变量名1 {值1, 值2, ...};
// 成员初始化结构体变量,会修改对应成员名称为指定的值。
// 该方式至少需要使用ISO C++20以上的标准,VS2022在项目属性中可以设置。
结构体名称 变量名1 { .成员名称1=值, .成员名称2=值, ...};
// 调用结构体内的成员
变量名.成员名
// 调用结构体内的嵌套结构体,以此类推
变量名.成员名.子成员名
// 修改结构体内的成员的值,但如果结构体成员被声明为常量则不能修改
变量名.成员名 = 值;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例子:
#include <iostream>
int main() {
// 定义一个结构体Book
struct Book {
std::string name = "Hello World!";
int price;
// 使用匿名创建嵌套结构体page
struct {
// 字符串常量成员
const std::string name = "page";
// int变量成员
int num = 0;
} page;
};
// 定义一个Book类型变量hello
Book hello;
// 修改该变量的page成员中的num的值
hello.price = 10;
hello.page.num = 1;
// 定义一个Book变量world,并进行直接初始化。
Book world { "world", 20 };
// 定义一个Book变量world,并进行成员初始化。
Book hw = { .name="hw", .price=50 };
std::cout << world.price << std::endl;
}
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
# 定义接口
类和结构体都可以定义接口,接口分为成员函数和非成员函数。成员函数和非成员函数都可以重载,但必须在参数上有所区别。
# 成员函数
成员函数就是定义在结构体内部的函数。成员函数内还可以使用this关键字来表示当前结构体对象,从而访问结构体内的其他成员,包括私有成员和公共成员。如果不使用this关键字直接访问名称,则会从函数体中由内往外查找名称。
this的使用格式为:
this->成员名
,表示访问当前结构体的指定成员。this是成员函数才有的额外隐式形参,它总是指向"这个"对象,所以它是一个常量指针,不允许改变this中保存的地址。
格式:
struct 结构体名称 {
// 定义成员函数
返回类型 函数名(形参) {
// 通过this关键字,来访问结构体内成员。
this->成员名;
}
};
2
3
4
5
6
7
例子:
// 定义结构体,一般定义在所有函数外。
struct Book {
bool is_read = false;
void read(bool is_read) {
// 将传入的参数is_read赋值给结构体对象的is_read成员
this->is_read = is_read;
return;
}
};
int main() {
// 实例化结构体对象。
Book b;
// 输出0。
cout << b.is_read << endl;
// 调用成员接口函数。
b.read(true);
// 输出1。
cout << b.is_read << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 非成员函数
非成员函数就是定义结构体外部的函数,非成员函数通过参数传递结构体对象的引用或指针,来操作结构体的数据。非成员函数只能访问结构体的公共成员,不能直接访问私有成员。
格式:
返回类型 函数名(结构体类型& 对象形参名, 其他形参...){
// 通过传入的结构体对象的引用,来访问结构体内成员
对象形参名.成员名;
}
2
3
4
例子:
// 因为有非成员函数所以必须定义在所有函数外,因为函数内不能嵌套定义函数
struct Book {
bool is_read = false;
};
void read(Book& object) {
// 修改传入的对象的is_read成员为true。
object.is_read = true;
return;
}
int main() {
// 实例化结构体对象。
Book b;
// 输出0。
cout << b.is_read << endl;
// 调用非成员函数修改b对象。
read(b);
// 输出1。
cout << b.is_read << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 常量成员函数
默认情况下,成员函数的this是非常量指针,是允许通过this修改其他非常量成员的值。
而如果我们在形参列表后面跟上const关键字,成员函数的this就会变成常量指针,会变成不允许通过this修改其他成员的值。
但是仍能修改非当前对象成员的值,比如局部对象仍然可以进行修改。
格式:
struct 结构体名称 {
// 定义常量成员函数
返回类型 函数名(形参) const {
// 此时通过this就只能读取成员,而不能修改成员的值。
this->成员名;
}
};
2
3
4
5
6
7
例子:
// 定义结构体,一般定义在所有函数外。
struct Book {
bool is_read = false;
string read_status() const {
// 常量this指针,只能读取而不能修改成员值
if (this->is_read) {
return "yes";
}
else {
return "no";
}
}
};
int main() {
// 实例化结构体对象。
Book b;
// 输出no。
cout << b.read_status() << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 在类外部定义成员函数
我们还可以在类的外部定义成员函数,但必须先在内部对函数进行声明,然后外部的成员函数定义必须与声明匹配,同时名称必须包含所属类名。
例如:
struct Book {
bool is_read = false;
// 成员函数声明
string read_status() const;
};
// 在外部定义函数
string Book::read_status() const {
if (this->is_read) {
return "yes";
}
else {
return "no";
}
}
int main() {
Book b;
cout << b.read_status() << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 成员函数返回this
成员函数也可以返回this,使得可以链式执行调用接口或数据成员。
格式:
struct Book {
bool is_read = false;
// 返回类型是当前类
Book& read() {
this->is_read = true;
// 返回this
return *this;
}
};
int main() {
Book b;
// 返回true
cout << b.read().is_read << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果*this由一个const成员函数返回,则它的返回类型将是常量引用。
# 构造函数
# 构造函数介绍
构造函数是特殊的成员函数,用于控制其类的对象的初始化过程。构造函数一般用于初始化类对象的数据成员,它会在类的对象被创建时自动执行。
构造函数名必须和类名一致,它没有返回类型,但可以有零个或多个参数,同时构造函数也可以重载,也就是一个类可以有多个构造函数,但必须在参数上有所区别。
另外构造函数不能被声明为const对象,因为类中的const对象直到构造函数执行完后,才会赋予其"常量"属性。但这也意味着在构造函数中我们可以对类中的const对象进行初始化。
# 默认构造函数
当类中没有声明任何构造函数时,编译器就会隐式的生成一个默认构造函数,又称合成的默认构造函数。默认构造函数无需任何实参,如果成员有初始值会使用初始值来初始化,否则会使用默认值来初始化。
如果我们定义了构造函数,但仍想使用默认构造函数,则必须手动定义默认构造函数,例如:构造函数名() = default;
# 构造函数初始化值列表
构造函数可以在参数列表后指定初始化值列表,用于对数据成员进行初始化,它可以使用构造函数的形参进行初始化,还能对const对象数据成员进行初始化。
如果想数据成员使用类内定义的初始值,则不写到初始化值列表即可,会使用默认构造函数相同的方式初始化。如果不想使用初始化值列表,可以直接不指定。
数据成员的初始化会在构造函数的初始化值列表中完成,而非构造函数体执行之后,所以在函数体中的只是赋值操作而非初始化操作。
格式如:构造函数名(参数列表):数据成员名(初始值表达式), 数据成员名(初始值表达式),...
# 定义格式
struct 类名{
// 定义默认构造函数
构造函数名() = default;
// 定义构造函数,不使用初始化值列表
构造函数名(参数列表){ 函数体... }
// 定义构造函数,使用初始化值列表
构造函数名(参数列表) : 数据成员名(初始值表达式), 数据成员名(初始值表达式),...{ 函数体... }
};
2
3
4
5
6
7
8
# 例子
struct Book {
string book_name = "default";
const int book_pages = 0;
bool is_read = false;
// 定义默认构造函数
Book() = default;
// 定义构造函数,使用初始化值列表初始化,没有指定的is_read会使用类中定义的false
Book(string name, int pages) : book_name(name), book_pages(pages){}
// 定义构造函数,不使用初始化值列表
Book(string book_name) {
// 进行初始化操作
this->book_name = book_name;
// book_pages = 20; // 构造函数体内不能修改常量对象成员,因为数据成员的初始化已经在函数体执行之前完成
}
};
int main() {
// 使用构造函数构造Book对象
Book b("hello", 100);
// 输出hello
cout << b.book_name << endl;
// 输出100
cout << b.book_pages << endl;
// 输出0表示false
cout << b.is_read << endl;
return 0;
}
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
# 访问控制与封装
# 访问控制
在定义类时,我们还会对类中的成员进行访问控制,以实现类的封装,让用户只能通过我们公开的成员访问类中的数据。
在C++中我们可以使用访问说明符来实现访问控制,访问说明符有以下三种:
public
:定义该访问说明符之后的成员可以在整个程序中访问,可以在类外通过对象访问,是公开的。private
:定义该访问说明符之后的成员只能在类的成员函数中访问,无法在类外访问,是私有的。protected
:定义该访问说明符之后的成员可以被类内、派生类或者友元的成员访问,无法在类外通过对象访问,是受保护的。
定义格式:
class 类名{
// 公共访问说明符,后面定义的都将是公共成员
public:
类型 成员名;
类型 成员名;
...
// 私有访问说明符,后面定义的都将是私有成员
private:
类型 成员名;
类型 成员名;
...
// 受保护访问说明符,后面定义的都将是受保护成员
protected:
类型 成员名;
类型 成员名;
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 友元
类可以允许其他类或函数访问它私有成员,但需要先将其他类或函数声明为友元(friend),我们在类中添加一条以friend关键字开头的类或函数的声明语句即可。
声明友元类后,友元类中的所有成员都可以访问此类的成员,包括公开和私有和受保护成员。
定义格式:
class 类名{
// 虽然位置不限,但一般在类定义的开头位置集中声明友元。
// 声明友元类。
friend class 类名;
// 声明友元函数。
friend 返回类型 函数名(形参列表);
// 声明友元成员函数。
friend 返回类型 类名::函数名(形参列表);
private:
...
};
2
3
4
5
6
7
8
9
10
11
# 封装的作用
封装可以防止用户意外破坏封装对象的状态。另外我们可以随时修改封装的类的实现,且只要我们不改变类的接口,用户代码就无须改变。
# 类的其他特性
# 可变数据成员
如果我们希望在const成员函数内修改类中的某个数据成员,就需要使用mutable关键字将其定义为可变数据成员。这样在const成员函数中也能对其进行修改。
定义格式:
class 类名{
mutable 数据类型 成员名;
};
2
3
# 常量函数重载
如果一个函数有两个版本,一个是常量一个是非常量,则调用哪个版本是根据类的对象是否是const决定的。
例如:
class Book {
public:
void read() {
cout << "no const" << endl;
}
const void read() const {
cout << "const" << endl;
}
};
int main() {
Book nobook;
const Book conbook;
// 输出no const
nobook.read();
// 输出const
conbook.read();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 委托构造函数
委托构造函数可以调用同类下的另一个构造函数,以便复用构造函数的代码。
委托构造函数在调用其他构造函数时,需要传入所构造函数所需要的参数,以便可以正常进行构造。当一个构造函数委托另一个构造函数时,受委托的构造函数的初始值列表和函数体会被依次执行。
注意:
- 每个构造函数只能委托一个其他构造函数,不能委托多个。
- 并且构造函数委托了其他构造函数后,就不能有其他初始化表达式了。
定义格式:
class 类名{
// 定义构造函数
构造函数名(形参列表A){}
// 定义委托构造函数,需要传入所委托构造函数所需的实参
构造函数名(形参列表B) : 构造函数名(形参列表A的实参){}
}
2
3
4
5
6
例子:
class Book {
string name;
int page;
int number;
// 构造函数A
Book(string name) : name(name) {}
// 构造函数B,委托了构造函数A
Book(string name, int page) : Book(name) { this->page = page; }
// 构造函数C,委托了构造函数B
Book(string name, int page, int num) : Book(name, page) { this->number = num; }
}
2
3
4
5
6
7
8
9
10
11
# 聚合类
聚合类是可以被用户直接访问其成员的类,它具有特殊的初始化语法。当一个类满足以下条件,我们就可以说它是聚合类:
- 所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual函数。
它的缺点是要求所有成员都是public的,且添加或删除一个成员后,所有的初始化语句都需要更新。
例如:
struct Book{
string name;
int page;
int number;
}
int main(){
// 定义聚合类对象并初始化,初始值会按初始化值列表顺序和成员定义顺序进行对应,按顺序给成员来进行初始化。没传到值的成员则会进行默认初始化。
Book mybook{ "Hello", 200 };
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 字面值常量类
字面值常量类的对象可以在编译时被求值,以支持更加灵活高效的代码。
当一个类满足以下条件,我们就可以说它是字面值常量类:
- 数据成员必须是字面值类型。
- 类必须至少含有一个constexpr构造函数。
- 如果数据成员含有类内初始值,则初始值必须是一条常量表达式。如果成员属于某个自定义类,则初始值必须使用成员所属类的constexpr构造函数。
- 类必须使用默认定义的析构函数,负责销毁类的对象。
constexpr构造函数体一般为空,否则它必须符合常量函数的要求,即只能有一条返回语句。
例如:
class Book {
public:
constexpr Book(int num) : number(num) {}
private:
int number;
}
2
3
4
5
6
# 类的静态成员
如果我们希望类中某个成员能被类的所有对象共用,并且其值的更新能够影响类的所有对象,此时就可以使用static关键字来定义静态成员。
静态成员独立于对象之外,对象中不存储任何与静态成员有关的数据。静态函数成员也一样,它不与任何对象绑定在一起,因此不能使用this指针,也不能被声明成const的。
静态成员的优势:
- 使用静态数据或函数无需额外的进行对象构造操作,可以提高代码的执行效率。
- 静态成员函数可以无需实例化对象即可调用。
- 静态成员函数不能访问类内其他的非静态数据成员和非静态成员函数。
例如:
class Account {
public:
// 定义一个静态成员函数,成员函数可以不通过作用域运算符直接访问静态成员
static int privateStaticInt() { return nconst_private_static_int; }
// 声明一个公共的非const静态数据成员
static int nconst_static_int;
// const静态数据成员可以在类内部进行定义
static const int const_static_int = 10;
private:
// 声明一个私有的非const静态数据成员
static int nconst_private_static_int;
};
// 定义静态数据成员,需要在类外进行定义
// 如果是非const静态成员(无论公共还是私有),需要在所有类与函数外用如下方式定义
int Account::nconst_private_static_int = 20;
int Account::nconst_static_int = 5;
int main() {
// 访问类静态函数与成员
cout << Account::privateStaticInt() << endl;
cout << Account::const_static_int << endl;
cout << Account::nconst_static_int << endl;
return 0;
}
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