类的两项基本能力:
(1) 数据抽象,即定义数据成员和函数成员的能力。
(2) 封装,即保护类的成员不被随意访问的能力。通过将类的实现细节设为private,我们就能完成类的封装。类可以将其他类或者函数设为友元,这样它们就能访问类的非公有成员了。
1. 如何设计一个类,如何定义类及类的成员
(1)类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果使用class关键字,则这些成员是private的。
// 使用struct关键字定义类
struct 类名 {
// 访问说明符之前的行为或属性默认是public的
public:
//公有的行为或属性
private:
//私有的行为或属性
};
// 使用class关键字定义类
class 类名 {
// 访问说明符之前的行为或属性默认是private的
public:
//公有的行为或属性
private:
//私有的行为或属性
};
Note: 类定义结束后的那个分号不能省略。
(2)尽管所有成员函数都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定 const 属性。同时,类外部定义的成员名字必须包含它所属的类名。
class Sales_data {
// 友元声明,后面会讲
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
public:
// 构造函数,采用参数初始化表
Sales_data(): units_sold(0), revenue(0.0) {
}
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0.0) {
}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {
}
// 成员函数的声明
Sales_data(const std::string &book, const unsigned num,
const double sellp, const double salep);
Sales_data(std::istream &is);
// 类的内部定义成员函数
std::string isbn() const {
return bookNo;
}
Sales_data &combine(const Sales_data &rhs) {
units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}
double avg_price() const {
if (units_sold) {
return revenue/units_sold;
} else {
return 0;
}
}
private:
std::string bookNo; // 书籍编号,隐式初始化为空串
unsigned units_sold = 0; // 销售量,显式初始化为0
double sellingprice = 0.0; // 原始价格
double saleprice = 0.0; // 实售价格
double discount = 0.0; // 折扣
double revenue; // 收入
};
// 类的外部定义成员函数,在类的内部已经做过声明
Sales_data::Sales_data(std::istream &is) {
read(is, *this); // read函数的作用是从is中读取一条交易信息然后
// 存入this对象中
}
Sales_data::Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep) {
bookNo = book;
units_sold = num;
sellingprice = sellp;
saleprice = salep;
if (sellingprice == 0) {
discount = saleprice / sellingprice; // 计算实际折扣
}
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
std::istream &read(std::istream &is, Sales_data &item) {
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
<< " " << item.saleprice << " " << item.discount;
return os;
}
(3)令成员作为内联函数: 在类中,常有一些规模较小的函数适用于被声明成内联函数,定义在类内部的成员函数是自动 inline 的。在类外定义内联函数时,可以在类的内部把 inline 作为声明的一部分显式地声明成员函数,在类的外部用 inline 关键字修饰函数的定义:
inline
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
Note: 虽然在声明和定义的地方同时说明 inline 是合法的,但最好只在类外部定义的地方说明 inline,这样可以使类更容易理解。
内联函数作用: 内联函数在编译的时候将不进行函数调用,编译器将内联函数的代码粘贴在调用(形式上调用)处,可以提高效率。内联函数只能是代码很少很简单的函数,如果一个很大很复杂的函数即使被设为内联,编译器也将自动设置该函数为非内联。
(4)重载成员函数: 如果同一作用域的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。和非成员函数一样,成员函数也可以被重载,只要函数之间在参数的数量和/或类型上有所区别就行。
// 构造函数的重载
Sales_data(){}
Sales_data(std::istream &is){};
Sales_data(const std::string &s){}
Sales_data(const std::string &s, unsigned n, double p){}
Note: 不能通过函数的返回值类型不同来定义重载函数,只能通过形参列表的不同。
2. 类成员的访问权限
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能操作该类中的成员。
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
Note: C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有公有私有之分。
3. 构造函数和析构函数
(1)构造函数: 在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。
(2)构造函数的重载: 和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。
(3)析构函数: 析构函数(Destructor)是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。
注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。
// constructor.cpp
#include <iostream>
#include <string>
using namespace std;
class Sales_data {
public:
// 构造函数
Sales_data(): units_sold(0), revenue(0.0) {
cout << "constructor1 executed" << endl;
}
// 构造函数的重载
Sales_data(const std::string &s):
bookNo(s), units_sold(0), revenue(0.0) {
cout << "constructor2 executed" << endl;
}
// 析构函数
~Sales_data() {
cout << "destructor executed" << endl;
}
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
int main(int argc, char *argv[])
{
Sales_data sale1;
Sales_data sale2("978-7-121-15535-2");
return 0;
}
由运行结果知,创建对象时系统会自动调用构造函数进行初始化工作,程序即将结束时系统会自动调用析构函数进行清理工作。
4. 如何声明并使用友元
(1)类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条 friend 关键字开始的函数声明语句即可。
friend Sales_data add(const Sales_data&, const Sales_data&);
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。
(2)类还可以把其他的类定义成友元,也可以把其他类(之前定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。
class Screen {
// Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mg;
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; // 默认构造函数
Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd),
contents(ht * wd, c) { }
};
class Window_mgr {
public:
// 窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
// 按照编号将指定的Screen重置为空白
void clear(ScreenIndex);
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
// s是一个Screen的引用,指向我们想清空的那个屏幕
Screen &s = screens[i];
// 将那个选定的Screen重置为空白
s.contents = string(s.height * s.width, ' ');
}
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。首先把s定义成screens vector中第i个位置上的Screen的引用,随后利用Screen的height和width成员计算出一个新的string对象,并令其含有若干个空白字符,最后我们把这个含有很多空白的字符串赋给contents成员。如果clear不是Screen的友元,上面的代码将无法通过编译,因为此时clear将不能访问Screen的height、width和contents成员。而当Screen将Window_mgr指定为其友元之后,Screen的所有成员对于Window_mgr就都变成可见的了。
Note: 友元关系不具有传递性,也就是说,如果Window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权,每个类负责控制自己的友元类或友元函数。
使用友元的利弊:当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以“工作在类的内部”,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎(比如随意设定友元),就有可能破坏类的封装性。
5. 类的静态成员和静态函数
有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。例如,一个银行账户类可能需要一个数据成员来表示当前的基准利率。在此例中,我们希望利率与类关联,而非与类的每个对象关联。从实现效率的角度来看,没必要每个对象都存储利率信息。而且更加重要的是,一旦利率浮动,我们希望所有的对象都能使用新值。
声明静态成员
我们通过在成员的声明之前加上关键字 static 使得其与类关联在一起。和其他成员一样静态成员可以是 public 的或 private 的。静态数据成员的类型可以是常量、引用、指针、类类型等。
class Account {
public:
void calculate() {
amount += amount * interestRate;
}
static double rate() {
return interestRate;
}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为静态成员变量分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了静态成员变量,也会影响到其他对象。
static 成员变量既可以通过对象来访问,也可以通过类来访问。
//通过类类访问 static 成员变量
Account::interestRate = 2.735;
//通过对象来访问 static 成员变量
Account account("hp", 10000);
account.interestRate = 2.735;
//通过对象指针来访问 static 成员变量
Account *pa = new Account("hp", 10000);
pa -> interestRate = 2.735;
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们并不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反地,必须在类的外部定义和初始化每个静态成员。一个静态数据成员只能定义一次,并且它将一直存在于程序的整个生命周期中。
我们定义静态数据成员的方式和在类的外部定义成员函数差不多。我们需要指定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:
// 定义并初始化一个静态成员
double Account::interestRate = initRate();
这条语句定义了名为 interestRate 的对象,该对象是类 Account 的静态成员,其类型是 double。从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了。因此,我们可以直接使用 initRate 函数。注意,虽然 initRate 是私有的,我们也能用它初始化 interestRate。和其他成员的定义一样,interestRate 的定义也可以访问类的私有成员。
Note: static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。具体来说,static 成员变量和普通的 static 变量类似,都在内存分区中的全局数据区分配内存。
使用 static 成员变量而不是全局变量的优点:
(1) static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
(2) 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
(3) 通过阅读程序容易看出 static 成员是与特定类关联的,这种可见性可清晰地显示程序员的意图。
定义静态成员函数
在类的内部声明函数时需要添加 static 关键字,但是在类外部定义函数时就不需要了。
static 成员函数是类的组成部分但不是任何对象的组成部分,它有以下几个特点:
(1) static 函数没有 this 指针。
(2) static 成员函数不能被声明为 const (将成员函数声明为 const 就是承诺不会修改该函数所属的对象)。
(3) static 成员函数也不能被声明为虚函数。
和其他的成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复 static 关键字,该关键字只出现在类内部的声明语句:
void Account::rate(double newRate) {
interestRate = newRate;
}
静态成员函数与普通成员函数的根本区别:
(1)普通成员函数有 this 指针,可以访问类中的任意成员。
(2)而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
Note: 在C++中,静态成员函数的主要目的是访问静态成员,静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用。