面经总结C++基础知识(已成功上岸字节)

1、C++基础知识

1.1、指针
1、指针函数和函数指针
1)函数指针

​ 本质是一个指针,该指针指向这个函数 int (*fun)(int x,int y);

​ 函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。函数指针与数据项相似,函数也有地址,也就是说在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。一个函数名就是一个指针,它指向函数的代码,一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用,函数指针还运行将函数作为变元传递给其他函数。

int add(int x,int y){
    return x+y;
}
int sub(int x,int y){
    return x-y;
}
//函数指针
int (*fun)(int x,int y);
int main()
{
    //第一种写法
    fun = add; cout << "(*fun)(1,2) = " << (*fun)(1,2) ; //3
	//第二种写法
    fun = ⊂ cout << "(*fun)(5,3) = " << (*fun)(5,3)  << fun(5,3); //2 2
    return 0;
}
2)指针函数

​ 返回一个指针的函数,本质是一个函数,该函数的返回值是一个指针 int *fun(int x,int y);

typedef struct _Data{
    int a;
    int b;
}Data;
//指针函数
Data* f(int a,int b){
    Data * data = new Data;
    data->a = a;
    data->b = b;
    return data;
}
int main()
{
    //调用指针函数
    Data * myData = f(4,5);
    cout << "f(4,5) = " << myData->a << myData->b; //f(4,5) = 4 5
    return 0;
}
3)举个例子
  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。

  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。

  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。

  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

2、常量指针和指针常量
  • 指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。

  • 常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。

3、野指针
	都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
1)野指针的概念
  • 野指针指向一个已删除的对象或不可用的地址的指针。
  • 指针变量中的值是非法的内存地址,进而形成野指针。
  • 野指针不是NULL指针,是指向不可用内存地址的指针。
2)出现原因:
  • 指针定义时未被初始化;程序未对指针进行初始化,会随机指向一个区域,除了static修饰的指针;
  • 指针被释放时没有置空;指针指向的内存空间在free()和delete()释放后,没有进行置空操作的话,就会称为一个野指针;
  • 指针操作超越变量作用域;不要返回指向栈内存的指针或者引用,栈内存在函数结束的时候就会自动被释放;
3)解决或避免:

​ delete后置为NULL,新建指针时判断是否为NULL,不是则释放并置为NULL,尽量不使用超出作用范围的指针

4)野指针:delete 指针后为什么需要置为NULL?

​ 首先delete指针只是编译器释放该指针所指向的内存空间(该空间可以给其他变量使用),而不会删除这个指针本身。这可能会导致后续申请指针时,系统新建的指针指向的地址可能会跟delete掉的指针相同,此时如果修改delete掉的指针的内容就会导致对新建的指针内容的修改。

​ 所以为了防止这种情况的发生,需要delete掉后立即置为NULL(避免变成野指针),同时在新建指针的时候需要判断新建的指针是否为NULL,为NULL才是申请成功。

​ 对null的delete可以无数次,因为delete会直接跳过NULL

​ 原文链接:https://blog.csdn.net/weixin_42067304/article/details/108451031

4、悬空指针
1)悬空指针概念

​ 悬空指针,指针最初指向的内存已经被释放了的一种指针。

int main(void) { 
 int * p = nullptr;
 int* p2 = new int;
 
 p = p2;
 delete p2; 
}

​ 此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

2)产生原因及解决办法:

​ 指针free或delete之后没有及时置空 => 释放操作后立即置空。

5、指针加减需要注意什么?

​ 指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果

1.2、指针和引用
1、在传递函数参数时,什么时候使用指针,什么时候使用引用?

总结

  • 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的。
  • 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小。
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式。
1)使用引用参数的主要原因有两个:
  • 程序员能修改调用函数中的数据对象

  • 通过传递引用而不是整个数据–对象,可以提高程序的运行速度

2)一般的原则:
  • 对于使用引用的值而不做修改的函数:

  • 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;

  • 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;

  • 如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;

  • 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);

3)对于修改函数中数据的函数:
  • 如果数据是内置数据类型,则使用指针
  • 如果数据对象是数组,则只能使用指针
  • 如果数据对象是结构,则使用引用或者指针
  • 如果数据是类对象,则使用引用
2、值传递、指针传递、引用传递去区别和效率?
  • 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)
  • 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
  • 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)
  • 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。
3、C++中的指针参数传递和引用参数传递有什么区别?(底层)
  • 指针参数传递本质上是值传递,它所传递的是一个地址值

​ 值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

  • 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

​ 被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

​ 因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

  • 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

​ 而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

  • 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

​ 指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。

​ 符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

4、继承机制中对象之间如何转换?指针和引用之间如何转换?
1)向上类型转换

​ 将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。

2)向下类型转换

​ 将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。

5、将引用作为函数参数有哪些好处?
  • 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

  • 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

  • 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

1.3、指针和引用的区别
  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名

  • 指针可以有多级,引用只有一级

  • 指针可以为空,引用不能为NULL且在定义时必须初始化

  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变

  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。

  • 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(具体情况还要具体分析)。

  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。

  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。

  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

1.4、结构对齐
1、字节对齐概念

​ 计算机系统对基本数据类型合法地址做出的限制,要求某种类型对象的地址必须是某个值(通常是2、4或8)的倍数。对齐跟数据在内存中的位置有关。

  • 分配内存的顺序是按照声明的顺序。
  • 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
  • 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
2、字节对齐单位决定因素:

(以上三者取最小的一个)

  • WIN、VS、qt默认8字节对齐;LINUX 32位默认4字节对齐,64位默认8字节对齐;
  • 结构体最大成员变量;
  • 预编译指令#pragma pack(n) n只能填1、2、4、8、16;
3、为什么要字节对齐

​ 需要字节对齐的原因在于CPU访问数据的效率问题。例,一个整型变量的地址不是自然对齐,如0x00000002,CPU访问这个整型数据需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据。要是变量在自然对齐位置上,则只需要一次就可以取出数据。

​ 对于32位机来说,4字节能够提高CPU访问速度,但是在32位中使用1字节或两字节对齐,会降低访问速度,所以字节对齐需要考虑处理器类型。在VC中默认是4字节对齐的,GCC也是默认4字节对齐,所以需要综合考虑处理器类型和编译器类型。

​ 在设计不同CPU通信的协议时,或者编写硬件驱动程序时寄存器的结构,都需要字节对齐,因为不同编译器生成的代码不一样,所以本身就自然对齐的也要使其对齐。

4、如何处理字节对齐

​ 编译器按照的字节对齐原则:

  • 数据类型自身的字节对齐值:指定平台上基本类型的长度。char自身对齐值为1,short为2,int\float\double为4;
  • 结构体或类的自身对齐值:成员中自身对齐值最大的那个值;
  • 指定对齐值:#pragma pack(value)时的指定对齐值value,#pragma pack ()取消自定义字节对齐方式;
  • 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中取小的那个值;
  • 标准的数据类型,地址只要是长度的整数倍就行了。对于非标准的数据类型,**数组:**按照基本数据类型对齐,第一个对齐了后面自然对齐;==联合:==按其包含的长度最大的数据类型对齐;**结构体:**结构体中每个数据类型都要对齐。

​ 数据类型为结构体时 ,编译器可能会在结构体字段的分配中插入间隙,以保证结构体中每个元素都满足它的对齐要求。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐,结构体本身也要对齐(就是说结构体总长度需要是结构体有效对齐值的整数倍),因此,有时需要在结构末尾填充空间,来满足结构体的自身对齐。

5、字节对齐的结构体举例
struct test01
{
    char a;    //1字节
    short b;   //2字节
    int c;     //4字节
    long d;    //4字节
};

struct test02
{
    char a;    //1字节
    int c;     //4字节
    long d;    //4字节
    short b;   //2字节
};
    test01 t1 = { 0 };
    cout << sizeof(t1) << endl; //12字节  因为char和short之间要自动补齐1字节
    test02 t2 = { 0 };
    cout << sizeof(t2) << endl; //16字节  因为char自动补齐3字节,short自动补齐2字节

struct test
{
    int c;     //4字节
    long d;    //4字节
    char a;    //1字节
};  //char自动补齐3字节,总共是12字节

#pragma pack(2)
struct test02
{
    int a;    //4字节
    long c;     //4字节
    char b;   //1字节
};
#pragma pack()  //按两字节对齐,就是10字节

​ 所以适当地编排结构体成员地顺序,可以在保存相同信息地情况下尽可能节约空间。

1.5、大小端
1、大小端存储概念
  • 大端存储:字数据的高字节存储在低地址中

  • 小端存储:字数据的低字节存储在低地址中

在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输。

面经总结C++基础知识(已成功上岸字节)_第1张图片

2、如何判断大小端
1)强制转换
int main()
{
    int a = 0x1234;
    char c = (char)(a);//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    if(c == 0x12) 
        cout << "big endian" << endl;
    else if(c == 0x34) 
        cout << "little endian" << endl;
}
2)union
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
    int a;
    char ch;
};
int main(){
    endian value;
    value.a = 0x1234;//a和ch共用4字节内存空间
    if(value.ch == 0x12)
        cout << "big endian" << endl;
    else if(value.ch == 0x34)
        cout << "little endian" << endl;
}
1.6、组合和继承
1、组合

​ 组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

1)组合的优点:
  • 当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。

  • 当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

  • 当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

2)组合的缺点:
  • 容易产生过多的对象。
  • 为了能组合多个对象,必须仔细对接口进行定义
2、继承

​ 继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

1)继承的缺点
  • 父类的内部细节对子类是可见的。
  • 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
  • 如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
1.7、实参和形参的区别
  • 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。
  • 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。
  • 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。
  • 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
  • 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。
1.8、声明和定义的区别
1、如果是指变量的声明和定义

​ 从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。

2、如果是指函数的声明和定义

​ 声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。

​ 定义:一般在源文件里,具体就是函数的实现过程 写明函数体。

1.9、ifdef 和 endif 代表什么?

1、 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

2、条件编译命令最常见的形式为:

\#ifdef
    //程序段1
\#else
    //程序段2
\endif

​ 它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

3、 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义

1.10、C和C++类型安全
1、C和C++的区别
  • C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
  • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
  • 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
  • C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的
  • 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
  • C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等
2、什么是类型安全?

​ 类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。

3、C的类型安全

​ C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。

1)printf格式输出
2)malloc的返回值

​ malloc是C中进行内存分配的函数,它的返回类型是void*即空类型指针,常常有这样的用法:

char* pStr=(char*)malloc(100*sizeof(char)) //语句1
int* pInt=(int*)malloc(100*sizeof(char))   //语句2

​ 这里明显做了显式的类型转换。类型匹配尚且没有问题,但是一旦出现语句2就很可能带来一些问题,而这样的转换C并不会提示错误。

4、C++的类型安全

​ 如果C++使用得当,它将远比C更有类型安全性。相比于C语言,C++提供了一些新的机制保障类型安全:

  • 操作符new返回的指针类型严格与对象匹配,而不是void;
  • C中很多以void为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
  • 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;
  • 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;
  • C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
1.11、回调函数
  • 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;
  • 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;
  • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
  • 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
1.12、拷贝初始化和直接初始化
  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
  • 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
    • 当拷贝构造函数为private时:语句3和语句4在编译时会报错
    • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
1.13、深拷贝和浅拷贝
1、深拷贝

​ 深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

2、浅拷贝

​ 浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

​ 浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。

浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题!!

1.14、public、private、protected

​ public的变量和函数在类的内部外部都可以访问;protected的变量和函数只能在类的内部和其派生类中访问;private修饰的元素只能在类内访问。

1、访问权限

​ 派生类可以继承基类中除了构造/析构、赋值运算符重载函数之外的成员,但是这些成员的访问属性在派生过程中也是可以调整的,三种派生方式的访问权限如下表所示:注意外部访问并不是真正的外部访问,而是在通过派生类的对象对基类成员的访问。

基类成员 public private protected public private protected public private protected
派生方式 private protected public
派生类中 private 不可见 private protected 不可见 protected public 不可见 protected
外部 不可见 不可见 不可见 不可见 不可见 不可见 可见 不可见 不可见

派生类对基类成员的访问形象有如下两种:

  • 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
  • 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
访问权限总结:
  • public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被 访问,在类外也是可以被访问的,是类对外提供的可访问接口;
  • private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
  • protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。
2、继承权限
1)public继承

​ 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问。

2)private继承

​ 私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承,访问规则如下表:

基类成员 public成员 private成员 protected成员
内部访问 可访问 不可访问 可访问
外部访问 不可访问 不可访问 不可访问
3)protected继承

​ 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的,访问规则如下表:

基类成员 public成员 private成员 protected成员
内部访问 可访问 不可访问 可访问
外部访问 不可访问 不可访问 不可访问
4)继承权限总结
  • 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
  • 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
  • 若继承方式是protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。
1.15、异常处理
1、常见的异常
  • 数组下标越界
  • 除法计算时除数为0
  • 动态分配的内存空间不足
2、try、 throw、 catch关键字

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

3、函数的异常声明列表

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

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

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

4、exception

面经总结C++基础知识(已成功上岸字节)_第2张图片
面经总结C++基础知识(已成功上岸字节)_第3张图片

  • bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常;

  • bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常

  • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

  • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

1.16、coredump错误

coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

1.17、怎么快速定位错误位置?
  • 如果是简单的错误,可以直接双击错误列表里的错误项或者生成输出的错误信息中带行号的地方就可以让编辑窗口定位到错误的位置上。
  • 对于复杂的模板错误,最好使用生成输出窗口。多数情况下出发错误的位置是最靠后的引用位置。如果这样确定不了错误,就需要先把自己写的代码里的引用位置找出来,然后逐个分析了。
1.18、四种强制转换
1、reinterpret_cast
reinterpret_cast (expression)

​ type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

2、const_cast
const_cast (expression)

​ 该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象
  • const_cast一般用于修改底指针。如const char *p形式
3、static_cast
static_cast < type-id > (expression)

​ 该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换

    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把空指针转换成目标类型的空指针

  • 把任何类型的表达式转换成void类型

    注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

4、dynamic_cast

​ 有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast (expression)

​ 该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*

  • 如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么expression 也必须是一个引用
  • dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)
  • dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
    • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的
    • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

在进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。

1.19、程序的运行过程

面经总结C++基础知识(已成功上岸字节)_第4张图片

1、预编译

​ 主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:

  • 删除所有的#define,展开所有的宏定义。
  • 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
  • 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
  • 删除所有的注释,“//”和“/**/”。
  • 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
  • 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
2、编译

​ 把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。

  • 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
  • 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
  • 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
  • 优化:源代码级别的一个优化过程。
  • 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
  • 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
3、汇编

​ 将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

4、链接

​ 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

1)静态链接

​ 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

  • 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

  • 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

  • 运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

2)动态链接

​ 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

  • 共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
  • 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
  • 性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
1.20、动态编译和静态编译
1、静态编译

​ 编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

2、动态编译

​ 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。

1)优点:
  • 缩小了执行文件本身的体积
  • 加快了编译速度,节省了系统资源
2)缺点:
  • 缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;
  • 二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。
1.21、友元
1、为什么友元函数必须声明在类内部?

​ 因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

2、友元函数和友元类

​ 友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

​ 友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。

2)友元类

​ 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。 但是另一个类里面也要相应的进行声明。

使用友元类时注意:

  • 友元关系不能被继承。
  • 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
  • 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。
1.22、类占内存的大小

​ 一般类所占的内存大小是非静态数据成员(在全局区)的总和大小 。

1、空类
	空类的大小讲道理应该是0,但实际上是1。就是涉及到一个实例化的问题,空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,所以编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存中就得到了一个独一无二的地址,所以空类所占内存大小为1。
2、virtual

​ 动态多态,会有个虚函数指针,就是指针的大小(64位指针所占内存是8字节)。

你可能感兴趣的:(c++,数据结构,mfc)