C指针定义
指针的定义的规则如下:
原有声明(函数声明也是声明)的地方, 在变量前面添加 * 即可.
如:
int a; ----> int *a; # 指向int类型的指针a
int a[10]; ----> int *a[10]; #指向int [10] 类型的指针a
void fun(int , int) ----> void (*func)(int,int); # 指向函
数的指针.
注意, 声明是不包含具体定义的
如
1. int a = 10
2. void func(int a,int b){
printf(“%d %d”,a,b)
}
都包含了定义, 不为声明, 所以不能简单的添加*修改为指针类型!
C 函数指针
一个简单的指针函数
定义函数指针: void (*ptrFunc) (int,int )
从这段代码中 我们知道
方法一:
int Max() 为一个定义了的函数类型 类似 int max ;
P的赋值使用&max的方法. 类似 int *p = &max;
而调用函数指针p ,使用 (*p)(a,b)
也请注意,C语言本身提供了一种简写方式如下:
方法二
p = max; // 赋值(assignment)操作
p(2, 3); // 函数调用操作
注意, 推荐使用方法一,这样可以理解函数指针方便
C语言右左法则
------适用于对类型的解析
右左法则:首先从最里面的圆括号看起,然后往右看,再往左看.每当遇到圆括号时,就应该掉转阅读方向.一旦解析完圆括号里面所有的东西,就跳出圆括号.重复这个过程直到整个声明解析完毕.
注: 对于函数,返回值的类型不止是简单的int,long等类型,还可以是函数指针或者结构体.
笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个.
现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:
int (*func)(int *p);
首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看
右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是 int.
int (*func)(int *p, int (*f)(int*));
func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型.再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int.
int (*func[5])(int *p);
func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰 func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5].跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int.
int (*(*func)[5])(int *p);
func被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针.总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数.
int (*(*func)(int *p))[5];
func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组.
要注意有些复杂指针声明是非法的,例如:
int func(void) [5];
func是一个返回值为具有5个int元素的数组的函数.但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组.
int func[5](void);
func是一个具有5个元素的数组,这个数组的元素都是函数.这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的.
作为练习,下面列几个复杂指针声明给读者自己来解析.
int (*(*func)[5][6])[7][8];
int (*(*(*func)(int *))[5])(int *);
int (*(*func[7][8][9])(int*))[5];
实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害.应该用typedef来对声明逐层
分解,增强可读性,例如对于声明:
int (*(*func)(int *p))[5];
可以这样分解:
typedef int (*PARA)[5];
typedef PARA (*func)(int *);
这样就容易看得多了.
答案,同时给出用typedef的分解方法:
int (*(*func)[5][6])[7][8];
func是一个指向数组的指针,这类数组的元素是一个具有5X6个int元素的二维数组,而这个二维数组的元素又是一个二维数组.
typedef int (*PARA)[7][8];
typedef PARA (*func)[5][6];
int (*(*(*func)(int *))[5])(int *);
func是一个函数指针,这类函数的返回值是一个指向数组的指针,所指向数组的元素也是函数指针,指向的函数具有int*形参,返回值为int.
typedef int (*PARA1)(int*);
typedef PARA1 (*PARA2)[5];
typedef PARA2 (*func)(int*);
int (*(*func[7][8][9])(int*))[5];
func是一个数组,这个数组的元素是函数指针,这类函数具有int*的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素
的数组.
typedef int (*PARA1)[5];
typedef PARA1 (*PARA2)(int*);
typedef PARA2 func[7][8][9];
运算符优先级
-------适用于对表达式解析
优先级 |
运算符 |
名称或含义 |
使用形式 |
结合方向 |
说明 |
1 |
[] |
数组下标 |
数组名[常量表达式] |
左到右 |
|
() |
圆括号 |
(表达式)/函数名(形参表) |
|||
. |
成员选择(对象) |
对象.成员名 |
|||
-> |
成员选择(指针) |
对象指针->成员名 |
|||
2 |
- |
负号运算符 |
-表达式 |
右到左 |
单目运算符 |
(类型) |
强制类型转换 |
(数据类型)表达式 |
|||
++ |
自增运算符 |
++变量名/变量名++ |
单目运算符 |
||
-- |
自减运算符 |
--变量名/变量名-- |
单目运算符 |
||
* |
取值运算符 |
*指针变量 |
单目运算符 |
||
& |
取地址运算符 |
&变量名 |
单目运算符 |
||
! |
逻辑非运算符 |
!表达式 |
单目运算符 |
||
~ |
按位取反运算符 |
~表达式 |
单目运算符 |
||
typeof |
获得结构体成员类型 |
sizeof(p->member) |
C99扩展 |
||
sizeof |
长度运算符 |
sizeof(表达式) |
|||
3 |
/ |
除 |
表达式/表达式 |
左到右 |
双目运算符 |
* |
乘 |
表达式*表达式 |
双目运算符 |
||
% |
余数(取模) |
整型表达式/整型表达式 |
双目运算符 |
||
4 |
+ |
加 |
表达式+表达式 |
左到右 |
双目运算符 |
- |
减 |
表达式-表达式 |
双目运算符 |
||
5 |
<< |
左移 |
变量<<表达式 |
左到右 |
双目运算符 |
>> |
右移 |
变量>>表达式 |
双目运算符 |
||
6 |
> |
大于 |
表达式>表达式 |
左到右 |
双目运算符 |
>= |
大于等于 |
表达式>=表达式 |
双目运算符 |
||
< |
小于 |
表达式<表达式 |
双目运算符 |
||
<= |
小于等于 |
表达式<=表达式 |
双目运算符 |
||
7 |
== |
等于 |
表达式==表达式 |
左到右 |
双目运算符 |
!= |
不等于 |
表达式!= 表达式 |
双目运算符 |
||
8 |
& |
按位与 |
表达式&表达式 |
左到右 |
双目运算符 |
9 |
^ |
按位异或 |
表达式^表达式 |
左到右 |
双目运算符 |
10 |
| |
按位或 |
表达式|表达式 |
左到右 |
双目运算符 |
11 |
&& |
逻辑与 |
表达式&&表达式 |
左到右 |
双目运算符 |
12 |
|| |
逻辑或 |
表达式||表达式 |
左到右 |
双目运算符 |
13 |
?: |
条件运算符 |
表达式1? 表达式2: 表达式3 |
右到左 |
三目运算符 |
14 |
= |
赋值运算符 |
变量=表达式 |
右到左 |
|
/= |
除后赋值 |
变量/=表达式 |
|||
*= |
乘后赋值 |
变量*=表达式 |
|||
%= |
取模后赋值 |
变量%=表达式 |
|||
+= |
加后赋值 |
变量+=表达式 |
|||
-= |
减后赋值 |
变量-=表达式 |
|||
<<= |
左移后赋值 |
变量<<=表达式 |
|||
>>= |
右移后赋值 |
变量>>=表达式 |
|||
&= |
按位与后赋值 |
变量&=表达式 |
|||
^= |
按位异或后赋值 |
变量^=表达式 |
|||
|= |
按位或后赋值 |
变量|=表达式 |
|||
15 |
, |
逗号运算符 |
表达式,表达式,… |
左到右 |
从左向右顺序运算 |
说明:
只有在出现同一优先级并列的时候, 如 p->a->b , a.b.c . 才会使用结合性定律.
左右法则和运算符优先级代表两个不同方向的问题, 前者是对数据类型的分析而后者是对表达式运算的解析.
C宏
在将一个C源程序转换为可执行程序的过程中, 编译预处理是最初的步骤. 这一步骤是由预处理器(preprocessor)来完成的. 在源流程序被编译器处理之前, 预处理器首先对源程序中的"宏(macro)"进行处理.
C初学者可能对预处理器没什么概念, 这是情有可原的: 一般的C编译器都将预处理, 汇编, 编译, 连接过程集成到一起了. 编译预处理往往在后台运行. 在有的C编译器中, 这些过程统统由一个单独的程序来完成, 编译的不同阶段实现这些不同的功能. 可以指定相应的命令选项来执行这些功能. 有的C编译器使用分别的程序来完成这些步骤. 可单独调用这些程序来完成. 在gcc中, 进行编译预处理的程序被称为CPP, 它的可执行文件名为cpp.
编译预处理命令的语法与C语言的语法是完全独立的. 比如: 你可以将一个宏扩展为与C语法格格不入的内容, 但该内容与后面的语句结合在一个若能生成合法的C语句, 也是可以正确编译的.
一. 预处理命令简介
预处理命令由#(hash字符)开头, 它独占一行, #之前只能是空白符. 以#开头的语句就是预处理命令, 不以#开头的语句为C中的代码行. 常用的预处理命令如下:
#define 定义一个预处理宏
#undef 取消宏的定义
#include 包含文件命令
#include_next 与#include相似, 但它有着特殊的用途
#if 编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef 判断某个宏是否被定义, 若已定义, 执行随后的语句
#ifndef 与#ifdef相反, 判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif 之后的语句, 相当于C语法中的else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else
之后的语句, 相当于C语法中的else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
表示宏的换行
defined 与#if, #elif配合使用, 判断某个宏是否被定义
Eg.
#if defined __MSYS__
#line 标志该语句所在的行号
# 将宏参数替代为以参数值为内容的字符窜常量
## 将两个相邻的标记(token)连接为一个单独的标记
#pragma 说明编译器信息
#warning 显示编译警告信息
#error 显示编译错误信息
二. 预处理的文法
预处理并不分析整个源代码文件, 它只是将源代码分割成一些标记(token), 识别语句中哪些是C语句, 哪些是预处理语句. 预处理器能够识别C标记, 文件名, 空白符, 文件结尾标志.
预处理语句格式: #command name(var, ...) token(s)
1, command预处理命令的名称, 它之前以#开头, #之后紧随预处理命令, 标准C允许#两边可以有空白符, 但比较老的编译器可能不允许这样. 若某行中只包含#(以及空白符), 那么在标准C中该行被理解为空白. 整个预处理语句之后只能有空白符或者注释, 不能有其它内容.
2, name代表宏名称, 它可带参数. 参数可以是可变参数列表(C99).
3, 语句中可以利用""来换行.
e.g.
# define ONE 1
等价于: #define ONE 1
#define err(flag, msg) if(flag)
printf(msg)
等价于: #define err(flag, msg) if(flag) printf(msg)
三. 预处理命令详述
1. #define
#define命令定义一个宏:
#define MACRO_NAME(args) tokens(opt)
之后出现的MACRO_NAME将被替代为所定义的标记(tokens). 宏可带参数, 而后面的标记也是可选的.
对象宏
不带参数的宏被称为"对象宏(objectlike macro)"
Eg..
#define __MSYS__
#define经常用来定义常量, 此时的宏名称一般为大写的字符串. 这样利于修改这些常量.
e.g.
#define MAX 100
int a[MAX];
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
#define __FILE_H__ 中的宏就不带任何参数, 也不扩展为任何标记. 这经常用于包含头文件.
要调用该宏, 只需在代码中指定宏名称, 该宏将被替代为它被定义的内容.
函数宏--就像函数一样被使用!
带参数的宏也被称为"函数宏". 利用宏可以提高代码的运行效率: 子程序的调用需要压栈出栈, 这一过程如果过于频繁会耗费掉大量的CPU运算资源. 所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率.
#define max(a,b) a+b
注意: 函数宏对参数类型是不敏感的, 你不必考虑将何种数据类型传递给宏. 那么, 如何构建对参数类型敏感的宏呢? 参考本章的第九部分, 关于"##"的介绍.
关于定义宏的另外一些问题
(1) 宏可以被多次定义, 前提是这些定义必须是相同的. 这里的"相同"要求先后定义中空白符出现的位置相同, 但具体的空白符类型或数量可不同, 比如原先的空格可替换为多个其他类型的空白符: 可为tab, 注释...
e.g.
#define NULL 0
#define NULL 0
上面的重定义是相同的, 但下面的重定义不同:
#define fun(x) x+1
#define fun(x) x + 1 或: #define fun(y) y+1
如果多次定义时, 再次定义的宏内容是不同的, gcc会给出"NAME redefined"警告信息.
应该避免重新定义函数宏, 不管是在预处理命令中还是C语句中, 最好对某个对象只有单一的定义. 在gcc中, 若宏出现了重定义, gcc会给出警告.
(2) 在gcc中, 可在命令行中指定对象宏的定义:
e.g.
$ gcc -Wall -DMAX=100 -o tmp tmp.c
相当于在tmp.c中添加" #define MAX 100".
那么, 如果原先tmp.c中含有MAX宏的定义, 那么再在gcc调用命令中使用-DMAX, 会出现什么情况呢?
---若-DMAX=1, 则正确编译.
---若-DMAX的值被指定为不为1的值, 那么gcc会给出MAX宏被重定义的警告, MAX的值仍为1.
注意: 若在调用gcc的命令行中不显示地给出对象宏的值, 那么gcc赋予该宏默认值(1), 如: -DVAL == -DVAL=1
(3) #define所定义的宏的作用域
宏在定义之后才生效, 若宏定义被#undef取消, 则#undef之后该宏无效. 并且字符串中的宏不会被识别
e.g.
#define ONE 1
sum = ONE + TWO
#define TWO 2
sum = ONE + TWO
#undef ONE
sum = ONE + TWO
char c[] = "TWO"
(4) 宏的替换可以是递归的, 所以可以嵌套定义宏.
e.g.
# define ONE NUMBER_1
# define NUMBER_1 1
int a = ONE
2. #undef
#undef用来取消宏定义, 它与#define对立:
#undef name
如够被取消的宏实际上没有被#define所定义, 针对它的#undef并不会产生错误.
当一个宏定义被取消后, 可以再度定义它.
3. #if, #elif, #else, #endif
#if, #elif, #else, #endif用于条件编译:
#if 常量表达式1
语句...
#elif 常量表达式2
语句...
#elif 常量表达式3
语句...
...
#else
语句...
#endif
4. #ifdef, #ifndef #if defined.
#ifdef, #ifndef, defined用来测试某个宏是否被定义
#ifdef name 或 #ifndef name
它们经常用于避免头文件的重复引用:
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
defined(name): 若宏被定义,则返回1, 否则返回0.
它与#if, #elif, #else结合使用来判断宏是否被定义, 乍一看好像它显得多余, 因为已经有了#ifdef和#ifndef. defined用于在一条判断语句中声明多个判别条件:
#if defined(VAX) && defined(UNIX) && !defined(DEBUG)
和#if, #elif, #else不同, #indef, #ifndef, defined测试的宏可以是对象宏, 也可以是函数宏. 在gcc中使用"-Wundef"选项不会显示宏未定义的警告信息.
5. #include , #include_next
#include用于文件包含. 在#include 命令所在的行不能含有除注释和空白符之外的其他任何内容.
#include "headfile"
#include <headfile>
#include 预处理标记
前面两种形式大家都很熟悉, "#include 预处理标记"中, 预处理标记会被预处理器进行替换, 替换的结果必须符合前两种形式中的某一种.
根据 Gcc中 -I 路径查询,有默认路径和指定路径
6. 预定义宏(标准宏)
标准C中定义了一些对象宏, 这些宏的名称以"__"开头和结尾, 并且都是大写字符. 这些预定义宏可以被#undef, 也可以被重定义.
下面列出一些标准C中常见的预定义对象宏(其中也包含gcc自己定义的一些预定义宏:
__LINE__ 当前语句所在的行号, 以10进制整数标注.
__FILE__ 当前源文件的文件名, 以字符串常量标注.
__DATE__ 程序被编译的日期, 以"Mmm dd yyyy"格式的字符串标注.
__TIME__ 程序被编译的时间, 以"hh:mm:ss"格式的字符串标注, 该时间由asctime返回.
__STDC__ 如果当前编译器符合ISO标准, 那么该宏的值为1
__STDC_VERSION__ 如果当前编译器符合C89, 那么它被定义为199409L, 如果符合C99, 那么被定义为199901L.
我用gcc, 如果不指定-std=c99, 其他情况都给出__STDC_VERSION__未定义的错误信息, 咋回事呢?
__STDC_HOSTED__ 如果当前系统是"本地系统(hosted)", 那么它被定义为1. 本地系统表示当前系统拥有完整的标准C库.
gcc定义的预定义宏:
__OPTMIZE__ 如果编译过程中使用了优化, 那么该宏被定义为1.
__OPTMIZE_SIZE__ 同上, 但仅在优化是针对代码大小而非速度时才被定义为1.
__VERSION__ 显示所用gcc的版本号.
可参考"GCC the complete reference".
要想看到gcc所定义的所有预定义宏, 可以运行: $ cpp -dM /dev/null
7.#line
#line用来修改__LINE__和__FILE__.
e.g.
printf("line: %d, file: %s ", __LINE__, __FILE__);
#line 100 "haha"
printf("line: %d, file: %s ", __LINE__, __FILE__);
printf("line: %d, file: %s ", __LINE__, __FILE__);
显示:
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha
8. #pragma, _Pragma
#pragma用编译器用来添加新的预处理功能或者显示一些编译信息. #pragma的格式是各编译器特定的, gcc的如下:
#pragma GCC name token(s)
#pragma之后有两个部分: GCC和特定的pragma name. 下面分别介绍gcc中常用的.
(1) #pragma GCC dependency
dependency测试当前文件(既该语句所在的程序代码)与指定文件(既#pragma语句最后列出的文件)的时间戳. 如果指定文件比当前文件新, 则给出警告信息.
e.g.
在demo.c中给出这样一句:
#pragma GCC dependency "temp-file"
然后在demo.c所在的目录新建一个更新的文件: $ touch temp-file, 编译: $ gcc demo.c 会给出这样的警告信息: warning: current file is older than temp-file
如果当前文件比指定的文件新, 则不给出任何警告信息.
还可以在在#pragma中给添加自定义的警告信息.
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file
注意: 后面新增的警告信息要用""引用起来, 否则gcc将给出警告信息.
(2) #pragma GCC poison token(s)
若源代码中出现了#pragma中给出的token(s), 则编译时显示警告信息. 它一般用于在调用你不想使用的函数时候给出出错信息.
e.g.
#pragma GCC poison scanf
scanf("%d", &a);
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意, 如果调用了poison中给出的标记, 那么编译器会给出的是出错信息. 关于第一条警告, 我还不知道怎么避免, 用""将token(s)引用起来也不行.
(3) #pragma GCC system_header
从#pragma GCC system_header直到文件结束之间的代码会被编译器视为系统头文件之中的代码. 系统头文件中的代码往往不能完全遵循C标准, 所以头文件之中的警告信息往往不显示. (除非用 #warning显式指明).
(这条#pragma语句还没发现用什么大的用处)
由于#pragma不能用于宏扩展, 所以gcc还提供了_Pragma:
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由于预处理之进行一次宏扩展, 采用上面的方法会在编译时引发错误, 要将#pragma语句定义成一个宏扩展, 应该使用下面的_Pragma语句:
#define PRAGMA_DEP _Pragma("GCC dependency "temp-file"")
注意, ()中包含的""引用之前引该加上转义字符.
9. #, ##
#和##用于对字符串的预处理操作, 所以他们也经常用于printf, puts之类的字符串显示函数中.
#用于在宏扩展之后将tokens转换为以tokens为内容的字符串常量.
e.g.
#define TEST(a,b) printf( #a "<" #b "=%d ", (a)<(b));
注意: #只针对紧随其后的token有效!
##用于将它前后的两个token组合在一起转换成以这两个token为内容的字符串常量.
注意##前后必须要有token.
10 #warning, #error
#warning, #error分别用于在编译时显示警告和错误信息, 格式如下:
#warning tokens
#error tokens
e.g.
#warning "some warning"
注意, #error和#warning后的token要用""引用起来!
(在gcc中, 如果给出了warning, 编译继续进行, 但若给出了error, 则编译停止. 若在命令行中指定了 -Werror, 即使只有警告信息
11. 展开和不展开
宏处理器在展开宏的时候, 对宏的展开是按照一定规则进行的.
1.宏不会再次展开自己的宏如:
Define: #define TEST( x ) ( x + TEST( x ) )
Usage: TEST(1) ----> 1 + TEST(1)
2.宏函数中如果在宏体中发现#arg, 则不会对参数进行扩展.
Define: #define PARAM( x ) #x 对参数使用#
#define ADDPARAM( x ) INT_##x
Usage:
PARAM( ADDPARAM( 1 ) ) ----- > "ADDPARAM( 1 )"
而
Define: #define PARAM( x ) x 没有使用#
#define ADDPARAM( x ) INT_##x
Usage:
PARAM( ADDPARAM( 1 ) );
INT_1
函数调用
调用一个函数库有两个注意点:
1. 函数的调用方式,是C 方式(cedel) 还是 stdcall 方式 还是其他
2. 函数名生成方式,是C++方式呢 还是C方式
3. 编译器问题
函数调用时,堆栈的变化:
调用约定:
__cdecl:c declare(c调用约定)的缩写,是c和c++程序的缺省调用方式,规则是,从右又向左的顺序压参数入,由调用者把参数弹出栈,对于传入参数的内存栈是由调用者来维护的,正因为如此,只有这种调用方式可以实现个数不定的入口参数(可变参数).
__stdcall:是pascal程序的缺省调用方式,规则是,按从右向左的顺序入栈,被调用的函数在返回前清理传送参数的内存栈.
上两者的主要区别是前者由调用者清理栈,后者由被调用的函数清理栈.当然函数名的修饰部分也是不同的.
__fastcall:采用寄存器传递参数,特点就是快了.
C/C++约定:
C文件不会根据函数名的参数和返回值修改函数名
C++会根据函数名的参数和返回值进行修改函数名
下表是VC和MingW(Gcc)的函数名生成的区别(C语言):
在编译的时候会根据函数的调用规则生成内部函数名,链接为可执行程序的时候,使用外部函数名作为导入/导出表的函数名.
链接阶段使用这个内部函数名进行链接库,而使用外部函数名链接dll或者so等可执行文件.
在编译器处理过程中, 统一把变量和函数都认为是标记, 都按照约定进行变化名称.
extern 概要
-----C 部分
在一个源文件里定义了一个数组:char a[6];在另外一个文件里用下列语句进
行了声明:extern char *a;请问,这样可以吗?
答案与分析:
1)、不可以,程序运行时会告诉你非法访问.原因在于,指向类型T的指针并不等价于类型T的数组.extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问.应该将声明改为extern char a[ ].
2)、例子分析如下,如果a[] = "abcd",则外部变量a=0x12345678 (数组的起始地址),而*a是重新定义了一个指针变量a的地址可能是0x87654321,直接使用*a是错误的.
3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜.
4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明.
这个关键字真的比较可恶,在声明(函数)的时候,这个extern居然可以被省略,所以会让你搞不清楚到底是声明还是定义,下面分变量和函数两类来说:
尤其是对于变量来说.
extern int a;//声明一个全局变量a,a的定义在其他源文件中
int a; //定义一个全局变量a
extern int a =0 ;//定义一个全局变量a 并给初值.
int a =0;//定义一个全局变量a,并给初值,
声明之后你不能直接使用这个变量,需要定义之后才能使用.
第四个等于第三个,都是定义一个可以被外部使用的全局变量,并给初值.
糊涂了吧,他们看上去可真像.但是定义只能出现在一处.也就是说,不管是int a;还是extern int a=0;还是int a=0;都只能出现一次,而那个extern int a可以出现很多次.
当你要引用一个全局变量的时候,你就要声明extern int a;这时候extern不能省略,因为省略了,就变成int a;这是一个定义,不是声明.
extern 的编译、链接
声明外部变量
现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是互相不透明的,也就是说,在编译时,全局变量的可见域限制在文件内部(而链接的时候全局可见).这就导致如果在两个源文件定义同样的变量i也不会报错.这就误用了原先的含义了,这时候就需要使用extern 关键字完成对全局变量的声明使用.
函数
常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
答案与分析:
如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用.即下述两个函数声明没有明显的区别:
extern int f(); 和int f();
当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰.
extern function 也有可能 引用的函数是在某个lib库中
extern “C”
-----C++部分
在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
答案与分析:
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名.
C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同.作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数.但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同.
因为C++在为了支持重载,而在编译的时候对函数名和变量名的规则不同于C语言的方式,就导致C++模块的代码和C模块的代码在相互引用的时候出现函数无法找到之类的错误,其根本原因就是命名方式的改变.而值得注意的是C++的函数调用参数载入方式也不同与C.
这里使用extern "c" 就是声明函数的编译的时候按照C语言的方式编译,就可以使得C++代码和C代码的兼容了.
下面是一个标准的写法:
//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif /* __cplusplus */
…
…
//.h文件结束的地方
#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif /* __cplusplus */
typedef基本概念剖析
几个函数指针
int* (*a[5])(int, char*); //#1
void (*b[10]) (void (*)()); //#2
1.C语言中函数声明和数组声明.函数声明一般是这样:
int fun(int, double);
对应函数指针(pointer to function)的声明是这样:
int (*pf)(int, double);
可以这样使用:
pf = &fun; //赋值(assignment)操作
(*pf)(5, 8.9);//函数调用操作
也请注意,C语言本身提供了一种简写方式如下:
pf = fun; // 赋值(assignment)操作
pf(5, 8.9); // 函数调用操作
不过我本人不是很喜欢这种简写,它对初学者带来了比较多的迷惑.
数组声明一般是这样:
int a[5];
对于数组指针(pointer to array)的声明是这样:
int (*pa)[5];
可以这样使用:
pa = &a; // 赋值(assignment)操作
int i = (*pa)[2]; // 将a[2]赋值给i;
2.有了上面的基础,我们就可以对付开头的二只纸老虎了!:) 这个时候你需要复习一下各 种运算符的优先顺序和结合顺序了,顺便找本书看看就够了.
#1:int* (*a[5])(int, char*);
首先看到标识符名a,“[]”优先级大于“*”,a与“[5]”先结合.所以a是一个数组,
这个数组有5个元素,每一个元素都是一个指针,
指针指向“(int, char*)”,对,指向一个函数,函数参数是“int, char*”,返回值是
“int*”.完毕,我们干掉了第一个纸老虎.
即int* (*a[5])(int, char*); 为 函数类型为 int* function(int,char*) 且大小为 5的数组,
#2:void (*b[10]) (void (*)());
b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数, 函数参数是“void (*)()”【注1】,返回值是“void”.完毕!
注1:这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”.
#1:int* (*a[5])(int, char*);
typedef int* (*PF)(int, char*);//PF是一个类型别名【注2】.
PF a[5];//跟int* (*a[5])(int, char*);的效果一样!
注2:很多初学者只知道typedef char* pchar;但是对于typedef的其它用法不太了
解.
#2:void (*b[10])(void (*)());
typedef void (*pfv)();
typedef void (*pf_taking_pfv)(pfv);
pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一样!
typedef使用
Stephen Blaha对typedef用法做过一个总结:“建立一个类型别名的方法很简单,
在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的
开头”.
Typedef声明有助于创建平台无关类型,甚至能隐藏复杂和难以理解的语法.
不管怎样,使用 typedef 能为代码带来意想不到的好处,通过本文你可以学习用typedef避免缺欠,从而使代码更健壮.
typedef声明,简称typedef,为现有类型创建一个新的名字.比如人们常常使用 typedef 来编写更美观和可读的代码.所谓美观,意指typedef 能隐藏笨拙的语法构造以及平台相关的数据类型,从而增强可移植性和以及未来的可维护性.
本文下面将竭尽全力来揭示 typedef 强大功能以及如何避免一些常见的陷阱,如何创建平台无关的数据类型,隐藏笨拙且难以理解的语法.
typedef使用最多的地方是创建易于记忆的类型名,用它来归档程序员的意图.类型出现在所声明的变量名字中,位于typedef关键字右边.
typedef int size;
此声明定义了一个 int 的同义字,名字为 size.注意typedef并不创建新的类型.它仅仅为现有类型添加一个同义字.
你可以在任何需要 int 的上下文中使用 size:
void measure(size * psz);
size array[4];
size len = file.getlength();
typedef 还可以掩饰复合类型,如指针和数组.例如,你不用象下面这样重复定义有81个字符元素的数组:
char line[81]; char text[81];
定义一个typedef,每当要用到相同类型和大小的数组时,可以这样:
typedef char Line[81];
Line text, secondline;
getline(text);
同样,可以象下面这样隐藏指针语法:
typedef char * pstr;
int mystrcmp(pstr, pstr);
用途一 定义一种类型的别名,而不只是简单的宏替换.
可以用作同时声明指针型的多个对象.比如:char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针,
// 和一个字符变量;
以下则可行:
typedef char* PCHAR; // 一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
虽然:
char *pa, *pb;
也可行,但相对来说没有用typedef的形式直观,尤其在需要大量指针的地方,typedef的方式更省事.
用途二 用在旧的C代码中(具体多旧没有查),帮助struct.
以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名 对象名,如:
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;
而在C++中,则可以直接写:结构名 对象名,即:
tagPOINT1 p1;
估计某人觉得经常多写一个struct太麻烦了,于是就发明了:
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
或许,在C++中,typedef的这种用途二不是很大,但是理解了它,对掌握以前的旧代码还是有帮助的,毕竟我们在项目中有可能会遇到较早些年代遗留下来的代码.
用途三 用typedef来定义与平台无关的类型.
比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改.
标准库就广泛使用了这个技巧,比如size_t.
另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健(虽然用宏有时也可以完成以上的用途).
用途四 ---- 为复杂的声明定义一个新的简单的别名.
type (*)(....)函数指针
type (*)[]数组指针
C语言单继承模型
因为使用C语言做为开发语言, 而C语言在类的支持方面几乎为零, 而Javascript语言的Object类型是一个非常明显的类支持对象,所以这里需要提出一个方案对Object类型的继承进行支持.
本章节介绍一个简单的基于C语言单继承结构的实现, 实现非常的简单, 但是体现了Java 的OO思想.
1 ////Object.h
2 #ifndef _Object_
3 #define _Object_
4 /*******************
5 cls:
6 类似于Java的接口, 子类想对某个函数进行重载 ,
7 则只要在具体同名函数的位置替换掉原先的指针 .
8 而子类想访问被覆盖掉的父类同名函数, 则直接访问
9 父类的函数指针.
10 而私有函数, 则都在.c 文件中
11 pb:
12 公共数据结构, 数据结构在.h中
13 通过指针来访问自己的和父对象的公用数据
14 ((struct Pb*)pb[0])->size ; 访问了跟对象的public block的size
15 sb:
16 私有的数据成员, 数据结构在.c中, 非本对象不可操作
17 ((struct Sb*)sb[0])->size
18 FLOOR:
19 该对象处于继承链的第几个位置, Root 为 0
20 注: pb 和 sb的 数据槽 都由new Object的对象来申请空间
21 初始化的方向为先初始化父类
22 析构方向为先析构子类
23 ********************/
24 struct Object;
25 //接口操作
26 typedef void (*PrintFn)(struct Object* self);
27 #define OBJECT_FLOOR 0
28 struct Class{
29 PrintFn p;
30 };
31 //公布的操作
32 struct Pb{
33 int size;
34 };
35 struct Object{
36 struct Class* cls;
37 void** pb;
38 void** sb;
39 };
40 void ObjectPrint(struct Object* self);
41 //申请基本的空间
42 //构建一个基本的空间, floor为子类所在的层, 根为第0层
43 struct Object* AllocObject(int floor);
44 //初始化对象
45 //如果o == NULL, 则Alloc一个空间
46 struct Object* CreateObject(struct Object* o);
47 #endif
通过Cls公布类似java接口操作, 而pb 和 sb 分别为公布数据结构和私有数据结构, 并且私有函数都写在 .c 文件中.
1 //Object.c
2 #include<stdlib.h>
3 #include<stdio.h>
4 #include"Object.h"
5
6 struct Sb{
7 int num;
8 };
9 void ObjectPrint(struct Object* self){
10 struct Pb* p;
11 struct Sb* s;
12 p = (struct Pb*)self->pb[OBJECT_FLOOR];
13 s = (struct Sb*)self->sb[OBJECT_FLOOR];
14 printf("root Object Pb: %d root Object Sb: %d ", p->size,s->num);
15 }
16 //申请基本的空间
17 //构建一个基本的空间, floor为子类所在的层, 根为第0层
18 struct Object* AllocObject(int floor){
19 if(floor <= 0){
20 //error
21 }
22 floor ++;
23 struct Object* o;
24 o = (struct Object*)malloc(sizeof(struct Object));
25 o->cls = (struct Class* )malloc(sizeof(struct Class));
26 //数据槽
27 o->pb = (void**) malloc(sizeof(void*) * floor );
28 o->sb = (void**) malloc(sizeof(void*) * floor );
29 }
30
31 //初始化对象
32 //如果o == NULL, 则Alloc一个空间
33 struct Object* CreateObject(struct Object* o){
34 if(o == NULL)
35 o = AllocObject(OBJECT_FLOOR);
36 //初始化pb数据块
37 o->pb[OBJECT_FLOOR] = (struct Pb*)malloc(sizeof(struct Pb));
38 //初始化sb数据块
39 o->sb[OBJECT_FLOOR] = (struct Sb*)malloc(sizeof(struct Sb));
40 o->cls->p = &ObjectPrint;
41 ((struct Pb*)o->pb[OBJECT_FLOOR])->size = 1;
42 ((struct Sb*)o->sb[OBJECT_FLOOR])->num = 2;
43 return o;
44 }
私有数据结构实现在.c 中, 私有函数也实现在.c 中
而下面就是一个简单的Object2.h 对象
1 ///Object2.h
2 #ifndef _Object2_
3 #define _Object2_
4 #define OBJECT2_FLOOR 1
5 #include"Object.h"
6 //新子类的共享数据
7 struct Pb2{
8 int size;
9 };
10 void ObjectPrint2(struct Object* self);
11 struct Object* CreateObject2(struct Object* o);
12 #endif
Object2.h 实现了自己的数据结构 和公布了一个多态函数
1 ///Object2.c
2 #include"Object.h"
3 #include"Object2.h"
4 #include<stdio.h>
5 #include<stdlib.h>
6 void ObjectPrint2(struct Object* self){
7 struct Pb2* p = (struct Pb2*)self->pb[1];
8 printf("Object2 : %d ",p->size);
9 ObjectPrint(self);
10 }
11 struct Object* CreateObject2(struct Object* o){
12 if(o == NULL)
13 //处于继承链第一个位置
14 o = AllocObject(OBJECT2_FLOOR);
15 //初始化父对象
16 CreateObject(o);
17 //初始化pb数据块
18 o->pb[OBJECT2_FLOOR] = malloc(sizeof(struct Pb2));
19 //初始化sb数据块
20 o->sb[OBJECT2_FLOOR] = NULL;
21 //重载
22 o->cls->p = &ObjectPrint2;
23 ((struct Pb2*)o->pb[OBJECT2_FLOOR])->size =3;
24 return o;
25 }
在第9行 , 调用了父类的该同名函数.
以下是一个基本的测试:
////main.c
#include"Object2.h"#include<stdlib.h>
int main(){
struct Object* o = CreateObject2(NULL);
o->cls->p(o);
}
编译命令:
gcc -c Object.c
gcc -c Object2.c
gcc -c main.c Obejct.o Object2.o
./a.out
测试成功!
通过pb 的连续内存块, 在继承结构中, 对象只能看到它自己和父对象的内存结构, 而不能发现该对象的子对象内存结构, 而父对象则照常使用属于他自己的内存空间, 从而实现了信息隐藏.
并且每个对象的访问数据和操作都是统一的(主要是父对象):
1. 子对象指针和父对象指针都可以通过pb数据块进行操作自己的数据(从共享内存中找到自己的内存地址)
2. 子对象和父对象指针访问API都指向正确的实现(可以访问重载函数)
3. 私有数据都是从sb数据块获取
C语言的继承和多态主要的核心是内存结构和CPU访问数据和函数的方式(参考C++对象继承内存模型), 或者采用动态语言的机制进行查询访问.
模块和隐藏
在C语言中, 使用.h 声明开放给其他模块的资源, 如API 数据结构等, 而在.c 中实现具体的API和该模块隐藏的部分.
如跨平台API中使用句柄来指向一个”锁”, 而句柄是void* 指针, 指向.c文件中具体和平台相关的数据结构。
.h 中一般不包含太多的#include文件, 仅仅包含.c模块和使用该.h 文件都要依赖的文件.
核心思想就是高内聚低耦合.
其他
对于(expr,expr,expr)类型的表达式
如 int a = (1,fun(),3); 则 a = 3 取最后一个表达式的eax数值
void* 和 union
void* 可以指向任何类型的指针类型,也可以被赋值任何指针类型
(void a)这种声明无效!
union 是完成了对同一内存空间不同的读取方法的设置.
static function / var
本地可见标志
inline extern function(){}
类似于宏定义函数
对于{}
1. {} 表示语句块 如
int main (){
{int a = 1; a++ ; a;}
}
2. 做为数组的赋值
int a[] = {1,2,3,4,5,6};
3. 做为结构体的赋值
struct struct a{int a,int b};
struct _str {
int a;
int b;
struct other a;
};
struct _str str_a = {1,2,{1,2}};
C99中对数组和结构体的赋值:
1. 数组 int array[10] = {[1]=10,[9]=10};
2. 结构体 struct {int a, int b,int c} _a = {.a = 1 , .c = 3};
对于volatile var 表示该变量从内存中获取, 不放置在寄存器中, 该特性有利于多线程对数值的修改
Enum 数据类型
在C语言中, enum作为基本数据类型, 声明一个enum类型的方式为
enum typename {one, two,three};
定义一个enum类型变量:
enum typename var;
赋值方式为
var = one;
原理:
在一个scope中定义了一个box类型的enum, 则pencil, pen 分别被认为常数 0,1 .
而具体的enum变量则被认为是int类型的数据, 但是它的赋值方式要使用pencil, pen 这中”常量”来赋值.
Struct 和 union 字节大小
Struct:
1. 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3.收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.
Unicon:
取结构中最大的为结构体大小.
C语言数据类型:
注意其中的指针类型, 指针类型包含了函数指针和数据指针.
所以在结构体中, 可以使用函数指针, 调用它指向的函数了.
其中空类型只能作为函数返回类型声明, 或者作为void* 指针类型,
C语言声明作用域
在代码中, 可以发现无论在哪里进行声明一个结构体,或者enum等声明, 在该文件作用域都为可见,包括#include<other.h>
函数中的字符串:
在函数中有一字符串如:
char* hello(){
char* c = “hello”;
return c;
}
则这个字符串被存储在CONST区, 不能被修改.所以在函数退出后依然可以引用该字符串
-----------------------------------------------------------------
在C语言中, 一个函数名只能对应一个函数定义, 即没有参数重载的功能
int a(int a,int b){
}
int a(int a){
}
会冲突