坤之路 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、计算机网络、嵌入式实战问题(基础题+场景题)、基础算法、数据库基础}