1 C编译与链接
1.1 词法分析贪心法
010不等于10(前者八进制)
需要注意这种情况,有时候在上下文中为了格式对齐的需要,可能无意中将十进制数写成了八进制数,例如:
Parttab[] =
{
046,
047,
125,
};词法分析中的贪心法:编译器将程序分解成符号的方法是:从左到有一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符床是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。
1.2 命名冲突与static修饰符
1,声明与定义
每个外部变量只能定义一次。
int a;
其出现在函数体外,称为外部对象a的定义。说明a是个外部整型变量,同时为a分配了空间,初值默认为0
同一个外部变量被多次定义(在同一个源文件或多个源文件),出现命名冲突。
2,命名冲突与static修饰符
避免命名冲突: static int a;
a的作用域只限于一个源文件内,对于其他源文件不可见。因此,当多个函数共享一个外部变量,可将这些函数放于同一源文件中,并将用到的外部变量也在同一个源文件中用static声明。
1.3 自定义函数与C函数库重名问题Interpositioning
C语言没有办法,所以要熟知系统函数名,尽量不要冲突。
C++等其他面向对象的语言可以通过命名空间在一定程度上解决这个问题,但命名空间也不能重名(可以嵌套)。
1.4 头文件extern c声明
《程序员的自我修养》P91相关
很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言代码或C++代码包含。比如很常见的,我们的C语言库函数中的string.h中声明了memset这个函数,它的原型如下:
void *memset (void *, int, size_t);C函数头文件string.h可能被其他C/C++程序include,其真正的string.o是由C编译器编译的。其中的memset函数被C编译器符号引用为_memset
1,如果该string.h声明如下:
//C库函数头文件:string.h
void *memset (void *, int, size_t);
include它的C文件正确。
include它的C++文件错误。
原因:
C++文件会将memset的符号修饰成_Z6memsetPvii,这样链接器就无法与C语言库中的_memset符号进行链接。
2,如果该string.h声明如下:
//C库函数头文件:string.h
extern "C" {
void *memset (void *, int, size_t);
}
结果:
include它的C文件错误。
include它的C++文件正确。
原因:
C语言又不支持extern “C”语法,编译错误。
C++语言支持extern,将memset的符号按C方式编译修饰成_memset。链接正确。
3,将该string.h声明如下:
//C库函数头文件:string.h
#ifdef __cplusplus
extern "C" {
#endif
void *memset (void *, int, size_t);
#ifdef __cplusplus
}
#endif
结果:
include它的C文件正确。
include它的C++文件正确。
原因:
使用C++的宏“__cplusplus”,C++编译器会在编译C++的程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码。
如果当前编译单元是C++代码,那么memset会在extern “C”里面被声明,按照C编译器编译为_memset;
如果当前编译单元是C代码,就直接声明。
上面这段代码中的技巧几乎在所有的系统头文件里面都被用到。
1.5 C预处理
预处理的过程主要处理包括以下过程:
将所有的#define删除,并且展开所有的宏定义
处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
删除所有注释 “//”和”/* */”.
添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
保留所有的#pragma编译器指令,因为编译器需要使用它们
通常使用以下命令来进行预处理:
gcc -E hello.c -o hello.i参数-E表示只进行预处理
1.4 头文件的命名冲突
a.c 函数定义
a.h 函数声明
这是一种习惯,在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前面加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H
#define _STDIO_H
......
#endif
2 C声明与定义
2.1 自增运算符
++a可以作为左值(因为返回的是引用,这时a值为7)
//++a
A& operator ++ ()
{
++ A.data;
return A;
}
a++不能作为左值(因为返回的是临时变量6,真正的a值为7)
//a++
A& operator ++ (int)
{
temp = A.data;
++ A.data;
return temp;
}若临时变量
1:是基本类型(int,float,double),不能做左值
2:是你自己定义的类类型,则可以做左值
示例:
#include <stdio.h>
main(){
int a=3,b;
b=(++a)+(++a);
printf("%d\n",b);
}结果是10,可以看做:
b=(++a)+(++a);
即
++a;(最右)
++a;(左边)
b = a + a;但是事实上ANSI C并不定义表达式运算的顺序(先算左边的++a还是右边的)
i++的副作用和顺序点:
http://www.cnblogs.com/smwikipedia/articles/1229984.html
而且也会跟实现有关,所以尽量不要做这种操作。
2.2 运算符求值顺序
C语言中只有四个运算符(&&、||、?:、,)存在规定的求值顺序。
1,运算符&&和运算符||首先对左侧操作数求值,只在需要时才对右侧操作数求值。
2,运算符?:有三个操作数:在a?b:c中,操作数a首先被求值,根据a的值再求操作数b或c的值。
3,逗号运算符,首先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。
注:分割函数参数的逗号并非逗号运算符。例如,x和y在函数f(x,y)中的求值顺序是未定义的,而在函数g((x,y))中却是确定的先x后y的顺序。在后一个例子中,函数g只有一个参数。这个参数的值是这样求得的,先对x求值,然后x的值被“丢弃”,接着求y的值。
C语言中其他所有运算符对其操作数求值的顺序都是未定义的,特别的,赋值运算符并不保证任何求值顺序。
示例:
单目运算符是自右向左结合的:
*p++ = *(p++)
&p->data.x = &(p->data.x)
2.3 整数溢出
两个有符号整数相加会发生溢出。
两个无符号整数相加不会发生溢出。
当两个操作数都是有符号数时,溢出就有可能发生,而且溢出的结果是未定义的。当一个运算的结果发生溢出时,做出任何假设都是不安全的。
假如a和b是两个非负整型变量,我们需要检查a+b是否会溢出,一种想当然的办法是这样:
if( a + b < 0 )
{
complain();
}这并不能正常运行。
当a+b确实发生溢出时,所有关于结果如何的假设都不再可靠。例如,在某些机器上,加法运算将设置一个内部寄存器为四种状态之一:正、负、零、溢出。在这种机器上,c编译器完全有理由这样来实现上面的例子,即a与b相加,然后检查内部寄存器的标志是否为“负”。当加法操作发生溢出时,这个内部寄存器的状态是溢出而不是负,那么if的语句的检查就会失败。
一种正确的方式是将a和b都强制转换为无符号整数:
if( ( unsigned )a + ( unsigned )b > INT_MAX )
{
complain();
}此处的INT_MAX是一个已定义的常量,代表可能的最大整数值。ANSI C标准在<limits.h>中定义了INT_MAX。不需要用到无符号算术运算的另外一种可行办法是:
if( a > INT_MAX - b )
{
complain();
}
2.4 结构体定义
结构体:加小写前缀"tag",之后以大写字母开头。
typedef struct tagPOINT
{
int x;
int y;
} POINT;
结构体可以定义为空:
typedef struct tagPOINT
{
} POINT;
在C中,空结构的大小为0。
在C++中,空结构的大小则为1。
2.5 结构体的声明与extern
struct tagPOINT;
typedef struct tagPOINT POINT;//是一个类型
不用extern,extern用来声明外部的变量(对象),而非声明一个类型(如这里是POINT结构的声明)。
如:
extern int a ;//是一个实例
2.5 结构体赋值
直接用=号,本质是memcpy。
2.6 字符串初始化
char str[] = "abc";//OK
char * str = "abc";//OK
//ERROR
char str[4];
str = "abc";
2.10 更新顺序文件
许多系统中的标准输入/输出库都允许程序打开一个文件,同时进行写入和读出的操作,但是为保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后进行一个输出操作,反之亦然。如果要同时进行输入和输出操作,必须在其中插入fseek函数的调用。
FILE *fp;
struct record rec;
fp = fopen(file,”r+”);
while(fread((char *)&rec,sizeof(rec),1,fp)==1)
{
fseek(fp,-(long)sizeof(rec),1);
fwrite((char *)&rec,sizeof(rec),1,fp);
fseek(fp,0L,1);//少了就错了!
}因为在fwrite执行完之后再执行的就是fread操作,虽然第二个fseek似乎什么都没有做,但是它改变了文件状态,使得文件可以正常进行读取。
2.11 使用errno检测错误
errno = 0;
/*调用库函数*/
if(errno)
/*处理错误*/上面的处理是错误的。
要理解这一点,我们不妨假想一下库函数fopen在调用时可能会发生什么情况,当fopen函数为要求新建一个文件以供程序输出,如果已经存在一个同名文件,fopen函数将先删除它,然后新建一个文件,这样fopen函数可能需要调用其他的库函数来检测同名文件是否已经存在。假设用于检测文件的库函数在文件不存在时,会设置errno,那么,fopen函数每次新建一个事先并不存在的文件时,及时没有任何程序错误发生,errno也仍然可能被设置。因此,在调用库函数是,我们应该先检测作为错误指示的返回值,确定程序执行已经失败,然后再检查errno:
/*调用库函数*/
if(返回的错误值)
/*检查errno*/
2.12 宏定义问题
宏只是对程序的文本起作用,提供了一种对组成程序的字符进行变换的方式,而并不作用域程序中的对象,因此可以使一段看上去完全不合法的代码变成一个有效的程序,也能使一段看上去无害的代码编程一个怪物。
1,宏并不是函数 (自增、自减作为参数需谨慎)
宏定义中每个参数最好用括号括起来,整个表达式也用括号括起来。但即使宏定义中各参数和整个表达式都被括起来,也仍然可能有其他问题存在(有副作用的表达式传入)。
例如:#define max(a,b) ((a)>(b)?(a): (b))
当a大于b时,如果a是一个自增表达式,则a被重复求值,造成最终结果错误。
所以,程序员必须确保max的参数没有副作用!
2,宏并不是语句 (宏中包含C语句需要注意)
assert(x>y); assert为一个宏,当参数为0时报告断言失败的文件名和失败处的行号,当参数不为0时,什么也不做。
例如: #define assert(e) if(!e) assert_error(__FILE__,__LINE__)当如下调用时出错:
if(x>0 && y>0)
assert(x>y);
else
assert(y>x);因为展开后,if else的流程结构出错。
其正确定义如下:
#define assert1(e) ( (void)( (e)||_assert_error(__FILE__,__LINE__) ) )这个定义不是语句,而是类似一个表达式。
3,宏并不是类型定义
宏的一个常见用途是使多个不同变量的类型可以在一个地方说明:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b,c; 之后a,b,c的类型可以一改全改。 但typedef 更通用一些!
当试图声明多个变量时,问题就来了:
#define T1 struct foo *
T1 a,b ;被扩展为 struct foo * a, b; 此时a 为指针,而b为结构体。
而使用下面的定义:
typedef struct foo *T2;
T2 c,d ;
此时c、d都为指向结构体的指针,T2的行为完全与一个新类型的行为相同。
所以定义类型最好用typedef。
4,宏定义示例
#define EP 1e-10
#define SWAP(x, y) \
do{ \
typeof(x) t = (x); \
(x) = (y); \
(y) = t; \
}while(0)
#define MIN(x, y) \
({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
_x < _y ? _x : _y; \
})
#define MAX(x, y) \
({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
_x > _y ? _x : _y; \
})
#define SET_BIT(val, n) (val |= 1<<n)
#define SET_BIT_U64(val, n) (val |= 1ull<<n)
#define CLEAR_BIT(val, n) (val &= ~(1<<n))
#define CLEAR_BIT_U64(val, n) (val &= ~(1ull << n))
#define IS_SET(val, n) (val & (1<<n))
#define IS_SET_U64(val, n) (val & (1ull << n))
#define ARR_LEN(arr) (sizeof(arr)/sizeof(arr[0]))
#define FEQ(a, b) fabs((a) - (b)) < (1e-5)
2.13 不能返回局部数组
1,区别这两个
返回局部数组,出错!
char *func1()
{
char a[]="12345";
return a;//数组不能作为返回值.这里返回的是数组名转换而来的char*指针(指向首元素),于是你这里就是返回局部变量的指针.出错.
}返回变量值,正确!
char func2()
{
char a='a';
return a;//这里返回的a的值.
}
2,如果需要返回数组可以用如下3种方法:
(1)在函数中定义静态数组
#define MAX 1024
char * f(){
static char a[MAX];
/*
*code
*/
return a;
}
(2)函数中定义数组指针,然后对指针进行分配空间,此时的内存空间是在堆区(注意要自行释放)
#define MAX 1024
char * f(){
char * a =(char *)malloc(MAX*sizeof(char));
/*
*code
*/
return a;
}
(3)调用之前先创建数组,将数组的地址也就是指针作为参数传递进函数。
void f(char* inout[])
{
/*
*code
*/
return inout;//反不返回都行,可以直接使用参数
}
2.14,字符串数组初始化
char dest[5] = {'D'};
实际结果 D\0\0\0\0,所以char dest[5]={'\0'}只是凑巧,因为这里只把dest[0]赋值为'\0',后面的默认初始化为'\0'。
char dest[5] = {'D', '\0'};
char dest[5] = {'D', 'D', 'D', 'D', 'D'};
3 C指针
3.1 空指针vs野指针vs悬挂指针
1,空指针
空指针是一个特殊的指针值,也是唯一一个对任何指针类型都合法的指针值。即NULL指针,指向内存空间地址为0的地方,如果实际访问该指针,系统若设置为不可读,那么会报错;如果是可读的,那么会读到无意义的数据。
char * p;
p = NULL;
空指针一般会有if语句进行判断。
2,野指针
“野指针”是未初始化的指针。
指针变量没有被初始化。 任何指针变量在刚被创建的时候不会自动成为NULL指针,它的缺省值是随机的。所以指针变量在创建的时候,要么设置为NULL,要么指向合法的内存。
char *p1; //野指针
static char *p2; //非野指针,因为静态变量初始化为0
3,悬挂指针
1,指针p被free/delete之后,没有置为NULL(最好加一句p = NULL;)。他们只是把指针指向的内存给释放掉,并没有把指针本身干掉。
char *dp = malloc(A_CONST);
free(dp); //dp成为,悬挂指针
dp = NULL; //dp不再是悬挂指针
2,指针操作超越了变量的作用范围。不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
char *dp = NULL;
{
char c;
dp = &c;
}
//c变量出作用域
//此时dp成为悬挂指针
4 C函数
4.1 拷贝字符串
将字符串s和t拼接,一定要申请空间(如malloc)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
//将字符串s和t拼接,并存储
char * s = "hello";
char * t = " world!";
char * r = malloc(strlen(s) + strlen(t) + 1);
if(!r){
exit(1);
}
strcpy(r,s);
strcat(r,t);
//具体的操作
printf("%s\n",r);
//释放
free(r);
}
4.2 swap函数交换两个变量
a = a + b;
b = a - b;
a = a - b;
或
a = a ^ b;
b = a ^ b;
a = a ^ b;
或
a^= b^=a^=b;
但是对于异或和加减法来讲,如果做成函数的话,一定要判断传入的两个数相同:
void swap(int &a, int &b){
if(a==b);
return ;
a^=b^=a^=b;
}
方法2:
或者还可以利用编译器表达式从左到右计算(表达式计算顺序不由C语言规定,而是由编译器决定)a = b + 0 * (b = a);
利用:
tmp = a;
a = b;
b = tmp;做成宏而非函数:
#define SWAP(x, y) \
do{ \
typeof(x) t = (x); \
(x) = (y); \
(y) = t; \
}while(0)
4.3 scanf函数残留换行符
while(scanf("%d",&num)!=1){
printf("input error,plz again!");
}上述代码是错误的,因为scanf()连续地从流中读入字符,并且对和格式说明符相符合的流进行解释(换行符也看做一个字符)。
当你输入"123换行"时,num获得值123,但是换行符依旧会残留在流中。以上代码,一旦用户输入错误一次,这段程序就会进入无限循环。
正确代码:
while(scanf("%d",&num)!=1){
getchar();//处理换行符
printf("input error,plz again!");
}
4.4 缓冲输出与内存分配
printf中如果有\n的话会强制刷新缓存。
4.5 使用fflush(stdin)清空缓冲区错误
C和C++的标准里从来没有定义过 fflush(stdin)。也许有人会说:“可是我用 fflush(stdin) 解决了这个问题,你怎么能说是错的呢?”的确,某些编译器(如VC6)支持用 fflush(stdin) 来清空输入缓冲,但是并非所有编译器都要支持这个功能(gcc3.2不支持),因为标准中根本没有定义 fflush(stdin)。MSDN 文档里也清楚地写着fflush on input stream is an extension to the C standard(fflush 操作输入流是对 C 标准的扩充)。
fflush(stdin) 是不正确的,至少是移植性不好的。
4.6 C函数参数从右向左入栈
http://blog.youkuaiyun.com/liuaigui/article/details/4143561
错误,参数在传递时首先尽可能地存放到寄存器中(追求速度)。——摘自《C专家编程》
4.7 什么是可变参数函数,特点
小星星
4.8 堆分配函数malloc和calloc和realloc
小星星
4.9 返回整数的getchar函数
#include "stdio.h"
void main()
{
char c;
while ( (c = getchar()) != EOF )
{
putchar( c );
}
}上面的例子是错误的,
因为getchar()函数返回的值类型是int,而c是char类型,c无法容下所有的字符,特别是不一定能容下EOF(定义为-1)。最后可能存在三种可能:
1.某些合法的输入字符在被“截断”后使得c的取值与EOF相同;
2.永远取不到EOF,陷入死循环;
3.与编译器有关,有些编译器在while的语句中确实将getchar的返回值截断附给c,但是与EOF的比较却仍然用返回的int值,而不是用c与EOF进行比较。即使可以运行,也是一种巧合和侥幸。
3.12 函数返回char*的解决方案
在C语言中,自动变量在堆栈中分配内存。当包含自动变量的函数或代码块退出时,它们所占用的内存便被回收,它们的内容肯定会被下一个所调用的函数覆盖。这一切取决于堆栈中先前的自动变量位于何处,活动函数声明了什么变量,写入了什么内容等。原先自动变量地址的内容可能被立即覆盖,也可能稍后才被覆盖。
解决方案:
1. 返回一个指向字符串常量的指针。
char* func() {
return "Only works for simple strings";
} 这是最简单的解决方案,但如果你需要计算字符串的内容,它就无能为力了,在本例中就是如此。如果字符串常量存储于只读内存区,但以后需要改写它时,你就会有麻烦了。
2. 使用全局声明的数组。
char* fun() {
...
global_array[i] = ....;
return global_array;
}这适用于自己创建字符串的情况,也很简单易用。它的缺点在于任何人都有可能在任何地方任何时候修改这个全局数组。
3. 静态数组。
char* func() {
static char buffer[25];
...
return buffer;
}这就可以防止任何人任何时候修改这个数组。只有拥有指向该数组的指针的函数(通过参数传递给它)才能修改这个静态数组。但是,该函数的下一次调用将覆盖这个数组的内容,所以调用者必须在此之前使用或者备份数组的内容。和全局数据一样,大型缓冲区如果闲置不用是非常浪费内存空间的。
4. 显式分配一些内存,保存返回的值。
char* func() {
char *s = malloc(120);
...
return s;
}
这个方法具有静态数组的优点,而且在每次调用时都创建一个新的缓冲区,所以该函数以后的调用不会覆盖以前的返回值。它适用于多线程的代码。它的缺点在于程序猿必须承担内存管理的责任。根据程序的复杂程度,这项任务可能很容易,也可能很复杂。如果内存尚在使用就释放或者出现“内存泄露”(不再使用的内存未回收),就会产生令人难以置信的Bug。人们非常容易释放已分配的内存。
5. const char* 返回
//错误
char* Func(void)
{
char str[] = "hello world";
return str;
}
//正确
const char* Func(void)
{
const char* p = "hello world";//字符串常量存放在程序的静态数据区
return p;
}
</pre><pre>
void func(char *result, int size)
{
...
strncpy(result, "That' d be in the data segment, Bob", size);
}
buffer = malloc (size);
func(buffer);
...
free(buffer);
如果程序员在同一块代码中同时进行“malloc” 和“free”操作,内存管理最为轻松。
7,数组位于结构体内部
/* 数组位于结构内部 */
struct s_tag { int a[100]; };
struct s_tag orange, lime, lemon;
struct s_tag twofold(struct s_tag s) {
int j;
for (j=0; j<100; j++)
s.a[j]*=2;
return s;
}
main(){
int i;
for(i=0, i<100; i++)
lime.a[i]=1;
lemon=twofold(lime);
orange=lemon; /*给结构体赋值*/
}
4.10 使一段代码第一次执行时的行为与以后执行不同
使一段代码第一次执行时的行为与以后执行不同。这种方法能是分支和条件检测减少到最小程度。
generate_initializer(char * string)
{
static char separator = ' ';
printf( "%c %s \n", separator, string);
separator = ',';
}
在第一次执行时,函数首先打印一个空格,然后打印一个初始化字符串。所有初始化字符串(如果有的话)的前面将加上一个逗号。
“第一次执行的前面加个空格”相比“最后一次执行,省略逗号后缀”对程序而言更简单了。
4.11 注意系统调用的打断时机
对于read/write函数,读写长度有可能被系统调用打断,这里最好做一个while语句:
while((got = write(fd, buf, need)) > 0 &&
(need --= got) > 0)
buf +=got;
4.12 从文件中随机提取一个字符串
题目是这样的:
一个文件中按行存放若干字符串,要求只能按顺序遍历文件一次,不能用表格存储字符串偏移,随机返回一个字符串。
解法:
基本的技巧是在幸存的字符串中挑选,并在过程中不断更新。
打开文件并保存第一个字符串,些时就有了一个备选字符串,并有100%的可能性选中它。保存这个字符串,继续读入下一个字符串,这样就有了两个字符串,选中每个的可能性都是50%。选中其一并保存,然后丢弃另一个。再读入下一个字符串,按照新字符串33%原先幸存的字符串67%的概率(它代表前两个字符串的幸存者),在两者之间选择一个,然后保存新选中的字符串。
根据这个方法,依次对整个文件进行处理。在其中每一步,读入字符串N,在它(按照1/N的概率)和前一个幸存的字符串(按照N-1/N的概率)之间进行选择。当达到文件末尾的时候,最后一个幸存的字符串就是从整个文件中随机提取的那个字符串!
请参阅《C专家编程》281页 附录A.6。
3.16 int * p = NULL
int *p = NULL;其实变量p分配空间了。
这时候我们可以通过编译器查看p 的值为0×00000000。这句代码的意思是:定义一个指针
变量p,其指向的内存里面保存的是int 类型的数据;在定义变量p 的同时把p 的值设置为
0×00000000,而不是把*p 的值设置为0×00000000。这个过程叫做初始化,是在编译的时候
进行的。
3.17 C函数没有重载
句号。
3.18 引用变量的实现
C语言没有引用啊,C++才有. 引用能起到指针的部分作用,但是比指针安全. 一个引用可以看作是某个变量的一个"别名"。对引用进行操作就像对原变量进行操作一样。
引用变量在内部实现其实就是一个常量指针变量;
《C++语言中引用机制的实现分析》
1 案例代码(VS2010C++环境下调试)
#include"stdafx.h"
int& __stdcall RefFun( int& n )//通过引用传递参数
{
n++;
return n;
}
int __stdcall ValueFun( int n )//通过变量值传递参数
{
n++;
return n;
}
int main( int argc, char* argv[] )
{
int a=10;
int& x=a;//引用变量初始化
int* pInt=&a;//取普通变量的地址
int* pref=&x;//取引用变量的地址(其实获取的并不是引用变量的地址,而是被引用变量a的地址)
int& b=RefFun(a);//函数返回引用,并将引用的别名赋值给引用变量
int c=RefFun(a);//函数返回引用,并将引用变量的值赋值给整数变量
int d=ValueFun(a);//函数返回变量值,
printf("a=%d,b=%d,c=%d\n",a,b,c);
return0;
}
2 对上面的Main函数反汇编代码分析
注:1)以下是在VS2010下,使用C++工程进行调试(切换到反汇编模式)显示的代码;
2)每行源代码下面为其对应的汇编代码;
3)读者也可以在自己的VS2010开发平台下进行反汇编分析!
反汇编分析代码如下:
int main( int argc, char* argv[] )
{
004295E0 push ebp
004295E1 mov ebp, esp
004295E3 sub esp, 0F0h
004295E9 push ebx
004295EA push esi
004295EB push edi
004295EC lea edi, [ebp-0F0h]
004295F2 mov ecx, 3Ch
004295F7 mov eax, 0CCCCCCCCh
004295FC rep stos dword ptr es:[edi]
int a=10;
//普通变量的初始化:解释为取10的内容
004295FE mov dword ptr[a], 0Ah //立即数寻址,给变量a赋值10
int& x=a;
//引用变量的初始化:解释为取a的地址
004272B5 lea eax, [a] //[a]表示变量a的地址;
004272B8 mov dword ptr[x], eax//其实质就是保存变量a的地址值到引用变量的内存单元!
int* pInt=&a;
//取普通变量的地址:解释为取a的地址
004272BB lea eax, [a]//[a]表示变量a的地址;
004272BE mov dword ptr[pInt], eax//其实质就是保存变量a的地址到指针变量pInt中
int* pref=&x;
//取引用变量的地址:解释为取x的内容
004272C1 mov eax, dword ptr[x]//取引用变量内存单元保存的值(不是引用变量地址)
004272C4 mov dword ptr[pref], eax//引用变量内存单元值保存到指针变量;
//对上面两行源代码的汇编分析补充:
//1)[x]表示引用变量x内存单元地址,
//2) dword ptr[x]:表示内存单元X保存的值(实际是一个地址值,实际指向被引用的变量a的值);
//3) 引用变量的实现秘密:引用变量在内部实现其实就是一个常量指针变量;
int& b=RefFun(a);//函数返回引用,并将引用的别名赋值给引用变量
00429605 lea eax, [a]//取变量a地址到EAX
00429608 push eax//变量a的地址为:0x4260FEh
00429609 call RefFun(4260FEh)//引用传递变量地址(指针)--引用实现的内部秘密!
0042960E mov dword ptr[b], eax//EAX返回的为变量a的地址:实际为变量a的地址(初始化引用变量b)
int c=RefFun(a);//函数返回引用,并将引用变量的值赋值给整数变量
00429611 lea eax, [a]
00429614 push eax//变量a的地址入栈(传递给函数的引用参数实际是变量的地址)
00429615 call RefFun(4260FEh)
0042961A mov ecx, dword ptr[eax]
0042961C mov dword ptr[c], ecx
int d=ValueFun(a);
0042961F mov eax, dword ptr[a]//取变量a的值送到EAX寄存器
00429622 push eax//变量a的值入栈(传递给函数:ValueFun);
00429623 call ValueFun(426103h)
00429628 mov dword ptr[d], eax
printf("a=%d,b=%d,c=%d\n",a,b,c);
0042962B mov eax, dword ptr[c]//取c的内容
0042962E push eax
0042962F mov ecx, dword ptr[b]//b为引用,取b所实际指向指针的内容
00429632 mov edx, dword ptr[ecx]
00429634 push edx
00429635 mov eax, dword ptr[a]//取a的内容
00429638 push eax
00429639 push offset string"a=%d,b=%d,c=%d\n"(471C6Ch)
0042963E call @ILT+3875(_printf)(425F28h)
00429643 add esp, 10h
return 0;
00429646 xor eax, eax
}对上面案例代码引用传递参数的RefFun分析--反汇编分析
int& __stdcall RefFun( int& n )//引用传递参数
{
00429560 push ebp
00429561 mov ebp, esp
00429563 sub esp, 0C0h
00429569 push ebx
0042956A push esi
0042956B push edi
0042956C lea edi, [ebp-0C0h]
00429572 mov ecx, 30h
00429577 mov eax, 0CCCCCCCCh
0042957C rep stos dword ptr es:[edi]
n++;//简单的一个加1对应5行汇编代码(下面的传值函数,只有3行,这是区别所在)
0042957E mov eax, dword ptr[n]//eax保存传递过来引用变量的地址值
00429581 mov ecx, dword ptr[eax]//取到传递过来的变量值
00429583 add ecx, 1
00429586 mov edx, dword ptr[n]//引用变量的地址保存到edx
00429589 mov dword ptr[edx],ecx//保存加1后的结果;
return n;//由于变量n保存的为变量的地址,因此这里返回的是传递进来的地址值(区别于传值)
0042958B mov eax, dword ptr[n]
}2对上面案例代码引用传递参数的ValueFun分析--反汇编分析
int __stdcall ValueFun( int n )//变量值传递参数
{
004295A0 push ebp
004295A1 mov ebp, esp
004295A3 sub esp, 0C0h
004295A9 push ebx
004295AA push esi
004295AB push edi
004295AC lea edi, [ebp-0C0h]
004295B2 mov ecx, 30h
004295B7 mov eax, 0CCCCCCCCh
004295BC rep stos dword ptr es:[edi]
n++;//对应的汇编代码(只有3行汇编代码,比上面的引用传递参数,相对简单):
004295BE mov eax, dword ptr[n]//dwordptr[n]为栈上的临时变量n的地址;
004295C1 add eax, 1
004295C4 mov dword ptr[n], eax
return n;
//对应的汇编代码:返回的是栈上变量的值(这一点区别于引用,引用返回地址)
004295C7 mov eax, dword ptr[n]
}
本文详细介绍了C语言的编译与链接过程,包括词法分析、命名冲突解决、函数库重名问题以及头文件的extern c声明。讨论了C声明与定义中的自增运算符、运算符求值顺序、整数溢出等问题。还深入探讨了指针、函数的使用,如空指针、野指针、悬挂指针的区别,以及字符串拷贝、错误处理等实用技巧。
3719

被折叠的 条评论
为什么被折叠?



