面试之CPP基础知识-3


#C++中有几种类型的new

在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new

(1)plain new 如果分配空间失败,会抛出异常std::bad_alloc。 定义如下:

1
2
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
using namespace std;
int main()
{
	try
	{
		char *p = new char[10e11];
		delete p;
	}
	catch (const std::bad_alloc &ex)
	{
		cout << ex.what() << endl;
	}
	return 0;
}
//执行结果:bad allocation

(2)nothrow new

nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:

1
2
void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
using namespace std;

int main()
{
	char *p = new(nothrow) char[10e11];
	if (p == NULL) 
	{
		cout << "alloc failed" << endl;
	}
	delete p;
	return 0;
}
//运行结果:alloc failed

(3)placement new

这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:

1
2
void* operator new(size_t,void*);
void operator delete(void*,void*);

使用placement new需要注意两点:

  • palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组

  • placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>
using namespace std;
class ADT{
	int i;
	int j;
public:
	ADT(){
		i = 10;
		j = 100;
		cout << "ADT construct i=" << i << "j="<<j <<endl;
	}
	~ADT(){
		cout << "ADT destruct" << endl;
	}
};
int main()
{
	char *p = new(nothrow) char[sizeof ADT + 1];
	if (p == NULL) {
		cout << "alloc failed" << endl;
	}
	ADT *q = new(p) ADT;  //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
	//delete q;//错误!不能在此处调用delete q;
	q->ADT::~ADT();//显示调用析构函数
	delete[] p;
	return 0;
}
//输出结果:
//ADT construct i=10j=100
//ADT destruct

#C++的异常处理的方法

通过在代码中使用 try-catch 块,程序可以在遇到异常时跳转到异常处理代码。

  • 避免程序崩溃和数据损坏。
  • 使程序的调试和维护更加容易。
  • 提高代码的可读性和可维护性。

https://www.zhihu.com/tardis/zm/art/610549350?source_id=1003


在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:

  • 数组下标越界
  • 除法计算时除数为0
  • 动态分配空间时空间不足
  • ...

如果不及时对这些异常进行处理,程序多数情况下都会崩溃。

(1)try、throw和catch关键字

C++中的异常处理机制主要使用trythrowcatch三个关键字,其在程序中的用法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;
int main()
{
    double m = 1, n = 0;
    try {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;  //抛出int型异常
        else if (m == 0)
            throw - 1.0;  //拋出 double 型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) {
        cout << "catch (double)" << d << endl;
    }
    catch (...) {
        cout << "catch (...)" << endl;
    }
    cout << "finished" << endl;
    return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished

代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(...)的方式捕获任何异常(不推荐)。 当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。(注意,匹配不是最佳匹配,如果基类异常在派生类异常之前,则会一直匹配基类异常)

(2)函数的异常声明列表

有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:

1
int fun() throw(int,double,A,B,C){...};

这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常

(3)C++标准异常类 exception

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的,如下图所示

  • bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常,例如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <typeinfo>
using namespace std;

class A{
public:
  virtual ~A();
};
 
using namespace std;
int main() {
	A* a = NULL;
	try {
  		cout << typeid(*a).name() << endl; // Error condition
  	}
	catch (bad_typeid){
  		cout << "Object is NULL" << endl;
  	}
    return 0;
}
//运行结果:bject is NULL
  • bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
  • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常
  • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

#形参与实参的区别?

  1. 形参只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。

  2. 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,实参应该具有确定的值, 以便把这些值传送给形参。

  3. 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。

  4. 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。

  5. 当形参和实参不是指针/引用类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

#值传递、指针传递、引用传递的区别和效率

  1. 值传递:形参会拷贝整个实参,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)

  2. 指针传递:形参会拷贝实参的地址。(传值,传递的是地址值)

  3. 引用传递:形参等同于实参。(传地址)

  4. 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

#静态变量什么时候初始化

静态局部变量和局部对象都在函数首次执行到对象定义时进行初始化; 全局变量和对象在程序启动时进行初始化。

#什么是类的继承?

  1. 类与类之间的关系

has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性,即一个类的成员属性是另一个已经定义好的类;

use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;

is-A,继承关系,关系具有传递性;子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;

#从汇编层去解释一下引用

1
2
3
4
5
6
7
8
9
9:      int x = 1;

00401048  mov     dword ptr [ebp-4],1

10:     int &b = x;

0040104F   lea     eax,[ebp-4]

00401052  mov     dword ptr [ebp-8],eax

x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。

lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器

mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址

ebp-8中上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?所以从汇编层次来看,的确引用是通过指针来实现的。

#delete p、delete [] p、allocator都有什么作用?

  • delete p:用于释放使用new运算符动态分配的单个对象的内存。delete p会调用对象的析构函数,并释放对象占用的内存。
  • delete [] p:用于释放使用new[]运算符动态分配的数组的内存。delete [] p会按照数组元素的逆序调用每个元素的析构函数,并释放数组占用的内存。
  • allocator:是C++标准库中的一个类模板,用于动态分配和释放内存,可以用于分配单个对象或数组。

#new和delete的实现原理, delete是如何知道释放内存的大小的?

  • new先调用operator new分配内存,然后调用构造函数初始化内存,返回该内存的指针
  • delete先调用析构函数再调用operator delete释放指针指向的内存
  • new[]先调用operator new[]分配内存,然后在内存块的首地址放入数组大小,然后依次调用数组元素的构造函数来初始化每个对象,返回该内存的指针。
  • delete[]获得内存首地址的数组大小,调用析构函数后,再调用operator delete 释放内存。

https://blog.csdn.net/passion_wu128/article/details/38966581

#malloc申请的存储空间能用delete释放吗?

混合使用mallocdelete会导致未定义的行为,可能引发内存泄漏或其他问题。建议malloc配套free使用。

#malloc与free的实现原理?

  1. malloc的实现原理:

    • malloc函数通过调用底层的操作系统函数(如brkmmap)来请求一块指定大小的内存空间。
    • 操作系统会在进程的地址空间中找到足够大小的连续内存块,并将其起始地址分配给malloc函数。
    • malloc函数返回起始地址给变量。
  2. free的实现原理:

    • free函数接收一个指针作为参数,该指针指向通过malloc分配的内存块的起始地址。
    • free函数将释放该内存块,并将它标记为可重新使用。
    • 内存管理子系统(如堆管理器)会将这块内存标记为可供后续的malloc调用使用
  • from GPT3.5

1、 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;

2、 brk是将「堆顶」指针向高地址移动,获得新的内存空间,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;

3、 malloc小于128k的内存,使用brk分配内存,将「堆顶」指针往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

4、 malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

#malloc、realloc、calloc的区别

  1. malloc函数

需要指定需要的内存空间,其内的值是随机的。

1
2
void* malloc(unsigned int num_size);
int *p = (int*)malloc(20*sizeof(int));申请20int类型的空间
  1. calloc函数

省去了人为空间计算;申请的空间的值是初始化为0

1
2
void* calloc(size_t n,size_t size);
int *p = (int*)calloc(20, sizeof(int));
  1. realloc函数

给动态分配的空间分配额外的空间,用于扩充容量

1
void realloc(void *p, size_t new_size);

#类成员初始化方式?为什么用成员初始化列表会快一些?

  1. 构造函数初始化,通过在构造函数体内进行赋值初始化;
  2. 列表初始化,在冒号后使用初始化列表进行初始化。
  3. 声明时初始化(也称就地初始化,c++11后支持),声明时直接初始化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class A
{
public:
    int a;
    int b = 1; // 声明时初始化 
    // 列表初始化
    A(int a_):a(a_){}
    // 构造函数初始化
    A(int _b) {b = _b;}
};

成员变量的初始化顺序是:

声明时初始化->初始化列表->构造函数初始化

因为列表初始化不需要再进行一次赋值,不会构建临时变量。 https://bbs.huaweicloud.com/blogs/281096

#构造函数的执行顺序 ?

一个派生类构造函数的执行顺序如下:

  1. 先执行静态成员的构造函数~~,如果静态成员只是在类定义中声明了,而没有定义,是不用构造的。必须初始化后才执行其构造函数。~~
  2. 任何虚拟继承基类的构造函数按照它们被继承的顺序构造(不是初始化列表中的顺序)
  3. 任何非虚拟继承基类的构造函数按照它们被继承的顺序构造(不是初始化列表中的顺序)
  4. 任何成员对象的构造函数按照它们声明的顺序构造
  5. 类自己的构造函数

代码

#有哪些情况必须用到成员列表初始化?顺序是什么?

必须使用列表初始化的情况

  • 初始化引用成员
  • 初始化const成员
  • 初始化成员对象
    • 不包含默认构造函数时
  • 基类的构造函数需要参数
    • 不包含默认构造函数时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class OtherClass {
public:
    OtherClass(int value) {
        // 构造函数体
    }
};
class BaseClass {
public:
    BaseClass(int value) {
        // 基类构造函数体
    }
};
class MyClass: public BaseClass {
public:
    MyClass(int value, int& ref) : myConst(value), myRef(ref), myObject(10), BaseClass(value) {
        // 构造函数体
    }

private:
    const int myConst;
    int& myRef;
    OtherClass myObject;
};

成员初始化的顺序

  • 列表初始化的顺序是由先基类,再按照类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

https://blog.csdn.net/lanchunhui/article/details/50987384

#C++中新增了string,它与C语言中的 char *有什么区别吗?它是如何实现的?

  1. 在实现上,std::string 内部通常会使用动态数组来存储字符串,可以动态地分配内存。同时,std::string 还可能使用一些优化技术,如内部缓存和rope等,以提高字符串操作的效率。具体的实现细节可能会因不同的 C++ 编译器和标准库实现而有所不同。

  2. 内存管理:string会自动管理内存,即在使用完成后会自动释放内存。而char *需要手动管理内存。

  3. 安全性:string提供了更多的安全性措施,比如支持多线程安全、内存泄漏检测等。

  4. 字符串操作:std::string 提供了一系列的成员函数,可以方便地进行字符串的拼接、子串提取、查找等操作,而 char* 则需要使用一些 C 语言的字符串操作函数(如 strcatstrchr 等)或者手动进行指针操作。