C++面试题目合集_01
题目摘录自“牛客网-题库-精华专题-c++校招面试题目合集”,选取对自己有帮助的进行记录,作为回顾笔记
分析代码问题
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1 = 'a'; // 代码无法通过编译,str1为 char *const类型的右值类型
}
strcpy( string, str1 ); // 库函数strcpy进行拷贝操作,strcpy会从源地址一直往后拷贝,直到遇到'\0'为止
}
// 修改方案
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<9; i++)
{
str1[i] = 'a';
}
str1[9] = '\0';
strcpy( string, str1 );
}
// 写出strcpy函数
// 2 points
void strcpy( char *strDest, char *strSrc )
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
// 4 points
void strcpy( char *strDest, const char *strSrc )
//将源字符串加const,表明其为输入参数,加2分
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
// 7 points
void strcpy(char *strDest, const char *strSrc)
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
// 10 points
// 为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc )
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}
void GetMemory( char *p ) // 传入形参并不能真正改变形参的值,执行完之后为空;
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str ); // 未判空即使用,可能分配失败
strcpy( str, "hello world" );
printf( str ); // 在函数GetMemory中和Test中没有malloc对应的free,造成内存泄露
}
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num ); // 未判断内存是否申请成功, if ( *p == NULL )
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" ); // 应该调用free, 把malloc生成的内存释放掉
printf( str ); // printf(str) 改为 printf("%s",str),否则可使用格式化 字符串攻击
}
swap( int* p1,int* p2 ) // 需要一个返回值void
{
int *p; // 在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
// 修改后
void swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
问答题
1.写出BOOL,int,float,指针变量 与“零值”比较的 if 语句
BOOL型变量:if(!var)
int型变量: if(var==0)
float型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
指针变量: if(var==NULL)
2. 编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefg”
void LoopMove(char *str, int steps)
{
int len = strlen(str);
char tmp[MAXSIZE];
strcpy(tmp, str+len-steps);
strcpy(tmp+steps, str);
*(tmp+len) = '/0';
strcpy(str, tmp);
}
void LoopMove(char *str, int steps)
{
int len = strlen(str);
char tmp[MAXSIZE];
memcpy(tmp, str+len-steps, steps);
memcpy(str+steps, str, len-steps);
memcpy(str, tmp, len);
}
3.编写类String的构造函数、析构函数和赋值函数,已知类String的原型为:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operator =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
//普通构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'\0'的空
//加分点:对m_data加NULL 判断
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
// String的析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷贝构造函数
String::String(const String &other) // 得分点:输入参数为const型
{
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//赋值函数
String & String::operator =(const String &other) // 得分点:输入参数为const型
{
if(this == &other) //得分点:检查自赋值
return *this;
delete [] m_data; //得分点:释放原有的内存资源
int length = strlen( other.m_data );
m_data = new char[length+1];
strcpy( m_data, other.m_data );
return *this; //得分点:返回本对象的引用
}
4. 说出static和const关键字尽可能多的作用
static关键字至少有下列5个作用:
- 全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
- 局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
- 静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
- 类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
- 类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
const关键字至少有下列n个作用:
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的 成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如:
const classA operator*(const classA& a1,const classA& a2);
operator的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错:
classA a, b, c;
(a * b) = c; // 对ab的结果赋值
操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
5. 请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1
int checkCPU()
{
union w // 联合体union的存放顺序是所有成员都从低地址开始存放
{
int a;
char b;
} c;
c.a = 1;
return (c.b == 1);
}
6. 说一说c++中四种cast转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
7. 介绍一下STL的allocator
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
delete运算分两个阶段:(1)调用对象希构函数;(2)掉员工::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
8. 一个C++源文件从文本到可执行文件经历的过程?
对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
9. C++的内存管理是怎样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
10. 如何判断内存泄漏?
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
11. new和malloc的区别
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
12. 如何采用单线程的方式处理高并发
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。
13.Linux虚拟地址空间
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存。
14. 缺页中断
malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
1、在指令执行期间产生和处理缺页中断信号
2、一条指令在执行期间,可能产生多次缺页中断
3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
15. 什么是大端小端以及如何判断大端小端
大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。
算法题
1. 给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内
#include <iostream>
#include <math.h>
using namespace std;
#define ABS_FLOAT_0 0.0001
struct point_float
{
float x;
float y;
};
float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2)
{
point_float AB, BC;
AB.x = pt1.x - pt0.x;
AB.y = pt1.y - pt0.y;
BC.x = pt2.x - pt1.x;
BC.y = pt2.y - pt1.y;
return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}
bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D)
{
float SABC, SADB, SBDC, SADC;
SABC = GetTriangleSquar(A, B, C);
SADB = GetTriangleSquar(A, D, B);
SBDC = GetTriangleSquar(B, D, C);
SADC = GetTriangleSquar(A, D, C);
float SumSuqar = SADB + SBDC + SADC;
if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))
{
return true;
}
else
{
return false;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)