第七章:类
类的声明
类的类名定义了唯一的类名。
类可以声明与定义分离,仅声明时称为前向声明,这种声明之后定义之前产生的是不完全类型,这可以用来帮助定义指向这种类型的引用或指针。
直到类被定义后数据成员才能被声明为这种类型,在创建类的对象之前必须完成类的定义,否则编译器不知道该分配多少存储空间。
类的作用域
一个类就是一个作用域,在类的外部尤其需要注意使用作用域运算符来访问。在外部使用函数时,如果返回类型也是定义在类内部,我们需要同时对返回类型和函数名利用作用域运算符确定类。
名字查找
通常规则为
1、首先在名字所在的块中寻找声明语句,只考虑在名字的使用之前出现的声明
2、如果没找到则继续查找外层作用域
3、报错
-
用于类的成员函数
1、首先,编译成员的声明
2、类全部可见后才编译函数体
编译器处理完类中的全部声明后才会处理成员函数的定义,好处是成员函数体中可以使用使用类中定义的任何名字。
-
用于类成员声明
包括返回类型或者参数列表中的名字,都必须在使用前确保可见。其规则遵循通常规则
-
用于类型名
一般来说,内层作用域允许重新定义外层作用域的名字,即使该名字已经在内层作用域中使用过,然而在类中,如果成员使用了外层作用域中的某个名字,则类不能重新定义该名字。
隐式的类类型转换
如果构造函数只接受一个实参,实际上定义了转换为此类型的隐式转换机制,又称为转换构造函数。
尽量不要使用,可以通过将构造函数声明为explicit来阻止隐式类型转换,但explicit构造函数只能用于直接初始化,
成员函数
特性:声明必须在类的内部,定义则不必,允许重载。
在外部定义时需要使用作用域运算符来确定该函数位于哪一个类的作用域。
double Sales_data::avg_price() const {
...
}
常量成员函数
double avg_price() const://在参数列表后添加关键字 const
常量成员函数不能改变调用它的对象的内容。
常量对象及其引用或指针都只能调用常量成员函数。
this相关
成员函数通过一个 this 的额外参数来访问调用它的那个对象,当我们调用成员函数时,用请求该函数的对象地址初始化 this。this 的目的总是指向该对象,实际上它是一个常量指针。
返回**this*的成员函数
class person {
public:
int age = 1;
person add1(int a) {
age += a;
return *this;
}
person& add2(int a) {
age += a;
return *this;
}
};
person.add1(10).add1(10);//age被修改为21
person.add2(10).add2(10);//age被修改为11
this将指向调用该成员函数所属的对象,通过解引用符操作后即得到该对象。又由于返回值类型是引用,这些函数返回值返回的是对象本身。而如果返回类型不是引用,则返回的是临时副本。
一个常量成员函数如果以引用的形式返回**this*,那么它的返回类型显示是常量引用。
总之,这种情况下尤其需要注意函数的返回类型。
基于const的重载
关键在于连续调用函数时返回的是常量对象还是非常量对象
数据成员
可变数据成员
通过关键字mutable声明变量,这样的变量能够被常量成员函数进行修改。
初始值
类内初始值必须以符号=,或者花括号表示。
静态成员
这一类成员直接与类相关联,而不是与类的各个对象相关联,所有对象共享。使用关键字static, 可以是常量、引用、指针、类类型等。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
声明与定义
不能被声明成const,不能在函数体内使用this指针(无论是显式还是隐式)。
当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
一般来说,类的静态成员不能在类内部初始化,但是可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。
使用
-
使用作用域运算符直接访问静态成员
-
使用类的对象、引用或指针来访问
-
成员函数可以不通过作用域运算符
静态成员可以是不完全类型,可以做实参。
构造函数
类通过构造函数来初始化类对象的数据成员,当类的对象被创建,构造函数被自动执行。
构造函数名与类名相同,无返回类型,可以不显式定义,允许重载,不允许被声明为const。
默认的构造函数
如果我们未显式的定义一个构造函数,编译器会定义一个默认构造函数,它会按照如下规则初始化类的数据成员
- 首先使用类内初始值来初始化成员;
- 如果不存在类内初始值,默认初始化该成员
但需要注意,某些类不能依赖于默认的构造函数
- 如果我们定义了一些构造函数,除非定义一个默认构造函数
构造函数名() = default;
否则类将没有默认构造函数。这会导致如果我们使用没有全部初始化类成员的构造函数时,会出现未定义的行为。
- 默认构造函数可能执行错误的操作。比如定义在块中的内置类型或复合类型(数组、指针等等)的对象被默认初始化时,它们的值将会是未定义的
- 有时编译器不能为某些类合成默认构造函数。比如,如果类中包含一个其他类类型的成员且这个成员的的类型没有默认构造函数,编译器将无法初始化该成员。
构造函数初始值列表
class person {
public:
int age = 0;
int tall = 0;
int c = 0;
public:
person(int a, int b): age(a), tall(b) { }
};
使用构造函数的前两个参数来初始化成员age和tall。如果将变量c仅声明并尝试访问
int c ;
person p1 = person(1,2);
cout<<p1.c;//c++11成功,c++14开始会报错
cout<<p1.age<<p1.tall;//不报错
需要注意的是,初始化的顺序与成员声明的顺序保持一致,而与初始值列表中的顺序无关
总之确保所有变量在构造结束时被显式初始化,以防止出现未定义的行为。
最好令构造函数初始值的顺序和成员声明的顺序保持一致,尽量避免使用某些成员初始化其他成员
委托构造函数
为了避免有多个参数表不同但是逻辑相近的构造函数造成代码重复,引入委托构造函数。只需要在委托构造函数的初始化列表中调用目标构造函数即可,会先执行目标构造函数(先执行初始值列表再执行函数体),再执行委托构造函数。
注意不要构造委托环。
类的拷贝、赋值与析构
第13章进一步学习
访问控制与封装
public:在整个程序内可以被访问
private:可以被类的成员函数访问,但是不能被直接使用类的代码访问
class的默认访问权限为private,struct为public
友元
友元可以允许其他类或者函数访问它的非公有成员,友元声明只能出现在类定义的内部。
友元关系不具有传递性。
定义友元
friend 函数声明;//非成员函数作友元
friend 类声明;//类作友元
friend 返回类型 类名::成员函数名;//类的成员函数作友元
friend void class1::func();
在成员函数作友元时,假定我们想要的情况为令类c1中的成员函数func作为c2的友元函数
class c2{
friend void c1::func();
}
需要满足:
1、首先定义c1类,其中声明了func函数,但是不能定义它。这是因为在func使用c2的成员前必须声明c2。
2、接下来定义c2,其中包括对func的友元声明。
3、最后定义func
聚合类
聚合类允许用户直接访问其成员,并且具有特殊的初始化语法。
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,没有virtual函
可以使用花括号来进行成员初始值列表,但是初始值的顺序必须与声明的顺序保持一致,如果列表中的元素数量不够,则靠后的成员被值初始化。
缺点:
- 需要类的成员变量的访问属性都是public。
- 增加了类的使用者的负担,类的使用者要明确清晰如何初始化一个聚合类。
- 任何对成员变量的改动,都需要改动初始化语句
字面值常量类
面值常量类是一种数据成员都是字面值类型的聚合类。如果一个类符合以下要求,则它也是个字面值常量类:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个constexpr构造函数。
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类型,则初始值必须使用成员自己的constexpr构造函数