C++ Primer:Sec 7
Sec7 类
-
类的基本思想是:
- 数据抽象 data abstraction
依赖接口 (interface) 和 实现 (implementation) 分离的编程技术 - 封装 encapsulation
封装实现了类的接口和实现的分离
类想要实现数据抽象和封装,首席按需要定义一个抽象数据类型 (abstract data type)
ADT - 数据抽象 data abstraction
7.1 定义抽象数据类型
7.1.1 / 7.1.2 关于Sales_data类的改进
-
定义成员函数
可以定义在类内和类外。但是类内一定要有声明- 定义在类内部的函数是隐式的inline函数
-
引入
this
成员函数通过一个名为this
额外隐式参数来访问调用它的哪个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this// 例子: total.isbn(); // 伪代码,用于说明调用成员函数的实际执行过程 Sales_data::isbn(&total)
在成员函数内部,可以直接使用调用该函数的对象的成员!无须通过成员访问运算符来做到这一点。
任何对类成员的直接访问都被看作this的隐式引用。
this的目的总是指向这个对象。所以this是一个常量指针! -
引入
const
成员函数std::string isbn() const {return bookNo; }
这里的const 的作用是修改隐式this指针的类型
默认情况下,this的类型是指向类类型非常量版本和常量指针。-
因为this是隐式的,所以在哪儿将this声明成指向常量的指针就成为我们必须要面对的问题:
C++的做法是允许把const关键字放在成员函数的参数列表之后。这时候,紧跟在参数列表后面的const代表this是一个指向常量的指针。这样使用const的成员函数被称为 (const member function)常量成员函数// 伪代码,说明隐式的this指针是如何使用的 // 下面的代码是非法的。因为不能显式定义自己的this指针 // 此处的this是一个指向常量的指针,因为isbn是一个常量成员 std::string Sales_data::isbn(const Sales_data *const this) { return this->isbn; }
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值
- 最后再来区分一下 const 的位置:
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如
const char * getname()
。 - 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如
char * getname() const
。
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如
- 最后再来区分一下 const 的位置:
-
-
类作用域和成员函数
double Sales_data::avg_price() const { if (units_sold) return revenue / units_sold; else return 0; }
-
定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs){ units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; // 返回调用该函数的对象 }
7.1.3 定义类相关的非成员函数
类常常需要辅助函数。但这些辅助函数并不属于类本身。
7.1.4 构造函数
constructor
-
默认构造函数 default constructor
- 如果存在类内的初始值,用它来初始化成员
- 否则,默认初始化该成员
当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
-
定义构造函数
struct Sales_data { Sales_data() = default; // 默认构造函数 Sales_data(const std::string &s, unsigned n, double p) : boookNo(s), units_sold(n), revenue(p*n) { } Sales_data(std::istream &is); ... }
:
后到函数体部分称为“构造函数初始值列表” (constructor initialize list) -
在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is) { read(is, *this); // read的作用是 }
我们在类的外部定义构造函数时,必须指名该构造函数是哪个类的成员。
7.1.5 拷贝、赋值、析构
当类需要分配类对象之外的资源时,合成的版本常常会失效。(但使用vector或者string的类可以避免分配和释放内存带来的复杂性)
(这章还没到详细讲自定义操作的时候)
7.2 访问控制与封装
-
访问说明符(access specifiers)
- public:
之后的成员在整个程序内可以被访问 - private:
之后的成员可以被类内的成员函数访问,但是不能被使用该类的代码访问
- public:
-
使用class或struct关键字
- 区别:
默认访问权限不一样。
类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式,如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的,相反,如果使用class关键字,则这些成员是private的- 建议:
出于统一编程风格的考虑,当我们希望定义的类所有成员是public,用struct,反之,希望成员是private的,使用class
- 建议:
- 区别:
7.2.1 友元(friend)
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
friend Sales_data add(const Sales_data&, const Sales_data&);
友元声明只能出现在类定义的内部!但是在类内出现的具体位置不限!
7.3 类的其他特性
-
用来定义类型的成员必须先定义后使用。
-
令成员作为内联函数
可以在类的内部声明为inline,也可以在类的外部用inline关键字修饰函数的定义
inline成员函数也应该跟相应的类,在同一个头文件中定义 -
重载成员函数
跟函数的重载类似 -
可变数据成员
mutable data member。
永远不会是const,即使它是const对象的成员。因此,一个const成员函数可以改变一个可变成员的值。 -
类数据成员的初始值
最好的方式就是把这个默认值声明成一个类内初始值class Window_mgr { private: // 这个Window_mgr追踪的Screen // 默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen std:vector<Screen> screens{Screen(24, 80, ' ')}; }
-
含有指针数据成员的类,一般不宜使用默认的拷贝和赋值操作。如果类的数据成员都是内置类型,则不受干扰
7.3.2 返回 *this
的成员函数
-
从const成员函数返回*this
注意,可能会不能把返回const 的引用的函数,嵌入到一组动作的序列中去
例如:如果disply是一个返回const的引用
则下面这个代码是错误的:myScreen.display(cout).set('*');
-
基于const的重载
通过区分成员函数是否为const,我们可以对其进行重载。不知道重新看看就行
当我们在某个对象上调用display时,该对象是否是const决定了应该调用display的哪个版本
7.3.4 友元再探
-
类之间的友元关系:
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。 -
令成员函数作为友元:
当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类friend void Window_mgr::clear(ScreenIndex);
- 设计的流程:
- 首先定义Window_mgr类,其中声明clear函数,但不能定义它。在clear使用Screen的成员之前必须先声明Screen
- 接下来定义Screen,包括对于clear的友元声明
- 最后定义clear,此时它才可以使用Screen的成员
- 设计的流程:
-
函数重载及其友元
需要逐个声明友元 -
友元声明及其作用域
struct X { friend void f() { /* 友元函数可以定义在类的内部 */} X() { f(); } // 错误,f还没有被声明 void g(); void h(); }; void X::g() {return f();} // 错误,f还没声明 void f(); void X::h(){return f();} // 正确
友元声明的作用,仅仅是影响访问权限!并非普通意义上的声明
7.4 类的作用域
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s)
{
...
}
返回类型使用的名字都在类的作用域之外,所以要声明返回类型在哪个作用域!
7.4.1 名字查找与类的作用域
-
name lookup(名字查找)
-
过程:
- 在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明
- 如果没找到,继续查找外层作用域
- 如果最终没有找到匹配的声明,则程序报错
-
类的定义分两步处理
- 首先,编译成员的声明
- 直到类全部可见后才编译函数体
-
注意:
编译器处理完类中的全部声明后才会处理成员函数的定义
-
-
类型名要特殊处理
在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。using Money = double; class Account { public: Money balance() {return bal;} private: typedef double Money; // 错误,不能重新定义money Money bal; ... }
-
成员定义的普通块作用域的名字查找
-
类作用域之后,在外围的作用域中查找
void Screen::dummy_fcn(pos height){ cursor = width * ::height; // 这个height是全局的height }
7.5 构造函数再探
7.5.1 构造函数初始值列表
-
遇到引用,const等,构造函数的初始值有时必不可少
建议养成使用构造函数初始值的习惯。 -
成员初始化的顺序:
成员初始化的顺序,与他们类定义中出现的顺序一致!与构造函数初始值列表中的顺序无关!所以最好他们的顺序一致。
-
默认实参和构造函数
如果一个构造函数为所有参数都提供了默认实参,则它实际上就定义了默认构造函数。
此时,=default类型的默认构造函数应该删除!Sales_data(std::istream &is = std::cin) {is >> *this}; 此时该函数具有了默认构造函数的作用!所以应该删去原来声明的默认构造函数=default,不然会有
7.5.2 委托构造函数
delegating constructor
class Sales_data {
public:
Sales_data(std::string s, unsigned cnt, double price) :
bookNo(s), units_sold(cnt), revenue(cnt*price) { } // 若函数体有内容,则先执行函数体中的代码,再将控制权交还给委托者的函数体
// 其余构造函数全都委托给另一个构造函数
Sales_data() : Sales_data("",0,0) { } // 默认构造函数
Sales_data(std::string s) : Sales_data(s, 0, 0) {}
Sales_data(std::istream &is) : Sales_data() { read(is, *this);}
}
7.5.4 默认构造函数的作用
-
使用默认构造函数
Sales_data obj(); // 正确。定义了一个函数并非对象 if(obj.isbn() == Primer_5th_ed.isbn()); // 错误,obj是一个函数 Sales_data obj; // 正确,定义了一个对象,且使用默认初始化
-
抑制构造函数的隐式转换
可以通过将构造函数声明为explicit
加以阻止注意:编译器只会自动地执行一步类型转换
item.combine(s); // s是string类型 item.combine("saswdsaw"); // 错误,要两步类型转换 explicit Sales_data(const std::string &s) : bookNo(s) { } item.combine(s); // 错误,需要explicit匹配
-
explicit构造函数只能用于直接初始化
Sales_data item1(null_book); // 正确,直接初始化 Sales_data item2 = null_book; // 错误,不能将explicit构造函数用于拷贝形式的初始化过程
-
为转换显示地使用构造函数
item.combine(Sales_data(null_book)); item.combine(static_cast<Sales_data>(cin));
7.5.5 聚合类
aggregate class 聚合类可以使得用户直接访问其成员,并且具有特殊的初始化语法形式。
当一个类为:
- 所有成员都是public时
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
struct Data{
int ival;
string s;
}
7.5.6 字面值常量类
-
定义:字面值常量类
- 数据成员必须是字面值类型
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式。或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
-
constexper构造函数
虽然构造函数不能是const,但是字面值常量类的构造函数可以是constexpr函数
7.6 类的静态成员
static
静态成员的类型可以是常量、引用、指针、类类型等
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
静态成员函数页不予任何对象绑定在一起,不包含this指针!所以静态成员函数不能声明为const。而且不能使用this指针。
-
使用类的静态成员
跟其他的没区别。 -
定义静态成员
内外都行
但在外部定义静态成员时,不能重复static关键字。该关键字只能出现在类内部的声明语句中必须在外部定义和初始化每个静态成员。一个静态数据成员只能定义一次
要想确保对象之定义一次,最好的方法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中 -
静态成员的类内初始化
通常情况不能再类内部初始化。然而可以为静态成员提供const整数类型的类内初始值。不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。static constexpr int period = 30;
即使一个常量静态数据成员在类内部被初始化了。通常情况下页应该在类的外部定义一下该成员
-
静态成员可以是不完全类型。
还可以使用静态成员作为默认实参
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具