您的位置:首页 > 编程语言 > C语言/C++

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语言 指针
相关文章推荐