C语言笔记

c语言一直有很多概念容易混淆, 最近花了点时间搞清楚, 在此记录一下

关键字

static

static用于修饰全局变量和函数时,表示该变量/函数只能在本文件中使用,不能在其他文件中使用,与普通变量/函数的差别在于作用域,static在这里起的作用就是隐藏,能避免多个文件之间的变量/函数定义存在冲突,类似C++中的private。

当static修饰函数中的局部变量时,该变量的作用域不变,还是仅作用于函数内,但是会将变量的存放位置由栈变为数据段(data段或bss段,根据初始化情况决定),这样局部变量的值可以在两次函数调用间传递,下次函数调用时其值就是上次函数调用后的值,跟全局变量一样;还有就是变量的生存周期将变为从程序开始运行到程序退出。strtok函数就是利用静态局部变量实现的。

静态局部变量与普通局部变量的差别是存储位置不同,以及生存周期不同,而他们的作用域是一致的。

注意:包含静态局部变量的数是不可重入、非线程安全的,所以当有多个线程都在调用strtok函数时,会造成错误的结果。

typedef

typedef用于为现有类型创建一个别名,提高代码可读性。

使用typedef的技巧在于先按普通方式定义一个变量,然后将变量名替换成新的类型别名,前面加typedef。

比如定义指向char的指针别名时,先定义一个普通的指针char * ptr,然后在此基础上将变量名ptr换为新类型别名名字,比如PCHAR:char * PCHAR,然后在前面加上typedef即可:typedef char * PCHAR

同理定义一个函数指针:

1
2
3
4
5
6
7
8
//定义普通函数指针
int (*max)(int, int)
//替换变量名
int (*MAX_FUNC)(int, int)
//前面加typedef
typedef int (*MAX_FUNC)(int, int)
//用新类型定义变量
MAX_FUNC func;

const

const的本质不是让变量变得不可修改,这是很多人对const的误解,const的本质是表示该变量在程序里只读

const定义的局部变量其实是可以修改的,const局部变量存放在栈中,所以通过指针指向它就可以修改它的值;而const全局变量存放在text段中,text段是只读的,所以无法修改。

const局部变量可以通过指针来修改它的值:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
void main()
{
int const a = 10;
int *p = (int*)&a;
*p = 20;
printf("*p=%d\n", *p);
printf("a=%d\n", a);
}
1
2
3
4
root@5494622fa74a:~# gcc -o const const.c
root@5494622fa74a:~# ./const
*p=20
a=20

可以看到a的值被修改了,通过gdb可以看到a的存放地址在栈中:

1
2
3
4
5
6
7
root@5494622fa74a:~# gdb --ex "disas main" --batch const
Dump of assembler code for function main:
0x00000000004004f4 <+0>: push %rbp
0x00000000004004f5 <+1>: mov %rsp,%rbp
0x00000000004004f8 <+4>: sub $0x10,%rsp
0x00000000004004fc <+8>: movl $0xa,-0x4(%rbp);a处于栈空间
...

const的定义很容易让人搞混,尤其是在有指针的情况下。总结起来const可以有一下几种情况:

1
2
3
4
5
6
7
8
const int a;
int const a;
const int * a;
int const * a;
int * const a;
const int * const a;
int const * const a;
const int const * a;

其实上面几种情况只有4种是完全不同的,其他的只是形式不同,意思是一样的。

关键在于const修饰第一个关键字时,可以放在前面也可以放在后面,所以const int aint const a表达的意思完全一样。为了容易理解,建议将const都放在所修饰关键字的后面,因为在有typedef的时候,const放在前面很容易引起错误的理解。比如:

1
2
3
4
typedef char * string;
const string name; //此时很容易让人误解为name的定义是const char *,实际是错误的; 这个定义表达的是char * const name; 如果将const放在类型后面就可以避免出现这种误解
string const name; //直观的替换也可以,表示的定义为char * const name,
//这个定义表达的真正的意思: name是一个常量指针,指向char类型。name指向的位置不能改变,但指向的内存中的字符串是可以改变的

将前面所有处在第一个位置的const放到关键字后面后就可以简化成下面几种情况:

1
2
3
4
5
6
int const a; //a是一个int常量
int const * a; //a是一个指向int常量的指针,a可以指向其他位置,但是现在*a只读
int * const a; //a是一个常量指针,指向int;a指向的位置不能改变,但是*a可以改变
int const * const a; //a是一个常量指针,指向int常量,a和*a都只读
int const const * a; //两个const在一起编译器不会报错,但是与int const * a意思一样

所以最终理解const就是跟在谁后面谁就是常量

volatile

volatile碰到的情况不多,一个定义为volatile的变量是说这变量可能会被意想不到地改变,所以编译器不会去假定这个变量的值,不会对其进行编译优化,而且运行时每次都会重新取值,而不是从寄存器中取缓存值。

常见用volatile的地方有:

  • 中断服务程序(ISR)
  • 并行设备的硬件寄存器(状态寄存器)
  • 多线程应用中的共享变量

volatile与const的的位置一样,放谁后面就是修饰谁,第一个关键字时可以放在最前面。

顺时针螺旋法则

C语言中变量定义可能会出现非常逆天的情况,比如void (*signal(int, void (*fp)(int)))(int);,这时候signal表示的是啥(╯‵□′)╯︵┻━┻

为此专门有一个法则来解读变量定义,就是顺时针螺旋法则,有能力的建议直接阅读原版http://c-faq.com/decl/spiral.anderson.html

这个法则更适合英语来解读变量,中文来解读有些地方会有倒置,不太通畅。

简单来讲就是从变量本身开始,按顺时针走,碰到什么符号就解读,碰到右括号不出去绕到括号左边的内容先解释再绕出该括号,依次走到最后就能把这个变量的意思搞清楚。

简单定义

1
2
3
4
5
6
7
+-------+
| +-+ |
| ^ | |
char *str[10];
^ ^ | |
| +---+ |
+-----------+
  • 从str开始,顺时针旋转,碰到第一个字符是[,所以表示str是一个数组:

    str是一个包含10个元素的…..数组(str is an array 10 of…)

  • 继续旋转,下一个东西是*,表示找到了指针:

    str是一个包含10个元素的指针数组(str is an array 10 of pointers to…)

  • 继续顺时针走,碰到;,这个不管,继续往下转,来到char:

    str是一个包含10个元素的指针数组,指向char类型(str is an array 10 of pointers to char)

    明显英文通顺的多,中文习惯要颠倒。

    理解一下意思:str是一个数组,长度为10,每一项是指向char的指针

函数指针定义

1
2
3
4
5
6
7
8
9
+--------------------+
| +---+ |
| |+-+| |
| |^ || |
char *(*fp)( int, float *);
^ ^ ^ || |
| | +--+| |
| +-----+ |
+------------------------+
  • 从fp开始转,首先碰到右括号),因此,fp是在括号里的,所以继续旋转,碰到*:

    fp是一个指针(fp is a pointer to…)

  • 现在我们出了括号,来到(,表示我们碰到了一个函数:

    fp是一个指向函数的指针,参数是int和float指针,返回…..(fp is a pointer to a function passing an int and a pointer to float returning…)

  • 继续旋转,来到*:

    fp是一个指向函数的指针,参数是int和float指针,返回一个指向…..的指针(fp is a pointer to a function passing an int and a pointer to float returning a pointer to…)

  • 接下来是;,但是还有东西没走完,继续顺时针旋转,来到char:

    fp是一个指向函数的指针,参数是int和float指针,返回一个指向char的指针(fp is a pointer to a function passing an int and a pointer to float returning a pointer to a char)

终极挑战

1
2
3
4
5
6
7
8
9
+-----------------------------+
| +---+ |
| +---+ |+-+| |
| ^ | |^ || |
void (*signal(int, void (*fp)(int)))(int);
^ ^ | ^ ^ || |
| +------+ | +--+| |
| +--------+ |
+----------------------------------+

那signal是什么意思?

注意signal在括号里面,所以我们必须先解决括号内部的定义

  • 从signal开始顺时针旋转,遇到(,表示我们碰到了一个函数:

    signal是一个函数,参数为int 和什么gui? (ノಠ益ಠ)ノ彡┻━┻

  • 呃…我们可以将顺时针螺旋法则同样运用于fp,fp顺时针旋转,碰到),所以继续旋转到*

    fp是一个指针,指向…

  • 继续顺时针来到(

    fp是一个指针,指向函数,传递参数为int,返回…

  • 继续旋转来到函数括号外面,到void:

    fp是一个指针,指向函数,传递参数为int,返回void(没有返回)

  • fp算是告一段落,接下来继续signal:

    signal是一个函数,参数为int和一个指向参数为int且没有返回值的函数指针fp,signal返回值为…

  • 记住,我们现在还处在signal的括号内,所以继续旋转来到字符*:

    signal是一个函数,参数为int和一个指向参数为int且没有返回值的函数指针fp,signal返回一个指向…..的指针

  • 接下来转到了右边的括号(,表示又来了一个函数:

    signal是一个函数,参数为int和一个指向参数为int且没有返回值的函数指针fp,signal返回一个指向函数的指针,参数为Int,返回值为…

  • 往下走, 最后旋转到左边的void, 所以最终signal的定义为:

    signal是一个函数,参数为int和一个指向参数为int且没有返回值的函数指针fp,signal返回一个参数为Int且没有返回值的函数指针.

回过头来看之前的const就很清楚明了了:

const char * chptr; chptr是一个指针,指向char类型的常量

char * const chptr; chptr是一个常量指针,指向char类型

volatile char * const chptr; chptr是一个常量指针, 指向容易改变的char

变量存储位置

一个C语言程序的内存主要由以下几部分构成:

  1. text段,存放程序代码
  2. data段,存放经过初始化的数据
  3. bss段,存放未经初始化的数据

Memory Layout

运行中进程的典型内存空间

bss段的命名来自一个古老的汇编程序操作符,表示Block Started by Symbol,bss段中的数据在程序执行前由内核初始化为0,所以存放在bss段中的数据默认都会初始化为0,包括全局变量和静态变量。

先说结论:全局变量和静态变量(包括静态全局变量和静态局部变量)生存周期都是从程序运行到程序结束;经过初始化且值不为0的变量会存放在程序的data段,如果未初始化(或初始化值为0)则存放在BSS段。

变量 未初始化 初始化为0 初始化为非0
全局变量 bss段 bss段 data段
静态全局变量 bss段 bss段 data段
普通局部变量
静态局部变量 bss段 bss段 data段

以下通过实验来证明:

基础情况

没有全局变量和局部变量的情况,记录data段和bss段的大小供后面参考

1
2
#include <stdio.h>
void main(void){}
1
2
3
4
root@5494622fa74a:~# gcc -o 1 1.c -m32
root@5494622fa74a:~# size 1
text data bss dec hex filename
1056 252 8 1316 524 1

data段长度为252字节,bss段长度为8字节

全局变量

未初始化

1
2
3
#include <stdio.h>
int global; //未初始化的全局变量
void main(void){}
1
2
3
4
root@5494622fa74a:~# gcc -o 2 2.c -m32
root@5494622fa74a:~# size 2
text data bss dec hex filename
1056 252 12 1320 528 2

bss段从8变成了12,增加了4个字节,说明未初始化的全局变量放在bss段中

初始化为0

1
2
3
#include <stdio.h>
int global = 0; //初始化为0
void main(void){}
1
2
3
4
root@5494622fa74a:~# gcc -o 4 4.c -m32
root@5494622fa74a:~# size 4
text data bss dec hex filename
1056 252 12 1320 528 4

可以看到,bss段增加了4,所以全局变量初始化为0时,数据存放在bss段!

初始化为非0值

1
2
3
#include <stdio.h>
int global = 1; //初始化为非0值
void main(void){}
1
2
3
4
root@5494622fa74a:~# gcc -o 3 3.c -m32
root@5494622fa74a:~# size 3
text data bss dec hex filename
1056 256 8 1320 528 3

data段由252变成了256,增加了4自己。说明全局变量初始化为非0值时,存放在data段中

静态全局变量

经过测试,静态全局变量的存放位置与全局变量一致,所以静态全局变量与全局变量最大的差别仅在于作用域不同(可以类比C++中的private),静态变量不能被当前文件以外的文件访问到。当然这里所说的文件是指经过预处理后的文件,所以被include文件中的静态全局变量在当前文件中是可以访问到的。

1
2
//head.h
static char *str = "hello world";
1
2
3
4
5
6
//inc.c
#include "head.h"
#include <stdio.h>
void main(){
printf("%s\n",str);
}
1
2
3
root@5494622fa74a:~# gcc -o inc inc.c
root@5494622fa74a:~# ./inc
hello world

可见,头文件中的静态全局变量包含后是可以访问到的

普通局部变量

普通的局部变量是程序运行时在栈上分配的空间,与data段和bss段没有关系。

静态局部变量

未初始化

1
2
3
4
#include <stdio.h>
void main(void){
static int a; //未初始化
}
1
2
3
4
root@5494622fa74a:~# gcc -o 6 6.c -m32
root@5494622fa74a:~# size 6
text data bss dec hex filename
1056 252 12 1320 528 6

可以看到bss段增加了4,所以未初始化的静态局部变量是存放在bss段中的

初始化为0

1
2
3
4
#include <stdio.h>
void main(void){
static int a=0; //初始化为0
}
1
2
3
4
root@5494622fa74a:~# gcc -o 8 8.c -m32
root@5494622fa74a:~# size 8
text data bss dec hex filename
1056 252 12 1320 528 8

可以看到bss段增加了4,所以初始化为0的静态局部变量也是存放在bss段中的

初始化为非0

1
2
3
4
#include <stdio.h>
void main(void){
static int a=1; //初始化为非0
}
1
2
3
4
root@5494622fa74a:~# gcc -o 7 7.c -m32
root@5494622fa74a:~# size 7
text data bss dec hex filename
1056 256 8 1320 528 7

data段增加了4,所以初始化为非0时,静态局部变量是存放在data段中的

由上可知,静态局部变量跟全局变量的存放位置完全一致!这也就能解释为什么值可以在两次函数调用间传递,因为静态局部变量和全局变量存在一起!唯一的差别在于作用域而已,静态局部变量的作用域被限制在函数内部。

参考

http://www.geeksforgeeks.org/memory-layout-of-c-program/

http://stackoverflow.com/questions/93039/where-are-static-variables-stored-in-c-c

http://www.inf.udec.cl/~leo/teoX.pdf

http://www.cnblogs.com/jeakon/archive/2012/10/06/2813685.html

http://stackoverflow.com/questions/14588767/where-in-memory-are-my-variables-stored-in-c

http://c-faq.com/decl/spiral.anderson.html