c++ 之类的前置声明

转自:http://blog.csdn.net/fjb2080/archive/2010/04/27/5533514.aspx 作者:清林,博客名:飞空静渡

刚开始学习c++的人都会遇到这样的问题:

定义一个类 class A,这个类里面使用了类B的对象b,然后定义了一个类B,里面也包含了一个类A的对象a,就成了这样:

 

  1. //a.h  
  2. #include "b.h"  
  3. class A  
  4. {  
  5. ....  
  6. private:  
  7.     B b;  
  8. };  
  9. //b.h  
  10. #include "a.h"  
  11. class B  
  12. {  
  13. ....  
  14. private:  
  15.     A a;  
  16. };  

 

一编译,就出现了一个互包含的问题了,这时就有人跳出来说,这个问题的解决办法可以这样,在a.h文件中声明类B,然后使用B的指针。

 

  1. //a.h   
  2. //#include "b.h"  
  3. class B;   
  4. class A   
  5. {  
  6.  ....   
  7. private:  
  8.  B b;   
  9. };   
  10. //b.h   
  11. #include "a.h"   
  12. class B  
  13. {  
  14.  ....   
  15. private:  
  16.  A a;   
  17. };  

 

然后,问题就解决了。

但是,有人知道问题是为什么就被解决的吗,也就是说,加了个前置声明为什么就解决了这样的问题。下面,让我来探讨一下这个前置声明。

类的前置声明是有许多的好处的。

我们使用前置声明的一个好处是,从上面看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。

另外一个好处是减小类A的大小,上面的代码没有体现,那么我们来看下:

 

  1. //a.h  
  2. class B;  
  3. class A  
  4. {  
  5.     ....  
  6. private:  
  7.     B *b;  
  8. ....  
  9. };  
  10. //b.h  
  11. class B  
  12. {  
  13. ....  
  14. private:  
  15.     int a;  
  16.     int b;  
  17.     int c;  
  18. };  

 

我们看上面的代码,类B的大小是12(在32位机子上)。

如果我们在类A中包含的是B的对象,那么类A的大小就是12(假设没有其它成员变量和虚函数)。如果包含的是类B的指针*b变量,那么类A的大小就是4,所以这样是可以减少类A的大小的,特别是对于在STL的容器里包含的是类的对象而不是指针的时候,这个就特别有用了。

在前置声明时,我们只能使用的就是类的指针和引用(因为引用也是居于指针的实现的)。

那么,我问你一个问题,为什么我们前置声明时,只能使用类型的指针和引用呢?

如果你回答到:那是因为指针是固定大小,并且可以表示任意的类型,那么可以给你80分了。为什么只有80分,因为还没有完全回答到。

想要更详细的答案,我们看下下面这个类:

 

  1. class A  
  2. {  
  3. public:  
  4.     A(int a):_a(a),_b(_a){} // _b is new add  
  5.       
  6.     int get_a() const {return _a;}  
  7.     int get_b() const {return _b;} // new add  
  8. private:  
  9.     int _b; // new add  
  10.     int _a;  
  11. };  

 

我们看下上面定义的这个类A,其中_b变量和get_b()函数是新增加进这个类的。

那么我问你,在增加进_b变量和get_b()成员函数后这个类发生了什么改变,思考一下再回答。

好了,我们来列举这些改变:

第一个改变当然是增加了_b变量和get_b()成员函数;

第二个改变是这个类的大小改变了,原来是4,现在是8。

第三个改变是成员_a的偏移地址改变了,原来相对于类的偏移是0,现在是4了。

上面的改变都是我们显式的、看得到的改变。还有一个隐藏的改变,想想是什么。。。

这个隐藏的改变是类A的默认构造函数和默认拷贝构造函数发生了改变。

由上面的改变可以看到,任何调用类A的成员变量或成员函数的行为都需要改变,因此,我们的a.h需要重新编译。

如果我们的b.h是这样的:

 

  1. //b.h  
  2. #include "a.h"  
  3. class B  
  4. {  
  5. ...  
  6. private:  
  7.     A a;  
  8. };  

 

那么我们的b.h也需要重新编译。

如果是这样的:

 

  1. //b.h  
  2. class A;  
  3. class B  
  4. {  
  5. ...  
  6. private:  
  7.     A *a;  
  8. };  

 

那么我们的b.h就不需要重新编译。

像我们这样前置声明类A:

class A;

是一种不完整的声明,只要类B中没有执行需要了解类A的大小或者成员的操作,则这样的不完整声明允许声明指向A的指针和引用。

而在前一个代码中的语句

A a;

是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含a.h来获得类A的大小,同时也要重新编译类B。

再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是只要这个声明没有执行需要了解类A的大小或者成员的操作就可以了,所以声明成指针或引用是没有执行需要了解类A的大小或者成员的操作的。

 

 

转自:http://blog.csdn.net/rogeryi/archive/2006/12/12/1439597.aspx

这篇文章很大程度是受到Exceptional C++ (Hurb99)书中第四章 Compiler  Firewalls and the Pimpl Idiom  (编译器防火墙和Pimpl惯用法) 的启发,这一章讲述了减少编译时依赖的意义和一些惯用法,其实最为常用又无任何副作用的是使用前置声明来取代包括头文件。

Item 26 的Guideline - "Never #include a header when a forward declaration will suffice"

在这里,我自己总结了可以使用前置声明来取代包括头文件的各种情况和给出一些示例代码。

首先,我们为什么要包括头文件?问题的回答很简单,通常是我们需要获得某个类型的定义(definition)。那么接下来的问题就是,在什么情况下我们才需要类型的定义,在什么情况下我们只需要声明就足够了?问题的回答是当我们需要知道这个类型的大小或者需要知道它的函数签名的时候,我们就需要获得它的定义。

假设我们有类型A和类型C,在哪些情况下在A需要C的定义:

  1. A继承至C
  2. A有一个类型为C的成员变量
  3. A有一个类型为C的指针的成员变量
  4. A有一个类型为C的引用的成员变量
  5. A有一个类型为std::list<C>的成员变量
  6. A有一个函数,它的签名中参数和返回值都是类型C
  7. A有一个函数,它的签名中参数和返回值都是类型C,它调用了C的某个函数,代码在头文件中
  8. A有一个函数,它的签名中参数和返回值都是类型C(包括类型C本身,C的引用类型和C的指针类型),并且它会调用另外一个使用C的函数,代码直接写在A的头文件中
  9. C和A在同一个名字空间里面
  10. C和A在不同的名字空间里面


1,没有任何办法,必须要获得C的定义,因为我们必须要知道C的成员变量,成员函数。

2,需要C的定义,因为我们要知道C的大小来确定A的大小,但是可以使用Pimpl惯用法来改善这一点,详情请
看Hurb的Exceptional C++。

3,4,不需要,前置声明就可以了,其实3和4是一样的,引用在物理上也是一个指针,它的大小根据平台不同,可能是32位也可能是64位,反正我们不需要知道C的定义就可以确定这个成员变量的大小。

5,不需要,有可能老式的编译器需要。标准库里面的容器像list, vector,map,
在包括一个list<C>,vector<C>,map<C, C>类型的成员变量的时候,都不需要C的定义。因为它们内部其实也是使用C的指针作为成员变量,它们的大小一开始就是固定的了,不会根据模版参数的不同而改变。

6,不需要,只要我们没有使用到C。

7,需要,我们需要知道调用函数的签名。

8,8的情况比较复杂,直接看代码会比较清楚一些。

            C& doToC(C&);
            C& doToC2(C& c) {return doToC(c);};


从上面的代码来看,A的一个成员函数doToC2调用了另外一个成员函数doToC,但是无论是doToC2,还是doToC,它们的的参数和返回类型其实都是C的引用(换成指针,情况也一样),引用的赋值跟指针的赋值都是一样,无非就是整形的赋值,所以这里即不需要知道C的大小也没有调用C的任何函数,实际上这里并不需要C的定义。

但是,我们随便把其中一个C&换成C,比如像下面的几种示例:

            1.
                C& doToC(C&);
            C& doToC2(C c) {return doToC(c);};
                
                2.
                C& doToC(C);
                C& doToC2(C& c) {return doToC(c);};

                3.
                C doToC(C&);
                C& doToC2(C& c) {return doToC(c);};

                4.
                C& doToC(C&);
                C doToC2(C& c) {return doToC(c);};


无论哪一种,其实都隐式包含了一个拷贝构造函数的调用,比如1中参数c由拷贝构造函数生成,3中doToC的返回值是一个由拷贝构造函数生成的匿名对象。因为我们调用了C的拷贝构造函数,所以以上无论那种情形都需要知道C的定义。

9和10都一样,我们都不需要知道C的定义,只是10的情况下,前置声明的语法会稍微复杂一些。
最后给出一个完整的例子,我们可以看到在两个不同名字空间的类型A和C,A是如何使用前置声明来取代直接包括C的头文件的:
A.h

Cpp代码  收藏代码
  1. #pragma once  
  2.   
  3. #include <list>  
  4. #include <vector>  
  5. #include <map>  
  6. #include <utility>  
  7.   
  8.     //不同名字空间的前置声明方式  
  9. namespace test1  
  10. {  
  11.           class C;  
  12. }  
  13.   
  14.   
  15.   
  16. namespace test2  
  17. {     
  18.        //用using避免使用完全限定名  
  19.     using test1::C;  
  20.       
  21.     class A   
  22.     {  
  23.     public:  
  24.                 C   useC(C);  
  25.             C& doToC(C&);  
  26.             C& doToC2(C& c) {return doToC(c);};  
  27.                            
  28.     private:  
  29.             std::list<C>    _list;  
  30.             std::vector<C>  _vector;  
  31.             std::map<C, C>  _map;  
  32.             C*              _pc;  
  33.             C&              _rc;  
  34.       
  35.     };  
  36. }  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/509782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java中随机数Random和ThreadLocalRandom()用法与区别

package com.test;import java.util.Random; import java.util.concurrent.ThreadLocalRandom;public class M1001{public static void main(String[] args) {Random random new Random();System.out.println("-----------产生1到10之间的随机数----------------");…

Hbase Compaction 源码分析 - RatioBasedCompactionPolicy 策略

目录 类的关系图 ​ RatioBasedCompactionPolicy selectCompaction 方法 getCurrentEligibleFiles方法 skipLargeFiles方法 createCompactionRequest方法 filterBulk方法 applyCompactionPolicy方法 removeExcessFiles方法 setIsMajor方法 其他相关文章 Hbase Compa…

Hbase Compaction 源码分析 - CompactSplitThread 线程池选择

目录 CompactSplitThread requestCompactionInternal方法 selectCompaction方法 requestCompaction方法 其他相关文章 Hbase Compaction 源码分析 - CompactionChecker Hbase Compaction 源码分析 - RatioBasedCompactionPolicy 策略 Hbase Compaction 源码分析 - CompactS…

java如何生成验证码

package com.test;import java.util.Random; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom;public class M1001{public static void main(String[] args) {System.out.println("-----------产生5个随机数的验证码----------------");Strin…

m3u8下载ts 合并成一个视频

我们在用网页看视频时&#xff0c;很多时候视频是下载不下来的&#xff0c;当然这里面有很多技术来防止我们下载视频&#xff0c;接下来我将破解使用m3u8格式来下载视频。一般情况下&#xff0c;我们使用浏览器中Network来查看服务器和本机的数据传输&#xff0c;而视频的原地址…

用行为树的方式思考问题

这段时间做了很多和AI无关的事情&#xff0c;做了个Flash的3D引擎&#xff0c;用汇编写了些shader&#xff0c;做了很多引擎的工具&#xff0c;脚本&#xff0c;插件&#xff0c;游戏也发布了首个预告片&#xff0c;一年多的工作收获满满&#xff0c;职位从AI Engineer变成了En…

Linux常用指令2

linux的命令操作 1、日常操作命令 **查看当前所在的工作目录 pwd **查看当前系统的时间 date **查看有谁在线&#xff08;哪些人登陆到了服务器&#xff09; who 查看当前在线 last 查看最近的登陆历史记录 2、文件系统操作 ** ls / 查看根目录下的子节点&#xff…

qt, connect参数,Qt::DirectConnection,Qt::QueuedConnection

connect用于连接qt的信号和槽&#xff0c;在qt编程过程中不可或缺。它其实有第五个参数&#xff0c;只是一般使用默认值&#xff0c;在满足某些特殊需求的时候可能需要手动设置。 Qt::AutoConnection&#xff1a; 默认值&#xff0c;使用这个值则连接类型会在信号发送时决定。如…

Hbase Compaction 队列数量较大分析

目录 问题 问题原因分析 总结建议 问题 前几天朋友公司Hbase集群出现Compaction队列持续处于比较大的情况&#xff0c;并且mem flush队列也比较大&#xff0c;一起看了下问题&#xff0c;大概情况如下图 从图中可以看出来压缩队列总和持续在1000-2000&#xff0c;平对压缩队列…

java中Date和DateFormat、SimpleDateFormat类

package com.test;import java.text.DateFormat; import java.util.Date; import java.util.Random; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom;public class M1001{public static void main(String[] args) {//Date表示特定的瞬间,精确到毫秒Dat…

Hbase 2.x Region in transition (永久RIT) 异常解决

环境 Hbase 版本&#xff1a;2.0 问题原因 hbase长时间出现RIT&#xff0c;并且发生RIT的Region是已经删除了的Hbase表&#xff0c;表未删除的情况下执行assgin可以消除该问题 Hbase Region in transition (RIT) 异常解决&#xff1a;https://datamining.blog.csdn.net/artic…

sigslot库源码分析

言归正传&#xff0c;sigslot是一个用标准C语法实现的信号与槽机制的函数库&#xff0c;类型和线程安全。提到信号与槽机制&#xff0c;恐怕最容易想到的就是大名鼎鼎的Qt所支持的对象之间通信的模式吧。不过这里的信号与槽虽然在概念上等价与Qt所实现的信号与槽&#xff0c;但…

Mysql for linux 安装

Mysql for linux 安装&#xff1a; 第一步&#xff0c;参考这个链接至第四步为止 https://jingyan.baidu.com/article/cd4c2979209c32756e6e60e1.html 第二步&#xff1a;使用rpm -qa | grep mysql 查询安装状态 第三步&#xff1a;使用/etc/rc.d/init.d/mysqld status 检…

Hue开发指南 - 提交 Spark 程序

目录 Hue开发指南 1.Spark文件打包成一个Jar包提交Hue运行 1.1 上传Spark Jar包至HDFS文件系统 1.2.Hue中创建Spark任务 2.多jar包导入执行&#xff08;依赖jar包与主程序jar包分开打包&#xff09; 2.1 修改worksapce 2.2 添加程序依赖jar包 Hue开发指南 Hue是面向 Had…

如何缩小码农和高手的差距

为什么同样的时间有的人可以漂亮的完成工作&#xff0c;而有些人废了很大的力气也没有完成&#xff1f;前者我们常常称之为“大牛”&#xff0c;后者我们常常叫他们“菜鸟”。当然“大牛”都是相对而言的&#xff0c;“大牛”也不可能方方面面都非常厉害&#xff0c;换句话说大…

OpenResty 安装,收集日志保存到文本文件

目录 安装 1.安装相关类库 2.安装编译openresty 3.编写配置启动openresty服务 4.通过 openresty 保存日志数据到系统 安装 1.安装相关类库 yum install -y readline-devel pcre-devel openssl-devel gcc 2.安装编译openresty wget https://openresty.org/download/open…

暗时间-领悟

作者&#xff1a;排长链接&#xff1a;https://www.zhihu.com/question/20689852/answer/23227406来源&#xff1a;知乎著作权归作者所有&#xff0c;转载请联系作者获得授权。第一次看到“暗时间”这个词&#xff0c;我的第一反应是有点不屑&#xff0c;又是一个概念噱头吧。直…

Openresty Nginx 负载均衡

目录 OpenResty Openresty 服务配置文件 启动Openresty服务 测试调用接口 Nginx 负载均衡服务 nginx 配置文件 启动服务 实现功能 测试结果 这里实现个简单的负载均衡&#xff0c;只做功能展示&#xff08;实际业务比这复杂高&#xff0c;单台服务器无法满足需求的情况…

总结--美丽的敷衍

我看新年 转眼间&#xff0c;2015已经悄然离去&#xff0c;正像2015年刚来时候感叹2014的白驹过隙。年年岁岁花相似&#xff0c;岁岁年年人不同。时间过去了&#xff0c;自己又有如何的不同呢&#xff1f;客观的说&#xff0c;这一年有了一定的长进&#xff0c;但总感觉失去/错…

Hadoop Yarn REST API未授权漏洞利用挖矿分析

目录 一、背景情况 二、 漏洞说明 攻击步骤&#xff1a; 三、入侵分析 四、安全建议 清理病毒 安全加固 五、IOCs 一、背景情况 5月5日腾讯云安全曾针对攻击者利用Hadoop Yarn资源管理系统REST API未授权漏洞对服务器进行攻击&#xff0c;攻击者可以在未授权的情况…