您的位置:首页 > 编程语言 > C语言/C++

C语言相关知识集锦

2009-12-18 17:06 225 查看
在论坛常看到有人问关于指针的问题,已经运行时候出错,或者程序崩溃,或者打印数据不对;或者段错误;
所以写篇文章希望对大家有用;
1;首先谈谈段;在intel处理器中逻辑地址;线性地址;物理地址;逻辑地址就是段地址+段内偏移量;在早期的8086中;逻辑地址是16位段地址×16+段内偏移量;到了后来;intel引入了所谓的实模式和保护模式;所谓实模式也是就兼容早期的8086;段地址依然是段寄存器内容;逻辑地址依然是段地址×16+段内偏移量;所谓保护模式是对于逻辑地址另外一种算法;
线性地址就是我们用段寄存器内容×16+段内偏移量算出的地址;很显然早期的地址只有20位;一种到引入MMU;就有了物理地址的和线性地址的区别;如果没有硬件MMU;那么线性地址就是时间的物理内存的地址;但是引入了MMU;这个线性地址将被MMU控制器重新解析;然后解析到不同的物理地址;这个就是所谓的分页技术;
2;谈到寄存器,我们不妨列出所有的寄存器EAX;EBX;ECX;EDX;ESP;EBP;EDI;ESI;这个8个叫通用寄存器;EIP;EFLAGS;是特殊寄存器;还有CS;DS;ES;SS;FS;GS这些段寄存器;这个里面的内容将作为段的首地址;8086的寄存器没有E;也就是说是AX;BX等;
3;指针;我们知道指针表示地址;我们也常说32bit的系统;指针长度是4字节的;那么这个4字节的内容最后到达写到哪里去呢?毫无疑问;这个指针的内容会被分为段地址和段内偏移量2个部分;段地址部分被存放到段寄存器中;(当然了保护模式下,寄存器的解析是不同于是模式的)在进一步解释指针之前;以及野指针问题;我们来看看保护模式;
4;保护模式;在现在的操作系统中都是开始用实模式;后来用保护模式;正是由于保护模式的存在;才让编程变得容易;
实模式有个缺点就是只能寻1M空间;也就是20位;对于保护模式;有一个8字节的段描述符;这个描述符描述你一个段的基本信息;包括段的起始地址;段的大小;段的颗粒大小;这里有必要说说段的大小和颗粒大小的问题;其实段的大小只是说段从起始地址开始;有多少个颗粒大小;颗粒可以是1byte;也可以是4kbyte;段就是只得颗粒的数量;段大小是20位的;如果颗粒是1byte那么段的最大就是1M;如果颗粒是4k;那么最大就是4G;
好了;那么我们一共有多少个这样的段描述符呢;换句话说我们有多少个段可以使用呢?这个时候16bit的段寄存器不再像实模式那样解释了;16bit的前13bit作为段选择符;后面3bit作为这个段的描述;这里我们不得不说GDT;LDT;GDT;和LDT;是2张表;这个2张表放着段描述符的地址;这个表有13^2个表项;好了现在我们知道所有的寄存器都有13bit的选择子;指示了选择GDT或者LDT表中段描述符的序号;比如说CS=0008H;那么我们知道;前13bit值是1;后面3bit是000;那么他将选择第2个标识符;那么GDT;LDT是什么呢?他们叫全局描述符和局部描述符;一个CPU只有1个GDT;而每个应用程序都可以有一个LDT;GDT;LTD的起始地址存放在寄存器GDTR;LDTD中;
5;说完了intel的段;我们再来看看分页机制;在linux中;并没有用到13^2个段;4个主要的段是内核的代码段;内核的数据段;应用程序代码段;应用程序数据段;他们都设置了段描述符使得其实地址是00000000H;结束地址是FFFFFFFFH;开始我们说段寄存器最后3位用作其他作用;其中一个就是设置CPU访问权限;2bit一个用4中权限;linux只用了0和3;也就是我们平时说的内核态和用户态;对于intel处理器来说如果CPU的寄存器cr0到cr4;里面有说明CPU访问权限;(我没有具体研究过)当CPU访问权限小于段的访问权限那么段是无法被访问的;也就是说CRX中如果说明CPU权限是3;而段权限是0;那么CPU是无法访问这个段的;(未完!下面继续)

6;对于线性地址;处理器通过分页机制来转换到实际的物理地址;一个线性地址;被分成页目录;页表项和偏移量;所以一个页未必存放在内存中;有寄存器标志标识;所以以前有人说一个文件很大是否要申请很大的内存来存放;实际是没有必要的;因为即使你申请了大的内存;操作系统也只有在需要的时候才会给你实际的页;一个页通常是4kb的;这个时候才是真正的分配了物理内存;所以有文章说;如果你申请的数据;或者全局变量很大;可能导致程序过慢;因为你的数据分别放在2个页上;而当系统负载很高;就会导致页面不断和磁盘交互;导致程序运行过慢;这里也说明了一个野指针为什么能够通过编译;但是运行会出错;因为你的指针指示到了本来不属于这个进程的页面;而操作系统这个时候就认为你进行了非法操作;
7;linux典型的程序图;
其实这个图有很多人写过了;但是我还是想copy下;
命令行参数和环境变量


未初始化的数据
初始化的数据
正文
保留区
这里我们可以看出;所谓的正文区就是我们说的代码区;也就是指令;初始化的数据;包括初始化的全局变量;static 修饰的局部变量;未初始化区;也就是未初始化的全局变量;也就是我们所说的BSS区;堆也就是用malloc分配的;所谓的动态内存;所以这个是要释放的;栈也就是我们的自动变量(应该叫这个吧)就是那些局部的变量;命令行参数也就是我们在shell里面输入的参数;
我们每次调用函数;就会吧入口参数;也就是实际参数放在堆中;包括这个函数声明的变量;比如说
int main(int argc,char **argv)
{
int a = 9;
f(a);
b=5;

}
int f(int a)
{
int b = 6;
}
虽然我们看到int a = 9;和f(a);但是他们并不是同一个变量;我们把前一个叫a1后面一个叫a2;
在堆里面是这样分配的
a1;
a2;
b;
这个就是所谓的传值;因为c语言没有传引;C++的引用我的理解是传引;这里看到main对b赋值;这个是不能够通过的;因为当f返回堆里面的a2和b就不存在了;换句话说堆栈寄存器的偏移量已经自动减少了;论坛有文章说Windows默认的栈是1M的;但是linux我没有看到文章说对栈有大小限制;
好了;如果a是指针呢?是传值吗?还是传引?C语言只有传值换句话说即使是指针也是2个变量;所以我们常常听到有人抱怨说为什么我的输出出错了;
7;对于初学者来说;不太理解指针和指针指向的东西;因为我刚开始的时候也很迷惑;
int a = 9;
int *p =&a;
这个时候你的栈中间有个变量a;有个变量p;只不过变量p的内容是a的地址;所以有人常常
int *p;
&p= 4;
问这个为什么是错的;你的p内是一个垃圾数据;他指向了一个或者在堆;或者在栈;或者在代码区;所以这样的指针必然导致灾难的;
我们常常说栈的变量是自动释放的;而堆是不可以的必须free;我们来看下C语言对于的汇编代码;
#include <stdio.h>
#include <stdlib.h>
int f(int a,int b);
int main(int argc,char**argvs)
{
int *p = malloc (100);
int a = 5;
int b = 6;
int c = f(a,b);
free(p);
}
int f(int a,int b)
{
int c = a+b;
return c;
}
汇编代码
.file "main.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movl $100, (%esp)
call malloc
movl %eax, 28(%esp)
movl $5, 24(%esp) //a
movl $6, 20(%esp) //b 我们看到栈是向下生长的
movl 20(%esp), %eax
movl %eax, 4(%esp)//将a放到堆栈里面
movl 24(%esp), %eax
movl %eax, (%esp)//将b放到堆栈里面;这里我们明显看到C语言的传值特性;
call f //调用f
movl %eax, 16(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call free
leave
ret
.size main, .-main
.globl f
.type f, @function
f:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
leal (%edx,%eax), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
.size f, .-f
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
(未完,待续)
8;想正确使用指针;必须正确理解指针;指针的声明一句话讲完了;从变量名开始;看优先级;然后读指针含义
int (*(*f)(int,int(*)(int,float)))(int);
从f开始
1;*f//f是一个指针
2;(*f)()//f是一个函数指针
3;(*f)(int,int(*)(int,float)))//这个函数指针有2个参数int 和int(*)(int,float);后面一个依然是函数指针;
4;函数f的返回值是什么;(*(*f)(int,int(*)(int,float)))//是指针;
5;这个指针指向什么(*(*f)(int,int(*)(int,float)))(int)//一个参数为int的函数;
6;这个返回的函数指针的返回值是什么int;
所以整体是说;我声明了一个指向2有个参数,返回值是函数指针的函数指针;而2个参数是int和执行函数的指针;
知道怎么读指针了;我们再来看看多维数组
int a[3][4];
这个C语言到底表达了什么意思?有人说3×4的数组;其实这个是不准确的;应该是3个向量;每个向量有4个int元素的数组;
现在我们看看这样一段代码
int a[3][4];
int **p;
p = a;
//这个代码是有警告的;说指针类型不匹配;
那么我们看看这样的情况
int a[3][4];
int (*p)[4];
p = a;
//这样的代码是没有警告的;因为类型匹配了;所以我们可以看出;p实际是指向了一个4个元素的数组;所以多维数组其实是一维数组的向量;
9;数组名字到底是什么;为什么有那么多错误和数组名字有关;
int a[4];
a++;
int *p = a;
p++;
//大家知道这段代码a++是不能够通过编译的;而p++却可以;所以有很多人问这样的问题;他们都是地址;为什么不可以;
首先在声明a[4]时候;你的栈空间有4个int的空间分别给了 a[0],a[1],a[2],a[3];声明int *p;有一个空间给了p;那么你做p++;这个时候p这个空间是能够存放++之后的值的;(当然这个值未必立刻存放到这个内存中,可能暂时存放在EAX;EBX等寄存器中);而数组名a是什么;没有空间存放a;换句话说;a这个东西对于C的源程序是有用的;他告诉编译器程序员将a[0]的地址用a来表示;但是不给a分配任何空间;所以不难明白为什么对于a++是非法的了;
好了;下面我们来谈谈多维数组的名字问题;
int a[3][4];
我们不难明白a表示a[0]的地址;而a[0]是一个有4个元素的向量;那么a[0]也就是表示这个向量的数组名;因此也是一个地址;同样的道理;a[0]是不允许做++运算的;我们不能明白*a[0];表是a[0][0]的值;
而&a[0];表示a的值;但是由于a[0]和a的表示同一个值;所以我不难明白a[0]=a;至于其他情况;大家应该能够分析;

9;关于main函数
C99有这样一段描述main函数的
5.1.2.2.1 Program startup
1 The function called at program startup is named main. The implementation declares no
prototype for this function. It shall be defined with a return type of int and with no
parameters:
int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be
used, as they are local to the function in which they are declared):
int main(int argc, char *argv[]) { /* ... */ }
or equivalent;9) or in some other implementation-defined manner.
2 If they are declared, the parameters to the main function shall obey the following
constraints:
— The value of argc shall be nonnegative.
— argv[argc] shall be a null pointer.
— If the value of argc is greater than zero, the array members argv[0] through
argv[argc-1] inclusive shall contain pointers to strings, which are given
implementation-defined values by the host environment prior to program startup. The
intent is to supply to the program information determined prior to program startup
from elsewhere in the hosted environment. If the host environment is not capable of
supplying strings with letters in both uppercase and lowercase, the implementation
shall ensure that the strings are received in lowercase.
— If the value of argc is greater than zero, the string pointed to by argv[0]
represents the program name; argv[0][0] shall be the null character if the
program name is not available from the host environment. If the value of argc is
greater than one, the strings pointed to by argv[1] through argv[argc-1]
represent the program parameters.
— The parameters argc and argv and the strings pointed to by the argv array shall
be modifiable by the program, and retain their last-stored values between program
startup and program termination.
这个是C99标准规定;他说明了main函数是没有原型的;他声明方法;int main(void);int main(int argc,char argv[])或者其他;同时标准规定argv[0]必须是程序名字;argv[argc]是NULL指针;其他是程序的参数;都是指向char的指针;并要求程序是能够修改这些参数的;这些参数的生命周期是从程序开始到程序结束;
所以不要再为main函数的方式迷惑了;同时C99规程当到达};或者return ;或者exit()函数;都是main函数返回给操作系统的值;实际上;当main函数结束都会调用exit函数来执行一些操作;比如说关闭文件描述符;把缓冲的数据写到磁盘上等等;同时我们看汇编知道返回值是通过eax返回给OS的;
10;关于有符号和无符号数;
对于大多数人来说无符号以为这不可能小于0;比如说unsigned int a = -9;是不合法的
请看下面的代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **args)
{
signed int a = -9 ;
signed int c = 8;
unsigned int b = 7;
unsigned int d = -10;
printf("%d,%d,%d,%d",a,b,c,d);
return 3;
}
汇编代码
.file "main.c"
.section .rodata
.LC0:
.string "%d,%d,%d,%d"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $48, %esp
movl $-9, 44(%esp)//a
movl $8, 40(%esp)//c
movl $7, 36(%esp)//b
movl $-10, 32(%esp)//d
movl $.LC0, %eax
movl 32(%esp), %edx
movl %edx, 16(%esp)
movl 40(%esp), %edx
movl %edx, 12(%esp)
movl 36(%esp), %edx
movl %edx, 8(%esp)
movl 44(%esp), %edx
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $3, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
事实上无符号数并没有和有符号数有多少差别;而且运行的结果也是正确的
那么机器里面究竟是如何区别有符号还是无符号的呢?
我们知道计算机的运算电路是很昂贵的;所以计算机只有加法电路;没有减法电路;减法电路是用加法电路来实现的比如说3-4;其实是做3+(-4);这个时候-4用补码来表示;通常是2的补码;也就是取反加1;对于机器来说他并不知道你给他的数字是补码还是原码;或者说这个码字到底表示什么只在于程序员怎么看待;上面的文章中间得到EFLAGS中有一个bit位S表示运算结果是否有符号;S=1表示最高位是有符号的;S=0表示最高位是无符号的;他只是表示运算的结果;并没有说明相加的数是否带符号;
这个S位的描述是这样的:符号标志存放算术或者逻辑运算之后的结果的算术符号;
int main(int argc,char** argv)
{
char a = 222;
char b= 222;
printf("%d",a+b);
system("PAUSE");
return 0;
}
这个代码运行的结果是-68;首先char 是8bit的;222也就是0xDE;a+b=444;这个时候由于数据溢出导致S=1
而结果以256位模得到444-256 = 188;这个时候符号为是1;所以printf认为这个是一个负数;这个负数的补码是188;转换成本来的值就是-68;
我查看了下C99对于unsigned 的说法;并没有找到对unsigned int必须大于0的说法;(如果那个高人找到请通知下);
所以对于有符号或者无符号数;其实是告诉编译器程序员的意图;而机器本身是不知道的;
再看下面的代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **args)
{
signed char a = -9 ;
signed char c = 8;
unsigned char b = 7;
unsigned char d = -10;
printf("%d,%d,%d,%d,%d",a,b,c,d,a+b);
return 3;
}
这里的输出结果有错误 ;
-9,7,8,246,-2
显然d错了;但是也不错;因为对于-10来说他是用246来表示的;我们再看汇编代码;
.file "main.c"
.section .rodata
.LC0:
.string "%d,%d,%d,%d,%d"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
pushl %edi
pushl %esi
pushl %ebx
subl $52, %esp
movb $-9, 47(%esp)//a
movb $8, 46(%esp)//c
movb $7, 45(%esp)//b
movb $-10, 44(%esp)//d
movsbl 47(%esp),%edx
movzbl 45(%esp), %eax
leal (%edx,%eax), %edi
movzbl 44(%esp), %esi//d
movsbl 46(%esp),%ebx
movzbl 45(%esp), %ecx
movsbl 47(%esp),%edx
movl $.LC0, %eax
movl %edi, 20(%esp)
movl %esi, 16(%esp)//d
movl %ebx, 12(%esp)
movl %ecx, 8(%esp)
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $3, %eax
addl $52, %esp
popl %ebx
popl %esi
popl %edi
movl %ebp, %esp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
我们可以看出来;由于S标志位只对算术和逻辑起作用;而对MOV指令不起作用;
(我还是想知道C99到底有没有对unsigned作出限制,请知道的兄弟跟贴教我下)
11;关于标识符连接性的C99解读;
An identifier declared in different scopes or in the same scope more than once can be
made to refer to the same object or function by a process called linkage.21) There are
three kinds of linkage: external, internal, and none.
这个C99的描述;标识符的连接性其实是为了让具有文件作用域的标识符能够作用与其他文件;一个用extern 说明的标识符;其实只是一个声明;他声明了这个标识符;如果外面有这样的标识符的作用范围作用到该标识符的代码块;那么这个标识符和刚才的标识符表示同一个;但是这个声明并不改变原来定义的标识符的链接性;
所以有贴子说在一个h文件定义了一个变量;然后有2个c文件include了这个h文件;导致编译出错;这个问题的解释是这样的;include相当于把h文件的内容直接写到c文件中;这样其实你就定义了2个相同名字的变量;当连接器链接的时候;他无法知道代码是用哪个变量;所以会报重复定义;
大家看3段代码;第1段我用Windows下的CODEBLOCK编译的;编译器选择的是GNU gcc
第2段和第一段一样;不过我是在linux 下用gcc编译的;第3段C代码和第2段有些不同;
//第1;第2段代码;他们相同;
#include <stdio.h>
#include <stdlib.h>

void f(void);
void g(void);
int main(int argc,char *argv[])
{
g();
f();
return 0;
}
void f(void)
{
extern int a;
a++;
printf("%d/n",a);
int a = 100;
extern int a ;
a++;
printf("%d/n",a);
}
void g(void)
{
extern int a;
printf("%d/n",a);
}
int a = 1;

//上面的代码能够在Windows下编译通过;输出是;1 ,2,101;
而在Linux下;编译出错
main.c: In function ‘f’:
main.c:17: error: declaration of ‘a’ with no linkage follows extern declaration
main.c:14: note: previous declaration of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
第三段代码;我们把最后一句放到include后的第一句
如下
main.c: In function ‘f’:
main.c:17: error: redefinition of ‘a’
main.c:3: note: previous definition of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
虽然这样的代码我们是不可能写的;我故意写出来和大家一起探讨下连接性的问题;对于第一个情况好像是正确的;其实不然;如果我们调换下f和g的调用顺序;我们会发现问题所在;
在代码中我们有 int a = 100;
extern int a ;
对于extern int a ;这个;其实我们是声明了一个a;根据C99标准;这个时候由于int a 的声明导致了全局变量a不可见;那么a就表示一个外部的值那么这个a是多少呢;我们不清楚;肯定不会是全局变量的那个a;因为这个a不可见;所以我们的编译器把那个局部变量作为了具有外部连接性;
C99这样说
If no prior declaration is visible, or if the prior
declaration specifies no linkage, then the identifier has external linkage

这个说法有点问题;究竟是说用extern修饰的a具有外部连接性;还是说int a这个本来没有链接性的;具有了外部链接性呢?linx下的gcc认为是后者;
main.c:17: error: declaration of ‘a’ with no linkage follows extern declaration
main.c:14: note: previous declaration of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here
这里第一句说int a这个变量在extern之后;编译器认为在此之前extern修饰的a表示一个外部变量;而声明int a却作用不到这个位置;第3句;是说既然声明的int a 屏蔽了其他a的作用;这个时候说明a的连接性之能够说明int a ;所以说错误的;
而第3中情况;我们由于将全局变量的a 放到了开始;
main.c:17: error: redefinition of ‘a’
main.c:3: note: previous definition of ‘a’ was here
main.c:18: error: extern declaration of ‘a’ follows declaration with no linkage
main.c:17: note: previous definition of ‘a’ was here

所以他报了a定义重复;
我想由于具备变量被声明为外部链接性;这个就和声明了2个相同名字的全局变量一样;会导致很诡异的错误;
而在开始我说如果Windows下将外部变量声明到第一句;输出结果是100;101;101;
嘿嘿;
所以规范代码是很有必要滴;
12;void类型的指针的探讨
我们知道没有void类型的变量;但是有void类型的指针变量;而void指针表示他能够转换成任何一种类型;
所以我们常常这样写
float *fp = NULL;
fp = (float *)calloc(ziseof(float),100);
其实这个是完全没有必要的;
甚至;我们这样的代码也能够通过编译
char **chp;
void *p;
p = chp;
另外;我们知道因为局部变量存放在栈空间;所以我们无法将函数内部的变量返还给调用它的函数;尤其是我们在函数内部用malloc申请了内存;如果处理不当;就会导致内存泄露;
所以我们常常这样声明函数
int f(char **p)
{
*p = malloc(100);
return 0;
}
这样在调用f的地方
char *p;
f(&p)
free(p);
这样我们就不会造成内存泄露;
现在我们提出这样一个问题;如果我想写一个函数;但是我事先并不知道申请的内存到底是给什么样的变量的;那么我们就必须用void指针
void f(void *p);
void g(void **p);
由于void是表示任意类型的指针;所以我们用f的方式也是能够表示char **chp;的;那是不是我们用f的形式表示呢
例如
void f(void *p)
{
*p = malloc(100);
}
而像这样调用
char *p;
f(&p)
free(p);
事实是不可以的;编译器认为*p表示了一个void类型的变量;而不是说void *类型的变量;即使f(&p)是合法的;我们也是无法通过编译的
所以我们不得不用void **这样的形式;换句话说我们必须用指向void *类型的指针;
这次就这么多
13;关于函数返回值的问题;
很多文章提到函数返回值是放到一个临时变量;然后呢再把他赋给某个值;下面我们来看这样一段代码
#include <stdio.h>
#include <stdlib.h>
int a = 1;
int f(int a);
int g(int b);
int main(int argc,char *argv[])
{
int a = 10;
int b = 11;
a = f(a);
b = g(b);
printf("%d/n",a+b);
return 0;
}
int f(int a)
{
return a+2;
}
int g(int b)
{
return b+10;
}
对应的汇编如下
.file "main.c"
.globl a
.data
.align 4
.type a, @object
.size a, 4
a:
.long 1
.section .rodata
.LC0:
.string "%d/n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movl $10, 28(%esp) //a栈中位置
movl $11, 24(%esp)//b
movl 28(%esp), %eax
movl %eax, (%esp)
call f
movl %eax, 28(%esp)//赋给a
movl 24(%esp), %eax
movl %eax, (%esp)
call g
movl %eax, 24(%esp)//赋给b
movl 24(%esp), %eax
movl 28(%esp), %edx
addl %eax, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $0, %eax
leave
ret
.size main, .-main
.globl f
.type f, @function
f:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $2, %eax//返回值放到了eax
popl %ebp
ret
.size f, .-f
.globl g
.type g, @function
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $10, %eax// 返回值放到了eax里面
popl %ebp
ret
.size g, .-g
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
由此可见一个函数的返回值是尽可能放到寄存器里面;而不会直接放到内存中;事实上我们知道intel处理器是不能够在内存和内存之间直接传递数据的;
所以用中间变量的说法有待商榷;就我个人对临时变量的理解是存放在内存中的;如果硬把寄存器当中存放临时变量;那么这样的说法也是可以的;
所以我们现在又个疑问;如果返回值是一个结构体而结构体很大;那么返回值会怎么做呢?
我没有去试验过;我想编译器应该会优先利用寄存器;如果寄存器不够;那么才会用到存储器;C与指针最后的对寄存器的试验;我们可以知道编译器优先利用寄存器;但是未必用完寄存器再用内存;
14;再论intel和linux分页问题;
在我上面的跟帖中;我和大家讨论了intel的内存的分段;也提到了分页;因为当时我没有仔细研究分页;所以也一直没有详细研究;今天写这个是因为很多人问到了关于文件指针,文件描述符,还就就是fopen和open函数的区别;我想要对这个作一个比较深入的探讨需要了解OS是如何来安排应用程序的;
所以我还想从分页开始;前面我们提到CR0-CR4 一共5个寄存器,这些寄存器是用来硬件分页的;我们知道;linux将主要的段系统代码段;系统数据段;用户代码段;用户数据段;都设置成线性地址从0X00000000开始到0Xffffffff结束;我们知道这个时候并不代表实际的内存地址;在32bit寄存器CR0的最高位叫PG;如果这个为1;表示需要讲线性地址分页处理;否则不做分页处理;
一个线性地址分为目录,页表和偏移量;这个怎么理解呢?比如说1个32bit的线性地址;分为10bit的目录,10bit的页表,和偏移量;这个时候10bit的目录和10bit的页表不在是实际的地址了而是对应的目录和页表的偏移量;这个就好像我们在分段时候说的;我们的寄存器的前13bit不在解释成地址,而是GDT或者LDT中的偏移量;那么目录的基地址;或者说起始地址存放在哪里呢;CR0-CRCR3中;(我上次看到具体的寄存器说明的,今天没有查到,忘记那本书上有说了)所以我们有2张表;一张是目录表;一张是页表;

我们知道上面说到的4个重要的段他们是共享线性空间的;所以在分页机制中;linux会把4G中的前3G分给 用户程序;后面1G给内核;(这里我没有深入研究,linux是如何做到的)所以有书说每个进程看到的都是4G的内存空间;这样的说法在linux中是有待商榷的;

好了;我们来注意下最后的所谓的偏移量;12bit;也就是4K;这个大小也就是我们平时说的一页的大小;操作系统会自动给我们的程序分配多少页来运行;

现在是谈论文件描述符的时候了;对于一个操作系统来说;多任务切换;OS需要知道进程的所有信息;在linux中task_struct这个结构体描述了这个进程的所有信息;
其中包括这个进程用了多少页;以及文件描述符;下面我们来看看FILE类型的指针到底是什么;这个是我在linux 下源代码中找来的
struct __stdio_file {
unsigned char *bufpos; /* the next byte to write to or read from */
unsigned char *bufread; /* the end of data returned by last read() */
unsigned char *bufwrite; /* highest address writable by macro */
unsigned char *bufstart; /* the start of the buffer */
unsigned char *bufend; /* the end of the buffer; ie the byte after the last
malloc()ed byte */

int fd; /* the file descriptor associated with the stream */
int mode;

char unbuf[8]; /* The buffer for 'unbuffered' streams */

struct __stdio_file * next;
};
我们看到里面有一个int fd;就是文件描述符;我们完全可以这样理解;对于linux内核来说是用文件描述符来操作文件的;而对于标准的C语言;用FILE来封装了
文件描述符;所以区别文件指针和文件描述符对于我们使用read还是fread来操作文件很有帮助;
另外;有人常问;以二进制打开和以文本方式打开有什么区别;对于linux来说是没有区别的;也就是说对于linux来说他看到的是文件描述符,看到的是二进制文件
但是对应C语言的库函数来说;对这些进行了抽象;产生了流的概念;又对二进制和文本进行了区分;我们知道一个进程能够自动打开3个流;标准输入流;标准输出流
;标准错误流;对于linux内核看来就是3个文件描述符;我们用printf;scanf;还有perror打印、获得这些流信息;看起来标准输入输出好像和文件有很大的区别;
其实就内核看来是没有多少区别的;
这里要提示下;在把一个进程变成守护进程;我们会关闭标准的输入输出流;(这里我没有详细研究过)所以如果一个进程是守护进程,我们无法用printf来打印信息
显示到标准输出;这个时候我们一般是写到一个log文件里面;另外对于初学者来说;可能会把标准输入理解成键盘;标准输出理解成显示器;其实在嵌入式中;由于
没有显示设备;我们通常的标准输出通常是串口;所以理解串口是进入嵌入式学习很基础的要求;
另外一个文件如果是2G;我们完全可以想象的出;OS是不会一次性把所有的文件读到内存中的;所以论坛有人说;一个文件应该用多大的内存来存放;然后进行处理;
其实我想这个一定不是一个很合理的方案;
我们通常说文件有带缓冲的,不带缓冲的;带缓冲又包括全缓冲的;行缓冲的;所有带缓冲的是说;一个文件每次读取他或者写他都会先放到一个缓冲区里面;
只有能缓冲区满才会被写到磁盘上;或者空才会从磁盘读取;如果是行缓冲的;就是缓冲区空;满;或者是一行就对其进行真实的读写操作;我曾经用wrie写了一句话到
某个文件中;总是不对;因为我其实我只是写到了缓冲区;并没有真正写到磁盘;所以必须用fflush冲刷缓冲区;既然有缓冲区的概念;那么我们也可以用setbuf来改写
缓冲区大小;
这次就写到这里吧;干活了;嘻嘻;
15;是谁神话了指针;
看到无数的人为指针伤脑子;想起我为之痛苦的日子,总想写个文章来帮助新人摆脱这样的痛苦;我看了 <C与指针>一句话,我就真正懂了指针;虽然离应用可能还有点距离;所以我一直在想;是谁神话了指针;指针是如此的容易;导致我都不知道怎么写;
指针应该叫指针变量;你有听说过char 类型的变量无法赋值吗?你有听说过把float赋给char 不警告吗?
那么为什么你会觉得把char类型的指针赋给float类型的指针;编译器不会警告呢?那为什么你觉得把char 赋给char的指针编译器会让你通过呢?
但是指针有指针的特殊性;就是他是用来记录地址的;float的长度是sizeof(float);但是我们说他的地址是第一个字节的地址;char的长度是1;他的地址也是第一个字节的地址;设想;如果float的地址不是用第一个字节的地址表示;可能我们就不会把float的指针和char的指针转换了;可惜这样的假设不会成立;
int *f; 其实我们是定义了一个f变量;可惜他是指针;我们不是定义了*f;这个是理解指针的关键;
所以有人问*f = var;和f = &var有什么区别;f是变量;你说有区别吗?后面一个是对f这个变量赋值;
但是前面一个绝对不是对f变量赋值;因为我们用*对f进行了运算;C标准告诉我们这个是对f的内容作为地址的空间进行赋值;
其实我要说的就是这么多;指针叫指针变量;
好了;让我们来看看让我们痛苦的指针吧;
int *f[10];
int (*f)[10];
你觉得有区别吗;f是变量;其他都是来说明这个变量的;在解释之前;我们来看看数组
int f[10];
几乎所有的书都说的不对;或者说根本没有说到点上;我们只是定义了一个变量f;不过有2个东西来说明f
[10] 和int;你觉得f是什么;f首先是一个数组;其次int说明这个数组里面是int类型的数据;
再看上面的指针;
int *f[10];
int (*f)[10];
这个都是说明了f;第一个首先说明f是一个数组;其次告诉f数组中式指针;再次告诉指针指向了int;
第二个;首先说明f是指针;其次说明指针指向了一个拥有10个元素的数组;再次说明这10个元素是int的;
我没有看过什么左右法则;说实在的;我只要懂得优先级就可以;有人说优先级只有在运算中才用到;不过很可惜编译器对指针的定义确实按照优先级表来做的;
好了;其他那么多举例我不想再举了;因为所有的都是按照这样的来看的;再多的也不过2句话
1;你定义的是变量符号;
2;你可以通过优先级来看指针到底是什么样的指针;
现在让我们来看看;
指针和函数的问题;都说在函数中数组退还为指针;我不知道谁先提出了这样的概念;(我没有看英文原版的书,)
我们完全可以设想;如果函数的入参不退化会怎么样;由于C的传值的;那么一个大的数组必然导致了很大的内存复制开销;而很多的时候;我们根本不需要这样的副本;如果你是c编译器设计者;你会不会退化为指针;
这个几乎是必然的;
既然是指针;那么实参和形参的类型匹配很重要;
void f(int a[10][3]);
int main(int argc,char *argv[])
{
int **p;
f(p);
return 0;
}
void f(int a[10][3])
{
return ;
}
这样一定会报类型不匹配的;由于数组退化为指针;所以他的长度是没有意义的;所以int a[10][3]那个10是没有用的;编译器认为我们要传入的参数是;一个指向有3个int元素的数组;所以把p这样定义就成功了
int (*p)[3];;
同样
f这样定义也是可以的
void f(int (*a)[3]);
好了;我想我已经把指针说明白了;希望对大家有些帮助;
16;关于内存的动态分配;
最近工作比较忙,自己也没有做过多的研究,所以论坛的朋友们顶了这么多贴,我如果还不写点,就对不起大家了;
我就写点关于内存分配的一些东西吧,因为这个研究不是很深,所以写起来也未必能说明些啥,抛砖引玉吧;
对于无数新手来说;变量究竟在哪里;已经什么时候起作用,常常是很迷糊的;
首先我们说变量存在于5个区域;我之所以说5个而不是一般教材上说的3个区域;是为了更好的细化;
首先我们知道全局变量存在2个区域;初始化的全局变量区和未初始化的全局变量区BSS;这些都是由编译器和链接器做好的;也就是说当我们的代码写完;这些区的大小就确定了;当然了BSS区只有符号表;不占用磁盘;这个一点如何理解呢;就对linux来说一种可执行的格式叫ELF格式;就像我们刚写代码的时候会把源文件改成exe来执行;其实这个是错误的;因为可执行文件的格式是固定的;所以这个格式是存放在磁盘上的;而BSS只是对未初始化的变量进行描述;
变量还存在一个区域就是栈;也就叫堆栈;这个是因为intel处理器本身硬件就有栈的段寄存器和栈指针;就是为了保持变量的;在中断处理显得尤其明显;
还有救是用malloc分配的动态内存,很多初学者朋友对这个不是很了解;其实在linux内部task_struct有个成员是mm_struct结构体,他描述了本进程所用的所有内存的描述;
在linux里面,内核为进程安排内存的时候,是用线性区来描述的;每个线性区描述了该进程使用的线性空间;而这些线性区不是连续的,而是采用链表的形式来表示的;所以我们不难想象其实虽然应用程序员看起来自己写的进程是使用了0-4G的内存空间;而实际上具体如何使用是有内核来确定的;而在这些线性区里面;有一部分是用来作为动态内存的;2个unsigned long 描述了进程可用的动态内存的起始和结束地址;
所以我们常常说即使我们没有显示的释放申请的动态内存,我们的进程结束之后也释放掉了;因为内存描述结构体不存在了;那么内核就会收回分配给该进程的内存;当然也包括分配给该进程的动态内存;这里我们有一个疑问;就是说如果开始内核分配内这个进程的动态内存是4k;那么如果我们需要8k呢;这点请放心;内存会动态的扩展该进程所使用的线性区;换句话说内核是动态的为该进程分配内存的;即使是该进程在运行期间;

我们来申请2个内存int *p1 = malloc(100); int *p2 = malloc(200);当我们释放的时候free(p1);free(p2);有人经常问;释放的内存到底怎么了;为什么我写的代码有问题;其实在内核分配给进程的动态内存中;也是按照链表实现的;也就是说当我们申请一个动态内存p1来指向;那么在这个开头和结束都是有标志的;所以我们能够确定释放那个部分;
这里我觉得我们应用工程师应该注意的地方;就是当大内存申请和小内存申请;最好是先大内存申请;后小内存申请;因为当我们释放了一个内存之后;就形成了很多洞;而如果这些洞不能够满足大内存的申请;那么内心就会再链表的最后面来申请内存;而不是使用已经释放的内存;(当然了,我没有研究是否内核会自己进行内存的整理)起这样我们就导致了很多内存碎片;这个一点是我个人看了linux内核对malloc处理得出来的结论;当然了未必多么权威;仅供参考;
其实我们并不关心释放后的内存到底是如何被使用的;所以我们只能够说读取已经被释放的内存的内容是不确定的;
我还把变量存放的地方分为寄存器;因为我看到论坛谈到register关键字;这个是建议编译器把用register修饰的变量放到寄存器里面;大家都知道C99把这个工作交给了编译器;而不是程序员;因为如果写了这个就一定放到寄存器里面的话;必然要求程序员了解他的CPU有多少个寄存器;个人觉得这个会导致移植问题;
对于初学者来说;个人觉得了解系统对c语言的理解是有帮助的;典型的就是fork函数;调用一次返回2次;一次是给父进程;一次是给子进程;这个对于初学者来说是不可理解的;为什么一个函数能返回2次;其实这个更多的说不应该是C的特性;而是系统的特性;
另外建议各位朋友看看库函数源代码;我已经在面试的时候被问了好多次了;囧;今天就这样抛砖引玉了;希望能给大家帮助;
17;再论函数指针;
最近研读了《C缺陷与陷阱》对函数和函数指针有了进一步的理解,同时也消除了我对《c与指针》《C缺陷与陷阱》2种讨论方法的误解,其实他们的方法是一致的;把心得写下来;和大家一起探讨;很多东西我都用代码去验证过;不过我是GNU gcc;
函数名字到底是什么?地址?标识符?如果是地址那么我们可以*f用;如果是标识符我们可以&f来用(f是函数名字)下面来看一段代码
int f(int a);
int main(int argc,char** args)
{
int(*fp)(int a);
fp = &f;
printf("%d/n%d/n%d/n",(*f)(44),(&f)(55),f(66));
return 0;
}
int f(int a)
{
return ++a;
}
输出结果是45,56,,67;
我们可以看出来;其实函数名字既是标识符也是地址,C标准就这个问题讨论过;最后是允许这个2种解释都是对的;
这样就带来一个问题,
int(*fp)(int a);
fp = &f;
这里的赋值fp = *f;或者fp = f;是否可以呢;我们发现是能够通过编译的;因此我们得到结论;对于函数名字;我可以认为是标识符或者地址;或者说int a 和int *p类似;而并非教材说的;函数名字就表示起始地址;至少就C语言这个层次来说是不贴切的;
好了;这次就说到这里;希望对大家有帮助
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: