Fork me on GitHub

C++ Primer学习笔记:(七)类

类是数据的抽象(data abstraction)和封装(encapsulation)。数据抽象是一种将接口(interface)和实现(implemention)分离的设计技术。接口是指用户可以对类使用的操作集。实现包括类的数据成员和接口函数体。封装使得类的使用者不必关注类内部是如何实现的,因为这些是类的设计者需要关注的。

定义抽象数据类型

类的定义和声明

类由类成员组成。类成员包括属性,字段,成员函数,构造函数,析构函数等组成。

类设计应该遵从抽象封装性。

类抽象性指对于类的使用者来说只需知道类接口即可使用类功能。类的具体实现由设计者负责。即使某个功能发生了变更但由于使用者是以接口方式调用类所以用户代码无需做任何修改。

类封装性指类用户只需知道类的功能无需了解具体实现。实现代码对用户来说不可见。

C++类没有访问级别限制,定义类时不能用publicprivate 做修饰。类成员有访问级别,可以定义 public protect private

this指针

每一个类的内部都有一个隐含的this指针,该参数是由系统负责维护。它的类型是CLASSTYPE *const this;,即指向某个类的const指针。所以this指针在初始化以后就不能改变。系统使用this指针来指明函数使用的是哪个实例的数据成员。

在调用成员函数时,系统会自动传递类实例的地址给this指针:

1
2
CLASSTYPE exm;
exm.func();

可以将该函数调用理解为:CLASSTYPE::func(&exm);

const成员函数

在调用成员函数时,会传递类实例的地址给this指针。如果该实例是const对象,那么非const指针是无法指向const对象的。可以在函数参数列表后加上const来表明是const成员函数。

因此,this也是指向常量的指针。

类的作用域与成员函数

编译器分两步处理类:

  • 首先编译成员的声明,
  • 然后编译成员函数体。

因此,成员函数体可以随意使用类中的其他成员。(不管定义先后)

在类的外部定义成员函数

1
2
3
4
5
6
double Sales_data::avg_price() const{
if(units_old)
return revenue/units_old;
else
return 0;
}

定义一个返回this对象的函数

1
2
3
4
5
Sales_data & Sales_data::combine(const Sales_data &rhs) const{
units_old += rhs.units_old; //把rhs的成员加到this对象上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}

因此,执行total.combine(trans)语句时,是更新了变量total的值。

引用类型返回左值。return *this;解引用this指针获得执行该函数的对象,也就是返回total的引用。

定义类相关的非成员函数(7.1.3,P234)

类的辅助函数,比如add,read和print等。概念上属于类的接口的组成部分,但实际上不属于类本身。

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中。

构造函数(7.1.4,P235)

构造函数(constructor)是特殊的成员函数。在类对象定义时被调用。不能通过定义的类对象调用构造函数,构造函数可以定义多个或者说构造函数允许重载。

如果没有定义任何构造函数,系统就会给类分配一个无参的默认构造函数(default constructor),类只要定义了一个构造函数,编译器也不会再生成默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。

定义类对象时不能写成 Sales_item myobj();,编译器会理解成:一个返回 Sales_item 类型叫 myobj的函数声明。 正确写法是去掉后面的括号。

构造函数不允许定义成 const,这样定义会产生语法错误: Sales_item() const {};

构造函数在执行时会做类数据成员的初始化工作。从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。

构造函数初始值列表

1
2
3
4
5
6
7
8
9
10
11
12
13
Sales_data{
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) {}
Sales_data(const std::string &s, unsighed n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
//之前已有的其他成员
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_old = 0;
double revenue = 0.0;
}

上述代码中,= default要求编译器生成默认构造函数。
Sales_data(const std::string &s, unsighed n, double p): bookNo(s), units_sold(n), revenue(p*n) {}中的冒号和花括号之间的部分叫做构造函数初始值列表(constractor initialize list)

如果编译器不支持类内初始值,那么所有构造函数都应该显式地初始化每个内置类型的成员。

当然也可以在类外定义构造函数。

拷贝、赋值和析构(7.1.5,P239)

尽管编译器能为我们合成拷贝、赋值和析构的操作,但某些情况下可能无法正常工作。如分配和管理动态内存的类(13.1.4, P447)

访问控制与封装(7.2,P240)

访问说明符(access specifiers):加强类的封装性:

  • public:在整个程序内可被访问;
  • private:可以被类的成员函数访问。

使用class和struct唯一的区别就是默认的访问权限
struct默认是publicclass默认是private

友元(7.2.1,P241)

友元(friend):friend关键字,允许其他类或者函数访问类的私有成员。

类的其他特性(7.3,P243)

可变数据成员(mutable data member):永远不会是const,即使是const对象的成员。

类内初始值:必须以符号=或者花括号。

返回*this的成员函数(7.3.2,P246)

返回值是调用对象的引用,返回引用的函数是左值的,意味着这些函数返回对象本身。

如果返回类型不是引用,则返回的是*this的副本。

类的作用域(7.4,P253)

编译器处理完全部声明后,才会处理成员函数的定义。

构造函数再探(7.5,P257)

成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

委托构造函数(7.5.2,P261)

委托构造函数(delegating constructor):使用使用所属类的其他构造函数执行自己的初始化过程。成员初始值列表唯一,是类名本身。

explicit:抑制构造函数定义的隐式转换,只对一个实参的构造函数有效。且只能在类内声明的时候采用explicit关键字。

字面值常量类:constexpr

类的静态成员(7.6,P269)

一般来说,我们不能在类内初始化静态成员。相反,必须在类的外部定义和初始化每个静态成员。

定义并且初始化一个静态成员:

1
double Account::interestRate = initRate();

定义静态数据成员的方式和在类外定义成员函数差不多。

只有字面值常量类型constexpr的静态成员可以在类内,且初始值必须是常量表达式。

如果类内部声明提供了初始值,则外部定义时不能提供初始值。

静态数据成员可以是不完全类型,如类类型;非静态成员只能是类的指针或者引用。

术语表

class 关键字: class keyword
构造函数: constructor
构造函数初始值列表: constructor initializer list
类的作用域: class scope
委托构造函数: delegating constructor
显示构造函数: explicit constructor
接口:interface
数据抽象:data abstraction
可变数据成员: mutable data member
友元: friend

参考

  1. C++ Primer 中文第五版
  2. C++primer第五版第七章学习笔记
------ 本文结束感谢您的阅读 ------
坚持原创技术分享,您的支持将鼓励我继续创作!