Table of Contents:

[toc]

## 内存

 程序编译后的text段、data段和bss段 


左边的是UNIX/LINUX系统的执行文件,右边是对应进程逻辑地址空间的划分情况。


一般情况,一个程序本质上都是由 bss段、data段、text段三个段组成——这是计算机程序设计中重要的基本概念。

 bss
bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零(bss段属于静态内存分配,即程序一开始就将其清零了)。
比如,在C语言程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中
而bss段不在可执行文件中,由系统初始化
 text
text段: 用于存放程序代码的区域, 编译时确定, 只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text段合在一起,但不是简单的将它们“堆”在一起就完事,还需要处理各个段之间的函数引用问题。
* data
data段 :用于存放在编译阶段(而非运行时)就能确定的数据,可读可写。也是通常所说的静态存储区,赋了初值的全局变量、常量和静态变量都存放在这个域。

 关于data和bss段区别

程序1:

int ar[30000];
void main()
{
......
}

程序2:

int ar[300000] = {1, 2, 3, 4, 5, 6 };
void main()
{
......
}

发现程序2编译之后所得的可执行文件比程序1大得多,为什么?
区别很明显,程序1位于bss段,程序2位于data段,两者的区别在于:
全局的未初始化变量存在于bss段中,具体体现为一个占位符,全局的已初始化变量存于data段中,而函数内的自动变量都在栈上分配空间。
bss不占用可执行文件空间,其内容由操作系统初始化(清零),裸机程序需要自行手动清零。
而data段则需要占用可执行文件空间,其内容由程序初始化,因此造成了上述情况。

 一个程序在计算机中到底是如何运行的?

程序是保存在硬盘中的,要载入内存才能运行,CPU也被设计为只能从内存中读取数据和指令
我们经常听说多少位的CPU,指的就是寄存器的的位数
缓存的容量是有限的,CPU只能从缓存中读取到部分数据,对于使用不是很频繁的数据,会绕过缓存,直接到内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。关于缓存的命中率又是一门学问,哪些数据保留在缓存,哪些数据不保留,都有复杂的算法。

 虚拟内存到底是什么?为什么我们在C语言中看到的地址是假的?

虚拟地址:虚拟地址的整个想法是这样的:把程序给出的地址看做是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。
让我们回到程序的运行本质上来。用户程序在运行时不希望介入到这些复杂的内存管理过程中,作为普通的程序,它需要的是一个简单的执行环境,有自己的内存,有自己的CPU,好像整个程序独占整个计算机而不用关心其他的程序。
使不同程序的地址空间相互隔离
如果所有程序都直接使用物理内存,那么程序所使用的地址空间不是相互隔离的。恶意程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意、但是有 Bug 的程序也可能会不小心修改其他程序的数据,导致其他程序崩溃。

 虚拟地址空间以及编译模式

int a = 1, b = 255;
int main(){
    int c = 1;
    printf("&a = %p, &b = %p &c = %p\n",&a, &b, &c);   // a, b的地址始终是相同的,c的地址会随机的变。
    pause();
    return 0;
}

全局去的变量在编译的时候就确定了,栈区的变量为保护程序安全会进行随机的栈地址。
所谓虚拟地址空间,就是程序可以使用的虚拟地址的有效范围。虚拟地址和物理地址的映射关系由操作系统决定,相应地,虚拟地址空间的大小也由操作系统决定,但还会受到编译模式的影响。
CPU是计算机的核心,决定了计算机的数据处理能力寻址能力,也即决定了计算机的性能。CPU一次(一个时钟内)能处理的数据的大小由寄存器的位数和能支持的数据总线的宽度(也即有多少根数据总线)决定,我们通常所说的多少位的CPU,除了可以理解为寄存器的位数,也可以理解数据总线的宽度,通常情况下它们是相等的。

数据总线主频都是CPU的重要指标:数据总线决定了CPU单次的数据处理能力,主频决定了CPU单位时间内的数据处理次数,它们的乘积就是CPU单位时间内的数据处理量。
需要注意的是,数据总线地址总线不是一回事,数据总线用于在CPU和内存之间传输数据,地址总线用于在内存上定位数据,它们之间没有必然的联系,宽度并不一定相等。实际情况是,地址总线的宽度往往随着数据总线的宽度而增长,以访问更大的内存。

 实际支持的物理内存

CPU支持的物理内存只是理论上的数据,实际应用中还会受到操作系统的限制,例如,Win7 64位家庭版最大仅支持8GB或16GB的物理内存,Win7 64位专业版或企业版能够支持到192GB的物理内存。
Windows Server 2003 数据中心版专为大型企业或国家机构而设计,可以处理海量数据,分为32位版和64位版,32位版最高支持512GB的物理内存,这显然超出了32位CPU的寻址能力,可以通过两次寻址来实现

 编译模式

 32位编译模式
在32位模式下,一个指针或地址占用4个字节的内存,共有32位,理论上能够访问的虚拟内存空间大小为 2^32 = 0X100000000 Bytes,即4GB,有效虚拟地址范围是 0 ~ 0XFFFFFFFF。 
对于32位的编译模式,不管实际物理内存有多大,程序能够访问的有效虚拟地址空间的范围就是0 ~ 0XFFFFFFFF,也即虚拟地址空间的大小是 4GB。换句话说,程序能够使用的最大内存为 4GB,跟物理内存没有关系。
如果物理内存大于 4GB,例如目前很多PC机都配备了8GB的内存,那么程序也无能为力,它只能够使用其中的 4GB。
 64位编译模式
在64位编译模式下,一个指针或地址占用8个字节的内存,共有64位,理论上能够访问的虚拟内存空间大小为 264。这是一个很大的值,几乎是无限的,就目前的技术来讲,不但物理内存不可能达到这么大,CPU的寻址能力也没有这么大,实现64位长的虚拟地址只会增加系统的复杂度和地址转换的成本,带不来任何好处,所以 Windows 和 Linux 都对虚拟地址进行了限制,仅使用虚拟地址的低48位(6个字节),总的虚拟地址空间大小为 248 = 256TB。

需要注意的是:
32位的操作系统只能运行32位的程序(也即以32位模式编译的程序),64位操作系统可以同时运行32位的程序(为了向前兼容,保留已有的大量的32位应用程序)和64位的程序(也即以64位模式编译的程序)。
64位的CPU运行64位的程序才能发挥它的最大性能,运行32位的程序会白白浪费一部分资源。

 C语言内存对齐,提高寻址效率

CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据;少了浪费主频,多了没有用。64位的处理器也是这个道理,每次读取8个字节。
以32位的CPU为例,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如 0、4、8、12、1000 等,而不会对编号为 1、3、11、1001 的内存寻址。

将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。在32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐。
为了提高存取效率,编译器会自动进行内存对齐
经过笔者测试,对于全局变量,GCC在 Debug 和 Release 模式下都会进行内存对齐,而VS只有在 Release 模式下才会进行对齐。而对于局部变量,GCC和VS都不会进行对齐,不管是Debug模式还是Release模式。

 改变对齐方式

内存对齐虽然和硬件有关,但是决定对齐方式的是编译器,如果你的硬件是64位的,却以32位的方式编译,那么还是会按照4个字节对齐。
对齐方式可以通过编译器参数修改,以VS2010为例,更改对齐方式的步骤为:项目 --> 属性 --> C/C++ --> 代码生成 --> 结构成员对齐
最后需要说明的是:内存对齐不是C语言的特性,它属于计算机的运行原理,C++、Java、Python等其他编程语言同样也会有内存对齐的问题。

 内存分页机制,完成虚拟地址的映射

现代计算机都使用分页(Paging)的方式对虚拟地址空间和物理地址空间进行分割和映射,以减小换入换出的粒度,提高程序运行效率。

页的大小是固定的,由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。目前几乎所有PC上的操作系统都是用 4KB 大小的页。

 分页机制究竟是如何实现的?

 MMU部件以及对内存权限的控制

通过页表完成虚拟地址和物理地址的映射时,要经过多次转换,还要进行计算,如果由操作系统来完成这项工作,那将会成倍降低程序的性能(比如cpu上下文的频繁切换),得不偿失,所以这种方式是不现实的。
在CPU内部,有一个部件叫做MMU(Memory Management Unit,内存管理单元),由它来负责将虚拟地址映射为物理地址

在页映射模式下,CPU 发出的是虚拟地址,也就是我们在程序中看到的地址,这个地址会先交给 MMU,经过 MMU 转换以后才能变成了物理地址。

即便是这样,MMU也要访问好几次内存,性能依然堪忧,所以在MMU内部又增加了一个缓存,专门用来存储页目录和页表。MMU内部的缓存有限,当页表过大时,也只能将部分常用页表加载到缓存,但这已经足够了,因为经过算法的巧妙设计,可以将缓存的命中率提高到 90%,剩下的10%的情况无法命中,再去物理内存中加载页表。

MMU 只是通过页表来完成虚拟地址到物理地址的映射,但不会构建页表,构建页表是操作系统的任务。在程序加载到内存以及程序运行过程中,操作系统会不断更新程序对应的页表,并将页目录的物理地址保存到 CR3 寄存器。MMU 向缓存中加载页表时,会根据 CR3 寄存器找到页目录,再找到页表,最终通过软件和硬件的结合来完成内存映射。
CR3 是CPU内部的一个寄存器,专门用来保存页目录的物理地址。
每个程序在运行时都有自己的一套页表,切换程序时,只要改变 CR3 寄存器的值就能够切换到对应的页表。

 Linux下C语言程序的内存布局(内存模型)

程序内存在地址空间中的分布情况称为内存模型(Memory Model)
内存模型由操作系统构建,在Linux和Windows下有所差异,并且会受到编译模式的影响
Windows 在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为1GB),而 Linux 默认情况下会将高地址的 1GB 空间分配给内核。也就是说,应用程序只能使用剩下的 2GB 或 3GB 的地址空间,称为用户空间(User Space)。

程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。

 常量区和全局数据区有时也被合称为静态数据区,意思是这段内存专门用来保存数据,在程序运行期间一直存在。

#include <stdio.h>

char *str1 = "c.biancheng.net";  //字符串在常量区,str1在全局数据区
int n;  //全局数据区

char* func(){
    char *str = "C语言中文网";  //字符串在常量区,str在栈区
    return str;
}

int main(){
    int a;  //栈区
    char *str2 = "01234";  //字符串在常量区,str2在栈区
    char  arr[20] = "56789";  //字符串和arr都在栈区
    char *pstr = func();  //栈区
    int b;  //栈区

    return 0;
}

注意:
 1) 全局变量的内存在编译时就已经分配好了,它的默认初始值是 0(它所占用的每一个字节都是0值),局部变量的内存在函数调用时分配,它默认初始值是不确定的,由编译器决定,一般是垃圾值,。
 2) 函数 func() 中的局部字符串常量"C语言中文网"也被存储到常量区,不会随着 func() 的运行结束而销毁,所以最后依然能够输出。
* 3) 字符数组 arr[20] 在栈区分配内存,字符串"56789"就保存在这块内存中,而不是在常量区,大家要注意区分。

Linux下64位环境的用户空间内存分布情况:

 Windows下C语言程序的内存布局(内存模型)

不像 Linux,Windows 是闭源的,有版权保护,资料较少,不好深入研究每一个细节,至今仍有一些内部原理不被大家知晓。

 用户模式和内核模式

前面一直说“程序的地址空间”,这其实是不严谨的,应该说“进程的地址空间”。
内核空间存放的是操作系统内核代码和数据,是被所有程序共享的
要想访问内核空间,必须借助操作系统提供的 API 函数,执行内核提供的代码,让内核自己来访问,这样才能保证内核空间的数据不会被随意修改,才能保证操作系统本身和其他程序的稳定性。
用户程序调用系统 API 函数称为系统调用(System Call);发生系统调用时会暂停用户程序,转而执行内核代码(内核也是程序),访问内核空间,这称为内核模式(Kernel Mode)。
用户空间保存的是应用程序的代码和数据,是程序私有的,其他程序一般无法访问。当执行应用程序自己的代码时,称为用户模式(User Mode)。

计算机会经常在内核模式和用户模式之间切换:
 当运行在用户模式的应用程序需要输入输出、申请内存等比较底层的操作时,就必须调用操作系统提供的 API 函数,从而进入内核模式;
 操作完成后,继续执行应用程序的代码,就又回到了用户模式。

既然内核也是一个应用程序,为何不让它拥有独立的4GB地址空间,而是要和用户程序共享、占用有限的内存呢?
让内核拥有完全独立的地址空间,就是让内核处于一个独立的进程中,这样每次进行系统调用都需要切换进程
而让内核和用户程序共享地址空间,发生系统调用时进行的是模式切换,模式切换仅仅需要寄存器进栈出栈,不会导致缓存失效;现代CPU也都提供了快速进出内核模式的指令,与进程切换比起来,效率大大提高了

 栈(Stack)是什么?栈溢出又是怎么回事?

从本质上来讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能对当前的栈进行定位。
通常使用ebp(Extend Base Pointer )寄存器指向栈底,而使用esp(Extend Stack Pointer)寄存器指向栈顶。
对每个程序来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。
在 Linux GCC 下,默认是 8M。
一个程序可以包含多个线程,每个线程都有自己的栈,严格来说,栈的最大值是针对线程来说的,而不是针对程序。

//这个就会造成Stack Overflow
int main(){
    char str[1024*1024*10] = {0};
    return 0;
}

 一个函数在栈上到底是怎样的?

函数的调用和栈是分不开的,没有栈就没有函数调用
当发生函数调用时,会将函数运行需要的信息全部压入栈中,这常常被称为栈帧(Stack Frame)或活动记录(Activate Record)。
 1) 函数的返回地址
 2) 参数和局部变量。
 3) 编译器自动生成的临时数据。例如,当函数返回值的长度较大(比如占用40个字节)时,会先将返回值压入栈中,然后再交给函数调用者。当返回值的长度较小(char、int、long 等)时,不会被压入栈中,而是先将返回值放入寄存器,再传递给函数调用者。
 4) 一些需要保存的寄存器,例如 ebp、ebx、esi、edi 等。之所以要保存寄存器的值,是为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。
理论上 ebp 寄存器应该指向栈底,但在实际应用中,它却指向了old ebp。

 关于数据的定位

由于 esp 的值会随着数据的入栈而不断变化,要想根据 esp 找到参数、局部变量等数据是比较困难的,所以在实现上是根据 ebp 来定位栈内数据的。ebp 的值是固定的,数据相对 ebp 的偏移也是固定的,ebp 的值加上偏移量就是数据的地址

 函数调用惯例(Calling Convention)

我们知道,一个C程序由若干个函数组成,C程序的执行实际上就是函数之间的相互调用。
函数的参数(实参)由调用方压入栈中供被调用方使用,它们之间要有一致的约定。例如,参数是从左到右入栈还是从右到左入栈,如果双方理解不一致,被调用方使用参数时就会出错。所以,函数调用方和被调用方必须遵守同样的约定,理解要一致,这称为调用惯例(Calling Convention)
一个调用惯例一般规定以下两方面的内容:
 1) 函数参数的传递方式,是通过栈传递还是通过寄存器传递(64位用寄存器)。
 2) 函数参数的传递顺序,是从左到右入栈还是从右到左入栈。
 3) 参数弹出方式。函数调用结束后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由调用方来完成,也可以由被调用方来完成。
 4) 函数名修饰方式。函数名在编译时会被修改,调用惯例可以决定如何修改函数名。

在C语言中,存在多种调用惯例,可以在函数声明或函数定义时指定。
在函数声明处是为调用方指定调用惯例,而在函数定义处是为被调用方(也就是函数本身)指定调用惯例。
例如:

#include <stdio.h>
int __cdecl max(int m, int n);
int main(){
    int a = max(10, 20);
    printf("a = %d\n", a);
    return 0;
}
int __cdecl max(int m, int n){
    int max = m>n ? m : n;
    return max;
}

__cdecl是C语言默认的调用惯例,在平时编程中,我们其实很少去指定调用惯例,这个时候就使用默认的__cdecl。除了 cdecl,还有其他调用惯例:cdecl、stdcall、fastcall等

 __cdecl 并不是标准关键字,上面的写法在 VC/VS 下有效,但是在 GCC 下,要使用 attribute((cdecl))。

 用一个实例来深入剖析函数进栈出栈的过程


经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。

 栈溢出攻击的原理是什么?

int main(){
    char str[10] = {0};
    gets(str);
    printf("str: %s\n", str);
    return 0;
}


局部数组也是在栈上分配内存,当输入"12345678901234567890" 时,会发生数组溢出,占用“4字节空白内存”、“old ebp”和“返回地址”所在的内存,并将原有的数据覆盖掉,这样当 main() 函数执行完成后,会取得一个错误的返回地址,该地址上的指令是不确定的,或者根本就没有指令,所以程序在返回时出错。
C语言不会对数组溢出做检测,这是一个典型的由于数组溢出导致覆盖了函数返回地址的例子,我们将这样的错误称为“栈溢出错误”。
这里的栈指的是当前的函数栈帧。
除了上面讲到的 gets() 函数,strcpy()、scanf() 等能够向数组写入数据的函数都有导致栈溢出的风险。
栈溢出一般不会产生严重的后果,但是如果有用户精心构造栈溢出,让返回地址指向恶意代码,那就比较危险了,这就是常说的栈溢出攻击。

 C语言动态内存分配

静态内存分配,在进程的地址空间中,代码区、常量区、全局数据区的内存在程序启动时就已经分配好了,它们大小固定,不能由程序员分配和释放,只能等到程序运行结束由操作系统回收。
动态内存分配栈区堆区的内存在程序运行期间可以根据实际需求来分配和释放,不用在程序刚启动时就备足所有内存。
使用静态内存的优点是速度快,省去了向操作系统申请内存的时间,缺点就是不灵活,缺乏表现力,例如不能控制数据的作用范围,不能使用较大的内存。而使用动态内存可以让程序对内存的管理更加灵活和高效,需要内存就立即分配,而且需要多少就分配多少,从几个字节到几个GB不等;不需要时就立即回收,再分配给其他程序使用。

 动态内存分配函数

1) malloc()

void* malloc (size_t size);

2) calloc()

void* calloc(size_t n, size_t size);

3) realloc()

void* realloc(void *ptr, size_t size);

4) free()

void free(void* ptr);//同一个地址不可以释放两次 ,Null可释放多次没有问题

 malloc函数背后的实现原理——内存池

一种做法是把 malloc() 的内存管理交给系统内核去做,既然内核管理着进程的地址空间,那么如果它提供一个系统调用,可以让 malloc() 使用这个系统调用去申请内存,不就可以了吗?当然这是一种理论上的做法,但实际上这样做的性能比较差,因为每次程序申请或者释放堆空间都要进行系统调用
malloc() 实现是向操作系统申请一块适当大小的堆空间,然后由 malloc() 自己管理这块空间。如果用完了就再向系统申请空间。

 链表式内存管理

在程序运行过程中,堆内存从低地址向高地址连续分配,随着内存的释放,会出现不连续的空闲区域,如下图所示:

带阴影的方框是已被分配的内存,白色方框是空闲内存或已被释放的内存。程序需要内存时,malloc() 首先遍历空闲区域,看是否有大小合适的内存块,如果有,就分配,如果没有,就向操作系统申请(发生系统调用)。为了保证分配给程序的内存的连续性,malloc() 只会在一个空闲区域中分配,而不能将多个空闲区域联合起来。
内存块(包括已分配和空闲的)的结构类似于链表,它们之间通过指针连接在一起。


malloc() 和 free() 所做的工作主要是对已有内存块的分拆和合并,并没有频繁地向操作系统申请内存,这大大提高了内存分配的效率。

 链表式内存管理缺点:

 一旦链表中的 pre 或 next 指针被破坏,整个堆就无法工作,而这些数据恰恰很容易被越界读写所接触到。
 小的空闲区域往往不容易再次分配,形成很多内存碎片。
* 经常分配和释放内存会造成链表过长,增加遍历的时间。

 内存池

不管具体的分配算法是怎样的,为了减少系统调用,减少物理内存碎片,malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,这就是内存池(Memory Pool)。
内存池的研究重点不是向操作系统申请内存,而是对已申请到的内存的管理
* 池化技术
在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。

 空指针和野指针(悬挂指针)

指针指向非法的内存地址,那么这个指针就是悬挂指针,也叫野指针。意为无法正常使用的指针。

#define NULL 0 # 实际上就是让指针=0,就是让指针受控制

解决野指针的方案:
1. 定义指针时 把指针变量赋值成null
2. 释放内存时,先判断指针变量是否为null 
3. 释放内存完毕后,把指针变量重新赋值成null

 if (p != NULL)
 {
  free(p);
  p = NULL;
 }

 1.使用未初始化的指针

void main()  
{  
    char *p,*p1="hello first!";  
    while((*(p++) = *(p1++)) != 0);  
}  

错处:
p定义时没有初始化,p是指向不定,是一个野指针。
p++可能引用得空间为非法的。编译时不会出错,但运行时会造成程序崩溃。

 2.操作NULL指针

#include   
void main()  
{  
    char *p=NULL,*p1="hello first!";  
    while((*(p++) = *(p1++)) != 0);  
}

NULL表示没有任何指向。p++没有任何意义,运行时会造成程序崩溃。这里就应该想到不能对NULL的指针进行直接操作。

 3.使用指向释放掉的内存

void function( void )  
{  
    char* str = new char[100];  
    delete[] str;  
    // Do something  
    strcpy( str, "Dangerous!!" );  
}      
```   
之所以说其危险,是因为这是一段完全合乎语法的代码,编译的时候完美得一点错误也不会有,
然而当运行到strcpy一句的时候,问题就会出现,因为在这之 前,
str的空间已经被delete掉了,所以strcpy当然不会成功。
#### 4.指针所指的对象已经消亡
比如函数中的局部变量在函数调用结束消失

char  getstring3()
{
    char buf[30];
    strcpy(buf, "abcde");
    return buf; //因为其在栈空间内,掉用此函数返回的是错的
}     
int main()
{
    char 
 ch = getstring3()

#### 5.指针指向常量区且对其写访问

 {
    char p = "abcdefg";
    p = 100;
    printf("%x \n", p);
    
p = 'z'; //也会报错,字符串是在全局区-->常量区
 }

#### 6.realloc函数使用不当

include<malloc.h>

void main()
{
    charp,q;
    p=(char)malloc(10);
    q=p;
    p=(char
)realloc(q,20);
    //…………………………
}

realloc时会把原来内存空间中的内容复制到新开辟的内存中,返回新的内存首地址。此时**q就成了悬空指针**。


### 内存泄露
#### 0.函数中调用malloc()返回分配的地址,但是调用函数没用free()
#### 1.malloc两次都指向同一个指针变量

int main(){
    char p = (char)malloc(100  sizeof(char));
    p = (char
)malloc(50 * sizeof(char));
    free(p);
    p = NULL;
    return 0;
}

#### 2.一个指针变量赋给了另一个

int pOld = (int) malloc( sizeof(int) );
int pNew = (int) malloc( sizeof(int) );
pOld=pNew;



## 指针
----------------------
### C语言指针是什么?1分钟彻底理解C语言指针的概念
**一切都是地址**
数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会**给不同的内存块指定不同的权限**,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。**编译和链接**过程的一项重要任务就是找到这些名称所对应的地址。

### C语言指针变量的定义和使用(精华)

int a, b, c; //a、b、c 的类型都是 int
int *a, b, c;   //只有a是指针

使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。

对星号`*`的总结

表示定义一个指针变量,以和普通变量区分开,例如int a = 100; int p = &a;。
表示获取指针指向的数据,是一种间接操作,例如int a, b, 
p = &a; p = 100; b = p;。

### 指针变量的运算(加法、减法和比较运算)

### 数组指针(指向数组的指针)详解
在C语言中,我们将第 0 个元素的地址称为**数组的首地址**
arr、p、&arr[0] 这三种写法都是等价的

int arr[] = { 99, 15, 100, 888, 252 };
int p = arr;
int 
p = &arr[0];

**arr本身就是一个指针”这种表述并不准确,严格来说应该是“arr 被转换成了一个指针”**
访问数组方式:
* 1) 使用下标
也就是采用 `arr[i]` 的形式访问数组元素。本质是编译器帮我们翻译成`*(p+i)`
* 2) 使用指针
也就是使用 `*(p+i)` 的形式访问数组元素。

不同的是,数组名是**常量,它的值不能改变**。数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。


### C语言字符串指针(指向字符串的指针)详解
两种方式:

char str[] = "http://c.biancheng.net";
char *str = "http://c.biancheng.net";    //我们将第二种形式的字符串称为字符串常量

它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。
全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,
而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

include <stdio.h>

int main(){
    char *str = "Hello World!";
    str = "I love C!"; //正确,可以把指针指向其他地方
    str[3] = 'P'; //错误
    return 0;
}

到底使用字符数组还是字符串常量?
获取用户输入的字符串就是一个典型的写入操作,只**能使用字符数组**,不能使用字符串常量。


### 指针变量作为函数参数

int max(int intArr[6], int len){ ... }

不管是int intArr[6]还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,它们最终还是会转换为`int *intArr`这样的指针。
int intArr[6]这种形式只能说明**函数期望**用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素。

// 二维数组的情况
void f(int a[5])-----> void f(int a[]); ----->  void f(int a);
void g(int a[3][5])-----> void g(int a[][5]); ----->  void g(int (
a)[5]);  
Int printArray(char p[3])----->Int printArray(char p[])----->Int printArray(char p)
一维数组 char a[30]             指针 char
指针的数组 char 
a[30]            指针的指针 char 
a
二维数组 char a[10][30]        指向数组的指针 char(*a)[30]

关于形参:
写在函数上形参变量,还是写在函数里面的变量,从CC++编译的角度来讲,是没有任何区别的(分配4字节内存);只不过是 写在函数上形参变量 ,具有**对外的属性**而已

### 指针和函数
1. 一级指针做输入

Int showbuf(char p);   
int showArray(int 
array, int iNum)

2. 一级指针做输出

char  changeNum(int num)
{
*num = 10;
}
int a = 1;
changeNum(&a);

3. 返回一级指针
注意:返回的指针不能指向局部变量。

char  getBuffer()
{
 char 
p = (char )malloc(100sizeof(char));
 return p;
}

4. 二级指针做输入(通常是字符串数组)

int main(int arc ,char *arg[]);          //指针数组
int shouMatrix(int [3][4], int iLine);  //二维字符串数组

5. 二级指针做输出(改变指针的值)

void getBuffer(char ** buff)
{
 char p = (char )malloc(100sizeof(char));
 
buff = p;
 return;
}

一般都有对应的释放函数

int freeBuffer(char *p)
{
    if (p ==NULL)
    {
        return -1;
    }
    if (p != NULL)
    {
        free(p);
    }
    return 0;
}


### C语言二级指针(指向指针的指针)详解
如果一个指针指向的是另外一个指针,我们就称它为二级指针
其实还是指针,只是用来表明含义用

int a =100;
int *p1 = &a;
int **p2 = &p1;

C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号`*`,实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针


### C语言空指针NULL以及void指针
在进程的虚拟地址空间中,最低地址处有一段内存区域被称为**保留区**,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。
强烈建议对没有初始化的指针赋值为 NULL。
很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作
NULL 是在stdio.h中定义的一个宏,它的具体内容为:
`#define NULL ((void *)0)`
C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来,例如下面的写法是不专业的:
`int *p = 0;`
而应该坚持写为:
`int *p = NULL;`

`void *`表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。
`void *p = &a;` //空类型的指针可以接受任何地址,但是不可以直接使用,需要转换成特定类型
C语言动态内存分配函数 malloc() 的返回值就是`void *`类型


### 数组和指针绝不等价,数组是另外一种类型
数组名表示数组的首地址,不幸的是,这是一种非常危险的想法
数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针
**站在编译器的角度讲**,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。
变量名用来指代一份数据,**数组名**用来指代一组数据(数据集合),**它们都是有类型的**,以便推断出所指代的数据的长度。
对,数组也有类型,这是很多读者没有意识到的,大部分C语言书籍对这一点也含糊其辞!我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。
int a[6] = {0, 1, 2, 3, 4, 5};   它的类型是`int [6]`,表示这是一个**拥有 6 个 int 数据的集合**。`int [3]`表示3个 int 数据的集合
整数、小数、数组、指针等不同类型的数据都是对内存的抽象,它们的名字用来指代不同的内存块,程序员在编码过程中不需要直接面对内存,使用这些名字将更加方便。
编译器在编译过程中会创建一张专门的**表格(符号表)** 用来保存名字以及名字对应的数据类型、长度、地址、作用域等信息,sizeof 是一个操作符,不是函数,使用 sizeof 时可以从这张表格中查询到符号的长度。
与普通变量名相比,数组名既有一般性也有特殊性:
一般性表现在数组名也用来指代特定的内存块,也有类型和长度;
特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值。


### 数组到底在什么时候会转换为指针
数组作函数参数
* 1) 用 a[i] 这样的形式对数组进行访问总是会被编译器改写成(或者说解释为)像 `*(a+i)` 这样的指针形式。
* 2) 指针始终是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。
* 3) 在特定的环境中,也就是**数组作为函数形参**,也只有这种情况,一个数组可以看做是一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。


### C语言指针数组(数组每个元素都是指针)详解
指针数组(数组的每个元素都是指针)的定义形式一般为:
`dataType *arrayName[length];`
`[ ]`的优先级高于`*`  解读的话可以按由近到远的原则判断
指针数组还可以和字符串数组结合使用

include <stdio.h>

int main(){
    char *str[3] = {
        "c.biancheng.net",
        "C语言中文网",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

### C语言二维数组指针(指向二维数组的指针)详解
二维数组的三种形式:
![](https://sunxvming.com/imgs/659b6b52-2b75-4a3a-acf1-fe40a0513e44.bmp)

int (p1[5]); //指针数组,可以去掉括号直接写作 int p1[5];    还可以用由近到远的原则判断
int (*p2)[5]; //二维数组指针,不能去掉括号,  它是一个指针,指向5个int数组的指针。

`[ ]`的优先级高于`*`,`( )`是必须要加的,如果赤裸裸地写作`int *p[4]`,那么应该理解为`int *(p[4])`,p 就成了一个指针数组,而不是二维数组指针

二维数组指针的含义:
* 1) p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。
* 2) `*(p+1)`表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素
* 3) `*(p+1)+1`表示第 1 行第 1 个元素的地址
`*(p+1)`单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为**使用整行数据没有实际的含义**,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针
* 4) `*(*(p+1)+1)`表示第 1 行第 1 个元素的值


int num[5] = {1,2,3,4,5}
num代表数组首元素地址 &num代表数组的地址 他们的步长不一样

sizeof(num) //4
sizeof(
&num) 20

int num[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}
这是一个包含3个元素,每个元素又包含4个int的数组

指针的步长,根据所指内存空间类型来定
num代表第一个4个int的东西 步长为16
*num 步长为4
&num步长为48



### C语言函数指针(指向函数的指针)详解
函数指针的定义形式为:
`returnType (*pointerName)(param list);`
注意`( )`的优先级高于`*`,第一个括号不能省略,如果写作`returnType *pointerName(param list);`就成了函数原型,它表明函数的返回值类型为`returnType *`。
例子:

include <stdio.h>

//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定义函数指针
    int (pmax)(int, int) = max; //也可以写作int (pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

### 只需一招,彻底攻克C语言指针,再复杂的指针都不怕
C语言标准规定,**对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析**。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!就近原则。


### main()函数的高级用法:接收用户输入的数据
main() 是C语言程序的入口函数,有且只能有一个,它实际上有两种标准的原型:

int main();
int main(int argc, char *argv[]);

### 对C语言指针的总结

定 义        含 义
int *p;       p可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p;      p为二级指针,指向 int  类型的数据。
int 
p[n];    p为指针数组。[ ] 的优先级高于 ,所以应该理解为 int (p[n]);
int (p)[n];  p为二维数组指针。
int 
p();     p是一个函数声明,它的返回值类型为 int 
int (
p)();   p是一个函数指针,指向原型为 int func() 的函数。

### 深copy和浅copy 
结构体复制的时候,如果有指针的话,只会复制其指针

typedef struct _AdvTeacher
{
 char *name;
 char buf[100];
 int age;
}Teacher ;

Teacher  creatT()
{
 Teacher 
tmp = NULL;
 tmp = (Teacher )malloc(sizeof(Teacher));
 tmp->name = (char 
)malloc(100);
 return tmp;
}
void FreeT(Teacher t)
{
 if (t == NULL)
 {
  return ;
 }
 if (t->name != NULL)
 {
  free(t->name);
 }
}
//copy函数
int copyObj(Teacher 
to, Teacher from)
{
 //
to = from;//copy;
 memcpy(to, from, sizeof(Teacher));
 to->name = (char 
)malloc(100);
 strcpy(to->name, from->name);
}
```

 段错误

段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况