关键字

部分内容参考
https://www.nowcoder.com/issue/tutorial?zhuanlanId=0rOeJm&uuid=8b0d1707bb0f43e0a6ce382773efc7d9

extern的作用

extern主要用于声明外部实体(变量或函数)
1.当一个全局变量在 A 文件中定义,需要在 B 文件中使用时,B 文件中需用 extern 声明该变量,表明它的定义在其他地方。
2. 声明外部函数(函数默认具有外部链接属性(可以被其他文件调用),因此 extern 修饰函数时可以省略)

static的作用

1.static的第一个作用是隐藏。(static函数,static变量均可)
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
2.static的第二个作用是变量只初始化一次,保持变量内容的持久。
3.static的第三个作用是默认初始化为0
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00。

volatile的作用

volatile 的核心作用是阻止编译器对变量访问的优化,确保每次读写都直接操作内存,适用于变量值可能被程序外部因素(硬件、中断、其他线程)异步修改的场景。
使用场景:硬件寄存器映射、中断服务程序中修改的变量、多线程共享的变量。
注意事项
1.volatile 仅保证每次访问都从内存读写,但不保证原子性(如 i++ 这类操作仍可能被打断)。
2.volatile 不能替代锁(mutex)或其他同步机制,复杂的多线程同步仍需专门的同步工具。
3.不要滥用 volatile:不必要的使用会降低程序效率(避免了编译器优化)。

const的作用

用于定义常量,表示变量的值不可修改。
使用场景:定义常量值、常量指针、指针常量、函数参数中的const、硬件寄存器的只读访问

const 与define区别

define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。

1.编译器处理方式不同
define宏是预编译指令,在预处理阶段展开。
const常量是普通变量的定义,编译运行阶段使用。

2.存储方式不同
define定义的是常量,仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const定义的是常变量,常量会在内存中分配(可以是堆中也可以是栈中),const 可以节省空间,避免不必要的内存分配。

const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。可以说,常变量是有名字的不变量,而常量是没有名字的。有名字就便于在程序中被引用,所以从使用的角度看,除了不能作为数组的长度,用const定义的常变量具有宏的优点,而且使用更方便。所以编程时在使用const和define都可以的情况下尽量使用常变量来取代宏。

3.类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。容易出问题,即“边际问题”或者说是“括号问题”。
const常量有具体的类型,在编译阶段会执行类型检查。

4.const可以调试
const 只读变量是可以进行调试的,define 是不能进行调试的,因为在预编译阶段就已经替换掉了。

typedef与define区别

typedef 是 “类型别名定义工具”,由编译器保证类型一致性;#define 是 “文本替换工具”,功能更杂但缺乏类型检查,需谨慎使用

  1. 本质与处理阶段不同
    typedef:是 C 语言的关键字,由编译器处理,属于语言层面的语法。
    #define:是预处理指令,由预处理器在编译前(预处理阶段)处理,属于文本替换工具。
  2. 功能范围不同
    typedef:仅用于为类型创建别名,不能定义常量、表达式或函数。
    #define:可定义常量、宏函数、类型别名等。
  3. 类型检查不同
    typedef:会进行类型检查,因为它定义的是 “真实的类型别名”,编译器会将其视为原类型的等价物。
    #define:仅做文本替换,不进行类型检查,可能导致隐蔽的错误。
  4. 对指针和复杂类型的处理不同
    typedef:为指针类型定义的别名是 “完整的类型”,声明变量时所有变量都会被视为该指针类型。
    #define:对指针的处理是简单替换,可能导致部分变量类型不符合预期。
typedef int* PINT;  // 定义 PINT 为 int* 的别名
PINT a, b;  // 等价于 int* a, int* b; (a 和 b 都是 int 指针)

#define PINT int*  // 宏替换:PINT 替换为 int*
PINT a, b;  // 替换后为 int* a, b; (a 是 int 指针,b 是 int 变量)
  1. 作用域不同
    typedef:有作用域限制,遵循 C 语言的作用域规则(如在函数内定义的 typedef 仅在该函数内有效)。
    #define:没有作用域限制,从定义处开始到文件结束(或被 #undef 取消)均有效。
  2. typedef 常用于简化结构体 / 枚举的声明,而 #define 无法实现同等效果:

宏定义,输入两个参数并返回较小的一个

#define Min(X, Y) ((X)>(Y)?(Y):(X))

宏定义,不用中间变量,实现两变量的交换

#define swap(x,y) \
	    x=x+y;\
	    y=x-y;\
	    x=x-y    //注意:结尾没有分号

指定位置1与清零

#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
    a |= BIT3;
    a |= 0x1<<3;
}
void clear_bit3(void)
{
    a &= ~BIT3;
    a &= ~(0x1<<3);
}

指定位反转

int mask = (1<<2) | (1<<4) | (1<<5);
a ^= mask;

访问硬件寄存器

#define  rBANKCON0  (*(volatile unsigned long *)0x48000004)  
rBANKCON0 = 0x12;

(1)由于是寄存器地址,所以需要先将其强制类型转换为 ”volatile unsigned long *”。
(2)由于后续需要对寄存器直接赋值,所以需要解引用。

指针

常量指针

指向一个常量的指针,这个指针无法修改所指向的数据,但是可以修改指向

const int* p= &a;

指针常量

指针是一个常量,指针所指向的地址是固定的,但是可以修改地址中的值

int* const pl = &a;

函数指针

指向一个函数的指针

int (*ptr)(int, int)= &add;

指针函数

一个函数的返回值是一个指针的函数

int* a(void)
{
}

指针的大小

指针的大小和编译器的位数有关,在32bit的系统下指针的大小是4个字节,在64bit的系统下指针是8个字节

数组名和指针的区别

1.数组名就是数组首元素的地址,也可以看作一个常量指针,这个指针是不能修改指向的,内存访问为4个字节。
2.使用指针访问数组的时候需要使用到解引用使用*,使用指针访问数组是间接访问,使用数组名访问数组是直接访问。
3.使用sizeof对指针和数组名进行计算的时候是不同的,指针的大小和编译器的仪数有关,使用sizeof计算数组名是整个数组的大小。

指针的指针用途

1.动态分配内存:使用指针的指针可以动态分配内存并将其传递给函数,以便函数可以修改指向该内存的指针。这样可以避免在函数中使用全局变量,提高程序的模块化程度。

#include <stdio.h>
#include <stdlib.h>

void allocate_memory(int** pptr) {
    *pptr = (int*)malloc(sizeof(int));
    **pptr = 10;
}

int main() {
    int* ptr;
    allocate_memory(&ptr);
    printf("%d\n", *ptr);
    free(ptr);
    return 0;
}

2.动态创建多维数组(如二维数组)

int main() {
    int rows = 3, cols = 4;
    int** arr;  // 二级指针:指向 int* 类型的数组(每行的首地址)
    
    // 第一步:为“行指针数组”分配内存(存储每行的地址)
    arr = (int**)malloc(rows * sizeof(int*));
    
    // 第二步:为每行分配内存(每行大小可不同)
    for (int i = 0; i < rows; i++) {
        arr[i] = (int*)malloc(cols * sizeof(int));
        arr[i][0] = i;  // 给每行第一个元素赋值
    }
    
    // 使用二维数组
    printf("%d", arr[2][0]);  // 输出 2
    
    // 释放内存(先释放每行,再释放行指针数组)
    for (int i = 0; i < rows; i++) free(arr[i]);
    free(arr);
    return 0;
}

3.处理指针数组

void print_strings(char** strs, int count) {
    for (int i = 0; i < count; i++) {
        printf("%s\n", strs[i]);  // strs[i] 是第 i 个字符串的地址(char*)
    }
}

int main() {
    char* fruits[] = {"apple", "banana", "cherry"};  // 指针数组(char* 数组)
    print_strings(fruits, 3);  // 传递指针数组名( decays 为 char** 类型)
    return 0;
}

绝对地址或者访问硬件指定内存下数据

1.如何对绝对地址0x100000赋值?

*(unsigned int*)0x100000 = 1234;

2.读硬件寄存器地址数据

1.#define GPA1DAT (*(volatile uint32_t*)0xE0200084) 
将 GPA1DAT 宏定义为地址 0xE0200084 上的内容

2.(volatile uint32_t*)0xE02000840xE0200084强制转换为地址int型指针(相当于*p中的p,指的是地址)

3.(*(volatile uint32_t*)0xE0200084)
这句代码则代表地址为0xE0200084上的存放内容(相当于*p,指的是地址上的内容)

4.#define GPA1DAT (*(volatile unsigned int*)0xE0200084) 
将地址0xE0200084上的内容定义为GPA1DAT,如果操作GPA1DAT = 1;则地址0x40000000上存放的内容就变成了1,也可以读.

3.那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?(万能函数指针)

*((void (*)( ))0x100000)( );   //((void(*)())p)(); //或者*(void(*)())p()

首先要将0x100000强制转换成函数指针,即:
(void (*)())0x100000
    
然后再调用它:
*((void (*)())0x100000();typedef可以看得更直观些:
typedef void(*)() voidFuncPtr;
*((voidFuncPtr)0x100000)();

或者这样写

void (*func_ptr)() = (void (*)(void)) 0x100000;
func_ptr();

内存

在这里插入图片描述

#include <stdio.h>

const int    g_A       = 10;         //常量区 
int          g_B       = 20;         //数据段  
static int   g_C       = 30;         //数据段  
static int   g_D;                    //BSS段  
int          g_E;                    //BSS段  
char        *p1;                     //BSS段  

int main()
{
    int           local_A;            //栈  
    int           local_B;            //栈  
    static int    local_C = 0;        //BSS段(初值为0 等于没初始化,会放在BSS段 )  
    static int    local_D;            //BSS段  
      
    char        *p3 = "123456";     //123456在代码段,p3在栈上  
  
    p1 = (char *)malloc( 10 );      //堆,分配得来得10字节的区域在堆区  
    char *p2 = (char *)malloc( 20 ); //堆上再分配,向上生长
    strcpy( p1, "123456" );//123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块  

堆和栈的区别

1.创建方式不同,栈是系统自动创建(栈主要用于保存局部变量),当函数执行完成,栈被销毁
堆是程序员手动进行创建和释放的,malloc进行创建,free进行释放
2.空间大小的区别,栈的空间是比较小的,堆的空间是比较大
3.访问速度,栈的访问速度是比堆要快的
4.栈当使用完成后就会自动被销毁,堆是要靠程序员自己进行手动销毁

内存分配方式

1.静态存储器分配,例如全局变量,静态变量
2.栈上分配(函数中定义出的局部变量)
3.堆上分配(malloc, new)

在局部数组中定义一个大数组可以吗?很大的数组,比如2048

不可以,会爆栈,栈溢出
解决方法,(解决局部大数组爆栈和局部作用域的问题)
1.定义成全局数组
2.加static放在静态存储区
3.数组用malloc申请空间放在堆区
​ 定义一个指针指向这个数组,栈中只占用一个指针的大小

char *fun()
{
     char *a = (char*)malloc(sizeof(char)*100);
     a = "hello,world";
     return a;
}

内存泄露

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
常见的内存泄露方式
1.未配对、2.丢失地址、3.未分级释放

char *pt = (char *)malloc(10); //堆上申请空间,未配对free(pt)
char *pt= (char *)malloc(10); 	
pt= (char *)malloc(20); 	//覆盖了指针,导致前面10个空间的地址丢失。
#include <stdio.h>
#include <stdlib.h>
struct birth
{
   int year;
   int month;
   int day;
};

struct student
{
   char is_male;
   char *name;
   struct birth * bi;
};

int main()
{
   struct student *pt;
   pt= (struct student *)malloc(sizeof(struct student)); //堆上申请空间

   pt->is_male =1;
   pt->name ="wangwei";
   pt->bi = (struct birth *)malloc(sizeof(struct birth)); //堆上申请空间
   pt->bi->year =2000;
   pt->bi->month =3;  
   pt->bi->day =2;  

   printf("%s %d \n",pt->name,pt->bi->day);

   //逐级释放空间,避免内存泄漏
   //pt->name 是字符串常量 不用释放 
   if(pt->bi!=NULL){ 
      free(pt->bi); //先释放子空间
      pt->bi=NULL;
   }
   free(pt); //后释放父空间
   pt =NULL; //避免野指针 (操作已释放的空间)

   return 0;
}

避免内存泄露的几种方式
显式释放内存:程序在使用动态分配的内存时,应该及时使用free函数将不再需要的内存释放掉。
避免重复分配内存:程序在使用动态分配的内存时,应该避免重复分配内存,特别是在循环中。如果需要多次分配内存,可以使用realloc函数重新调整内存块的大小,以减少内存碎片的产生。
使用智能指针:智能指针是一种自动管理内存的工具,可以避免手动释放内存的繁琐操作。智能指针会在对象不再被使用时自动释放内存,并且可以避免内存泄漏和悬空指针等问题。
计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露。

字节对齐

字节对齐是指数据在内存中的起始地址必须是某个值的倍数(通常是2、4、8等)。
为什么需要字节对齐?
1.性能原因
CPU访问对齐的数据通常只需要一次内存访问操作
非对齐的数据访问可能需要多次内存访问,然后进行拼接操作
非对齐访问可能导致性能下降
2.硬件限制
某些处理器架构(如ARM、RISC-V)要求特定类型的数据必须对齐访问,
非对齐访问在某些平台上会导致硬件异常或错误
3.缓存效率
对齐的数据可以更好地利用CPU缓存行(cache line)
非对齐的数据可能跨越多个缓存行,降低缓存效率

结构体对齐规则
1.结构体的对齐要求是其所有成员中最大对齐要求的倍数
2.结构体的大小必须是其对齐要求的整数倍
3.成员按照声明顺序存储,但编译器可能会插入填充字节以满足对齐要求

其他

struct结构体 和 union联合体的区别

union 联合体:成员共享一块地址。共用体大小= 成员中占内存最大的成员的大小。
struct 结构体:不同的成员放在不同的地址中。结构体大小 = 所有成员大小之和(字节对齐)。
enum 枚举 :定义一组有名字的整数常量

编译过程

在这里插入图片描述

定义一个指针数组,指针指向一个函数,返回值为int,函数形参也为int

//1.指针数组
int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    // 定义数组指针,指向包含4个int元素的一维数组,即指向二维数组的一行
    int (*ptr)[4] = arr; 

    // 输出二维数组第一行第一个元素的值
    printf("二维数组第一行第一个元素的值:%d\n", (*ptr)[0]); 


//2.指针数组(函数)
返回类型 (*指针变量名)[数组长度](函数参数列表);
int (*funcArrayPtr[3])(int, int) = {add, subtract, multiply};
for (int i = 0; i < 3; i++) {
        // 通过函数数组指针调用不同的函数
        int result = (*funcArrayPtr[i])(num1, num2); 
}

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐