C语言
1.请描述下堆和栈的区别
栈(stack):由操作系统自动开辟空间,函数执行完毕后,空间会被系统自动回收。申请空间效率较快,但不够灵活。栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOS下站的大小是2M,如果申请的空间超过栈的剩余空间时,将提示overflow,因此,能从栈获得的空间较小。
堆(heap):由程序员手动开辟空间,使用以后需要主动释放空间,否则可能会造成地址泄露。申请空间的速度比较慢,容易产生内存碎片,但是用起来方便。堆是向高地址拓展的数据结构且是不连续的内存区域。这是由于系统是用链表来存储空闲的内存地址的,所以是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中的有效的虚拟内存,所以堆获得的空间比较灵活,也比较大。
2.请描述c语言五种内存模型
c语言中内存可以分为五个部分:
a.BSS段(Block Started by Symbol):用于存放程序中未初始化的全局变量的内存区域。
b.数据段(data segment):用于存放程序中已初始化的全局变量的内存区域
c.代码段(text segment):用来存放程序执行代码的内存区域
d.堆(heap):用来存放进程运行中被动态分配的内存段,他的大小并不固定,可动态扩张或者缩减。当进程调用malloc分配内存时(需判断分配是否成功),新分配的内存就被动态添加到堆上,当进程调用free释放内存的时候(释放后要把指针的值设为NULL),会从堆中删除。
int *data = (int *)malloc();
if(data == 0)
{
return false;
}
free(data);
data = NULL;
e.栈(stack):存放程序中的局部变量(但不包括static声明的变量,static变量存放在数据段中)同时,函数调用时,栈是用来传递参数和返回值。由于栈先进先出的特点,所以特别方便用来保存和恢复调用现场。
3.编写一个程序,判断当前是大端字节还是小段字节序
#include <stdio.h>
int main()
{
int a = 1;
char *p = (char *)&a;
if(*p == 1)
{
printf("小端\n");
}
else{
printf("大端\n");
}
return 0;
}
大小端的储存方式:
(1) 小端模式下:数值的低位字节储存在内存的低地址中,高位字节储存在高位地处
(2) 大端模式下:数值的高位字节储存在内存的低地址处,低位字节储存在高地址处
4.编写类似strcpy函数的实现,函数原型: char *strcpy(char *strDest, const char *strSrc);
#include <stdio.h>
#include <assert.h>
char *strcpy(char *strDest, const char *strSrc)
{
char* dest = strDest;
assert(dest != NULL && s != NULL);//声明指针不为空
while((*dest++ = *s++) != '\0'){
//循环复制直到遇到源字符串的结束符'\0'
}
return strDest;
}
int main(){
char dest[100];
const char*src = "camellia";
my_strcpy(dest,src);
printf("%s\n",dest);
return 0;
}
5.编写类似atoi函数的实现,函数原型:int atoi(char *str);
#include <stdio.h>
#include <string.h>
int atoi(char *str)
{
int n = 0;
while(*str)
{
n = n*10+(*str - '0');
str++;
}
return n;
}
int main()
{
char a[100] = '';
printf("输入数字");
scanf("%s",a);
int num = atoi(a);
printf("%d\n",num);
return 0;
}
6.指针数组和数组指针的写法
数组指针(也成为行指针)
int(*p)[a] 定义了一个指向含有a个元素的的一维数组的指针【优先级:()>[]>*】
其中()优先级高,说明p是指针,指向一整个的一维数组。当p+1的时候,p指针可以跨过a个整形数据的长度;
int(*p)[a];
int x[n];
p = x;
指针数组
int *p[a];在此定义中[]>*,所以p是数组,是一个由a个指针类型的元素组成的指针数组,也可以说一个数组里面包含的元素都是指针类型的时候,它就被称为数组。当p+1的时候,p指向下一个数组元素
7.函数指针和指针函数的写法,函数指针一般用在什么地方?
C语言指针进阶(一)——深入详解“函数指针”与“指针函数”-CSDN博客
函数指针使用地方:
1. 回调函数:函数指针可以用作回调函数的机制,允许在程序运行时动态地指定要调用的函数。通过将函数指针作为参数传递给其他函数,可以实现在特定事件或条件发生时调用不同的函数。
2.函数选择和调度:通过使用函数指针,可以根据特定条件或运行时的情况选择要调用的函数。这种动态选择函数的能力可以提高程序的灵活性和可扩展性。
3.函数指针数组和函数表:函数指针可以放置在数组或表中,以实现根据索引或其他标识符来调用不同的函数。这在编写处理多个相关函数的代码时非常有用,可以通过索引或其他标识直接访问所需的函数。
8.指针常量与常量指针的定义以及区别,分别用在什么地方?
int * const p = &a;//指针常量
指针常量是现有int *指针,再有的const 修饰p,所以p的值(指向的内存地址)是不可以i需改的,但是指针指向内存的值是可以修改的
举例:
int a = 100;
int *const p = &a;//定义指针常来那个,指向int a的地址
*p = 20;//正确,指向的内存地址中的数据可以修改
p = &b;//错误,指向内存地址不可以修改
const int *p = &a;//常量指针
常量指针首先是要常量,就是指向的值要是指向一个常量,但是指向的内存地址不做显示。指针的指向可以修改,但是指针指向的值不可以修改。
int a = 10;
int b = 20;
const int *P = &a;//定义常量指针,指向int a的地址
*p = 20;//错误,指向的内存地址中的数据不可以修改
p = &b;//正确,指向的内存地址可以修改
9.strlen与sizeof的区别?
1. strlen 是函数,sizeof 是运算符。
2. strlen 测量的是字符的实际长度,以'\0' 结束(不包含'\0' )。而sizeof 测量的是字符的分配大小,如果未分配大小,则遇到'\0' 结束(包含'\0' ,也就是strlen测量的长度加1),如果已经分配内存大小,返回的就是分配的内存大小。
3.strlen的结果要在运行的时候才能计算出来,是用来计算字符串的长度,不是类型占内存的大小。而大部分编译程序在编译的时候就把sizeof计算过了是类型或是变量的长度。
4.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。
10.define与typedef的区别
两个的大体功能都是使用的时候给对象取一个别名,增强程序的可读性。
区别:1.定义不一样define定义后面不用加分号,并且它的别名在对象的前面typedef需要加分号,并且它的别后面替换对象的前面
2.原理不一样#define是预处理中的宏定义命令,在预处理时进行简单的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错, typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。typedef主要用来定义数组、指针、结构体等类型 ,不仅使程序书写简单,也使意义明确,增强可读性。
3.作用域不同#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,因此使用#define之后为了防止错误,要将其解除掉。但是typedef有自己的作用域。
11. C语言程序文件的编译过程分为几个步骤?每个步骤都做什么事情?
1. 预处理
编译过程的第一步就是预处理,与处理结束后会产生一个后缀位(.i)的临时文件,这一步由预处理器完成。预处理器主要完成以下任务:
- 删除所有的注释
- 宏扩展
- 文件包含
2. 编译
C中的编译阶段使用内置编译器软件将(.i)临时文件转换为具有汇编级指令(低级代码)的汇编文件(.s)。为了提高程序性能,程序的性能,编译器将中间文件转换为程序集文件。
3.汇编
使用汇编程序将程序集代码(.s文件)转换为机器可理解的代码(二进制/十六进制形式)。
4.链接
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
12. const关键字的含义以及常见用法
const含义:只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着“只读”(readonly)。
const作用:
1:可以用来修饰变量,修饰函数参数,修饰函数返回值,且被const修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性
2:使编译器保护那些不希望被修改的参数,防止无意代码的修改
3:增强代码的可读性
const修饰全局变量
const修饰普通局部变量
const修饰指针变量的类型(即常量指针)
const修饰指针变量(即指针常量)
const既修饰指针变量类型又修饰指针(即常量指针常量)
数据结构
数据结构常见笔试题【备战春招秋招】-CSDN博客
1.请描述数组和链表的区别以及各自的优缺点
数组和链表在内存储存上的表现不同。
(1) 数组的元素个数是固定的,而组成链表的节点个数可按需增减,
(2)数组元素的存储单元再数组定义时分配,链表节点的存储单元程序执行时动态向系统申
(3)数组中的元素顺序关系由元素再数组中的位置(即下标)确定,链表中的结点顺序关系由结点所包含的指针来体现
(4)对于不是固定长度的列表,用可能最大长度的数组来表述,会浪费许多内存空间
(5)对于元素的插入、删除操作非常频繁的列表处理场合,用数组表示列表也是不合时宜的。若用链表实现,会使程序结构清晰,处理的方法也较为简便
数组的优点:a.随机访问性强 数组的缺点:a.插入和删除效率低可能浪费内存空间
b.查找速度快 b.内存空间要求高需要足够的连续的内存空间
c.数组大小固定,不能拓展
链表的优点:a.插入删除速度快 链表的缺点:不能随机查找,必须从第一个开始遍历
b.内存利用率高不会浪费内存 查找效率低
c.大小没有固定, 拓展很灵活
IO进程线程
如何理解文件IO,与标准IO有什么区别?
文件I/O:文件I/O(低级I/O)是Linux系统调用,通过系统调用来访问文件,返回一个文件描述符,可以直接对文件进行读写。需要进行内核与用户之间的切换。
标准I/O:标准I/O(高级I/O)是c语言的标准库,返回一个指向文件结构的指针,建立缓冲区存储文件的数据,结构中记录着文件的信息,不在内核上操作,不需要频繁切换。
区别 :
1.使用标准不一样,文件IO遵循POSIX标准,智能遵循POSIX标准的类UNIX环境下使用。标准IO遵循 ANSI标准,只要重新编译就可以在不同的环境下运行。
2.文件IO属于系统文件职能在linux下进行系统调用 ,可移植性差,标准IO属于C库(C语言环境),可以在不同的操作系统下移植使用
3.文件 IO使用文件描述符(fd:起的编号),标准IO使用文件流指针(结构体:包括文件的各种数据)
4.文件IO不带缓冲,执行效率低;标准IO带缓冲,执行效率高(缓冲:理解为一块内存,标准IO可以写完数据后找到合适时机一起放入磁盘,而文件IO是每写一个数据都会放一次磁盘,分为全缓冲(大小4096,向文件写入时):写满,程序结束,主动刷新(fflush),执行.行缓冲(1024,向终端输出时用):写满,结束,主动刷新(fflush),换行符,执行。无缓冲:每次执行。程序正常结束也会刷新缓冲区)
5.文件IO属于系统调用,可以访问不同类型的文件,如普通文件,设备文件(open时不能create),管道文件,套接字文件等;标准IO属于C库只能访问普通文件。
简述标准IO中的缓存机制以及刷新条件和所用位置
在标准IO中使用文件时,会对每个文件在内存中申请两段内存空间,用于缓存要进行读,写的数据,实现多次读写文件内容,只需要内核操作一次文件就可以,提高效率
缓存区用于减少应用程序访问内核资源次数,只有当缓冲区满或者刷新的时候,才将数据进行读写到文件或者程序的内存空间中。
标准缓存IO提供三种缓存:
全缓存:当缓冲区被填满或出现特定的条件时,才会刷新缓冲区
行缓存:当输入输出遇到新行符('\n'),就会刷新缓冲区
无缓存:不进行缓存直接刷新
通过 setbuf() 、 setvbuf()函数 可以设置打开文件的缓冲类型以及缓冲大小
#include <stdio.h>
int fflush(FILE *stream); //刷新缓冲区
三个特殊打开的文件:终端文件
当执行程序时,系统默认为程序打开终端文件,
标准输入文件: stdin (读取终端) ---- 行缓存
标准输出文件: stdout (写入终端) ----- 行缓存
标准错误输出文件: stderr (写入终端) ---- 无缓存
简述五种IO模型,并说明是同步操作还是异步操作
参考文章:五种IO模型:操作系统五种IO模型大全-CSDN博客
阻塞IO模型:当应用A发起读取数据申请时,在内核数据没有准备好之前,应用A会一直处于等待数据状态,直到内核把数据准备好了交给应用A结束。
非阻塞IO模型: 当应用A发起读取数据申请时,如果内核数据没有准备好会即刻应用A(返回错误码等),不会让A在这里等待。一旦内核中的数据准备好了,并且又再次收到了A的请求,那么它马上就将数据拷贝到了用户线程,然后返回
复用IO模型:系统提供了一种函数(select/poll/epoll)可以同时监控多个fd的操作,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,如果select监听的fd都没有可读数据,select调用进程会被阻塞;而只要有任何一个fd准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应的线程此时再发起请求去读取内核中准备好的数据;
信号驱动IO模型(异步非阻塞IO):当进程发起一个IO操作,系统调用sigaction执行一个信号处理函数,该函数向内核注册一个信号处理函数(回调函数),然后进程返回,并且不阻塞当前进程;当内核数据准备好时,内核使用信号(SIGIO)通知应用线程调用recvfrom来读取数据(运行回调函数)。
异步IO模型:在异步IO模型中,应用只需要向内核发送一个请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间(而信号驱动是告诉应用程序何时可以开始拷贝数据),异步IO模型真正的做到了完完全全的非阻塞;
线程同步与互斥的具体实现方法
多线程编程--5种方法实现线程同步_多线程同步数据库-CSDN博客
线程同步方法:
1:用Interlocked系列函数实现线程同步;
2:用CRITICAL_SECTION及其系列函数实现线程同步;
3:用RTL_SRWLOCK及其系列函数实现线程同步;
4:用事件内核对象实现线程同步;
5:用信号量内核对象实现线程同步;
互斥具体实现:进程互斥的实现方式_实现进程互斥可采用的方法-CSDN博客
死锁是如何产生的?怎么避免死锁的产生
死锁产生原因:
1.加锁后忘记解锁
2.重复加锁,造成死锁
3.B锁内部调用A,A运行时又加锁,导致A,B都无法运行
避免死锁产生:
1.多检查
2.使用trylock 替换
3. 通过互斥锁实现线程同步 --避免死锁
简述线程池的概念与作用
概念:在应用程序启动时创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,从线程池中获取一个空闲的线程,将任务分配给该线程执行。当任务执行完毕后,线程将返回到线程池,可以被其他任务复用。
作用:为了避免频繁地创建和销毁线程的开销,以及控制并发执行的线程数量,从而提高系统的性能和资源利用率
进程与线程的区别,以及实际什么场景运用它们
进程:是资源(cpu,内存等)分配的基本单位,他是程序执行时的一个实例,程序运行时候系统就会为其创建一个进程然后给他分配资源,然后把该进程放到进程就绪队列,进程调度其中选中它的时候为他分配cpu时间,程序才开始真正运行
线程:是程序执行的最小单位,它是一个进程的执行流,是cpu调度和分派的基本单位一个进程可以是由多个线程组成,线程间共享进程所有资源,每个线程都有自己的堆栈和局部变量。线程由cpu独立调度执行,在多cpu环境下就允许多个线程同时运行,同样多线程也可以实现并发操作,每个请求分配一个线程来处理
进程和线程的区别:
1.调度 :在引入线程的操作系统中,线程是调度和分配的基本单位 ,进程是资源拥有的基本单位 。把传统进程的两个属性分开,线程便能轻装运行,从而可 显著地提高系统的并发程度 。在同一进程中,线程的切换不会引起进程的切换;在由一个进程中的线程切换到另一个进程中的线程时,才会引起进程的切换。
2.并发性 :在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,因而使操作系统具有更好的并发性,从而能 更有效地使用系统资源和提高系统吞吐量。
3.拥有资源 :不论是传统的操作系统,还是设有线程的操作系统,进程都是拥有资源的一个独立单位,它可以拥有自己的资源。一般地说,线程自己不拥有系统资源(只有一些必不可少的资源,但它可以访问其隶属进程的资源。
4.系统开销:由于在创建或撤消进程时,系统都要为之分配或回收资源,因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。进程切换的开销也远大于线程切换的开销。
5.通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性,因此共享简单。但是线程的数据同步要比进程略复杂。
进程线程的使用场景:
1.需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
2.线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3.因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
fork函数如何使用?为何fork的返回值如此设计?
fork函数: fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程代码执行到fork()的时候,fork就会复制一份原来的进程就是创建一个新进程,我们成为子进程,而原来的进程我们称为父进程,父子进程是共存的,他们一起向下执行代码。
注意:调用fork函数后,一定是两个进程同时执行fork函数之后的代码,而之前的代码由父进程执行完成
返回值:
在父进程中,fork返回新创建子进程的ID;
在子进程中,fork返回0
如果出现错误,fork返回一个负值
getppid():得到一个进程的父进程的PID
getpid():得到当前进程的PID
注意:在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
简述写时拷贝技术 与 计数技术
https://www.cnblogs.com/renzhuang/articles/6092847.html
进程间通信有哪些?各有哪些特点?
简述三种IO多路复用方法以及各自的特点
实现io多路复用的方式,以及他们各自的特点_poll为什么不需要重新构造文件描述符表-CSDN博客
1:通过三种方式实现select ,poll,epoll
select 实现io多路复用的特点:
(1):一个进程最多只能监听1024个文件描述符
(2):select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
(3):select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低
poll实现io多路复用的特点
(1):优化文件描述符个数的限制
(2):poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
(3):poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
epoll实现io多路复用的特点
(1);监听的最大的文件描述符没有个数限制
(2):不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
(3):epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高.
为何进程需要有僵尸态?描述几种僵尸子进程回收的方法。
【Linux】浅聊僵尸态进程的产生原因与危害_linux僵尸进程产生的原因-CSDN博客
- 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信 号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处 理是忽略,如果想响应这个消息,可以设置一个处理函数。
SIGCHLD信号:子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为 僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自 动由init进程来接管)。
- kill -18 PPID (PPID是其父进程)
这个信号是告诉父进程,该子进程已经死亡了,请收回分配给他的资源。
SIGCONT也是一个有意思的信号。如前所述,当进程停止的时候,这个信号用来告诉进程恢复运行。该信号的有趣的地方在于:它不能被忽略或阻塞,但可以被捕获。缺省行为是丢弃该信号。
3. 终止父进程
如果方法2不能终止,可采用终止其父进程的方法(如果其父进程不需要的话)父进程死后,僵尸进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。
先看其父进程又无其他子进程,如果有,可能需要先kill其他子进程,也就是兄弟进程。方法是:
kill –15 PID1 PID2 (PID1,PID2是僵尸进程的父进程的其它子进程)。
然后再kill父进程:kill –15 PPID
这样僵尸进程就可能被完全杀掉了。
守护进程的功能是什么?如何创建守护进程?
什么是虚拟地址?有什么作用?
虚拟地址实际上是操作系统为应用程序提供的一个统一的内存访问接口,这样做的好处是——所有的应用程序只需要面向虚拟地址进行编写,而不用考虑实际的物理地址的使用情况。
虚拟地址的作用:
1.方便编译器和操作系统安排程序的地址分布。
程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
2.方便进程之间隔离
不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
3. 一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行,程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
4.虚拟内存管理最 主要的作用是让每个进程有独立的地址空间 (进程间的安全),每个进程都认为自己独占整个虚拟地址空间,这样链接器和加载器的实现会比较容易,不必考虑各进程的地址范围是否冲突
5. 读写内存的安全性
执行错误指令或恶意代码的破坏能力受到了限制,顶多使当前进程因段错误终止,而不会影响整个系统的稳定性。
线程间有哪些资源是共享的?哪些资源是独有的?
共享的资源有:
- 堆。 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)。
- 全局变量 。它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的
- 静态变量。 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的。
- 文件等公用资源。 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
独享的资源有:
- 栈 。栈是独享的。
- 寄存器 。 这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC。
线程的属性有哪些?他们有什么区别?如何设置线程的属性?
【Linux】线程属性的定义&如何修改线程属性(附图解与代码实现)_pthread_create线程属性-CSDN博客
说出三种进程的状态,并描述如何进入该状态
C++
C++常见面试题【备战春招秋招】_c++应届生面试题-CSDN博客
C和C++有什么区别?C语言如何实现面向对象编程?
【1】C语言采用的是一种有序的编程方法,属于结构化编程
优点: 将一个大型编程项目分解成为多个小型的模块,调动所有小型模块完成整个运行,侧重实现的过程性的编程思想,即C语言是一门面向过程的语言,更注重程序实现逻辑、怎么更好、更快、更直接的完成某功能
缺点:编写大型项目时,不利于程序的复用性和拓展性,导致后期维护的时候带来很多麻烦。
【2】C++ 是一门面向对象编程的语言,把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为,更注重的是程序的整体设计。
方便程序后期维护、优化和管理,让一个功能尽可能的通用。
面向对象编程只有一个价值:应对需求的变化,本意是要处理大型复杂系统的设计和实现。
c语言实现面向对象:
#include <stdio.h>
#include <stdlib.h>
// 假设的类类型
typedef struct MyClass MyClass;
// 类的定义
struct MyClass {
int value;
void (*set_value)(MyClass *self, int new_value);
int (*get_value)(MyClass *self);
};
// 设置值的函数实现
void set_value(MyClass *self, int new_value) {
self->value = new_value;
}
// 获取值的函数实现
int get_value(MyClass *self) {
return self->value;
}
// 创建类的实例
MyClass* MyClass_create(int value) {
MyClass *instance = malloc(sizeof(MyClass));
if (instance == NULL) {
return NULL;
}
instance->value = value;
instance->set_value = set_value;
instance->get_value = get_value;
return instance;
}
// 使用类的实例
int main() {
MyClass *obj = MyClass_create(10);
if (obj == NULL) {
return 1;
}
printf("Initial value: %d\n", obj->get_value(obj));
obj->set_value(obj, 20);
printf("New value: %d\n", obj->get_value(obj));
free(obj);
return 0;
}
这个例子中,我们定义了一个名为MyClass
的结构体,它有一个value
成员和两个函数指针,分别指向设置和获取value
的函数。我们还实现了这些函数,并提供了一个创建MyClass
实例的函数MyClass_create
。在main
函数中,我们创建了一个MyClass
实例,调用了它的方法,并在最后释放了分配的内存。这个例子展示了如何在C语言中实现基本的面向对象编程。
new/delete与malloc/free的区别
1.属性
new、delete是c++中的操作符需要编译器支持,而malloc和free是标准库函数需要加入头文件stdlib.h
2.参数
malloc需要自己去指定动态分配内存的大小,而new在指定指针类型后可以自动分配内存,不需要指定大小
3.返回值类型
malloc返回的指针需要进行强制类型转换,new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换
4.分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
5.自定义类型(指针)
malloc是无法完成动态对象要求的,一般创建对象的时候需要调用构造函数,当对象消亡的时候自动调用析构函数。
new在为对象申请分配内存空间时,可以自动调用,delete可以自动调用析构
malloc只做一件事,就是为变量分配内存,free也只是为了释放变量的内存
内联函数与宏函数的区别
主要区别在于处理方式、类型安全、参数处理。调试能力以及使用场景
处理方式:内联函数实在编译时展开,被直接嵌入到目标代码中,而宏函数是在预编译时进行简单的文本替换。
类型安全:内类函数可以进行类型安全检查和语法检查,而宏函数不具备这些功能,只能进行简单的文本替换,这可能导致类型不匹配和语法错误。
参数处理:内联函数在处理参数的时候会进行参数传递和局部变量储存,而宏函数在替换时候会直接使用参数,不经过机选,这可能导致预期之外的结果
调试能力:内类函数可以像普通函数一样进行调试,而宏函数由于是进行文本替换,调试起来较为困难
使用场景:内联函数适用于调用频繁的函数,可以减少函数调用的开销,提高程序的执行效率,而宏函数适用于简单的代码替换,如常量定义和简单的函数调用等。
总结来说,内联函数提供了一种更安全、可调试的替代方案来减少函数调用的开销,而宏函数则因其简单性在某些场景下仍有所应用。
const关键字的作用
const关键字在c++中主要用于定义常量,以增加程序的安全性和可靠性,它有以下常见的几种用途:
定义常来那个变量:使用const声明的变量是只读的,一旦赋值以后不能再进行修改。
const int a = 100 //表示a的值不能被重新赋予其他的数值
修饰指针:const可以用来修饰指针本身或者指针指向的数据,或者二者都修饰
const char *str //表示str指向的内容不可以修改,但是str可以指向不同的内存地址
char *const str //表示str指向的地址不能修改,但是可以通过该指针修改所指向内容的值
const char *const str //表示str指向的地址及其所指向的内容均不可被修改
修改函数参数:在函数生命中,使用const修饰形参可以表明该参数是输入参数,在函数内部不会改变其值。
修改类的成员函数:在类定义中,如果成员函数被声明为const,表示这个函数不会修改类的任何成员的变量
优化编译器:
在嵌入式系统或裸机开发中,编译器可以利用const关键字知道某些变量是常量,进行如常量传播、常量折叠等优化,提高代码效率和性能。
- 节省内存。将一些不需要修改的数据声明为const可以将其存储在ROM或Flash等只读存储器中,节省RAM空间。6
- 增加代码的可读性和可维护性。使用const关键字可以明确标识出哪些变量是常量,防止在代码中不小心修改常量的错误。
static关键字的作用
(c):
static关键字修饰局部变量(c)
概念: static修饰局部变量就使之成为静态局部变量。
作用域: 静态局部变量的作用域并未发生变化,在其所在的局部范围,也就是其所定义的代码块内部。
生存期: 静态局部变量实际上是特殊的全局变量,它们位于相同的内存区域,内存分为栈区,堆区,静态存储区。静态局部变量和全局变量都位于静态存储区,因此静态局部变量的生存期与全局变量一样是全局的,随程序启动而生,随程序结束而消亡。
特点: 静态局部变量的初始化只会在第一次进入这个函数时进行初始化,当离开函数的时候,静态局部变量会继续存在并保持其值,以后进入函数时会保持上次离开时的值, 如果在多次调用一个函数的时候,需要这个函数的局部变量的值具有继承性,则需要这样做。
-
void func() { static int count = 0; // 局部静态变量,只会初始化一次 count++; cout << "Count: " << count << endl; }
2.static关键字修饰全局变量
因为全局变量具有外部连接属性,外部任意一个源文件想要使用其他源文件中的全局变量,只需要先使用extern关键字进行声明,然后就可以使用,也就是说某个源文件的全局变量可以在整个项目中被任意的一个源文件使用,所以全局变量的作用域是整个工程。当一个全局变量被static所修饰的时候,它就称为了静态全局变量,静态全局变量具有内部连接属性,使得这个静态全局变量只能在自己所在的编译单元中被使用,而不能被其它编译单元所使用,否则会出现链接性错误。 为了防止自己的全局变量和其他文件定义的全局变量产生同名冲突的问题
static修饰变量时,如果变量没有被初始化会被自动初始化为0
外部静态变量/函数:
// 外部声明
static int global_var;
static void global_function();
// 定义
int global_var;
void global_function() {
// 外部静态函数只在声明它的文件内可见
}
3.static关键字修饰函数
函数也是具有外部连接属性的,编译器每次编译只处理一个编译单元,当某个编译单元需要使用其他编译单元中的函数,只需要声明该函数(或者包含该函数声明所在的头文件)然后就可以使用,当一个函数被static所修饰的时候,这个函数的外部连接属性就变成了内部连接属性,也就成为了静态函数,使得这个静态函数只能在自己所在的编译单元中被使用,而不能被其它编译单元所使用,否则会出现链接性错误。为了防止自己的函数名和其他文件定义的函数名产生同名冲突的问题
通常我们在头文件中声明一个函数,然后在源文件中去定义该函数,在定义某函数时需要将某一段代码封装成为另一个函数以达到复用的目的,这段代码所封装成的函数只是为了定义那个函数所使用,不会在其它源文件中使用,所以将其声明为static。
(c++):
类:
- 静态类:在某些编程语言中,你可以声明一个类为 static。这意味着这个类不能被实例化,只能包含静态成员。
-
class MyClass { public: static int static_var; // 类静态成员变量 static void static_function(); // 类静态成员函数 }; int MyClass::static_var = 0; // 静态成员变量的定义 void MyClass::static_function() { // 类静态成员函数 } 类成员的静态变量(也称为类数据成员的静态初始化): class MyClass { private: static const int static_const_var = 10; // 常量静态成员变量 类成员的静态函数: class MyClass { private: static const int static_const_var = 10; // 常量静态成员变量
STL中vector与list的区别
vector与list都属于STL中的序列式容器(顺序容器),元素以严格的线性形式组织起来,每个元素都有固定位置。
vector本质是动态数组:随机存取任何元素都能在常数事件完成,在尾端增删元素性能高。增加数据时大致按以下流程:
建立空间->填充数据->重建更大空间->复制原空间数据->删除原空间->添加新数据
list本质是双向循环链表:在任何位置增删元素都能在常数时间完成,但随机访问偏慢。
主要区别在于vector支持下标操作,而list不支持,vector多用于存储已知长度(模糊范围也可)且经常随机访问的数据,list主要用于存储长度未知且经常增删数据而少量随机访问。
简述什么是迭代器?有什么作用?迭代器失效是什么意思?
在STL中,容器的迭代器被称为容器元素对象或者io流中的对象的位置指示器,我们可以迭代器理解为面向对象的指针(一种泛型指针或者通用指针)
迭代器只是在某些操作上跟指针类似,但是迭代器不是指针
指针代表真正的内存地址,即对象在内存中的储存位置
迭代器则代表元素在容器中的相对位置
由于一些对容器的操作如删除元素或移动元素会修改容器的内在状态,会使原本指向被移动元素的迭代器失效,也可能使其他迭代器失效。使用无效的迭代器是没有意义的,可能会导致和使用空指针相同的问题,所以使用迭代器时,需要特别留意哪些操作会使迭代器失效,使用无效迭代器会导致严重的运行错误。