零基础学习专栏-C语言篇

C语言面试高频一

1.全局变量与局部变量的区别?

从四个角度分析,分别是:生命周期,声明是否需要主动赋值,存储区间。

全局变量

生命周期长,在程序执行期间都有效,在其他的函数当中也可以访问该变量;

声明时自动初始化,不需要额外主动赋值;

存储区间在全局数据区;

局部变量

类比说明,其他的都是反过来,存储区间在栈区。

另 程序的内存分配为:栈、堆、全、常、代(战队全长待有3米)

2.int main(int argc ,char ** argv)函数作为C语言程序的入口,两个参数的含义?

int argc指的是命名行参数的个数,大于等于1,因为包含程序名称在内。

char ** argv是字符指针数组,存储命令行参数的字符串,字符传结尾处为NULL。

3.static的使用

修饰局部变量:多次调用一个函数中使用该变量,不会重置该变量了。

修饰函数:限制使用范围,该变量只能在该文件中调用的函数,不能跨文件使用。无法使用类似“find the definition of .. ”找到该函数。

修饰全局变量:使用该变量,无法使用exert引用该变量到其他的文件。去掉static的全局变量才可以。

修饰成员变量:使其属于类本身而不是对象,多个对象共享同一份内存。

修饰静态限定符:控制变量的初始化和生命周期。

4.const的用法是?

值不能改变,一旦数据赋值就无法在进行改变。使用场景:修饰常量,修饰函数参数,修饰函数的返回值,都是属于第一种值不能改变。

作用域限制:在哪里声明,作用域就在哪里,暂时将作用域等同于两个{}之间的程序。

编译时确定,面试记住即可。

5.const和#define的区别

从代码处理阶段,是否有类型检查,作用域限制解释。

const在编译阶段处理,#define在预处理阶段处理;

const定义常量类型,需要类型检查(变量的数据类型)。#define只是简单的文本替换;

const作用域在声明的位置有效,#define定义的常量没有作用域限制;

6.extern关键字

可以声明一个变量,这个变量是在其他的文件,一般使用.h文件跨文件调用。

告诉编译在链接过程中找到对应的定义。

7.#include<>与#include""的区别是?

#include <>用于查找系统预定义的标准库文件

#include ""用于查找当前源文件的目录下的头文件,用于查找对应绝对地址或相对地址对应目录的头文件

8.volatile声明的作用

volatile声明的变量,编译器不会优化掉的。

三个场景:

多线程中共享变量

并行设备的硬件寄存器

中断程序中访问的非自动变量(time)

9.C语言的基本类型占用字节数量?(以32为系统为例)

int /long 占用4个字节

char* /int */任何的指针 占用4个字节

float 占用4个字节

double 占用8个字节

10.strcpy与memcpy的区别

“strcpy”用于字符串的拷贝,遇到“\0”就结束拷贝,并且目标文件长度不知,有可能拷贝过程中可能出现缓冲区溢出。

“memcpy”是字节级别的拷贝,不只有字符串,数组与结构体也可以拷贝,当前源文件与目标文件的内存区域不能重叠,这里指定拷贝的长度,可以拷贝一部分的数据。

另 

11.一个变量可以同时被const与volatile修饰吗?

可以的。加上const就表示该变量不能在该程序中更改了。但是可以通过外部程序更改。比如一个硬件寄存器,如温度传感器读取数据,可以被硬件自动更新该数据。

12.sizeof与strlen的区别?

sizeof:查看数据类型、变量的字节数量,参数可以是数据类型、变量、数组名,包含“\0”在内。

strlen:查看字符串的字符数量,不包含“\0”在内。

13.数组名与指针的区别

数组名:常量指针,指向的是数组的首元素,无法进行指针运算,大小固定为整个数组的大小。

指针正好:与上面正好相反,大小固定指针类型的大小。

14.变量的定义

int * a[10]与int (*a)[10]:前者a是数组,存储10个指向int类型的指针;后者a是一个指针,指向的是y一个数组,一个10个元素int类型的数组。

int (*a)(int)与int (*a[10])(int):前者a是指针,指向一个函数指针,一个参数为int类型,返回值为int类型的函数指针;后者a是一个数组,存储着10个函数指针,每个指针都是只想一个参数为int类型返回值为int类型的函数指针。

1.函数指针数组存储的对象与应用
#include <stdio.h>

// 定义两个简单的函数
int add_one(int x) {
    return x + 1;
}

int multiply_by_two(int x) {
    return x * 2;
}

int main() {
    // 定义一个函数指针数组,大小为 2
    int (*a[2])(int);

    // 将函数指针赋值给数组元素
    a[0] = add_one;
    a[1] = multiply_by_two;

    // 调用并打印每个函数的结果
    int value = 5;

    // 调用第一个函数(加 1)
    int result1 = a[0](value);
    printf("Result of add_one: %d\n", result1);

    // 调用第二个函数(乘以 2)
    int result2 = a[1](value);
    printf("Result of multiply_by_two: %d\n", result2);

    return 0;
}
2.使用函数指针数组作为另一个函数的参数,直接把这个数组存储的东西等价于函数名

#include <stdio.h>

// 定义一些简单的函数,它们符合函数指针数组中的签名
int add_one(int x) {
    return x + 1;
}

int square(int x) {
    return x * x;
}

int multiply_by_two(int x) {
    return x * 2;
}

// 函数,接受一个函数指针数组作为参数,并调用数组中的函数
void execute_functions(int (*funcs[10])(int), int value) {
    for (int i = 0; i < 10; i++) {
        if (funcs[i] != NULL) {
            printf("Result of function %d: %d\n", i, funcs[i](value));
        }
    }
}

int main() {
    // 定义一个函数指针数组并初始化
    int (*a[10])(int);

    // 将函数指针赋值给数组元素
    a[0] = add_one;
    a[1] = square;
    a[2] = multiply_by_two;

    // 剩下的指针可以初始化为 NULL
    for (int i = 3; i < 10; i++) {
        a[i] = NULL;
    }

    // 调用 execute_functions 函数,执行数组中的函数
    int value = 5;
    execute_functions(a, value);

    return 0;
}

C语言面试高频二

1.结构体与共用体的区别

每个成员是否是独立存储,占用内存大小,同时被访问的成员数量,适合存储的数据。

结构体:每个成员独立存储,占用内存大小为成员占用内存之和,每个成员都可以同时被访问,适合存储不同数据类型的数据变量;
共用体:与上面正好相反,每个成员都是共享一个存储空间,占用内存大小是成员中内存最大的,同时只能访问一个成员,适合使用只存储一种数据类型,节省内存空间。

2.几种传值方式的区别?

值传递:函数不会影响原始数据,会在函数内部创建参数的副本。
引用传递:函数直接影响原始数据,引用就等于别名,就是加了一个&,这个别名只能在初始化中确定,确定之后就不能更改,并且这个别名只能是初始化时候的别名,不能为其他变量别名了(=这里的初始化后就不能改了,改的是绑定的变量,不是数值大小哟)。
指针传递:使用指针间接影响原始数据,指针就等于地址,我们通过解引用更改数据,也可随便改写指针变量,注意空指针的使用与解引用的使用。

3.数组指针与指针数组之间区别?

数组指针:指向数组的指针;下面示例:使用指针的解引用、引用的使用与指针的指向,读取数组数据。
指针数组:存储指针的数组;下面示例:如上;

#include <stdio.h>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int (*ptr)[5];  // 声明一个指向包含5个整数的数组的指针
  ptr = &arr;  // 数组指针指向数组
  printf("数组元素通过指针访问: ");
  for (int i = 0; i < 5; i++) {
    printf("%d ", (*ptr)[i]); // 使用指针访问数组元素需要解引用
  }
  return 0;
}
数组元素通过指针访问: 1 2 3 4 5


#include <stdio.h>
int main() {
  int num1 = 1, num2 = 2, num3 = 3, num4 = 4, num5 = 5;
  int* arr[5];  // 声明一个包含5个整型指针的指针数组
  arr[0] = &num1;
  arr[1] = &num2;
  arr[2] = &num3;
  arr[3] = &num4;
  arr[4] = &num5;
  printf("指针数组元素的值: ");
  for (int i = 0; i < 5; i++) {
    printf("%d ", *arr[i]); // 解引用指针数组的元素以获取其值
  }
  return 0;
}
指针数组元素的值: 1 2 3 4 5

4.指针函数与函数指针

指针函数:返回值为指针的函数;

函数指针:一种新的指着表达方式,如上面int *a(int),一种参数为int 类型返回值类型为int的函数指针;

#include <stdio.h>
int addIntegers(int a, int b) {
  return a + b;
}
int main() {
  int (*ptr)(int, int);  // 声明一个指向以两个整数为参数并返回整数的函数的指针变量
  ptr = addIntegers;  // 将函数地址赋值给函数指针
  int sum = ptr(5, 3);  // 通过函数指针调用函数
  printf("和: %d\n", sum);
  return 0;
}
输出:和: 8

5.原码、反码与补码的定义

原码:第一个表示数据标志位,表示正负值,‘0’为正值,‘1’为负值,后面所有二进制位表达改数据的绝对值;

反码:正数与原码一样,负值全部位取反;

补码:正数与原码一样,负数是反码加1;

6.内存分布模型

内核空间:表示操作系统核心的部分,负责管理设备的所有硬件资源(CPU,硬盘),还有对应的基础服务程序。

命令行参数与环境变量不用解释;

栈区:这个是存储局部变量,函数参数,函数返回值以及函数调用。这个数据数据后进先出的,比如函数迭代运行,里面的函数先执行结束,知道执行到最外面的函数。

共享区:也称为文件映射或共享内存,这里用于malloc动态分配过大时,可能会在共享区找一块空间,实现不同进程之间的内存共享。
堆区:一些使用动态存储的内存空间,如malloc,realloc,calloc等。

全局区:包含未初始化的全局区bass与已初始化的全局区data。全局变量初始化的状态可以判断存储区间,如果没有没初始化或初始化为0,通常会被分配到BSS段。如果全局变量初始化为非零值,直接存储带数据段。只要程序运行,数据段中数据就会被赋予初值,bass段中数据不会被初始化或者初始化为0.

7.malloc与calloc的区别

是否初始化,分配失败后,函数的返回值是什么或者说分配至失败是否排除异常

malloc分配内存不初始化,其中的内存中数据不确定的,分配失败,返回一个空指针;

calloc分配内存直接初始化为0,分配失败,抛出一个异常。

8.malloc分配的原理?

  1. 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟

实现过程:调用malloc(size)时,首先计算需要分配的内存大小,主要是内村管理信息(结构体)与用户请求大小(size),具体是遍历结构体,找到对应的空闲内存块,找不到就通过系统调用向操作系统请求更多的内存,将分配的内存标记为已分配。使用free,将内存释放,并标记为未分配,将空闲内存块添加到空闲列表中。

另:1GB的内存计算机课是否可以malloc(1.2G)呢?答:是有可能的,malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。根据 malloc 函数的作用和原理,应用程序通过 malloc 函数在虚拟地址空间中申请内存,并且与物理内存没有直接的关系。

9.如何在指定的地址赋值为指定的数据?

在 C 语言中,要在地址为 0x3355 的内存位置赋值为 79,可以使用指针来实现。

unsigned char* p = (unsigned char*)0x3355;  // 将地址转换为无符号字符型指针类型
*p = 79;  // 赋值操作

10.两种字节序(大端与小段)

大部分的计算机都是小端字节序,如X86架构的计算机;

小端:对于多字节数据存储要求,高位字节数据存储在高存储位置。存储数据从低位到高位;

大端:如上反回来就是。

11.数组的存储位置

栈区:存储局部变量,函数参数与函数返回值都可以是数组,这个部分函数执行完成内存自动回收;
堆区:存储全局变量,动态分配的内存空间,需要手动分配与释放;

12.引用与指针的区别

本质,是否是实体,是否需要解引用,是否可以为空,是否可以有const,sizeof 的使用有什么不一样,两者的自增不同

引用:本质是别名,没有解引用,不可以为空,不使用const,就是int & const a不存在,不过常引用存在,const int &a = 1;sizeof(引用)表示数据占用内存大小;

指针:本质是存储地址的变量,需要解引用 ,可以为空指针,可以使用const ,int * const a;表示指针常量,表示存储的地址不变,解引用只能更改固定内存地址存储的数据,sizeof(指针)表示指针的占用内存大小,一般为4;

13.内存泄漏如何检测与避免?

内存泄漏:程序在运行过程中,分配的内存没有被及时释放,导致该内存无法在被程序调用。

常见内存泄露情况:

  1. new和malloc申请资源使用后,没有用delete和free释放;
  2. 子类继承父类时,父类析构函数不是虚函数。
  3. 比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
  4. shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。

内存泄漏分析工具:

使用工具如Valgrind(Linux),Dr. Memory(Windows),Instruments(macOS)等来检测内存泄漏。

避免内存泄漏:

  1. 务必正确配对分配和释放:确保每次内存分配都有相应的释放,释放内存的操作必须与分配内存的操作对应(malloc和free 一定要malloc和free的次数一致否则就泄露)。
  2. 减少全局变量和长时间存活的对象:全局变量和长时间存活的对象可能导致无法释放的内存,尽量避免过多使用。
  3. 规范化资源管理:对于文件、网络连接等资源,确保在使用后及时释放,避免产生不必要的资源泄漏。
  4. 使用智能指针:C++中的智能指针(如std::shared_ptr、std::unique_ptr)可以自动管理内存释放,避免手动释放内存的疏忽。
  5. 使用容器类的自动销毁机制:使用容器类如std::vector、std::map等,它们在销毁时会自动释放内部元素的内存。
  6. 注意循环引用:避免出现对象之间的循环引用,可以使用弱引用或断开引用关系的方式解决。

14.函数参数压栈顺序

函数参数压栈顺序是指在函数调用过程中,函数的参数是如何在栈上被压入内存的顺序。

在C/C++中,有

两种常见的调用方式:__stdcall和__cdecl。下面是对它们的简要说明:

  1. __stdcall调用方式:在__stdcall调用方式下,函数的参数是从右往左依次压入栈中。被调函数负责清理栈上的参数。__stdcall调用方式通常被用于Win32 API,因为它具有固定的参数顺序,方便调用方和被调用方之间的交互。
  2. __cdecl调用方式:在__cdecl调用方式下,函数的参数是从右往左依次压入栈中。调用方负责清理栈上的参数。__cdecl调用方式是C/C++默认的函数调用方式。

使用__stdcall调用方式的主要原因是为了确保与其他代码库或操作系统的交互的正确性和一致性,两种不同的调用方式的区别就是为什么应对不同库的一致性和兼容性。

C语言面试高频三

1.栈与堆的区别

从空间分配不同,缓存方式不同,生长方向不同,生命周期不同,空间大小不同,是否能产生内存碎片角度分析。

栈的内存空间:分配数据为函数的调用与返回值,局部变量,缓存方式为一级缓存,是直接存储在处理器核心中,处理速度快,生长方向为从上到下,向地址较小的内存分配,生命周期较短,函数调用结束就会回收栈空间数据,大小一般不超过3M,不会产生内存碎片。

堆的内存空间:分配数据是动态管理的函数调用的内存,缓存方式为二级缓存,处理速度较慢,生长方向与栈相反,生命周期较长,需要用户手动管理该内存,可以在程序的任意位置进行访问,大小为接近3G(对于32位的系统而言),有可能产生内存碎片。

2.在函数中申请堆空间(malloc)内存需要注意什么?

函数返回值要求:不能返回栈内存的指针(如char str[100] ,函数中栈空间局部数组,其中str就是栈内存指针),因为函数执行完成,该部分的内存也会消失,应该使用char *str = (char *)malloc(100 * sizeof(char));这样执行函数完成,该内存的数据也不会消失。不能返回常量区的数据指针,如const char * g ="HELLO",返回的数据也不能修改,没有意义。
函数内部要求:为了防止内存碎片产生,需要匹配好动态分配内存与释放操作。
函数参数要求:若要修改指针变量,需要使用二级指针,使用一级指针可以修改指针指向的变量,无法修改指针变量,原因是函数传递本质还是值传递,需要使用二级指针,就可以修改指针数据了。

3.内存碎片的产生与解决办法?

内存碎片就是:在内存管理中有一些零星、不连续的、无法有效使用的内存空间。

内部碎片:由于内存对齐原则与固定大小内存分配的方式,造成部分内存未被利用起来。

外部碎片:由于动态内存分配,造成连续可有效使用内存空间太小。

解决办法包括:段页式管理与内存池管理,前者是使用虚拟内存管理技术,先将内存分配为页与段,然后在对应的页与段管理内存。后者是直接分配一个大的内存快,分配为内存池,好像一个第三方机构一样,内存池完成数据的分配与释放。

4.内存池是什么?

就是为了解决内存碎片而存在。如果内存池大小不够,需要从操作系统中申请更大的内存池。

5.指针使用要注意的地方?

定义指针,最好初始化为NULL,防止未初始化带来的问题。

使用动态内存分配时,记得要判空,防止内存分配错误,确保申请与释放是配对的,防止存在内促碎片,释放内存之后,记得指针置为NULL,防止存在野指针(不置为NULL,再进行解引用可能修改的上次分配的地址)。

当使用指针指向数组或者动态分配的内存时,一定要记得为其赋初值

尽量避免存在多重解引用,代码可读性变差。

注意指针操作的少1错误。如遍历字符串的时候,一定要遍历到末尾的'\0'。

野指针危险的原因是它可能指向内存中的任何地方,这可能是:

  • 未分配的内存:使用未初始化的指针访问未分配的内存通常会导致程序崩溃。
  • 其他变量的内存:野指针可能意外地指向其他有效的变量内存,导致数据被意外修改,可能会引发程序中的错误。
  • 系统内存区域:如果指针指向了操作系统管理的内存区域,访问它可能导致严重的程序崩溃或系统异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值