C++类
-
类的基本思想是数据抽象和封装;
-
数据抽象是一种依赖于接口和实现分离的编程技术;
- 类的接口包括用户所能执行的操作;
- 类的实现则包括类的成员、负责接口实现的函数以及定义类所需要的各种私有函数。
定义抽象数据类型
-
成员函数定义
-
定义和声明成员函数的方式与普通函数类似,成员函数的声明必须在类的内部,但它的定义既可以在类的内部,也可以在类的外部。定义在类内部的函数是隐式的inline函数。
struct Sales_data { string isbn() const { return bookNo; } Sales_data &combine(const Sales_data&); double avg_price() const; string bookNo; unsigned units_sold = 0; double revenue = 0.0; };
-
-
this
-
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符;
-
在函数中任何对类成员的直接访问都被看做this的隐式引用,即隐式地使用this指向的成员,就像写了this->bookNo;
-
任何自定义名为this的参数或变量都是非法的,且不允许改变this中保存的地址。
-
-
const成员
- 紧跟在参数列表后的const关键字表示this是一个指向常量的指针,这个成员函数被称作常量成员函数;
- 常量对象以及常量对象的引用或指针只能调用常量成员函数;
- 普通对象可以调用普通成员函数和const成员函数。
-
定义类相关的非成员函数
- 类的作者需要定义一些辅助函数,尽管这些函数定义的操作从概念上说属于类的接口的组成部分,但它们实际上不属于类;
- 通常把函数的声明和定义分离开;
- 非成员函数的声明一般应与类声明(而非定义)在同一个头文件中。
-
构造函数
-
作用:初始化类对象的数据成员;
-
类可以包含多个构造函数,不同的构造函数之间必须在参数数量或参数类型上有所区别;
-
构造函数不能被声明成const;
-
当类没有显式地定义构造函数,编译器会为隐式地定义一个默认构造函数;
-
定义构造函数
struct Sales_data { // 新增的构造函数 Sales_data() = default; Sales_data(const string &s) : bookNo(s) { } Sales_data(const string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { } Sales_data(istream &); // 之前的成员 ... };
-
构造函数初始化列表:负责为新创建的对象的一个或几个数据成员赋初值;
-
-
拷贝、赋值和析构
访问控制与封装
-
访问说明符:加强类的封装性;
- public: 在整个程序内可被访问,public成员定义类的接口;
- private:可以被类的成员函数访问,private部分封装了(即隐藏了)类的实现细节。
- 使用struct: 定义在第一个访问说明符之前的成员是public的;
- 使用class: 定义在第一个访问说明符之前的成员是private的;
-
友元
- 类可以允许其他类或者函数访问它的非公有成员, 方法是令其他类或者函数成为它的友员, 使用friend关键字;
- 一般来说,最好在类开始或结束之前的位置集中声明友元;
- 通常把友元的声明与类本身放置在同一个头文件中。
类的其他特性
-
在类中自定义某种类型的别名
typedef std::string::size_type pos; using pos = std::string::size_type pos;//以上二者等价
-
令成员作为内联函数:inline关键字
-
可变数据成员:const成员函数不能修改普通的数据成员,但可修改可变数据成员mutable;
-
即使两个类的成员列表完全一致,他们也是不同的类型;
-
类的声明
- 可以仅声明(称为前向声明)类而暂时不定义它(此时该类是一个不完全类型);
- 对一个类来说,创建它的对象或使用引用或指针访问其成员前必须被定义过;
- 类允许包含指向它自身类型的引用或指针。
类的作用域
- 在类的作用域外,普通数据和函数成员只能由对象、引用或指针使用成员访问运算符(.和->)来访问,通过作用域运算符(::)访问类类型成员;
- 作用域和定义在类外部的成员
- 当成员函数定义在类外,类名之后的参数列表和函数体都在类的作用域中;
- 当成员函数定义在类外,返回类型中使用的名字都位于类的作用域外。
- 名字查找与类的作用域
- 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明;
如果没找到,继续查找外层作用域;
如果最终没有找到匹配的声明,则程序报错。 - 编译器处理完类中的全部声明后才会处理成员函数的定义,所以成员函数能使用类中定义的任何名字。
- 在类中不能重新定义外层作用域中代表一种类型的名字;
- 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明;
构造函数再探
-
构造函数初始化列表
- 如果成员是const、引用或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值;
- 成员的初始化顺序与它们在类定义中的出现顺序一致;
-
委托构造函数
-
委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程:
std::istream &read(std::istream &is, Sales_data &item); class Sales_data { public: Sales_data(string s, unsigned cnt, double rev) : bookNo(s), units_sold(cnt), revenue(cnt * rev) {} Sales_data() : Sales_data("", 0, 0) {}、//默认构造函数 Sales_data(string s) : Sales_data(s, 0, 0) {} Sales_data(std::istream &is) : Sales_data() { read(is, *this); } };
-
-
默认构造函数
- 当对象被默认初始化或值初始化时自动执行默认构造函数;
- 若定义其它构造函数,那么最好提供一个默认构造函数。
-
隐式的类类型转换
-
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数;
-
编译器只会自动地执行一步类类型定义的转换,如果同时提出多个转换请求,这些请求将被拒绝:
//下列代码错误:需要用户定义的两种转换: //(1)把“9-999-99999-9”转换成string //(2)再把这个(临时的)string转换成Sales_data item.combine("9-999-99999-9"); //下列代码正确: //显示地转换成string,隐式地转换成Sales_data item.combine(string("9-999-99999-9")); //隐式地转换成sting,显式地转换成Sales_data item.combine(Sales_data("9-999-99999-9"));
-
explicit构造函数:通过将构造函数声明为explicit能抑制构造函数定义的隐式转换:
class Sales_data { public: explicit Sales_data(const std::string &s) : bookNo(s) { } // 不能再将string类型转化成Sales_data类型 private: string bookNo; };
-
explicit只对一个实参的构造函数有效;
-
explicit构造函数只能用于直接初始化;
Sales_data item1(null_book); // 正确,直接初始化 Sales_data item2 = nullbook; // 错误,但当构造函数不是explicit时可以这样
-
可以显式地强制转换static_cast<Sales_data>来使用这样的构造函数。
-
-
-
聚合类
-
用户可以直接访问其成员,并且有特殊的初始化语法:所有成员都是public;没有定义任何构造函数;没有类内初始值;没有基类和virtual函数;
-
聚合类可用花括号括起来的成员初始值列表初始化:
Sales_data item = {"978-0590353403", 25, 15.99};
-
-
字面值常量类
- 数据成员都是字面值类型的聚合类属于字面值常量类;
- 非聚合类但满足条件的类也是字面值常量类:数据成员都是字面值类型;类必须至少含有一个constexpr构造函数;…;
- constexpr构造函数的函数体一般为空,必须初始化所有数据成员,初始值或使用constexpr构造函数,或是一条常量表达式。
类的静态成员
-
静态成员:
- 类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联;
- 静态成员包含静态成员函数和静态成员变量;
- 无论创建多少个类的对象,静态成员都只有一个副本。
-
声明静态成员:在成员的声明之前加上关键字static;
- 静态成员函数不包含this指针,即不能在static函数体内使用this指针。
-
使用类的静态成员
- 作用域运算符直接访问静态成员;
- 使用类的对象、引用或者指针来访问静态成员;
- 类的成员函数可直接使用静态成员。
-
定义静态成员
- 当指向类外部的静态成员时,必须指明成员所属的类名;
- 静态成员函数既可以在类的内部也可以在类的外部定义;
- 在外部定义静态成员函数时,不能重复static,static关键字只出现在类内部的声明语句中;
- 静态成员变量只能定义一次,一旦被定义,就会一直存在于程序的整个生命周期中。
- 通常在类的外部定义和初始化每一个静态成员变量。
-
可以使用静态成员作为默认参数,而非静态成员则不可以(因为它的值本身属于对象的一部分)。