类
1. 类的特性
1.1 类类型
概述
每个类定义了唯一的类型,跟他们的内部成员没有关系。
可以把类名作为类型的名称使用,直接指向类类型;也可以将类名跟在关键字class
或struct
后面
1 | class A{}; |
类的声明
类可以声明但不定义,这种声明有时也成为前向声明。类声明向程序中引入了类名字并且指明它是一种类类型
1 | class A; |
声明之后定义之前的类类型是不完全类型,因为不知道它所包含的成员
不完全类型只能用于以下情况
- 定义指向这种类型的指针或引用
- 声明(但不能定义)以不完全类型为参数或者返回类型的函数
- 静态成员
1.2 this指针
基本原理
1 |
|
上述例子中,对象obj
调用test
函数。当我们调用成员函数时,实际上实在替相应的对象调用它。上述调用中,test
函数返回a
变量时,实际上返回的是obj.a
。
成员函数通过一个名为this
的隐式的参数来获取调用它的对象,当使用对象调用成员函数时,会用该对象的地址初始化this
指针,并在类内部隐式的使用this
指针访问类内成员
this
形参是隐式定义的,并且是一个常量指针(指针本身是常量)。可以按照如下方式理解this
及函数调用
1 | A*const this; //A类对应的this指针,常量指针 |
const成员函数
默认情况下this
为常量指针,没有底层const
资格,因此不能把this
绑定到一个常量对象上(具有底层const
资格),这使得常量对象无法调用对应类中的成员函数。此时便可以使用const
成员函数
const
成员函数可以将this
指针的隐式定义从原来的A*const this
修改为const A*const this
,即为将this
声明为常量指针和常量指针。当成员函数内不改变this
所指向的对象时,就可以使用const
成员函数
const
成员函数的格式即为将const
放置在函数的参数列表之后,const
成员函数又称为常量成员函数
1 | class A{ |
返回this的函数
成员函数可以以引用或者指针的形式返回this
所指的对象或者this
本身
1 | //返回引用 |
1.3 成员函数和内联函数
定义在类内的成员函数是自动inline
的(注意是定义,不是声明)
可以显式的在类的内部使用inline
修饰函数声明或者在类的外部使用inline
修饰函数定义来将成员函数显式的设置为内联函数,当然也可以同时。最好在函数定义处说明inline
1 | class A{ |
inline
成员函数必须与相应的类定义在同一个头文件中,因为要进行展开
1.4 可变数据成员
在任何情况下都可修改的类数据成员,永远不会是const
,即使是在const
成员函数、const
对象等情况下。
使用mutable
关键字将数据成员声明为可变数据成员
1 | class A{ |
1.5 类内初始值
C++11
标准规定,可以为类内数据成员提供一个类内初始值,用于初始化
类内初始值或者放在花括号里,或者放在等号右边,不能使用圆括号
1 | class A{ |
2. 特殊成员函数
2.1 构造函数
构造函数用于类对象的初始化,在对象被创建时会自动被调用执行。必须是public
访问权限
构造函数具有以下基本特点
- 函数名和类名相同
- 没有返回类型和返回值
- 一个类可以包含多个构造函数,类似普通函数的重载
- 不能被声明为
const
成员函数,因为对象构造过程中可修改
默认构造函数
没有任何参数或者所有参数都有默认值的构造函数称为默认构造函数,默认构造函数用于控制类的默认初始化过程
编译器会为没有显式定义构造函数的类隐式的定义一个默认构造函数,称为合成的默认构造函数
合成的默认构造函数按照如下规则初始化类数据成员
- 如果类内定义数据成员时给出了初始值,则使用此初始值进行初始化
- 否则,对该成员执行默认初始化
默认构造函数(包括初始值列表)中没有初始化的成员也按照上述方式进行初始化
注意
-
类中必须有一个默认构造函数(包括合成的默认构造函数)
-
只有类没有声明任何构造函数时,编译器才会自动生成默认构造函数
-
含有内置类型或者复合类型成员的类应该在类的内部初始化这些成员(类内初始值)或者自定义构造函数。因为此时合成的默认构造函数如果使用默认初始化会导致这些成员的值未定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class A{
private:
int a;
int b;
public:
void print(){
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
};
int main(){
A obj;
obj.print();
return 0;
} -
当类内包含其他类类型的成员时,无法生成默认的构造函数,此时必须显式提供构造函数
默认构造函数的作用
默认构造函数在对象被默认初始化和值初始化时被自动执行
默认初始化的发生情况
-
块作用域内定义非静态变量和数组时没有给出初始值
1
2class A{};
A item1; //调用A的默认构造函数 -
类本身含有类类型成员且使用合成的默认构造函数
1
2
3
4class A{};
class B{
A item1; //B使用合成的默认构造函数时会调用A的默认构造函数
}; -
类类型成员没有在构造函数初始值列表中显式的初始化
1
2
3
4
5class A{};
class B{
A item1;
B(){} //B调用构造函数时会调用A的默认构造函数
}
值初始化的发生情况
-
数组初始化的过程中提供的初始值少于数组的大小
1
2
3
4
5
6
7
8
9
10class A{
public:
A(){
cout << "默认构造函数" << endl;
}
};
int main(){
A a[2] = {}; //执行两次默认构造函数
return 0;
} -
不使用初始值定义局部静态变量
1
2
3
4int count(){
static int num; //num=0,内置类型值初始化为0
return ++num;
}1
2
3
4
5
6
7
8
9
10class A{
public:
A(){
cout << "默认构造函数" << endl;
}
};
int main(){
static A a; //调用默认构造函数进行初始化
return 0;
} -
书写形如
T()
的表达式显式的请求值初始化(容器初始化只提供数量而没有提供初始值?)
构造函数初始值列表
1 | //类内 |
使用冒号开始构造函数初始值列表,每个名字后紧跟括号括起来的(或者花括号)成员初始值,不同成员的初始化通过逗号分隔开
初始化和赋值
1 | //初始化 |
1 | //初始化 |
上述两种方式都为变量的初始化操作,下面这种为变量的赋值操作。构造函数体执行之前会对没有进行初始化(没有在定义时初始化也没有在构造函数初始值列表中初始化)的成员执行默认初始化操作
1 | //赋值 |
即类内定义变量时给出初始值和使用构造函数初始值列表都是进行初始化,而在构造函数函数体中的操作则为赋值
对于const类型、引用类型和未提供默认构造函数的类类型等,初始化和赋值必须严格区分,否则会引发错误
1 | class A{ |
成员初始化顺序
构造函数初始值列表的优先级高于定义时初始化,如果一个变量在初始值列表和定义时进行了初始化,则以初始值列表为准
构造函数初始值列表只说明初始化成员的值,而不影响初始化的顺序。成员的初始化顺序由其在类中定义的顺序决定,先定义的先被初始化,以此类推
1 | class A{ |
委托构造函数
构造函数可以使用同一类中的其他构造函数执行自己的初始化过程,即把它自己的一些职责委托给了其他构造函数。这种构造函数即为委托构造函数
委托构造函数的语法类似构造函数初始值列表,在函数参数列表之后使用:
后接想要委托的构造函数,其后再接函数体。委托构造函数会在函数体执行之前执行受委托的构造函数,然后再执行自己的函数体
1 | class A{ |
隐式的类类型转换
如果类的构造函数只接受一个实参,则实际上定义了一个从实参类型转换为此类类型的转换机制,此机制就自动调用此构造函数完成。
隐式类类型转换的触发条件为
- 类含有只接收一个实参(假设为
T
类型)的构造函数(必须接收一个实参,可以有其他默认实参) - 在需要使用类类型变量的地方,直接使用
T
类型变量替代 - 只能进行一步转换,即必须是
T
类型,而不能是其他可以转换为T
类型的类型
1 | class A{ |
进行隐式类类型转换时,编译器自动调用相应的构造函数创建临时的类类型对象传入当前语句或函数。当前语句或函数使用完成后,临时对象会被销毁
可以通过将构造函数声明为explicit
来阻止隐式的类类型转换
1 | class A{ |
explicit
关键字只对接收一个实参(除默认实参外)的构造函数有用,并且只能用于类内的构造函数声明中,在类外的构造函数定义时不能使用
使用explicit
关键字修饰的构造函数只能用于直接初始化,不能用于拷贝形式的初始化过程
1 | //使用上一代码中的定义 |
使用explicit
关键字修饰的构造函数不会被用于隐式的类型转换过程,但是还是可以显式的调用来强制转换类型
1 | A a; |
2.2 析构函数
2.3 拷贝构造函数
2.4 拷贝赋值构造函数
2.5 移动析构函数
2.6 移动赋值析构函数
3. 访问控制与封装
3.1 访问权限控制
C++
使用访问说明符加强类的封装性,一个类可包含多个访问说明符,一种访问说明符可出现多次
访问说明符:
public
:说明符后的成员在整个程序内可被访问,用于提供接口private
:说明符后的成员只能被所在类的成员函数访问,用于封装实现细节
1 |
|
类的关键字也会影响类的访问控制权限。类关键字有两个,struct
和class
。二者都可用于类的定义,且只有默认的访问权限不同,其余完全一致。这两个关键字所影响的访问权限为类中第一个访问说明符之前定义的成员。
struct
关键字定义的类,在定义第一个访问说明符之前的成员是public
的
class
关键字定义的类,在定义第一个访问说明符之前的成员是private
的
1 |
|
3.2 友元
类可以允许其他类或者函数访问它的私有成员,只需要将其他类或者函数使用friend
关键字设置为其友元。
友元声明必须在类的内部,并且位置不限,不受所在区域访问控制级别的约束和限制
友元声明最好在类定义开始或者结束的位置统一声明,这样代码更加简明
友元函数
将需要定义为友元函数的函数在要访问的类中加上friend
关键字重新声明即可
类外函数为友元函数
1 |
|
1 |
|
友元函数的声明只是指定函数的访问权限,而非真正的函数声明。相应友元函数的声明还需要在类外再进行一次,否则无法调用友元函数。通常将友元函数的类外声明和类本身放在同一个头文件中,防止无法调用的情况。
友元函数可以定义在友元声明的位置,但是友元声明不是函数声明,函数必须重新进行声明才能被正确调用
1 |
|
类内函数为友元函数
其他类的成员函数为友元函数
在声明时明确指出成员函数属于哪个类,注意依赖关系
A类声明B类中的成员函数print为A类的友元。此时A和B的声明与定义需要按照如下顺序
- 定义B类,并在其中声明print函数。如果函数要用到A类,则提前声明A类
- 定义A类,声明对于B类中的clear函数的友元关系
- 定义print函数,此时print函数能够访问A的私有成员
1 |
|
重载函数的友元
重载函数本质上是不同的函数,因此如果想要将一组重载函数声明为类的友元,则需要对每一个函数分别声明
友元类
将需要定义为友元类的类在要访问的类中加上friend
关键字重新声明即可。
友元类中的成员函数可以访问此类的所有成员
1 | class Sales_data{ |
友元关系无法传递,类A
有友元类B
,B
有友元类C
,则B
可以访问A
的所有成员,C
可以访问B
的所有成员,但是C
不能访问A
的非公有成员。
4. 类的作用域
每个类都有自己独立的作用域,在类外部访问类成员需要使用相应的运算符
对象、指针和引用访问类内成员使用.
或者->
两种成员访问运算符进行访问
类类型成员使用::
作用域运算符进行访问,类类型成员为类内定义的函数和类型别名
在类外部定义成员
即为在类外部使用::
访问类类型成员,并进行相关操作。注意,只能在类外部定义,相应的声明必须在类内部
1 | //在外部访问类内定义的函数和类型 |
1 | class A{ |
1 | class A{ |
4.1 名字查找
编译器对于类的定义分为两步处理
- 编译类内成员的声明
- 类内成员全部可见后编译函数体
因此,类中的任意成员函数可以调用类中定义的所有名字。但是注意,类内成员声明中使用的名字必须在使用前可见
类成员声明中的名字查找
对于类成员声明中的名字,编译器首先在类作用域内查找,如果找不到,则向类外层作用域中查找,以此类推,如果最终没有找到,则报错
1 |
|
在类中,如果一个类成员声明首先使用了外层作用域中的名字,且此名字为一种类型,则之后类中声明的同名类型不会覆盖同名外部名字,只会对其后的其他声明和定义起作用。注意此处可能产生的错误
1 | typedef string Money; |
最好将类型名的定义放置在类的开始处,防止可能产生的错误
类成员定义中的名字查找
类成员函数定义中使用的名字的查找过程如下
- 首先在成员函数内查找该名字的声明
- 成员函数内没有找到,则在类内进行查找,查找所有的类成员
- 如果类内也没有找到,则在类外(成员函数定义之前)的作用域中查找
1 | int a = 0; |
5. 特殊的类
5.1 聚合类
聚合类的用户可以直接访问其成员,并且具有特殊的初始化形式
聚合类需满足如下条件
- 所有成员的访问权限都是
public
- 没有定义任何构造函数
- 没有使用类内初始值
- 没有基类,没有
virtual
函数
1 | //典型的聚合类 |
聚合类的初始化形式比较特殊,除类通用的初始化方式,还可以使用花括号包含的成员列表进行初始化(显式初始化),类似数组的列表初始化过程,并且规则相同。初始化的过程中类成员声明顺序和初始值列表中的顺序相对应进行初始化;如果初始值列表中的元素数量少于类的成员数量,则剩余的成员执行值初始化;初始值列表中的成员个数不能多于类中的成员数量
1 | //初始化Data聚合类 |
聚合类的显式初始化的问题
- 修改类成员之后,所有初始化语句都要更新
- 将正确初始化每个对象的每个成员的责任交给了用户
5.2 字面值常量类
字面值常量类类型属于字面值类型,可以被声明为constexpr
的
字面值常量类有两种情况,符合任一种的都是字面值常量类
- 聚合类,且数据成员都是字面值类型
- 非聚合类,但满足下述要求
- 数据成员都是字面值类型
- 类必须至少含有一个
constexpr
构造函数 - 如果一个数据成员含有类内初始值
- 此成员为内置类型,则初始值必须为常量表达式
- 此成员为类类型,则初始值必须使用成员自己的
constexpr
构造函数
- 类必须使用析构函数的默认定义
constexpr
构造函数可以有如下三种形式
=default
形式- 删除函数的形式
- 既符合构造函数的形式(无返回语句)又符合
constexpr
函数的形式(只能有返回语句这一种可执行语句),综合来看,即函数体无任何语句。注意:可以有构造函数初始值列表
1 | class B{ |
6. 类的静态成员
类的静态成员只和类相关联,而不是与类对象保持关联。即类的所有对象共享此静态成员
6.1 声明静态成员
类静态成员可以是public
或者private
的,只要在声明时加上关键字static
即可。
类静态成员包括静态数据成员,静态函数成员,二者具有相似的性质,都被对象所共享,并且与类相绑定,而不是对象。因此静态成员函数不包含this
指针,所以静态成员函数不能声明为const
的,也不能直接访问类内的非静态成员(隐式采用this
指针访问)
1 | class A{ |
6.2 使用静态成员
静态成员的访问
类的静态成员有如下几种访问方式
-
作用域运算符
1
2int c = A::b;
A::test(); -
类的对象、引用或指针
1
2
3
4
5
6A t1;
A *t2 = &t1;
A &t3 = t1;
int a = t1.b;
t2->test();
t3.test(); -
直接访问,类内部函数和表达式可以直接访问静态成员
1
2
3
4
5
6
7
8
9
10class A{
public:
static int a;
static int test(){return 1;}
int b = a*2; //直接访问
int test2(){ //直接访问
test();
return a;
}
};
静态成员的特殊应用场景
静态成员可以是不完全类型,甚至是它所属的类类型。因为静态成员在类内只声明,不定义和初始化
1 | class A{ |
静态成员可以作为默认实参,静态成员在对象创建之前就可用,且存在于整个对象的生命周期中
1 | class A{ |
6.3 定义和初始化静态成员
静态成员函数
类的静态成员函数可以在类内定义,也可以类似其他成员函数在类外进行定义。在类外定义静态成员函数时不用重复static
关键字。
1 | class A{ |
静态数据成员
静态数据成员不属于任何一个类对象,因此也不应该使用构造函数初始化。一般在类的外部定义和初始化所有的静态数据成员。static
关键字同样只能用于类内数据成员的声明,而不能用于类外数据成员的定义和初始化
1 | class A{ |
类的静态成员也可以在类内声明和初始化,此时要求此成员必须是constexpr
的,并且初始值必须是常量表达式。在类内初始化的静态成员,通常也需要在类外进行定义
1 | class A{ |