C++八股补充

0. gcc/g++/MingW/MSVC与make/CMake

GNU是一个开源计划,GNU中有GCC和g++,gcc 和 g++ 是用在Linux 上的
MinGW 和 MSVC 是用在windows下的,VS 使用的就是 MSVC
LLVM的clang/clang++ 是用在 IOS 下的
make 是包含了 gcc 和 g++ 的,可以编写 makefile 文件来构建项目
Cmake 是对 make 的升级,支持跨平台

1. 在main执行之前和之后执行的代码可能是什么

__attribute__是用来指定变量、函数、结构体等属性的。__attribute__是GCC编译器的扩展,不是C++标准的一部分。
attribute((constructor)):用于指定在程序加载时自动执行的函数,类似于C++的构造函数。
attribute((destructor)):用于指定在程序退出时自动执行的函数,类似于C++的析构函数。

atexit是用于注册在程序退出时自动调用的函数。可以用于执行一些清理操作或释放资源的任务。原型是:
int atexit(void (*func)(void));
该函数接受一个指向无返回值、无参数的函数的指针作为参数。在程序退出时,注册的函数将按照它们被注册的顺序被调用。

2、结构体内存对齐问题(完整)

内存对齐的好处:

  • 提高访问速度
    内存对齐可以使数据按照自然边界对齐,使得处理器能够更有效地访问内存。
  • 减少内存访问次数
    处理器可以一次性读取或写入对齐的数据块。从而减少内存访问的总次数,提高系统性能。
  • 提高缓存性能
    如果按照缓存行的大小对齐,那么每次加载的数据块将是完整的缓存行,从而提高缓存性能。

对齐的地址一般都是 n(n = 2、4、8)的倍数。

  • 1 个字节的变量,例如 char 类型的变量,放在任意地址的位置上;
  • 2 个字节的变量,例如 short 类型的变量,放在 2 的整数倍的地址上;
  • 4 个字节的变量,例如 float、int 类型的变量,放在 4 的整数倍地址上;
  • 8 个字节的变量,例如 long long、double 类型的变量,放在 8 的整数倍地址上;

即 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍。
结构体嵌套:子结构体的成员变量起始地址要视子结构体中最大变量类型决定,比如 struct a 含有 struct b,b 里有 char,int,double 等元素,那 b 应该从 8 的整数倍开始存储。
含数组成员:比如 char a[5],它的对齐方式和连续写 5 个 char 类型变量是一样的,也就是说它还是按一个字节对齐。

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
  • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

c++11 以后引入两个关键字 alignasalignof 。其中 alignas 可以指定结构体的对齐方式,alignof 可以计算出类型的对齐方式。 比如 alignas(4) 指的是按照4字节对齐内存

若alignas小于自然对齐的最小单位,则被忽略。因此可以使用:

attribute((packed)):取消变量对齐,让变量之间排列紧密,不留缝隙。(不是 C++ 的标准,gcc 才支持)
#pragma pack (n):让变量强制按照 n 的倍数进行对齐,并会影响到结构体结尾地址的补齐

添加了#pragma pack(n)后规则就变成了下面这样:
1、 偏移量要是n和当前变量大小中较小值的整数倍
2、 整体大小要是n和最大变量大小中较小值的整数倍
3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

5.堆和栈 的区别

alloca 是一个用于在栈上分配内存的函数,其名称来源于 “allocate”。alloca 的作用是在运行时分配一块内存空间,并在函数退出时自动释放这块空间,类似于栈上的局部变量。
特点:
自动释放: 内存由系统在函数退出时自动释放,而不需要手动调用 free 或类似的释放函数。
生命周期受限: 分配的内存仅在包含 alloca 的函数执行期间有效。一旦函数返回,该内存将不再可用。
潜在风险: 由于内存是在栈上分配的,如果请求的内存过大,可能导致栈溢出。

8.new / delete 与 malloc / free的异同

malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(bad_alloc)
类型安全性:
new: “new” 是面向对象的,它知道所分配的内存应该包含什么类型的对象。因此,在使用 “new” 时,编译器能够确保你分配的内存与指定的类型相匹配。
malloc: “malloc” 是C语言中的函数,它只负责分配一块内存,而不关心这块内存将用于存储什么类型的数据。你需要显式地将返回的 void* 指针转换为适当的类型。由于 “malloc” 不知道你想要存储的类型,它无法提供类型安全性。

15.声明和定义区别

对于变量:

  1. 声明:是指出存储类型,并给存储单元指定名称。
  2. 定义:是分配内存空间,还可为变量指定初始值。
  3. extern关键字标识的才是声明,其余都是定义;声明有指定初始值:如果指定了初始值,即使前面加了 extern 关键字,也是定义。
extern int val; // 声明
int val; // 定义
extern int val = 1; // 定义
  1. 一个变量的定义永远只能有一个,但是可以有多个声明。

对于函数:

  1. 有函数体的是定义,无函数体的是声明,不管加没加 extern,只要没函数体就是声明
  2. 函数声明的 返回值类型、函数名 和 定义是一样的。形参表的类型与顺序 和定义也是一样的,声明可以不写形参名称,形参名称也可以和定义不一样。当有默认参数时,声明和定义只能有一处写默认参数
  3. 如果被调用的函数定义在调用者之后,那么被调用函数必须在调用者之前声明

16.strlen和sizeof区别

动态分配的内存:在堆区 new 出的内存,智能指针等

17. 常量指针和指针常量区别

指针常量:int * const p =&a; 指针的指向不可以修改,指针指向的内存的值可以修改

*p = 20; //正确,指向的内存地址中的数据可以修改   
p=&b;  //错误,指向的内存地址不可以修改

常量指针:const int *p=&a; 指针的指向可以修改,但是指针指向的值不可以修改。

*p = 20; //错误,指向的内存地址中的数据不可以修改   
p=&b;  //正确,指向的内存地址可以修改

指向常量的常指针:const int const *p=&a; 指针的指向不可以修改,指针指向的值也不可以修改。

*p = 20; //错误,指向的内存地址中的数据不可以修改   
 p=&b;  //错误,指向的内存地址不可以修改

20.C++与C语言的区别

  1. C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。
  2. C++是面对对象的编程语言;C语言是面对过程的编程语言。
  3. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用。
  4. C语言有一些不安全的语言特性,如指针使用的潜在危险、内存泄露、强制转换的不确定性等。而C++对此增加了不少新特性来改善安全性,如四种 cast 转换、智能指针、try—catch等等;

22.C++中struct和class的区别

struct一般用于描述一个数据结构集合,class是对一个对象数据的封装
class 可以用于定义模板参数,类似于 typename,而 struct 不可以

C++和C的 struct 的区别
在这里插入图片描述

23.define宏定义和const的区别

在这里插入图片描述

24.C++中const和static的作用(完整)

不考虑类的情况下:

  • static 可以修饰全局变量和局部变量,修饰之后是全局静态变量和局部静态变量,全局静态变量和局部静态变量存放在全局区(静态区)。并且变量的生存周期是:编译阶段分配内存,程序运行结束回收内存
    const 同样可以修饰全局变量和局部变量,被 const 修饰的全局变量存放在全局区,被 const 修饰的局部变量存放在栈区。生存周期:被 const 修饰的全局变量也是编译阶段分配内存,程序运行结束回收内存,而被 const 修饰的局部变量在函数运行结束之后就会回收内存

  • static 修饰的变量可以默认初始化,比如 int 类型的默认初始值是0 const 修饰的变量在定义时必须初始化,之后无法更改

  • static 有隐藏功能。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    一般不用 const 来隐藏

考虑类的情况下:

  • static 修饰成员变量:只与类关联,不与类的对象关联。定义时要分配空间,类内生命类外初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    const 修饰成员变量:只能通过构造函数初始化列表进行初始化,并且必须有构造函数

  • static 修饰成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
    const 修饰成员函数:如果 const 在函数最前面,说明返回值是 const 的,不能改变。如果 const 是在函数的最后面修饰,说明此函数是常函数,常函数不可以改变对象的成员变量的值,除非成员变量前加上 mutable,常函数不能调用非常函数,而非常函数可以调用常函数

  • static 不可以直接修饰对象
    而 const 可以直接修饰对象,称为常对象,常对象也是不能改变成员变量的值的,所以常对象只能调用常函数,不能调用非常函数

  • const 和 static 可以同时修饰成员变量,称为静态成员常量

  • 不可以同时用 const 和 static 修饰成员函数。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

25.C++的顶层const和底层const

顶层表示指针本身是个常量(比如指针常量),底层表示指针所指向的对象是个常量(常量指针)
当执行对象的拷贝操作时,常量是顶层const还是底层const的区别明显。
顶层 const 不受什么影响。
拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转化为常量,反之不行。

int i = 0;
int *const p1 = &i;     //  不能改变 p1 的值,这是一个顶层
const int ci = 42;      //  不能改变 ci 的值,这是一个顶层
const int *p2 = &ci;    //  允许改变 p2 的值,这是一个底层
const int *const p3 = p2;   //  靠右的 const 是顶层 const,靠左的是底层 const
const int &r = ci;      //  所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const

int *p = p3;    //  错误:p3 包含底层 const 的定义,而p没有。假设成功,p 就可以改变 p3 指向的对象的值。
p2 = p3;            //  正确:p2 和 p3 都是底层 const
p2 = &i;            //  正确:int* 能够转化为 const int*,这也是形参是底层const的函数形参传递外部非 const 指针的基础。
int &r = ci;    //  错误:普通 int& 不能绑定到 int 常量中。
const int &r2 = i;  //  正确:const int& 可以绑定到一个普通 int 上。

从拷贝上:…
从函数重载方面:
顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开:

Record lookup(Phone);
Record lookup(const Phone);         //重复声明了Record lookup(Phone)
​
Record lookup(Phone*);
Record lookup(Phone* const);        //该const是顶层,重复声明了Record lookup(Phone* const)

如果形参是某种类型的指针或引用,则通过区分其是否指向的是常量对象还是非常量对象可以实现函数重载。此时的const是底层的。(或者说底层 const 是可以区分开的)

Record lookup(Phone&);
Record lookup(const Phone&);        //正确,底层const实现了函数的重载
​
Record lookup(Phone*);
Record lookup(const Phone*);        //正确,底层const实现了函数的重载

从 const_cast 上看:…

26.数组名和指针(这里为指向数组首元素的指针)区别

1.概念:

  • (1)数组:数组是用于储存多个相同类型数据的集合。数组名是首元素的地址。
  • (2) 指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。

2.区别:

  • (1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
  • (2)存储方式:
    数组:数组在内存中是连续存放的,开辟一块连续的内存空间。所以 sizeof(数组名) 是整个数组在内存中的大小
    指针:灵活,指针可指向其他变量的地址,在32位平台下,无论指针的类型是什么,sizeof (指针)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。

28.拷贝初始化和直接初始化(完整)

当用于类类型对象时
什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且编译器直接调用与实参匹配的构造函数
什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。使用场景:

  • 使用赋值运算符定义变量 (只要是 = ,就是拷贝初始化)
  • 将对象作为实参传递给一个非引用类型的形参
  • 将一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

拷贝初始化首先创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了。因此当调用 赋值运算符 时其实不会创建临时对象
需要注意两点:
当拷贝构造函数为private时:拷贝初始化时会报错
使用 explicit 修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

补充:explicit:指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化

29.初始化和赋值的区别(完整)

在这里插入图片描述
C++语言的初始化方式
1、直接初始化
2、拷贝初始化
(接上一题)
赋值时比如调用 operator= 函数,可能传入的是1,其实赋的值是2,这个要看函数中的具体实现

31.野指针和悬空指针

C语言对 NULL 的定义:#define NULL ((void *)0),因此在 C语言 可以隐式类型转换,将 void* 转换成 int*等其他指针
而 C++ 中不允许隐式转换成其他类型的,其对 NULL 的定义:#define NULL 0
NULL在 C++ 中就是0,所以在重载整形的情况下,会出现问题,传入 NULL 调用的是 int 参数的函数:

void func(void* i) {
	cout << "func1" << endl;
}
 
void func(int i) {
	cout << "func2" << endl;
}
 
void main(int argc,char* argv[])
{
	func(NULL); //func2
	func(nullptr); //func1
}

所以,C++11 加入了 nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议用 nullptr 替代 NULL

34.C++有哪几种的构造函数

C++中的构造函数可以分为5类:默认构造函数、有参构造函数、拷贝构造函数、转换构造函数、移动构造函数。
默认构造函数
也称无参构造函数,即构造函数无参数或者所有参数都有默认值
用于在构造对象时没有提供参数的情况下来创建对象的

Student();//没有参数
Student(int num=0;int age=0);//所有参数均有默认值

当一个类没有构造函数时,如果满足以下四个条件其中之一,则编译器会为该类自动生成一个默认的构造函数:

  • 该类含有一个类类型(非内置类型)的成员变量,且该类型含有默认构造函数。
  • 该类含有虚基类。
  • 该类继承自含有默认构造函数的基类。
  • 该类继承或定义了虚函数。

有参构造函数
顾名思义,有参构造函数是有参数的。
用于在构造对象时传递参数,编译器会自动匹配合适的有参构造进行创建对象

拷贝构造函数
拷贝构造函数的参数是此类类型的对象,并且是 const 的
是用于在创建对象时,将一个已存在的对象的属性拷贝给新对象
拷贝构造函数一般的使用场景是:

  • 构造函数时传的参数是此类类型的对象
  • 将对象作为实参传递给一个非引用类型的形参
  • 将一个返回类型为非引用类型的函数返回一个对象

当一个类没有拷贝构造函数时,如果满足以下四个条件其中之一,则编译器会为该类自动生成一个默认的拷贝构造函数:

  • 该类含有一个类类型(非内置类型)的成员变量,且该类型含有拷贝构造函数。
  • 该类含有虚基类。
  • 该类继承自含有拷贝构造函数的基类。
  • 该类继承或定义了虚函数。

需要注意的是,默认的拷贝构造函数实现的是浅拷贝。

转换构造函数
转换构造函数的参数是不同于此类类型的对象
可以认为是将其形参转换成此类类型的一个对象
比如 string 类中就有转换构造函数:string(char *);//形参是其他类型变量,就是将 char* 转成 string 类型的变量

移动构造函数
移动构造函数就是使用移动语义,也就是以移动而非深拷贝的方式初始化含有指针成员的类对象,即将其他对象的内存资源 “移为己用”。
一般用在使用临时对象创建新对象的时候,因为临时对象用完之后会被销毁,所以可以直接将临时对象指针成员指向的内存资源直接移给新对象所有,而无需拷贝,大大提高了效率

class Str {
public:
        char *str;
        Str(Str &&s)//移动构造函数
        {
        }
};

36.内联函数和宏定义的区别

在 C++ 中,为了解决一些频繁调用的小函数大量消耗栈空间的问题,引入了 inline修饰符,表示为内联函数。
使用 inline 修饰带来的好处表面看不出来,实际上在执行过程中,会将函数调用直接替换成相关代码,避免了频繁调用函数对栈内存重复开辟所带来的消耗。

inline使用限制
inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数

inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,如果编译器认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。

类中的成员函数与inline
定义在类中的成员函数缺省都是内联的
类内声明类外定义的成员函数,又想内联该函数的话,那在类外定义时要加上inline,否则就认为不是内联的。

关键字 inline 必须与函数定义放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。建议若需使用内联函数,一般声明不加 inline,而定义加 inline

38. 如何用代码判断大小端存储(完整)

先补充一下 union 的知识:
union 实际上就是多个变量共用同一块内存,union的大小就为最大的那一个变量

union U
{
    struct node
    {
        int x;
        long long y;
    }u;
    double z;
};
int main()
{
    cout << sizeof(U) << endl;       //16
    cout << sizeof(U::node) << endl; //16
}

大端存储:数据的高字节存储在低地址中,数据的低字节存储在高地址中
C51是大端存储;socket编程中网络字节序一般是大端存储
小端存储:数据的高字节存储在高地址中,数据的低字节存储在低地址中
x86结构、ARM和DSP都是小端存储

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

判断大小端存储的两种方式:
方式一:使用强制类型转换

#include <iostream>
using namespace std;
int main()
{
    int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)//0x12是高字节,0x34是低字节
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}

方式二:巧用union联合体

#include <iostream>
using namespace std;
//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;
}

39.volatile、mutable和explicit关键字的用法

volatile
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
对于多线程,当多个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么可能有的线程使用内存中的变量,有的线程使用寄存器中的变量,这会造成程序的错误执行。
volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
基本数据类型和自定义数据类型(类类型)都可以由 volatile 修饰。可以把一个非volatile int赋给volatile int,但是不能把非 volatile 对象赋给一个volatile对象。
类似于 const,指针后面跟 volatile 指的是 指针是 volatile 的,指针指向的值不是 volatile 的
volatile 后面跟指针,指的是 指针不是 volatile 的,指针指向的值是 volatile 的。

explicit
explicite 是 C++ 语言中的一个关键字,它可以用来修饰单个参数的构造函数,表示该构造函数只能显式调用,禁止隐式转换。在 C++11 中,explicit 还可以用来修饰类型转换函数,表示只能显式进行类型转换。

40.什么情况下会调用拷贝构造函数

RVO 和 NROV 是编译器对于函数返回值的一种优化技术,旨在消除临时对象的创建。简单来说就是允许省略拷贝构造函数。就是在调用的地方,将需要初始化对象的引用作为函数参数传递给函数,进而避免不必要的拷贝。
如果没有优化:

Obj fun() {
  Obj obj;
  // do sth;
  return obj;
}

int main() {
  Obj obj = fun();
  return 0;
}

对于以上代码,执行过程是:

  1. 在 fun() 函数中,构造 obj 对象
  2. 通过拷贝构造创建临时变量 ( fun() 函数定义的 obj —>临时对象)
  3. 析构 fun() 函数中构造的obj对象 ( fun() 函数定义的 obj )
  4. 通过拷贝构造函数构建 obj (main 函数中的) 对象 (临时对象—> main() 函数定义的 obj)
  5. 释放临时对象
  6. 释放main()函数中定义的obj对象

也就是说没有优化的情况下调用了1次构造函数,2次拷贝构造函数,以及3次析构函数。
编译器对函数返回值的优化方式分为 RVO 和 NROV(C++11引入)

RVO

RVO(Return Value Optimization),通过 RVO,编译器可以减少函数返回时生成临时对象的个数。
当一个未具名且未绑定到任何引用的临时变量被移动或拷贝到一个相同的对象时,拷贝和移动构造可以被省略。
比如:

Obj fun() {
  return Obj(); //返回未具名字的对象
}

int main() {
  Obj obj = fun();
  std::cout << "&obj is " << &obj << std::endl;
  return 0;
}

对于上述代码,采用了 RVO 优化后,执行过程是:

  1. 调用默认构造函数创建对象
  2. 调用析构函数

NRVO

NRVO,(Named Return Value Optimization)。NRVO 与 RVO 的区别是返回的对象是具名的,即对象是在return语句之前就构造完成。

Obj fun() {
  Obj obj; // 具名对象
  // do sth;
  return obj;
}

int main() {
  Obj obj = fun();
  std::cout << "&obj is " << &obj << std::endl;
  return 0;
}

对于上述代码,采用了 NRVO 优化后,执行过程是:

  1. 调用默认构造函数创建对象
  2. 调用析构函数

对于具名对象,比如上述代码来说,如果采用的是 RVO 优化,那么会多一次拷贝构造

原理

简单来说就是在调用的地方,将需要初始化对象(假设为 a)的引用作为函数参数传递给函数,然后在函数中会用 此引用参数(a) 的地址而并非函数中定义的参数(b)的地址,这样在返回对象 b 时,就不会拷贝了。
RVO 的编译器自动优化代码是:
在这里插入图片描述

NRVO 的编译器自动优化代码是:
在这里插入图片描述

如果是对具名对象的 RVO 优化:
在这里插入图片描述
所以是会多一次拷贝构造的。

优化失效的情况

  1. 当编译器无法单纯通过函数(比如通过 if 来判断返回什么对象)来决定返回哪个实例对象时,会禁用RVO 和 NRVO。(通过 if 来判断单个对象的返回不会禁用)
    在这里插入图片描述
  2. 当返回的对象不是在函数内(比如全局变量、形参)创建时,无法执行返回值优化的。
  3. RVO 和 NRVO 只能在从返回值创建对象时发生,在现有对象上使用 operator= 而不是拷贝/移动构造函数
Obj fun() {
  return Obj();
}

int main() {
  Obj obj;
  obj = fun(); //现有对象
  return 0;
}
  1. 在返回值上调用 std::move() 进行返回是一种错误的方式。它会尝试强制调用移动构造函数,但这样会导致 RVO 和 NRVO 失效。因为即使没有显示调用 std::move(),编译器优化中也会执行 move 操作。(所以加上 move 反而效率更低了,增加了一次拷贝构造)
Obj fun() {
  Obj obj;
  return std::move(obj);
}

int main() {
  Obj obj = fun();
  return 0;
}

如果想使用 RVO 和 NRVO 优化,那么需要给类显式的提高拷贝构造函数

一般如果想真的减少构造函数的调用的话,可以将调用函数中需要接收的对象(a)引用传递给被调用函数,这样被调用函数不用返回对象,自然就提高了效率。而且还不用编写代码时关系哪些编译器有没有返回值优化。

再说本题目

用类的一个实例化对象去初始化另一个对象的时候会调用拷贝构造
函数的参数是类的对象时(非引用传递)
函数的返回值是函数体内局部对象的类的对象时,并且是非引用返回,那么如果没有 RVO 和 NRVO优化,会调用拷贝构造函数。如果有 RVO 和 NRVO优化,不会调用拷贝构造。

42.C++的异常处理的方法

程序没有正常执行的情况就是异常,常见的异常有数组角标越界,空指针异常,动态分配空间异常等

try-catch

可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

try {
    // 可能抛出异常的语句
} catch (exceptionType variable){
    // 处理异常的语句
}

try 中包含可能会抛出异常的语句,而 catch 用来捕获异常并处理异常。如果 try 语句块没有检测到异常,那么就不会执行 catch 中的语句。如果检测到异常,执行流会从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了。执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。
如果 catch 到了异常,当前函数如果不进行处理,或已经处理了想通知上一层的调用者,可以在 catch 里面再 throw 异常。
catch 关键字后面的 exceptionType variable 指明了当前 catch 可以处理的异常类型,异常类型exceptionType variable 可以是 int、char、float、bool、指针等基本类型,也可以是结构体、类等自定义数据类型。但是一般使用的话都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。

可以一个 try 对应多个 catch:

try {
    //可能抛出异常的语句
} catch (exception_type_1 e){
    //处理异常的语句
} catch (exception_type_2 e){
    //处理异常的语句
}
//其他的catch
catch (exception_type_n e){
    //处理异常的语句
}

当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。
catch 在匹配异常类型的过程中,会进行类型转换,比如:

  1. 向上转型:也就是子类向父类的转换
  2. const 转换:将非 const 类型转换为 const 类型。
  3. 数组或函数指针转换:如果不是引用传递,那么数组名和函数会转换为指针。

throw

使用 throw 关键字来显式地抛出异常,用法为:throw exceptionData;
exceptionData 一般一个 exception 类或其子类的对象 或 字符串
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,成为异常规范(或异常列表):double func (char param) throw (int);
C++ 规定,如果父类虚函数抛出了异常,那么子类重写父类虚函数时,也需要抛出异常,并且抛出的异常规范需要和父类虚函数一样严格,或者更严格。只有这样,当通过产生多态时,才能保证不违背父类成员函数的异常规范。

class Base{
public:
    virtual int fun1(int) throw();
    virtual int fun2(int) throw(int);
    virtual string fun3() throw(int, string);
};
class Derived:public Base{
public:
    int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
    int fun2(int) throw(int);   //对!有相同的异常规范
    string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
}

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。

exception

C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常。exception 类位于 <exception> 头文件中
exception 类的继承层次:
在这里插入图片描述
在这里插入图片描述

logic_error 的子类:
在这里插入图片描述

runtime_error 的派生类:
在这里插入图片描述

what() 函数返回一个能识别异常的字符串,正如它的名字“what”一样,可以粗略地告诉你这是什么异常。

补充:
可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。

但是 catch 和真正的函数调用又有区别:
真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。

43、static的用法和作用

不考虑类的情况下:

  • static 可以修饰全局变量和局部变量,修饰之后是全局静态变量和局部静态变量,全局静态变量和局部静态变量存放在全局区(静态区)。并且变量的生存周期是:编译阶段分配内存,程序运行结束回收内存。所以.static 是将变量具有记忆功能和全局生存周期,全局变量也有这个作用。

  • static 修饰的变量可以默认初始化,比如 int 类型的默认初始值是0 ,全局变量也有这个作用。

  • static 有隐藏功能。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用

考虑类的情况下:

  • static 修饰成员变量:只与类关联,不与类的对象关联。定义时要分配空间,并且内存中只有一份拷贝。类内声明类外初始化,因为static变量先于对象存在,所以需要在类外初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。

  • static 修饰成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问

  • 不可以同时用 const 和 static 修饰成员函数。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

  • static 和 virtual 不能同时修饰成员函数,因为 static 修饰的成员函数没有 this 指针。被 virtual 修饰的成员函数是虚函数,虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 是通过 this 指针调用的,所以不能为 virtual;虚函数的调用关系,this->vptr->ctable->virtual function

49、什么是类的继承

  1. 继承中的构造和析构函数:构造函数,析构函数以及赋值运算符不能自动地从基类继承到派生类。
    基类的析构函数为虚函数,为了使用多态时,通过父类指针指向子类对象,通过父类的指针来析构一个派生类的对象时,父类和子类的析构函数都会被调用,防止内存泄漏。
    建立对象时,首先调用基类的构造函数,然后在调用下一个派生类的构造函数,依次类推;
    析构对象时,其顺序正好与构造相反;
  2. 类的兼容性是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,通过公有继承,派生类得到了基类中出构造函数、析构函数之外的所有成员。这样,公有派生类实际上就具有了基类的所有功能,凡是基类可以解决的问题,公有派生类都可以解决。继承类有以下五个原则:
    1):子类对象可以当做父类对象使用
    2):子类对象可以直接赋值给父类对象
    3):子类对象可以直接初始化父类对象
    4):父类指针可以直接指向子类对象
    5):父类引用可以直接引用子类对象

53、delete p、delete [] p、allocator都有什么作用(完整)

1、 动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;
因为 new 是运行时动态分配内存,所以可以是非常量整数;但是普通数组大小在编译时确定,并在栈上分配,所以需要是常量整数

//普通数组
const int size = 10;
int arr[size]; // 合法,size 是一个常量整数

int n = 5;
int arr[n]; // 非法,n 是一个变量而不是常量整数

//new 的数组
int n = 5;
int* dynamicArr = new int[n]; // 合法,n 是一个非常量整数

int m;
std::cin >> m;
int* dynamicArr2 = new int[m]; // 合法,m 是一个非常量整数

2、new动态数组返回的并不是数组类型,而是一个元素类型的指针,即指向首元素的指针,可以通过 [] 和 下标来访问数组中的元素
3、 delete[]时,数组中的元素按逆序的顺序进行销毁;
4、 new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

std::allocator<int> alloc;
int* memory = alloc.allocate(5); // 分配 5 个未初始化的 int 内存空间

for (int i = 0; i < 5; i++) {
    alloc.construct(&memory[i], i); // 在分配的内存上构造对象,传入初始值
}

// 手动调用析构函数
for (int i = 0; i < 5; i++) {
    alloc.destroy(&memory[i]); // 析构对象
}

alloc.deallocate(memory, 5); // 释放内存空间

55、malloc申请的存储空间能用delete释放吗(完整)

理论上是可以的,因为 new 中封装了 malloc,但是 new 除了可以分配内存,还可以调用构造函数。delete 中封装了 free,delete除了可以释放内存,也可以调用析构函数
但是既然用 malloc 了,因该是给一个基本数据类型分配内存,所以调用 delete 去释放内存应该是可以的。

但是 malloc /free 主要为了兼容C,new 和 delete 完全可以取代 malloc /free 的。并且 new 和 delete 是类型安全的,而且会自动计算分配的内存,比 malloc /free 方便。

如果用 free 释放 “ new 创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用 delete 释放 “ malloc 申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以 new/delete 必须配对使用,malloc/free 也一样。

并且 《Effective C++》中说 对一个用 new 获取来的指针调用 free,或者对一个用 malloc 获取来的指针调用 delete,其后果是不可预测的。“不可预测”的意思:它可能在开发阶段工作良好,在测试阶段工作良好,但也可能会最后在你最重要的客户的脸上爆炸。所以建议配对使用。

56、malloc与free的实现原理(完整)

malloc 用于动态分配堆区内存,返回一个指向该空间的指针。返回 void* 指针,需要强制类型转换后才能引用其中的值。free 释放一个由 malloc 所分配的内存空间。
在这里插入图片描述
Linux 提供了两个系统调用 brk 和 sbrk:
brk 用于返回堆的顶部地址;sbrk 用于扩展堆。先通过 sbrk 扩展堆,将这部分空闲内存空间作为缓冲池,然后通过 malloc / free 管理缓冲池中的内存。这样可以避免频繁的系统调用,提高程序性能。

malloc 使用空闲链表组织堆中的空闲区块。malloc 分配时会搜索空闲链表,根据内存匹配算法(比如首次适应算法、最佳适应算法、最大适应算法等),找到一个大于等于所需空间的空闲区块。然后将其分配出去,返回这部分空间的指针。如果没有这样的内存块,则向操作系统申请扩展堆内存。

free 会将区块重新插入到空闲链表中。free 只接受一个指针,却可以释放恰当大小的内存,这是因为在分配的区域的首部保存了该区域的大小。( malloc 分配的空间中包含一个首部来记录控制信息,因此它分配的空间要比实际需要的空间大一些。malloc 返回的是紧跟在首部后面的地址,即可用空间的起始地址。)

57、malloc、realloc、calloc的区别

先说明一下 size_t :
size_t 是一个无符号整数,它是 sizeof 操作符返回的结果类型,该类型的大小可选择。其大小足以保证存储内存中对象的大小(简单理解为 unsigned int就可以了,64位系统中为 long unsigned int)

typedef unsigned int size_t;
size_t a = sizeof(b);

类似的还有wchar_t, ptrdiff_t。

  • wchar_t就是 wide char type,“一种用来记录一个宽字符的数据类型”。
  • ptrdiff_t就是 pointer difference type,“一种用来记录两个指针之间的距离的数据类型”。

size_t还经常出现在C++标准库中,此外C++库中经常会使用一个相似的类型size_type。

  • size_t是全局定义的类型;size_type是STL类中定义的类型属性,用以保存任意string和vector类对象的长度
  • string::size_type 制类型一般就是unsigned int, 但是不同机器环境长度可能不同 win32
    和win64上长度差别;size_type一般也是unsigned int

再说本题:
malloc函数。其原型 void *malloc(unsigned int num_bytes); 需要我们手动的去计算要分配的内存。
calloc函数,其原型 void *calloc(size_t n, size_t size); 其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,会int *p = (int *)calloc(20, sizeof(int)),这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
calloc 函数由于给每一个空间都要初始化值,所以效率较 malloc 要低,并且现实很多情况的空间申请是不需要初始值的,所以大家用 malloc 更多。
realloc 函数和上面两个有本质的区别,其原型 void realloc(void *ptr, size_t new_Size); 用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr 为指向原来空间基址的指针, new_size 为接下来需要扩充容量的大小。
如果 size 较小,原来申请的动态内存后面还有空余内存,系统将直接在原内存空间后面扩容,并返回原动态空间基地址;如果 size 较大,原来申请的空间后面没有足够大的空间扩容,系统将重新申请一块大的内存,并把原来空间的内容拷贝过去,原来空间 free(类似于 vector 的动态扩容); 如果size 非常大,系统内存申请失败,返回 NULL,原来的内存不会释放。注意:如果扩容后的内存空间较原空间小,将会出现数据丢失,如果直接 realloc(p, 0),相当于 free(p)。

58、类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些

第一个问题:
初始化方式有三种:
成员声明的同时做初始化,称为默认初始化
构造函数初始化列表,在构造函数的冒号后使用初始化列表进行初始化。
构造函数体内赋值,通过在构造函数的函数体内进行赋值初始化;

成员初始化列表 和 构造函数体的区别:
成员初始化列表 和 构造函数的函数体 都可以为成员变量指定一些初值,但是两者在给成员指定初值的方式上是不同的。成员初始化列表 使用 初始化 的方式来为数据成员指定初值(也就是说 给数据成员分配内存空间时就进行初始化),而构造函数的函数体是通过赋值的方式来给数据成员指定初值(即成员变量被分配内存空间后才进行赋值的)。
这样的区别就造成了,在有些场景下,是必须要使用成员初始化列表:

  • 当初始化一个引用成员时(reference member)
  • 当初始化一个常量成员时(const member)。注意:从C++11开始这一条不再适用,常量成员也可以使用成员默认初始化。
  • 当需要调用父类的带有参数的构造函数时
  • 当需要调用当前类的另一个有参构造函数时
class S {
public:
    S(int s) {
        printf("S::S(), s=%d\n", s);
    }
};
class InitList : public S {
private:
    int a;
    int &ref_a = a;
    const int const_b;
public:
    InitList(int a, int b) :
            S(10),       // 当需要调用父类的带有参数的构造函数时
            a(a),        // 
            ref_a(a),    // 当初始化一个引用成员时(reference member)
            const_b(b) { // 当初始化一个常量成员时(const member)
        printf("InitList(), a=%d, ref_a=%d, const_b=%d\n",
            this->a, ref_a, const_b);
    }
};

第二个问题
类成员初始化顺序:无论采用哪种初始化方式,类成员的初始化顺序始终跟成员的声明顺序相同。
初始化方式的优先级:声明时初始化->初始化列表->构造函数初始化
所以假如三种初始化方式同时存在的话,那么最后保留的成员变量值肯定是构造函数中初始化的值。

第三个问题
构造函数初始化的本质是赋值操作(“=”),对于基本数据类型,效率没什么影响。
但是对于类类型,初始化列表 相对于 构造函数 少了一次调用默认构造函数的过程。(在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作(对象已存在),所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。)

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

转换关系:

a)  string转const char* 

string s = “abc”; 

const char* c_s = s.c_str(); 

b)  const char* 转string,直接赋值即可 

const char* c_s = “abc”; 
 string s(c_s); 

c)  string 转char* 
 string s = “abc”; 
 char* c; 
 const int len = s.length(); 
 c = new char[len+1]; 
 strcpy(c,s.c_str()); 

d)  char* 转string 
 char* c = “abc”; 
 string s(c); 

e)  const char*char* 
 const char* cpc = “abc”; 
 char* pc = new char[strlen(cpc)+1]; 
 strcpy(pc,cpc);

f)  char*const char*,直接赋值即可 
 char* pc = “abc”; 
 const char* cpc = pc;
  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值