C语言深度剖析学习笔记-指针、数组、内存、函数

指针与数组

指针

指针就是一个变量, 只不过这个变量的值,是一个内存地址而已;

  • 指针变量 加/减 整数,所表达的含义是:相对当前指针值的偏移,偏移量为: 整数*指针指向的数据类型的大小;
  • 相同类型的指针 相减,所表达的含义是:这两个指针之间的偏移量(元素的个数) <=> ((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[N];
char* (*pfunc[N])(const char *p); // <=> char* (*)(const char *p) pfunc[N];
//                                      |-----------------------|
                                                函数指针类型

上面的代码定义了一个函数指针数组,跟指针数组没什么区别,只不过指针数组里的元素指向的是数据,而函数指针数组里面的元素指向的是函数而已,但就数组本身而言,二者是完全一样的,N一定,二者大小都一样.

既然是数组,当然也可以定义函数指针数组的指针,与普通数组指针没啥区别:

#define N 10
char* (*pdata)[N];
char* (*(*pfunc)[N])(const char *p); // <=> char* (*)(const char *p) (*pfunc)[N];

数组

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;
}
栈和堆的区别:
  1. 管理方式
    栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。

  2. 生长方向
    栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。

  3. 空间大小
    栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。

  4. 存储内容
    栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。

    堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。

  5. 分配方式
    栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。[动态分配由alloca函数在栈上申请空间,用完后自动释放](没这么用过,不太清楚)。堆只能动态分配且手工释放。

  6. 分配效率
    栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。

  7. 分配后系统响应
    只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。

    操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。

    此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

  8. 碎片问题
    栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

函数

递归

不使用库函数编写strlen,但是当str很长时间,这个函数并不实用:

size_t myStrlen(const char *str)
{
    if (*str != '\0') {
        return (myStrlen(++str) + 1);
    } else {
        return 0;
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛应用于计算机科学和软件开发的编程语言。它具有强大的功能和灵活性,适用于开发各种类型的应用程序。 C语言专题精讲篇是一个对C语言进行深入学习和讲解的系列文章或课程。它汇总了C语言相关的重要知识点和技巧,旨在帮助学习者更好地理解和运用C语言。 这个专题中的笔记涵盖了C语言的各个方面,包括基本语法、数据类型、运算符、流程控制、函数数组指针、结构体、文件操作等。通过系统性的学习和总结,这些笔记可以帮助学习者逐步掌握C语言的核心概念和常用技巧。 在这个专题中,学习者可以学到如何编写简单的C程序,如何使用变量和运算符进行计算,如何使用条件和循环语句控制程序流程,如何使用函数进行代码的模块化,如何使用数组指针进行数据的处理,如何使用结构体组织复杂数据,如何进行文件的读写等等。 C语言专题精讲篇的目的是帮助学习者全面、深入地了解C语言的各个方面,并能够独立编写和调试简单到中等难度的C程序。通过反复实践和练习,学习者可以逐渐提高自己的编程能力,并为进一步学习更高级的编程语言打下坚实的基础。 总之,C语言专题精讲篇的笔记汇总是一份重要的学习资料,可以帮助学习者系统地学习和掌握C语言的基础知识和常用技巧,为他们未来的编程之路打下坚实的基石。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值