3.2 访问控制
访问说明符¶
我们使用 访问说明符(access specifiers) 加强类的封装性:
- 定义在 public 说明符之后的成员在整个程序内可被访问,public成员定义类的接口
- 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装(即隐藏)了类的实现细节。
一个类可以包含0个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或到达类的结尾处为止。
添加访问说明符的Sales_data.h
class Sales_data {
public:
// 添加了访问说明符
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(std::istream&);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
private:
// 添加了访问说明符
double avg_price() const
{ return units_sold ? revenue/units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
class
与struct
关键字
二者的唯一区别是,struct
和class
的默认访问权限不太一样。
类可以在它的第一个说明符之前定义成员,若是struct则定义在第一个说明符之前的成员是public的;若是class,反之。
类的组合¶
在组合中,一个类(整体)包含另一个类(部分)的实例作为其成员。这种关系类似于“拥有”关系,即整体对象拥有部分对象。部分对象的生命周期由整体对象控制,当整体对象被销毁时,其包含的部分对象也会被销毁。
-
不仅要负责对本类中的基本类型成员数据赋初值,也要对对象成员初始化。
class Point { public: Point(int x = 0, int y = 0) : x(x), y(y) {} // 初始化对象成员 int x, y; }; class Student { public: Student(const std::string& name, int x, int y) : name(name), address(x, y) {} // 初始化基本类型成员和对象成员 std::string name; Point address; };
-
构造函数调用顺序:
- 先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。
- 然后调用本类的构造函数(析构函数的调用顺序相反) 。
- 若调用默认构造函数(即无形参的),则内嵌对象的初始化也将调用相应的默认构造函数。
- 如果要为组合类编写拷贝构造函数,则需要为内嵌成员对象的拷贝构造函数传递参数。
前向引用¶
前向声明(forward declaration)是一种在C++中声明类或函数的方式,它允许在不包含完整定义的情况下使用某个类或函数。前向声明通常用于避免循环依赖问题。
class Fred; // 前向声明
1-循环依赖问题¶
当两个类之间存在相互依赖时,就会出现循环依赖问题。例如,class A
中包含class B
的成员,而class B
中又包含class A
的成员。
class B; // 前向声明
class A {
public:
void f(B b);
};
class B {
public:
void g(A a);
};
在上述例子中,A
和B
之间存在循环依赖。通过前向声明class B
,A
类可以声明一个接受B
作为参数的函数f
,而不需要包含B
的完整定义。
2-前向声明的限制¶
- 不能声明对象:在提供完整的类定义之前,不能声明该类的对象。
- 不能访问类的细节:在前向声明之后,不能在内联成员函数中使用该类的任何细节(如成员变量或成员函数)。
class Fred; // 前向声明
class Barney {
public:
void method() {
x->yabbaDabbaDo(); // 错误:Fred的完整定义尚未提供
}
private:
Fred* x; // 正确:可以声明指针
};
class Fred {
public:
void yabbaDabbaDo(); // 正确:在完整定义中声明成员函数
private:
Barney* y; // 正确:可以声明指针
};
3-前向声明的正确使用¶
- 指针和引用:可以使用前向声明的类来声明指针或引用。
- 函数参数和返回值:可以使用前向声明的类作为函数的参数或返回值类型。
class Fred; // 前向声明
class Barney {
public:
void setFred(Fred* f); // 正确:使用指针
Fred* getFred(); // 正确:返回指针
private:
Fred* x; // 正确:声明指针
};
class Fred {
public:
void setBarney(Barney* b); // 正确:使用指针
Barney* getBarney(); // 正确:返回指针
private:
Barney* y; // 正确:声明指针
};
4-完整代码示例¶
以下是一个完整的代码示例,展示了如何使用前向声明来解决循环依赖问题:
// 前向声明
class Fred;
class Barney {
public:
void setFred(Fred* f) { x = f; }
Fred* getFred() { return x; }
private:
Fred* x; // 指针
};
class Fred {
public:
void setBarney(Barney* b) { y = b; }
Barney* getBarney() { return y; }
void yabbaDabbaDo() { /* 实现细节 */ }
private:
Barney* y; // 指针
};
int main() {
Fred f;
Barney b;
b.setFred(&f);
f.setBarney(&b);
return 0;
}
友元¶
类允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的 友元(friend)。
友元的声明(待整理)¶
struct X {
friend void f() { /* 友元函数可以定义在类的内部*/ }
X() { f(); } // 错误:f 还没有被声明
void g();
void h();
};
void X::g() { return f(); } // 错误:f 还没有被声明
void f(); // 声明那个定义在 X 中的函数
void X::h() { return f(); } // 正确:现在 f 的声明在作用域中了
类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。
友元声明只能出现在类定义的任何地方,友元不是类的成员也不受它躲在区域访问控制级别的约束。
友元的声明仅仅指定了访问的权限,我们必须在友元声明之外再专门对函数进行一次声明。
为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。
非成员函数的友元¶
如果类想把一个函数作为它的友元,只需要增加一条以 friend
关键字开始的函数声明语句即可。
添加友元的Sales_data.h
class Sales_data {
// 为 Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream& read(std::istream&, Sales_data&);
friend std::ostream& print(std::ostream&, const Sales_data&);
// 其他成员及访问说明符与之前一致
public:
// 添加了访问说明符
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(std::istream&);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data&);
private:
// 添加了访问说明符
double avg_price() const
{ return units_sold ? revenue/units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
类之间的友元¶
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
-
友元类:一个类可以被声明为另一个类的友元,从而访问其私有和保护成员。
-
单向性:友元关系是单向的,除非显式声明为双向。
-
声明方式:使用
friend
关键字在类中声明友元类。
class MyClass {
friend class FriendClass; // 声明友元类
private:
int privateVar;
};
class FriendClass {
public:
void accessPrivateVar(MyClass& obj) {
obj.privateVar = 42; // 可以访问 MyClass 的私有成员
}
};
注意: 友元关系不存在递进性。
其他类的成员函数的友元¶
成员函数的友元 是指一个类的成员函数被声明为另一个类的友元,从而可以访问后者的私有和保护成员。
-
访问权限:友元成员函数可以访问另一个类的私有和保护成员。
-
声明方式:使用
friend
关键字在类中声明友元成员函数。 -
单向性:友元关系是单向的,除非显式声明为双向。
class MyClass {
private:
int privateVar;
};
class FriendClass {
public:
void accessPrivateVar(MyClass& obj) {
obj.privateVar = 42; // 可以访问 MyClass 的私有成员
}
};
// 声明 FriendClass 的成员函数为 MyClass 的友元
class MyClass {
private:
int privateVar;
public:
friend void FriendClass::accessPrivateVar(MyClass& obj);
};
其他¶
- 想要把一组重载函数声明为友元,需要对这组函数中的每一个分别声明