文章目录
定义抽象数据类型
类的基本思想:
- 数据抽象:
- 依赖于接口和实现分离的编程技术;接口包括用户执行的操作,实现包括类的数据成员,接口实现函数,定义类的私有函数
- 封装: 实现了类的接口和实现的分离,封装隐藏了类的实现细节,用户只能通过接口使用,而无法访问实现细节
1.0 类的引入
struct 结构拟写一个类的实现
struct Sales_data
{
//新成员 :关于对象的操作 定义在类内的是inline函数
string isbn()const
{
return bookNo;
}
Sales_data& combine(const Sales_data& s);
double avg_price()const;
string bookNo; //isbn编号
unsigned units_sold = 0; //某本书的销量
double revenue = 0.0; //某本书总销售收入
};
//非成员接口函数
Sales_data add(const Sales_data& s1, const Sales_data& s2);
std::ostream& print(std::ostream& out, const Sales_data& s);
std::istream& read(std::istream& in, Sales_data& s);
-
this指针:引入 在调用成员函数isbn时,一个隐式的参数this来访问调用的对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化this,相当于 this=&对象,对任何类成员的直接访问都可以看做this的隐式引用,this是一个常量指针
-
const :把this声明为指向常量的指针,使用const的成员函数称为常量成员函数
-
在类内部声明的函数称为成员函数
-
成员函数要在类的外部定义
必须要与类内部声明匹配且成员函数的名字要包含所属的类名
//在类的外部定义成员函数 double Sales_data::avg_price() const { if (units_sold) { return revenue / units_sold; } else { return 0.0; } }
-
返回this对象的函数:引用做函数的返回类型,表明要返回一个可以修改的非常量对象,左值返回要返回引用类型 用*this 表示返回这个对象本身
//在类的外部定义成员函数 Sales_data& Sales_data::combine(const Sales_data & s) { units_sold += s.units_sold; revenue += s.revenue;//内置赋值运算符把他当作左值 return *this; }
-
定义类相关的非成员函数:在概念上来说属于类的接口组成部分,但是不属于类,目的是为类的实现提供一些额外的功能
//非成员接口函数 Sales_data add(const Sales_data& s1, const Sales_data& s2); std::ostream& print(std::ostream& out, const Sales_data& s); std::istream& read(std::istream& in, Sales_data& s);
1.1 构造函数
通过一个或者几个特殊的成员函数来对类的对象初始化的过程称为构造函数
-
构造函数没有返回类型,不能用const修饰,可以进行函数重载
-
默认构造函数
默认构造函数无需任何实参,如果没有显示定义构造函数,则隐式生成默认构造函数,又被称为合成的默认构造函数
可用类内初始化或者默认的初始化
如果定义了一个构造函数,应该再次定义一个默认构造函数,以便实现对类对象初始化的完全控制
//构造函数 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(std::istream&);
-
= default:表示默认行为,如果你需要默认行为,在函数后面加上 =default表示生成一个默认构造函数
-
构造函数初始化列表:为成员赋一个初始且唯一的值
-
在类外部定义构造函数:执行函数,进行用户输入赋值初始化
-
拷贝,赋值和析构
1.2 访问控制与封装
使用访问控制符,加强类的封装
public 和 private
-
struct 默认 public ;class默认 private
封装的优点
- 1.确保用户的代码不会无意间破坏封装对象的状态。
- 2.被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
1.3 友元函数
使得类的非公有成员可以被其他类或者函数访问,即把他们定义为友元类型,以friend标记开头
友元 不受区域及访问级别的控制
2.0 类的其他特性
-
2.1 类成员再探
-
重新定义类型成员如用using或者typedef取别名,通常定义在类开始的位置,以共有形式
-
成员函数默认为inline内联函数
-
重载成员函数
-
可变数据成员 : 通过mutable定义,它一定是可变的,不受const的限制,可以在任意时候修改他的值,即使在const的成员函数调用它的时候,它也可以修改
-
类数据成员的初始化
直接调用类成员的构造函数生成一个vector对象
class Window_mgr { private: vector<Screen> screen{ Screen(24,80,' ') };//调用Screen构造函数创建对象来初始化vector
-
-
2.2 返回*this 成员函数
- 返回*this即返回对象本身 因而用引用类型,如果不用引用,则返回的是一份副本,对副本的修改不会影响对象本身
Screen& Screen::set(char c) { contents[cursor] = c; return *this; }
-
从const成员函数返回*this,基于对const 的重载
this是一个指向常量的指针,this是const对象,则函数返回类型变为了const类型,在此后修改对象将会报错,简言之:为什么this报错? 因为const成员函数返回const常量,它把*this对象转换为了const Screen& ,把他变成了常量引用,因此不符合原始返回类型Screen&版本
将函数的实现放在私有成员里,在外部定义调用这个函数的接口,非常量版本返回非常量的普通引用;常量版本返回常量引用,根据对象是否是const来决定调用哪个版本
private: //私有函数实现 只负责显示数据 void do_display(ostream& out)const { out << contents; } public: //根据是否是const重载display函数 Screen& display(ostream& out) { do_display(out); return *this; } const Screen& display(ostream& out)const { do_display(out); return *this; }
核心:对于共有代码使用私有功能函数
-
2.3 将类作为友元
-
如另一个类需要访问一个类的私有成员,则需要将要访问的类定义为友元类
-
只令成员函数成员友元
这种情况下条件较多,
- 首先要定义Window_mgr类,并且声明clear函数(不定义:因为clear的实现需要Screen,此时还未定义 Screen类)
- 其次定义Screen类,包括clear友元声明
- 最后定义clear友元函数
但是我这个地方会报错,在Window_mgr的私有成员vector对象,显示Screen未定义,我已经在上面前向声明了 Screen,但是还是会显示Screen未定义,有没有大佬谁知道怎么回事
class Screen;
-
函数重载和友元
-
友元声明和作用域:就算在类内部定义该函数,也必须在类外相应位置提供声明使得函数可见,即在类中的友元声明不是普通意义上的声明
-
3.0 类的作用域
-
返回值为自定义的类型,需要指出其位于哪个作用域中
-
3.1 名字查找与类的作用域
-
用于类声明的名字查找
首先在类中查找有没有声明,如果没有找到,再转到外层作用域中查找
-
类型名不能重新定义,内层类中的声明不会覆盖外层声明,书中说类内部覆盖外部定义会报错,但是VS2022 没有报错啊
-
查找的顺序:成员函数参数,类作用域,外层作用域
-
4.0 构造函数再探
-
4.1 构造函数初始值列表
-
对于有const变量或者引用的私有成员,构造函数必须对其初始化,否则报错,但是用单一赋值的方式初始化可能会引发错误,必须要采用初始化列表的形式初始化
-
成员初始化的顺序:数值的初始化顺序无关紧要;但是如果是成员之间的赋值,必须要根据声明的顺序来赋值,否则i是个垃圾值
-
-
4.2 委托构造函数
使用类中其他的构造函数来执行它自己的初始化,或者说把它的职责委托给了其他构造函数,如果受委托的构造函数中有代码,会先执行代码在转回委托者函数体
-
4.3 隐式的类类型转换
隐式的类型转换只允许进行一步
-
可以执行隐式转换:通过string 自动创建了一个对象传递给combine,一个临时变量初始化
-
不可以执行隐式转换:先从“9999”转换为string,再从string转换为Sales_data,进行了两步,只允许进行一步类类型转换
-
隐式与显式相结合进行隐式转换
-
有了istream&的构造函数,甚至可以cin隐式转换
-
通过explicit来阻止进行隐式转换:只对一个实参的构造函数有效,因为多个实参的无法进行隐式转换,所以无需添加explicit,只能在类内声明时添加,在类外定义时不能重复添加,并且只要添加了explicit,则表示构造函数只能直接初始化(列表初始化),无法进行赋值(=)
尽量使用显式转换:
-
-
4.4 聚合类
用户可以直接访问其成员,特征:(普通结构体)
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类与虚函数
-
4.5 字面值常量类
-
数据成员都是字面值类型
-
类中至少有一个constexpr构造函数
-
如果一个成员具有类内初始值,则初始化必须是一条常量表达式;如果属于类类型,则初始化必须是constexpr构造函数
-
类要有自己的析构函数默认定义
constexpr构造函数: 可以声明=default,应该是一条空的函数体
//字面值常量类 class Debug { public: constexpr Debug(bool b = true) :hw(b), io(b), other(b) {} constexpr Debug(bool h,bool i,bool o) : hw(h), io(i), other(o) {} constexpr bool any() { return hw || io || other; } void set_io(bool b) { io = b; } void set_hw(bool b) { hw = b; } void set_other(bool b) { other = b; } private: bool hw; bool io; bool other; };
-
-
4.6 类的静态成员
- 静态成员与类关联在一起,对于不同的类对象,每个对象都包含两个数据成员:a和b,但只存在一个static数据成员c和d,静态对象不与任何对象绑定在一起,没有this指针,不能声明为const,也不能static函数体内使用this
class A { private: int a; int b; static int c; static int d; public: static int GetA()const; //错误语法 };
- 使用作用域解析运算符直接访问类的静态成员函数
int num=A::getA();