-
修饰函数的返回值
- 如果用const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函数调用结束后这个临时变量的生命周期也就结束了,因此把这些返回值修饰为const是没有意义的。
- 临时性:返回值是一个临时变量,其生命周期很短。
- 不可修改性:即使不使用const修饰符,返回值在大多数情况下也不会被直接修改,因为它是一个立即数或寄存器中的值。
-
typedef和define有什么区别?
- typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下4个方面的不同
- (a)原理不同
- #define是C语言中定义的语法,它是预处理指令
- typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能
- (b)功能不同
- 定义机器无关的类型例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度: typedef long double REAL ,在不支持 long double的机器上,该 typedef 看起来会是下面这样: typedef double real ,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等
- (c)作用域不同
- (d)对指针的操作不同
- 全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
- 不建议在头文件中定义全局变量,因为这会导致重复定义的问题。
- 推荐在头文件中使用 extern 声明全局变量,并在一个 .c 文件中定义它。
- 如果需要在不同的 .c 文件中使用同名但独立的全局变量,可以在各自的 .c 文件中使用 static 关键字声明这些变量。
- 重要:最后需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算
- 数组和指针的区别与联系是什么?
- (c1)存储方式
- 数组通常存储在静态存储区或栈上;指针可以随时随地地指向任意类型的内存块
- 数组在内存中是连续存放的,开辟一块连续的内存空间数组是根据数组的下标进行访问的;指针很灵活,它可以指向任意类型的数据指针的类型说明了它所指向地址空间的内存
- (c2)求sizeof
- (c3)数据访问方面
- 指针对数据的访问方式是间接访问,需要用到解引用符号(*数组名)
- 数组对数据的访问则是直接访问,可通过下标访问或数组名+元素偏移量的方式
- (c4)使用环境
- 指针多用于动态数据结构(如链表,等等)和动态内存开辟
- 数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配
- (d)指针进行强制类型转换后与地址进行加法运算,结果是什么?
- 指针和引用的异同是什么?如何相互转换?(C++)
- (a)相同
- (1)都是地址的概念,指针指向某一内存、内容是所指内存的地址;引用则是某块内存的别名
- (2)从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针常量,指向对象不能变,但指向对象的值可以变两者都是地址概念,所以本身都会占用内存
- (b)区别
- (1)指针是实体,而引用是别名
- (2)指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,引用是对值的自增
- (3)引用使用时无需解引用(*),指针需要解引用;
- (4)引用只能在定义时被初始化一次,之后不可变;指针可变
- (5)引用不能为空,指针可以为空
- (6)“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32 位系统指针变量一般占用4字节内存
- (c)转换
- (1)指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中
- (2)引用转指针:把引用类型的对象用&取地址就获得指针了
- (d)野指针是什么?
- (1)野指针是指向不可用内存的指针,当指针被创建,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针
- (2)当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生野指针,因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉
- (3)第三个造成野指针的原因是指针操作超越了变量的作用范围
- 如何避免野指针?
- (1)对指针进行初始化
- (2)指针用完后释放内存,将指针赋NULL
- 注:malloc函数分配完内存后需注意:
- (1) 检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL可以通过if语句来判断)
- (2) 清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)
- C++中的智能指针是什么?
- 智能指针是一个类,用来存储指针(指向动态分配对象的指针)
- C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存
- (j)智能指针的内存泄漏如何解决?
- 为了解决循环引用导致的内存泄漏,引入了弱指针 weak_ptr , weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存, 但是可以检测到所管理的对象是否已经被释放,从而避免非法访问
- (k)this指针是什么?
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参所以对象中不存储this指针
- (1)this指针指向当前对象,可以访问当前对象的所有成员变量包括private、protected、public
- (2)this指针是const指针,一切企图修改该指针的操作,如赋值(改变指向)、增减都是不允许的!
- (3)this指针只有在成员函数中才有定义因此,在创建一个对象后,也不能通过对象使用this指针所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的
- (4)只有创建对象后,this指针才有意义
- (5)static静态成员函数不能使用this指针原因静态成员函数属于类,而不属于某个对象,所以static静态成员函数压根就没有this指针
- (6)this在成员函数的开始执行前构造的,在成员函数的执行结束后清除至于如何清除的,由编译器实现,程序员不关心this是通过函数参数的首参数来传递的
-
C语言中内存分配的方式有几种?
- (a)静态存储区分配
- 内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等
- (b)栈上分配
- 在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放局部变量、函数内参数都在栈上
- (c) 堆上分配
- New开辟的空间在堆上
-
堆与栈有什么区别?
- (a)申请方式
- 栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放
- (b)申请大小的限制栈空间有限
- 在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow因此,能从栈获得的空间较小堆是很大的自由存储区堆是向高地址扩展的数据结构,是不连续的内存区域这是由于系统是用 链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址堆的大小受限于计算机系统中有效的虚拟内存由此可见,堆获得的空间比较灵活,也比较大
- (c)申请效率
- 栈由系统自动分配,速度较快但程序员是无法控制的 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
-
栈在C语言中有什么作用?
- (1)C语言中栈用来存储临时变量,包括函数参数和函数内部定义的临时变量函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景
- (2)多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石
-
C语言函数参数压栈顺序是怎样的?
- 栈的范围是由 ss * 10H 至 ss * 10H + sp
- (ss)指堆栈寄存器:存放堆栈段起始地址的高16位(即16进制下五个数的前四个数)
- (sp)指堆栈指针:用于存放栈顶的逻辑偏移地址
- 栈的栈底指针不变,栈顶的指针随sp的改变而改变由于栈的栈底地址是高地址,栈顶地址是低地址所以当栈存入数据时,会先将sp减去存入数据的字节数,然后再将数据存入反之,当栈取出数据时,会将数据取出后将sp加上取出数据的字节数(例如,当sp=0800H,ss=2360H时,若此时加入20个字节的数据,那么就要将sp-20,此时的栈顶就是ss * 10H + sp)
- 注:所谓高地址与低地址,前面的地址称为低地址,后面的地址称为高地址,例如23600H与23E00H,此时23600H为低地址,23E00H为高地址
- 回答这个问题:从右至左
- C语言参数入栈顺序的好处就是可以动态变化参数个数自左向右的入栈方式,最前面的参数被压在栈底除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式
- 例如: printf(const char* format,…)
- (1)printf函数是一个不定参函数
- (2)编译器通过format的%占位符的个数来获取参数的个数
- (3)假设函数压栈顺序是从左至右,format先入栈,各个参数再入栈,最后pc入栈入栈完之后,想知道参数的个数就要读取format,但要读取format就得知道参数的个数,陷入了一个死循环
- (4)但是,如果函数压栈顺序是从右至左,未知个数的参数先入栈,format再入栈,最后压pc入栈这时候要想知道参数的个数只需要将栈顶指针加2即可读取到format
-
C++的内存管理是怎样的?
- 在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分
- 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码
- 数据段:存储程序中已初始化的全局变量和静态变量
- BSS段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量
- 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存
- 映射区:存储动态链接库以及调用mmap函数进行的文件映射
- 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
-
什么是内存泄漏?
- 简单地说就是申请了一块内存空间,使用完毕后没有释放掉
- 它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了
-
如何判断内存泄漏?
- (1)良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉
- (2)将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表
- (3)Boost 中的smart pointer
- (4)一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等
-
new/delete与malloc/free的区别是什么?
- 在C++中,申请动态内存和释放动态内存,用new/delete 和 malloc/free都可以,new和malloc动态申请的内存都位于堆中,无法被操作系统回收,需要对应的delete/free来释放空间
- void *malloc(int size);
- 说明:malloc向系统申请分配指定size个字节的内存空间返回类型是 void* 类型void* 表示未确定类型的指针C,C++规定,void* 类型可以强制转换为任何其它类型的指针
- 对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建时要自动执行构造函数,在对象消亡之前要自动执行析构函数,而malloc/free 不在编译器控制权限之内,无法执行构造函数和析构函数
- 当然对于没有资源要清理的类,不调用析构函数也没有太大的问题,即使用free或delete没有区别但万一有一些类的成员是指针,而这个指针又在堆上开辟了空间,这时不调用析构函数去释放这个指针指向的这段空间,就会造成内存泄漏delete会调用析构函数,释放指针成员变量的空间,再销毁对象本身的空间;而free只释放了对象本身的空间,而指针成员所指向的空间没有被释放
- (1)new 能够自动计算需要分配的内存空间,而malloc需要手工计算字节数
- (2)new与delete带具体类型的指针,malloc与free返回void类型的指针
- (3)new 将调用构造函数,而malloc不能;delete将调用析构函数,而free不能
- (4)malloc/free 需要库文件<stdlib.h>支持,而new/delete不需要库文件支持
- (5)new操作可以重载,可以自定义内存分配策略,不做内存分配,或者分配到非内存设备上而malloc不能
- delete和free被调用后,内存不会不会立即收回,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放啦,还可以做其他用途由于没有对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况,因此,释放完内存后需要将指针向量置为nullpter。
-
预处理
-
预处理器标识#error的目的是什么?
- #error预处理指令的作用是,编译程序时,只要遇到#error就会生成一个编译错误提示消息,并停止编译其语法格式为:#error error-message
- 当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的,当你 不太确定当前是否定义了 XXX 时,就可以改成如下这样进行编译:
- 这样,如果编译时出现错误,输出了XXX has been defined,表明宏XXX已经被定义
-
# include< filename. h>和#include" filename. h"有什么区别?
- 对于 include< filename. h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快而 对于# include“ filename.h”,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。
- 头文件的作用有哪些?
- 头文件的作用主要表现为以下两个方面:
- (1)通过头文件来调用库功能出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的编译器会从库中提取相应的代码
- (2)头文件能加强类型安全检查当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担
-
在头文件中定义静态变量是否可行,为什么?
- 不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误因为如果在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定任何变量,当然也包括静态变量
-
其他C语言面试问题
-
C语言宏中“#”和“##”的用法
-
(a)“#”字符串化操作符
- 作用:#可以把一个宏参数直接转换成相应的字符串比如有下面这个宏
- 也就是说,最后输出的是宏参数的参数名即:将宏参数直接转换成相应得字符串
-
(b)“##”符号连接操作符
- 作用:将宏定义的多个形参转换成一个实际参数名
- 则下面第一个图的代码和第二个图的代码等价:
-
extern”C” 的作用是什么?
- extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的
-
strlen("\0") =? sizeof("\0")=? 两者结果与区别
- strlen("\0") =0,sizeof("\0")=2
- strlen用来计算字符串的长度(在C/C++中,字符串是以"\0"作为结束符的),它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串结束符\0为止,然后返回计数器值sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定
-
C语言中 struct与 union的区别是什么?
- struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,区别主要表现在以下两个方面:
- (1)结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的, 其所有成员都存在,不同成员会存放在不同的地址在计算一个结构型变量的总长度时,其内存空间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度
- (2)对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的不同成员赋值是互不影响的
-
- 假设为32位机器,int型占4个字节, double型占8个字节,char型占1个字节,而DATE是一个联合型变 量,联合型变量共用空间,uion里面最大的变量类型是int[5],所以占用20个字节,它的大小是20,而 由于 union中,double占了8个字节,因此 union是要8个字节对齐,所占内存空间为8的倍数为了实现 8个字节对齐,所占空间为24.而data是一个结构体变量,每个变量分开占用空间,依次为
- sizeof(int) + sizeof(DATE)+ sizeof( double)=4+24+8=36按照8字节对齐,占用空间为40,所以结果为 40+24=64
-
左值和右值是什么?
- 左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值
- 右值是指只可以出现在等号右边的变量或表达式它最重要的特点是可读一般的使用场景都是把一个右值赋值给一个左值通常,左值可以作为右值,但是右值不一定是左
-
有符号数和无符号数的运算?
- int a = -20, unsigned int b = 6,a+b是否大于6?
- 有符号和无符号运算,强制转换为无符号,所有a+b会变成(unsigned int)a+b;
- (unsigned int)a 就会相当于无符号最大值-20,那么是一个非常大的值,这个值加上6,那么肯定是大于6的;
- 最后的值是2^32-20+6=4294967282,肯定大于
-
什么是短路求值
-
- 输出结果为1输出为什么不是2,而是1呢?
- 其实,这里就涉及一个短路计算的问题由于i语句是个条件判断语句,里 面是有两个简单语句进行或运算组合的复合语句,因为或运算中,只要参与或运算的两个表达式的值都为真,则整个运算结果为真,而由于变量i的值为6,已经大于0了,而该语句已经为true,则不需要执行后续的j+操作来判断真假,所以后续的j++操作不需要执行,j的值仍然为1
- 因为短路计算的问题,对于&&操作,由于在两个表达式的返回值中,如果有一个为假则整个表达式的值 都为假,如果前一个语句的返回值为 false,则无论后一个语句的返回值是真是假,整个条件判断都为 假,不用执行后一个语句,而a>b的返回值为 false,程序不执行表达式n=c>d,所以,n的值保持为初值2
-
什么是大端和小端?
- 大端:高地址存低字节,低地址存高字节
- 小端:低地址存低字节,高地址存高字节
-
++a和a++有什么区别?两者是如何实现的?
- a++的具体运算过程为: 先用a,再执行++
- ++a的具体运算过程为: 先执行++,再用a
C面试八股 总结
最新推荐文章于 2024-08-20 20:01:22 发布