C++面试常考基础(C++常考基础知识)

(由修根据深信服等公司的流出面试题和各公司的面经总结)

本文地址:https://x.wolfmark.org/x-select_2018-09-10_481.html

1.C++基本类型和占用的存储空间:

①整型:char(1B)、char16_t(2B)、char32_t(4B)、short(2B)、int(2B/4B)、long(4B)、long long (8B)、指针(2B\4B);32\64位系统int和指针占用内存大小不同、int至少和short一样长,long至少32位(4B)、且至少和int一样长;long long 至少64位(8B)、且至少和long一样长。

②浮点型:float(4B)、double(8B)、long double(16B)

数值范围:

类型 比特数 有效数字 数值范围
float 32 6~7 -3.4*10(-38)~3.4*10(38)
double 64 15~16 -1.7*10(-308)~1.7*10(308)
long double 128 18~19 -1.2*10(-4932)~1.2*10(4932)

 

2.C++中的拷贝构造函数用形参用值传递会有什么影响(网易游戏、尼禄)?

当一个函数需要以值 的方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个副本;所以一个类的拷贝构造函数以值的方式传递参数时,需要以值的方式传递一个(这个类的)对象作为实参,从而需要调用类的拷贝构造函数;结果是调用类的拷贝构造函数导致又一次调用拷贝构造函数,导致无限递归。

 

3.C++中new和C中的malloc的区别(尼禄、zoom):

①属性:

new/delete是C++关键字,需要编译器的支持,malloc/free是库函数,需要头文件stdlib.h的支持。

②参数:

使用new操作符申请内存分配时无需制定内存块的大小,编译器会根据类型信息自行计算;而malloc则需要显示的指出所需的内存尺寸。

③返回类型:

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是复合类型安全的操作符。而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型。例如:
buffer=(char*)malloc(i+1);

④分配失败:

new内存分配失败时,会跑出bad_malloc异常。malloc分配内存失败时返回NULL。

⑤自定义类型:

new会先调用operator new 函数,申请足够的内存(通常在底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型的指针。delete先调用析构函数,然后调用opertor delete函数来释放内存(通常底层用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

⑥重载:

C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是制定了一个地址作为内存的起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回地址。而malloc不允许重载。

⑦内存区域:

new操作符从自由存储区(freestore)上为对象动态分配内存,而malloc函数从堆上动态分配内存。

 

4.C++中用父类指针指向了子类实例,用父类指针调用了虚函数,调用的是子类的函数还是父类的函数?

父类指针指向子类实例对象,调用普通重写函数时,会调用父类中的函数。而调用被子类重写虚函数时,会调用子类中的函数。
再次说明了,子类中被重写的虚函数的运行方式是动态绑定的,与当前指向类实例的父类指针类型无关,仅和类实例对象本身有关。

 

5.C++中哪些机制可以替代宏定义常量:

①const替换宏

有两个需要注意的地方:
(1) 当定义的是常量指针,需要const两次( const char* const iVar = “C++”),第一个const是防止值被改变,第二个const防止地址被改变。
(2) 当定义到class内时,需要用static来修饰这个变量,防止出现多个实体。

② enum代替#define

 

6.C++在new失败后的处理方法:

①C++标准做法

new失败会抛出bad_alloc异常,用try catch来捕获

try
{
    double *ptr=new double[1000000];
}
catch(bad_alloc &memExp)
{
    //失败以后,要么abort要么重分配
    cerr<<memExp.what()<<endl;
}

② 分配内存的时候创建一个指针,判断是否是空指针

double *ptr=new double[1000000];
if( 0 == ptr)

 

7.某函数希望既可以被C调用,又可以被C++调用,应该怎么声明它的原型?

①C++调用C:

C++语言支持函数重载,C 语言不支持函数重载。函数被C++编译后在库中的名字与C 语言的不同。假设某个函数的原型为: void foo(int x, int y);该函数被C 编译器编译后在库中的名字为_foo , 而C++ 编译器则会产生像_foo_int_int 之类的名字。
C++提供了C 连接交换指定符号extern“C”来解决名字匹配问题。

/*TestC.h*/
#ifndef TESTC_H
#define TESTC_H

#ifdef __cplusplus
extern "C"{
#endif
int add(int a , int b);

#ifdef __cplusplus
}
#endif

#endif

 

/*TestC.c*/
#include"TestC.h"

int add(int a , int b)
{
  return(a+b);
}
/*TestC.cc*/
#include <iostream>
#include "TestC.h"
int main()
{
  cout << add(2,5) << endl ;
  return 0;
}

②C调用C++函数:

用一个函数将C++类的使用封装起来,然后将它外部声明为C函数就可以了。

/* ADD.h */
#ifdef ADD_H
#define ADD_H
class ADD
{
public:
	int add(int a, int b)
	{
		return(a+b);
	}
};
#endif // ADD_H

//*将C++类封装为C函数文件
/*ADD.cc*/
#include "ADD.h"

extern "C" int add_cpp(int a , int b);

int add(int a , int b)
{
	ADD tmp ;
	return tmp.add(a,b);
}

//*实际调用C++代码的C文件
/* add.c */

extern int add_cpp(int a , int b);

int main()
{
	cout << add_cpp(2,3) << endl;
	return 0;
}

 

8.编写C++静态成员函数需要注意哪些注意事项,通常有什么作用:

注意事项:

①静态成员函数只能访问类的静态数据成员,不能访问非静态数据成员;
②静态成员函数不要滥用,否则背离了面向对象编程的初衷;
③类的非静态成员函数可以调用静态成员函数,但是反之则不行。
作用:

C++提供了静态成员,用以解决同一个类的不同对象之间数据成员和函数的共享问题。
静态成员的特点是:不管这个类创建多少个对象,其静态成员在内存中只保留一份副本,这个副本为该类的所有对象所共享。

 

9.举例说明explicit关键字的用法:

C++中的explicit关键字只能用于修饰构造函数, 它的作用是表明该构造函数是显式的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。explicit关键字的作用就是防止类构造函数的隐式自动转换。

//A使用隐式转换implicit
class A
{
public:
	A(int){}
	A(int,int){}
	operator bool const {return true;}
};
//B类型使用显式转换explicit
class B
{
public:
	explicit B(int){}
	explicit B(int,int){}
	explicit operator bool const {return true;}
};


int main()
{
	A a1 = 1 ; //OK,使用A::A(int)
	A a2(2); //OK,使用A::A(int)
	A a3{4,5};//OK,使用A::A(int,int);
	A a4 ={4,5};//OK,使用A::A(int,int);
	A a5 =  (A)1;//OK,使用 static_cast
	bool na1 = a1 ;//OK,复制构造函数使用 A::operator bool()
	bool na2 = static_cast<bool>(a1);//OK  使用static_cast 直接赋值


	B b1 = 1 ; //出错,不会使用B::B(int)
	B b2(2); //OK,使用B::B(int)
	B b3{4,5};//OK,使用B::B(int,int);
	B b4 ={4,5};//出错,不会使用B::B(int,int);
	B b5 =  (B)1;//OK,使用 stbtic_cbst
	bool nb1 = b1 ;//出错,不会使用 B::operbtor bool()
	bool nb2 = stbtic_cbst<bool>(b1);//OK  使用stbtic_cbst 直接赋值
}

 

10.c++的普通成员函数能否作为线程的线程函数,为什么?假设原型都是void fun(void *):

一般来说,C++的类成员函数不能作为线程函数。这是因为在类中定义的成员函数,编译器会给其加上this指针。当把线程函数封装在类中,this指针会作为默认的参数被传进函数中,从而和线程函数参数(void*)不能匹配,不能通过编译。
解决方法:
①将该成员函数声明为static类型,去掉this指针;
②不定义类成员函数为线程函数,而将线程函数定义为类的友元函数。

 

11.如果父类的析枸函数不是virtual的,会有什么影响?

如果父类的析构函数不是virtual,在释放子类对象的内存时不会调用子类的析构函数而会直接调用父类的析构函数,导致内存泄露。

 

12.const 成员函数: const int fun() const ;两个const 分别表示什么意思:

①函数的返回值类型是const。 这个const修饰没什么意义,因为函数的返回值必须是固定值。
②int fun() const{} 则是类的常成员函数。它不能更新对象的数据成员,也不能调用该类中没有const修饰的成员函数(这就保证了在常成员函数中绝对不会更改数据成员的值)。
③如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数。

 

13.写出重载前++和后++的函数声明:

①前++:

self & operator++();

②后++:

self operator++(T);//T是类型名,如int

(只能自增一次,实际上在内部调用了“前++”)

 

14.纯虚函数和虚函数的区别:

①虚函数:
C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的特殊化。
②纯虚函数:
C++中的纯虚函数也是一种“运行时多态”。
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。

 

15.C++中禁止类被派生的方法:

将构造函数声明为私有(private)或者保护的(protected),就会阻止 类被实例化。

 

16.C++中如何禁止子类默认的拷贝函数,默认构造拷贝函数不会被执行?

class T{
    T(const T&) = delete; //禁用复制构造函数
    T(T&&) = default;   //启用默认的移动构造函数
};

 

17.C++全局对象的和构造顺序可控么?

C++规定,在同一个编译单元(同一个.cpp程序)中,先定义的对象先创建,后定义的对象后创建;而销毁时则相反,后定义的对象先销毁,先定义的对象后销毁。
可以用一个类来保证初始化顺序(详见Thinking in C++)。

 

18.c++基类的构造函数或者析构函数中调用的是被派生的类覆盖的虚函数么?

在子类对象的基类子对象构造期间,调用的虚函数的版本是基类的而不是子类的。因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类, 派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。
析构期间也是一样的逻辑:一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

 

19.虚函数和纯虚函数以及重载:

纯虚函数由子类实现,虚函数是默认实现,允许基类指针来调用子类的函数,普通函数是强制实现,子类不应该重写。重载函数在类型和参数数量上一定不同,重定义要求参数类型和个数、函数返回值类型相同;虚函数必须是类的成员函数;构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。

 

20.C++中与类型转换相关的四个关键字和它们的特点:

static_cast
特点:静态转换,在编译处理期间。
应用场合:主要用于C++中内置的基本数据类型之间的转换,但是没有运行时类型的检测来保证转换的安全性。
用于基类和子类之间的指针或引用之间的转换,这种转换把子类的指针或引用转换为基类表示是安全的;进行下行转换,把基类的指针或引用转换为子类表示时,由于没有进行动态类型检测,所以是不安全的。把void类型的指针转换成目标类型的指针(不安全)。
不能用于两个不相关的类型转换。
不能把const对象转换成非const对象。

const_cast
特点:去常转换,编译时执行。
应用场合:const_cast操作不能在不同的种类间转换。相反,它仅仅把它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。

reinterpret_cast
特点: 重解释类型转换
应用场合:它有着和c风格强制类型转换同样的功能;它可以转化任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;它的机理是对二进制进行重新的解释,不会改变原来的格式。

 

dynamic_cast
特点:可以将基类指针或引用安全的转化为派生类的指针或者引用。
应用场合:dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

 

21.C++中struct和class的区别,C#?

1.)C++:
最本质的一个区别就是默认的访问控制,体现在两个方面:
② 默认的继承访问权限。struct是public的,class是private的。
②struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
2.)C#:
class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,通常包含字段、属性、方法、属性、构造函数、索引器、操作符等。在.NET中,所有的类都最终继承自System.Object类,因此是一种引用类型,也就是说,new一个类的实例时,在堆栈(stack)上存放该实例在托管堆(managed heap)中的地址,而实例的值保存在托管堆(managed heap)中。
struct(结构)是一种值类型,用于将一组相关的变量组织为一个单一的变量实体 。所有的结构都继承自System.ValueType类,因此是一种值类型,也就是说,struct实例在创建时分配在线程的堆栈(stack)上,它本身存储了值。所以在使用struct时,我们可以将其当作int、char这样的基本类型类对待。

22.C++什么时候会调用析构函数?

析构函数在下边3种情况时被调用:
①对象生命周期结束,被销毁时(析构函数的调用顺序与构造函数的调用顺序相反);
②delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
③对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

 

23.C++解决内存泄漏的方法:

①养成配套使用new和delete的习惯
②使用智能指针(智能指针会自动删除分配的内存):
a. shared_ptr:
shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
b. unique_ptr
a) unique_ptr是一个独占的智能指针,他不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个 unique_ptr;
b) unique_ptr不允许复制,但可以通过函数返回给其他unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。
c) 如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
c. weak_ptr
弱引用的智能指针weak_ptr是用来监视shared_ptr的,不会使引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命 周期,更像是shared_ptr的一个助手。 weak_ptr没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中关连的资源是否存在。 weak_ptr还可以用来返回this指针和解决循环引用的问题。

 

24.C++引用和指针的区别(zoom面试):
①引用必须在创建时初始化,而指针不需要;
②引用一旦绑定了一个对象,则不能改变为对另一个对象的引用,而指针可以随时改变;
③引用时必须有一块合法的内存单元,不能为nullptr引用。
④需要为指针分配内存空间,而不必为引用;
⑤可以有多级指针,但是没有多级引用;
⑥用指针传递参数,可以实现对实参的改变;引用传递的是实参本身,不会创建临时副本,对形参的修改实际上是修改实参。

 

25.C++单例模式:

通过单例模式可以保证类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例模式的要点有三个:a. 单例类只能有一个实例 b. 它必须自行创建这个实例 c. 它必须自行向整个系统提供提供这个实例
优点:减少了时间和空间的开销,提高了封装性。
具体实现:将构造函数、复制构造函数、析构函数和重载赋值号=等设为私有,禁止重载;提供一个静态的函数接口以供外部调用。

class SingleSample
{
public:
	static SingleSample * getInterface();
private:
	SingleSample();
	SingleSample(const SingleSample &);
	SingleSample & operator=(const SingleSample &);
	~SingleSample();

	static SingleSample * interface ;
}

//初始化:
SingleSample * SingleSample::interface;
SingleSample * SingleSample::getInterface()
{
	return interface;
}

 

26.C++ 11 C++14 C++17 带来的新功能(尼禄面试):

详细内容:https://x.wolfmark.org/x-select_2018-09-29_497.html

(1)C++11:

①使用nullptr代表空指针
②自动类型推断(关键字auto和decltype()得到类型)
③使用默认构造函数和删除(default:A()=default; A()=delete;)
④匿名函数lambda表达式
⑤智能指针(shared_ptr、unique_ptr、weak_ptr、auto_ptr)
⑥右值引用T&&与移动构造函数
⑦初始化列表(更加优雅的初始化方法)
⑧加长参数模板
⑨tuple元组
⑩序列化for循环

(2)C++14:

①Lambda函数在C++11的基础上更加方便
②constexpr
③类型推导的拓展

(3)C++17:

①使 static_assert 的文本信息可选
②删除 trigraphs
③在模板参数中允许使用 typename(作为替代类)
④来自 braced-init-list 的新规则用于自动推导
⑤嵌套命名空间的定义,例如:使用 namespace X::Y { … } 代替 namespace X { namespace Y { … }}
⑥允许命名空间和枚举器的属性
⑦新的标准属性:[[fallthrough]], [[maybe_unused]] 和 [[nodiscard]]
⑧UTF-8 字符文字
⑨对所有非类型模板参数进行常量评估
⑩Fold 表达式,用于可变的模板
⑪A compile-time static if with the form if constexpr(expression)
⑫结构化的绑定声明,现在允许 auto [a, b] = getTwoReturnValues();
⑬if 和 switch 语句中的初始化器
⑭在某些情况下,确保通过编译器进行 copy elision(Guaranteed copy elision by compilers in some cases)
⑮一些用于对齐内存分配的扩展
⑯构造函数的模板推导,允许使用 std::pair(5.0, false) 代替std::pair<double,bool>(5.0, false)
⑰内联变量,允许在头文件中定义变量
⑱__has_include,允许由预处理程序指令检查头文件的可用性
⑲__cplusplus 的值更改为 201703L

 

27.strlen()和sizeof()的区别(zoom面试):

(1)定义不同:

sizeof()是运算符,在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
strlen()是函数,要在运行时才能计算。参数必须是字符串指针类型。当数组名作为参数传入时,实际上数组就退化为指针了。

(2)功能不同:

sizeof():获得能保证容纳实现所建立的最大对象的字节大小。

由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。

strlen():返回字符串的长度。字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字串的的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL。

(3)举例:

	char s[] ="hello";
	char *p = s;
        //注意 \0 不算在内
	cout << strlen(s) << endl;//5
	cout << strlen(p) << endl;//5
	cout << sizeof(s) << endl;//6
	cout << sizeof(p) << endl;//4

28.C++继承中重载、重写、重定义的区别(zoom面试):

(1)成员函数重载的特征:

①在相同的范围内(同一个类中)重载
②函数名称相同
③函数参数不同
④virtual关键字可有可无

(2)重写(覆盖)是指派生类函数覆盖基类函数的特征:

①不同的范围,分别位于派生类和基类中
②函数名称相同
③函数参数相同
④基类函数中必须有virtual关键字

(3)重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

①如果派生类的函数和基类的函数同名,但参数不同,此时不管有没有virtual,基类的函数被隐藏
②如果派生类的函数与基类的函数同名,但参数也相同,基类函数没有virtual关键字,基类的函数被隐藏

 

 本文地址:

(由修根据深信服等公司的流出面试题和各公司的面经总结)

更多秋招知识请看-秋招总结-猿类面试小书:

2 pings

  1. […] Pingback: C++面试参考手册(C++常考基础知识) | 脩 | Ghost-X […]

  2. […] « C++面试参考手册(C++常考基础知识) […]

发表评论

Your email address will not be published.