C++的基本操作对象是类和对象,类是模板,对象是类的实例。
在类中,声明了private、protected和public,用来区分不同的访问级别。
对于类内部访问的级别来说,public一般指接口访问,private为实现访问。而Protected为可继承的实现。
private是私有的成员变量或成员函数,只能本类或对象调用。
protected表示继承级别的访问,用来表示子类可以调用基类的成员变量或函数。
public是全局访问权限,类的内部外部都可以访问。
关于private的访问权限,其实是分为同一对象内访问和同类对象间访问,C++中,同类对象都可以访问对方的私有成员。
在C++中,访问控制是在每个类的基础上工作,而不是在每个对象的基础上。
首先,每个实例的访问控制的成本可能非常高。在理论上,这可以通过这个指针检查来完成。然而,这不能在编译时完成,只能在运行时完成。所以你必须在运行时识别每个成员的访问控制,当它被违反的时候可能只有异常会被引发。这个代价是很高的。
C++中的访问控制是作为一个静态的、编译时的特性来实现的。在编译时实现任何有意义的每个对象的访问控制是不可能的。只有每个类的控制可以通过这种方式实现。
所以,作为静态编译语言的C++,适用类访问级别的控制就可以解释并理解了。
那关于权限访问可以理解为以下几点:
1,一个对象可以访问另一个同类对象的私有成员(这样也方便,比如编写拷贝构造函数,直接访问对方成员就很容易)
2,既然同类对象可以访问私有成员,那么公共访问和继承访问也不在话下。
3,如果不是同类对象,即使是子类,也不能访问私有成员包括继承访问权限的成员。
4,子类只能访问同一个对象的父类的方法。
通过代码来理解体验一下:
#include <cstdio>
class A
{
private:
void printPrivate(){printf("private hello!\n");}
protected:
void printProtected(){printf("protected hello!\n");}
public:
void callPrivate(A obj){obj.printPrivate();}
void callProtected(A obj){obj.printProtected();}
};
class B: public A
{
public:
// Compile error, class B can't call class A's private method.
// void callPrivateB(A obj){obj.printPrivate();}
// Compile error, class B can't call class A's protected method
// void callProtectedB(A obj){obj.printProtected();}
// object of B can call the base class's method
void callProtectedB(){A::printProtected();}
};
int main()
{
A b;
A a;
b.callPrivate(a);
b.callProtected(a);
B c;
c.callProtectedB();
}
--------------------------------------------------------
关于访问控制,在类继承时,对同名函数的重载或覆盖,可不可以修改访问控制修饰符呢?
比如子类有一个private的funcA( ) 函数,然后父类有一个public的funcA( ) 函数,这种函数重载或覆盖吗,会发生什么呢?
首先,对于成员变量是不存在这个问题的,因为子类父类的同名变量是独立存在,根据相应的访问方式来决定是否能访问和访问哪个变量(之前有篇文章介绍了)。
其次,对于函数来讲,同一个类里的同名函数而参数列表不同,可认为是函数重载。
派生类和基类的同名和同参数列表的函数,可认为是覆盖,因为子类调用时会使用子类实现,在子类层面,父类的这个方法除了显示直接调用就会被隐藏起来了。
我们要说的就是覆盖的这种情况。
这样的话,其实对于子类父类拥有的同名函数(对于子类来说是子类的实现覆盖了父类的实现),道理和前面文件介绍的子父类同名变量是一样的。
第一步,确认调用对象。
第二步,确认是否存在这个成员函数,如果没有,向上一层父类查找。
第三步,找到以后,判断当前调用方式和访问修饰符(private、protected、public)是否相符。不符合则编译出错。
第一步,确定调用对象的声明类型。
#include <cstdio>
class A
{
public:
void func(){printf("A func.\n");}
};
class B: public A
{
public:
void func(){printf("B func.\n");}
};
int main()
{
A * x;
B * y;
A a;
B b;
x = &a;
x->func();
printf("==========\n");
x = &b;
x->func();
printf("==========\n");
y = &b;
y->func();
// compile error, object of A is not a B type.
// y = &a;
return 0;
}
$ g++ -o test test.cpp
$ ./test
A func.
==========
A func.
==========
B func.
这里,使用对象的之类来调用,则根据声明的类型来确定调用,而不是实际指向的对象的类型。
注意,类B的对象地址可以赋值给类A的指针类型,而类A的对象地址不能赋值给类B的指针类型。
因为可以说一个子类B对象同时也是一个类A的对象,但反过来不成立。
但如果使用了虚函数,则不再根据对象的声明类型来调用,而是根据实际的对象类型来:
#include <cstdio>
class A
{
public:
virtual void func(){printf("A func.\n");}
};
class B: public A
{
public:
virtual void func(){printf("B func.\n");}
};
int main()
{
A * x;
B * y;
A a;
B b;
x = &a;
x->func();
printf("==========\n");
x = &b;
x->func();
printf("==========\n");
y = &b;
y->func();
// compile error, object of A is not a B type.
// y = &a;
return 0;
}
$ g++ -o test test.cpp
$ ./test
A func.
==========
B func.
==========
B func.
第二步,判断是否拥有此成员函数。第三步,判断是否符合访问修饰符。
#include <cstdio>
class A
{
public:
void func(){printf("A func.\n");}
};
class B: public A
{
private:
void func(){printf("B func.\n");}
};
int main()
{
B b;
b.func(); // compile error, class B has a private func()
return 0;
}
类B拥有func函数,但是private的,所以不能外部调用。
基类A有public的func函数,但无关。
如果类B没有此函数,再去父类里找。
#include <cstdio>
class A
{
public:
void func(){printf("A func.\n");}
};
class B: public A
{
};
int main()
{
B b;
b.func();
return 0;
}
Output:
A func.
在这里,我们可以仔细想一下,其实无所谓什么重载或覆盖,当你给出一个函数名字和参数列表,就会再当前的调用范围内去找这个函数,找到了就判断是否能访问。
如果找不到再去父类里找,找到后根据访问修饰符判断是否能访问。
覆盖的意思,在当前的调用规则下,根据调用优先级,先在哪个范围里找,先找到谁而已。
那这样的话,子类父类的同名同参数列表的函数,是否使用不同的修饰符,就根据使用情况来决定即可。
而对于虚函数来讲,在对象生成时,有一个虚函数列表,当调用虚函数时,是直接查这个表。
就是第一步确定调用关系上和平时不同,后面确定修饰符的步骤是一样的。
举个例子看一看:
#include <cstdio>
class A
{
private:
virtual void func(){printf("A func.\n");}
};
class B: public A
{
public:
virtual void func(){printf("B func.\n");}
};
int main()
{
A * obj;
obj = new B();
// compile error
obj->func();
delete obj;
return 0;
}
上面代码编译错误,因为虚函数列表是运行时生成的,而访问修饰是编译器决定的。
所以在使用基类指针调用某个虚函数时,调用父类还是子类函数是根据虚函数表来决定,而访问修饰符只能根据当前使用的对象指针类型来决定。
这里是基类A,它的虚函数是私有的,所以不能外部访问。
改成下面代码则可以运行:
#include <cstdio>
class A
{
public:
virtual void func(){printf("A func.\n");}
};
class B: public A
{
private:
virtual void func(){printf("B func.\n");}
};
int main()
{
A * obj;
obj = new B();
obj->func();
delete obj;
return 0;
}
输出:
B func.
-----------------------------------------------------
在类继承的时候,也会使用到访问控制符,比如:
class A
{ };
class B : public A
{
};
class C : protected A
{
};
class D : private A
{
};
通常我们使用public继承就够了。
而private和protected继承看起来很奇怪,用的也不多,其实也不是为一般业务开发人员准备的,比如一些库(boost、LLVM、Clang)的开发会用到这些特性。
C++这么多特性,很多冷门的,我们一般程序员看起来很奇怪,但其存在必然有其道理,说不定就是为了解决某些棘手问题而出现的最合适的解决方案。
而Java的话,就默认只有public继承,没有其他两种。
继承的访问权限如下:
基类权限 | public | private | protected |
public 继承 | public | private | protected |
private继承 | private | private | private |
protected继承 | protected | private | protected |
继承访问权限三看原则:
①:看使用的方法在类的内部还是外部
②:看子类的继承权限(public private protected)
③:看基类的权限(public private protected)
通过代码来看一下, 先看public继承:
#include <cstdio>
class A
{
public:
void pubA(){printf("public A.\n");}
protected:
void proA(){printf("protected A.\n");}
private:
void prvA(){printf("private A.\n");}
};
class B : public A
{
};
int main()
{
B b;
b.pubA();
// b.proA(); // compile error
// b.prvA(); // compile error
return 0;
}
protected继承:
#include <cstdio>
class A
{
public:
void pubA(){printf("public A.\n");}
protected:
void proA(){printf("protected A.\n");}
private:
void prvA(){printf("private A.\n");}
};
class B : protected A
{
public:
void callPro(){pubA();proA();}
// void callPriv(){prvA();} // compile error
};
int main()
{
B b;
//b.pubA(); // compile error
//b.proA(); // compile error
//b.prvA(); // compile error
b.callPro();
// b.callPriv(); // compile error
return 0;
}
可以看到public访问变成了protected访问。
关于private继承比较特殊的一个。
class B{};
class D : private B{};
B *obj = new D(); // 将编译错误
使用private继承,子类就不能说它也是一个父类类型了,因为语法上来看,什么都没继承过来,都是private。
那这个有什么用呢?私有继承了父类,什么都访问不到了?
使用private继承,父类的public和protected成员,子类内部是可以访问的,但再继承的话,就不能访问了。
#include <cstdio>
class A
{
public:
void pubA(){printf("public A.\n");}
protected:
void proA(){printf("protected A.\n");}
private:
void prvA(){printf("private A.\n");}
};
class B : private A
{
public:
void callPro(){pubA();proA();}
};
class C: public B
{
public:
void callPro(){
// pubA(); // compile error
// proA(); // compile error
}
}
int main()
{
B b;
b.callPro();
return 0;
}
输出:
public A.
protected A.
那和protected继承相比,在第一层子类这里,效果是一样的。
但使用using语句,可以改变protectd和public的继承的访问属性,变成public的。
#include <cstdio>
class A
{
public:
void pubA(){printf("public A.\n");}
protected:
void proA(){printf("protected A.\n");}
private:
void prvA(){printf("private A.\n");}
};
class B : private A
{
public:
using A::proA;
using A::pubA;
};
class C: public B
{
};
int main()
{
B b;
b.proA();
b.pubA();
C c;
c.proA();
c.pubA();
return 0;
}
$ g++ -o test test.cpp
输出:
protected A.
public A.
protected A.
public A.
总结下:
1.private继承就是一种纯粹的实现技术 : 意味着老子继承你,纯粹是看中了你里面的某些函数实现罢了,不想跟你有别的关系;
2.一般来说私有继承,与复合类的作用类似,可以互换(复合类更容易理解)
3.这个新的类将不会与父类指针有关系(接口都变private了)
-------------------------------------------------------------
上面讲的都是类内部的访问控制和可见性。
对于类层级间的可见性和访问控制,使用namespace来做。
当你在某个namespace比如”MY“中定义了一个类A,那其他地方引用这个类,要么先声明using namespace MY,或者在使用这个类时用MY::A。
否则的话,就是不可使用和访问这个类的,即使是在同一文件中也不行。
#include <cstdio>
namespace MY{
class A
{
};
}
using namespace MY;
int main()
{
MY::A a;
return 0;
}
这样做的用处,在大型的项目中,为了防止各个模块或库之间的重名的类名或函数,并且更好的管理可见范围。
类似的语义在java中也有。每个java类或接口的定义开头,都有一行package语句,其实就是一个namespace类似的名字,在java里也对应了类在包内的路径。
然后如果其他类要使用某个类,就要import这个package定义的路径名。
只不过比较来看,Java的package机制比C++的namespace更加精致好用些。
参考:
C++ protected继承和private继承是不是没用的废物? - 知乎https://www.zhihu.com/question/425852397/answer/1528656579
https://segmentfault.com/a/1190000017766306https://segmentfault.com/a/1190000017766306