坤之路 C基础(一)
坤坤防伪标签!!!!!!!!!!
1.Static关键字有什么用?static变量修饰不同变量时,在什么时候进行初始化?
Static的作用(简答)
(1)static关键字改变局部变量的生命周期,保持变量内容的持久。
(2)static修饰的变量对其他文件隐藏,可以避免命名冲突。
(3)static修饰的变量默认初始化为 0(若未显式初始化),适用于所有静态存储期的变量。
详解:Static的作用
常见static的C用法
修饰局部变量(称为静态局部变量)
修饰全局变量(称为静态全局变量)
修饰函数(称为静态函数)
1.static修饰局部变量:
在函数中声明变量时, static 关键字指定变量只初始化一次,并在之后调用该函数时保留其状态。static修饰局部变量时,会改变局部变量的存储位置,从而使得局部变量的生命周期变长。
接下来用们用一段代码来进行解析:
#include <stdio.h>
#include <stdlib.h>
void test()
{
static int z = 0;
z++;
printf("%d ", z);
}
int main()
{
int i = 0;
printf("%d
", i);
while (i < 10)
{
test();
i++;
}
return 0;
}
如果没有static关键字z的生命周期在test 返回时就结束了,输出会是
1 1 1 1 1 1 1 1 1 1
加上static关键字后 生命周期会变,输出变成
1 2 3 4 5 6 7 8 9 10
总结:
(1)static关键字修饰局部变量不改变作用域,但是生命周期变长。
(2)本质上,static关键字修饰局部变量,改变了局部变量的存储位置,因为存储位置的差异,使得执行效果不一样。普通的局部变量放在栈区,这种局部变量进入作用域创建,出作用域释放。局部变量被static修饰后成为静态局部变量,这种变量放在静态区,创建好后,直到程序结束后才释放。
2.static 关键字 没有赋值时,默认赋值为 0
接下来用们用一段代码来进行解析:
int a;
int main()
{
char str[10];
printf("integer: %d; string: (begin)%s(end)
", a, str);
return 0;
}
在这段代码中,我们并没有对全局变量 a 和字符串数组 str 进行赋值,所以在输出时会出现随机值的现象。
接着我们用上 static关键字 来修饰 全局变量 a 和字符串数组 str,那么就会被初始化为0,其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
总结:
static的另一个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘’;太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 ‘’
3.static修饰全局变量和函数(隐藏功能)
针对上面这个概念的理解我们一次来解析以下:
1. 首先说一下全局变量,全局变量的作用域十分的广,只要在一个源文件中定义后,这个程序中的所有源文件、对象以及函数都可以调用,生命周期更是贯穿整个程序。文件中的全局变量想要被另一个文件使用时就需要进行外部声明(以下用extern关键字进行声明)。
-----也即是说全局变量既可以在源文件中使用,也可以在其他文件中使用(只需要使用extern外部链接以下即可)
2. static修饰全局变量和函数时,会改变全局变量和函数的链接属性-------变为只能在内部链接,从而使得全局变量的作用域变小。
static变量修饰不同变量时,在什么时候进行初始化?
- 全局 static 变量:初始化时机:全局 static 变量在程序启动时进行初始化,且只会初始化一次。作用域:它的作用域仅限于定义它的文件,其他文件无法访问。
- 函数内 static 变量:初始化时机:函数内的 static 变量在第一次调用该函数时进行初始化,且只会初始化一次。之后的调用将不会再重新初始化。作用域:它的作用域仅限于定义它的函数,但其生命周期与程序相同。
- 结构体内的 static 变量:在C语言中,结构体内不能定义 static 变量。static 只能用于全局作用域或函数作用域的变量。
- 全局
static变量在程序启动时初始化。 - 函数内
static变量在第一次调用时初始化。 - 结构体内没有
static变量的定义。
2.内存分布模型
上图是比较经典的内存分布的模型图,下面将对上图中的不同的组成部分进行详细解释(从低地址到高地址)注:必须知道组成结构但是具体的含义只需要理解。
- 代码段:存放程序的机器指令(即二进制代码)。通常是只读的,因为程序的指令在执行过程中不应该被修改。
- 数据段:存放已初始化的全局变量和静态变量。这些变量在程序开始运行时已经赋予了初始值。
- BSS 段:存放未初始化的全局变量和静态变量。它们在程序开始运行时会自动初始化为0或者空指针。
- 堆区:动态分配的内存空间,用于存放程序运行时动态申请的内存。(程序员可以通过函数(如malloc、calloc等)或者操作系统提供的接口来申请和释放堆内存,堆从低地址向高地址增长。)
- 栈区:存放函数的局部变量、函数参数值以及函数调用和返回时的相关信息。栈区是按照"先进后出"的原则进行管理,内存的分配和释放是自动进行的,栈从高地址向低地址增长。是一块连续的空间。
- 共享区:也称为文件映射或共享内存,用于实现不同进程之间的内存共享。
面试实战:
1.平时定义变量在那个段,全局变量在那个地方存储?
局部变量:通常在栈(Stack)中分配内存。当函数被调用时,局部变量会在栈上分配空间,函数返回时,这些空间会被自动释放。
全局变量:存储在全局区(Data Segment)中。
- 数据段又分为:初始化数据段:存储已初始化的全局变量。
- 未初始化数据段(BSS段):存储未初始化的全局变量,通常在程序启动时会被初始化为0。
2.static定义的变量在哪里?
静态变量(static):无论是静态局部变量还是静态全局变量,都是存储在静态全局区中。
3.malloc的值在哪?
malloc:用于动态分配内存,分配的内存空间位于堆(Heap)中。
3.全局变量和局部变量的区别
1. 作用域不同
- 全局变量:作用域是整个程序,从定义它的地方开始到程序结束。可以在任何函数中访问,适合需要在多个函数之间共享数据的场景。由于全局变量的可见性,可能会导致命名冲突(解决方法加static)
- 局部变量:作用域仅限于定义它的函数或代码块(如循环、条件语句等)。只能在其作用域内访问,避免了全局变量的命名冲突问题。
2. 内存存储方式不同
- 全局变量:存储在全局数据区(数据段),其内存分配在程序启动时完成。程序运行期间全局变量的值可以被任何函数修改。
- 局部变量:存储在栈区,每次函数调用时在栈上分配空间,函数返回时自动释放。由于栈的特性,局部变量的存储效率较高,
3. 生命周期不同
- 全局变量:生命周期从程序启动到程序结束,内存不会被释放,直到程序终止。
- 局部变量:生命周期仅限于函数调用期间,函数返回后局部变量的内存会被自动释放。
4. 使用方式不同
- 全局变量:在声明后可以在程序的任何部分使用,适合存储需要跨多个函数共享的状态。
- 局部变量:只能在定义它的函数或代码块中使用,增强了函数的封装性。
4.malloc和calloc的区别
1、参数个数上的区别:
malloc函数:malloc(size_t size)函数有一个参数,即要分配的内存空间的大小。
calloc函数:calloc(size_t numElements,size_t sizeOfElement)有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小。
2、初始化内存空间上的区别:
malloc函数:不能初始化所分配的内存空间,在动态分配完内存后,里边数据是随机的垃圾数据。
calloc函数:能初始化所分配的内存空间,在动态分配完内存后,自动初始化该内存空间为零。
malloc与calloc没有本质区别,malloc之后的未初始化内存可以使用memset进行初始化。
主要的不同是malloc不初始化分配的内存,calloc初始化已分配的内存为0。
注:calloc返回的是一个数组,而malloc返回的是一个对象。calloc等于malloc后在memset很可能calloc内部就是一个malloc再来一个memset清0。所以malloc比calloc更高效。
5.malloc的底层原理
1.结论:
- 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
- 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为"文件映射区域"的地方)找一块空间来开辟。
2.函数详解:brk(sbrk)和mmap函数
首先,系统向用户提供申请的内存有brk(sbrk)和mmap函数。下面我们先来了解一下这几个函数。brk() 和 sbrk()
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
Mmap的第一种用法是映射此盘文件到内存中;第二种用法是匿名映射,不映射磁盘文件,而向映射区申请一块内存。
Malloc使用的是mmap的第二种用法(匿名映射)。
Munmap函数用于释放内存。
主分配区和非主分配区
Allocate的内存分配器中,为了解决多线程锁争夺问题,分为主分配区main_area和非主分配区no_main_area。
1. 主分配区和非主分配区形成一个环形链表进行管理。
2. 每一个分配区利用互斥锁使线程对于该分配区的访问互斥。
3. 每个进程只有一个主分配区,也可以允许有多个非主分配区。
4. ptmalloc根据系统对分配区的争用动态增加分配区的大小,分配区的数量一旦增加,则不会减少。
5. 主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块
6. 申请小内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。
2.具体实现
- 当调用 malloc(size) 时,它首先计算需要分配的内存块大小,包括用户请求的大小以及内存管理所需的额外空间(例如内存块的管理信息)。
- malloc 会遍历一个数据结构(例如空闲链表或空闲块列表),查找合适大小的空闲内存块。
- 如果找到了合适的内存块,malloc 会将其标记为已分配,并返回一个指向该内存块的指针给用户。
- 如果没有足够大的空闲内存块可用,malloc 可能需要扩展程序的虚拟内存空间。它通过系统调用(例如 brk 或 mmap)向操作系统请求更多的连续内存空间。
- 当操作系统提供了更多的内存空间后,malloc 可以从新的空间中分配出合适大小的内存块,并将其标记为已分配。
- 在内存块被释放时,通过调用 free 函数,malloc 将其标记为未分配,并将该内存块添加到空闲内存块的列表中,以便后续的内存分配可以重复使用它们。
代码实现
#include <unistd.h> // 包含系统调用相关的头文件
typedef struct Block {
size_t size; // 内存块的大小
struct Block* next; // 指向下一个内存块的指针
} Block;
Block* freeList = NULL; // 空闲链表的头指针
void* malloc(size_t size) {
// 检查参数是否合法
if (size <= 0) {
return NULL;
}
// 计算需要分配的内存大小
size_t blockSize = sizeof(Block) + size;
// 在空闲链表中查找符合要求的内存块
Block* prevBlock = NULL;
Block* currBlock = freeList;
while (currBlock != NULL) {
if (currBlock->size >= blockSize) {
// 找到合适大小的空闲块
if (prevBlock != NULL) {
// 删除这个空闲块
prevBlock->next = currBlock->next;
} else {
// 这个空闲块是链表的头节点
freeList = currBlock->next;
}
// 返回指向内存块的指针
return (void*)(currBlock + 1);
}
prevBlock = currBlock;
currBlock = currBlock->next;
}
// 没有找到可用的内存块,请求更多内存空间
Block* newBlock = sbrk(blockSize);
if (newBlock == (void*)-1) {
return NULL; // 请求失败,返回 NULL
}
// 返回指向新内存块的指针
return (void*)(newBlock + 1);
}
void free(void* ptr) {
// 检查参数是否合法
if (ptr == NULL) {
return;
}
// 获取指向内存块起始位置的指针
Block* block = ((Block*)ptr) - 1;
// 将内存块标记为未分配状态,然后将其添加到空闲链表中
block->next = freeList;
freeList = block;
}
6.数组指针与指针数组的区别
定义:
指针数组:指针这个词是修饰数组的,所以它本质是一个数组,是个数组元素类型为指针的一个数组。
数组指针:数组这个词是修饰指针的,所以它本质是一个指针,是个指向数组的一个指针。
详解:
1. 指针数组 (Array of Pointers)
定义:
int *array[5];
这里 array 是一个指针数组,存储 5 个 int* 类型的指针。
内存分布与存储位置:
- 指针数组本身:array 是一个存储指针的数组,这个数组的每个元素都是指针,它们分别指向不同的内存地址。这些指针的存储位置在内存的栈或全局数据段(取决于它的声明位置)。
- 每个指针指向的地址:这些指针可以指向任意的内存位置,例如堆(动态分配的内存)或栈(局部变量的地址)等。指针数组的元素只保存地址,而不直接存储指向数据。
占用的内存大小:
- 指针数组的每个元素是一个指针,而在大多数系统中,指针的大小是固定的,通常为 4 字节(32 位系统)或 8 字节(64 位系统)。
- 在 64 位系统中,int *array[5]; 占用的总内存大小为:
5 个指针 * 8 字节 = 40 字节
这 40 字节只是用来存储指针的空间,不包括这些指针所指向的数据。
int a = 10, b = 20, c = 30; int *array[3]; // 指针数组 array[0] = &a; // 指向 a 的地址 array[1] = &b; // 指向 b 的地址 array[2] = &c; // 指向 c 的地址
在这个例子中,array[0]、array[1]、array[2] 都是存储地址的指针,每个指针占用 8 字节(在 64 位系统上)。
2. 数组指针 (Pointer to an Array)
定义:
int (*ptr)[5];
这里 ptr 是一个指向包含 5 个 int 元素的数组的指针。
内存分布与存储位置:
- 数组指针本身:ptr 是一个指向数组的指针,它存储的是一个数组的起始地址。这个指针的存储位置与指针数组相似,也可以位于栈或全局数据段(取决于声明的位置)。
- 数组本身:ptr 指向的数组是实际存储数据的区域。数组的存储空间通常分配在栈或堆中(如果是静态数组,通常在栈中;如果是通过动态分配,通常在堆中)。数组的内存是连续的,所有元素在内存中是紧挨着存储的。
占用的内存大小:
数组指针 ptr 本身只占用一个指针大小的内存(4 字节在 32 位系统,8 字节在 64 位系统)。
它所指向的数组的大小取决于数组的长度以及元素的类型。假设指向的是 int 类型的数组:
- 如果 ptr 指向一个大小为 5 的 int 数组,则该数组占用的内存为 5 * sizeof(int) 字节,即 20 字节(在 32 位或 64 位系统上 int 通常是 4 字节)
因此,假设 ptr 指向一个包含 5 个 int 元素的数组,在 64 位系统上总共占用的内存为:
8 字节(指针大小) + 20 字节(数组大小) = 28 字节
int arr[5] = {1, 2, 3, 4, 5};
int (*ptr)[5]; // 数组指针
ptr = &arr; // 指向数组 arr
7.指针函数与函数指针的区别
指针函数:本质是一个函数,此函数返回某一类型的指针。
函数指针:本质是一个指针,指向函数的指针变量,其包含了函数的地址,通过它来调用函数。
1、指针函数
它是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针。
类型标识符 *函数名(参数表)
int *f(x,y);
首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
float *fun( ); float *p; p = fun(a);
示例:int *GetDate( ); int * aaa(int,int); 函数返回的是一个地址值,经常使用在返回数组的某一元素地址上。
#include "stdio.h" //包含输入输出头文件
int * GetDate(int wk,int dy); //声明指针函数GetDate( )
void main(void)
{
int wk,dy;
do
{
printf("Enter week(1-5),day(1-7)
");
scanf("%d,%d",&wk,&dy);
}
while(wk<1||wk>5||dy<1||dy>7);
printf("%d
",*GetDate(wk,dy));
}
int * GetDate(int wk,int dy) //指针函数GetDate( )
{
static int calendar[5][7]=
{
{1,2,3,4,5,6,7},
{8,9,10,11,12,13,14},
{15,16,17,18,19,20,21},
{22,23,24,25,26,27,28},
{29,30,31,-1}
};
return &calendar[wk-1][dy-1];
}
2、函数指针
它是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); f=func;
指向函数的指针包含了函数的地址,可以通过它来调用函数。声明格式:类型说明符 (*函数名)(参数)
其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数。指针的声明和它指向函数的声明保持一致。
指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。
void (*fptr)( );
把函数的地址赋值给函数指针,可以采用下面两种形式:
fptr=&Function; fptr=Function;
示例:
#include "stdio.h" //包含输入输出头文件
void (*funcp)( ); //声明函数指针
void FileFunc( ),EditFunc( ); //声明函数
void main(void)
{
funcp=FileFunc; //FileFunc函数的地址赋给funcp
(*funcp)( );
funcp=EditFunc; //EditFunc函数的地址赋给funcp
(*funcp)( );
}
void FileFunc( ) //函数
{
printf("File
");
}
void EditFunc( ) //函数
{
printf("Edit
");
}
8.数组名与指针的区别
数组名:
- 是一个常量指针,指向数组的首元素。
- 大小固定为整个数组的大小。
- 无法被改变或重新赋值。
- 无法进行指针运算。
指针:
- 是一个变量,存储一个内存地址。
- 大小固定为指针类型的大小。
- 可以指向任意类型的对象。
- 可以被改变或重新赋值。
- 可以进行指针运算,如加法、减法等。
9.int main(int argc, char **argv)函数中,参数argc和argv分别代表什么意思?
在C语言中,主函数int main(int argc, char **argv)用来作为程序的入口,argc和argv是其参数。
argc是整型参数,表示命令行参数的个数。它记录了程序在运行时附带的命令行参数的数量,至少为1,因为程序自身的名称也算一个参数。argv是字符指针数组,用来存储命令行参数的字符串。每个元素指向一个以null结尾的字符串,表示一个命令行参数。argv[0]指向程序的名称,argv[1]指向第一个参数,以此类推,argv[argc-1]指向最后一个参数。
举个例子,假设我们在命令行中执行以下命令:
./program arg1 arg2 arg3
那么argc的值为4,argv的值如下所示:
argv[0] = "./program" argv[1] = "arg1" argv[2] = "arg2" argv[3] = "arg3" argv[4] = NULL
10.extern关键字
1. extern 关键字的基本概念
extern 关键字用于声明一个变量或函数的存在,但不定义它。它告诉编译器该变量或函数的定义在其他地方。这是实现模块化和代码组织的重要手段。以下表格总结了 extern 关键字的主要用途。
变量声明 | 告诉编译器变量在其他文件中定义,不分配内存。 |
|
变量定义 | 实际分配内存并初始化变量。 |
|
函数声明 | 告诉编译器函数在其他文件中定义,提供函数的签名。 |
|
函数定义 | 实现函数的具体功能。 |
|
1.1 变量声明与定义
- 声明:extern 声明一个变量或函数,告诉编译器该变量或函数在其他文件中定义。例如:
extern int global_var; // 声明 global_var 变量
这表明 global_var 变量在其他地方定义,但在当前文件中可以使用它。
1.2 函数声明与定义
- 声明:函数的声明也可以使用 extern 关键字,尽管在C语言中函数默认是 extern 的。例如:
extern void my_function(int); // 声明 my_function 函数
2. extern 的实际应用
2.1 跨文件共享全局变量
假设我们有两个文件:file1.c 和 file2.c,并希望在这两个文件之间共享一个全局变量。
file1.c
#include <stdio.h>
// 声明全局变量
int shared_var = 42; // 定义并初始化全局变量
void display() {
printf("Value of shared_var in file1.c: %d
", shared_var);
}
file2.c
#include <stdio.h>
// 声明全局变量
extern int shared_var; // 声明在其他文件中定义的全局变量
void modify() {
shared_var += 10; // 修改全局变量
printf("Value of shared_var in file2.c: %d
", shared_var);
}
main.c
#include <stdio.h>
// 函数声明
void display();
void modify();
int main() {
display(); // 显示初始值
modify(); // 修改全局变量
display(); // 再次显示修改后的值
return 0;
}
2.2 跨文件共享函数声明
类似于全局变量,函数也可以通过 extern 关键字在文件之间共享。
file1.h
// file1.h #ifndef FILE1_H #define FILE1_H // 函数声明 void greet(); #endif // FILE1_H
file1.c
// file1.c
#include <stdio.h>
#include "file1.h"
// 函数定义
void greet() {
printf("Hello from file1.c!
");
}
file2.c
// file2.c
#include "file1.h"
int main() {
// 调用 file1.c 中的函数
greet();
return 0;
}
11.#include<> 和 #include""的区别
一、使用场景上的区别
1、#include< >一般用于包含系统头文件,诸如stdlib.h、stdio.h、iostream等;
2、#include" "一般用于包含自定义头文件,比如自定义的test.h、driver.h等
二、查找目录的区别
1、#include<>
- 告诉编译器直接在系统头文件目录中搜索包含的头文件,查找失败直接报错
2、#include" "
- 告诉编译器首先在当前目录下搜索包含的头文件
- 如果没有找到,则在项目配置的头文件引用目录中搜索该文件
- 如果项目配置的头文件引用目录中仍然查找失败,再从系统类库目录里查找头文件
三、总结
- 如果是标准库函数的头文件,则使用 < > 包含头文件;
- 如果是自定义的头文件,优先使用 " " 包含头文件。
作者背景:北邮本硕成绩前1%,学习嵌入式两坤年。专栏适合选手:c++/嵌入式软件学习求职的学生或人士。你是坤坤的真爱粉吗。 文章内容{C/C++、操作系统、freertos、计算机网络、嵌入式实战问题(基础题+场景题)、基础算法、数据库基础}
