C++实操 - 访问控制

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++ - Why do objects of the same class have access to each other's private data? - Stack Overflowhttps://stackoverflow.com/questions/6921185/why-do-objects-of-the-same-class-have-access-to-each-others-private-data

C++ protected继承和private继承是不是没用的废物? - 知乎https://www.zhihu.com/question/425852397/answer/1528656579

https://segmentfault.com/a/1190000017766306https://segmentfault.com/a/1190000017766306

Using-declaration - cppreference.comhttps://en.cppreference.com/w/cpp/language/using_declaration#In_class_definition

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜流冰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值