C++知识拾遗

最近开始看各种“宝典”了,发现自己有好多C++死角,赶紧查漏补缺,见招拆招吧。

1、外部链接和内部链接。

在C++中,外部链接主要包括全局变量(非static,非const)和非静态自由函数(非类成员函数),类的非inline函数(包括静态和非静态),类的静态数据成员(这也是为什么类的static数据成员必须在类定义体外定义)。他们在一个编译单元中定义,其它编译单元通过extern声明访问它们,在整个程序中这些外部链接的变量和函数共用相同的地址;

const,static变量和static函数,,类的定义,enum定义,Union定义,inline函数定义,类的inline函数定义,typedef声明是内部链接,即它们的作用域只存在于定义它们的编译单元,在其它单元可以定义同名的变量而不会发生冲突。

这也就是为什么在头文件中可以添加const变量的定义,对非const变量却只能用声明。类的静态数据成员不能再函数体内部定义,也不能在类的构造函数里定义,就是这个道理。但是整型的const static成员可以在类定义体内部初始化,这个问题还不知道怎么解释。

2、命名空间使用的注意事项

命名空间可以在全局作用域或其它作用域内部定义,但不能在函数或类内部定义。

命名空间可以在几个部分定义,但是名字只在声明名字的文件中可见,这一常规限制继续应用。

3、ptrdiff_t

用来表示两个指向同一数组的指针进行减法操作的类型。它在cstddef头文件定义,是一个signed整型

4、字节对齐

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。

这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以VC6为例,以后不再说明):

struct S1

{

char c;

int i;

}

struct S3

{

char c1;

S1 s;

char c2;

};

S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。

c1的偏移量为0,s的偏移量呢这时s是一个整体,它作为结构体变量也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。

到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以直接修改/Zp编译开关。#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么 该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,

公式如下:

offsetof( item ) = min( n, sizeof( item ) )

再看示例:

#pragma pack(push) // 将当前pack设置压栈保存

#pragma pack(2) // 必须在结构体定义之前使用

struct S1

{

char c;

int i;

};

struct S3

{

char c1;

S1 s;

char c2;

};

#pragma pack(pop) // 恢复先前的pack设置

计算sizeof(S1)时,min(2, sizeof(i))的值为2,所以i的偏移量为2,加上sizeof(i)等于6,能够被2整除,所以整个S1的大小为6。

同样,对于sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能被2整除,添加一个填充字节,所以sizeof(S3)等于10。



现在,朋友们可以轻松的出一口气了,:)

还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下:

struct S5 { };

对于类的sizeof大小,主要看它的非静态数据成员以及是否有虚函数(无论是继承还是自己本身有的),有虚函数则添加一个4字节的虚函数表指针,如果是多重继承(n重),则应该加n*4

对于联合体来说,只要求整个联合体的大小是最宽数据类型的整数倍。

结构体有位域的情况下,也不要忘记满足成员偏移量和整个结构体大小是对齐字节整数倍这一限制

位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,

允许其它类型类型的存在。使用位域的主要目的是压缩存储,其大致规则为:

1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;

4) 如果位域字段之间穿插着非位域字段,则不进行压缩;

5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

5、extern "C"

#ifdef __cplusplus 
extern "C" { 
#endif 

//一段代码 

#ifdef __cplusplus 
} 
#endif

这段代码的含义是:如果当前代码是C++代码,那么加入extern "C"{和}处理其中的代码。

extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等

C++代码调用c编写的模块,需要

extern"C"{

 #include"***.h"

}

或者
extern"C" {

  对C模块中函数的声明;
}

c调用C++编写的模块,在C源文件中不能出现extern"C",只能在C++文件中添加extern"C"声明,表示相关函数用c进行编译和链接。

这里有篇讲的很细致的文章:http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html

注:__cplusplus是C++的一个宏,一个后缀名是CPP的文件自然地定义了这个宏。

6、#ifdef和#if defined

ifdef NAME == #if defined(NAME)

ifndef NAME == #if !defined(NAME)

在处理多重预定义时

#ifdef A
#ifdef B
.......

#endif
#endif    等价于

#if defined(A)&& if defined(B)
#endif

7、动态库和静态库

动态库(Dynamic Link Library abbr,DLL)技术是程序设计中经常采用的技术。其目的减少程序的大小,节省空间,提高效率,具有很高的灵活性。采用动态库技术对于升级软件版本更加 容易。与静态库(Static Link Library)不同,动态库里面的函数不是执行程序本身的一部分,而是根据执行需要按需载入,其执行代码可以同时在多个程序中共享。

程序编译一般需经预处理、编译、汇编和链接几个步骤。在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中。这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。

静态库和动态库是两种共享程序代码的方式,它们的区别是:静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。使用动态库的优点是系统只需载入一次动态库,不同的程序可以得到内存中相同的动态库的复本,因此节省了很多内存。

8、assert()宏

define NDEBUG:用来关闭assert()宏

NDEBUG宏定义必须在头文件assert.h引入“之前”,才起到阻止assert宏起作用的效果 !有些人的文章中写到在release模式下默认定义了#define NDEBUG,在vs2008下测试后发现不是这样的。也就是说,debug和release中assert宏都会得到展开,除非手动显式定义了#define NDEBUG。

debug版本会自动的定义_DEBUG宏,在此条件下_ASSERT宏才可使用,而release下_DEBUG宏被关闭。
原文链接: https://www.cnblogs.com/Ranger98/archive/2012/03/04/2379382.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/43215

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 下午7:55
下一篇 2023年2月8日 下午7:56

相关推荐