六、指针数组 和 数组指针
1. 指针数组: 定义形式: int *p[n] = { 0 }; []的优先级高于*,那么p先和[]结合,说明这是一个数组。 再和int *结合,说明这个数组里的每个元素都是一个指针,每个元素都能保存一个地址。
1) 使用指针数组保存多个数据的地址 int main(void) { int a[3] = { 10, 20, 30}; int *p[3] = { 0 }; for (int i = 0; i < 3; ++i) p = &a;
for (int i = 0; i < 3; ++i) { printf("%p\n", p); printf("%d\n", *p); } }
2) 使用指针数组保存多个字符串的首地址 #include <stdio.h>
int main(void) { char *str[5] = { "ISO/IEC9899:2011", "Programming", "Dennis Ritchie", "c", "bell ssss" };
char *str1 = str[1]; //取出 第2行的字符串 char *str2 = *(str + 3); // 取出 第4行的字符串 char ch1 = *(*(str + 4) + 2); //取出 第5行的字符串的第3个字符 char ch2 = (*str + 5)[7]; //取出第1个字符串的第6个字符,并以此做数组首元素,向后遍历到第7个。 char ch3 = *str[0] + 6; // 取出第1个字符串的第1个字符,然后把这个字符ASCII码加上6,再用%c打印出来
printf("str1 = %s\n", str1); // Programming printf("str2 = %s\n", str2); // c printf("ch1 = %c\n", ch1); //l printf("ch2 = %c\n", ch2); // 2 printf("ch3 = %c\n", ch3); // O (不是0)
return 0; }
2. 数组指针(行指针) 定义形式: int *p; int (*p)[n]; int (*p)[n][m]; ()和[]的优先级相同,但是结合性是从左往右,那么p先和*相结合,说明这是一个指针。 然后再和[]结合,说明这个指针指向了一个数组。
示例: 1) #include <stdio.h>
void func(int (*p)[3], intn); int main(void) { int a[2][3] = {10, 20, 30, 40, 50, 60}; // 行: 0 1 // 列: 0 1 2 0 1 2 func(a, sizeof(a)); return 0; }
void func(int p[2][3], intn); // 明确行和列的数量
void func(int p[][3], intn); //明确列的数量
void func(int (*p)[3], intn) //明确列的数组指针 { printf("%d\n", p[1][2]); // 取出第2行的第3个元素:60
printf("%d\n", **p); //p是行指针,先是取出当前行的列地址,再取出这个列地址里的值: 10
printf("%d\n", (*p + 1)[2]); // 先取出列地址,然后往后移动一个int(4个字节),再以这一列的下标为起点,取出后面第2个元素的值:40
printf("%d\n", *(*p + 1)); // 先取出列地址,往后移动1位(1个int),再取出这个下标的值:20
printf("%d\n", *(p[1] + 2)); // 先取出p[1]的列下标地址,再往后移动2位(2个int),再取出这个下标的值:60
printf("%d\n", *(*(p + 1))); // 先将行指针p后移一位(3个int),再取出这一行的这一列的下标地址,再根据这个下标取值:40
printf("%d\n", *((*p + 1) + 2)); //这个表达式相当于 *(*p + 3),先取出列下标地址,然后往后移动3位(3个int),再取值:40
printf("%d\n", *(*p + 1) + 2); // 先取出列下标的地址,然后往后移动一位(1个int),再进行取值得出20,再加2,结果是:22
}
// 三维数组同理 void func(int (*p)[3][4]) { printf("%d\n", *(*(*(p + 0) + 1 ) + 2); //70 }
int main(void) { int a[2][3][4] = { {{10, 20, 30, 40}, {50, 60, 70, 80}, {90, 100,110, 120}}, {{11, 22, 33, 44}, {55, 66, 77, 88}, {99, 101,111, 121}} };
func(a); return 0; }
总结: 1.指针数组:就是一个数组,这个数组里的每个元素都是一个指针,这个数组在内存空间中占用了n个指针的大小。
2.数组指针:就是一个指针,这个指针指向一个数组,这个指针在内存空间中占用一个指针的大小。
3.二级指针p 和二维数组名p 的区别: int **p; 这个是一个整型的二级指针,p是一个可变句柄/钥匙,我们可以让这个指针指向任何我们希望它指向的地方。 这个句柄不需要指定内存空间的大小。 int p[n][m]; 这个是一个整型的二维数组,p是引用了这块内存空间的句柄/钥匙,数组在定义的时候, 就被固定指向某个内存空间了,这个空间大小是 sizeof(int) * n * m,而且不可修改p的指向, p就是一个不可变的常量,永远的都只能指向这里了。
如果想确定某个一维数组的值: * 、 [] 如果想确定某个二维数组的值:** 、 [][] 、 *[]
七、内存四区
stack: 栈区,是由编译器自动分配和释放,主要是存放函数参数的值,局部变量的值。
heap:堆区,是由程序员自己申请分配和释放,需要 malloc(); calloc(); realloc();函数来申请,用free()函数来释放 如果不释放,可能出现野指针。
**函数不能返回指向栈区的指针,但是可以返回指向堆区的指针。**
data:数据区 -> 静态(全局)区 和 常量区 静态(全局)区:标有 static 关键字,保存了静态变量和全局变量 1. 初始化的全局变量和初始化的静态变量,在一块区域; (data段存放在编译阶段(而非运行时)就能确定的数据,可读可写。 也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存放在这个区域;)
2.未初始化的全局变量和为初始化的静态变量,在一块区域; (BSS段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0)
3. 静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。 (如果不初始化,数值数据将被默认初始化为 0, 字符型数据默认初始化为 NULL )。
常量区:这里的数据是只读的,常量和字符串都保存在这里。(不包括字符数组类型的字符串 -> 栈区) 除了第一次初始化外,常量区的数据在程序执行的时候不允许再次赋值。
整个数据区的数组,在程序结束后由系统统一销毁。
code:代码区,用于存放编译后的可执行代码,二进制码,机器码。
/* BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。
BSS是英文Block Startedby Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(datasegment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。
数据段属于静态内存分配。
代码段:代码段(codesegment/text segment)通常是指用来存放程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为
可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当
进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用
free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量
(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,
其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把
堆栈看成一个寄存、交换临时数据的内存区。 */
static 关键字详解: static 在C语言里面既可以修饰变量,也可以修饰函数。
static 变量: 1. 静态局部变量:在函数中定义的,生命周期是整个源程序,但是作用域和自动变量没区别。 都是只能在定义这个变量的函数范围内使用,而且只能在第一次进入这个函数时候被初始化, 之后的初始化会跳过,并保留原来的值。退出这个函数后,尽管这个变量还在,但是已经不能使用了。
2. 静态全局变量:全局变量本身就是静态存储的,但是静态全局变量和非静态全局变量又有区别: 1) 全局变量:变量的作用域是整个源程序,其他源文件也可以使用,生命周期整个源程序。 2) 静态全局变量:变量的作用域范围被限制在当前文件内,其他源文件不可使用,生命周期整个源程序。
static 函数(内部函数): 只能被当前文件内的其他函数调用,不能被其他文件内的函数调用,主要是区别非静态函数(外部函数)
总结: 作用域:变量或函数在运行时候的有效作用范围 。 生命周期:变量或函数在运行时候的没被销毁回收 的存活时间。
作用域 生命周期
局部变量 所在代码块内 所在函数结束
全局变量 所有文件内 程序执行结束
静态局部变量 所在代码块内 程序执行结束
静态全局变量 当前文件内 程序执行结束
普通函数 所有文件内 程序执行结束
静态函数 当前文件内 程序执行结束
八、堆区内存
#include <stdlib.h>
1. void* malloc(n * sizeof(int)); 请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回 NULL;
2. void* calloc(n, sizoef(int)); 请求 n 个连续的、每个长度是一个int大小的堆空间,如果创建成功,将返回这个堆空间的首地址,如果创建失败,返回NULL ; (和 malloc() 函数的区别在于,calloc()在创建成功后,会把空间自动初始化为 0 );
3. void *realloc(p, n * sizeof(int)); 给一个已经分配了地址的指针 p 重新分配空间,p 是原来空间的首地址,n * sizeof(int) 基于这个首地址重新分配的大小; 1) 如果当前内存段后面有足够的内存空间,那么就直接扩展这段内存,realloc()返回原来的首地址; 2) 如果当前内存段后面没有足够的内存空间,那么系统会重新向内存树申请一段合适的空间,并将原来空间里的数据块释放掉, 而且 realloc() 会返回重新申请的堆空间的首地址; 3) 如果创建失败,返回 NULL, 此时原来的指针依然有效;
4. void free(); 1) free(p); 只是释放了申请的内存,系统将这块内存标记为可用。也就是可以被其他进程使用,但是并不改变 p 的指向; 2) p 所指向的内存空间被释放,所以其他程序就有机会使用这段空间了,相当于 p 指向了不属于自己的空间,里面的数据也是未知的。 (这个就叫野指针) 3) 为了避免野指针,最好在 free(p)之后,将 p = NULL; void *(0); 4) free()函数在执行的时候,其实是把这个块内存返回了内存红黑树上,让别人可以使用这块内存。 从逻辑上来说,释放p之后,你是不能再访问原先p指向的这块内存了,但是现在操作系统没有做到, 所以你还是可以访问到这块内存的,只是里面可能存有的数据不属于你。 free(p)之后,其实系统并没有做数据清空处理,所以你既可以访问这个空间,也可以用里面的值。 但是严格意义上来说,这样做是非法的!会造成野指针!
示例: // 如何释放自定义函数内申请的堆空间 #include <stdio.h> #include <stdlib.h> #include <string.h>
char *funcA(); char *funcB();
int a = 10; // 全局 初始化区域 char *p1; // 全局 为初始化区域
int main(void) { int b; //栈区 char arr[] = "hello"; // 栈区 char *p2; //p2 在栈区 const char *p3 = "world!"; // p3 在栈区,"world!\0" 在常量区
static int c = 0; //静态区 初始化区域
char *p;
p1 = (char *)malloc(20); //p1 指向堆区 20个字节
memset(p1, 0, sizeof(char) * 20); // 使用memset()函数将内存空间初始化为 0 strcpy(p1, "Are you Sleep?"); // "Are you Sleep?" 是在常量区
printf("%s\n", p1); // p1 指向的 拷贝到堆空间的 "Are you Sleep?" 的首地址,通过首地址打印这个字符串
p2 = funcB(); //p2 接收了funcB()回传堆空间首地址,可以通过这个地址找到funcA()申请的堆空间
free(p2); //也可通过 p2 释放自定义函数里申请的对空劲啊 free(p1);
p1 = NULL; //安全起见,释放堆空间指针后,重置将指针变量置为 NULL p2 = NULL;
return 0; //返回 0 给系统表示 main()正常执行结束,也就代表程序执行结束 }
char *funcA() { int a = 10; const char *pa = "1234567"; // pa 在栈区, "1234567\0" 在常量区 char *pb = NULL; // pb 在栈区,pb 占4字节(32bit system)
pb = (char *)malloc(20); //pb 指向了一个20个字节大小的堆空间 strcpy(pb, "Yes, I'm!"); // 拷贝字符串给 pb,"Yes, I'm!\0" 在常量区
return pb; //返回 指针变量 pb 保存的堆空间首地址给调用函数funcB() }
char *funcB() { char *pa = NULL; //pa 是一个栈上的指针变量 pa = funcA(); //pa 接收了 funcA()函数返回的堆空间地址 return pa; //返回指针变量 pa 保存的堆空间首地址给调用函数 main(); }
论空间分配速度: 栈区确实略快于堆区, 使用栈的时候,是直接从分配的地址里读取值,放到寄存器里,然后再放到目标地址。 使用堆的时候,是先将分配的地址放到寄存器里,然后再从这个地址里取值,再放到寄存器里,再放到目标地址。
论空间访问速度: 栈区和堆区是一样的,都是一个直接寻址的过程,没有区别。
CPU -> 寄存器 > L1 >L2 > L3 (属于缓存) > RAM(内存) > SRAM(主板的存储器) > 硬盘 CPU 只和 寄存器做数据存取,寄存器是用来存储临时数据的,对于需要重复操作的数据,会放到缓存里。 不管是寄存器还是缓存,数据都来自于内存, 内存呢又分为四个区....
未完待续,请看下一篇《C语言核心知识点相关总结(四)》
|