指针和引用之间的区别
- 指针是一个新的变量,指向一个变量的地址。可以通过这个地址来修改另一个变量;引用是变量的别名,对引用的操作就是对变量本身的操作。
int a = 996;
int *p = &a;//p是指针,&在此是求地址运算
int &q = a;//q是引用,&在此是表示作用
- 指针可以有很多级,但是引用只有一级
- 传参的时候,使用指针需要解引用才可以对参数进行修改;函数使用引用作为参数,直接怼参数进行修改
- 32位操作系统中,指针一般是4个字节,引用的大小取决于被引用对象的大小
- 指针可以为空,但是引用不可以为空
- 指针定义的时候可以不用初始化,但是引用必须得进行初始化
- 指针初始化之后可以再次改变,但是引用不可以
- 自增的含义不一样:指针是指向变量之后的内存;引用是对变量本身进行自增操作
- C++中指针和引用的区别
函数传递的过程中,什么时候使用指针?什么时候使用引用?
- 需要返还函数内的局部变量的时候需要使用指针。但是使用指针需要开辟内存空间,用完之后需要对内存空间进行释放,否则会导致内存的泄露。但是返还局部变量的引用是没有任何意义
- 对于栈的大小比较敏感的时候(比如使用递归)的时候使用引用。使用引用不需要创建临时变量,开销较小
- 类对象作为函数传递的时候使用引用,这是C++标准对象传递的方式
- 函数传参的三种方式
补充:函数值传递不改变变量的数值,如果想要改变变量的数值,需要返回值,使用变量接收函数返回的数值;或者使用指针和引用作为函数的参数,都可以在函数内改变变量的数值,不需要通过返回值的方式进行改变;
- 如果想要使用引用传递的方式提升性能,但是不想改变某个变量的数值,可以将相应的输入参数定义为一个常量;使用const进行修饰
- 指针传递:指针传递本质上也是数值传递,但是传入的是地址,指针传递的外部实参的地址,当被调用函数的形参数值发生改变的时候,自然外部实参数值也会发生改变
堆和栈的区别
- 使用new方式开辟的内存空间将存储于栈区,需要程序猿手动管理,进行内存的分配,对象的构造和释放;栈是编译器自动管理的内存区域,存放函数的参数和局部变量
- 考虑到堆会有频繁的内存分配和释放,会导致内存碎片
- 栈的内存空间向下,地址越来越小;堆的内存空间向上,地址越来越大
- 栈的空间较小,一般是2M左右,使用堆存储的数据会很大
- 堆分配的是不连续的空间,但是栈分配的是连续的地址空间
- 栈是系统自动配置,速度很快,堆一般速度比较慢
- 堆(heap)和栈(Stack)的区别是什么?为什么平时都把堆栈放在一起讲?
- 什么是堆?什么是栈
堆快还是栈快
- 栈快
- 操作系统底层对栈提供了支持。会分配专门的存储器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行
- 堆的操作是由C和C++函数库提供的,分配内存的时候需要使用算法寻找合适大小的内存,并且获取堆的内存需要两次访问,第一个访问指针,第二次根据指针存储的地址访问内存,因此堆的速度比较慢
new和delete是如何实现的?new和delete的异同处
- new的时候,首先调用malloc为对象分配内存空间,然后调用对象的构造函数;delete会调用对象的析构函数,然后使用free进行对象的回收
- new和malloc都会进行内存空间的分配,但是new还会调用对象的构造函数进行初始化
- malloc需要给定空间的大小,new只需要对象的名字
既然有了malloc和delete为什么还要使用new和delete?
- 他们都是用于申请内存和回收内存的
- 对于非进本数据类型对象进行操作的时候,在其生命周期内,涉及到对象的构造和析构。malloc和free是库函数,是已经编译的代码,所以不能将构造函数和析构函数强加给malloc和free函数
- new和delete属于操作符,可以重载
- malloc和free属于函数,可以重写
- new和delete返回的是 某种数据类型的指针,malloc和free返回的是void指针
C和C++的区别
delete和delete[]的区别
- delete和delete[]的区别
- delete只会调用一次析构函数,但是delete[]会调用每个成员的析构函数
- 使用new分配的内存使用delete进行释放,使用new[]分配的内存使用delete[]进行释放
int *a = new int[10];
delete a;
delete[] a;
- 对于简单的类型,使用new分配后的不管是数组还是非数组形式的内存空间使用delete和delete[]两种方式都可以;原因在于,分配简单的数据类型,内存的大小是确定的,进行对象析构的时候也不会调用析构函数;直接通过指针就可以获得实际分配的内存空间,哪怕是数组内存空间(分配的过程中,系统会记录分配内存大小的信息,将其存储于结构体的CrtMemBlockHeader中)
- 针对类class,二者表现出具体的差异;
class A{
private:
char* m_cBuffer;
int m_nLen;
public:
A(){
m_cBuffer = new char[m_nLen];
}
~A(){
delete[] m_cBuffer;
}
};
int main(){
A *a = new A[10];
delete a;
}
- ?使用 delete a;仅仅释放了a指针指向的全部内存空间,也就是只调用了a[0]对象的析构函数,剩余的a[1]到a[9]这些内存空间不能释放,造成了数据的泄露
- delete[] a;//调用使用类对象的析构函数,释放用户自己分配的内存空间,释放了a指向的全部内存的空间
C++、java的联系和区别
- C++和java都是面向对象的编程语言,C++是编译成可执行文件直接运行的,Java是编译之后在JAVA虚拟机上运行的,因此Java具备很好地跨平台的特性,但是执行的效率不是很高
- C++内存由操作员手动管理,Java的内存管理是由Java虚拟机来完成的,使用的是标记回收的方式
- C++具备指针和引用类型,但是Java只有引用
- Java和C++都具备构造函数,但是C++存在析构函数,JAVA不具备
- 区别 参考链接
struct和class的区别
- 类中的成员 struct默认采用public继承方式,class默认采用private方式
- struct继承默认采用 public继承,class默认采用private继承
- class 可以作为模板,但是struct不可以
补充
- C++继承了C语言的struct,并对其进行了补充。struct可以包含1,成员函数;2,struct可以实现继承;3,struct可以实现多态
define和const的联系和区别(编译阶段、安全性和内存占用)
- 联系:都是定义常量的一种方法
- 区别
- define定义的常量没有类型,只是进行简单的替换,可能存在多个拷贝,占用的空间很大;
- const定义的变量是有类型的,存储于静态存储区,只有一个拷贝,占用的空间很小
- deifne定义的变量在预编译阶段进行替换,const在编译阶段确定他的数值
- define不会进行类型的安全检查,但是const会进行类型的安全检查,安全性更高
- const可以定义函数,但是define不可以
- const可以进行调试,但是define不可以进一步进行调试,因为define在预编译阶段就已经发生了替换
- const不支持重定义,但是define可以,使用#undef取消某个符号的定义,重新进行定义
- define可以避免头文件的重复引用
- C++中define和const的区别
C++中const的用法
- const修饰类的成员变量的时候,表示常量不可以修改
- const修饰的成员函数,表示函数不会修改类中的数据成员,不会调用其他非const的成员函数
const修饰基本的数据类型
- const修饰一般常量和数组 const int a=10;等价于int const a = 10;int和const的位置随便替换;对于基本数据类型,修饰符const和类型说明符号可以互换位置,其结果也是一致的,只要不改变这些常量的数值即可
- const修饰指针变量*和引用变量&:*返回的地址指向的变量,&返回变量的实际地址
- const位于*的左侧,则const修饰的是指针所指向的变量,即指针指向的是常量;这种情况不允许对内容进行修改操作,比如 *a=3;但是a可以指向别的变量
- const位于*的右侧,则const修饰的是指针本身,即指针本身是常量;指针指向的内容不是常量,*a=4是可以的,但是a=别的变量是不允许的
- const int* const a=4;//常量数据常量指针,a和*a都不可以更改
const应用到函数中
- 作为参数的const修饰符:则在函数体中,不可以对传递进来的指针的内容进行改变,保护了原指针指向的内容;
- 作为函数返回数值的const修饰符,一般用于二目操作符重载函数并且产生新对象的时候
***C++11不允许在类声明中初始化static非const类型的数据成员
- 对于static const类型的成员变量都支持定义的时候初始化
- 对于static 非const类型的成员变量,不支持在定义的时候初始化
- 对于const非static类型的成员变量要求在构造函数初始化的列表中初始化
- 对于非const非static类型的成员变量,允许定义的时候进行初始化
- 对于static非const成员变量,在类的定义之后进行初始化,使用int 类名::b=5;的方式
类中定义常量
常量对象只可以调用常量函数,别的成员函数都不可以调用
使用const的建议
- 能用const一定要用
- 避免赋值操作错误,比如对const变量进行赋值
- 在参数中使用const应该使用引用或者指针,而不是一般的对象实例
- const在函数中的三种用法,参数、返回值、函数
- 不要将返回数值的类型轻易确定为const
- 除了重载操作符,一般不要将返回数值的类型定位对某个对象的const引用?
- C++ const的用法详解
C++中的static用法和意义
- C++中的static
- static是静态的,用来修饰变量、类成员和函数
- 变量:被static修饰的变量就是静态变量,他的声明周期会持续到程序的结束。被static修饰的变量会存储在静态存储区域。局部静态变量的作用域在函数体中,全局静态变量的作用域在这个文件中
- 函数:被static修饰的函数是静态函数,静态函数只能在本文件中使用,不可以被其他文件调用,也不会和其他文件中的同名函数相互冲突
- 类:在类中,被static修饰的成员变量就是类的静态成员,这个静态成员会被类的多个对象公用;被static修饰的函数也属于静态成员,不是属于某个对象的,访问这个静态函数,不需要引用对象的名字,而是通过引用类名来访问。
- 静态成员要访问非静态成员时,通过对象来引用。局部静态变量在函数调用结束之后也不会被回收,会一直保存在内存中,如果这个函数被再次调用,他存储的数值是上次调用结束后存储的数值
- 注意static和const的区别:const强调数值不会被修改,而static强调数值的唯一性拷贝,对所有类的对象都共用
- 面向对象的static:类中的静态变量,声明为static的变量只会被初始化一次,因为它在单独的静态存储区域内分配了空间,因此类中静态变量由对象共享。但是对于不同的对象,不能有相同静态变量的多个副本,因此静态变量不可以使用构造函数进行初始化。
- 类中的静态成员变量必须在类内声明,在类外定义(被const 修饰的除外)
class Apple{
public:
static int i;//类内声明
Apple(){};
};
int Apple::i = 10;//类外定义
class Apple{
public:
//被const修饰的static变量直接在类内被初始化
const static int i=10;//类内声明
Apple(){};
};
- 类内的静态成员函数:静态成员函数不需要依赖于类的静态对象,可以使用对象和. 来调用静态成员函数。但是建议使用类名和范围解析运算符调用静态成员。
- 静态成员函数只可以访问静态数据成员 和 其他静态成员函数,无法访问类的非静态成员函数和非静态的成员变量
- 静态类:和变量一样,静态类的声明周期持续到程序的结束。在main函数结束之后才会调用静态类的析构函数
class Apple{
public:
//被const修饰的static变量直接在类内被初始化
const static int i=10;//类内声明
Apple(){};
static void find(){
std::cout << "静态成员函数" <<std::endl;
}
};
int main(){
Apple::find();
return 0;
}
计算几个类的大小?
class A{};
int main(){
std::cout << sizeof (A)<<std::endl;
A a;
std::cout << sizeof (a)<< std::endl;
return 0;
}
- 空类的大小是1,在C++中占用一个字节,这是为了让对象的实例能够相互区别,
- 具体来讲 空类同样可以被实例化,并且每个实例在内存中都占用独一无二的地址
- 因此编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的地址
- 当该空白类作为基类的时候,该类的大小就会被优化为0.? 测试不是
- 子类的大小就是子类本身的大小,这就是所谓的空白基类的最优化
- 空类的实例大小就是类的大小,所以sizeof(a)=1字节,如果a是指针,那么sizeof(a)的大小就是指针的大小,即4字节
class A{
virtual int Fun(){};
};
int main(){
std::cout << sizeof (A)<<std::endl;//32为机器输出为4,64位机器输出为8
A a;
std::cout << sizeof (a)<< std::endl;//32为机器输出为4,64位机器输出为8
return 0;
}
因为虚函数的类对象都有一个虚函数表指针 _vptr,其大小是4字节
class A{
static int a;
};
int main(){
std::cout << sizeof (A)<<std::endl;//输出为1
A a;
std::cout << sizeof (a)<< std::endl;//输出为1
return 0;
}
静态存储成员存放在静态存储区域,不占据类的大小,普通函数也不占用类的大小
class A{
int a;
};
int main(){
std::cout << sizeof (A)<<std::endl;//输出为4
A a;
std::cout << sizeof (a)<< std::endl;//输出为4
return 0;
}
STL介绍 (内存管理、allocator、函数、实现机理、多线程)
- STL从广义上讲涉及到了三类,算法、容器和迭代器
- 容器就是数据的存放方式,包括序列式容器list、vector等,关联式容器map和set和非关联容器
- 迭代器就是不暴露容器的内部结构的条件下对容器实现遍历
STL中hash的表现
- STL中unordered_map底层调用哈希表实现的。记录的键是哈希的数值,通过比对元素的哈希数值确定元素的数值是否存在
- 采用开链法解决哈希冲突,当桶的大小超过8的时候,就自动转换为红黑树进行组织
解决哈希冲突的方式
- 线性探查:当该元素的哈希值对应的桶不可以存放数据的时候,循环往后一一查找,直到找到一个空桶为止,在查找的时候如果该元素的哈希值对应的桶不匹配的啥时候,就一一往后查找,直到找到数据一致的元素,或者查找的元素不存在
- 二次探测:该元素哈希值对应的桶不能存放数据的时候,就往后寻找1^2,2^2,3^2,4^2......i^2个位置
- 双散列函数法:当使用第一个散列函数计算得到存储位置与对应存储的桶发生冲突的时候,再次调用第二个散列函数进行哈希,作为步长
- 开链法:在每一个桶内维护一个链表,由元素哈希数值寻找到这个桶,然后将元素插入到对应的链表中,STL就是使用hash_table这种实现方式
- 建立公共溢出区,当发生冲突的时候,将所有出现冲突的区域,放在公共溢出区
STL 中unordered_map和map的区别
- unordered_map底层使用哈希实现的,占用内存比较多,查询的速度较快,是常数时间的复杂度。内部是无序的,需要实现==操作符号
- map底层是采用红黑树实现的,插入删除时间复杂度是O(logn),他的内部是有序的,因次需要实现比较操作符号<
- C++STL标准模板库简介
std::map<int,std::string>m1;
//map的三种插入方式
m1.insert(make_pair(10,"abc"));//方式1
m1[9] = 'cdc';//方式2
m1.insert(std::pair<int,std::string>(13,"chy"));//方式3
//map使用insert方式插入元素数值,只能插入的是不存在的主键
m1.insert({8,"kkk"});
C++vector的实现
- STL中的vector是封装了动态数组的顺序容器。不过与动态数组不同的是,vector可以根据需要自动扩充容器的大小。具体的策略是每次容量不够使用的时候,重新申请一块大小为原来大小的两倍的内存,将原容器的元素拷贝到新的容器,并释放原先空间,返回新空间的指针。这也是为什么容器在扩容之后,与其相关的指针、引用和迭代器都会失效
- 在原来空间不够存储新数值的时候,每次调用push_back方法会重新分配新的空间从而满足新的数据的添加操作,如果频繁的进行内存的分配,会导致性能的损耗
- 三个指针:first指向的是容器对象的起始字节位置;last指向的是最后一个元素的末尾字节;end指针指向的是vector容器所占用的内存空间的末尾字节;使用last和first,描述容器中已经使用的内存空间;last和end描述容器空闲的内存空间;first和end描述容器的容量
- 频繁的调用 push_back()会使得程序花费很多时间在vector上,这种情况可以使用list链表或者提前申请内存空间
C++list和vector的区别
- vector拥有一段连续的内存空间,内存不够会进行内存的拷贝,时间复杂度是O(n)
- list 基于双向链表实现的,因此内存空间不连续,只能通过指针访问数据,因此list查询元素需要遍历整个链表,时间复杂度是o(n),但是链表具备高速的插入和删除的特性
- vector拥有连续的内存空间,可以很好地支持随机存取,因此,vector::iterator支持+、+=、<等操作符号;list内存空间可以是不连续的,因此不支持随机访问,因此不会支持+、+=、<等操作符号
- vector和list的iterator都实现了++运算符号
- vector适用于随机访问,list适用于插入和删除,不关心访问
C++中重载和重写的区别
- overload 重载一般用于同一个类内部,函数的名字相同,但是函数的参数列表不同(参数的类型和数量),具体的内部实现机制不一样,返回数值是可以不一样的,但是这个不是区分重载函数的标志
- override覆盖一般用于子类和父类之间,函数的名字相同,参数的列表相同,只有方法体不相同的实现方法。子类中的同名方法屏蔽了父类的方法的现象称为隐藏;子类改写父类中的virtual方法
- overwrite重写:一般用于子类和父类之间,函数的名字相同,参数的列表相同,只有方法体不相同的实现方法。子类中的同名方法屏蔽了父类的方法的现象称为隐藏;和override类似,但是父类中的方法不是虚函数;和overload类似,只是范围不同,是父类和子类之间
- C++overlode override overwrite的详细解释
C++内存管理
- C++内存分为堆、栈、全局/静态存储区域、常量存储区域和代码区
- 栈:执行的函数的时候,函数内局部变量的存储单元都可以在栈上创建,函数执行结束,这些存储单元会被释放;栈内存分配运算内置于处理器的指令集,效率很高,但是分配的内存容量有限
- 堆:使用new分配的内存区域,编译器不负责次对象的释放,由程序员自己写析构函数,一般一个new对应一个delete。如果程序员没有对其进行释放,程序结束之后,操作系统会自动回收
- 全局静态存储区域,内存在程序编译的时候就已经分配好,这个内存区域的生命周期持续到程序的终止,主要存放静态数据(局部static变量、全局static变量、全局变量和常量)
- 常量存储区域,比较特殊的存储区域,存放的是常量字符串,不允许修改
- 代码区:存放程序的二进制代码
- C++内存空间:静态存储区域、堆、栈、文字常量区、程序代码区
介绍面向对象的三大特性,并且举例说明
- 面向对象的三大特性是封装、继承和多态
- 封装:隐藏了类的实现细节和成员的数据,实现了代码的模块化,如类里面的private和public
- 继承:子类可以复用父类的成员和方法,实现了代码的重用
- 多态:“一个接口,多个实现”,通过父类调用子类的成员,实现了接口的重用,如父类的指针指向了子类的对象
多态的实现
- 多态包含编译时多态和运行时多态,编译时多态体现在函数的重载和模板上,运行时多态体现在虚函数上
- 虚函数:在基类的函数前面加上virtual关键字,在派生类中重写该函数,运行的时候根据对象的实际实际类型来调用相应的函数。如果对象的类型是派生类,就使用派生类的函数;如果对象的类型是基类,就使用基类的函数
- 面向对象的三个基本特征
C++虚函数相关(虚函数表,虚函数指针)虚函数的实现原理(重要)
- C++虚函数的实现是多态机制。他是通过虚函数表来实现的,虚函数表是每个类中存放虚函数地址的指针数组,类的实例在调用函数时会在虚函数表中寻找函数的地址进行调用,如果子类覆盖了父类的函数,则子类的虚函数表会指向子类实现的函数地址,否则指向的是父类的函数地址。一个类的所有实例都共享同一张虚函数表
- 多重继承的情况下,越是祖先的父类的虚函数更靠前,多继承的情况下,越是靠近子类名称的类的的虚函数在虚函数表中约靠前
- C++虚函数表剖析
- C++多态虚函数表详解
编译器如何处理虚函数表
- 编译器处理虚函数的方式:如果类的里边具有虚函数,就将虚函数的地址记录在类的虚函数表中。派生类在继承基类的时候,如果有重写基类的虚函数,就将虚函数表中的相应的虚函数指针设置为派生类的函数地址,否则指向基类的函数地址。
- 为每个类的实例增加一个虚表指针(vptr),虚表指针指向类的虚函数。实例在调用虚函数的时候通过虚函数表指针找到类中的虚函数表,找到对应的函数进行调用
- 虚函数的作用及其底层的实现机制
基类的析构函数一般写成虚函数的原因
- 首先析构函数可以为虚函数。当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找子类的虚构函数进行调用,从而正确释放子类对象的资源
- 如果析构函数不被声明为虚函数,则编译器实施静态绑定,在删除指向子类的父类指针的时候,只会调用父类的析构函数,就会导致子类对象析构不完全从而造成内存的泄露
构造函数为什么不定义成虚函数
- 因为创建一个对象需要确定对象的类型,而虚函数是运行的时候确定其类型的。构造一个对象时,由于对象还未创建成功,编译器不能确定对象的实际类型,是类的本身还是派生类等等
- 虚函数的调用需要虚函数表指针,而这个指针存放在对象的内存空间;如果构造函数声明为虚函数,由于对象还未创建,还没有内存空间,更没有虚函数表地址来调用构造函数了
构造函数或者析构函数调用虚函数会怎么样?
- 构造函数调用虚函数,因为当前对象还未构造完成,调用虚函数指向的是基类的函数的实现方式
- 在析构函数中使用虚函数,调用的是子类的函数的实现方式
纯虚函数
- 纯虚函数是只是声明但是没有定义实现的虚函数,是对子类的约束,是接口的继承
- 包含纯虚函数的类是一个抽象的类,不能被实例化,只有实现了纯虚函数的子类才可以生成对象
- 使用场景:当这个类本身产生实例没有意义的时候,将这个类的函数实现为纯虚函数。比如动物可以派生出老虎、兔子等,但是实例化一个动物的对象没有任何的意义。并且可以规定派生出的子类必须重写某些函数的时候可以写成纯虚函数
静态绑定和动态绑定的介绍
- C++中静态绑定和动态绑定的介绍
- 静态绑定也就是将该对象相关的属性或者函数绑定为他的静态类型,也就是他声明的类型,在编译阶段就确定。在调用的时候编译器会寻找它声明的类型进行访问
- 动态绑定是将该对象的属性或者函数绑定为它的动态类型,具体的属性或者函数是在运行期间确定的,通常通过函数实现动态绑定
- 如果删除 C中的func函数,在main函数中调用p_c->func(),考虑到派生类里面没有这个函数,就会到基类中进行调用
class B : public A{
public:
void func(){
std::cout << "B::func()" << std::endl;
}
};
class C : public A{
public:
void func(){
std::cout << "C::func()" << std::endl;
}
};
int main(){
C* p_c = new C(); //p_c 的静态类型和动态类型都是 C*
B* p_b = new B(); //p_b 的静态类型和动态类型都是 B*
A* p_a = p_c; //p_a 的静态类型是它声明的类型 A*,但是动态类型是p_a所指向的对象p_c的类型 C*
p_a = p_b; //p_a 的动态类型是可以修改的,现在他的动态类型是B*,但是其静态类型仍然是声明的时候使用的 A*
C* p_c_1 = nullptr;//p_c_1的静态类型是他声明的类型C*,没有动态类型,因为它指向了nullptr
p_a->func(); //A::func() p_a的静态类型是A*,不管指向的是哪个子类,都是直接调用的 A::func()
p_b->func(); //B::func() p_b的静态类型和动态类型都是B*,因此调用 B::func()
p_c_1->func(); //C::func() 虽然是空指针,但是他的类型在编译阶段就确定了,和空指针空不空没有任何关系
return 0;
}
- 使用virtual函数修饰class A中的函数func()
class A{
public:
virtual void func(){
std::cout << "A::func()" << std::endl;
}
};
class B : public A{
public:
void func(){
std::cout << "B::func()" << std::endl;
}
};
class C : public A{
public:
void func(){
std::cout << "C::func()" << std::endl;
}
};
int main(){
C* p_c = new C(); //p_c 的静态类型和动态类型都是 C*
B* p_b = new B(); //p_b 的静态类型和动态类型都是 B*
A* p_a = p_c; //p_a 的静态类型是它声明的类型 A*,但是动态类型是p_a所指向的对象p_c的类型 C*
p_a = p_b; //p_a 的动态类型是可以修改的,现在他的动态类型是B*,但是其静态类型仍然是声明的时候使用的 A*B
C* p_c_1 = nullptr;//p_c_1的静态类型是他声明的类型C*,没有动态类型,因为它指向了nullptr
p_a->func(); //B::func() p_a的静态类型是A*,因为有了virtual虚函数的特性,p_a的动态属性指向的是B*,因此先在B中查找,找到后直接调用的 B::func()
p_b->func(); //B::func() p_b的静态类型和动态类型都是B*,因此调用 B::func()
p_c_1->func(); //C::func() 空指针异常,因为func是virtual虚函数,因此对func的调用只能等到运行期才可以确定,然后发现是空指针
return 0;
}
- 如果基类的函数不是virtual虚函数,派生类对象对其的调用都按照其静态类型来处理,早已在编译期就确定了
- 如果是虚函数,调用需要等到运行时根据其指向的对象的类型才可以确定,虽然相较于静态绑定损失了性能,但是却可以实现多态特性
- 注意:参见Effective C++第三版 条款 37:不要重新定义一个继承而来的virtual函数的缺省参数数值,因为缺省参数值是静态绑定的(为了执行的效率),但是virtual是动态绑定的 例子
class A{
public:
virtual void func(int i = 1){
std::cout << "A::func()\t" << i << std::endl;
}
};
class B : public A{
public:
virtual void func(int i = 2){
std::cout << "B::func()\t" << i << std::endl;
}
};
int main(){
B* p_b = new B();
A* p_a = p_b;
p_b->func(); //B::func() 2 正确
p_a->func(); //B::func() 1 错误 调用子类的函数,但是却使用的是基类中的参数的默认数值
return 0;
}
深拷贝和浅拷贝的区别(需要说明深拷贝的安全性)
- 浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源
- 深拷贝是新开辟一块内存空间,将对象的资源复制到新的空间中,并且返还该空间的地址
- 深拷贝可以避免重复的释放和写冲突。如果对采用浅拷贝的对象进行释放之后,对原对象的的释放会导致内存的泄露或者程序的崩溃
?补充
- 深拷贝和浅拷贝的区别和原理
- 深拷贝和浅拷贝的区别 简单易懂版本
- 浅拷贝:obj2新建了一个对象,但是obj2对象复制的是obj1的指针,也就是堆内存的地址,不是复制对象的本身,因此obj1和obj2共用内存地址;浅拷贝只是数据对象之间的简单的赋值,比如a.size = b.size,a.data = b.data
- 如果对象中没有其他的资源(堆、文件、系统资源等)则深拷贝和浅拷贝没有任何的区别
- 深拷贝:obj3是对obj1的深拷贝,他们不共享内存;当拷贝对象中有对其他资源比如堆、文件、?系统等的引用的时候(引用可以是指针或者引用)时,对象需要另外开辟一段新的资源,而不是单纯的赋值
class A{
public:
A(int _size):size(_size){
data = new int[size]; //假设其中有一段动态分配的内存
}
A(){}
int get_val(){
return *data;
}
~A(){
delete[] data;
data = nullptr;//析构的时候释放资源
}
private:
int size;
int* data;
};
int main(){
A a(5);
A b(a);
//b=a;
std::cout << b.get_val() << std::endl;
return 0;
}
- A中的复制构造函数是由编译器生成的,所以A b(a)执行的是一个浅拷贝,浅拷贝只是对象数据之间的简单赋值?比如a.size = b.size,a.data = b.data
- 这里b的data指针和a的data指针指向的是同一块内存空间,b析构的时候会将data指向的内存地址进行释放,a析构的时候会将已经释放过的内存再次释放,这就会导致内存的泄露或者程序的崩溃
class A{
public:
A(int _size):size(_size){
data = new int[size]; //假设其中有一段动态分配的内存
}
A(){}
int get_val(){
return *data;
}
int *get_val_add(){
return data;
}
A(const A& _A):size(_A.size){
data = new int[size];//深拷贝
}
~A(){
delete[] data;
data = nullptr;//析构的时候释放资源
}
private:
int size;
int* data;
};
int main(){
A a(5);
A b(a);
//b=a;
std::cout << b.get_val() << std::endl;
return 0;
}
- 手动书写 拷贝构造函数,为其分配一段新的内存空间,不会出现内存的泄露问题?
对象复用的了解?零拷贝的了解
- 对象的复用指的是设计模式,对象可以采用不同的设计模式从而达到复用的目的,最常见的就是继承和组合模式了
- 零拷贝就是指进行操作的时候,避免cpu从一处存储拷贝到另外一处存储。在linux环境下,可以减少数据在内核空间和用户空间来回拷贝,比如使用mmap()替代read调用
- 程序调用mmap(),磁盘上的数据通过DMA拷贝到内存缓冲区,接着操作系统会把这段内存缓冲区和应用程序共享,这样就不需要将内核缓冲区的内容拷贝到用户的空间。应用程序再次调用write(),操作系统直接将内存缓冲区的内容拷贝到socket缓冲区中,这一切发生在内核态,最后,socket缓冲区再把数据发送到网卡
C++的构造函数
- 构造函数的全面解析
- 默认构造函数、重载构造函数和拷贝构造函数
- 默认构造函数是党类没有实现自己的构造函数的时候,编译器默认提供的一个构造函数
- 重载构造函数也被称为一般的构造函数,一个类可以拥有多个重载构造函数,但是需要参数的类型或者参数的个数不相同。可以在重载构造函数中定义类的初始化的方式
- 拷贝构造函数是发生在对象复制的时候调用的;使用旧的对象初始化新的对象,如果程序员自己没有写,会进行自我创建
什么情况下会调用拷贝构造函数?三种情况
- 对象使用值传递的方式传入函数的参数 void func(Dog dog){}
- 对象以值的方式从函数进行返回 Dog func(){ Dog d;return d;}
- 对象需要通过另外一个对象进行初始化Dog b; Dog a(b);
- 拷贝构造函数详解
结构体内存对齐的方式和为什么要实现内存对齐
- 因为结构体成员可以有不同的数据类型,所占的大小也是不一样的。考虑到CPU是按照数据块的方式读取数据的,采用内存对齐的方式就可以使得CPU一次将所需要的数据读取进来
- 为什么进行内存对齐,以及对齐的规则
对齐的规则
- 第一个成员在与结构体变量偏移量为0的地址
- 其他成员变量需要对齐到某个数字(对齐数)的整数倍的地址处
- 对齐数等于编译器默认的一个对齐数 与 结构体成员大小中的较小值
- linux默认使用4
- vs中默认数值为8,结构体的总大小为最大对齐数的整数倍(每个变量除了第一个成员都有一个对齐数)
内存泄漏的定义?如何检测和避免?
- 动态分配的内存空间,使用完毕之后没有进行释放,导致一直占用该内存,就是内存的泄露
造成内存的泄露的原因
- 类的构造函数和析构函数中的new和delete字段没有配套使用
- 释放对象数组的时候使用的是delete,没有使用delete[],但是这个对象数组必须是用户自己构建的对象,不是基本的数据类型,如果是基本的数据类型,使用delete和delete[]的含义是一样的
- 没有将基类的析构函数定义为虚函数,当基类指针指向的是子类对象的时候,如果基类的析构函数不是virtual,就会调用的是父类的析构函数,而不是子类的析构函数,造成子类的资源美誉被正确的释放,从而造成内存的泄露
- 没有正确的清除嵌套的对象的指针
避免的方法
- malloc/free 需要配套使用
- 使用智能指针
- 将基类的析构函数设置为虚函数
C++智能指针
- 智能指针是对指针进行了简单的 =包装,可以像普通指针一样进行使用,同时可以实现自行的释放,避免了用户使用的时候忘记释放指针指向的内存地址造成的内存泄露问题
- unique_ptr 使用unique_ptr指向的对象,不能对其进行赋值和拷贝,保证了同一个对象同一个时间只能有一个智能指针指向
- shared_ptr? 使用多个指针指向同一个对象,当这个对象的所有智能指针都被销毁的时候就会自动进行回收。内部使用计数机制进行维护
- weak_ptr 为了协助shared_ptr出现的,他不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁
调试程序的方法
- 设置断电进行调试
- 打印log进行调试
- 打印中间结果进行调试
遇到coredump如何调试
- coredump是由于异常或者bug在运行的时候异常退出或者终止,在一定的条件下生成一个叫做core的文件,这个core文件会记录程序运行时候的内存、寄存器的状态、内存指针和函数的堆栈信息等等。对这个文件进行分析可以得到程序异常的时候对应的堆栈的调用信息
- gdb调试coredump问题
- ulimit -a查看core file size大小是否为0,如果是表明即使产生coredump问题也不会生成core文件,使用ulimit -c unlimited方式设置core文件的大小没有限制后,再次执行错误程序,默认会在当前文件夹下面产生一个名字为 core的文件
- 需要注意编译代码的时候需要加上-g参数,例如 g++ coredumpTest.cpp -g -o coredumpTest
- 使用gdb进行代码的调试 gdb [可执行文件名] [core文件名]
- 在gdb函数内部使用 bt 或者 where命令就可以查看 出现错误原因时的堆栈信息
inline关键字和宏定义之间的区别是什么
- inline的含义是内联的意思,可以定义比较小的函数。考虑到函数的频繁调用需要占用很多的栈空间,进行入栈操作也需要耗费计算资源,因此可以使用inline关键字修饰频繁调用的小函数。编译器会在编译阶段将inline修饰的代码嵌入到所有调用的语句块中
区别
- 内联函数是在编译的时候展开,而宏是在预编译阶段时展开
- 编译的时候,内联函数直接嵌入到目标代码中,而宏是一个简单的简单的文本替换
- 内联函数可以进行诸如类型的安全检查、语句是否正确等编译功能,宏不具备这样的功能
- 宏不是函数,而inline是函数
- 宏在定义的时候要小心处理宏参数,一般用括号括起来,否则会出现二义性,内联函数不会出现二义性
- inline可以不展开,但是宏一定是需要展开的,因为inline对于编译器来讲只是一个建议,编译器可以选择忽略该建议,不对函数进行展开
- 宏定义在形式上类似于一个函数,但是在使用它的时候仅仅进行预处理器符号表中的简单的替换,因此他不能有效的进行参数的检测,也不能享受C++编译器严格的类型检查的好处,另外他的返回数值也不能被强制转换为合适的类型,存在一系列的隐患和局限性
模板的用法和使用的场景
- 使用template<typename T>关键字进行声明,接下来就可以进行模板函数和模板类的编写
- 编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法的检查,并不会生成具体的代码。
- 在运行时对代码进行参数替换后再进行变异,生成具体的函数的代码
成员初始化列表的概念,为什么使用成员初始化列表会快一些(性能修饰)
- 成员初始化列表就是在类或者结构体的构造函数中,在参数列表列表的后面以冒号开头,逗号分隔进行的一系列的初始化字段
class A{
public:
A(const int& input_id,std::string& input_name):id(input_id),name(input_name){};
private:
int id;
std::string name;
};
- 因为使用成员初始化列表进行初始化的话,会直接使用传入的参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程,否则会调用一次传入参数的默认的构造哈数
- 一般针对 类类型,使用内置基本的数据类型,差异不是很明显,因为减少一次调用默认构造函数的过程,直接使用拷贝构造函数,省去了调用默认构造函数的过程
- 成员初始化列表
三种情况必须使用成员初始化列表进行初始化
- 常量成员的初始化,因为常量成员只能初始化但是不可以赋值
- 引用类型
- 没有默认构造函数的对象必须使用 成员初始化列表的方式进行初始化。因为直接使用拷贝构造,跳过了 默认构造函数这一步
C++11 的新特性
- 自动类型推导auto:auto的自动类型推导用于从初始化表达式里面推导出变量的数据类型。通过auto的类型推导,大大简化编程工作
- nullptr:nullptr是为了解决先前C++中NULL的二义性问题而提出的一种新的数据类型,因为实际上NULL表示的是0,而nullptr表示的是void*类型的
- lambda表达式,类似于javascript的中的闭包,用于创建并定义匿名的函数对象,从而简化编程的工作。lambda的表达式语法如下:[函数对象参数](操作符重载函数的参数)mutable或exception声明->返回数值的类型{函数体}
- thread 和 mutex类
- 智能指针 shared_ptr? unique_ptr
- C++ 11的新特性
C++函数调用的惯例 (C++函数调用的押栈的过程)
函数调用的过程
- 从栈的空间分配存储空间
- 从实参的存储空间复制数值到形参栈的空间
- 进行计算
- 形参在函数调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈的空间,清除形参的空间;
- 数组作为参数的函数调用方式是地址传递,形参和实参都指向的是相同的内存空间,调用完成之后,形参指针被销毁,但是指向的内存空间不会被释放,也不会被销毁
- 当函数有多个返回数值的时候,不能使用普通的return的方式实现,需要通过传回地址的形式进行,即地址/指针传递。
C++的四种强制类型转换
- 四种强制的类型转换,static_cast、dynamic_cast、const_cast和reinterpret_cast
- static_cast 用于各种隐式转换,具体的讲就是各种的基本数据类型之间的转换,比如将int转换成char,float转换成int;以及将派生类(子类)的指针转换成基类的指针(父类的指针)
- 特性和要点
- 没有运行时候的类型检查,所以是具备一定的安全隐患的
- 在派生类的指针转换成基类指针时不会出现任何问题,但是将基类指针转换成派生类指针的时候会出现安全性的问题
- static_cast不能转换const? volatile等属性
- dynamic_cast 用于动态类型的转换。具体的讲,就是基类指针到派生类指针,或者派生类指针到基类指针之间的转换。dynamic_cast可以提供运行时候的类型检查,只用于含有虚函数的类。
- 如果dynamic_cast不能转换,则返还NULL
- const_cast: 用于去除const属性,使其const属性失效,可以对其数据进行修改。还可以volatile属性的转换
- reinterpret_cast 几乎什么都可以进行转换,用于任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不具备安全性
string的底层的实现
- string继承自basic_string,本质上是对char*进行的封装,封装的string包含了char*数组、容量,长度等特性。
- string可以进行动态的内存扩展,在每次扩展的时候另外申请一块先前两倍大的空间,然后进行字符串的拷贝操作,并添加新增的内容
一个函数或者可执行文件的生成过程或者编译过程是怎样的
- 预处理:对预处理命令进行替换等预处理操作
- 编译:代码优化和生成汇编代码
- 汇编:将汇编代码转换成机器代码
- 链接:将目标文件彼此链接起来
set、map和vector的插入复杂度
- set、map的插入复杂度是红黑树的插入复杂度,O(logn)
- unordered_map和unordered_set的插入时间复杂度是常数,O(N)
- vector的插入复杂度是O(N),最坏的情况就是从头部插入,需要移动其他所有的元素,如果存储的空间不足,需要进行内存的开辟和拷贝复制
定义和声明的区别
- 声明是告诉编译器变量的类型和名字,但是并不会为变量分配内存空间
- 定义就是对这个变量和函数进行内存的分配和初始化。需要分配空间,同一个变量可以被声明很多次,但是只能被定义一次
typedef和define的区别
- #definde是预处理的命令,在预处理阶段执行简单的替换,不做正确性的检查
- typedef是编译时处理的,他是在自己的作用域内给已经存在的类型起一个别名
被free回收的内存是立即返回给操作系统吗
- 不是的,被free回收的内存首先被pt malloc使用双链表保存起来,当用户进行下一次申请内存的时候,会尝试从这些内存中找到合适的内存空间进行返回。
- 这样就避免了频繁的系统调用,占用太多的系统资源。
- 同时ptmalloc也会尝试对小块内存进行合并,避免产生过多的内存碎片
- 参考链接
引用作为函数的参数以及返回数值的好处
- 在函数内部可以对此参数进行修改
- 提高函数的调用和运行的效率(没有了传值和生成副本的时间和空间的损耗)
- 如果函数的实质是形参,不过这个形参的作用域只是在函数的内部也就是形参和实参是两个不同的东西,如果想要形参代替实参肯定需要一个数值的传递。函数调用的时候,数值的传递是通过”形参=实参“来对形参进行赋值从而达到传值的目的,产生一个实参的副本。即使函数的内部对参数进行修改,针对的也是形参,也就是拷贝的副本,实参不会发生任何的改变,函数一旦执行结束,形参生命周期也被宣告终结,做出修改一样没有对任何变量产生影响
- 使用引用最大的好处是内存中不产生返回数值的副本
- 但是需要有以下方面的限制
- 1,不能返回局部变量的引用。因为函数结束之后局部变量的内存地址就会被销毁
- 2,不可以返回函数内部new分配的内存的引用,虽然不存在局部变量的被动销毁的问题,但是返回函数内部new分配的内存的引用,会出现别的问题。比如,函数返回的引用只是作为一个临时变量出现但是没有被赋予一个实际的变量,那么这个引用所指向的内存空间(new分配)就无法进行释放,造成内存的泄露
- 3,可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,对这个属性的单纯的赋值就会导致业务规则的完整性的破坏
友元函数和友元类
- 友元提供了不同类的成员函数、类的成员函数和一般函数之间的数据共享的机制
- 通过友元,一个不同的函数或者一个类的=中的成员函数可以访问类中的私有成员和保护成员
- 友元的正确使用可以提高程序的运行效率,但是友元会破坏了类的封装和数据的隐藏性,导致程序可维护性变差
- 友元函数是定义在类外的普通函数,不属于任何类,但是可以访问其他类的私有和保护成员,但是需要在类的定义中声明所有可以访问他的友元函数,就是表明谁是我的朋友
class A{
public:
friend void set_show(int x,A &a);//这个函数是友元函数的声明
private:
int data;
};
void set_show(int x,A &a){
a.data = x;
std::cout << a.data << std::endl;
}
int main(){
A a;
set_show(1,a);
return 0;
}
- 友元类
- 友元类的所有成员函数都是另外一个类的友元函数,都可以访问另外一个类中的隐藏信息(包括私有成员和保护成员)
- 但是需要在另外一个类里面进行对应的声明
class A{
public:
friend class B;//这个是友元类的声明
private:
int data;
};
class B{
public:
void set_show(int x,A &a){
a.data = x;
std::cout << a.data << std::endl;
}
};
int main(){
A a;
B b;
b.set_show(1,a);
return 0;
}
使用友元类的注意事项
- 友元关系是不能被继承的
- 友元关系是单向的,不具备交换性。如果类B是类A的友元,但是类A不一定是类B的友元,需要看在类中是否有对应的声明
- 友元关系不具备传递性,需要看类中是否有对应的声明。就像朋友关系不可以传递一样
说一下volatile关键字的作用
- volatile翻译是脆弱的意思,表明用其修饰的变量十分容易被改变,所有编译器不会对其进行优化(CPU的优化是将变量的数值存放到CPU寄存器而不是内存),进而提供稳定的访问。每次读取volatile变量的时候系统总是会从内存中读取这个变量,并将其数值立刻保存
STL中sort()算法是用什么实现的?stable_sort()呢?
- sort()使用的是快速排序和插入排序相互结合的方式实现的
- stable_sort使用的是归并排序
vector的迭代器会失效吗?什么情况下迭代器会失效
- 会失效
- vector插入的时候,如果先前分配的空间不足,会申请新的空间并将原来的元素移动到新的内存,这个时候指向先前的地址的迭代器就会失效,first和end迭代器都会失效
- vector插入的时候,end迭代器肯定失效
- vector删除的时候,被删除的元素以及删除元素之后元素迭代器会失效
为什么C++没有实现垃圾回收机制
- 实现垃圾回收机制需要带来额外的空间和时间的开销,你需要开辟一定的空间来保存指针和引用的计数、以及对他们的标价mark。然后需要开辟一个线程在空闲的时候进行free操作
- 垃圾回收机制会使得C++不适合很多底层的操作
|