《C语言进阶》学习笔记

1.1 堆栈

堆和栈在使用时“生长”方向相反, 栈向低地址方向“生长”,而堆向高地址方向“生长”。

在print()函数中打印出main()函数中arr数组中的各个元素。

#include <stdio.h>
void print()
{
//填充代码
}
int main()
{
int a=1;
int b=2;
char c='c';
int arr[]={11,12,13,14,15,16,17};
print();
return 0;
}
注意 如无特殊说明,本书代码均通过VC++6.0来编译运行。

看看上面的代码和相关要求,可能会让很多读者束手无策,如果能联系前面的知识点,就应该想到用栈。那么我们该如何来解决问题呢?先别急,在讲解之前,我们先来回顾几个知识点。

1)push操作先移动栈顶指针,之后将信息入栈。

2)esp为堆栈指针,栈顶由esp寄存器来定位。 压栈的操作使栈顶的地址减小,弹出的操作使栈顶的地址增大。

3)ebp是32位的bp,是基址指针。bp为基指针寄存器,用它可直接存取堆栈中的数据, 它在调用函数时保存esp,以便函数结束时可以正确返回。

4)默认的函数内部变量的压栈操作为:从上到下、从左向右,采用4字节对齐。 数组压栈方法略有不同,即从最后一个元素开始,直到起始元素为止,即采用从右向左的方法压栈。

现在看一下以上代码的汇编代码,在main()函数的return语句处按F9键设置一个断点,然后按F5键运行代码,代码运行到断点时把光标移动到断点处,右击选择Go to Disassembly,就可以看到上面那段代码的汇编代码了。我们发现,在main()函数和print()函数的开头都有如下两句汇编指令:

push ebp
mov ebp,esp
为了使读者易于理解,在此通过图1-1来分析说明。根据图上的标注,函数开头部分的第一个push指令的操作步骤是,首先移动栈顶指针esp,然后将ebp内容压栈,注意此时压栈的ebp的值为上一个函数的esp的值, 而esp恰好就是上一个函数的栈底, 所以每个函数一开始的push指令就是保存上一个函数的栈底。那么接下来的mov指令有什么作用呢?由于esp是当前的栈顶指针, 所以该指令的作用就是保存当前栈顶指针的值。由此就可以分析出,ebp存放的是此刻栈顶的地址,就是说,ebp是一个指针,指向栈顶,而栈顶存放的数据其实是 上一个函数的ebp的值,即上一个函数的栈底。

(点击查看大图)图1-1 函数调用过程中的压栈流程

通过上面的分析可知,ebp压栈后,接着就是函数中临时变量的压栈操作,由此可知,我们只需要在print()函数中得到main()函数的栈底,就可以取出数组中的每个元素了,看看下面的实现方法。

#include <stdio.h>
void print()
{
unsigned int _ebp;
__asm{
mov _ebp,ebp
}
int *p=(int *)(*(int *)_ebp-4-4-4-7*4);
for(int i=0;i<7;i++)
printf("%d\t",p[i]);
}
int main()
{
int a=1;
int b=2;
char c='a';
int arr[]={11,12,13,14,15,16,17};
print();
return 0;
}
运行结果为:

11 12 13 14 15 16 17

在没有传递任何参数的情况下,成功地在print()函数中打印出了main()函数中arr数组内的每个元素。现在来看看上面代码的实现方法,在print()函数中定义了一个_ebp无符号整型变量,通过VC++ 6.0内嵌汇编把ebp的值保持到_ebp中,按照上面的分析,可以将在函数print()中通过这条内嵌汇编语句得到的ebp看成一个指针,指针所指向的单元存放的就是print()函数的上一个函数的栈底,在此是main()函数的栈底。知道了_ebp的作用后,我们来分析下代码,通过 (int*)_ebp将_ebp转换为一个整型指针,然后通过 *(int*)_ebp即可得到main()函数的栈底地址由于栈的压栈操作是从上到下(向低生长)、从右到左的,所以main()函数中的变量a先压栈,然后是b、c,最后是arr数组,数组的压栈顺序是从右到左。通过“int *p=(int *)(*(int *)_ebp-4-4-4-7*4);”即可得到数组元素的首地址。接下来,根据首地址就可以取出数组中的每个元素了。有的读者可能会有一个疑惑,main()函数中有一个字符型变量,是不是在求数组元素的首地址时应该把其中的减4改为减1呢?因为它只占用了一个字节!即将“int *p=(int *)(*(int *)_ebp-4-4-4-7*4);”修改为“int *p=(int *)(*(int *)_ebp-4-4-1-7*4)”。我们暂且不说其对与错,先来看看修改后的运行结果:

3072 3328 3584 3840 4096 4352 -859021056

1.5 指针变量

#include <stdio.h>
void main(int argc,char *argv[])
{
int a[10];
printf("a的值为:\t%d\n",a);
printf("&a的值为:\t%d\n\n",&a);
printf("a+1的值为:\t%d\n",a+1);
printf("&a+1的值为:\t%d\n",&a+1);
return ;
}
运行结果:
a的值为: 1245020
&a的值为: 1245020
a+1的值为: 1245024
&a+1的值为: 1245060

很多读者看了上面的运行结果会觉得不可思议,a和&a都表示数组a的起始地址,打印出来的结果相同是显而易见的,为什么a+1和& a+1打印出来的结果却相差如此之大呢?回想前面讲述的内容,出现这种情况的原因是它们是不同类型的指针变量。代码中的a其实相当于一个整型指针变量,所以它加1的结果就和之前的分析一样,那么&a又意味着什么呢?别急,我们先 把“int a[10];”变形为“int *(&a)[10];”,这样就可以很直观地看出来, &a就相当于指向一个int [10]类型的指针变量,于是上面的运行结果就很容易理解了,a到a+1的变化就是它指向的变量所占用的内存单元的大小4字节,而&a到&a+1的变化就是它指向的变量所占用的内存单元的大小4×10字节=40字节。

2.2.2 带参数的宏替换

#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })
#define max(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x > _y ? _x : _y; })
在上面的两个宏中都有代码“(void) (&_x == &_y);”,可能不少读者对其并不理解,下面进行仔细分析。首先分析“==”,这是一个逻辑表达式的运算符,它要求两边的比较类型必须一致。如果&x和&y的类型不一致,一个为char*,另一个为int*,那么使用gcc编译就会出现警告信息,用VC++ 6.0编译时则会报错“error C2446: '==' : no conversion from 'char *' to 'int *'”。代码“(void) (&_x == &_y); ”的功能就 相当于执行一个简单的判断操作,判断x和y的类型是否一致。别小看了这句代码,学会使用它会为编码带来不少便捷。
看了上面给出的宏,细心的读者会有另外一个疑惑:在“#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })”中,为什么要使用“ typeof(y) _y = (y)”这样的替换,而不直接使用“typeof(x)==typeof(y)”或者“x < y ? x : y;”呢?因为使用“typeof(x)==typeof(y)”就像使用“char==int”一样,这是不允许的。如果在宏中没有使用“(void) (&_x == &_y);”这样的语句,那么编译时就相当于失去了类型检测功能。在上面的宏中使用“ typeof(y) _y = (y)”这样的转换是为了防止x和y为一个表达式的情况,如x=i++,如果不转换,那么i++就会多执行几次操作,得到的就不是想要的结果。如果使用了“typeof(y) _y = (y)”这样的转换,就不会出现这样的问题了。我们可以通过下面一段代码来看看它们之间的区别。

#include <stdio.h>
#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })
#define min_replace(x,y) ({ x < y ? x : y; })
void main()
{
int x=1;
int y=2;
int result = min(x++,y);
printf("没有替换时的运行结果为:%d\n",result);
int x1=1;
int y1=2;
int result1 = min_replace(x1++,y1);
printf("替换之后的运行结果为:%d\n",result1);
return ;
}
在Linux环境下使用gcc编译的运行结果:
没有替换时的运行结果为:1
替换之后的运行结果为:2

分析上面的运行结果可以发现,使用相同输入的两种宏得到的最终结果并不一样.

下面来看如何使用宏定义实现变参,先看看实现方法。

#define    print(...)   printf(__VA_ARGS__)

在这个宏中, “...”指可变参数。可变参数的实现方式就是使用“...”所代表的内容替代__VA_ARGS__,看看下面的代码。
#include<stdio.h>
#define print(...) printf(__VA_ARGS__)
int main(int argc,char*argv)
{
print("hello world----%d\n",1111);
return 0;
}
在Linux环境下采用gcc进行编译的运行结果:
hello world----1111

再看代码:

#define printf (tem, ...) fprintf (stdout, tem, ## __VA_ARGS__)

可能有些读者对fprintf()函数感觉有些陌生,在此对fprintf()函数进行简单的讲解,其函数原型为:

int printf(FILE *stream,char *format [,argument])

这个函数的功能为根据指定的format格式发送消息到stream(流)指定的文件中,在前面的宏中使用stdout表示标准输出,fprintf()的返回值是输出的字符数,发生错误时返回一个负值。

#include<stdio.h>
#define print(temp, ...) fprintf(stdout, temp, ##__VA_ARGS__)
int main(int argc,char*argv)
{
print("hello world----%d\n",1111);
return 0;
}
在Linux环境下采用gcc进行编译的运行结果:
hello world----1111

temp在此处的作用为设定输出字符串的格式,后面的“...”为可变参数。现在问题来了,在宏定义中为什么要使用“##”呢?如果没有使用##,会怎么样呢?看看下面的代码:
#include<stdio.h>
#define print(temp, ...) fprintf(stdout, temp, __VA_ARGS__)
int main(int argc,char*argv)
{
print("hello world\n");
return 0;
}

在Linux环境下采用gcc进行编译时发生了如下错误:

arg.c: In function 'main':
arg.c:7:2: error: expected expression before ')' token

为什么会出现上述错误呢?现在我们来分析一下。进行宏替换,“print("hello world\n")”变为“fprintf(stdout, "hello world\n",)”后,会发现后面出现了一个逗号导致发生错误。如果有“##”,就不会出现这样的错误,这是因为可变参数被忽略或为空, “##”操作将使预处理器去除它前面的那个逗号。如果存在可变参数,“##”也能正常工作。

介绍了“##”,再来介绍一下“#”。先来看看下面一段代码。

#include<stdio.h>
#define return_exam(p) if(!(p)) \
{printf("error: "#p" file_name:%s\tfunction_name:%s\tline:%d .\n",\
__FILE__, __func__, __LINE__); return 0;}
int print()
{
return_exam(0);
}
int main(int argc,char*argv)
{
print();
printf("hello world!!!\n");
return 0;
}

在Linux环境下采用gcc进行编译的运行结果:

error: 0 file_name:arg.c function_name:print line:9 .
hello world!!!

因为这里只是为了体现要讲解的宏,所以对代码做了最大的简化,后续章节还将深入讲解如何使用宏来调试代码。 “#”的作用就是对其后面的宏参数进行字符串化操作,即在对宏变量进行替换之后在其左右各加上一个双引号,这就使得“"#p"”变为了“""p""”,我们发现这样两边的“""”就消失了。

2.4 条件编译指令的使用

预处理程序提供了条件编译的功能,用户可以选择性地编译程序,进而产生不同的目标代码文件,这对程序的移植和调试来说是非常有用的。下面先来看看条件编译命令的几种使用方式。

第一种方式:
#ifdef 标识符
程序段1;
[#else
程序段2;]
#endif

功能:当常量表达式为非0(“逻辑真”)时,编译程序段1,否则编译程序段2。
常量表达式不能是变量和含有sizeof等在编译时求值的操作符,在使用条件编译命令时尤其要注意。

第二种方式:
#ifndef 标识符
程序段1;
[#else
程序段2;]
#endif

功能:如果标识符已经被#define命令定义过,则编译程序段1,否则编译程序段2。

第三种方式:
#ifndef 标识符
程序段1;
[#else
程序段2;]
#endif

功能:如果标识符未被#define命令定义过,则编译程序段1,否则编译程序段2。

2.5 #pragma指令的使用

如果读者认真阅读了本书前面的代码,那么应该对#pragma指令有印象,在之前的代码中曾使用过#pragma指令来设置编译器的字节对齐方式。接下来看看预处理中的#pragma 指令,其作用是设置编译器的状态或指示编译器完成一些特定的动作。使用#pragma指令的一般形式为:

#pragma para

其中,para为参数。下面对一些常见的参数进行讲解。

(1)#pragma  message("消息")

至于“#pragma message("消息")”究竟有什么作用,可以通过下面的一段代码来了解其具体的使用方式。
#include<stdio.h>
#define STR
void main(int argc,char*argv)
{
printf("学习#pragma命令中message参数的使用!\n");
#ifdef STR
#pragma message("STR 已经定义过了")
#endif
return ;
}

在Linux环境下使用gcc编译运行的结果:
root@ubuntu:/home# gcc message.c -o msg
message.c: In function 'main':
message.c:10:11: note: #pragma message: STR 已经定义过了
root@ubuntu:/home# ./msg

学习#pragma命令中message参数的使用!

我们发现,在编译的时候会打印出message参数中的信息。通过这种方式,可以在代码中输出想要的信息,也可以看某个宏是否已经被定义过。与之前使用printf()函数实现打印的不同之处在于:message打印消息出现在编译的时候,不会出现在程序最终的运行结果中;而printf()函数的打印消息却会出现在最终的运行结果中。有时候,我们并不希望运行结果中出现与结果无关的信息,这时可以使用#pragma命令,选择message参数来实现信息的打印输出。

(2)#pragma once

如果在头文件的开头部分加入这条指令,那么就能保证头文件只被编译一次。

(3)#pragma hdrstop

该指令表示编译头文件到此为止,后面的无需再编译了。

(4)#pragma  pack()

设置编译器的字节对齐方式

(5)#pragma  warning()

“#pragma  warning(disable:M  N;once:H;error:K)”表示不显示M号和N号的警告信息,H号警告信息只报告一次,把K号警告信息作为一个错误来处理。也可以将其分开来实现,代码如下。
#pragma warning(disable:M N)
#pragma warning(once:H)
#pragma warning(error:K)

这样的实现方式与前面的“#pragma warning(disable:M N;once:H;error:K)”是等价的。也可以使用#pragma warning(enable:N)启用N号警告信息。

生存期:变量占用内存或者寄存器的时长。
静态存储区:存放全局变量和静态变量
动态存储区:存放函数里的局部变量、函数返回值、形参。
自动auto:非静态标量即为自动变量,类型说明符为auto,可省略。
寄存器register:只有局部变量才可以定义为寄存器变量。
静态static:静态存储区,生存期从程序开始到程序运行结束。
外部extern:全局变量前加上关键字extern,全局变量默认为extern。

多维动态数组
一维动态数组
类型说明符 * 数组名= ( 类型说明符 * ) malloc ( 数组长度 * sizeof ( 类型说明符 ) );

二维动态数组
类型说明符 ** 数组名= ( 类型说明符 ** ) malloc ( 数组长度 * sizeof ( 类型说明符 * ) );

三维动态数组
类型说明符 *** 数组名= ( 类型说明符 *** ) malloc ( 数组长度 * sizeof ( 类型说明符 ** ) );

offsetof宏——Linux内核链表里的一个宏
#define offsetof(TYPE, MEMBER) ( ( size_t ) & ( (TYPE *)0 )->MEMBER)
理解:获取TYPE结构体重 MEMBER成员的偏移地址
通过 (TYPE *)0 将)地址强制转换为TYPE类型的指针;
通过 ( (TYPE *)0 )->MEMBER 访问TYPE结构中的MEMBER数据成员
通过 & ( (TYPE *)0 )->MEMBER 取出TYPE结构中数据成员MEMBER的地址
通过  ( size_t ) (& ( (TYPE *)0 )->MEMBER) 将结果转换为size_t类型

若没有指定字节数对其,编译器将按照默认方式处理,即选择数据成员中占用内存最大的类型字节数,为默认字节数对其。嵌套结构体中,逐层进行分解,在根据分解出来的“原子类型”分析结构体的字节对齐方式。
#pragma  pack(N)  //指定N字节对齐
#pragma  pack()     //取消指定对齐
编译器进行字节对齐数选取时,选择指定字节对齐数和默认字节对齐数两者中最小值。

Linux内核双向循环链表
传统的双向循环链表,需要为不同的数据结构维护各自的链表。Linux内核中将结构体中的前向指针prev和后向指针next从具体的数据结构中提取出来不,构成一个通用的双向循环链表数据结构list_head。如果需要构造某类对象的特定链表,那么只要在其结构体中定义一个类型为list_head类型的成员,通过这个list_head类型的成员,将类对象联系起来(list_entry宏定义实现),形成所需的双向循环链表。
如何取出宿主结构的指针?
#define list_entry(ptr, type, member) \
( ( type * ) ( ( char *) ( ptr ) - ( unsigned long ) ( &( ( type * ) 0 )->member ) ) )

其中:
 ( unsigned long ) ( &( ( type * ) 0 )->member ) 为取出type类型中member成员的偏移量,
ptr为指向member的指针,因为指针类型不同,所以要先进行(char *)转换在进行计算。
用ptr减去member的偏移量就得到了宿主结构体的指针。这是最精华的地方。

变参数函数
typedef char *va_list;
#define _INTSIZEOF(n)          ( ( sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )     //实现内存中的字节对齐
#define va_start( ap, v )          ( ap = (va_list)&v + _INTSIZEOF(v)  )                    //初始化va_list。通过v实现对可变参数初始位置的定位,并为va_list分配内存,将可变参数复制该内存块中,使va_list指向该内存块的初始位置;
#define va_arg( ap, t )          ( *(t *) ( ap +=  _INTSIZEOF(t) -  _INTSIZEOF(t) ) )
#define va_end(ap)               ( ap = (va_list) 0 )

关闭断言assert时,#define NODEBUG 必须卸载 #include<assert.h>的前面

const int *p;     //const int 类型指针,即指针指向常量
int const *q;     //const int 类型指针,即指针指向常量
int * const r;     // int 类型const指针,即指针本身是常量

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值