C++面经(简洁版)

1. 谈谈C和C++的认识

  • C++在C的基础上添加类,C是一种结构化语言,它的重点在于数据结构和算法
  • C语言的设计首要考虑的是如何通过一个过程,对输入进行运算处理得到输出
  • 而对C++,首先要考虑的是如何构造一个对象,通过封装一下行为和属性,通过一些操作将对象的状态信息输出

2. 对象

2.1 什么是面向对象

  • 就是一种对现实世界的理解和抽象,将问题转换成对象进行解决需求处理的思想。

2.2 如何限制一个类对象只能在堆上分配空间 

2.2.1 方法一: 使用delete禁掉默认析构函数 

#include <iostream>
using namespace std;
 
 
class HeapOnly
{
public:
	HeapOnly()
	{
		_str = new char[10];
	}
 
	~HeapOnly() = delete;
 
	void Destroy()
	{
		delete[] _str;
 
		operator delete(this);
	}
 
private:
	char* _str;
	//...
};
 
int main()
{
	HeapOnly* ptr = new HeapOnly;
	ptr->Destroy();
	return 0;
}
  •  只能在堆上申请空间,可以直接使用delete把析构函数禁掉就行了 
  • 自己再实现一个释放空间的函数(Destory)

 2.2.2  方法二: 将析构函数私有化 

#include <iostream>
#include <stdlib.h>
using namespace std;
class HeapOnly
{
public:
	/*static void Delete(HeapOnly* p)
	{
		delete p;
	}*/
	void Delete()
	{
		delete this;
	}
 
private:
	// 析构函数私有
	~HeapOnly()
	{
		cout << "~HeapOnly()" << endl;
	}
private:
	int _a;
};
 
int main()
{
	//HeapOnly hp1;// error
	//static HeapOnly hp2;// error
 
	HeapOnly* ptr = new HeapOnly;
	ptr->Delete();
	return 0;
}
  • 只能在堆上申请空间,可以直接将析构函数私有化
  • 自己再实现一个调用析构函数的函数(Delete)

2.2.3 方法三: 将构造函数私有化(禁掉拷贝) 

#include <iostream>
#include <stdlib.h>
using namespace std;
class HeapOnly
{
public:
	// 提供一个公有的,获取对象的方式,对象控制是new出来的
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
 
	// 防拷贝
	HeapOnly(const HeapOnly& hp) = delete;
	HeapOnly& operator=(const HeapOnly& hp) = delete;
private:
	// 构造函数私有
	HeapOnly()
		:_a(0)
	{}
private:
	int _a;
};
 
int main()
{
	/*HeapOnly hp1;
	static HeapOnly hp2;
	HeapOnly* hp3 = new HeapOnly;
	delete hp3;*/
 
	HeapOnly* hp3 = HeapOnly::CreateObj();
	//HeapOnly copy(*hp3);
 
	delete hp3;
 
	return 0;
}
  • 只能在堆上申请空间,可以直接使用构造函数私有化
  • 并禁止掉2个拷贝构造和赋值重载
  • 自己再实现提供一个公有的,获取对象的方式*(CreatObj),返回值是static

注意: 如果需要派生该类,就不能将构造函数设为私有或删除。如果需要限制派生类对象只能在堆上构造,可以在派生类中重载 new 和 delete 运算符,强制所有派生类对象都通过堆来创建和销毁 

2.3 如何限制一个类对象只能在栈上分配空间

2.3.1  方法一: 构造函数私有化(禁掉new)  

#include <iostream>
#include <stdlib.h>
using namespace std;
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		StackOnly st;
		return st;
	}
 
	// 不能防拷贝
	//StackOnly(const StackOnly& st) = delete;
	//StackOnly& operator=(const StackOnly& st) = delete;
	void* operator new(size_t n) = delete;
private:
	// 构造函数私有
	StackOnly()
		:_a(0)
	{}
private:
	int _a;
};
 
int main()
{
	/*StackOnly st1;
	static StackOnly st2;
	StackOnly* st3 = new StackOnly;*/
 
	StackOnly st1 = StackOnly::CreateObj();
 
	// 拷贝构造
	static StackOnly copy2(st1); // 不好处理,算是一个小缺陷
	//StackOnly* copy3 = new StackOnly(st1);
 
	return 0;
}
  • 只能在堆上申请空间,可以直接使用构造函数私有化
  • 并且使用delete禁止掉new
  • 自己再实现提供一个公有的,获取对象的方式*(CreatObj),返回值是static

2.4 private  protected public

第一: private,public,protected的访问范围: 

类中的函数 友元函数 子类函数 类的对象 

  • private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.

  • protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问

  • public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

 第二:类的继承后方法属性变化:

  • 使用private继承,父类的所有方法在子类中变为private;
  • 使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
  • 使用public继承,父类中的方法属性不发生改变;

2.5 拷贝构造函数参数中为什么有时候要加const

class test{
  public:
    test(const test& a)
    {
        cout<<"拷贝构造函数"<<endl;
    }
};

test get_test()
{
    test a;
    return a;
}
int main()
{    
   test  b=get_test();	
}
  • 有时候时候需要加上const修饰符的原因是为了确保被拷贝的对象在拷贝过程中不会被修改

  • 可以提高代码的可靠性安全性(特别是在多线程的情况下)

3.多态

3.1 什么是多态 

就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会

产生出不同的状态

  • 派生类对象的地址可以赋值给基类指针。对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;
  • 当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,
  • 如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)

静态多态(编译阶段确定)

  • 函数重载 和 函数模板 的使用

 动态多态(运行阶段确定)

  • 派生类 和 虚函数 的使用

3.2 继承和多态区别与联系?

  • 不同点: 继承是子类使用父类的方法,而多态则是父类使用子类的方法。 
  • 继承是为了重用代码,有效实现代码重用,减少代码冗余
  • 多态是一种接口继承,增强接口的扩展性

3.3 什么是重载、重写(覆盖)、重定义(隐藏)? 

  • 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
  • 重写(覆盖):
    两个函数分别在基类派生类的作用域
    函数名/参数/返回值都必须相同(协变除外) -> 简称三同
    特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
    特殊点2: 如果返回值不同,则必须是父类的指针或引用
  •  重定义(隐藏)
    两个函数分别在基类派生类的作用域
    函数名相同
    两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)

 3.4 多态的实现原理? 

  •  每一个类中都有一个vfptr,它是一张虚函数的表,本质就是一个函数指针数组
  • 多态调用:运行时去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)

 3.5 产生多态的2个条件

  • 父类的指针或引用去调用
  • 满足虚函数的重写

3.6 inline函数可以是虚函数吗?

  • 不可以,但是inline只是一个建议,当一个函数是虚函数以后,多态调用中,inline会直接失效

3.7 静态成员可以是虚函数吗? 

  • 不可以 ,因为 静态成员函数没有this指针 ,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表

3.8  构造函数可以是虚函数吗

  • 不可以,virtual函数为了实现多态,运行时去虚表中找对应虚函数进行调用
  • 而对象中虚表指针都是构造函数初始化列表阶段才初始化的
  • 所以构造函数的虚函数是没有意义的

 3.9 析构函数可以是虚函数吗

  • 可以,建议基类的析构函数定义成虚函数
  • 析构函数名都会被处理成destructor,所以这里析构函数完成了虚函数重写
  • 当基类的指针指向派生类对象的时候,发生多态,然后基类和派生类就会统一析构
    如果不将基类的析构函数定义为虚函数的话,那么派生类的析构函数就无法执行。

3.10  拷贝构造 和 operator=可以是虚函数?

  •  拷贝构造不可以,拷贝构造也是构造函数,同上
  • operator=可以但是没有什么实际价值

3.11  对象访问普通函数快还是虚函数更快?

  • 不构成多态, 是一样快的。
  • 构成多态,则调用的普通函数快
  • 因为构成多态,运行时调用虚函数需要到虚函数表中去查找

3.12 虚函数是在什么阶段生成的,存在哪的?

  • 编译阶段就生成好的,存在代码段(常量区)

3.13 什么是抽象类?抽象类的作用? 

  • 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类
  • 抽象类强制重写了虚函数,另外抽象类体现出了 接口继承关系

4. 内存管理

4.1 C++的内存分配

  • 堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。

  • 栈区:由编译器自动分配和释放,存放为运行函数分配的局部变量,函数参数,返回数据,返回地址等,其操作类似于数据结构中的栈。

  • 全局区(静态区static):存放全局变量,静态变量,常量。结束后由系统释放

  • 常量区(字符串常量区):存放常量字符串,程序结束后有系统释放

  • 代码区:存放函数体(类成员函数和全局区)的二进制代码。

4.2 简述c、C++程序编译的内存分配情况

  • 从静态存储区域分配
  • 在栈上分配
  • 在堆上分配

5. 关键字

5.1 extern 和 static 的区别,什么情况用前者什么情况用后者

  •  extern外部变量: 叫做外部声明,被extern修饰的变量,会告诉编译器这个变量的定义需要再其他文件中查找
  • static静态变量: 被static修饰的变量,将会被改变生命周期,直到程序运行结束的时候,系统才会释放,所以无需手动释放

5.2 声明和定义的区别

  • 声明是告诉编译器名字的存在

  • 而定义是为名字分配内存并实现其功能

  • 在使用变量或函数之前,必须先进行声明或定义。 

5.4 strcpy和memcpy的区别 

  • strcpy和memcpy都是在C语言和C++语言中用于复制内存块的函数,但它们在使用和效率上有所不同。

  • 但他们两者在拷贝时,如果源地址不够时,都会出现内存越界和缓冲区溢出问题

  • strcpy用于将一个以null结尾的字符串从源地址复制到目标地址。它会复制整个字符串,包括null终止符,直到遇到null为止 
  • memcpy用于将一段内存块从源地址复制到目标地址,可以复制任意长度的内存块,而不仅限于字符串,且不关心null 
  • 但是strcpy具有更简单的语法和更高的可读性,因此在处理字符串时,通常首选strcpy函数

5.5 关于类模板是否可以定义虚函数 

  • 类模板可以定义成虚函数,但是需要注意一些细节。
    首先,需要明确的是,类模板本身是一个模板,不能直接定义为虚函数类模板的实例化才能被定义为虚函数。

 6. 运算操作符

6.1 x=x+1,x+=1,x++哪个效率高

  • 在大多数情况下,这两者是差不多的,编译器都会转换成相同的机器码,这意味着它们在程序运行时具有相同的性能和速度
  • 但是由于x++需要创建一个临时变量来保存x的旧值,所以它就会比其他两者的效率稍微慢点,但是由于CPU处理数据的速度太快了,所以可以忽略不记

7.编译内存相关

7.1 C++程序编译过程

编译过程分为四个过程:编译(编译预处理编译、优化),汇编链接 

  • 预处理:头文件的展开,宏替换,去注释
  • 编译:把C/C++语言变成汇编代码;
  • 汇编:通过汇编变成以.o结尾的目标二进制文件(不可执行)
  • 链接:多个目标文件连接库进行链接的,从而生成可执行的程序 .exe 文件。

 7.2 栈和堆的区别

  • 申请方式:栈是系统自动分配,堆是程序员主动申请
  • 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的

  • 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片
  • 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制

7.3  全局变量定义在头文件中有什么问题

  • 如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。 

7.4  内存对齐

什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?

内存对齐编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中

内存对齐的原则:

  • 结构体变量的首地址 = min(最宽基本类型大小,对齐基数)的整除
  • 其他成员的地址偏移量 = min(成员大小,对齐基数)的整数倍 + 填充字节
  • 总大小 = min(最宽基本类型大小,对齐基数) + 填充字节

进行内存对齐的原因:(主要是硬件设备方面的问题)

  • 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
  • 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作
  • 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间
  • 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
  • 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。

7.5  类的大小 

说明:类的大小是指类的实例化对象的大小,用 sizeof 对类型名操作时,结果是该类型的对象的大小。

计算原则:

  • 遵循结构体的对齐原则
  • 与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
  • 虚函数对类的大小有影响,是因为虚函数表指针的影响。
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响。
  • 空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。

 7.6 什么是内存泄漏

  • 内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存

7.7  智能指针有哪几种?智能指针的实现原理?

  •  unique_ptr: 禁掉了拷贝构造和赋值重载

  • shared_ptr: 共同管理一段空间,引用计数 记录管理者 

  • weak_ptr: 为了解决shared_ptr循环引用的问题,是它的nextprev不增加计数 

 8. 语言对比

8.1 C++11 引入了什么新增加的内容

8.1.0 智能指针

  • auto_ptr  资源管理权转移,不负责任的拷贝,会导致被拷贝对象悬空
  • unique_ptr: 禁掉了拷贝构造和赋值重载

  • shared_ptr: 共同管理一段空间,引用计数 记录管理者 

  • weak_ptr: 为了解决shared_ptr循环引用的问题,是它的nextprev不增加计数 

8.1.1 auto类型推导

  • auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。

 8.1.2 lambda表达式

[捕捉列表](参数列表)mutable->返回值类型{函数体实现}

  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时参数列表不可省略 (即使参数为空)。

8.1.3 右值引用 

不能取地址的叫常量,只能用右值引用

    // 对右值的左值引用
	// double& r1 = 1.1 + 2.2;// error
	const double& r1 = 1.1 + 2.2;
 
	// 对左值的右值引用
	//int&& rr5 = b;// error
	int&& rr5 = move(b);

出现的原因 : 

  • 左值引用: 中的引用返回,只能解决出了作用域还存在的
    且无法解决string中的to_string的返回值,以及有些函数返回值是二维数的问题

具体使用是:

  •  右值引用+ 移动构造/移动赋值 (用swap, 交换将亡值)

8.2 C和C++的区别

  • 语言自身:
     C 语言是面向过程的编程,它的主要特点是函数
    C++ 是面向对象的编程, 它的主要特点是

  • 应用领域:
    C 语言主要用于嵌入式领域,驱动开发等与硬件直接打交道的领域
    C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域

  • C++ 对 C 的“增强”,表现在以下几个方面:
    类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)

8.3 面向对象

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含成员变量成员方法

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。

  • 继承:子类使用父类的方法

  • 多态:去完成某个行为,当不同的对象去完成时会产生出不同的状态

9. 关键字库函数 

9.1 sizeof 和 strlen 的区别

  1. strlen 是头文件 中的函数sizeof 是 C++ 中的运算符
  2. strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。
    而 sizeof 测量的是字符数组的分配大小
  3. sizeof会计算\0

9.2  explicit 的作用 

#include <iostream>
#include <cstring>
using namespace std;

class A
{
public:
    int var;
    explicit A(int tmp)
    {
        var = tmp;
        cout << var << endl;
    }
};
int main()
{
    A ex(100);
    A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
    return 0;
}
  • 作用: 避免编译器进行隐式类型转换

9.3  static 的作用

作用: 定义静态变量,静态函数

  • 保持变量内容持久:改变了变量的生命周期
  • 隐藏:static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得被static修饰的变量和函数只能在当前文件中访问
  • 类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数

9.3.1 static在类中使用的注意事项(定义、初始化和使用 )

静态成员变量

  • 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。

  • 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。

静态成员函数

  • 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。
  • 静态成员函数能做为类作用域的全局函数
  • 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。 

 9.4 static全局变量和普通全局变量的异同

相同点: 

  • 存储方式:普通全局变量和 static 全局变量都是静态存储方式

不同点:

  • 作用域: 普通全局变量的作用域是整个源程序,而static全局变量只在当前文件有效,且只能初始化一次

9.5 const 作用及用法

9.5.1 作用:

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
  •  const 修饰函数参数,使得传递过来的函数参数的值不能改变
  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。

9.5.2 在类中的用法:

const 成员变量:

  • const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化
  • const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值

const 成员函数:

  • 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
  • 不能调用非常量成员函数,以防修改成员变量的值

9.6 define 和 const 的区别

区别

  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值
  • 安全性:define 定义的宏常量没有数据类型,所以不会进行类型安全检查,而const 定义的常量是有类型的,所以会进行类型安全检查
  • 内存占用:define 定义的宏常量,是进行替换的,所以内存中有多个备份会占用的是代码段的空间;const 定义的常量占用静态存储区的空间,因为整个程序运行过程中只有一份
  • 调试:define不支持调试,而const支持调试,因为define在预编译阶段就已经进行替换

const 的优点:

  • 有数据类型,在定义式可进行安全性检查。
  • 可调式。
  • 占用较少的空间。

9.7  define 和 typedef 的区别

  • define不进行类型安全的检查,而typedef会进行类型安全的检查
  • define没有作用域的限制,而typedef有作用域的限制

9.8 用宏实现比较大小,以及两个数中的最小值 

#include <iostream>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;

int main ()
{
    int var1 = 10, var2 = 100;
    cout << MAX(var1, var2) << endl;
    cout << MIN(var1, var2) << endl;
    return 0;
}
/*
程序运行结果:
100
10
*/
  • 使用define模拟函数,但是需要记得加()

9.9 inline 作用及使用方法

作用:

  • inline是一个关键字,被inline修饰的函数叫做内联函数,内联函数不会进行函数跳转,而是直接展开(通过反汇编观察),但是具体展不展开还是要看编译器,
  • 这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率
  • 去除函数只能定义一次的限制(内联函数可以在头文件中被定义,并被多个 .cpp 文件 include,而不会有重定义错误)

 使用方法:

  • 类内定义成员函数默认是内联函数
  • 而在类外定义就直接加 inline关键字就行了

9.10 宏定义(define)和内联函数(inline)的区别

  • 二者都是在编译阶段处理的,但是lnline是直接内嵌到目标代码中的,而宏只是一个简单的文本替换
  • 内联函数是真正的函数,而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
  • 宏定义只进行文本替换,不会做各种安全检查。而内联函数是真正的函数,会做各种检查

9.11 new 的作用?

new 是 C++ 中的关键字,用来动态分配内存空间,实现方式如下:

int *p = new int[5]; 

9.12 new 和 malloc 如何判断是否申请到内存?

  • malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针
  • new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bad_alloc 异常

9.13 delete 实现原理?delete 和 delete[] 的区别?

delete 的实现原理:

  • 首先执行该对象所属类的析构函数
  • 进而通过调用 operator delete 的标准库函数来释放所占的内存空间。

delete 和 delete [] 的区别:

  • delete 用来释放单个对象所占的空间,只会调用一次析构函数;
  • delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

9.14 new 和 malloc 的区别,delete 和 free 的区别 

  • 相同点:都是从堆上申请空间,并且需要用户手动释放。

  • 不同点:
    malloc和free是函数,new和delete是操作符
    malloc申请的空间不会初始化,new申请的空间会初始化,即调用构造函数
    malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
    malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型
    malloc申请失败时,返回的是NULL,因此使用时必须判空而new不需要,但是new需要捕获异常

9.15 malloc 的原理?malloc 的底层实现?

malloc 的原理:

  • 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
  • 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现

9.16 C 和 C++ struct 的区别?

  • 一个是自定义数据类型,而另一个是抽象数据类型
  • C语言中的struct不能包含成员函数,C++中的struct可以包含成员函数,区别在于访问控制 

9.17 为什么有了 class 还保留 struct?

  • C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct

9.18 struct 和 union 的区别

// 大小 = 所有变量类型最大的那个 的 整数倍
typedef union
{
    char c[10];
    int i;
    double d; // double 8 字节,按该类型的倍数分配大小
} u22;

// 需要遵循内存对齐规则
typedef struct s2
{
    char c;   // 1 字节
    char cc;  // 1(char)+ 1(char)= 2 字节
    double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;
  • 联合体和结构体都是由若干个数据类型不同的数据成员组成。
    使用时,联合体只有一个有效的成员;
    而结构体所有的成员都有效。

  • 对联合体的不同成员赋值,将会对覆盖其他成员的值
    而对于结构体的对不同成员赋值时,相互不影响。

  • 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小
    结构体分配内存的大小遵循内存对齐原则。

9.19 truct 和 class 区别

  • struct的成员默认是公有的,而类的成员默认是私有的 

  • C语言中的struct不能包含成员函数,C++中的struct和class都可以包含成员函数,
    区别在于访问控制 
  • class可以用于定义模板参数,struct不能用于定义模板参数。

9.20  volatile 的作用?是否具有原子性,对编译器有什么影响?

  • volatile 的作用:
    当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,
    告知编译器不应对这样的对象进行优化。
  • volatile不具有原子性
  • volatile 对编译器的影响:
    使用该关键字后,编译器不会对相应的对象进行优化,应该在真实的物理地址空间中拿数据
    不会将变量从内存缓存到寄存器中
    防止多个线程有可能使用内存中的变量,也有可能使用寄存器中的变量,从而导致程序错误。

9.21 什么情况下一定要用 volatile, 能否和 const 一起使用?

使用 volatile 关键字的场景:

  • 多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;
  • 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。

volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。

在C++多线程中,volatile不具有原子性;无法对代码重新排序实施限制。
能干什么:告诉编译器不要在此内存上做任何优化。如果对内存有只写未读的等非常规操作,如

x=10;
x=20;

编译器会优化为:

x=20;

volatile 就是阻止编译器进行此类优化。

9.22 extern C 的作用?

当 C++ 程序 需要调用 C 语言编写的函数

// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
    int strcmp(const char*, const char*);
}
  • C++ 和 C语言编译函数签名方式不一样
  • extern关键字可以让两者保持统一,这样才能找到对应的函数

9.23 sizeof(1==1) 在 C 和 C++ 中分别是什么结果?

  • C语言没有布尔类型,因此按整数处理,有可能是4字节,也有可能是8字节
  • 而C++有布尔类型,占1字节

10. 类相关

10.1 什么是虚函数?什么是纯虚函数? 

  • 虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。

  • 纯虚函数:纯虚函数在类中声明时,加上 =0;

10.2 虚函数和纯虚函数的区别?

  • 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
  • 定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;

10.3 对虚函数表的理解

  •  虚函数表存放的内容:类的虚函数的地址
  • 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。

  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

10.4 如何禁止构造函数的使用?

  • 为类的构造函数增加 = delete 修饰符,可以达到虽然声明了构造函数但禁止使用的目的。

10.5 什么是类的默认构造函数?

  • 编译器自动生成的,且未提供任何实参的构造函数 -> 默认构造函数

 10.6 如何禁止一个类被实例化

  • 在类中定义一个纯虚函数,使该类成为抽象类,因为不能创建抽象类的实例化对象
  • 构造函数私有化

10.7 为什么用成员初始化列表会快一些?

  • C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前
  • 所以初始化列表在没有进入函数体之前,就将成员变量设为初始值了
  • 总之就是一个进入了函数体,另一个没有进入函数体

10.8 实例化一个对象需要哪几个阶段

  • 1.分配空间  2.初始化  3.赋值

10.9 友元函数的作用

  • 作用: 让普通的成员函数可以访问类中的私有成员和保护成员

10.10 深拷贝和浅拷贝的区别 

  • 深拷贝:该对象和原对象占用不同的内存空间
  • 浅拷贝:该对象和原对象占用同一块内存空间

10.11 如何让类不能被继承?

#include <iostream>

using namespace std;

class Base final
{
};

class Derive: public Base{ // error: cannot derive from 'final' base 'Base' in derived type 'Derive'

};

int main()
{
    Derive ex;
    return 0;
}
  • final关键字修饰的基类 不能被派生类继承 

11. 语言特性

11.0 谈谈STL各个容器

11.1 左值和右值的区别?两者之前如何转换 

#include <iostream>
#include <utility>
using namespace std;
 
int main()
{
	// 左值: 能取地址
	int* p = new int(0);
	int b = 1;
	const int c = 2;
 
	// 对左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
 
	// 右值:不能取地址
	10;
	1.1 + 2.2;
 
	// 对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = 1.1 + 2.2;
 
	// 对右值的左值引用
	// double& r1 = 1.1 + 2.2;// error
	const double& r1 = 1.1 + 2.2;
 
	// 对左值的右值引用
	//int&& rr5 = b;// error
	int&& rr5 = move(b);
 
	return 0;
}
  • 能取地址的叫左值,不能取地址的叫右值
  • 如果对右值使用左值引用,需要加上const
  • 如果对左值使用右值引用,需要是使用move函数

 11.2 什么是野指针和悬空指针?

void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针。
  • 悬空指针: 这个指针指向的地址空间已经被释放了,但这个指针还是指向原来的那段地址空间
void *p; 
// 此时 p 是“野指针”。
  • 野指针: 不确定指向地址空间的指针,和未进行初始化的指针

11.3 C++ 11 nullptr 比 NULL 优势

  • 在使用nullptr表示指针空值时,不需要包含头文件,且能提高代码的健壮健壮性
  • NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知道调那个函数的问题

11.4 指针和引用的区别

11.4.1 从使用场景来说

  • 引用和指针使用场景基本一样,但是链表的链式结构是引用无法代替的,只能使用指针

11.4.2 从语法特性来说

  • 引用在定义时必须初始化,指针没有要求
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,
    而指针可以在任何时候指向任何一个同类型实体。
  • 没有NULL引用但有NULL指针
  • 在sizeof中的含义不同:引用的结果为引用类型的大小
    但指针始终是4或者8字节
  • 引用进行自增操作就相当于实体增加1
    而指针进行自增操作是指针向后偏移一个类型的大小
  • 有多级指针,但是没有多级引用。
  • 访问实体的方式不同引用是编译器自己处理,指针需要显示解引用
  • 引用比指针使用起来相对更安全

11.5 说一说c++的强制类型转换

  • static_cast关键字 -> 隐式类型转换 

  • reinterpret_cast关键字 -> 强制类型转换

  • const_cast关键字->取消变量的const属性

  • dynamic_cast关键字->父类指针 转换 子类指针(保证安全性)

11.6 什么是模板?如何实现?

  • 模板:创建类或者函数的蓝图或者公式,分为函数模板类模板
  • 实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。
    模板参数列表不能为空;模板类型参数前必须使用关键字 class 或者 typename

11.7 函数模板和类模板的区别

  • 实例化: 函数模板实例化后是一个函数,类模板实例化后是一个类

  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化
  • 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用

11.8 什么是可变参数模板?

可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。

  • 模板参数包:表示零个或多个模板参数;
  • 函数参数包:表示零个或多个函数参数。

11.9 什么是模板特化?为什么特化?

  • 原因: 当我们需要针对某些特定类型或条件进行特殊处理时,
    可以使用模板特化来定义专门的实现
  •  概念: 模板参数在某种特定类型下的具体实现。分为函数模板特化类模板特化

 特化分为全特化和偏特化:

  • 全特化:模板中的模板参数全部特例化。
  • 偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。

函数模板只能全特化;而类模板可以全特化,也可以偏特化


 11.10  迭代器的作用

  • 无需知道容器底层原理的情况下,遍历容器中的元素

 11.11 泛型编程如何实现

  • 容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。

  • 迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。

  • 模板:可参考本章节中的模板相关问题。

12 多线程交替打印奇偶数 

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;// 条件变量
	bool ready = true;

	// t1打印奇数
	thread t1([&]() {
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready]() {return !ready; });// 等待线程
				cout << "t1--" << this_thread::get_id() << ":" << i << endl;
				i += 1;
				ready = true;
				cv.notify_one();// 解除线程等待
			}
			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(100));
		}
		});

	// t2打印偶数
	thread t2([&]() {
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready]() {return ready; });
				cout << "t2--" << this_thread::get_id() << ":" << i << endl;
				i += 1;
				ready = false;
				cv.notify_one();
			}
		}
		});

	this_thread::sleep_for(chrono::seconds(3));
	cout << "t1:" << t1.get_id() << endl;
	cout << "t2:" << t2.get_id() << endl;

	t1.join();
	t2.join();

	return 0;
}
  •  cv.wait(lock, [&ready]() {return !ready; });

    ready返回的是false时,这个线程就会阻塞
    阻塞当前线程,并自动调用lock.unlock()允许其他锁定的线程继续执行

  •  cv.notify_one();
    唤醒当前线程并自动调用lock.lock();就只允许自己一个线程执行

13. 单例模式(饿汉模式 && 懒汉模式)

那两种模式都是将构造函数私有化,自己实现一个构造生成一个静态对象

  • 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享

13.1  饿汉模式: 程序启动时就创建一个唯一的实例对象

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return &m_instance;
	}
private:
	// 构造函数私有
	Singleton() {};
 
	// C++11 : 防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
 
	static Singleton m_instance;// 声明
};
 
Singleton Singleton::m_instance;// 定义
  • 优点:简单

  • 缺点:可能会导致进程启动慢,且如果 有多个单例类对象实例启动顺序 不确定。

  • 总结: 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避 免资源竞争,提高响应速度更好

13.2 饿汉模式->多线程下

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 保护第一次,后续不需要加锁
		// 双检查加锁
		if (_pInstance == nullptr)
		{
			unique_lock<mutex> lock(_mtx);
			if (_pInstance == nullptr)
			{
				_pInstance = new Singleton;
			}
		}
 
		return _pInstance;
	}
 
private:
	// 构造函数私有
	Singleton(){};
 
	// C++11
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
 
	static Singleton* _pInstance;
	static mutex _mtx;
};
 
Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx; 
 
int main()
{
	Singleton::GetInstance();
	Singleton::GetInstance();
 
	return 0;
}

 13.3 懒汉模式 : 第一次使用对象再创建实例对象

  • 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取 文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,
  • 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
#include <iostream>
#include <stdlib.h>
using namespace std;
class MemoryPool
{
public:
	static MemoryPool* GetInstance()
	{
		if (_pinst == nullptr) {
			_pinst = new MemoryPool;
		}
 
		return _pinst;
	}
 
	void* Alloc(size_t n)
	{
		void* ptr = nullptr;
		// ....
		return ptr;
	}
 
	void Dealloc(void* ptr)
	{
		// ...
	}
 
	// 实现一个内嵌垃圾回收类    
	class CGarbo {
	public:
		~CGarbo()
		{
			if (_pinst)
				delete _pinst;
		}
	};
 
private:
	// 构造函数私有化
	MemoryPool()
	{
		// ....
	}
 
	char* _ptr = nullptr;
	// ...
 
	static MemoryPool* _pinst; // 声明
};
 
// 定义
MemoryPool* MemoryPool::_pinst = nullptr;
 
// 回收对象,main函数结束后,他会调用析构函数,就会释放单例对象
static MemoryPool::CGarbo gc;
 
int main()
{
	void* ptr1 = MemoryPool::GetInstance()->Alloc(10);
	MemoryPool::GetInstance()->Dealloc(ptr1);
}
  • 优点: 有控制顺序, 不影响启动速度
  • 缺点: 相对复杂, 存在线程安全问题

13.4 单例对象释放问题:

  1. 一般情况下,单例对象不需要释放的。因为一般整个程序运行期间都可能会用它。单例对象在进程正常结束后,也会资源释放。
  2. 有些特殊场景需要释放,比如单例对象析构时,要进行一些持久化(往文件、数据库写)操作。

原文出处: 整理的C++面经(较全)-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/579532.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Node.js -- 包管理工具

文章目录 1. 概念介绍2. npm2.1 npm 下载2.2 npm 初始化包2.3 npm 包(1) npm 搜索包(2) npm 下载安装包(3) require 导入npm 包的基本流程 2.4 开发依赖和生产依赖2.5 npm 全局安装(1) 修改windows 执行策略(2) 环境变量Path 2.6 安装包依赖2.7 安装指定版本的包2.8 删除依赖2.…

jenkins教程

jenkins 一、简介二、下载安装三、配置jdk、maven和SSH四、部署微服务 一、简介 Jenkins是一个流行的开源自动化服务器&#xff0c;用于自动化软件开发过程中的构建、测试和部署任务。它提供了一个可扩展的插件生态系统&#xff0c;支持各种编程语言和工具。 Jenkins是一款开…

PotatoPie 4.0 实验教程(27) —— FPGA实现摄像头图像拉普拉斯边缘提取

拉普拉斯边缘提取有什么作用&#xff1f; 拉普拉斯边缘检测是一种常用的图像处理技术&#xff0c;用于检测图像中的边缘和边界。它的主要作用包括&#xff1a; 边缘检测&#xff1a;拉普拉斯算子可以帮助检测图像中的边缘&#xff0c;即图像中亮度快速变化的位置。这些边缘通常…

前端HTML5学习2(新增多媒体标签,H5的兼容性处理)

前端HTML5学习2新增多媒体标签&#xff0c;H5的兼容性处理&#xff09; 分清标签和属性新增多媒体标签新增视频标签新增音频标签新增全局属性 H5的兼容性处理 分清标签和属性 标签&#xff08;HTML元素&#xff09;和属性&#xff0c;标签定义了内容的类型或结构&#xff0c;而…

RocketMQ 消息重复消费

现象 触发消息后&#xff0c;在1s内收到了两次消息消费的日志。 消息消费日志重复&#xff0c;reconsumeTimes0&#xff0c;主机实例也不同&#xff0c;说明是同一条消息被消费了两次 分析 生产者发送消息的时候使用了重试机制&#xff0c;发送消息后由于网络原因没有收到MQ…

永磁同步电机PMSM负载状态估计simulink模型

永磁同步电机PMSM负载状态估计simulink模型&#xff0c;龙伯格观测器&#xff0c;各种卡尔曼滤波器&#xff0c;矢量控制&#xff0c;坐标变换&#xff0c;永磁同步电机负载转矩估计&#xff0c;pmsm负载转矩测量&#xff0c;负载预测&#xff0c;转矩预测的matlab/simulink仿真…

【C++】---STL容器适配器之queue

【C】---STL容器适配器之queue 一、队列1、队列的性质 二、队列类1、队列的构造2、empty()3、push()4、pop()5、size()6、front()7、back() 三、队列的模拟实现1、头文件&#xff08;底层&#xff1a;deque&#xff09;2、测试文件3、底层&#xff1a;list 一、队列 1、队列的…

【NR RedCap】Release 18标准中对5G RedCap的增强

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

R语言贝叶斯方法在生态环境领域中的应用

贝叶斯统计已经被广泛应用到物理学、生态学、心理学、计算机、哲学等各个学术领域&#xff0c;其火爆程度已经跨越了学术圈&#xff0c;如促使其自成统计江湖一派的贝叶斯定理在热播美剧《The Big Bang Theory》中都要秀一把。贝叶斯统计学即贝叶斯学派是一门基本思想与传统基于…

使用微信开发者工具模拟微信小程序定位

哈喽&#xff0c;各位同僚们&#xff0c;我们平时在测试微信小程序的时候&#xff0c;如果小程序中有获取定位或者地图的功能&#xff0c;测试场景中常常需要去模拟不同的位置&#xff0c;例如我们模拟在电子围栏的外面、里面和边界区域等。那么&#xff0c;我们如何在模拟微信…

[笔试训练](八)

目录 022&#xff1a;求最小公倍数 023&#xff1a;数组中的最长连续子序列 024&#xff1a;字母收集 022&#xff1a;求最小公倍数 求最小公倍数_牛客题霸_牛客网 (nowcoder.com) 题目&#xff1a; 题解&#xff1a; 求最小公倍数公式&#xff1a;lcm(a,b)a*b/gcd(a,b)&am…

创建springboot项目的问题

IDEA搭建spring boot时报错Error: Request failed with status code 400 Could not find artifact org.springframework.boot:spring-boot-starter-parent:pom:3.2.5.RELEASE in alimaven (http://maven.aliyun.com/nexus/content/repositories/central/) 原因是父级依赖的版本…

Web前端开发 小实训(一) 成绩分类统计

用于学生web前端开发课程实训练习&#xff0c;掌握基本语法和数据类型 实训目的 使用分支语句&#xff0c;完成分数统计与等级对比,通过输入框输入分数&#xff0c;可以根据分数多少划分等级。 参考思路&#xff1a; 分析题目&#xff1a;根据输入分数进行等级划分。 操作过…

yolov8 dll 编译

1. 每次用yolo v8 都要用python &#xff0c;对于我这种写软件的太不方便了&#xff0c;下面尝试编译dll 调用, 我已经有做好的模型.best.pt 参考视频方法: yolov8 TensorRT C 部署_哔哩哔哩_bilibili 【yolov8】tensorrt部署保姆级教程&#xff0c;c版_哔哩哔哩_bilibili 需…

怎么做视频二维码更方便?在线一键生成视频活码二维码

现在经常会发现很多的二维码可以用来展示视频内容&#xff0c;通过这种方式来实现视频的快速分享与传播。二维码是一种成本低传播快的内容传播方式&#xff0c;很多的内容都可以通过生成二维码的方式来分享给其他人&#xff0c;可以同时扫描相同的二维码来获取内容&#xff0c;…

电脑的无用设置功能(建议关闭)

目录 1、传递优化 ​2、常规​ 3、电源 1、传递优化 2、常规3、电源

Vue2和Vue3的生命周期对比

beforeCreate 、created 两个钩子被setup()钩子来替代。 所有生命周期前面加了on

【Kotlin】Channel简介

1 前言 Channel 是一个并发安全的阻塞队列&#xff0c;可以通过 send 函数往队列中塞入数据&#xff0c;通过 receive 函数从队列中取出数据。 当队列被塞满时&#xff0c;send 函数将被挂起&#xff0c;直到队列有空闲缓存&#xff1b;当队列空闲时&#xff0c;receive 函数将…

PotatoPie 4.0 实验教程(41) —— FPGA实现RISC-V 扩展 GPIO UART Timer功能

TD工程介绍 我们提供的TD工程里的RISC-V核默认就开启了GPIO UART扩展&#xff0c;可以看到还有SPI和I2C扩展。因此后面的实验中TD的工程我们基本不怎么修改TD的内容&#xff0c;只需要修改TD工具中Soc_Top.v文件中的TCM0_INITFILE为FD生成的固件名称即可&#xff0c;主要修我以…

数据集市的详细建设方案!

▶ 什么是数据集市&#xff1f; 数据集市是处理单一事务的数据仓库的子集。它们通常由单个业务部门构建和管理。由于它们是面向主题的&#xff0c;因此通常仅从少数来源获取数据&#xff0c;这些来源可能是内部操作系统&#xff0c; 数据湖&#xff0c;一个集中的 数据存储库&a…
最新文章