嵌入式C语言
文章目录
4 内存空间
4 - 1 指针
-
指针
指针是C语言中的一种数据类型,它是用来存储变量的地址的变量。通过指针,可以实现对内存中变量的直接访问和操作。
定义指针变量需要使用指针类型,并使用
*
作为指针的声明符号。例如:cCopy codeint a = 10; // 定义一个整型变量 a,并赋值为 10 int *ptr; // 定义一个指向整型变量的指针 ptr ptr = &a; // 将变量 a 的地址赋值给指针 ptr
在上面的例子中,
ptr
是一个指向整型变量的指针,通过&
运算符获取变量a
的地址,并将其赋值给ptr
。通过指针可以实现对变量的间接访问,例如:
cCopy code int b = *ptr; // 通过指针 ptr 间接访问变量 a 的值,并将其赋值给变量 b
在这个例子中,
*ptr
表示通过指针ptr
访问其指向的内存中的值,即变量a
的值,并将其赋值给变量b
。指针在C语言中有广泛的应用,包括动态内存分配、函数指针、数组和字符串的处理等。但需要注意的是,指针的使用需要谨慎,因为不正确的指针操作可能导致内存错误和程序错误。
-
大小端
中,多字节数据通常被划分为若干字节(例如,32位整数被划分为4字节),并按照一定的字节顺序存储在内存中。
在大小端中,主要有两种存储方式:
- 大端字节序(Big-Endian):在大端字节序中,高字节(Most Significant Byte,简称MSB)存储在低地址,低字节(Least Significant Byte,简称LSB)存储在高地址。
- 小端字节序(Little-Endian):在小端字节序中,低字节(LSB)存储在低地址,高字节(MSB)存储在高地址。
具体来说,对于一个4字节的整数值
0x12345678
,在大端字节序中,它的存储方式如下:makefileCopy code地址: 0x1000 0x1001 0x1002 0x1003 数据: 0x12 0x34 0x56 0x78
而在小端字节序中,它的存储方式如下:
makefileCopy code地址: 0x1000 0x1001 0x1002 0x1003 数据: 0x78 0x56 0x34 0x12
#include <stdio.h> int main() { union { int i; char c; } u; u.i = 0x12345678; if (u.c == 0x78) { printf("小端\n"); } else if (u.c == 0x12) { printf("大端\n"); } return 0; }
-
指针
const
在 C 语言中,
const
关键字可以用来修饰指针,从而限制通过指针修改其指向的内容。以下是const
修饰指针的三种情况:这表示
p
是一个指向常量int
的指针,不可通过p
修改其指向的地址,但可以通过p
读取其指向的地址处的内容。const
修饰变量符:指针指向的内容是常量,不可通过该指针修改其指向的内容。例如:
cCopy codeint a = 10; const int* p = &a; // 指向常量 int 的指针,指向 a
const修饰变量
:表示p
是一个指向整型变量的常量指针,其指向的内容是可修改的,但p
自身的值是常量,不可修改。例如:
cCopy codeint a = 10; int* const p = &a; // 常量指针,指向 a
cons
修饰全部:指针本身是常量,且指向的内容也是常量,不可通过该指针修改其指向的地址和内容。例如:
cCopy codeconst int a = 10; const int* const p = &a; // 指向常量 int 的常量指针,指向 a
需要注意的是,
const
关键字修饰的指针只保证了通过该指针不可修改其指向的内容,但并不保证该指向的内容真的是常量。如果通过其他方式修改了指针指向的内容,那么通过const
修饰的指针可能会访问到修改后的内容。因此,在使用const
修饰指针时,需要注意确保指针指向的内容真的是常量,或者在修改指针指向的内容时谨慎操作。 -
指针±
在 C 语言中,指针可以进行加法和减法运算,这两种运算对指针的值进行调整,使其指向新的内存地址。指针运算的结果是根据指针类型的大小来计算的,例如
int*
指针的加减操作会根据int
类型的大小来进行调整。指针运算的规则如下:
- 指针加法:指针加上一个整数值 n,将指针的值增加 n 乘以指针类型的大小。例如:
cCopy codeint* p = ...; // 指向 int 类型的指针 p = p + 1; // 指针值增加 sizeof(int) 个字节
- 指针减法:指针减去一个整数值 n,将指针的值减少 n 乘以指针类型的大小。例如:
cCopy codeint* p = ...; // 指向 int 类型的指针 p = p - 1; // 指针值减少 sizeof(int) 个字节
- 指针之间的减法:两个指针相减,得到的是两个指针之间的偏移量(以指针类型的大小为单位)。例如:
cCopy codeint* p1 = ...; // 指向 int 类型的指针 int* p2 = ...; // 指向 int 类型的指针 int offset = p2 - p1; // 指针 p1 和 p2 之间的偏移量,以 sizeof(int) 为单位
在进行指针运算时,应谨慎避免越界访问和未定义行为,以防止出现错误和不稳定的程序行为。同时,指针运算也是与平台相关的,不同的编译器和硬件可能对指针运算的行为有所不同,需要根据具体的环境和需求来使用。
-
指针[]
通过指针使用
[]
运算符访问内存空间,可以实现对指针指向的内存空间进行偏移,从而访问不同的内存单元。例如,假设有一个指针
p
指向了某一块内存空间,可以使用p[index]
来访问该内存空间中的某个元素,其中index
表示偏移量。下面是一个示例:
cCopy codeint arr[5] = {1, 2, 3, 4, 5}; int* p = arr; // 指针 p 指向数组首元素 int x = p[2]; // 使用指针 p 访问数组元素 arr[2] printf("%d\n", x); // 输出 3
在上面的示例中,指针
p
指向了数组arr
的首元素,通过p[2]
可以访问数组中的第三个元素(偏移量为 2),得到的值为 3。需要注意的是,数组下标从 0 开始,因此
p[0]
表示数组的第一个元素,p[1]
表示数组的第二个元素,依此类推。同时,使用[]
运算符访问数组元素时,编译器会自动进行指针算术运算,不需要显式地进行指针的加法操作。 -
指针逻辑操作符
指针和逻辑运算符在C语言中的操作主要用于判断指针的空值或非空值,常用的逻辑运算符有
!
(逻辑非)和&&
(逻辑与)。-
逻辑非(
!
):用于判断指针是否为空指针。当指针为NULL时,即指针不指向任何有效的内存地址时,!
运算符会返回1
(真),否则返回0
(假)。cCopy codeint* p = NULL; if (!p) { printf("p is a null pointer.\n"); }
-
逻辑与(
&&
):用于判断多个指针是否都非空指针。当多个指针都非NULL时,&&
运算符会返回1
(真),否则返回0
(假)。cCopy codeint* p1 = NULL; int* p2 = malloc(sizeof(int)); if (p1 && p2) { printf("p1 and p2 are both non-null pointers.\n"); }
需要注意的是,逻辑运算符
!
和&&
的操作数通常为指针或表达式,其结果为真(非零)或假(零),可以用于控制流程的条件判断。在进行指针的逻辑运算时,应注意指针的合法性和有效性,以避免空指针引发的错误。 -
-
多级指针
多级指针是指在一个指针的基础上再次定义指向指针的指针,即指向指针的指针。在C语言中,可以通过使用多级指针来传递指针的指针,实现对指针的间接操作。
例如,定义一个指向整型数据的指针
p
,则p
的类型为int*
,而如果再定义一个指向p
的指针q
,则q
的类型为int**
,即指向指针的指针。类似地,如果再定义一个指向q
的指针r
,则r
的类型为int***
,依此类推,可以定义任意级别的多级指针。以下是一个简单的多级指针的示例:
cCopy code#include <stdio.h> int main() { int a = 10; int* p = &a; // 指向整型数据的指针 int** q = &p; // 指向指针 p 的指针 printf("a = %d\n", a); printf("&a = %p\n", &a); printf("p = %p\n", p); printf("*p = %d\n", *p); printf("&p = %p\n", &p); printf("q = %p\n", q); printf("*q = %p\n", *q); printf("**q = %d\n", **q); return 0; }
输出结果:
cssCopy codea = 10 &a = 0x7ffd27d5c5fc p = 0x7ffd27d5c5fc *p = 10 &p = 0x7ffd27d5c600 q = 0x7ffd27d5c600 *q = 0x7ffd27d5c5fc **q = 10
需要注意的是,多级指针在实际应用中用得相对较少,通常在特定的场景下使用,例如函数传参时需要修改指针的值,或者动态分配多维数组的内存等。使用多级指针时,需要注意指针的合法性、内存管理以及指针间接操作的规则,以避免潜在的错误。
- 例
int main(int argc, char* argv[])
或者int main(int argc, char** argv)
第一个形式不接受任何命令行参数,而第二个形式接受命令行参数,其中
argc
表示命令行参数的数量,argv
是一个指向字符串数组的指针,每个字符串表示一个命令行参数。argv
是一个指向指针数组的指针,其中每个指针指向一个字符串。这是因为命令行参数通常以字符串形式传递给程序,例如执行./program arg1 arg2 arg3
,argv
指向的字符串数组将包含"./program"
、"arg1"
、"arg2"
、"arg3"
这些字符串。所以,
main
函数的正确声明应该是:cCopy code int main(int argc, char* argv[])
或者
cCopy code int main(int argc, char** argv)
注意,参数名
argc
和argv
只是一种约定,可以使用任意合法的标识符来代替。另外,argc
和argv
参数的顺序不可颠倒,argc
必须是第一个参数,argv
必须是第二个参数。
4 - 2 数组
-
定义及初始化
定义一个空间:1.大小2.读取方式
数据类型
数组名[m]
m的作用域是在申请时确定数组名是数组的首地址,也可以视为数组的指针。当数组名出现在表达式中时,它会自动转换为指向数组第一个元素的指针。
数组名是一个常量符号,不能放在=左边
-
数组空间的初始化
数组的初始化是空间的拷贝
在C语言中,数组的空间可以通过以下几种方式进行初始化:
- 静态初始化:在定义数组时,可以使用花括号
{}
来为数组的每个元素指定初始值,例如:
cCopy code int numbers[5] = {1, 2, 3, 4, 5}; // 静态初始化数组
这样数组
numbers
的前5个元素将被初始化为 1, 2, 3, 4, 5。- 动态初始化:在定义数组后,可以使用赋值操作符
=
和数组下标来为数组的元素赋值,例如:
cCopy codeint numbers[5]; // 定义数组 numbers[0] = 1; // 动态初始化数组元素 numbers[1] = 2; numbers[2] = 3; numbers[3] = 4; numbers[4] = 5;
这样数组
numbers
的前5个元素也将被初始化为 1, 2, 3, 4, 5。- 部分初始化:在静态初始化或动态初始化时,可以只对数组的部分元素进行赋值,未赋值的元素将被自动初始化为0,例如:
cCopy code int numbers[5] = {1, 2}; // 部分初始化数组
这样数组
numbers
的前两个元素将被初始化为 1, 2,而后三个元素将被自动初始化为 0。需要注意的是,数组一旦初始化,其大小和元素类型是固定的,不能再改变。初始化数组可以在定义数组时进行静态初始化,或在定义后通过赋值进行动态初始化。
- 静态初始化:在定义数组时,可以使用花括号
-
数组和指针区别
char buf[10] = {"ABC"};
和char *p = "ABC";
分别是字符数组和字符指针的定义和初始化方式。
本质区别:常量拷贝到栈空间/指针指向了常量区
char buf[10] = {"ABC"};
这是一个字符数组的定义和初始化。数组名为
buf
,大小为10,类型为char
。在定义时,使用了初始化列表{"ABC"}
,其中包含了一个字符串字面值"ABC"
。- 编译时:编译器会在编译时为数组
buf
分配10个字节的内存空间。 - 运行时:在运行时,初始化列表中的字符字面值
"ABC"
中的字符 ‘A’, ‘B’, ‘C’ 和结尾的空字符 ‘\0’ 会依次被复制到数组buf
的前4个元素中,而后面的6个元素会被初始化为0。
char *p = "ABC";
这是一个字符指针的定义和初始化。指针名为
p
,类型为char*
,指向一个字符串字面值"ABC"
。- 编译时:编译器会为指针
p
分配一个指针大小的内存空间,用于存储指向字符串字面值的地址。 - 运行时:在运行时,指针
p
会指向字符串字面值"ABC"
的首字符 ‘A’ 的地址。
区别:
- 内存分配方式:字符数组在定义时会分配一块固定大小的内存空间,而字符指针在定义时只会分配一个指针大小的内存空间。
- 内容复制方式:字符数组在定义时可以使用初始化列表直接复制字符串字面值的内容到数组中,而字符指针只会指向字符串字面值的首地址,并不会复制字符串内容。
- 内存访问方式:字符数组的元素在内存中是连续存储的,可以通过数组名和下标访问;而字符指针只是存储了字符串字面值的首地址,访问字符串内容需要通过指针解引用操作。
- 内存修改方式:字符数组是数组类型,其内容可以被修改;而字符指针指向的字符串字面值是常量字符串,其内容是只读的,不能通过指针修改。
-
strcpy
strcpy
是 C 语言中的一个字符串拷贝函数,用于将一个字符串从源地址复制到目标地址。函数原型:
cCopy code char* strcpy(char* destination, const char* source);
函数参数:
destination
:目标字符串的地址,即拷贝后的字符串将会存放的位置。source
:源字符串的地址,即要被复制的字符串的起始地址。
函数返回值:
- 返回目标字符串的地址,即
destination
参数的值。
函数功能:
strcpy
函数会将源字符串(source
参数指向的字符串)中的字符逐个复制到目标字符串(destination
参数指向的字符串)中,直到遇到字符串结尾符'\0'
。复制完成后,目标字符串会包含和源字符串相同的内容。需要注意的是,
strcpy
函数不会检查目标字符串的空间是否足够容纳源字符串,因此在使用strcpy
函数时,需要确保目标字符串有足够的空间来存放源字符串的内容,以避免内存溢出错误。此外,如果源字符串和目标字符串的长度相同,那么目标字符串将不会包含字符串结尾符'\0'
,因此需要在目标字符串末尾手动添加'\0'
,以确保目标字符串正确终止。 -
strncpy
strncpy
是 C 语言中的一个字符串拷贝函数,用于将一个字符串从源地址复制到目标地址,并指定最大拷贝的字符数。函数原型:
cCopy code char* strncpy(char* destination, const char* source, size_t num);
函数参数:
destination
:目标字符串的地址,即拷贝后的字符串将会存放的位置。source
:源字符串的地址,即要被复制的字符串的起始地址。num
:最大拷贝的字符数,包括字符串结尾符'\0'
在内。
函数返回值:
- 返回目标字符串的地址,即
destination
参数的值。
函数功能:
strncpy
函数会将源字符串(source
参数指向的字符串)中的字符逐个复制到目标字符串(destination
参数指向的字符串)中,直到遇到字符串结尾符'\0'
或者复制了num
个字符为止。如果源字符串的长度小于num
,则在目标字符串末尾填充零字节'\0'
,以确保目标字符串正确终止。需要注意的是,
strncpy
函数不会自动在目标字符串末尾添加'\0'
,因此在使用strncpy
函数时,需要手动在目标字符串末尾添加'\0'
,以确保目标字符串正确终止。此外,如果源字符串长度大于num
,则目标字符串将不会以'\0'
结尾,因此需要在目标字符串末尾手动添加'\0'
,以确保目标字符串正确终止。 -
memcpy
memcpy
是 C 语言中的一个内存拷贝函数,用于将一段内存块从源地址复制到目标地址。函数原型:
cCopy code void* memcpy(void* destination, const void* source, size_t num);
函数参数:
destination
:目标内存块的起始地址,即拷贝后的数据将会存放的位置。source
:源内存块的起始地址,即要被复制的数据的起始地址。num
:要复制的字节数。
函数返回值:
- 返回目标内存块的起始地址,即
destination
参数的值。
函数功能:
memcpy
函数会将源内存块(source
参数指向的内存块)中的数据逐字节复制到目标内存块(destination
参数指向的内存块)中,共复制num
字节。这意味着memcpy
函数会将源内存块中的二进制数据按字节精确地复制到目标内存块中,不关心数据类型和内容。因此,在使用memcpy
函数时需要注意目标内存块的大小,以免发生越界访问。需要注意的是,
memcpy
函数没有自动在目标内存块末尾添加任何终止符,因此在拷贝字符串时,需要手动在目标内存块末尾添加字符串结尾符'\0'
,以确保字符串正确终止。 -
指针数组和二维指针
指针数组(Pointer Array)和二维指针(Pointer to Pointer or Double Pointer)在定义、使用和内存结构上有一些区别。
- 定义方式:指针数组是一个数组,其中的每个元素都是指针类型,而二维指针是一个指针,其存储的值是另一个指针的地址。
- 内存结构:指针数组的每个元素都是独立的指针,可以指向不同的内存地址,从而指向不同的对象或者数据。而二维指针是一个指向指针的指针,其存储的值是另一个指针的地址,即指向了一个指针变量的指针。二维指针可以用于表示二维数组的地址或者多级指针的概念。
- 访问方式:指针数组可以通过下标访问数组元素,每个元素都是一个指针,可以用于存储指向相应类型的对象或数据的内存地址。而二维指针需要通过两次解引用(即两次使用"*"操作符)才能访问到最终指向的对象或数据。
- 用途:指针数组通常用于存储一组指针,每个指针可以指向不同的对象或数据,常用于管理一组相关的指针。而二维指针通常用于表示二维数组的地址,或者用于实现多级指针的概念,例如在函数参数传递中传递二维数组的地址。
需要注意的是,指针数组和二维指针在使用时需要注意内存管理和指针的合法性,避免出现悬挂指针、越界访问等问题,以确保程序的正确性和安全性。
-
指针数组和数组指针
int *p[5];
和int (*p)[5];
的区别在于它们分别定义了两种不同类型的指针。int *p[5];
可以看成是int *(p[5]);
编译器先从右边读取p和[]结合变成数组,然后再和*组合成为指针,本质是数组。int *p[5];
定义了一个数组,数组名为p
,包含 5 个元素,每个元素都是int*
类型的指针。可以理解为一个包含 5 个int*
类型指针的数组。可以通过下标访问数组元素,每个数组元素都可以指向一个int
类型的对象或数据。例如:
cCopy codeint a = 1, b = 2, c = 3, d = 4, e = 5; int* arr[5] = {&a, &b, &c, &d, &e}; // 定义了一个 int* 类型的数组,每个元素都是指向 int 类型的指针
int (*p)[5];
定义了一个指针,指针名为p
,指向一个包含 5 个int
类型元素的数组。可以理解为一个指向包含 5 个int
类型元素的数组的指针。需要注意的是,(*p)
是一个数组,因此p
是一个指向数组的指针。例如:
cCopy codeint arr[5] = {1, 2, 3, 4, 5}; int (*ptr)[5] = &arr; // 定义了一个指向包含 5 个 int 类型元素的数组的指针
综上所述,
int *p[5];
定义了一个数组,数组的元素都是int*
类型的指针;而int (*p)[5];
定义了一个指针,指向一个包含 5 个int
类型元素的数组。两者的类型和用途略有不同,需要根据具体的需求来选择使用。
4 - 3 结构体
-
字节对齐
字节对齐(Byte Alignment)是一种内存对齐的方式,用于在计算机中分配和布局内存空间时,保证数据的存储地址是特定字节的倍数,以提高程序的执行效率和内存访问速度。例如,在某些体系结构中,要求整型数据以4字节对齐,即其存储地址必须是4的倍数
例如,假设在某个体系结构中,整型数据的字节对齐要求为4,我们声明了以下的结构体:
cCopy codestruct Example { int a; // 4 bytes char b; // 1 byte short c; // 2 bytes };
根据字节对齐的规则,编译器在分配内存空间时会根据对齐要求进行调整,可能会在结构体的成员之间添加填充字节,以保证结构体的每个成员都满足对齐要求。因此,这个结构体的实际大小可能会大于成员变量的总大小之和。
例如,如果我们在某个平台上使用sizeof运算符来获取Example结构体的大小,结果可能是8字节,而不是7字节(4字节的a + 1字节的b + 2字节的c),因为编译器可能会在成员b和c之间添加3字节的填充字节,以满足4字节对齐要求。
- 结构体成员变量顺序不一致,也会影响存储空间大小
- 结构体成员变量顺序不一致,也会影响存储空间大小
**
4 - 4 内存分布
-
代码段/init/text/
代码段(Code Segment)是存储程序执行代码的区域,通常是只读的。
包含了程序的可执行指令,如系统初始化代码和用户代码的二进制表示形式。代码段在程序执行时被加载到计算机的指令缓存中,并由 CPU 执行。
-
只读数据段(常量)/rodata
只读数据段(Read-Only Data Segment),也称为常量数据段,是一种存储在程序执行期间不可被修改的数据的内存段。这段内存通常用于存储程序中的常量、静态常量和字符串等不可修改的数据。
-
数据段(初始化)/data
数据段(Data Segment)是用于存储初始化的全局变量、静态变量的一个存储区域,可以被程序读取和修改
数据段在可执行文件中包含了程序中已初始化的全局静态变量和静态常量的初始值,它位于静态存储区域,并在程序加载时映射到进程的虚拟地址空间中。与BSS段不同的是,数据段中的变量和常量在编译时已经被初始化,因此在程序加载时不需要进行额外的初始化操作。这些变量在程序的整个生命周期内有效。
-
BSS段(未初始化)/bss
BSS段(Block Started by Symbol)主要用于存储未初始化的全局变量、静态变量。BSS段中的变量在编译时被赋予了默认的零值,因此BSS段中存储的数据是全局变量和静态变量的初始值,这些变量在程序运行时会被系统自动初始化为零或空值。这些变量在程序的整个生命周期内有效。
BSS段中的变量在编译时会被赋予默认值,但在程序运行时并不占用实际的存储空间,而是在程序加载到内存时由系统自动分配和初始化。这种延迟初始化的方式可以有效减小程序的内存占用
以上四个空间段在编译时就确定,生存周期为整个程序,程序结束时释放内存
-
堆空间/RW
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
-
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)
-
malloc返回分配好的空间地址
char *p; if(p ==NULL){ error } p = (char *)malloc(5*sizeof(int));
-
当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
free(p);
堆运行时可以自由分配和释放的空间,生存周期程序员决定
-
-
栈空间/RW
栈又称堆栈,是用户存放程序临时创建的局部变量(初始化/未初始化),也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段/BSS段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。
我们可以把堆栈看成一个寄存、交换临时数据的内存区。它是由操作系统分配的,内存的申请与回收都由OS管理。
栈运行时函数内部自行使用的空间,函数一旦返回就释放,生存周期为函数内
-
段查询
(size 查看该文件text/data/bss段占用空间
strings 查看常量段)部分功能的描述