C++语言基础面试题

C++语言

C++常见面试3中类型

类型1:对C++概念的理解,常问C++关键字的理解程度

1、sizeof求空类型

定义一个空类型,里面没有任何成员变量和成员函数,对该类型求sizeof的结果:
答案:是1不是0;
原因:虽然空类型的实例不包含任何信息,但是我们在生命该类型的实例的时候,它必须在内存种占有一定的空间,否则无法使用这些实例。

2、如果在该类新种添加一个构造函数和析构函数,再求sizeof

答案:还是1.因为调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,与类型的实例无关。

3、如果把析构函数标记为虚函数?

答案:类型中有虚函数则会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。指针占4个字节(32位机器)或8个字节(64位机器)。

类型2:分析代码运行结果

class A{
private:
	int value;
public:
	A(int n){value = n;}
	A(A other){value = other.value}  //这里有问题
	
	void Print(){std::cout<<value<<std::endl;}
};

int _tmain(int argc,_TCHAR *argv[]){
	A a = 10;
	A b = a;
	b.Print();

	return 0;
}

复制构造函数A(A other)传入的参数是A 的一个实例,如果是传值参数,此时形参赋值到实参会调用复制构造函数;因此如果允许复制构造函数传值,就会在复制构造函数中调用复制构造函数,就会形成无限递归调用从而导致栈溢出

构造函数修改如下:
A(const A& other)  //把传值参数改为常量引用

类型3:写代码定义一个类型或是实现类型中的成员函数

写代码的难度自然比分析要难很多。常考察构造函数,析构函数和运算符重载;如下的赋值运算符重载!

1、赋值运算符重载(深拷贝浅拷贝)

题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数

class CMyString
{
private:
	char* m_pData;
public:
	CMyString(char *pData = nullptr);  // 构造函数“声明”
	CMyString(const CMyString& str);   // 复制构造函数
	~CMyString(void);
}

面试官关注的点:
1、返回值类型是否为该类型的引用,并在函数结束前返回实例自身的引用(*this)。只有返回一个引用,才可以允许连续赋值。–返回为void可不能用于赋值
2、传入的参数类型是否为常量引用。----避免调用一次复制沟槽函数
3、是否释放实例自身已有的内存。—在分配新内存之前没有释放自身已有的空间,则程序将出现内存泄露!----要知道在当前赋值之前,该实例可能是有分配空间的了!
4、判断传入的参数和当前的实例(*this)是不是同一个实例----如果是则不进行赋值直接返回,否则不判断就进行赋值,其在释放实例自身内存的时候就会导致严重问题。

 教材上的参考代码,初级程序员
//返回引用、所属类、函数名、常引用作参数
CMyString& CMyString::operator=(const CMyString& str) 
{
	if(this == &str)  //与当前实例判断
		return *this;
	delete []m_pData;  // 释放实例已有内存
	m_pData = nullptr;
	m_pData =  new char[strlen(str.m_pData)+1];  //重新分配空间
	strcpy(m_pData, str.m_pData);  //赋值
	
	return *this   //返回自身引用
}

考虑异常安全性的解法,高级程序员必备
----考虑当前内存可能不足,所以先不释放实例已有内存!
解决办法2种:
1、先new char成功分配新内容,在用delete释放已有的内容
2、先创建一个临时实例,再交换临时实例和原来的实例 --- 更好的解法
CMyString& CMyString::operator=(const CMyString& str) 
{
	if(this != &str)  //与当前实例判断
	{
		CMyString strTemp(str);  //使用复制构造函数,看是否能成功
		//交换指针指向
		char *pTemp = strTemp.m_pData;
		strTemp.m_pData = m_pData;  //strTemp为局部变量,出作用域会自动析构回收内存
		m_pData = pTemp;
	}	
	return *this   //返回自身引用
}

C++语法部分

1、static关键字的作用

① 全局静态变量

  • 定义:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
  • 内存中的位置:静态存储区,在整个程序运行期间一直存在。
  • 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
  • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

② 局部静态变量

  • 定义:在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
  • 内存中的位置:静态存储区
  • 初始化:未经初始化的局部静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
    作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变

static 修饰的局部变量和没有初始化的全局变量在同一个bss段,bss段是被清零的段,所以static修饰的局部变量的在没有显示的初始化的情况下是0

③ 静态函数

  • 定义:在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
    函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
    warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

④ 类的静态成员

  • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

⑤ 类的静态函数

  • 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名
    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用
    从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

2、说一下C++和C的区别

① 设计思想上:

  • C++是面向对象的语言,而C是面向过程的结构化编程语言

② 语法上:

  • C++具有封装、继承和多态三种特性
  • C++相比C,增加多许多类型安全的功能,比如强制类型转换、
  • C++支持范式编程,比如模板类、函数模板等

3、说一说c++中四种cast类型转换 (不熟悉)

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
① const_cast

  • 用于将const变量转为非const

② static_cast

  • 用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

③ dynamic_cast

  • 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
    向上转换:指的是子类向基类的转换
    向下转换:指的是基类向子类的转换
    它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

④ reinterpret_cast

  • 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

为什么不使用C的强制转换?

  • C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

4、 请说一下C/C++ 中指针和引用的区别?

① 引用
引用就是C++对C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样
引用的声明方法:类型标识符 &引用名=目标变量名;引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了
② 指针
指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
③ 区别
1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
5.可以有const指针,但是没有const引用
6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变
7.指针可以有多级指针(**p),而引用止于一级;
8.指针和引用使用 ++运算符的意义不一样;
9.如果返回 动态内存分配 的对象或者内存,必须使用指针,引用可能引起内存泄露。

5、 请你说一下你理解的c++中的smart pointer四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr

C++里面的四个智能指针: auto_ptr, unique_ptr, shared_ptr, weak_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针

  • 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

① auto_ptr(c++98的方案,cpp11已经抛弃)

使用方式:给智能指针这一模板类传递一个动态分配的地址
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

② unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
【可跳过】注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

③ shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数
use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

④ weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

链接: C++ shared_ptr智能指针循环引用造成的内存泄露.

6、智能指针有没有内存泄露的情况

当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

7、请你来说一下智能指针的内存泄漏如何解决

为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

8、请回答一下数组和指针的区别(讲的一般)

在这里插入图片描述

7、 请你回答一下野指针是什么?

  • 定义:野指针是指向位置随机的、不正确的指针,系统无法对其进行操作;
  • 避免方法:在创建指针时必须进行初始化;在释放指针指向的内存之后必须将指针置空

8、为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数 考点:虚函数 析构函数

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间防止内存泄漏

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

9、 C++中析构函数的作用

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
  • 析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  • 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数
  • 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏

10、 静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

11、你理解的虚函数和多态

  • 多态的实现主要分为静态多态和动态多态静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定
    举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

12、 说一说重载和覆盖

① 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
② 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

13、 说一下函数指针

  • 定义:函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。
  • C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
  • 用途:调用函数和做函数的参数,比如回调函数。
char * fun(char * p)  {}       // 函数fun
char * (*pf)(char * p);             // 函数指针pf
pf = fun;                        // 函数指针pf指向函数fun
pf(p);                        // 通过函数指针pf调用函数fun

14、 strcpy和strlen

  • strcpy是字符串拷贝函数,原型:
    char strcpy(char dest, const char *src);
    从src逐字节拷贝到dest,直到遇到’\0’结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。

  • strlen函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数

15、 写个函数在main函数执行前先运行

__attribute((constructor))void before()
{
    printf("before main\n");
}

在C++中,也利用全局变量和构造函数的特性,通过全局变量的构造函数在main()函数之前执行
class BeforeMain{
public:
 BeforeMain();
};

BeforeMain::BeforeMain() {
 cout << "Before main" << endl;
}

BeforeMain bM; // 利用全局变量和构造函数的特性,通过全局变量的构造函数执行

16、智能指针shared_ptr的实现

核心要理解引用计数,什么时候销毁底层指针,还有赋值,拷贝构造时候的引用计数的变化,析构的时候要判断底层指针的引用计数为0了才能真正释放底层指针的内存

template <typename T>
class SmartPtr
{
private:
	T *ptr;    //底层真实的指针
	int *use_count;//保存当前对象被多少指针引用计数   【注意是个指针】
public:
	SmartPtr(T *p); //SmartPtr<int>p(new int(2)); 构造函数
	SmartPtr(const SmartPtr<T>&orig);//SmartPtr<int>q(p); 拷贝构造函数(要深拷贝)
	SmartPtr<T> &operator=(const SmartPtr<T> &rhs);//q=p   赋值函数(要深拷贝)
	~SmartPtr();  //析构函数
	
	T operator*();  //为了能把智能指针当成普通指针操作定义解引用操作
	T* operator->();  //定义取成员操作
	T* operator+(int i);//定义指针加一个常数
	int operator-(SmartPtr<T>&t1,SmartPtr<T>&t2);//定义两个指针相减

void getcount() { return *use_count } }; 

template <typename T> 
int SmartPtr<T>::operator-(SmartPtr<T> &t1, SmartPtr<T> &t2)
 { return t1.ptr-t2.ptr; }

template <typename T> 
SmartPtr<T>::SmartPtr(T *p) 
{ 
	ptr=p; 
	try { use_count=new int(1); }
	catch (...) 
	{
		delete ptr;    //申请失败释放真实指针和引用计数的内存
		ptr= nullptr; 
		delete use_count;
		use_count= nullptr; 
	}
} 

template <typename T>
SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig) //复制构造函数
{
	use_count=orig.use_count;//引用计数保存在一块内存,所有的SmarPtr对象的引用计数都指向这里
	this->ptr=orig.ptr;
	++(*use_count);//当前对象的引用计数加1
} 

template <typename T> 
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs)
 {
 //重载=运算符,例如SmartPtr<int>p,q; p=q;这个语句中,首先给q指向的对象的引用计数加1,因为p重新指向了q所指的对象,所以p需要先给原来的对象的引用计数减1,如果减一后为0,先释放掉p原来指向的内存,然后讲q指向的对象的引用计数加1后赋值给p
 ++*(rhs.use_count);
  if((--*(use_count))==0) 
  { delete ptr; ptr= nullptr; delete use_count; use_count= nullptr; } 
  ptr=rhs.ptr; *use_count=*(rhs.use_count); return *this; }
 
 template <typename T> 
 SmartPtr<T>::~SmartPtr() { getcount();
if(--(*use_count)==0)  //SmartPtr的对象会在其生命周期结束的时候调用其析构函数,在析构函数中检测当前对象的引用计数是不是只有正在结束生命周期的这个SmartPtr引用,如果是,就释放掉,如果不是,就还有其他的SmartPtr引用当前对象,就等待其他的SmartPtr对象在其生命周期结束的时候调用析构函数释放掉
{
getcount();
delete ptr;
ptr= nullptr;
delete use_count;
use_count=nullptr;
}
}
template <typename T>
T SmartPtr<T>::operator*()
{
return *ptr;
}
template <typename T>
T*  SmartPtr<T>::operator->()
{
return ptr;
}
template <typename T>
T* SmartPtr<T>::operator+(int i)
{
T *temp=ptr+i;
return temp;
}
}

17、const修饰成员函数的目的是什么?

const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

18、以下四行代码的区别是什么? const char * arr = “123”; char * brr = “123”; const char crr[] = “123”; char drr[] = “123”;

const char * arr = "123";
//字符串123保存在**常量区**,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样

char * brr = "123";
//字符串123保存在**常量区**,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值

const char crr[] = "123";
//这里123**本来是在栈上的**,但是编译器可能会做某些优化,将其放到常量区

char drr[] = "123";
//**字符串123保存在栈区**,可以通过drr去修改

19、C++里是怎么定义常量的?常量存放在内存的哪个位置?

  • 对于局部对象,常量存放在栈区
  • 对于全局对象,常量存放在全局/静态存储区。
  • 对于字面值常量,常量存放在常量存储区。

20、如果同时定义了两个函数,一个带const,一个不带,会有问题吗?

不会,这相当于函数的重载

21、C++函数栈空间的最大值

默认是1M,不过可以调整

22、new/delete与malloc/free的区别是什么

  • 首先,new/delete是C++的关键字,而malloc/free是C语言的库函数
  • malloc需要指定定申请内存的大小返回的指针需要强转
  • new会调用构造函数,不用指定内存大小,返回的指针不用强转

23、C语言是怎么进行函数调用的?

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈

24、C++中拷贝构造函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满

25、C++创建一个空对象,对象占据的空间大小是多少

  • 1个字节,不要误以为是0,因为对象必须占据一定的空间!

26、在没有显示定义函数的情况下,C++默认生成哪些函数

  • 自动生成4个函数:构造函数、析构函数、拷贝构造函数、赋值函数?
    注意:自动生成的拷贝构造函数和赋值函数都只能实现浅拷贝

27、C++的深拷贝与浅拷贝理解,深拷贝代码实现

28、结构体struct 和 类class

容器和算法

1、map和set有什么区别,分别又是怎么实现的?

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)
map和set区别在于:

  • ① map中的元素是key-value(关键字—值)对; set中每个元素只包含一个关键字。
  • set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效
  • map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中!只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用。如果find能解决需要,尽可能用find。

2、介绍一下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()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

3、说一说STL迭代器删除元素

这个主要考察的是迭代器失效的问题。

  • 1.对于序列容器vector, deque来说,使用erase(iterator)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,同时erase会返回下一个有效的迭代器;
  • 2.对于关联容器map, set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
  • 3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

4、说说STL中map、multimap与unordered_map

  • Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复
    底层实现:红黑树
    适用场景:有序键值对不重复映射

  • Multimap 多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
    底层实现:红黑树
    适用场景:有序键值对可重复映射

  • 数据存放形式:map为红黑树。unordered map底层结构是哈希表

5、STL有什么基本组成

  • STL主要由:以下几部分组成:
    容器迭代器仿函数算法分配器配接器

  • 他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

6、vector和list的区别,应用,越详细越好

  • ① 概念

  • Vector:连续存储的容器,动态数组,在堆上分配空间
    底层实现:数组
    两倍容量增长:vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
    性能
    访问:O(1)
    插入:在最后插入(空间够):很快
    在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
    在中间插入(空间够):内存拷贝
    在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
    删除:在最后删除:很快; 在中间删除:内存拷贝
    适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

  • List :动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
    底层:双向链表
    性能
    访问:随机访问性能很差,只能快速访问头尾节点。
    插入:很快,一般是常数开销
    删除:很快,一般是常数开销
    适用场景:经常插入删除大量数据

  • 区别
    1)vector底层实现是数组;list是双向 链表。
    2)vector支持随机访问,list不支持。
    3)vector是顺序内存,list不是。
    4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
    5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
    6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

  • 应用
    vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
    list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

7、STL中迭代器的作用,有指针为何还要迭代器

  • Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
  • 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符 ->、++、- - 等。
    迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,- - 等操作。迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。

迭代器产生原因:Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

8、STL里resize和reserve的区别 【有待确认】

  • resize():改变当前容器内含有元素的数量(size()),eg: vector< int >v; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
  • reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor 复制 过来,销毁之前的内存;
#include <iostream>
#include <vector>
using namespace std;
int main() {
    注意不同编译器输出结果可能不同,VS输出如下
    vector<int> a;
    a.reserve(100);
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;//50  100
    a.resize(150); //会影响最大空间大小
    cout<<a.size()<<"  "<<a.capacity()<<endl; //150  150
    a.reserve(50);  //小于原最大空间值,没影响
    cout<<a.size()<<"  "<<a.capacity()<<endl;//150  150
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;  //50  150
}

9、左值、右值

C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
链接: link.

C++11有哪些新特性

1、C++11 最常用的新特性如下:

  • auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
    nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
    智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
    初始化列表:使用初始化列表来对类进行初始化
    右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
    atomic原子操作用于多线程资源互斥操作
    新增STL容器array以及tuple

编译与底层

1、C++源文件从文本到可执行文件经历的过程?

对于C++源文件,从文本到可执行文件一般需要四个过程

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
  • 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

2、include头文件的顺序以及双引号””和尖括号<>的区别?

  • 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样
  • 对于使用双引号包含的头文件,查找头文件路径的顺序为:
    ① 当前头文件目录
    编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    系统变量 CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
  • 对于使用尖括号包含的头文件,查找头文件的路径顺序为:
    ① 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    ② 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

3、C++的内存管理是怎样的?

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
:使用栈空间存储函数的返回地址、参数、局部变量、返回值
在这里插入图片描述

如何判断内存泄露

  • 内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

内存泄漏的分类
堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

Others

说一下fork函数

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。 pid指进程id?

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1

说一下select

select在使用前,先将需要监控的描述符对应的bit位置1,然后将其传给select,当有任何一个事件发生时,select将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大

请你说一说epoll原理

  • 首先epoll_create创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。
调用顺序:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

IO多路复用,select、poll、epoll

  • IO多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
  • select:是最初解决IO阻塞问题的方法。用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。
    存在的问题:
    ① 内置数组的形式使得select的最大文件数受限于FD_SIZE;
    ② 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
    轮寻排查当文件描述符个数很多时,效率很低;
  • poll:通过一个可变长度的数组解决了 select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态poll解决了select重复初始化的问题。轮寻排查的问题未解决。
  • epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
    epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值