坤之路 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变量修饰不同变量时,在什么时候进行初始化?

  1. 全局 static 变量:初始化时机:全局 static 变量在程序启动时进行初始化,且只会初始化一次。作用域:它的作用域仅限于定义它的文件,其他文件无法访问。
  2. 函数内 static 变量:初始化时机:函数内的 static 变量在第一次调用该函数时进行初始化,且只会初始化一次。之后的调用将不会再重新初始化。作用域:它的作用域仅限于定义它的函数,但其生命周期与程序相同。
  3. 结构体内的 static 变量:在C语言中,结构体内不能定义 static 变量。static 只能用于全局作用域或函数作用域的变量。

  • 全局 static 变量在程序启动时初始化。
  • 函数内 static 变量在第一次调用时初始化。
  • 结构体内没有 static 变量的定义。

2.内存分布模型

上图是比较经典的内存分布的模型图,下面将对上图中的不同的组成部分进行详细解释(从低地址到高地址)注:必须知道组成结构但是具体的含义只需要理解。

  1. 代码段:存放程序的机器指令(即二进制代码)。通常是只读的,因为程序的指令在执行过程中不应该被修改。
  2. 数据段:存放已初始化的全局变量和静态变量。这些变量在程序开始运行时已经赋予了初始值。
  3. BSS 段:存放未初始化的全局变量和静态变量。它们在程序开始运行时会自动初始化为0或者空指针。
  4. 堆区:动态分配的内存空间,用于存放程序运行时动态申请的内存。(程序员可以通过函数(如malloc、calloc等)或者操作系统提供的接口来申请和释放堆内存,堆从低地址向高地址增长。
  5. 栈区存放函数的局部变量、函数参数值以及函数调用和返回时的相关信息。栈区是按照"先进后出"的原则进行管理,内存的分配和释放是自动进行的,栈从高地址向低地址增长。是一块连续的空间
  6. 共享区:也称为文件映射或共享内存,用于实现不同进程之间的内存共享。

面试实战:

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.结论:

  1. 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 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.具体实现

  1. 当调用 malloc(size) 时,它首先计算需要分配的内存块大小,包括用户请求的大小以及内存管理所需的额外空间(例如内存块的管理信息)。
  2. malloc 会遍历一个数据结构(例如空闲链表或空闲块列表),查找合适大小的空闲内存块。
  3. 如果找到了合适的内存块,malloc 会将其标记为已分配,并返回一个指向该内存块的指针给用户。
  4. 如果没有足够大的空闲内存块可用,malloc 可能需要扩展程序的虚拟内存空间。它通过系统调用(例如 brk 或 mmap)向操作系统请求更多的连续内存空间。
  5. 当操作系统提供了更多的内存空间后,malloc 可以从新的空间中分配出合适大小的内存块,并将其标记为已分配。
  6. 在内存块被释放时,通过调用 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)用来作为程序的入口,argcargv是其参数。

  • 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 关键字的主要用途。

变量声明

告诉编译器变量在其他文件中定义,不分配内存。

extern int global_var;

变量定义

实际分配内存并初始化变量。

int global_var = 10;

函数声明

告诉编译器函数在其他文件中定义,提供函数的签名。

extern void my_function(int);

函数定义

实现函数的具体功能。

void my_function(int x) { ... }

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

全部评论

相关推荐

1 1 评论
分享
牛客网
牛客企业服务