总结之《征服C指针》
对于接触过c语言,而没有理解指针以及内存布局的人来说,《征服C指针》是不错的选择。
这本书中的补充内容也值得阅读,通过和java,c++等一些语言比较,来体会c语言的魅力。
第一章 从基础开始
几乎所有的处理程序中,所谓的“指针类型的值”,实际是指内存的地址。
p28 的补充值得看,主要讲了NULL, 0,'\0'的区别,不要使用NULL来结束字符串,一般都用'\0'
p49 补充,c语言是不做数组下标越界检查的
要点: 1.变量不一定按照声明的顺序保存在内存中。
2.对变量使用&运算符,可以取得该变量的地址。这个地址成为指向该变量的指针。
3.指针变量hoge_p保存了指向其他变量的地址的情况下,可以说“hoge_p指向hoge”。
4.对指针变量运用*运算符,就等同于它指向的变量。如果hoge_p指向hoge, *hoge_p就等同于hoge。
5.对指针加N,指针前进“当前指针指向的数据类型的长度*N”
6.p[i]是*(p+i)的简便写法。
7.在C中是不能将数组作为函数参数进行传递的。但是,你可以通过传递指向初始元素的指针来达到将数组作为参数进行传递的目的。
第二章 虚拟内存
这一章对于操作系统内存管理薄弱的人来说,看看有益
一个程序用两个窗口来跑,相当于是两个进程,两个进程之间相互独立互不影响,但是在打印变量的内存地址时却是同一个地址。why?
原因是输出指针的时候,打印输出的并不是物理内存地址本身。
当今操作系统都会给每一个进程分配独立的“虚拟地址空间”。这和C语言没有关系,而是操作系统和cpu协同工作的结果。正是因为操作系统和cpu努力地为每一个进程分配独立的地址空间,所以就算我们制造了一个bug,破坏了某个内存区域,顶多会使得当前程序趴窝,不影响其他进程。
当然,真正保存内存数据的还是物理内存。操作系统负责将物理内存分配给虚拟地址空间,同时还会对每一个内存区域设定“只读”或者“可读写”等属性。
通常,因为程序的执行代码是只读的,所以有时候会和其他进程共享物理内存。另外,当我们启动了几个笨重的应用程序而使内存出现不足时,操作系统把物理内存中还没有被引用的部分倒腾出来,保存到硬盘上。当程序再次需要引用这个区域的数据的时候,再从磁盘写回到内存。这个操作是在操作系统的后台进行的,对于应用程序来说,压根儿不知道背后发生的事。这是硬盘“咔嗒咔嗒”响,机器就慢了下来。
之所以能够这样,多亏了虚拟地址。正式因为避免了让应用程序直接面对物理内存的地址,操作系统才能够顺利地对内存区域进行重新配置。
要点:
C中有三种内存领域的寿命
1.静态变量的寿命从程序运行时开始,到程序关闭时结束。
2.自动变量的寿命到声明该变量的语句块执行结束为止。
3.通过malloc()分配的领域的寿命到调用free()为止。
将函数自身和字符串常量汇总配置在一个只读内存区域。执行时程序是只读的,在同一份程序被同时启动多次的时候,通过在物理地址上共享程序
能够节约物理内存。此外,由于硬盘上已经存放了可执行程序,就算内存不足,也不需要将程序交换到虚拟内存,相反可以将程序直接从内存中销毁。
char *s = "abc";
s[0] = 'd';
这个字符串常量是配置在只读内存区域的,所以该写法s[0]='d' 是不行的。
静态变量总是在虚拟地址空间上占有固定的区域。
各个目标代码之间通过链接器链接处理生成可执行程序 .o文件即符号表(可重定位文件),就是根据符号表的内容对函数,变量进行重定位链接。比如函数声明在一个a.h文件中,
定义在a.cpp文件中,而调用在b.cpp文件中,这时需要用链接器将函数的定义链接到b.cpp。
在声明自动变量的函数执行结束后,自动变量就不能被使用了。所以自动变量重复使用内存区域,自动变量的地址是不一定的。
C语言中,在现有被分配的内存区域之上以“堆积”的方式,为新的函数调用分配内存区域。在函数返回的时候,会释放这部分内存区域供下一次
函数调用。
C语言函数调用的实现:
1.在调用方,参数从后往前按顺序被堆积在栈中。这就为可变长参数提供了基础,可变长参数永远都可以在内存中寻找到第一个参数。
2.和函数调用关联的返回信息也被堆积在栈中。所谓的返回地址,是指函数处理完毕后应该返回的地址。正因为返回地址被堆积在栈中,所以无论函数从什么地方
被调用,它都能返回到调用点的下一个处理。
3.跳转到作为被调用对象的函数地址。
4.栈为当前函数所使用的自动变量增长提供所需大小的内存区域。
5.在函数的执行过程中,为了进行复杂的表达式运算,有时候会将计算过程中的值放在栈中。
6.一旦函数调用结束,局部变量占用的内存区域就被释放,并且使用返回信息返回到原来的地址。
7.从栈中出去调用方的参数。
p70的补充 一旦函数执行结束,自动变量的内存区域就会被释放。
assert(条件表达式) 若条件表达式的结果为真,什么也不会发生;若为假,则会输出相关信息并且强制终止程序。
利用malloc来进行动态内存分配
p = malloc(size);
free(p);
并且可以通过任意的顺序释放的记忆区域,称为堆。
malloc的返回值的类型为 void*
系统调用brk()就是通过设定这个内存区域的末尾地址,来伸缩内存空间的函数的。
调用函数的时候,栈会向地址较小的一方伸长。多次调用malloc()时,会调用一次brk(),内存区域会向地址较大的一方伸长。
要点:
1.malloc不是系统调用
malloc()遍历链表寻找空的块,如果发现尺寸大小能够满足使用的块,就分割出来将其变成使用中的块,并且向应用程序返回紧邻管理区域
的后面区域的地址。
free()将管理区域的标记改写成“空块”,顺便也将上下空的块合并成一个块,这样可以防止块的碎片化。
free()之后,对应的内存区域是不会立刻返还给操作系统的。
calloc()使用和malloc()相同的方式分配nmemb*size大小的内存区域,并且将该区域清零返回。也就是和一下代码有同等效果
p = malloc(nmemb * size);
memset(p, 0, nmemb * size);
就算我们使用了calloc(),在释放内存的时候也请使用free()。
还有一个realloc()
realloc(void *ptr, size_t size);
如果通过ptr传递的区域后面有足够大小的空闲空间,就直接实施内存区域扩展。但是,如果后面的区域没有足够多的空闲空间,就分配其他新的
空间,然后将内容复制过去,而原来的部分会被操作系统自动回收。
另外,注意内存布局的对齐,和字节排序 大端存储和小端存储。
第三章
主要着重讲解了指针类型和数组类型,以及多维数组(其实在C中,是没有多维数组的,因为在内存中的存储是一维的)
解读C的声明 const修饰符是重点 也是面试里面的基础
const char *str 或 char const *str
str = NULL;// OK 只读的是str所指的对象
*str = 'a';// not OK
char * const str 只读的是str自身
str = NULL;// not OK
*str = 'a';// OK
const char * const str
str = NULL;// not OK
*str = 'a';// not OK
另外注意结构体所用const的修饰符,在结构体上用const,结构体中的元素仍然可以修改,除非给结构体中的元素也加上const修饰。
要点:
1.只有在声明函数形参的情况下,int a[]和int *a才具有相同的意义。
2.extern的声明,意味着“使在某处声明的对象能够在当前的地方使用”,因此它不是定义 extern int a;
字符串常量
char *str = "abc";
"abc"就是普通的"char的数组",在表达式中被解释成"指向char的指针",然后被赋给str。
第四章
注意数组和指针的常用方法
可变长数组 和 p165 java数组的联系。 java的数组是保存在堆中的(因为声明int[] hoge = new int[10]),但不能改变长度,他没有类似于realloc()这样的函数。
可变长结构体
第五章
实践,推荐读者认真将这一章的两个案例 分别实现 有助于理解