C语言深度剖析学习笔记-指针、数组、内存、函数
2017-06-28 21:04
1026 查看
指针与数组
指针
指针就是一个变量, 只不过这个变量的值,是一个内存地址而已;指针变量 加/减 整数,所表达的含义是:相对当前指针值的偏移,偏移量为: 整数*指针指向的数据类型的大小;
相同类型的指针 相减,所表达的含义是:这两个指针之间的偏移量(元素的个数) <=>
((long)ptr1-(long)ptr2) / sizeof(data_type);
相同类型的指针不能进行相加运算;
不同类型的指针不能进行算数运算。
函数指针
1. 函数指针
函数与变量/常量一样,也要存储与内存中,只不过变量/常量存的是数据,而函数存的是逻辑或者叫算法。之前说过,凡是内存中的东西,都可以通过指针访问(当然这只是理论上),那函数也不例外,也可以通过指针来访问,指向函数的指针成为函数指针,函数名称就代表函数的地址,当然也可以对函数名取地址,二者效果相同,即func() => func <=> &func。
char *func(const char* ch1, const char* ch2); // 声明一个函数 char (*pfunc)(const char* ch1, const char* ch2); // 定义一个函数指针
上面的代码中,func是一个返回字符指针的函数,而pfunc是一个返回字符的函数指针,二者的形参列表是相同的。
2. 函数指针数组
#define N 10 char* pdata ; char* (*pfunc )(const char *p); // <=> char* (*)(const char *p) pfunc ; // |-----------------------| 函数指针类型
上面的代码定义了一个函数指针数组,跟指针数组没什么区别,只不过指针数组里的元素指向的是数据,而函数指针数组里面的元素指向的是函数而已,但就数组本身而言,二者是完全一样的,N一定,二者大小都一样.
既然是数组,当然也可以定义函数指针数组的指针,与普通数组指针没啥区别:
#define N 10 char* (*pdata) ; char* (*(*pfunc) )(const char *p); // <=> char* (*)(const char *p) (*pfunc) ;
数组
1. 数组名的含义
数组名不能作为左值(放在赋值运算符左边), 作为右值时代表的是数组首元素地址, 但sizeof(数组名)的结果是整个数组的大小,因为此处数组名不是右值;如下例中,a是数组名(代表数组首元素的地址), 而&a的含义是数组的地址,它们在值上是相等的,因为数组首元素相对于整个数组来说,它的偏移是0,所以在内存中数组的地址等于数组首元素的地址;但是它们两个的含义完全不同,通过取值就能看出,*(&a) 是地址,等同与数组名a,而*a是一个值,是首元素的值。
int a[5] = {1, 2, 3, 4, 5}; printf("a = %p\n", (void*)(a)); printf("&a[0] = %p\n", (void*)(&a[0])); printf("&a = %p\n", (void*)&a); printf("*a = %d\n", *a); printf("*&a[0] = %d\n", *&a[0]); // 返回的是地址i,其值与&a[0]、a相同,但含义不同,代表整个数组的首地址 printf("*&a = %p\n", (void*)(*&a)); printf("sizeof(*a) = %lu\n", sizeof(*a)); printf("sizeof(*&a[0]) = %lu\n", sizeof(*&a[0])); // &a代表的是整个数组的首地址,作为右值代表的数组首元素的地址\n printf("sizeof(*&a) = %lu", sizeof(*&a)); *a = 11; printf("a[0] = %d\n", a[0]); // OUT: // a = 0x7ffc336df2a0 // &a[0] = 0x7ffc336df2a0 // &a = 0x7ffc336df2a0 // // *a = 1 // *&a[0] = 1 // *&a = 0x7ffc336df2a0 // // sizeof(*a) = 4 // sizeof(*&a[0]) = 4 // sizeof(*&a) = 20a[0] = 11
2. 数组与指针
它们没有任何关系, 只是有相似的使用方式而已.虽然前面我们说数组名代表首元素的地址,但只是代表而已,实际上并没有a这么一个变量来存储数组首元素的地址。
char a[] = "pointer vs array"; char *p = "pointer vs array"; printf("%lu, %lu\n", sizeof(a), sizeof(p)); printf("%lu\n", sizeof(&a)); printf("%lu, %lu\n", sizeof(*(&a)), sizeof(*p)); // OUT: // 17, 8 // 8 // 17, 8
由上例可以看出,a显然不是指针,因为指针变量的大小是8,而&a才是实实在在的指向数组的指针;
另外*(&a)与*p也是完全不同的,*(&a)表示的是整个数组,而*p只是代表一个字符而已;
只要是存在与内存中的数据,都能通过指针来访问,数组也是存在于内存中的数据,所以它也可以通过指针(利用基址+偏移地址方式)来访问,就跟用指针访问基本数据一样,只是恰好C语言支持指针通过下表来操作,同时数组支持通过偏移的形式来操作,所以看起来指针与数组比较像而已。
char a[] = "pointer vs array"; char *p = "pointer vs array"; char tmp1 = a[0]; // <=> *((char*)&a + 0) char tmp2 = p[0]; // <=> *(p + 0)
-------栈--------|------|-----堆/静态区--------- |-----| | | |-----| | | a | ... | | | |-----| | | |-----| | | |-----| | | | | | p |------|------|---->|-----| | | |-----| | | | ... | | | |-----| | | |-----| | | |-----| -----------------|------|-----------------------
编译器总是把基于下标的操作解析为基于’基址+偏移’的操作方式,汇编就是这么干的。
3. 基址+偏移方式访问数组元素问题
直接看下面的例子即可,ptr3这种情况需要特别注意:int a[5] = {1, 2, 3, 4, 5}; // &a指向数组的指针,+1就是偏移一个数组的大小,现在它指向a[5]下面紧邻的那个 // 5×sizeof(int)的区域(未定义区域)。本来是指向数组的指针,值为数组首元素 // 的地址,强制转换成int*后就是指向一个int值的指针了,此时*ptr1是一个int值 int *ptr1 = (int *)(&a + 1); // a是数组名,指向数组首元素,是一个int*,对它加1,就是向后偏移sizeof(int) // 个字节,指向数组第二个元素,其实这里的强制类型转换是多余的,但写上会使概念更清晰。 int *ptr2 = (int*)((int*)a + 1); // <=> a + 1 // 转换成标量, +4就是+4bytes, 指向第二个元素 int *ptr3 = (int*)((unsigned long)a + 4); // ptr1是一个指向int的指针, 它目前指向数组a最后一个元素后面紧跟的那个地址, // ptr1[-1]就是向前偏移sizeof(int)bytes, 也就是a的最后一个元素. printf(“%x, %x, %x\n", ptr1[-1], *ptr2, *ptr3); // OUT // 5, 2, 2
4. 数组指针与指针数组
int (\*p)[]=> 数组指针,p是指向数组的指针,有一点需要注意,此时*p跟数组名同义,代表数组首元素地址,所以**p就是一个int型变量,大小为4bytes;
int \*p[]=> []小标运算符的优先级高于*, 所以等价于int* (p[]),首先p是一个数组, 数组元素的类型是int*, 即指针数组。
int a[5] = {1, 2, 3, 4, 5}; // 指针数组与数组指针 int (*p)[5] = &a; // p是个指针 // pp是一个数组,数组元素是int型指针 int *pp[5] = {NULL, NULL, NULL, NULL, NULL}; // p是指针,大小是8bytes printf("sizeof(p) = %lu\n", sizeof(p)); // *p是p所指向的数组,它等效于数组名 printf("sizeof(*p) = %lu\n", sizeof(*p)); // **p是p所指向数组的第一个元素4bytes printf("sizeof(**p) = %lu\n", sizeof(**p)); printf("sizeof(pp) = %lu\n", sizeof(pp)); printf("sizeof(*pp) = %lu\n\n", sizeof(*pp)); // 等效于数组名 printf("*p = %p\n", (void *)(*p)); // 数组名代表数组首元素的地址, 这里只是偏移sizeof(int) printf("*p+1 = %p\n\n", (void *)(*p+1)); // OUT: // sizeof(p) = 8 // sizeof(*p) = 20 // sizeof(**p) = 4 // // sizeof(pp) = 40 // sizeof(*pp) = 8 // // *p = 0x7fff90402500 // *p+1 = 0x7fff90402504
5. 数组作为函数参数
一维数组作为函数参数, 编译器会把它处理成指针:void Print(char text[]) { printf("%lu\n", sizeof(text)); // 输出始终为8, 不论传什么进来 }
6. 多维数组
一维数组作为函数参数,编译器会把它处理成指针,但这个过程不是递归的,也就是说只有一维数组才会这样,当数组超过一维时,将第一维改写为指向数组的指针后,后面的维再也不可改写:void func1(char a[][4]) { // code } // 两个函数等价 void func2(char (*p)[4]) { // code }
二维数组做参数第一维的维度可以省略,但第二维的不行,因为它标示了第一维的指针指向数据的类型。
有个小陷阱要注意:
int a[3][2] = {{0, 1}, {2, 3}, {4, 5}}; // 二维数组赋值 int b[3][2] = {(0, 1), (2, 3), (4, 5)}; // 注意里面不是中括号是逗号表达式 <=> {1, 3, 5} int *p1, *p2; p1 = a[0]; p2 = b[0]; printf("%d, %d\n", p1[0], p2[0]); // OUT: // 0, 1
内存管理
linux程序内存模型
每一个进程都有一个大小与物理内存相同的虚拟内存空间,然后具体用时映射到物理内存,因为有虚拟内存空间的存在,所以编译器和连接器可以在编译或链接时直接分配内存地址,它们分配的是虚拟内存地址。用户栈: 局部自动变量,函数栈等,向低地址生长;
运行时堆(动态内存分配区): malloc/new,向高地址生长;
读/写段(静态数据区):
.data:已经初始化的全局自动变量和静态变量(全局的和局部的);
.bss:未初始化的全局自动变量和静态变量(全局的和局部的)。
只读段(代码段):
.text:存放程序代码;
.rodata:常量区,存储字符串常量,const常量(全局的或者静态的,局部的const在栈上)。
所谓的内存管理,其实就是管理“运行时堆”。
来个例子助助兴:
int a = 0; //全局初始化区 char *p1; //全局未初始化区 int main() { int b; //栈 // val 和 cc 这两个的地址邻接,说明局部const位于栈上, // 这也说明,const修饰的是变量,只是只读。 int val = 50; cout << "&val: " << &val << endl; const int cc = 10; cout << "&cc: " << &cc << endl; char s[] = “abc”; //栈 char *p2; //栈 char *p3 = “123456”; //字符串位于常量区,p3位于栈 static int c = 0; //全局(静态)初始化区 p1 = new char[10]; //p1位于栈,p1指向的对象位于堆 return0; }
栈和堆的区别:
管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。
生长方向:
栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
空间大小:
栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
存储内容:
栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。
堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。
分配方式:
栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。[动态分配由alloca函数在栈上申请空间,用完后自动释放](没这么用过,不太清楚)。堆只能动态分配且手工释放。
分配效率:
栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。
分配后系统响应:
只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。
操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。
此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。
碎片问题:
栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。
可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。
函数
递归
不使用库函数编写strlen,但是当str很长时间,这个函数并不实用:size_t myStrlen(const char *str) { if (*str != '\0') { return (myStrlen(++str) + 1); } else { return 0; } }
相关文章推荐
- C语言深度剖析笔记(指针和数组)
- C语言深度剖析之—指针与内存地址(函数指针,普通指针,指针数组,数组的指针,指针的指针)
- C语言深度剖析之—指针与内存地址(函数指针,普通指针,指针数组,数组的指针,指针的指针)
- <<C语言深度剖析>>学习笔记之五:指针与数组
- C语言深度剖析之—指针与内存地址(函数指针,普通指针,指针数组,数组的指针,指针的指针)
- C语言学习笔记之成员数组和指针
- C语言学习笔记.指针4--数组指针和指向数组的指针变量(一)
- 指针,函数,数组打杂混学习以及typedef学习笔记
- 《C语言深度剖析》学习笔记----C语言中的符号
- C++学习笔记2--函数重载 复杂的数据 内存对齐 指针数组 结构与指针 传值传址传引用 联合枚举类型别名
- 【C语言学习笔记】数组、字符串、指针
- c++ primer 第五版学习笔记-第6章-返回数组指针的函数和函数指针的数组
- C语言学习9: malloc动态内存存储,动态内存分配去空格字符增长版,动态内存分配去符号incr增长版,型参和返回值都是int型的函数的指针,main函数的地址也可以用指针指向,typedef定义函数指针,函数定义与嵌套的作用,返回函数指针类型,const作用
- 数组和指针————C语言学习笔记1
- what's in string? c语言string类函数实现汇总 觉得都是学习使用指针的好例子(算是读书摘抄和笔记吧)
- c语言学习,指针函数、函数指针、指针的指针、指向指针数组的指针
- what's in string? c语言string类函数实现汇总 都是学习使用指针的好例子啊(算是读书摘抄和笔记吧)
- 【学习笔记】【C语言】返回指针的函数
- c语言学习,指针函数、函数指针、指针的指针、指向指针数组的指针
- iOS开发学习笔记 2-9 C语言部分 内存分配函数 函数指针 指针函数 void*