cpp八股文(4)

101、程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针

char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。
argc和argv参数在用命令行编译程序时有用。

main( int argc, char argv[], char *env ) 中
第一个参数,int型的argc,为整型,用来统计程序运行时发送给main函数的命令行参数的个数,在VS中默认值为1。
第二个参数,char*型的argv[],为字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。各成员含义如下:
argv[0]指向程序运行的全路径名
argv[1]指向在DOS命令行中执行程序名后的第一个字符串
argv[2]指向执行程序名后的第二个字符串
argv[3]指向执行程序名后的第三个字符串
argv[argc]为NULL

102、volatile关键字的作用?

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

volatile用在如下的几个地方:

中断服务程序中修改的供其它程序检测的变量需要加volatile;

多任务环境下各任务间共享的标志应该加volatile;

存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

103、如果有一个空类,它会默认添加哪些函数?

1) Empty(); // 缺省构造函数//
2) Empty( const Empty& ); // 拷贝构造函数//
3) ~Empty(); // 析构函数//
4) Empty& operator=( const Empty& ); // 赋值运算符//

104、C++中标准库是什么?

C++ 标准库可以分为两部分:

标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。

面向对象类库: 这个库是类及其相关函数的集合。

输入/输出 I/O、字符串和字符处理、数学、时间、日期和本地化、动态分配、其他、宽字符函数

标准的 C++ I/O 类、String 类、数值类、STL 容器类、STL 算法、STL 函数对象、STL 迭代器、STL 分配器、本地化库、异常处理类、杂项支持库

105、你知道const char* 与string之间的关系是什么吗?

string 是c++标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*给string类初始化

const char* 可隐式转换为string

string 不可隐式转换为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;

106、你什么情况用指针当参数,什么时候用引用,为什么?

使用引用参数的主要原因有两个:

程序员能修改调用函数中的数据对象

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

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

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

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

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

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

对于修改函数中数据的函数:

如果数据是内置数据类型,则使用指针

如果数据对象是数组,则只能使用指针

如果数据对象是结构,则使用引用或者指针

如果数据是类对象,则使用引用

107、你知道静态绑定和动态绑定吗?讲讲?

对象的静态类型:对象在声明时采用的类型。是在编译期确定的。

对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。

静态绑定:绑定的是对象的静态类型,某特性(比如函数依赖于对象的静态类型,发生在编译期。)

动态绑定:绑定的是对象的动态类型,某特性(比如函数依赖于对象的动态类型,发生在运行期。)

108、如何设计一个类计算子类的个数?

1、为类设计一个static静态变量count作为计数器;

2、类定义结束后初始化count;

3、在构造函数中对count进行+1;

4、 设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;

5、设计复制构造函数,在进行复制函数中对count+1操作;

6、在析构函数中对count进行-1;

109、怎么快速定位错误出现的地方

1、如果是简单的错误,可以直接双击错误列表里的错误项或者生成输出的错误信息中带行号的地方就可以让编辑窗口定位到错误的位置上。

2、对于复杂的模板错误,最好使用生成输出窗口。

多数情况下出发错误的位置是最靠后的引用位置。如果这样确定不了错误,就需要先把自己写的代码里的引用位置找出来,然后逐个分析了。

110、成员初始化列表会在什么时候用到?它的调用过程是什么?

当初始化一个引用成员变量时;

初始化一个const成员变量时;

当调用一个基类的构造函数,而构造函数拥有一组参数时;

当调用一个成员类的构造函数,而他拥有一组参数;

编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户代码前。list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。

112、说一说strcpy、sprintf与memcpy这三个函数的不同之处

操作对象不同

① strcpy的两个操作对象均为字符串

② sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串

③ memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

执行效率不同

memcpy最高,strcpy次之,sprintf的效率最低。

实现功能不同

① strcpy主要实现字符串变量间的拷贝

② sprintf主要实现其他数据类型格式到字符串的转化

③ memcpy主要是内存块间的拷贝。

113、将引用作为函数参数有哪些好处?

传递引用给函数与传递指针的效果是一样的。

这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;

而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;

如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;

另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

114、你知道数组和指针的区别吗?

数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);

用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。

编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。

在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;

在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。

115、如何阻止一个类被实例化?有哪些方法?

将类定义为抽象基类或者将构造函数声明为private;

不允许类外部创建类对象,只能在类内部创建对象

116、 如何禁止程序自动生成拷贝构造函数?

为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。

类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;

针对上述两种情况,我们可以定一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。

117、你知道Debug和release的区别是什么吗?

调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试。Debug模式下生成两个文件,除了.exe或.dll文件外,还有一个.pdb文件,该文件记录了代码中断点等调试信息;

发布版本,不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。(调试信息可在单独的PDB文件中生成)。Release模式下生成一个文件.exe或.dll文件。

实际上,Debug 和 Release 并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。

118、main函数的返回值有什么值得考究之处吗?

main函数的返回值用于说明程序的退出状态。如果返回0,则代表程序正常退出。返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。

程序运行过程入口点main函数,main()函数返回值类型必须是int,这样返回值才能传递给程序的调用者(如操作系统)表示程序正常退出。

main(int args, char **argv) 参数的传递。参数的处理,一般会调用getopt()函数处理,但实践中,这仅仅是一部分,不会经常用到的技能点。

119、模板会写吗?写一个比较大小的模板函数

#include<iostream>;  
using namespace std;  
template<typename type1,typename type2>;//函数模板  
type1 Max(type1 a,type2 b)  
{ 
    return a > b ? a : b; 
}  
void main()  

120、strcpy函数和strncpy函数的区别?哪个函数更安全?

函数原型

char* strcpy(char* strDest, const char* strSrc)  
char* strncpy(char* strDest, const char* strSrc, int pos)

strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。 strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

如果目标长>指定长>源长,则将源长全部拷贝到目标长,自动加上’0’ 如果指定长<源长,则将源长中按指定长度拷贝到目标字符串,不包括’0’ 如果指定长>;目标长,运行时错误 ;

121、static_cast比C语言中的转换强在哪里?

更加安全;

更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图

122、成员函数里memset(this,0,sizeof(*this))会发生什么

有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;

类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;

类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

123、你知道回调函数吗?它的作用?

当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;

回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;

因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

124、什么是一致性哈希?

一致性哈希

一致性哈希是一种哈希算法,就是在移除或者增加一个结点时,能够尽可能小的改变已存在key的映射关系

尽可能少的改变已有的映射关系,一般是沿着顺时针进行操作,回答之前可以先想想,真实情况如何处理

一致性哈希将整个哈希值空间组织成一个虚拟的圆环,假设哈希函数的值空间为0~2^32-1,整个哈希空间环如下左图所示

一致性hash的基本思想就是使用相同的hash算法将数据和结点都映射到图中的环形哈希空间中,上右图显示了4个数据object1-object4在环上的分布图

结点和数据映射

假如有一批服务器,可以根据IP或者主机名作为关键字进行哈希,根据结果映射到哈希环中,3台服务器分别是nodeA-nodeC

现在有一批的数据object1-object4需要存在服务器上,则可以使用相同的哈希算法对数据进行哈希,其结果必然也在环上,可以沿着顺时针方向寻找,找到一个结点(服务器)则将数据存在这个结点上,这样数据和结点就产生了一对一的关联,如下图所示:

移除结点

如果一台服务器出现问题,如上图中的nodeB,则受影响的是其逆时针方向至下一个结点之间的数据,只需将这些数据映射到它顺时针方向的第一个结点上即可,下左图

添加结点

如果新增一台服务器nodeD,受影响的是其逆时针方向至下一个结点之间的数据,将这些数据映射到nodeD上即可,见上右图

虚拟结点

假设仅有2台服务器:nodeA和nodeC,nodeA映射了1条数据,nodeC映射了3条,这样数据分布是不平衡的。引入虚拟结点,假设结点复制个数为2,则nodeA变成:nodeA1和nodeA2,nodeC变成:nodeC1和nodeC2,映射情况变成如下:

这样数据分布就均衡多了,平衡性有了很大的提高

126、为什么友元函数必须在类内部声明?

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。

有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

友元函数和友元类的基本情况

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

1)友元函数

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

#include <iostream>;  
using namespace std;  
class A  
{  
public:  
    friend void set_show(int x, A &a); //该函数是友元函数的声明  
private:  
    int data;  
};  
void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员  
{  
    a.data = x;  
    cout << a.data << endl;  
}  
int main(void)  
{  
    class A a;  
    set_show(1, a);  
    return 0;  
}

一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。

2)友元类

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

#include <iostream>;  
using namespace std;  
class A  
{  
public:  
    friend class C; //这是友元类的声明  
private:  
    int data;  
};  
class C //友元类定义,为了访问类A中的成员  
{  
public:  
    void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}  
};  
int main(void)  
{  
    class A a;  
    class C c;  
    c.set_show(1, a);  
    return 0;  
}

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

127、用C语言实现C++的继承

#include <iostream>;  
using namespace std;  
//C++中的继承与多态  
struct A  
{  
    virtual void fun() //C++中的多态:通过虚函数实现  
    {  
        cout<<"A:fun()"<<endl;  
    }  
    int a;  
};  
struct B:public A //C++中的继承:B类公有继承A类  
{  
    virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加)  
    {  
        cout<<"B:fun()"<<endl;  
    }  
    int b;  
};  
//C语言模拟C++的继承与多态  
typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承  
struct _A //父类  
{  
    FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现  
    int _a;  
};  
struct _B //子类  
{  
    _A _a_; //在子类中定义一个基类的对象即可实现对父类的继承  
    int _b;  
};  
void _fA() //父类的同名函数  
{  
    printf("_A:_fun()n");  
}  
void _fB() //子类的同名函数  
{  
    printf("_B:_fun()n");  
}  
void Test()  
{  
    //测试C++中的继承与多态  
    A a; //定义一个父类对象a  
    B b; //定义一个子类对象b  
    A* p1 = &a; //定义一个父类指针指向父类的对象  
    p1->fun(); //调用父类的同名函数  
    p1 = &b; //让父类指针指向子类的对象  
    p1->fun(); //调用子类的同名函数  
    //C语言模拟继承与多态的测试  
    _A _a; //定义一个父类对象_a  
    _B _b; //定义一个子类对象_b  
    _a._fun = _fA; //父类的对象调用父类的同名函数  
    _b._a_._fun = _fB; //子类的对象调用子类的同名函数  
    _A* p2 = &_a; //定义一个父类指针指向父类的对象  
    p2->_fun(); //调用父类的同名函数  
    p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转  
    p2->_fun(); //调用子类的同名函数  
}

128、动态编译与静态编译

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

动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

129、hello.c 程序的编译过程

以下是一个 hello.c 程序:

#include <stdio.h>;
int main(){ 
    printf("hello, worldn"); 
    return 0;
}

在 Unix 系统上,由编译器把源文件转换为目标文件。

gcc -o hello hello.c

这个过程大致如下:

预处理阶段:处理以 # 开头的预处理命令;

编译阶段:翻译成汇编文件;

汇编阶段:将汇编文件翻译成可重定位目标文件;

链接阶段:将可重定位目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。

静态链接

静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:

符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。

重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

目标文件

可执行目标文件:可以直接在内存中执行;

可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;

共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;

动态链接

静态库有以下两个问题:

当静态库更新时那么整个程序都要重新进行链接;

对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。

共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点:

在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;

在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。

源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件

预处理

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件。

编译阶段

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件

汇编过程

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件

链接阶段

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

130、介绍一下几种典型的锁

读写锁

多个读者可以同时进行读

写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

131、delete和delete[]区别?

delete只会调用一次析构函数。

delete[]会调用数组中每个元素的析构函数。

132、为什么不能把所有的函数写成内联函数?

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

函数体内的代码比较长,将导致内存消耗代价

函数体内有循环,函数执行时间要比函数调用开销大

133、为什么C++没有垃圾回收机制?这点跟Java不太一样。

首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。

垃圾回收会使得C++不适合进行很多底层的操作。

134、在进行函数参数以及返回值传递时,可以使用引用或者值传递,其中使用引用的好处有哪些?

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(因为没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

评论区 0