一、用变量a给出下面的定义

此题除了笔试题出现,在面试或双选会中hr也偶尔会让人去手写

一个有十个指针的数组,该指针指向的是一个整形数

一个指向有十个整型数组的指针

一个指向函数的指针,该函数有一个整形参数并返回一个整形数

一个有10个指针的数组,该指针指向一个函数,该函数有一个整形参数并返回一个整型数

int *a[10];
int (*a)[10];
int (*a)(int);
int (*a[10])(int);

二、函数指针一般用在什么地方?

1)需要调用函数但不知道函数名,只能知道函数地址的情况

  • 回调函数:当一个对象需要响应另一个对象的某些事件时,可以使用函数指针作为回调机制的一部分。例如,在图型用户界面中,可能需要将鼠标点击或键盘输入的事件处理程序与某个函数绑定,以便执行特定的操作。

  • 动态库:在编程语言如C/C++中,函数指针也用于访问外部库中的函数。通过将函数地址存储在函数指针中,可以间接地调用这些函数。

2)建立函数指针表(函数指针数组)管理多个函数

  • 多态性:在面向对象编程中,函数指针可以用于实现方法的重载或多态。例如,一个基类可能定义多个同名虚函数,而子类可以通过重写这些函数来实现不同的行为。(虚函数表)

3)函数指针作为参数传递给其他函数

  • signal():该函数用于设置信号处理函数,在触发对应信号的时候,调用指定的函数。
  • pthread_create():该函数用于创建线程,传递一个函数指针用于指定线程处理函数

三、指针常量与常量指针的定义以及区别,分别用在什么地方?

int a = 10;
const int *p = &a;      //常量指针,不能通过指针p去修改变量a的值,但可以改变p的指向
int const *p = &a;      //常量指针,同上
int * const p = &a;     //指针常量,可以通过指针p去修改变量a的值,但不能改变p的指向

常量指针一般用于传递常量数据给函数,以确保数据不被修改。另外调用函数的人员也能明确传入的参数不会被修改。

指针常量可以确保指针不会意外地指向其它非法地址,减少段错误出现概率。比如在C++中的引用的实现原理就是指针常量,另外C++中this指针也是指针常量。

四、strlen与sizeof的区别?

strlen主要用于计算字符串的长度,不包含'\0'。而sizeof主要用于计算类型的大小。如下示例

char buf[1024] = "hello";
char *p = buf;
char buf2[] = "hello";

sizeof(buf);   //1024,buf类型为char[1024],所占空间大小为1024
strlen(buf);   //5, 字符串长度为5

sizeof(p);     //32位系统上为4,64位系统上为8  p的类型为char *是指针类型
strlen(p);     //5,计算的依然是字符串长度

sizeof(buf2);  //6,数组长度会被初始化为6,最后一位用于保存'\0'
strlen(buf2);  //5,字符串长度依然为5

五、define与XX的区别

define是预处理指令,用于定义常量、宏等,是在编译之前进行文本替换。

1)define与const的区别

        C/C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后\者有更多的优点:
        const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会 产生意料不到的错误(边际效应)。
        有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。 

2)define与typedef的区别       

        typedef 可以为数据类型定义新的名称,使代码更易读,减少重复代码,提高代码的可维护性。而如果使用宏替换数据类型,在声明一系列变量时会有问题。例如:

#define FLOAT_POINTER float *
FLOAT_POINTER pa, pb;

预处理器会将该声明转换为

float * pa, pb;  //pa为float的指针类型,但pb是float类型

typedef是在C++11前替换类型别名的唯一选择(C++11后可使用using替换)。不过typedef只能用于替换类型别名

3)宏函数与函数 / 内联函数 的优缺点

宏函数优点是效率高,没有函数调用开销,但缺点是可读性差,容易出错。

函数的优点是可读性好,易于调试与维护,缺点是有函数调用开销。

扩展:函数调用开销在C++中可以用内联函数规避,内联函数是在编译阶段进行函数体替换,但缺点是函数体内必须简洁,不能使用循环以及开关语句。

六、请描述C语言五种内存模型

  • BSS段:用于存放未初始化的全局变量和静态变量,在程序执行前会被清零。

  • 数据段(data):用于存放已初始化的全局变量和静态变量。

  • 只读数据段(rodata):存放一些常量,例如常量字符串

  • 代码段(text):存放程序的机器指令,即可执行的代码。

  • 栈:用于存放函数的局部变量、函数参数、返回地址等,以及函数调用的上下文信息。

  • 堆:用于动态分配内存,由程序员手动管理,通常用于存放动态分配的数据结构和对象

数据段与BSS段可以归为 静态区

只读数据段与代码段可以归为 常量区

一般描述可以直接说:堆区、栈区、静态区、常量区、代码段

七、C语言程序文件的编译过程分为几个步骤?每个步骤都做什么事情?


1. 预处理(Preprocessing):处理以#开头的预处理指令,如#include、#define等,生成经过宏替换的源文件。
2. 编译(Compilation):将预处理后的源文件翻译成汇编代码。
3. 汇编(Assembly):将汇编代码翻译成目标文件(Object file)。
4. 链接(Linking):将目标文件与库文件链接,生成可执行文件。

八、请描述下堆和栈的区别

注意此题有陷阱,在数据结构中与内存中都存在堆与栈的概念。所以需要分开回答。

数据结构中分为顺序栈与链式栈,为顺序存储结构,存储数据要求先进后出,数据结构中主要使用二叉树实现,分为大顶堆与小顶堆。

内存中一般有系统管理,分配方式按照“后进先出”原则进行,另外栈的空间容量较小,访问速度相对较快。内存中的管理由程序员自行管理,使用完毕后必须手动释放,否则会导致内存泄漏,堆的空间容量相对较大,可以动态扩展空间,访问堆需要通过指针间接访问,响应速度相对较慢。

九、const关键字的理解

为最常见的面试问题之一,发挥空间很大,切忌只谈一点点内容,最好回答问题的时候增加使用位置

const关键字用于声明常量,表示该变量的数值在程序执行期间不能被修改。主要用法如下

1. 常量声明:用于声明常量,如const int MAX_SIZE = 100;。参考第五题与define的区别

2. 函数参数:用于声明函数参数为常量,以防止函数修改参数值。

3. 指针:用于声明指针为常量指针或指向常量的指针,以确保指针指向的数据不被修改。参考第三题

4.成员函数(C++):用于声明成员函数为常量成员函数,表示该函数不会修改对象的状态。

使用const能提高函数健壮性与可读性。我们一般在能加const的地方都建议加上const。

十、static关键字的理解

1. 在全局变量前使用static:
        静态全局变量:限制全局变量的作用域,使其只在当前文件内可见,不同文件中的同名静态全局变量不会冲突。

2. 在局部变量前使用static:
        静态局部变量:使局部变量在函数调用结束后仍保持其值,不会被销毁,下次调用函数时仍保持上次的值。

3. 在函数前使用static:
        静态函数:限制函数的作用域,使其只在当前文件内可见,不同文件中的同名静态函数不会冲突。


        总的来说,static关键字可以用于限制变量或函数的作用域,使其在特定范围内可见,同时可以保持静态变量的值不变

十一、内存分配的方式

内存分配方式有三种:

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

十二、volatile关键字的理解

此题目多出现于嵌入式相关岗位或者涉及操作系统开发的公司,应用层开发的岗位考的概率不大

volatile关键字用于告诉编译器该变量是易变的,可能会在未知的时间被改变,编译器不应该对其进行优化,每次访问时都必须从内存中重新读取。volatile关键字一般会用在以下情况:

1. 硬件寄存器访问:用于声明与外部硬件相关的变量,如IO端口状态、定时器计数器等。硬件寄存器的值可能随时被硬件改变,编译器不应假设它的值不变或进行缓存优化

unsigned volatile *const port = (unsigned *)0x12345678;

// 读取硬件端口值
unsigned value = *port;

// 写入硬件端口
*port = 0xABCD;

2. 多线程共享变量:确保一个线程对flag的修改能立即被另一个线程看到。不过要注意,对于多线程同步,通常应该使用专门的同步原语(如互斥锁、原子变量等),volatile不能保证操作的原子性。

volatile bool flag = false;

// 线程1
void thread1() {
    while(!flag) {
        // 等待flag被设置
    }
    // 执行操作
}

// 线程2
void thread2() {
    // 做一些工作
    flag = true;
}

3. 信号处理:信号处理程序可能在任意时刻被调用,修改的变量需要声明为volatile,防止编译器优化导致主程序看不到变量的变化。

volatile sig_atomic_t signal_received = 0;

void signal_handler(int sig) {
    signal_received = 1;
}

int main() {
    signal(SIGINT, signal_handler);
    
    while(!signal_received) {
        // 正常程序执行
    }
    // 处理信号
}

4.嵌入式系统中的循环延时:如果没有volatile,编译器可能会优化掉整个循环,因为它认为这个循环没有实际效果。volatile确保循环确实被执行。

volatile int i;
for(i = 0; i < 1000000; i++); // 延时循环

结语

编写该文章目的主要为想从事相关工作的同学找到一份好的工作,以上题目在面试中经常出现,如果有在外面试的朋友发现有更常见更经典的题目也可以私信告知,后续也会更新到博客当中。

如果有朋友想系统的学习相关知识从事相关的行业,可以私信我,有一些经典的电子档书籍资料和开源网课学习链接

Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐