**C++面试题**
第一部分:c
**1,内存分为5个区:**堆(malloc),栈(如局部变量,函数参数),程序代码区(存放二进制代码),全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,c++中有自由存储区(new)一说。
2,堆和栈的区别?
1,堆存放动态分配的对象–即哪些在程序运行时动态分配的对象,比如new出来的对象,其生存期由程序控制,
2,栈用来保护定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在。
3,静态内存用来保存static对象,类static数据成员以及定义在仍和函数外部的变量,static对象在使用之前分配,程序结束时销毁。
4,栈和静态内存的对象由编译器自动创建和销毁。
3,堆和自由存储区的区别?
总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是c++中通过new与delete动态分配和释放对象的抽象概念,他们并不是完全一样。
从技术上来说,堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。自由存储时c++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的c++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete业余会按照malloc和free的方式来被实现,这是藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。
4,程序编译的过程?
程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要四个过程:预处理,编译,汇编和链接。
5,计算机内部如何存储负数和浮点数?
负数比较容易,就是通过一个标志位和补码来表示。
对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f=2.25f的时候,是如何分配内存的呢?
1,符号位(sign):0代表正,1代表为负,
2,指数位:用于存储科学计数法中的指数数据,并且采用移位存储
3,尾数部分:
6,函数调用的过程?
int main(void)
{
…
d = fun(a, b, c);
cout<<d<<endl;
…
return 0;
}
调用fun()的过程大致如下:
main()========
1).参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
2).保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位置);
3).跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
fun()=====
4).移动ebp、esp形成新的栈帧结构;
5).压栈(push)形成临时变量并执行相关操作;
6).return一个值;
7).出栈(pop);
8).恢复main函数的栈帧结构;
9).返回main函数;
main()
7,左值和右值
左值指既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。举例a是一个左值,malloc返回的是一个右值。或者左值就是在程序中能够寻值的东西,右值就是一个具体的真实的值或者对象,因此没法对右值进行赋值,
可以取地址的,有名字的,非临时的就是左值
不能取地址的,没有名字的,临时的,通常生命周期就在某个表达式之内的就是右值
8,什么是内存泄露?面对内存泄露和指针越界,采用哪些方法?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。
1,使用的时候要记得指针的长度
2,malloc的时候得确定在哪里free
3,对指针赋值得时候应该注意被赋值指针需要不需要释放
4,动态分配内存的指针最好不要再次赋值
5,在c++中应该有限考虑使用智能指针 shared_ptr
**
第二部分:c vs c++
**
1,c和c++的区别
1,c++是c的超集
2,c是一个结构化语言,它的重点在于算法和数据结构。c程序的设计首要考虑的是如何通过一个过程,对输入进行运算处理得到输出,而对于c++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程控制。
2,int fun()和int fun(void)的区别?
在c中,int fun()会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制,而int fun(void)则限制输入类型为一个void。
在c++下,这梁洪情况都会解读为返回int类型,输入void类型。
3,const有什么用途
1,定义只读变量,或者常量;
2,修饰函数的参数和函数的返回值,
3,修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数。
4,只读对象。只读对象只能调用const成员函数。
class Screen {
public:
const char cha; //const成员变量
char get() const; //const成员函数
};
const Screen screen; //只读对象
4,在c中用const能定义真正意义上的常量吗?c++中的cosnt呢?
不能。c中的const仅仅是从编译层来限定,不允许对const变量进行赋值操作,在运行期是无效的,所以并非是真正的常量,比如通过指针对const常量是可以修改值的,但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后遇到这个变量会从符号表中查找,所以在c++中是不可能修改到const变量的。
1,c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
2,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
3,c++中的const和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
4,c语言中只有ennum可以实现真正的常量
5,c++中只有用字面量初始化的const常量会加入符号表,而变量初始化的const常量依然只是只读变量。
6,c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。
同样一段代码,在c编译器下,打印结果为*pa = 4, 4
在c++编译下打印的结果为 *pa = 4, 8
**int main(void)
{
const int a = 8;
int *pa = (int *)&a;
*pa = 4;
printf("pa = %d, a = %d", pa, a);
return 0;
}
另外值得一说的是,由于c++中const常量的值在编译期就已经决定,下面的做法是OK的,但是c中是编译通不过的。
int main(void)
{
const int a = 8;
const int b = 2;
int array[a+b] = {0};
return 0;
}
5,宏和内联(inline)函数的比较?
1,首先宏是c中引入的一种预处理功能,
2,内联(inline)函数是c++中引用的一个新的关键字,c++中推荐使用内联函数来替代宏代码片段。
3,内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程,
4,由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全。
5,inline会向编译器提出内联请求,但是是否内联由编译器决定。当然可以通过设置编译器强制使用内联
6,由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
7,内联函数不能过于复杂。
6,c++中有了mallic/free,为什么还需要new/delete?
1,malloc与free是c++/c语言的标准库函数,new/delete是c++的运算符,他们可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求,对象在创建的同事要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此c++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
new在申请内存的时候就可以初始化,而malloc是不允许的。由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是c++语言所自带的运算符。
nt p = new int(1);
特别的,在C++中,如下的代码,用new创建一个对象(new 会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。
#include
class Test
{
…
}
Test pn = new Test;
Test pm = (Test)malloc(sizeof(Test));**
7,c和c++中的强制类型转换?
c中是直接在变量或者表达式前面加上目标类型来进行转换,
c++引入了四种转换:
1). static_cast
a. 用于基本类型间的转换
b. 不能用于基本类型指针间的转换
c. 用于有继承关系类对象间的转换和类指针间的转换
2). dynamic_cast
a. 用于有继承关系的类指针间的转换
b. 用于有交叉关系的类指针间的转换
c. 具有类型检查的功能
d. 需要虚函数的支持
3). reinterpret_cast
a. 用于指针间的类型转换
b. 用于整数和指针间的类型转换
4). const_cast
a. 用于去掉变量的const属性
b. 转换的目标类型必须是指针或者引用
在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。
**class test{
int m_value;
…
public:
operator int() //类型转换函数
{
return m_value;
}
int toInt() //显示调用普通函数来实现类型转换
{
return m_value
}
};
int main()
{
...
test a(5);
int i = a;
...
return 0;
}**
8,static有什么用途
1,静态(局部/全局)变量
2,静态函数
3,类的静态数据成员
4,类的静态成员函数
9,类的静态成员变量和静态成员函数各有哪些特性?
静态成员变量
1,需要在类内声明(加static),在类外初始化(不能加static)
2,在类外单独分配存储控件,位于全局数据区,因此生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量。
3,可通过对象名直接访问公有静态成员变量。
4,可通过类名直接调用共有静态成员变量,即不需要通过对象
class examoleL{
public:
static int m_int;
}:
int example::m_int = 0;
cout<<example::m_int;
静态成员函数:
1,类所共享的;
2,可访问静态成员变量,但不能直接访问普通成员变量(需要通过对象来访问),
3,可通过对象名直接访问共有静态成员函数;
4,可通过类名直接调用共有静态成员函数,
class example{
private:
static int m_int_s;
int m_int;
static int getI()
{return m_int_s;} //如果返回m_int报错。
};
cout<<example::getI():
10,在c++程序中调用被c编译器编译后的函数,为什么要加extern“C”?
c++支持函数重载,c不支持,函数被c++编译器编译后在库中的名字与c的不同
void foo(int x, int y);
该函数被c编译后名字为_foo,而c++为_foo_int_int之类的名字。所以c++提供了c链接交换指定符号extern“C”。
11,头文件中的ifndef/define/endif是干什么用的?和program once的区别?
它们的作用是防止头文件被重复包含。
1,ifndef是语言本身提供,但是program once一般由编译器提供,也就是说,有可能出现编译器不支持的情况。
2,运行速度ifndef一般慢于program once,特别是在大型项目上,区别比较明显,
3,ifndef作用于某一段被包含(define和endif之间)的代码。而program once则是针对包含该语句的文件,
4,如果用ifndef包含某一段宏定义,当这个宏名字出现撞车,可能会出现这个宏在程序中提示宏未定义的情况。相反,由于program once针对整个文件,不存在宏名字撞车,但如果某个头文件被多次拷贝,program once无法保证不被多次包含
12,i++和++i的区别?
**int operator++(int)
{
int temp = *this;
++*this;
return temp;
}//i++返回对象本身
int& operator++()
{
*this += 1;
return *this;
}//++i返回对象引用**
i++返回的是i的值,i++返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,
**cout<<++(++(++i))<<endl;
cout<<++ ++i<<endl;**
可不停的嵌套++i;
**int main()
{
int i = 1;
printf("%d,%d\n", ++i, ++i); //3,3
printf("%d,%d\n", ++i, i++); //5,3
printf("%d,%d\n", i++, i++); //6,5
printf("%d,%d\n", i++, ++i); //8,9
system("pause");
return 0;
}**
首先函数的入栈顺序是从右往左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作;
对于第一个,首先执行++i,再执行++i;将i压入栈中,所以是3,3;
对于第二个,首先执行i++,就是3,再执行++i,将3,5压入栈中
对于第三个,首先执行i++,就是5,再执行i++,就是6,
对于第四个,首先执行++i;i为8,再执行i++,返回值为8,此时i为9.
第三部分:数组、指针 & 引用
1,指针和引用的区别?
相同点:
1,都是地址的概念;
2,都是指向一块内存,指针指向一块内存,它的内容是所指内存的地址,而引用是某块内存的别名。
3,引用再内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。
不同点:
1,指针是一个实体,而引用看起来仅是个别名;
2,引用只能再定义时被初始化一次,之后不可变;指针可变;
3,引用不能为空,指针可以为空;
4,sizeof引用 得到的是所指向的变量的大小,而sizeof指针 得到的是指针本身的大小;
5,指针和引用的自增(++)运算意义不一样;
6,引用是类型安全的,而指针不是
7,引用具有更好的可读性和实用性。
2,引用占用内存控件吗?
1,对引用取地址,其实是取的引用所对应的内存空间的地址。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。
type& name; === type* const name.
**int main(void)
{
int a = 8;
const int &b = a;
int p = &a;
p = 0;
cout<<a; //output 0
return 0;
}
3,三目运算符
在c中三木运算符(?:)的结果仅仅作为右值,下面的做法c编译器下会报错,但是c++中却可以通过。这个进步就是通过引用来实现的,因为下面的三木运算符的返回结果是一个引用,然后对引用进行赋值是允许的。
int main(void)
{
int a = 8;
int b = 6;
(a>b ? a : b) = 88;
cout<<a; //output 88
return 0;
}
4,指针数组和数组指针的区别
数组指针,是指向数组的指针,而指针数组则是该数组的元素均为指针。
1,数组指针本质为指针,如 int (*p)【10】,p即为指向数组的指针,()优先级高,说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长,也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。
*类型名 (数组标识符)[数组长度]
2,指针数组,在c和c++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每个元素均为指针,其本质为数组,如 int *p【n】,【】优先级高,先于p结合成一个数组,再由int *说明这是一个整型指针数组,它有n个指针类型的数组元素,这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p【0】、p【1】。。。p【n-1】,而且它们分别是指针变量可以用来存放变量地址。但可以这样*p=a;这里*p表示指针数组第一个元素的值,a的首地址的值。
**类型名 *数组标识符[数组长度]**
5,左值引用与右值引用
左值引用就是我们通常所说的引用,可以看作的变量的别名。
**type-id & cast-expression
// demo
int a = 10
int &b = a
int &c = 10 // 错误,无所对一个立即数做引用
const int &d = 10 // 正确, 常引用引用常数量是ok的,其等价于 const int temp = 10; const int &d = temp**
右值引用是c++11新增的特性,其形式如下所示,右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
**type-id && cast-expression
// demo
int &&var = 10; // ok
int a = 10
int &&b = a // 错误, a 为左值
int &&c = var // 错误,var 为左值
int &&d = move(a) // ok, 通过move得到左值的右值引用**
第四部分:c++特性
1,什么是面向对象(opp)?面向对象的意义?
核心思想是数据抽象、继承和动态绑定(多态)。
将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中,以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。
2,封装、继承和多态
1,封装:
是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(类)。
封装的意义在于保护或者防止代码被我们无意中破坏。
public 是一种暴露的手段,比如暴露接口,类的对象可以访问
private 是一种隐藏的手段,类的对象不能访问
protected 成员:
和 public 一样可以被子类继承
和 private 一样不能在类外被直接调用
特例:在派生类中可以通过衍生类对象访问,如下代码所示
**2, 继承:**
继承主要实现重用代码,节省开发时间。
子类可以继承父类的一些东西。
a.**公有继承(public)**公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态(基类的私有成员仍然是私有的,不能被这个派生类的子类所访问)。
b.**私有继承(private)**私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员(并且不能被这个派生类的子类所访问)。
c.**保护继承(protected)**保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员(并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的)。
**3,多态:**
多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。
3. 什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?
1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。
更多可以参考下面的代码,比较容易混淆的是赋值操作符,其实区分很简单,在出现等号的时候,如果有构造新的对象时调用的就是构造,不然就是赋值操作符。
4. 构造函数和析构函数的执行顺序?
构造函数
1). 首先调用父类的构造函数;
2). 调用成员变量的构造函数;
3). 调用类自身的构造函数。
析构函数
对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。
5. 虚析构函数的作用?
基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。
class A{
public:
A(){}
//~A(){}
virtual ~A(){} // 虚析构
};
class B : public A{
public:
B(){
// new memory
}
~B(){
// delete memory
}
};
int main(int argc, char *argv)
{
A *p = new B;
// some operations
delete p; // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数
return 0;
}
但并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
6. 细看拷贝构造函数
对于 class A,它的拷贝构造函数如下:
A::A(const A &a){}
1) 为什么必须是当前类的引用呢?
循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
**2) 为什么是 const 引用呢?**
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
7. C++的编译环境
如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。
#include //C++标准库,不带".h"
#include<string.h> //C语言兼容库,由编译器厂商提供
值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。
8. Most vexing parse
直接上代码吧。下面 f 和 g 是有问题的,这种情况就称为 Most vexing parse。
class A {
public:
A() { cout << "const without param" << endl; }
A(int a) { cout << "const with param" << endl; }
A(const A& b) { cout << "copy construct" << endl; }
};
int main(void)
{
A a; // const without param
A b(10); // const with param
A c = A(); // const without param
A d = A(10); // const with param
A e(d); // copy construct
A f();
A g(A());
A h{}; // const without param
A i{A{}}; // const without param
return 0;
}
问题在哪?
A f(); // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 f,参数无
A g(A()); // 这个是不是可以看做声明了一个返回值为A的函数,函数名为 g, 参数类型为函数指针,这个函数指针的返回值类型为A,参数无
解决办法参考上面的 h, j。