C++ 学习笔记(八)(类篇一)

前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。下半篇传送门:C++ 学习笔记(九)(类篇二)

引言:类的基本思想

在这里插入图片描述

类的基本思想是数据抽象(data abstraction)封装(encapsulation)。数据抽象是一种依赖于将接口(interface)实现(implementation) 分离开的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体,以及定义类所需的各种私有函数。

封装将类的接口与类的实现分开,只向外提供类的接口,从而隐藏了类的实现。类就好比一个多功能盒子,用户可以通过盒子上的按钮实现多个功能,但用户并不知道每个功能在盒子内是如何实现的。

类要想实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要思考类做了什么,而无需了解类的实现过程和细节。

1 类和对象概论

在现实世界中,经常能遇见许多具有相同特征的事物,我们说它们是属于一类的。比如不同的笔记本品牌,它们都具有相同的特征:显示屏、主板、键盘、触摸板等等,尽管它们的性能和形状各异,但都属于电脑这一范畴。因此我们可以将这些公有的特征提取成一个集合,这个集合就称为类,对于某一个电脑品牌,它就是这个类的对象。也就是说,对象就是类的具体实现,而类则是对象的抽象概括

类是定义同一类型所有对象的原型。例如,你可以定义一个电脑类,它相当于生产一台电脑的说明书。这个说明书包含了关于电脑的所有基本属性(比如电脑都有显示屏,主板等等),以及对电脑的相关操作(如何开机,如何使用键盘等等)。在 C++ 中,我们把在类里定义的变量和函数,称为类的成员变量成员函数,统称为类成员或对象成员。

我们不能对说明书进行电脑的操作,但能按照说明书生产出一台电脑来。因此当你创建了一个类之后,你必须在使用它之前对它进行实例化,因为类只是一个集合而不是具体的对象。创建类的实例时,就建立了这种类型的一个对象,然后系统为实例化的对象分配内存。

一般来说,成员变量和成员函数只能通过实例对象来访问或调用。但类也能定义属于“它自己”的类变量类方法。在这种情况下,我们可以通过实例对象访问或调用它们,也可通过类来访问或调用。但需要注意的是,类方法只能访问类变量,而不能访问非类变量或非类方法。它们的区别还在于,系统在编译过程就会为类建立所有类变量的拷贝,这个类的所有实例对象共享类的所有类变量。

2 类的入门

2.1 类的定义

2.1.1 声明成员函数

接下来我就按照 C++ Prime 上的顺序,引用它的例子并稍作修改以便理解,来展开这一节的内容。

假如说我们有如下结构体表明一本书的相关销售信息(之所以用结构体,是因为结构体和类具有相似之处;此外代码中有许多的关键字用以修饰,我们先不用管,后面会慢慢解释):

读者可以把每一处的代码复制到一个文本中,就不需要重复的往前看了。

struct Sales
{
    // 成员函数————关于 Sales 对象的操作
    string isbn() const { return bookNo; }	// bookNo 即为 book number
    Sales& combine(const Sales&);
    double avg_price() const; // 返回售出书籍的平均价格

    // 成员变量
    string bookNo;			// isbn 编号
    int sold = 0;			// 该书的销量
    double revenue = 0.0;	// 该书销售收入
};	// 这个分号不要忘记

// Sales的非成员接口函数
Sales add(const Sales&, const Sales&);
istream &read(istream&, Sales&);
ostream &print(ostream&, const Sales&);

国际标准书号(International Standard Book Number),简称 isbn,是专门为识别图书等文献而设计的国际编号。

成员函数的声明必须在类的内部,但它的定义既可以在内部也可以在外部。作为接口组成部分的非成员函数,例如 add、read 和 print 等,它们的定义和声明都在类的外部。

函数声明和定义的区别:

  • 函数的定义包括以下部分:
返回类型 函数名字 (0个或多个形参组成的列表) 
{
	函数体
}
  • 函数的声明无须函数体,用一个分号替代大括号和函数体即可。
  • 函数的声明无须函数体,所以也就无须形参的名字。但写上形参的名字还是有用处的,它可以帮助使用者更好的理解函数的功能。
  • 函数的声明也称作函数原型
内联函数

定义在类内部的函数是隐式的内联函数。将函数指定为内联函数后,编译器会在每个调用点上直接展开函数。比如求最大值的函数:

int max(int a, int b)
{	// 如果 a 大于 b 返回 a,否则返回 b
	return a > b ? a : b;
}

那么下面这条语句:

cout << max(a, b) << endl;

将会在编译过程中被编译成下面的形式:

cout << (a > b ? a : b) << endl;

从而无须利用栈来保存现场,节省了函数调用的开销。一般来说,内联机制适用于结构简单、规模较小,流程直接、频繁调用的函数。因此,内联函数不能有循环语句

2.1.2 定义成员函数

2.1.1 中提到,成员函数既可以定义在类的内部,也可以定义在类的外部。对于前面定义的 Sales 类,我们将 isbn 函数在类内进行了定义,而 combine 和 avg_price 只进行了简单的声明。

首先看看 isbn 函数,它的参数列表为空,返回值是一个 string 对象。假如 Sales 类有一个对象 object,它调用了 isbn 函数:

...
	string isbn() const { return bookNo; }	// bookNo 即为 book number
...
// 使用点运算符来访问 object 对象的成员函数 isbn()
object.isbn()
this 指针

我们知道 isbn 函数会返回 bookNo,那么编译器怎么区分是谁的 bookNo 呢?实际上,所有成员函数都有一个隐式的形参指针 this,它指向调用函数的对象。编译时,编译器会把对象的地址传给 this。当 isbn 返回 bookNo 时,实际上返回的是 object 的 bookNo,即 object.booNo。可以等价地认为该调用被重写成了如下的形式:

// 隐式定义的 this 指针,因为对象是 Sales 类的,所以是 Sales*
string isbn(Sales *this) const {return this->bookNo;}

// 调用函数时传入了 object 的地址
object.isbn(&object);

// 在这时,返回的就是 object 对象的成员 bookNo
return object.bookNo;
// 或
return this->bookNo;

this 指针就像是对象的姓名卡,时时刻刻告诉你,这里面的东西都是属于这个对象的。调用函数时,总是给里面的东西加上限制的形容词——对象的名词。

this 指针始终指向调用函数的对象,任何对类成员的直接访问,都会隐式地使用 this 指向的成员。因此在成员函数的内部,我们无需通过成员访问运算符来访问对象的成员。举个例子就是,如果我们想得到 object 对象的 bookNo,不需要使用 object.bookNo 或 this->bookNo,调用函数返回的就是 object 对象的 bookNo。

this 形参指针是隐式定义的,因此我们可以在成员函数内部使用 this。设立 this 的目的,就是希望令它始终指向调用函数的对象,所以 this 是一个常量指针,我们不允许改变 this 中保存的地址。

2.1.3 引入常量成员函数

我们继续看 isbn 函数的定义,在其参数列表后有一个 const 关键字。在这里,const 的作用是修改 this 指针的类型。

string isbn() const {return bookNo;}

这一小节的内容,如果读者不是很能理解,可以先阅读文章:C++ 学习笔记(六)(const 限定符篇),文章不长,但是能有助于你更全面的了解这一小节的知识。

默认情况下,this 是指向非常量对象常量指针。但要想存放常量对象的地址,只能使用指向常量的指针。和所有指针一样,this 也需要遵循初始化的规则:两边的类型保持一致,因此 this 不能绑定到一个常量对象上。指向常量对象意味着不能通过该指针修改对象的值,因此必须将 this 定义成指向常量的指针。这也表明我们不能在一个常量对象上调用普通的成员函数,因为普通的成员函数,其隐式形参 this 不能指向常量对象。

常量指针仅仅表明指针本身是一个常量,常量的属性保证了它存储的地址值不会变。在此例中,this 的类型是 Sales *const,即指向 Sales 类非常量对象常量指针

但毕竟,isbn 函数仅仅返回对象的 bookNo 成员,而不做任何修改。this 是指向非常量对象的指针,依然存在着不小心修改成员的风险(很多漏洞就是如此产生的),所以把 this 设置为指向常量的指针有助于提高函数的灵活性。

因为 this 是隐式形参,我们无法直接声明它的类型。所以 C++ 允许我们把 const 关键字放在成员函数的参数列表之后,用以表明 this 是一个指向常量的指针。像这样使用 const 的成员函数被称作常量成员函数(cosnt member function),表明这个成员函数不会对类或对象的任何数据成员做任何改变。

在参数列表后加 const,等价于对 isbn 函数进行如下的重写(不能真的这么写):

string Sales::isbn(const Sales *const this) { return this->isbn; }

在设计类的时候,一个基本原则就是对于不改变数据成员的成员函数都声明成常量成员函数,const 关键字对成员函数的行为作了更加明确的限定:

  • 常量成员函数能访问任意对象的所有数据成员,对数据成员只读不写
  • 非常量成员函数只能访问非常量对象的所有数据成员,对数据成员可读可写
  • 常量对象、常量对象的引用、指向常量的指针只能调用常量成员函数,而不能调用非常量成员函数
  • 如果只有常量成员函数,非常量对象可以调用常量成员函数。当常量和非常量成员函数同时出现时,非常量对象只能调用非常量成员函数。

在这里插入图片描述

2.1.4 在类的外部定义成员函数

在类内声明过,才能在类外定义成员函数。在类外定义成员函数时,有以下几点需要注意:

  • 定义和声明除了函数体外均需保持一致。
  • 函数名前要加上函数所属类名,在类名和函数名间要加上作用域运算符“ :: ”

在类外定义 avg_price() 函数时加上类名,就表明该函数是被声明在 Sales 类内的,那么变量 sold 和 revenue 就会隐式地使用 Sales 类的数据成员。

double Sales::avg_price() const
{
    if (sold) // 如果销量不为0
        return revenue / sold; // 销售收入 / 销量 = 每本书的平均价格
    else
        return 0;
}

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

在类外定义 combine 函数,用于实现将两个对象的相关数据相加:

Sales& Sales::combine(const Sales &rhs)
{
    sold += rhs.sold;	// 把 rhs 的成员加到 this 对象的成员上
    revenue += rhs.revenue;
    return *this;		// 返回解引用指针 this 后的结果
}

当 object 对象调用 combine 函数时:

object.combine(temp);	// 更新遍历 object 当前的值

object 的地址传给 this,而引用 rhs 绑定到了 temp 上。该函数的作用类似于复合赋值运算符 +=,它将赋值运算符右侧对象“加到”赋值运算符的左侧对象上。

当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符会将它的左侧运算对象当成左值返回,因此为了与它保持一致,combine 函数必须返回引用类型。因为此时左侧运算对象是一个 Sales 类的对象,所以返回类型应该是 Sales 的引用,即 Sales&。

return *this; 语句中, “ * ”号解引用 this 指针得到它存放的地址,即调用函数的对象的地址。换句话说,这个调用返回 object 的引用。

2.2 定义类的非成员函数

我们重新看看这个类:

struct Sales
{
    // 成员函数————关于 Sales 对象的操作
    string isbn() const { return bookNo; }	// bookNo 即为 book number
    Sales& combine(const Sales&);
    double avg_price() const; // 返回售出书籍的平均价格

    // 成员变量
    string bookNo;			// isbn 编号
    int sold = 0;			// 该书的销量
    double revenue = 0.0;	// 该书销售收入
};	// 这个分号不要忘记

// Sales的非成员接口函数
Sales add(const Sales&, const Sales&);
istream &read(istream&, Sales&);
ostream &print(ostream&, const Sales&);

通常情况下,我们可能需要定义从概念上来说属于类的接口,但并不在类里面进行声明的函数。比如 add、print 和 read 函数,它们用于对类的对象进行操作,但并不在类中进行声明或定义。定义非成员函数时,通常把它的声明和类的声明写在同一个头文件中,然后在另一个文件中定义它。这样做的好处是通过引入头文件,即可使用接口的任何部分。

2.2.1 定义 read 和 print 函数

定义如下:

// 输入的交易信息包括 isbn、销量和售价
istream &read(istream &is, Sales &item)
{
    double price = 0;
    is >> item.bookNo >> item.sold >> price;
    item.revenue = price * item.sold; // 销售收入 = 售价 * 销量
    return is;
}

// 此处只打印 item 的数据成员,所以形参是对常量的引用
ostream &print(ostream &os, const Sales &item)
{
    // 输出 isbn、销量、销售收入和平均价格
    os << item.isbn() << " " << item.sold << " " << item.revenue << " " << item.avg_price();
    return os;
}

三点需要注意:

  1. 两个参数都接受一个 IO 对象的引用作为参数,因为 IO 对象不能被拷贝,所以只能使用引用来传递。
  2. 读取和写入的操作会改变流的内容,所以两个函数接受的都是普通 IO 对象的引用,而非对 IO 常量的引用。
  3. print 函数不负责换行。执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。

2.2.2 定义 add 函数

Sales add(const Sales &lhs, const Sales &rhs)
{
    Sales sum = lhs;	// 把 lhs 的数据成员拷贝给 sum
    sum.combine(rhs);	// 把 rhs 的数据成员加到 sum 上
    return sum;
}

返回的 sum 实际上是两笔交易的和。拷贝类的对象,同时也会拷贝对象的数据成员。该函数和 combine 函数的区别在于,add 函数是返回一个新的 Sales 对象,而 combine 函数返回的是 this 指向的对象。

2.3 构造函数

类通过一个或几个特殊的成员函数初始化对象的数据成员,这些函数叫做构造函数。构造函数的特点:

  • 名字和类名相同。
  • 没有返回类型。
  • 可以重载,不同的构造函数之间必须在参数数量或参数类型上有所区别。
  • 至少有一个构造函数。
  • 只要类的对象被创建,就一定会执行某一个构造函数。

不同于其他成员函数,构造函数不能被声明成常量成员函数。因为当我们创建一个常量对象时,直到构造函数完成初始化的过程,该对象才能真正地取得其“常量”属性。因此,构造函数在常量的构造过程中可以向其写值。

2.3.1 合成的默认构造函数

类通过一个特殊的构造函数来控制对象的默认初始化过程,这个函数叫做默认构造函数。如果类中没有定义构造函数,编译器便会隐式地定义一个默认构造函数。默认构造函数不接收任何参数

换句话说,不接受任何参数的构造函数是默认构造函数。

编译器创建的默认构造函数又称为合成的默认构造函数。由于合成默认构造函数没有实参,其将按照该规则初始化对象的数据成员:如果在类定义时为成员变量赋予了初始值,就用初始值来初始化;否则,按照 C++ 的规则执行默认初始化

比如 string 对象 bookNo 没有赋初始值,所以在初始化时会成为一个空字符串。

2.3.2 某些类不能依赖于合成的默认构造函数

合成的默认构造函数只适合非常简单的类,比如前面定义的 Sales 类。一般来说,我们需要手动定义一下默认构造函数。原因有三个:

  • 编译器只有在发现类内没有构造函数时才会自动生成合成的默认构造函数。一旦我们定义了其他的构造函数,创建类的对象时就必须使用我们自己定义的构造函数,否则编译器就会报错。
  • 对于某些类来说,合成的默认构造函数可能执行错误的操作。如果在类中的有内置类型或复合类型的对象,比如数组和指针被默认初始化后,它们的值就是未定义的,这样可能会导致很严重的错误。因此,含有内置类型或复合类型成员的类应该在类的内部初始化所有这些成员,或者自己定义一个默认构造函数。
  • 有时候编译器不能为某些类合成默认的构造函数。例如,如果类 A 中包含一个类 B 的成员且这个类 B 没有默认构造函数,那么编译器将无法初始化该成员(LittleGoldFish 提供这一条注意事项)。对于这类 A 来说,我们必须自定义默认构造函数,否则它也没有默认构造函数。

2.3.3 定义 Sales 类的构造函数

为 Sales 类添加构造函数:

struct Sales
{
    // 新增的构造函数
    Sales() = default;	// 默认构造函数
    Sales(const string &s) : bookNo(s) {}
    Sales(const &s, int n, double p) : bookNo(s), sold(n), revenue(p * n) {}
    Sales(istream &);	// 从 istream 流中读取一条交易信息

    // 成员函数————关于 Sales 对象的操作
    string isbn() const { return bookNo; }	// bookNo 即为 book number
    Sales& combine(const Sales&);
    double avg_price() const; // 返回售出书籍的平均价格
    
    // 成员变量
    string bookNo;			// isbn 编号
    int sold = 0;			// 该书的销量
    double revenue = 0.0;	// 该书销售收入
};	// 这个分号不要忘记

如果在类中定义其他的构造函数,最好同时定义一个默认的构造函数。在 C++ 新标准中,可以在参数列表后面写上 = default 来要求编译器生成默认构造函数,当然不加也是可以的(此时就需要自己提供一个默认构造函数)。该规则既可以和声明一起出现在类内,此时默认构造函数是内联的;也可以作为定义出现在类外。此时默认构造函数是不内联的。

2.3.4 构造函数初始值列表

看看中间两个构造函数:

...
    Sales(const string &s) : bookNo(s) {}
    Sales(const &s, int n, double p) : bookNo(s), sold(n), revenue(p * n) {}
...

C++ 中把构造函数中从冒号到函数体左大括号前的部分称为构造函数初始值列表。它负责为新创建的对象的数据成员赋初始值,格式是:成员名字 (初始值),各成员之间用逗号隔开。当某一个成员是另一个类的对象时,对该成员也要执行另一个类的构造函数。当某个成员被构造函数初始值列表忽略时,它们以合成默认构造函数初始化的规则进行初始化。

只接受一个 string 类型参数的构造函数等价于:

// 与上面定义的那个构造函数效果相同
Sales(const string &s) : bookNo(s), sold(0), revenue(0) {}

2.3.5 在类的外部定义构造函数

2.3.3 代码的第四个构造函数,是一个在类外定义的构造函数,它接受一个 istream 对象的引用作为实参,并且需要执行一些实际的操作:

Sales::Sales(istream &is)
{
    read(is, *this); // read 函数的作用是从 is 中读取一条交易信息然后存入 this 对象中
}

和其他函数一样,当我们在类的外部定义构造函数时,必须加上类名和作用域运算符来指明构造函数是哪个类的成员函数。尽管这个构造函数的初始值列表为空,但由于它是构造函数,对象的成员仍然能被初始化。

2.4 拷贝、赋值和析构

类也需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等;当我们使用了赋值运算符时会发生对象的赋值操作;当对象不再存在时执行销毁的操作,如局部对象会在创建它的块结束时被销毁。

如果我们不主动定义这些操作,编译器就会替我们自动合成。但要清楚地一点事,对于某些类来说,合成的版本可能无法正常工作。特别的是,当类需要分配类对象之外的资源时,合成的版本常常会失效。话虽是这么说,但一般情况下,我们写个析构函数就可以了。

2.5 构造函数变量初始化的顺序

这一点是要另外提一下的,有些时候可能会现很难排查的问题。 在构造函数中,成员初始化的顺序,是以类内成员定义的顺序来的,而不是初始值列表中的顺序。举个简单的栗子,如果在类中以如下顺序定义了两个成员变量:

int m;
int n;

那么在构造函数的初始值列表中,我们以这样的顺序 m(1), n(m + 1) 来初始化是没有问题的,因为在类中 m 定义在前,n 定义在后。执行构造函数时,m 先被初始化,n 后被初始化,两者都能正确的得到值。但如果在类中定义的顺序是这样的:

int n;
int m;

此时若仍以 m(1), n(m + 1) 的顺序初始化就会出错。因为 n 会先获得初始值 m + 1,但m 的初始值是未知的,此时 n 的值就是未知的。尽管 m 仍然能被初始化为1,但显然初始化出错了,而且即便这样程序也并不会报错。所以初始值列表的成员初始化顺序,最好按照类中成员定义的顺序来实现

3 访问控制与封装

在 C++ 中,我们使用访问说明符加强类的封装性。

  • 定义在 public 说明符之后的成员在整个程序内均可被访问,public 成员定义类的接口。
  • 定义在 private 说明符之后的成员只可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了(或者说是隐藏了)类的实现细节。

对前面的类再一次定义后如下所示:

// 此处将 struct 换成了 class
class Sales
{
// 公有访问说明符
public:
    // 新增的构造函数
    Sales() = default;	// 默认构造函数
    Sales(const string &s) : bookNo(s) {}
    Sales(const &s, int n, double p) : bookNo(s), sold(n), revenue(p * n) {}
    Sales(istream &);	// 从 istream 流中读取一条交易信息

    // 成员函数————关于 Sales 对象的操作
    string isbn() const { return bookNo; }	// bookNo 即为 book number
    Sales& combine(const Sales&);
    double avg_price() const; // 返回售出书籍的平均价格

// 私有访问说明符
private:    
    // 成员变量
    string bookNo;			// isbn 编号
    int sold = 0;			// 该书的销量
    double revenue = 0.0;	// 该书销售收入
};	// 这个分号不要忘记

一个类可以有0个或多个访问说明符,每个访问说明符出现的次数是没有限定的。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止。

封装有两个重要的优点:

  • 确保用户的代码不会无意间破坏封装对象的状态。
  • 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

封装性对于一个类的设计非常重要。假设我们有一个 public 或者 protected 成员,由于public 成员完全没有封装性,一旦不小心修改了某个 public 或者 protected 成员,则所有使用了该成员的代码都可能失效。因此在设计类的时候,尽量将成员变量声明为 private。

同时,在设计成员函数的时候,直接接触 private 成员的成员函数越少越好。如果成员函数确实需要访问这些成员,最好通过定义接口间接访问。只要类的接口不变,用户代码就无需改变。如下所示:

class person
{
public:
    int getAge() { return age; }
private:
    int age;
} 

如上所示,在获取 person 类对象的年龄时,我们使用 getAge 函数来获取,而不是直接访问 Age 成员得到其值。

3.1 使用 class 或 struct 关键字

在上面可以看到,我们将 struct 关键字改为了 class 关键字。实际上这两个关键字都可以用来定义类,唯一的区别是两者的默认访问权限不一样

类可以在它的第一个访问说明符之前定义成员。如果使用 struct 关键字,则这些成员默认是 public 的;如果使用 class 关键字,则这些成员默认是 private 的。出于统一编程风格的考虑,当类的所有成员是 public 时,应使用 struct 关键字定义类;当类的所有成员是 private 或两者都有时,应使用 class 关键字定义类。

3.2 友元

3.2.1 friend 关键字

由于 read、print 和add 函数不是类的成员,也就无法访问到私有成员了。但是类可以允许其他类或者函数访问他的私有成员,方法是令替他类或者函数成为它的友元。只需要在前面加一个关键字 friend 即可:

class Sales
{
// 为Sales的非成员函数所做的友元声明
friend Sales add(const Sales&, const Sales&);
friend ostream &print(ostream&, const Sales);
friend istream &read(istream&, Sales);

// 公有访问说明符
public:
    // 新增的构造函数
    Sales() = default;	// 默认构造函数
    Sales(const string &s) : bookNo(s) {}
    Sales(const &s, int n, double p) : bookNo(s), sold(n), revenue(p * n) {}
    Sales(istream &);	// 从 istream 流中读取一条交易信息

    // 成员函数————关于 Sales 对象的操作
    string isbn() const { return bookNo; }	// bookNo 即为 book number
    Sales& combine(const Sales&);
    double avg_price() const; // 返回售出书籍的平均价格

// 私有访问说明符
private:    
    // 成员变量
    string bookNo;			// isbn 编号
    int sold = 0;			// 该书的销量
    double revenue = 0.0;	// 该书销售收入
};	// 这个分号不要忘记

// Sales 接口的非成员组成部分的声明
Sales add(const Sales&, const Sales&);
istream &read(istream&, Sales&);
ostream &print(ostream&, const Sales&);

友元声明只能出现在类定义的内部,一般定义在类开始或结束处。友元不是类的成员,也不受它所在区域访问控制级别的约束。

3.2.2 友元的声明

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。因此,我们的 Sales 头文件应该为 read、print 和 add 提供独立的声明(除了类内的友元声明之外)。


希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值