一、C/C++语言 struct深层探索
1.自然对界
struct是一种复合数据类型,其构成元素既可以是基本数据类型(如 int、long、float等)的变量,也可以是一些复合数据类型(如 array、stuct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。
缺少情况下,编译器为结构体的每个成员按其自然对界(natural alignment:即默认对齐方式,是指按结构体成员中 size最大的成员对齐)条件分配空间。各个成员按照它们被声明的顺序在内存中的顺序存储,第一个成员的地址和整个结构的地址相同。
2.指定对界
一般地,可以能过下面的方法来改变缺少的对界条件
- 使用伪指令 #prama pack(n),编译器将按照 n个字节对齐
- 使用伪指令 #prama pack(),取消自定义字节对齐方式
注意:如果 #pragma pack(n)中指定的 n大于结构体中最大成员的 size,则其不起作用,结构体仍然按照 size最大的成员进行对界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n为2 时,其发挥了作用,使得sizeof(naturalalign)的结果为6。
3.C 和 C++间 struct 的深层区别
在C++语言中struct具有了“类” 的功能,其与关键字class的区别在于struct中成员变量 和函数的默认访问权限为public,而class的为private。
C++中的struct保持了对C中struct的全面兼容(这符合C++的初衷——“a better c”),
因而,下面的操作是合法的:
//定义struct
struct structA
{
char a;
char b;
int c;
};
structA a = {'a' , 'a' ,}; // 定义时直接赋初值
即struct可以在定义的时候直接以{ }对其成员变量赋初值,而class则不能
4.struct 编程注意事项
在 C 语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将 2 个实例中的指针型成员指向了同一片内存。
在C++语言中,当结构体中存在指针型成员时,我们需要重写struct的拷贝构造函数并进行“=” 操作符重载。
二、C++中 extern "C"含义深层探索
1.引言
C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程 式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支 持函数的重载,C++对全局函数的处理方式与C有明显的不同。
2.从标准头文件说起
某企业曾经给出如下的一道面试题:
面试题:为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */
分析
显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。
那么下面代码的作用又是什么呢?
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
3.深层揭密 extern "C"
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
(1)被extern "C"限定的函数或变量是extern类型的;
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
extern int a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函
数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A
编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
(2)被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。
未加 extern “C”声明时的编译方式
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不
同,但是都采用了相同的机制,生成的新名字称为“mangled
name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函
数void foo( int x, int y )与void foo( int x, float y
)编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质
上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加 extern "C"声明时的连接方式
假设在C++中,模块A的头文件如下:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(,);
实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
加 extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A的头文件变为:
在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。
所以,可以用一句话概括extern
“C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么
做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题)
实现 C++与C 及其它语言的混合编程。
明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
4.extern "C"的惯用法
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
编写一个C++引用C函数例子工程中包含的三个文件的源代码如下:
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
编写一个C引用C++函数例子工程中包含的三个文件的源代码如下:
如果深入理解了上面所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。
对示例代码,需要特别留意各个细节。
三、C 语言高效编程的几招
第 1 招:以空间换时间
例如:字符串的赋值。
方法 A,通常的办法:
方法 B:
从上面的例子可以看出,A 和 B 的效率是不能比的。在同样的存储空间下,B 直接使用指针就可以操作了,而 A 需要调用两个字符函数才能完成。
B 的缺点在于灵活性没有A 好。在需要频繁更改一个字符串内容的时候,A 具有更好的灵活性;如果采用方法 B,则需要预存许多字符串,虽然占用了 大量的内存,但是获得了程序执行的高效率。
如果系统的实时性要求很高,内存还有一些,那么推荐使用该招数。
该招数的边招:使用宏函数而不是函数。
函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。
大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU 也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些 CPU 时间。
而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。
第 2 招:数学方法解决问题
数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序的执行效率有数量级的提高。
举例如下,求 1~100 的和。
方法 E
方法 F
方法 E 循环了 100 次才解决问题,也就是说最少用了 100 个赋值、100 个判断、200 个加法(I 和 j) ;而方法 F 仅仅用了 1 个加法、1 个乘法、1 次除法。效果自然不言而喻。
第 3 招:使用位操作
使用位操作,减少除法和取模的运算。
在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。
举例如下:
方法 G
方法 H
在字面上好象 H 比G 麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法 G
调用了基本的取模函数和除法函数,既有函数调用,还有很多汇编代码和寄存器参与运算;而方法 H
则仅仅是几句相关的汇编,代码更简洁、效率更高。当然,由于编译器的不同,可能效率的差距不大,但是,以目前的 MS C,ARM C
来看,效率的差距还是不小。相关汇编代码就不列举了。
运用这招需要注意的是,因为 CPU 的不同而产生的问题。比如说,在 PC 上用这招编写的程序,并在 PC 上调试通过,在移植到一个 16 位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。
第 4 招:汇编嵌入
在熟悉汇编语言的人眼里,C 语言编写的程序都是垃圾”。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方法:嵌入汇编、混合编程。
举例如下,将数组一赋值给数组二,要求每一个字节都相符。char string1[1024], string2[1024];
方法 I
方法 J
方法 I 是最常见的方法,使用了 1024 次循环;方法 J 则根据平台不同做了区分,在 ARM 平台下,用嵌入汇编仅用
128次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为 0
的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于 LCD 数据的拷贝过程。根据不同的
CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。
虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在不同平台移植的过程中,卧虎藏龙、险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才可以采用。切记。
摘自《单片机与嵌入式系统应用》