C语言的内存

内存属性:(1)内存操作的大小。int/char大小不同。 (2)内存的变化性,可读可写。const表示仅可读。

存储类别:auto,static,register,extern
内存四区:全局区,栈区,堆区,代码区

变量的作用域和生命周期

  • 作用域:变量的作用范围(在何处能够访问到变量)
    • 全局变量:定义在所有函数之外的变量,定义之后,都可以访问,而且数据共享(内存只有一块),全局变量自动初始化为0
      作用域:从定义开始到程序执行结束
    • 局部变量:在函数或者代码块里面定义的变量,从定义开始到函数或者代码块结束。
      局部变量默认就是auto,表示由编译器自动释放内存(函数结束自动释放内存)
  • 生命周期
    • 自动变量:没有任何存储类别修饰的变量,都是自动变量
    • 静态变量:用static修饰的变量,就是静态变量,不会自动释放内存,而是程序结束之后,系统自动回收,自动初始化为0
      加了static,就像吃了长生不老药,生命周期延长了,但是作用域不变
    • register:表示把变量放到寄存器(CPU)里面,注意:这个只是说给编译器建议,但是编译器不一定会采纳
    • extern:表示外部变量,这个一般用在多文件中



参考:
https://www.bilibili.com/video/BV1U24y1k7Gp
https://www.bilibili.com/video/BV1De4y1K7W

指针变量【本质理解,非常重要】

指针,是内存类型资源地址,门牌号的代名词。
指针变量,是一个装门牌号的小盒子。

C语言编译器对指针这个特殊概念,有两个疑问:【重点】

  • (1)分配的小盒子有多大?
    • 1GB(4Gbit)的内存是2^32位,需要一个32位的地址来寻址。所以,32位系统的所有指针都是32位,也就是4B。
      不管指针定义时前面是个char还是int还是结构体,只要是指针变量,这个小盒子就是4B。
  • (2)盒子里存放的地址所指向的内存如何读取,一次读取多长?
    • 这个就要看前面的那个char/int了,如果是char,就是一次读取1B,如果是int就是一次读取4B。
      前面的类型char/int是修饰*p这个整体的,也就是描述地址所指向的内存类型的,指明一次读取多大内存
      • 做个练习,以下宏函数定义怎么理解?
        #define CONTENTS_OF(addr) \
                (*((volatile unsigned int *)(addr)))
        
        理解:
        • volatile unsigned int类型指的是addr指针指向的内存类型,也就是要读的是一个unsigned int类型的数。有点函数返回值的意思。【unsigned int 修饰的是读出的内存数据类型,而不是指针】
        • \表示换行,在宏定义中使用较多,换行是为了方便阅读代码,避免一行过长。
        • 该宏定义了一个操作,当代码中用到CONTENTS_OF(addr)时,含义是,把addr中的内容读取成一个unsigned int类型的数。【因为数据本身没类型,这里强转就是为其赋予了类型,只能用一个unsigned int的变量接收这个数。】

【王海宁老师的指针讲的真的很好,更重要的是,音色好听】:重点看P1就行
https://www.bilibili.com/video/BV1fg4y1o7ja

指针赋值实例

讨论前提,先定义:
int *p;
int a;

输出时,变量名表示其内容
指针变量也是变量,也符合普通变量的内存分配,也分auto/static/register/extern全局/局部

printf("%d\n",a); // a:整数变量名表示其内容,是数值3
printf("%p\n",p); // p: 指针变量名表示其内容,是地址0x0001

改变指向vs改变指向地址的内容
*p表示指向地址的内容,p表示指向的地址
要改变指针的指向,即改变指针指向的地址,就给p赋值一个地址0x0001
要改变指针指向地址的值(指针指向地址不变),就给*p赋值一个数值。

p=a; // 改变指向,a得是一个地址才行,即a和p都是指针变量。
p=&a; // 改变指向,p存a的地址,p指向变量a 【最常用】
*p=a; // 改变指针地址内容,a是一个数值 【常用】
*p=&a; // 改变指针地址内容,p指向的地址存储a的地址,即p是指针的指针

指针存谁的地址,表示,指针指向谁。
如果让指针p指向变量a,则:

p = &a; // 把a的地址存到指针p中去,即,指针p指向变量a

把地址0x7ffeefbff58c赋值给一个指针

int *p = (int *)0x7ffeefbff58c; //这里强转了一下,否则默认就是个int类型

再往这个地址里存储一个变量a,变量a的16进制是0x12

int a = 0x12; //a的值用10进制表示,是16+2=18
*p = a;

指针修饰符

指针修饰符:const,volatile,typedef
【未完,需要再继续看上面的b站视频】

const

常量、只读【直观感受/常用场景是不可变,但在某些黑客代码中实际是可变的】

//指向常量的指针,内存不可改,p可以指向不同常量
//应用场景:p指向字符串常量
const char *p; //cont修饰的是*p即内存,表示内存不能变,*p不能再被赋值。但是,指针指向可以变,p可以被赋值,p=&a可以操作!
char const *p; //同上,只是写法不同。不建议这种写法,推荐上面写法。

//指针常量/常量指针,指向/地址不可改,p指向的内存可以更改
//应用场景:硬件资源定义,例如 LCD的显卡缓存,缓存地址是芯片公司提前指定好了、不可修改,用户使用时只是不断刷新缓存中的内容、写入红绿蓝、LCD就显示不同的颜色。
char * const p; //const修饰的是p,也就是指针的指向不能变,p不能再被赋值,p=&a不能操作。但是,p指向的内存可以变,*p可以被赋值,*p=a可以操作。
char *p const; //同上,只是写法不同。不建议这种写法,推荐上面写法。

//内存和地址/指向 都不能改
//应用场景:ROM
const char * const p; //指向也不能改,内存也不能改,p和*p都不可以被赋值。

例子

char *p = "Hello World\n"; //不合规的写法,""中是字符串常量,前面应该声明const char
*p = 'a'; //会报错,Segmentation fault

char const *p = "Hello World\n"; //标准写法
*p = 'a'; //const就不能修改,若改会报错,直接提示是const问题,error: assignment of read-only location '*p'

//常量无法修改,但是,数组可以修改
buf[] = {"Hello World\n"};
char *p = buf;
*p = 'a';
printf("%s",p); // 此时buf变成 aello World\n

volatile

防止优化指向内存地址
volatile是对指针指向内容的修饰,只和硬件设备访问有关。
场景:硬件修改内存内容时,一直是最新状态。

volatile char *p;
while(*p == 0x10)
//【没听懂,说之前讲过?再找视频吧。。。】
// 5:00,https://www.bilibili.com/video/BV1fg4y1o7ja?p=6&vd_source=e66e14782161c833644b3ac32f1d7532

typedef

指针类型太复杂时,程序员看起来很麻烦,需要用typedef简化、起个别名。

我们定义一个变量时,实际上只需要知道两个信息:类型 变量名
因此在typedef简化时,就是把那一大堆复杂表示的指针取个名称,就是指针类型。【注意,typedef是起别名、不要和#define宏定义混淆】

char *name_t; // name_t是一个指针,指向一个char类型内存
typedef char *name_t; // name_t是一个指针类型的名称,指向了一个char类型的内存

typedef int (*p[10])(int,void (*p)(int))) *name_t; // name_t是一个指针类型的名称
name_t a; //name_t是一个类型,具体啥类型,需要去typedef中找

数组名是指针吗?

数组名在其定义的作用域内:是数组对象

  • 不是指针:其sizeof是数组的字节数,所以不是指针。
  • 神似指针:
    • 形式上,数组名可以作为strcpy(str2, str1);这个函数的参数应该是指针。
    • 原因是:数组名可以自动转换为指向其指代实体的指针,而且是一个指针常量,不能修改;
  • 【还有一种说法,说本来就是指针,只是sizeof自动处理成了数组长度】

数组名作为函数形参时:是指针

  • 作为局部变量,成了指向数组的指针变量、可以修改、而且sizeof是指针大小4Byte(32位系统)。
    • 数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
    • 很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
数组名测试代码
typedef unsigned char INT8U;
INT8U g_ucRxdata[10];
void f(INT8U *arr)
{
    printf("4 %p\n", arr);
    arr[0] = 1;
}
int main(void)
{
    printf("1 %p\n", g_ucRxdata);
    printf("2 %p\n", &g_ucRxdata);
    printf("3 %p\n", &g_ucRxdata[0]);
    printf("%d\n", g_ucRxdata[0]);
    f(g_ucRxdata);
    printf("%d\n", g_ucRxdata[0]);
}
// 输出:
1 0000000000407980
2 0000000000407980
3 0000000000407980
0
4 0000000000407980
1

【其实就可以把数组名当成指针,除了sizeof特殊以外。
这意味着,指针传参时,不需要再取数组地址了&arr,只需要传递数组名arr,函数内更改同样会被改变数组值!
以后可以少写一个&符号了!】

//以下两种写法都对
f(g_ucRxdata); // 针对数组,懒人可以用第一种,省略&
f(&g_ucRxdata);

//也就数组可以省略&,结构体变量就不能自动转为指针、就得老老实实用&取地址
typedef struct
{
	INT32U uiHead; //帧头
	INT8U ucSrc; //信源
	INT8U ucDst; //信宿
	INT32U uiTail; //帧尾
} ST_Msg;
ST_Msg g_stMsg;
void f1(ST_Msg *pstMsg_i){
    xxx;
}
int main(void)
{
    f1(&g_stMsg); //必需带上&
}

参考:
https://www.cnblogs.com/fenghuan/p/4775023.html
https://www.cnblogs.com/zhangzihan/p/6358365.html

posted on 2024-03-29 17:48  西伯尔  阅读(33)  评论(0编辑  收藏  举报