C++面试题
指针和引用的区别:
引用不是一个对象,所以它没有实际的地址。指针是一个对象,它有实际的地址,所以我们可以定义指向指针的指针,但是绝对不能定义指向引用的指针。
引用在其生命周期中不能改变所绑定的对象,但是指针可以在其生命周期内先后指向不同的对象。
引用在定义的时候必须进行初始化,而指针可以不必进行初始化。
堆和栈的区别:
管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。
生长方向不同。堆向高地址生长,栈向低地址生长。
分配方式不同。堆都是动态分配的,栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配。
new和delete是如何实现的,new 与 malloc的异同处:
delete会调用对象的析构函数,和new对应。free只会释放内存,new调用构造函数。malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
三种继承方式:
(1) 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
(2) 私有继承(private) ------------------默认的继承方式(如果缺省,默认为private继承)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
(3) 保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
class的区别:
默认的继承访问权限。struct是public的,class是private的。
struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的
define 和const的区别(编译阶段、安全性、内存占用等):
编译阶段:define 是预编译阶段展开,而const是在运行阶段使用
安全性:const常量是有数据类型的,那么编译器会对const变量的类型等安全性进行检查,但是define只是在预编译阶段展开,不会进行类型的安全检查,替换时可能产生安全错误。
内存占用:define不会占用内存,单纯的替换而已,const会占用内存,会有对应的内存地址。
# define pi 3.14+3.14 在a=pi*pi;时----->a=3.14+3.14*3.14+3.14;
const float pi = 3.14+3.14;实际为pi分配了内存,a=pi*pi;------------->a=(3.14+3.14)*(3.14+3.14);
在C++中const和static的用法(定义,用途):
定义:
在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
const数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。
const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。
static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate=2.25;static关键字只能用于类定义体内部的声明中,定义时不能标示为static。
用途
cosnt成员函数主要目的是防止成员函数修改对象的内容。即const成员函数不能修改成员变量的值,但可以访问成员变量。当方法成员函数时,该函数只能是const成员函数。
static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1、不能直接存取类的非静态成员变量,调用非静态成员函数2、不能被声明为virtual.
const和static在类中使用的注意事项(定义、初始化和使用):
定义:
const可以在类内部定义,但是定义的位置不能初始化;static只能在类内部声明,定义只能在类的外面,并且定义的时候不能加static关键字
初始化:
const不能再定义的位置初始化,只能在类的构造函数的初始化列表中初始化;static初始化不能再类的内部进行初始化,必须在外部定义的时候初始化。
使用:
const的使用主要目的是防止成员函数修改对象的内容,即const成员函数不能修改成员变量的值,但可以访问成员变量。
static的使用目的是作为类作用域的全局函数。不能访问类的非静态数据成员,类的静态成员函数没有this指针,这导致不能直接存取类的非静态成员变量,调用非静态成员函数,不能声明为virtual.
C++中的const类成员函数(用法和意义),以及和非const成员函数的区别:
1.void fun() const;
表明是常量成员函数,这个const表明了该函数不会改变任何成员数据的值。
2.void fun(const a) const;
表明是参数是常量的常量成员函数,接收的参数是常量,同时不能改变成员数据的值。
意义:为什么要这么做?
这是为了保证它能被const常量对象调用,我们都知道,在定义一个对象或者一个变量时,如果在类型前加一个const,如const int x;,则表示定义的量为一个常量,它的值不能被修改。但是创建的对象却可以调用成员函数,调用的成员函数很有可能改变对象的值。所以这个时候const类成员函数就出现了。
于是,我们把那些肯定不会修改对象的各个属性值的成员函数加上const说明符,这样,在编译时,编译器将对这些const成员函数进行检查,如果确实没有修改对象值的行为,则检验通过。以后,如果一个const常对象调用这些const成员函数的时候,编译器将会允许。
C++的顶层const和底层const:
指针如果添加const修饰符时有两种情况:
1 指向常量的指针(底层const):代表不能改变其指向内容的指针。声明时const可以放在类型名前后都可,拿int类型来说,声明时:const int和int const 是等价的。声明指向常量的指针也就是底层const,下面举一个例子:
int num_a = 1;
int const *p_a = &num_a; //底层const
//*p_a = 2; //错误,指向“常量”的指针不能改变所指的对象
注意:指向“常量”的指针不代表它所指向的内容一定是常量,只是代表不能通过解引用符(操作符*)来改变它所指向的内容。上例中指针p_a指向的内容就不是常量,可以通过赋值语句:num_a=2; 来改变它所指向的内容。
2 常量指针(顶层const):代表指针本身是常量,声明时必须初始化,之后它存储的地址值就不能再改变。声明时const必须放在指针符号*后面,即:*const 。声明常量指针就是顶层const,下面举一个例子:
int num_b = 2;
int *const p_b = &num_b; //顶层const
//p_b = &num_a; //错误,常量指针不能改变存储的地址值
其实顶层const和底层const很简单,一个指针本身添加const限定符就是顶层const,而指针所指的对象添加const限定符就是底层const。
final和override关键字:
1.final
final限定某个类不能被继承或某个虚函数不能被重写。如果修饰函数只能修饰虚函数,且要话到类或函数后面。参考如下:
struct A
{
virtual void fun() final; //该虚函数不能被重写
virtual bar() final; //err: 非虚函数不能被final修饰
};
struct B final : A
{
void fun(); //err: 该虚函数不能被重写,因为在A中已经被声明为final
};
struct C : B //err: B是final
{
};
2.override
override关键字保证了派生类中声明重写的函数与基类虚函数有相同的签名,可避免一些拼写错误,如加了此关键字但基类中并不存在相同的函数就会报错,也可以防止把本来想重写的虚函数声明成了重载。同时在阅读代码时如果看到函数声明后加了此关键字就能立马知道此函数是重写了基类虚函数。保证重写虚函数的正确性的同时也提高了代码可读性。
struct A
{
virtual void fun();
};
struct D : A
{
void fun() override;//显示重写
};
拷贝初始化和直接初始化,初始化和赋值的区别:
拷贝初始化和直接初始化:(1)对于一般的内建类型,这两种初始化基本上没有区别。(2)当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后使用复制构造函数将那个临时对象复制到正在创建的对象
初始化和赋值的区别:对象的初始化是说你在声明的时候就调用默认的或者非默认的构造函数进行初始化工作,而赋值指的是你用一个已经存在的对象去给另一个已经存在的对象赋值。
区别的例子见上一篇随笔。
extern "C"的用法:
https://blog.csdn.net/qq_24282081/article/details/87530239
简单来说,c++中对函数名在编译后进行了更改,加入了返回类型,参数类型等(这样才能实现重载)。而在C中没有这个功能,因此如果定义函数的文件为.c文件,则编译后不会对函数名做处理,而如果此时main函数为cpp文件,则它在调用处对函数名进行修正,并按照这个修正的函数名去寻找函数,但由于定义函数的文件为.c,则不会更改函数名,造成寻找不到函数的问题。加入extern C后,main中就不会按照cpp的方式来更改函数名。
模板函数和模板类的特例化:
引入原因:编写单一的模板,它能适应大众化,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化。
定义:是对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上。
函数模板特例化:必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参。
template<typename T> //函数模板
int compare(const T &v1,const T &v2)
{
if(v1 > v2) return -1;
if(v2 > v1) return 1;
return 0;
}
//模板特例化,满足针对字符串特定的比较,要提供所有实参,这里只有一个T
template<>
int compare(const char* const &v1,const char* const &v2)
{
return strcmp(p1,p2);
}
特例化版本时,函数参数类型必须与先前声明的模板中对应的类型匹配,其中T为const char*。
本质:特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。例如,此处如果是compare(3,5),则调用普通的模板,若为compare(“hi”,”haha”)则调用特例化版本(因为这个cosnt char*相对于T,更匹配实参类型),注意,二者函数体的语句不一样了,实现不同功能。
注意:普通作用于规则使用于特例化,即,模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。
类模板特例化:原理类似函数模板,不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。对类进行特例化时,仍然用template<>表示是一个特例化版本,例如:
template<>
class hash<sales_data>
{
size_t operator()(sales_data&);
//里面所有T都换成特例化类型版本sales_data
};
按照最佳匹配原则,若T != sales_data,就用普通类模板,否则,就使用含有特定功能的特例化版本。
类模板的部分特例化:不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参。此功能就用于STL源码剖析中的traits编程。详见C++primer 628页的例子。(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,那个最匹配,就用相应的模板)
特例化类中的部分成员:可以特例化类中的部分成员函数而不是整个类。
template<typename T>class Foo
{
void Bar();
void Barst(T a)();
};
template<>
void Foo<int>::Bar()
{
//进行int类型的特例化处理
}
Foo<string> fs;
Foo<int> fi;//使用特例化
fs.Bar();//使用的是普通模板,即Foo<string>::Bar()
fi.Bar();//特例化版本,执行Foo<int>::Bar()
//Foo<string>::Bar()和Foo<int>::Bar()功能不同
STL及底层实现:
另开随笔专门记录一下。
C++中的重载和重写的区别:
重写:
首先,重写是指派生类的方法覆盖基类的方法,要求方法名、方法的参数都相同。重写是C++中实现多态这个特性基础。重写又称为覆盖,是指派生类函数覆盖基类函数,与重定义不同,重写要求被重写的基类函数为虚函数。
class Base{
public:
virtual int Total(int unit_price, int num) = 0;
};
class Derived :public Base{
public:
virtual int Total(int unit_price, int num)
{
cout << "test" << endl;
return 0;
}
};
class Child :public Derived{
public:
int Total(int unit_price, int num)
{
//
return unit_price*num;
}
};
重载(overload):
然后是重载(overload),重载是应用于相同作用域之内的同名函数,由于参数列表不同而产生的不同的实现方法。此处提到的作用域有:全局作用域、局部作用域以及类作用域,当在同一个作用域内的时候同名的函数或者称之为方法,由于参数列表的不同,而获得的不同的函数。
重载是一种语言特性,是一种语法规则,与多态无关,与面向对象无关。
重定义:
重定义则是经常出现在基类和派生类之间,归结起来有如下的特点:
1)不在同一个作用域,主要是指类作用域,分别位于基类和派生类之中;
2)函数名称相同,但是返回值可以不同;
3)参数不同时,无论有没有virtual关键字,基类的函数都会被隐藏;参数相同时,但是基类函数没有关键字virtual,此时基类函数被隐藏。
例如:
class Base{
public:
virtual double Total(double a, double b)
{
return a + b;
}
void Print()
{
cout << "Base" << endl;
}
int Sum(int a, int b);
private:
//
};
class Derived :public Base{
public:
virtual double Total(double a, double b, double c)
{
return a + b + c;
}
void Print()
{
cout << "Derived" << endl;
}
int Sum(int a, int b, int c);
};
在运行Derived的实例的时候可以很容易的发现,基类中Sum函数被隐藏了。因此重定义又称为隐藏,是指派生类的函数屏蔽了与其同名的基类函数。
C++内存管理:
https://blog.csdn.net/caogenwangbaoqiang/article/details/79788368
https://www.jianshu.com/p/19771f5a89ea
设计一个类:只能在堆上创建对象?只能在栈上创建对象?只能创建一个对象?
在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。
静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数;
动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
只能在堆上创建对象
类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。
方法一:
容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。
然而,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。
方法二:
当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。
我们试想,如果编译器无法调用类的析构函数,情况会是怎样的呢?
比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。
只在栈上生成对象
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
内存池技术:
https://blog.csdn.net/K346K346/article/details/49538975
内存池(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
介绍面向对象的三大特性,并且举例说明每一个:
面向对象的三大特性:封装、继承、多态。
封装:将很多有相似特性的内容封装在一个类中,例如学生的成绩学号、课程这些可以封装在同一个类中;
继承:某些相似的特性,可以从一个类继承到另一个类,类似生活中的继承,例如有个所有的汽车都有4个轮子,那么我们在父类中定义4个轮子,通过继承获得4个轮子的功能,不用再类里面再去定义这4个轮子的功能。
多态:多态指的相同的功能,不同的状态,多态在面向对象c++里面是通过重载和覆盖来完成的,覆盖在c++里面通过虚函数来完成的。例如鸭子的例子,所有的鸭子都有颜色,我们可以将这个颜色设置成为一个虚函数,通过继承子类对虚函数进行覆盖,不同子类中有各自的颜色,也就是有各自不同的鸭子颜色,这就是多态的典型表现之一。
!
C++多态的实现:
多态通过覆盖和重载来完成。
虚函数分为两种,纯虚函数和虚函数,纯虚函数适用于抽象基类,不需要定义,类似一种接口,是多态的典型处理方式。
一个类如果定义了虚函数,那么编译器会自动为它加上一个虚函数表,并提供一个指向虚函数表的指针,子类通过继承,可以覆盖父类的虚函数,当用户调用虚函数的时候,会调用指针,去虚函数表中找匹配的虚函数,如果当前对象有覆盖的虚函数,则去执行覆盖的虚函数,否则执行父类的虚函数。
基类的虚函数存放在内存的什么区:
1)虚表
虚表在静态区,因为对每个类,所有对象共用虚表。
2)虚表指针
虚表指针在对象内部,对象在哪他在哪。
三大区域都有可能。
虚函数表指针vptr的初始化时间:
综合来说构造函数的顺序是:
1、 在派生类(最终对象)的构造函数中,所有的虚拟基类的构造函数和上一层的构造函数将会被调用。
2、 上述完成之后,对象的vptr被初始化,指向相关的virtualtable。
3、 如果有成员初始化列表的话,在构造函数体内扩展开来。
4、 最后执行程序员所提供的代码。
C++中类的数据成员和成员函数内存分布情况:
成员函数:所有类成员函数和非成员函数代码存放在代码区
析构函数一般写成虚函数的原因:
因为在继承中,我们最后要销毁对象的时候,会调用析构函数,这个时候我们希望析构的是子类的对象,那么我们需要调用子类的析构函数,但是这个时候指针又是父类的指针,所以这个时候我们也要对析构函数写成虚构函数,这样析构函数的虚属性也会被继承,那么无论我们什么时候析构,都能动态绑定到我们需要析构的对象上。
构造函数、拷贝构造函数和赋值操作符的区别:
构造函数:对象不存在,没用别的对象初始化
拷贝构造函数:对象不存在,用别的对象初始化
赋值运算符:对象存在,用别的对象给它赋值
构造函数声明为explicit:
C++中的 explicit关键字主要是用来修饰类的构造函数,表明该构造函数是显式的,禁止单参数构造函数的隐式转换。
所谓隐式转换,即,将构造函数一个值(其类型为构造函数对应的数据类型)转换为一个类对象。
1.关键字explicit只对一个实参的构造函数有效
2.需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit的
3.只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复
构造函数为什么一般不定义为虚函数:
虚函数相应一个指向vtable
虚函数表的指针,这大家都知道,但是这个指向vtable
的指针事实上是存储在对象的内存空间的。
问题出来了,假设构造函数是虚的。就须要通过 vtable
来调用。但是对象还没有实例化,也就是内存空间还没有,怎么找vtable
呢?所以构造函数不能是虚函数。
构造函数的几种关键字(default delete 0):
= default:将拷贝控制成员定义为=default显式要求编译器生成合成的版本
= delete:将拷贝构造函数和拷贝赋值运算符定义删除的函数,阻止拷贝(析构函数不能是删除的函数 C++Primer P450)
= 0:将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)
构造函数或者析构函数中调用虚函数会怎样:
程序会崩溃
为什么呢?这是由于构造函数或者析构函数中调用虚函数这个时候,子类或许出于一个未初始化的状态,因为c++中父类先构造然后是子类,那么父类中构造调用子类,都没有构造,调用子类的虚函数,显然是错误的。
析构时,如果允许调用虚函数,那么可能已经析构掉vtbl了,如果还允许调用虚函数,则是错误的。
纯虚函数:
纯虚函数不需要定义,我们不能够为纯虚函数提供函数体,同样的,包含纯虚函数的基类是抽象基类,抽象基类是不能创建对象的,只能通过继承,继承子类中覆盖纯虚函数,执行自己的功能,子类是可以创建对象的。
静态类型和动态类型,静态绑定和动态绑定的介绍:
1、对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
2、对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
class B
{
}
class C : public B
{
}
class D : public B
{
}
D* pD = new D();//pD的静态类型是它声明的类型D*,动态类型也是D*
B* pB = pD;//pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C();
pB = pC;//pB的动态类型是可以更改的,现在它的动态类型是C*
3、静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
4、动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
class B
{
void DoSomething();
virtual void vfun();
}
class C : public B
{
void DoSomething();//首先说明一下,这个子类重新定义了父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。
virtual void vfun();
}
class D : public B
{
void DoSomething();
virtual void vfun();
}
D* pD = new D();
B* pB = pD;
让我们看一下,pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。
让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。
上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。
只有虚函数才使用的是动态绑定,其他的全部是静态绑定.
引用是否能实现动态绑定,为什么引用可以实现:
可以实现,因为动态绑定是发生在程序运行阶段的,c++中动态绑定是通过对基类的引用或者指针调用虚函数时发生。
因为引用或者指针的对象是可以在编译的时候不确定的,如果是直接传对象的话,在程序编译的阶段就会完成,对于引用,其实就是地址,在编译的时候可以不绑定对象,在实际运行的时候,在通过虚函数绑定对象即可。
深拷贝和浅拷贝的区别(举例说明深拷贝的安全性):
深拷贝就是拷贝内容,浅拷贝就是拷贝指针
浅拷贝拷贝指针,也就是说同一个对象,拷贝了两个指针,指向了同一个对象,那么当销毁的时候,可能两个指针销毁,就会导致内存泄漏的问题。
深拷贝不存在这个问题,因为是首先申请和拷贝数据一样大的内存空间,把数据复制过去。这样拷贝多少次,就有多少个不同的内存空间,干扰不到对方
对象复用的了解,零拷贝的了解:
零拷贝:零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
https://www.jianshu.com/p/fad3339e3448
介绍C++所有的构造函数:
默认构造函数、一般构造函数、拷贝构造函数
默认构造函数(无参数):如果创建一个类你没有写任何构造函数,则系统会自动生成默认的构造函数,或者写了一个不带任何形参的构造函数
一般构造函数:一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
拷贝构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。
什么情况下会调用拷贝构造函数(三种情况):
(1)用类的一个对象去初始化另一个对象时
(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用
(3)当函数的返回值是类的对象或引用时
结构体内存对齐方式和为什么要进行内存对齐?
1.前面的地址必须是后面的地址正数倍,不是就补齐
2.整个Struct的地址必须是最大字节的整数倍
接下来我们好好讨论一下内存对齐的作用?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。具体原因接下来解释
cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为(memory granularity)内存读取粒度。
我们再来看看为什么内存不对齐会影响读取速度?
假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:
1.数据从0字节开始
2.数据从1字节开始
解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。
当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。
但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。
内存泄露的定义,如何检测与避免?
内存泄漏指的是开辟的内存没有释放,或者是存在用户操作的错误,导致野指针,无法释放原来分配的内存。
在编程习惯上要注意使用尽量使用STL函数,使用vector而不是数组,使用智能指针而不是指针。
智能指针的循环引用:
https://blog.csdn.net/jfkidear/article/details/9034455
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用。
弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
https://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html
通过boost::weak_ptr来打破循环引用
由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。
模板的用法与适用场景:
模板是C11里面添加的,使用与在不知道类型的情况下,编写一个泛型的程序,模板通过用一个指定的关键字来代替类型,进行泛型编程。
成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
C++的调用惯例(简单一点C++函数调用的压栈过程):
https://blog.csdn.net/suhuaiqiang_janlay/article/details/44488557
C++的四种强制转换:
四种强制转换是static_cast、dynamic_cast、const_cast、reinterpret_cast。
static_cast:静态强制转换,类似传统c语言里面括号的强制转换dynamic_cast:动态强制转换,主要应用于多态,父子类的类型转换,dynamic_cast和static_cast不同的是,它会检查类型转换是否正确,不能转换,则会返回null,所以不算是强制转换。将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数。如果是基类指针或引用调用的是虚函数无需转换就能在运行时调用派生类的虚函数。
C++的异常处理:
throw 表达式;
该语句拋出一个异常。异常是一个表达式,其值的类型可以是基本类型,也可以是类。执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行
inline和宏定义的区别:
1、内联函数在编译时展开,而宏在预编译时展开
2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
4、宏不是函数,而inline是函数
5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。
6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。
7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。